Merge remote-tracking branch 'upstream/master' into feature/dt-model-io

This commit is contained in:
ackwell 2024-09-01 14:52:27 +10:00
commit c1f58b16ba
66 changed files with 1196 additions and 515 deletions

@ -1 +1 @@
Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a
Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97

@ -1 +1 @@
Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a
Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623

@ -1 +1 @@
Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee
Subproject commit 66bc00dc8517204e58c6515af5aec0ba6d196716

View file

@ -10,6 +10,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
=> textureType switch
{
TextureType.Png => textureManager.SavePng(inputFile, outputFile),
TextureType.Targa => textureManager.SaveTga(inputFile, outputFile),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile),
@ -26,6 +27,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA
=> textureType switch
{
TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),
TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width),

View file

@ -1,16 +1,21 @@
using Dalamud.Plugin.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Files.Utility;
using Penumbra.GameData.Structs;
using Penumbra.Interop.PathResolving;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Api.Api;
public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService
public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers)
: IPenumbraApiMeta, IApiService
{
public const int CurrentVersion = 0;
public const int CurrentVersion = 1;
public string GetPlayerMetaManipulations()
{
@ -24,7 +29,32 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers)
return CompressMetaManipulations(collection);
}
public Task<string> GetPlayerMetaManipulationsAsync()
{
return Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false);
return CompressMetaManipulations(playerCollection);
});
}
public Task<string> GetMetaManipulationsAsync(int gameObjectIdx)
{
return Task.Run(async () =>
{
var playerCollection = await framework.RunOnFrameworkThread(() =>
{
helpers.AssociatedCollection(gameObjectIdx, out var collection);
return collection;
}).ConfigureAwait(false);
return CompressMetaManipulations(playerCollection);
});
}
internal static string CompressMetaManipulations(ModCollection collection)
=> CompressMetaManipulationsV0(collection);
private static string CompressMetaManipulationsV0(ModCollection collection)
{
var array = new JArray();
if (collection.MetaCache is { } cache)
@ -38,6 +68,228 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers)
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
}
return Functions.ToCompressedBase64(array, CurrentVersion);
return Functions.ToCompressedBase64(array, 0);
}
private static unsafe string CompressMetaManipulationsV1(ModCollection? collection)
{
using var ms = new MemoryStream();
ms.Capacity = 1024;
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
{
zipStream.Write((byte)1);
zipStream.Write("META0001"u8);
if (collection?.MetaCache is not { } cache)
{
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
zipStream.Write(0);
}
else
{
WriteCache(zipStream, cache.Imc);
WriteCache(zipStream, cache.Eqp);
WriteCache(zipStream, cache.Eqdp);
WriteCache(zipStream, cache.Est);
WriteCache(zipStream, cache.Rsp);
WriteCache(zipStream, cache.Gmp);
cache.GlobalEqp.EnterReadLock();
try
{
zipStream.Write(cache.GlobalEqp.Count);
foreach (var (globalEqp, _) in cache.GlobalEqp)
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
}
finally
{
cache.GlobalEqp.ExitReadLock();
}
}
}
ms.Flush();
ms.Position = 0;
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
return Convert.ToBase64String(data);
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache)
where TKey : unmanaged, IMetaIdentifier
where TValue : unmanaged
{
metaCache.EnterReadLock();
try
{
stream.Write(metaCache.Count);
foreach (var (identifier, (_, value)) in metaCache)
{
stream.Write(identifier);
stream.Write(value);
}
}
finally
{
metaCache.ExitReadLock();
}
}
}
/// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set.
/// Only returns true if all conversions are successful and distinct.
/// </summary>
internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (manipString.Length == 0)
{
manips = new MetaDictionary();
return true;
}
try
{
var bytes = Convert.FromBase64String(manipString);
using var compressedStream = new MemoryStream(bytes);
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
using var resultStream = new MemoryStream();
zipStream.CopyTo(resultStream);
resultStream.Flush();
resultStream.Position = 0;
var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length);
var version = data[0];
data = data[1..];
switch (version)
{
case 0: return ConvertManipsV0(data, out manips);
case 1: return ConvertManipsV1(data, out manips);
default:
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
manips = null;
return false;
}
}
catch (Exception ex)
{
Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}");
manips = null;
return false;
}
}
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (!data.StartsWith("META0001"u8))
{
Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix.");
manips = null;
return false;
}
manips = new MetaDictionary();
var r = new SpanBinaryReader(data[8..]);
var imcCount = r.ReadInt32();
for (var i = 0; i < imcCount; ++i)
{
var identifier = r.Read<ImcIdentifier>();
var value = r.Read<ImcEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var eqpCount = r.ReadInt32();
for (var i = 0; i < eqpCount; ++i)
{
var identifier = r.Read<EqpIdentifier>();
var value = r.Read<EqpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var eqdpCount = r.ReadInt32();
for (var i = 0; i < eqdpCount; ++i)
{
var identifier = r.Read<EqdpIdentifier>();
var value = r.Read<EqdpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var estCount = r.ReadInt32();
for (var i = 0; i < estCount; ++i)
{
var identifier = r.Read<EstIdentifier>();
var value = r.Read<EstEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var rspCount = r.ReadInt32();
for (var i = 0; i < rspCount; ++i)
{
var identifier = r.Read<RspIdentifier>();
var value = r.Read<RspEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var gmpCount = r.ReadInt32();
for (var i = 0; i < gmpCount; ++i)
{
var identifier = r.Read<GmpIdentifier>();
var value = r.Read<GmpEntry>();
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
return false;
}
var globalEqpCount = r.ReadInt32();
for (var i = 0; i < globalEqpCount; ++i)
{
var manip = r.Read<GlobalEqpManipulation>();
if (!manip.Validate() || !manips.TryAdd(manip))
return false;
}
return true;
}
private static bool ConvertManipsV0(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
{
var json = Encoding.UTF8.GetString(data);
manips = JsonConvert.DeserializeObject<MetaDictionary>(json);
return manips != null;
}
internal void TestMetaManipulations()
{
var collection = collectionResolver.PlayerCollection();
var dict = new MetaDictionary(collection.MetaCache);
var count = dict.Count;
var watch = Stopwatch.StartNew();
var v0 = CompressMetaManipulationsV0(collection);
var v0Time = watch.ElapsedMilliseconds;
watch.Restart();
var v1 = CompressMetaManipulationsV1(collection);
var v1Time = watch.ElapsedMilliseconds;
watch.Restart();
var v1Success = ConvertManips(v1, out var v1Roundtrip);
var v1RoundtripTime = watch.ElapsedMilliseconds;
watch.Restart();
var v0Success = ConvertManips(v0, out var v0Roundtrip);
var v0RoundtripTime = watch.ElapsedMilliseconds;
Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal");
Penumbra.Log.Information(
$"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
Penumbra.Log.Information(
$"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}");
}
}

View file

@ -82,7 +82,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
!= Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName)))
return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args);
_modManager.AddMod(dir);
_modManager.AddMod(dir, true);
if (_config.MigrateImportedModelsToV6)
{
_migrationManager.MigrateMdlDirectory(dir.FullName, false);
@ -91,7 +91,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
if (_config.UseFileSystemCompression)
new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories),
CompressionAlgorithm.Xpress8K);
CompressionAlgorithm.Xpress8K, false);
return ApiHelpers.Return(PenumbraApiEc.Success, args);
}

View file

@ -1,10 +1,8 @@
using OtterGui;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings;
using Penumbra.String.Classes;
@ -62,7 +60,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!ConvertManips(manipString, out var m))
if (!MetaApi.ConvertManips(manipString, out var m))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch
@ -88,7 +86,7 @@ public class TemporaryApi(
if (!ConvertPaths(paths, out var p))
return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args);
if (!ConvertManips(manipString, out var m))
if (!MetaApi.ConvertManips(manipString, out var m))
return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args);
var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch
@ -153,24 +151,4 @@ public class TemporaryApi(
return true;
}
/// <summary>
/// Convert manipulations from a transmitted base64 string to actual manipulations.
/// The empty string is treated as an empty set.
/// Only returns true if all conversions are successful and distinct.
/// </summary>
private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips)
{
if (manipString.Length == 0)
{
manips = new MetaDictionary();
return true;
}
if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion)
return true;
manips = null;
return false;
}
}

View file

@ -1,3 +1,4 @@
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
@ -5,7 +6,7 @@ using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public class GlobalEqpCache : Dictionary<GlobalEqpManipulation, IMod>, IService
public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>, IService
{
private readonly HashSet<PrimaryId> _doNotHideEarrings = [];
private readonly HashSet<PrimaryId> _doNotHideNecklace = [];

View file

@ -3,7 +3,6 @@ using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
@ -16,6 +15,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
public readonly RspCache Rsp = new(manager, collection);
public readonly ImcCache Imc = new(manager, collection);
public readonly GlobalEqpCache GlobalEqp = new();
public bool IsDisposed { get; private set; }
public int Count
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count;
@ -42,6 +42,10 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
public void Dispose()
{
if (IsDisposed)
return;
IsDisposed = true;
Eqp.Dispose();
Eqdp.Dispose();
Est.Dispose();

View file

@ -1,3 +1,4 @@
using OtterGui.Classes;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
@ -5,27 +6,19 @@ using Penumbra.Mods.Editor;
namespace Penumbra.Collections.Cache;
public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager, ModCollection collection)
: Dictionary<TIdentifier, (IMod Source, TEntry Entry)>
: ReadWriteDictionary<TIdentifier, (IMod Source, TEntry Entry)>
where TIdentifier : unmanaged, IMetaIdentifier
where TEntry : unmanaged
{
protected readonly MetaFileManager Manager = manager;
protected readonly ModCollection Collection = collection;
public void Dispose()
{
Dispose(true);
}
protected readonly MetaFileManager Manager = manager;
protected readonly ModCollection Collection = collection;
public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry)
{
lock (this)
{
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
return false;
if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer<TEntry>.Default.Equals(pair.Entry, entry))
return false;
this[identifier] = (source, entry);
}
this[identifier] = (source, entry);
ApplyModInternal(identifier, entry);
return true;
@ -33,17 +26,14 @@ public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager
public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
{
lock (this)
if (!Remove(identifier, out var pair))
{
if (!Remove(identifier, out var pair))
{
mod = null;
return false;
}
mod = pair.Source;
mod = null;
return false;
}
mod = pair.Source;
RevertModInternal(identifier);
return true;
}
@ -54,7 +44,4 @@ public abstract class MetaCacheBase<TIdentifier, TEntry>(MetaFileManager manager
protected virtual void RevertModInternal(TIdentifier identifier)
{ }
protected virtual void Dispose(bool _)
{ }
}

View file

@ -46,5 +46,8 @@ public sealed class CollectionChange()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
ModSelection = 10,
}
}

View file

@ -23,5 +23,8 @@ public sealed class CollectionInheritanceChanged()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnInheritanceChange"/>
ModSelection = 10,
}
}

View file

@ -1,5 +1,4 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
@ -35,5 +34,8 @@ public sealed class ModSettingChanged()
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnSettingChange"/>
ModSelection = 10,
}
}

View file

@ -1,4 +1,6 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using Lumina.Extensions;
using OtterGui;
using Penumbra.GameData.Files;
@ -23,11 +25,11 @@ public class MeshExporter
? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints])
: scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity);
var extras = new Dictionary<string, object>(data.Attributes.Length);
var node = new JsonObject();
foreach (var attribute in data.Attributes)
extras.Add(attribute, true);
node[attribute] = true;
instance.WithExtras(JsonContent.CreateFrom(extras));
instance.WithExtras(node);
}
}
}
@ -233,10 +235,7 @@ public class MeshExporter
// Named morph targets aren't part of the specification, however `MESH.extras.targetNames`
// is a commonly-accepted means of providing the data.
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
{
{ "targetNames", shapeNames },
});
meshBuilder.Extras = new JsonObject { ["targetNames"] = JsonSerializer.SerializeToNode(shapeNames) };
string[] attributes = [];
var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask);

View file

@ -1,4 +1,6 @@
using System;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Memory;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Export;
@ -11,35 +13,40 @@ and there's reason to overhaul the export pipeline.
public struct VertexColorFfxiv : IVertexCustom
{
// NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0).
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
// NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0).
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector4 FfxivColor;
public int MaxColors => 0;
public int MaxColors
=> 0;
public int MaxTextCoords => 0;
public int MaxTextCoords
=> 0;
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
public IEnumerable<string> CustomAttributes => CustomNames;
public IEnumerable<string> CustomAttributes
=> CustomNames;
public VertexColorFfxiv(Vector4 ffxivColor)
{
FfxivColor = ffxivColor;
}
=> FfxivColor = ffxivColor;
public void Add(in VertexMaterialDelta delta)
{
}
{ }
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
=> new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
public Vector2 GetTexCoord(int index)
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetTexCoord(int setIndex, Vector2 coord)
{
}
{ }
public bool TryGetCustomAttribute(string attributeName, out object? value)
{
@ -65,12 +72,17 @@ public struct VertexColorFfxiv : IVertexCustom
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{
}
{ }
public void Validate()
{
var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W };
var components = new[]
{
FfxivColor.X,
FfxivColor.Y,
FfxivColor.Z,
FfxivColor.W,
};
if (components.Any(component => component < 0 || component > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
}
@ -78,22 +90,32 @@ public struct VertexColorFfxiv : IVertexCustom
public struct VertexTexture1ColorFfxiv : IVertexCustom
{
[VertexAttribute("TEXCOORD_0")]
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0;
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
public Vector4 FfxivColor;
public int MaxColors => 0;
public int MaxColors
=> 0;
public int MaxTextCoords => 1;
public int MaxTextCoords
=> 1;
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
public IEnumerable<string> CustomAttributes => CustomNames;
public IEnumerable<string> CustomAttributes
=> CustomNames;
public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor)
{
TexCoord0 = texCoord0;
TexCoord0 = texCoord0;
FfxivColor = ffxivColor;
}
@ -103,9 +125,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
{
return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
}
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
public Vector2 GetTexCoord(int index)
=> index switch
@ -116,8 +136,10 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0) TexCoord0 = coord;
if (setIndex >= 1) throw new ArgumentOutOfRangeException(nameof(setIndex));
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex >= 1)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
@ -144,12 +166,17 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{
}
{ }
public void Validate()
{
var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W };
var components = new[]
{
FfxivColor.X,
FfxivColor.Y,
FfxivColor.Z,
FfxivColor.W,
};
if (components.Any(component => component < 0 || component > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
}
@ -157,26 +184,35 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom
public struct VertexTexture2ColorFfxiv : IVertexCustom
{
[VertexAttribute("TEXCOORD_0")]
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
{
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR",
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
}
public Vector2 TexCoord0;
[VertexAttribute("TEXCOORD_1")]
public Vector2 TexCoord1;
[VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)]
public Vector4 FfxivColor;
public int MaxColors => 0;
public int MaxColors
=> 0;
public int MaxTextCoords => 2;
public int MaxTextCoords
=> 2;
private static readonly string[] CustomNames = ["_FFXIV_COLOR"];
public IEnumerable<string> CustomAttributes => CustomNames;
public IEnumerable<string> CustomAttributes
=> CustomNames;
public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor)
{
TexCoord0 = texCoord0;
TexCoord1 = texCoord1;
TexCoord0 = texCoord0;
TexCoord1 = texCoord1;
FfxivColor = ffxivColor;
}
@ -187,9 +223,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
}
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
{
return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
}
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
public Vector2 GetTexCoord(int index)
=> index switch
@ -201,9 +235,12 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
public void SetTexCoord(int setIndex, Vector2 coord)
{
if (setIndex == 0) TexCoord0 = coord;
if (setIndex == 1) TexCoord1 = coord;
if (setIndex >= 2) throw new ArgumentOutOfRangeException(nameof(setIndex));
if (setIndex == 0)
TexCoord0 = coord;
if (setIndex == 1)
TexCoord1 = coord;
if (setIndex >= 2)
throw new ArgumentOutOfRangeException(nameof(setIndex));
}
public bool TryGetCustomAttribute(string attributeName, out object? value)
@ -230,12 +267,17 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom
=> throw new ArgumentOutOfRangeException(nameof(index));
public void SetColor(int setIndex, Vector4 color)
{
}
{ }
public void Validate()
{
var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W };
var components = new[]
{
FfxivColor.X,
FfxivColor.Y,
FfxivColor.Z,
FfxivColor.W,
};
if (components.Any(component => component < 0 || component > 1))
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
}

View file

@ -61,7 +61,7 @@ public class SubMeshImporter
try
{
_morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
_morphNames = node.Mesh.Extras["targetNames"].Deserialize<List<string>>();
}
catch
{

View file

@ -148,7 +148,7 @@ public partial class TexToolsImporter : IDisposable
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
private static ZipArchiveEntry? FindZipEntry(ZipArchive file, string fileName)
=> file.Entries.FirstOrDefault(e => !e.IsDirectory && e.Key.Contains(fileName));
=> file.Entries.FirstOrDefault(e => e is { IsDirectory: false, Key: not null } && e.Key.Contains(fileName));
private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding)
{

View file

@ -82,7 +82,7 @@ public partial class TexToolsImporter
if (name.Length == 0)
throw new Exception("Invalid mod archive: mod meta has no name.");
using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key));
using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key!));
s.Seek(0, SeekOrigin.Begin);
s.WriteTo(f);
}
@ -155,13 +155,9 @@ public partial class TexToolsImporter
ret = directory;
// Check that all other files are also contained in the top-level directory.
if (ret.IndexOfAny(new[]
{
'/',
'\\',
})
>= 0
|| !archive.Entries.All(e => e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\')))
if (ret.IndexOfAny(['/', '\\']) >= 0
|| !archive.Entries.All(e
=> e.Key != null && e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\')))
throw new Exception(
"Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too.");
}

View file

@ -55,6 +55,14 @@ public partial class CombinedTexture : IDisposable
SaveTask = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height);
}
public void SaveAsTarga(TextureManager textures, string path)
{
if (!IsLoaded || _current == null)
return;
SaveTask = textures.SaveTga(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height);
}
private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex)
{
if (!IsLoaded || _current == null)
@ -72,6 +80,7 @@ public partial class CombinedTexture : IDisposable
".tex" => TextureType.Tex,
".dds" => TextureType.Dds,
".png" => TextureType.Png,
".tga" => TextureType.Targa,
_ => TextureType.Unknown,
};
@ -85,6 +94,9 @@ public partial class CombinedTexture : IDisposable
break;
case TextureType.Png:
SaveAsPng(textures, path);
break;
case TextureType.Targa:
SaveAsTarga(textures, path);
break;
default:
throw new ArgumentException(

View file

@ -177,7 +177,9 @@ public static class TexFileParser
DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1,
DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2,
DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3,
DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina
DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5,
DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina
DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7,
DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16,
DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8,
@ -202,7 +204,9 @@ public static class TexFileParser
TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm,
TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm,
TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm,
(TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina
TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm,
(TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, // TODO: upstream to Lumina
TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm,
TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless,
TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless,

View file

@ -10,6 +10,21 @@ public enum TextureType
Tex,
Png,
Bitmap,
Targa,
}
internal static class TextureTypeExtensions
{
public static TextureType ReduceToBehaviour(this TextureType type)
=> type switch
{
TextureType.Dds => TextureType.Dds,
TextureType.Tex => TextureType.Tex,
TextureType.Png => TextureType.Png,
TextureType.Bitmap => TextureType.Png,
TextureType.Targa => TextureType.Png,
_ => TextureType.Unknown,
};
}
public sealed class Texture : IDisposable

View file

@ -66,7 +66,7 @@ public static class TextureDrawer
current.Load(textures, paths[0]);
}
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false);
fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false);
}
ImGui.SameLine();

View file

@ -8,6 +8,7 @@ using OtterGui.Tasks;
using OtterTex;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
@ -33,10 +34,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
}
public Task SavePng(string input, string output)
=> Enqueue(new SavePngAction(this, input, output));
=> Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Png));
public Task SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
=> Enqueue(new SavePngAction(this, image, path, rgba, width, height));
=> Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Png, rgba, width, height));
public Task SaveTga(string input, string output)
=> Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Targa));
public Task SaveTga(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
=> Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Targa, rgba, width, height));
public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output)
=> Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output));
@ -66,44 +74,65 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
return t;
}
private class SavePngAction : IAction
private class SaveImageSharpAction : IAction
{
private readonly TextureManager _textures;
private readonly string _outputPath;
private readonly ImageInputData _input;
private readonly TextureType _type;
public SavePngAction(TextureManager textures, string input, string output)
public SaveImageSharpAction(TextureManager textures, string input, string output, TextureType type)
{
_textures = textures;
_input = new ImageInputData(input);
_outputPath = output;
_type = type;
if (_type.ReduceToBehaviour() is not TextureType.Png)
throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp.");
}
public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0)
public SaveImageSharpAction(TextureManager textures, BaseImage image, string path, TextureType type, byte[]? rgba = null, int width = 0,
int height = 0)
{
_textures = textures;
_input = new ImageInputData(image, rgba, width, height);
_outputPath = path;
_type = type;
if (_type.ReduceToBehaviour() is not TextureType.Png)
throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp.");
}
public void Execute(CancellationToken cancel)
{
_textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}...");
_textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as {_type} to {_outputPath}...");
var (image, rgba, width, height) = _input.GetData(_textures);
cancel.ThrowIfCancellationRequested();
Image<Rgba32>? png = null;
Image<Rgba32>? data = null;
if (image.Type is TextureType.Unknown)
{
if (rgba != null && width > 0 && height > 0)
png = ConvertToPng(rgba, width, height).AsPng!;
data = ConvertToPng(rgba, width, height).AsPng!;
}
else
{
png = ConvertToPng(image, cancel, rgba).AsPng!;
data = ConvertToPng(image, cancel, rgba).AsPng!;
}
cancel.ThrowIfCancellationRequested();
png?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel).Wait(cancel);
switch (_type)
{
case TextureType.Png:
data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel)
.Wait(cancel);
return;
case TextureType.Targa:
data?.SaveAsync(_outputPath, new TgaEncoder()
{
Compression = TgaCompression.None,
BitsPerPixel = TgaBitsPerPixel.Pixel32,
}, cancel).Wait(cancel);
return;
}
}
public override string ToString()
@ -111,7 +140,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
public bool Equals(IAction? other)
{
if (other is not SavePngAction rhs)
if (other is not SaveImageSharpAction rhs)
return false;
return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input);
@ -165,11 +194,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
return;
}
var imageTypeBehaviour = image.Type.ReduceToBehaviour();
var dds = _type switch
{
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba,
width, height),
CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel,
rgba, width, height),
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height),
@ -218,7 +248,9 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
=> Path.GetExtension(path).ToLowerInvariant() switch
{
".dds" => (LoadDds(path), TextureType.Dds),
".png" => (LoadPng(path), TextureType.Png),
".png" => (LoadImageSharp(path), TextureType.Png),
".tga" => (LoadImageSharp(path), TextureType.Targa),
".bmp" => (LoadImageSharp(path), TextureType.Bitmap),
".tex" => (LoadTex(path), TextureType.Tex),
_ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."),
};
@ -234,17 +266,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
public BaseImage LoadDds(string path)
=> ScratchImage.LoadDDS(path);
/// <summary> Load a .png file from drive using ImageSharp. </summary>
public BaseImage LoadPng(string path)
/// <summary> Load a supported file type from drive using ImageSharp. </summary>
public BaseImage LoadImageSharp(string path)
{
using var stream = File.OpenRead(path);
return Image.Load<Rgba32>(stream);
}
/// <summary> Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. </summary>
/// <summary> Convert an existing image to ImageSharp. Does not create a deep copy of an existing ImageSharp file and just returns the existing one. </summary>
public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0)
{
switch (input.Type)
switch (input.Type.ReduceToBehaviour())
{
case TextureType.Png: return input;
case TextureType.Dds:
@ -261,7 +293,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0,
int height = 0)
{
switch (input.Type)
switch (input.Type.ReduceToBehaviour())
{
case TextureType.Png:
{
@ -291,7 +323,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null,
int width = 0, int height = 0)
{
switch (input.Type)
switch (input.Type.ReduceToBehaviour())
{
case TextureType.Png:
{
@ -470,6 +502,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image",
TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image",
TextureType.Png => $"Custom {_width} x {_height} .png Image",
TextureType.Targa => $"Custom {_width} x {_height} .tga Image",
TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image",
_ => "Unknown Image",
};

View file

@ -69,6 +69,7 @@ public class MetaDictionary
public void Clear()
{
Count = 0;
_imc.Clear();
_eqp.Clear();
_eqdp.Clear();

View file

@ -225,7 +225,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod))
{
mod = new Mod(modDirectory);
modManager.Creator.ReloadMod(mod, true, out _);
modManager.Creator.ReloadMod(mod, true, true, out _);
}
Clear();

View file

@ -25,6 +25,21 @@ public class ModEditor(
public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor;
public readonly FileCompactor Compactor = compactor;
public bool IsLoading
{
get
{
lock (_lock)
{
return _loadingMod is { IsCompleted: false };
}
}
}
private readonly object _lock = new();
private Task? _loadingMod;
public Mod? Mod { get; private set; }
public int GroupIdx { get; private set; }
public int DataIdx { get; private set; }
@ -32,28 +47,42 @@ public class ModEditor(
public IModGroup? Group { get; private set; }
public IModDataContainer? Option { get; private set; }
public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0);
public void LoadMod(Mod mod, int groupIdx, int dataIdx)
public async Task LoadMod(Mod mod, int groupIdx, int dataIdx)
{
Mod = mod;
LoadOption(groupIdx, dataIdx, true);
Files.UpdateAll(mod, Option!);
SwapEditor.Revert(Option!);
MetaEditor.Load(Mod!, Option!);
Duplicates.Clear();
MdlMaterialEditor.ScanModels(Mod!);
await AppendTask(() =>
{
Mod = mod;
LoadOption(groupIdx, dataIdx, true);
Files.UpdateAll(mod, Option!);
SwapEditor.Revert(Option!);
MetaEditor.Load(Mod!, Option!);
Duplicates.Clear();
MdlMaterialEditor.ScanModels(Mod!);
});
}
public void LoadOption(int groupIdx, int dataIdx)
private Task AppendTask(Action run)
{
LoadOption(groupIdx, dataIdx, true);
SwapEditor.Revert(Option!);
Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Mod!, Option!);
FileEditor.Clear();
Duplicates.Clear();
lock (_lock)
{
if (_loadingMod == null || _loadingMod.IsCompleted)
return _loadingMod = Task.Run(run);
return _loadingMod = _loadingMod.ContinueWith(_ => run());
}
}
public async Task LoadOption(int groupIdx, int dataIdx)
{
await AppendTask(() =>
{
LoadOption(groupIdx, dataIdx, true);
SwapEditor.Revert(Option!);
Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Mod!, Option!);
FileEditor.Clear();
Duplicates.Clear();
});
}
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>

View file

@ -151,7 +151,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
if (deletions <= 0)
return;
modManager.Creator.ReloadMod(mod, false, out _);
modManager.Creator.ReloadMod(mod, false, false, out _);
files.UpdateAll(mod, option);
}

View file

@ -10,22 +10,21 @@ using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.UI.ModsTab;
namespace Penumbra.Mods.Editor;
public class ModMerger : IDisposable, IService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ModGroupEditor _editor;
private readonly ModFileSystemSelector _selector;
private readonly DuplicateManager _duplicates;
private readonly ModManager _mods;
private readonly ModCreator _creator;
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ModGroupEditor _editor;
private readonly ModSelection _selection;
private readonly DuplicateManager _duplicates;
private readonly ModManager _mods;
private readonly ModCreator _creator;
public Mod? MergeFromMod
=> _selector.Selected;
=> _selection.Mod;
public Mod? MergeToMod;
public string OptionGroupName = "Merges";
@ -41,23 +40,23 @@ public class ModMerger : IDisposable, IService
public readonly IReadOnlyList<string> Warnings = new List<string>();
public Exception? Error { get; private set; }
public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates,
public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates,
CommunicatorService communicator, ModCreator creator, Configuration config)
{
_editor = editor;
_selector = selector;
_duplicates = duplicates;
_communicator = communicator;
_creator = creator;
_config = config;
_mods = mods;
_selector.SelectionChanged += OnSelectionChange;
_editor = editor;
_selection = selection;
_duplicates = duplicates;
_communicator = communicator;
_creator = creator;
_config = config;
_mods = mods;
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
}
public void Dispose()
{
_selector.SelectionChanged -= OnSelectionChange;
_selection.Unsubscribe(OnSelectionChange);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
}
@ -257,7 +256,7 @@ public class ModMerger : IDisposable, IService
if (dir == null)
throw new Exception($"Could not split off mods, unable to create new mod with name {modName}.");
_mods.AddMod(dir);
_mods.AddMod(dir, false);
result = _mods[^1];
if (mods.Count == 1)
{
@ -390,7 +389,7 @@ public class ModMerger : IDisposable, IService
}
}
private void OnSelectionChange(Mod? oldSelection, Mod? newSelection, in ModFileSystemSelector.ModState state)
private void OnSelectionChange(Mod? oldSelection, Mod? newSelection)
{
if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text)
OptionName = newSelection?.Name.Text ?? string.Empty;

View file

@ -1,12 +1,17 @@
using System.Collections.Frozen;
using OtterGui.Services;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.SubMods;
namespace Penumbra.Mods.Editor;
public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
public class ModMetaEditor(
ModGroupEditor groupEditor,
MetaFileManager metaFileManager,
ImcChecker imcChecker) : MetaDictionary, IService
{
public sealed class OtherOptionData : HashSet<string>
{
@ -62,12 +67,111 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService
Changes = false;
}
public static bool DeleteDefaultValues(MetaFileManager metaFileManager, ImcChecker imcChecker, MetaDictionary dict)
{
var clone = dict.Clone();
dict.Clear();
var count = 0;
foreach (var (key, value) in clone.Imc)
{
var defaultEntry = imcChecker.GetDefaultEntry(key, false);
if (!defaultEntry.Entry.Equals(value))
{
dict.TryAdd(key, value);
}
else
{
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
++count;
}
}
foreach (var (key, value) in clone.Eqp)
{
var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot);
if (!defaultEntry.Equals(value))
{
dict.TryAdd(key, value);
}
else
{
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
++count;
}
}
foreach (var (key, value) in clone.Eqdp)
{
var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot);
if (!defaultEntry.Equals(value))
{
dict.TryAdd(key, value);
}
else
{
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
++count;
}
}
foreach (var (key, value) in clone.Est)
{
var defaultEntry = EstFile.GetDefault(metaFileManager, key);
if (!defaultEntry.Equals(value))
{
dict.TryAdd(key, value);
}
else
{
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
++count;
}
}
foreach (var (key, value) in clone.Gmp)
{
var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key);
if (!defaultEntry.Equals(value))
{
dict.TryAdd(key, value);
}
else
{
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
++count;
}
}
foreach (var (key, value) in clone.Rsp)
{
var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute);
if (!defaultEntry.Equals(value))
{
dict.TryAdd(key, value);
}
else
{
Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}.");
++count;
}
}
if (count == 0)
return false;
Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option.");
return true;
}
public void DeleteDefaultValues()
=> Changes = DeleteDefaultValues(metaFileManager, imcChecker, this);
public void Apply(IModDataContainer container)
{
if (!Changes)
return;
modManager.OptionEditor.SetManipulations(container, this);
groupEditor.SetManipulations(container, this);
Changes = false;
}
}

View file

@ -46,7 +46,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
if (!config.AutoReduplicateUiOnImport)
return;
if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod)
if (modManager.Creator.LoadMod(modDirectory, false, false) is not { } mod)
return;
Dictionary<FullPath, List<(IModDataContainer, Utf8GamePath)>> paths = [];

View file

@ -79,7 +79,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
return false;
}
modManager.AddMod(directory);
modManager.AddMod(directory, true);
mod = modManager.LastOrDefault();
return mod != null && mod.ModPath == directory;
}

View file

@ -81,13 +81,13 @@ public sealed class ModManager : ModStorage, IDisposable, IService
}
/// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod(DirectoryInfo modFolder)
public void AddMod(DirectoryInfo modFolder, bool deleteDefaultMeta)
{
if (this.Any(m => m.ModPath.Name == modFolder.Name))
return;
Creator.SplitMultiGroups(modFolder);
var mod = Creator.LoadMod(modFolder, true);
var mod = Creator.LoadMod(modFolder, true, deleteDefaultMeta);
if (mod == null)
return;
@ -141,7 +141,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
var oldName = mod.Name;
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!Creator.ReloadMod(mod, true, out var metaChange))
if (!Creator.ReloadMod(mod, true, false, out var metaChange))
{
Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead."
@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
dir.Refresh();
mod.ModPath = dir;
if (!Creator.ReloadMod(mod, false, out var metaChange))
if (!Creator.ReloadMod(mod, false, false, out var metaChange))
{
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
@ -332,7 +332,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService
var queue = new ConcurrentQueue<Mod>();
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
{
var mod = Creator.LoadMod(dir, false);
var mod = Creator.LoadMod(dir, false, false);
if (mod != null)
queue.Enqueue(mod);
});

View file

@ -82,7 +82,7 @@ public static partial class ModMigration
foreach (var (gamePath, swapPath) in swaps)
mod.Default.FileSwaps.Add(gamePath, swapPath);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true, true);
foreach (var group in mod.Groups)
saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport));
@ -182,7 +182,7 @@ public static partial class ModMigration
Description = option.OptionDesc,
};
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true);
return subMod;
}
@ -196,7 +196,7 @@ public static partial class ModMigration
Priority = priority,
};
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true);
return subMod;
}

View file

@ -2,7 +2,6 @@ using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;

View file

@ -39,7 +39,7 @@ public class ModGroupEditor(
ImcModGroupEditor imcEditor,
CommunicatorService communicator,
SaveService saveService,
Configuration Config) : IService
Configuration config) : IService
{
public SingleModGroupEditor SingleEditor
=> singleEditor;
@ -57,7 +57,7 @@ public class ModGroupEditor(
return;
group.DefaultSettings = defaultOption;
saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1);
}
@ -68,9 +68,9 @@ public class ModGroupEditor(
if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true))
return;
saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
saveService.ImmediateDelete(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
group.Name = newName;
saveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
saveService.ImmediateSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1);
}
@ -81,7 +81,7 @@ public class ModGroupEditor(
var idx = group.GetIndex();
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1);
mod.Groups.RemoveAt(idx);
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx);
}
@ -93,7 +93,7 @@ public class ModGroupEditor(
if (!mod.Groups.Move(idxFrom, groupIdxTo))
return;
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom);
}
@ -104,7 +104,7 @@ public class ModGroupEditor(
return;
group.Priority = newPriority;
saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1);
}
@ -115,7 +115,7 @@ public class ModGroupEditor(
return;
group.Description = newDescription;
saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1);
}
@ -126,7 +126,7 @@ public class ModGroupEditor(
return;
option.Name = newName;
saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
}
@ -137,7 +137,7 @@ public class ModGroupEditor(
return;
option.Description = newDescription;
saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
}
@ -149,7 +149,7 @@ public class ModGroupEditor(
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
subMod.Manipulations.SetTo(manipulations);
saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
@ -161,13 +161,13 @@ public class ModGroupEditor(
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
subMod.Files.SetTo(replacements);
saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
/// <summary> Forces a file save of the given container's group. </summary>
public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue)
=> saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
=> saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
/// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> additions)
@ -176,7 +176,7 @@ public class ModGroupEditor(
subMod.Files.AddFrom(additions);
if (oldCount != subMod.Files.Count)
{
saveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
saveService.QueueSave(new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
}
@ -189,7 +189,7 @@ public class ModGroupEditor(
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
subMod.FileSwaps.SetTo(swaps);
saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}

View file

@ -10,6 +10,7 @@ using Penumbra.GameData.Data;
using Penumbra.Import;
using Penumbra.Import.Structs;
using Penumbra.Meta;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
@ -20,11 +21,12 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods;
public partial class ModCreator(
SaveService _saveService,
SaveService saveService,
Configuration config,
ModDataEditor _dataEditor,
MetaFileManager _metaFileManager,
GamePathParser _gamePathParser) : IService
ModDataEditor dataEditor,
MetaFileManager metaFileManager,
GamePathParser gamePathParser,
ImcChecker imcChecker) : IService
{
public readonly Configuration Config = config;
@ -34,7 +36,7 @@ public partial class ModCreator(
try
{
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
_dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty);
dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty);
CreateDefaultFiles(newDir);
return newDir;
}
@ -46,7 +48,7 @@ public partial class ModCreator(
}
/// <summary> Load a mod by its directory. </summary>
public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges)
public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges, bool deleteDefaultMetaChanges)
{
modPath.Refresh();
if (!modPath.Exists)
@ -56,7 +58,7 @@ public partial class ModCreator(
}
var mod = new Mod(modPath);
if (ReloadMod(mod, incorporateMetaChanges, out _))
if (ReloadMod(mod, incorporateMetaChanges, deleteDefaultMetaChanges, out _))
return mod;
// Can not be base path not existing because that is checked before.
@ -65,21 +67,29 @@ public partial class ModCreator(
}
/// <summary> Reload a mod from its mod path. </summary>
public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange)
public bool ReloadMod(Mod mod, bool incorporateMetaChanges, bool deleteDefaultMetaChanges, out ModDataChangeType modDataChange)
{
modDataChange = ModDataChangeType.Deletion;
if (!Directory.Exists(mod.ModPath.FullName))
return false;
modDataChange = _dataEditor.LoadMeta(this, mod);
modDataChange = dataEditor.LoadMeta(this, mod);
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
return false;
_dataEditor.LoadLocalData(mod);
dataEditor.LoadLocalData(mod);
LoadDefaultOption(mod);
LoadAllGroups(mod);
if (incorporateMetaChanges)
IncorporateAllMetaChanges(mod, true);
if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges)
{
foreach (var container in mod.AllDataContainers)
{
if (ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, container.Manipulations))
saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport));
}
}
return true;
}
@ -89,13 +99,13 @@ public partial class ModCreator(
{
mod.Groups.Clear();
var changes = false;
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod))
foreach (var file in saveService.FileNames.GetOptionGroupFiles(mod))
{
var group = LoadModGroup(mod, file);
if (group != null && mod.Groups.All(g => g.Name != group.Name))
{
changes = changes
|| _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true)
|| saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true)
!= Path.Combine(file.DirectoryName!, ReplaceBadXivSymbols(file.Name, true));
mod.Groups.Add(group);
}
@ -106,13 +116,13 @@ public partial class ModCreator(
}
if (changes)
_saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport);
saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport);
}
/// <summary> Load the default option for a given mod.</summary>
public void LoadDefaultOption(Mod mod)
{
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
var defaultFile = saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
try
{
var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject();
@ -157,7 +167,7 @@ public partial class ModCreator(
List<string> deleteList = new();
foreach (var subMod in mod.AllDataContainers)
{
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false);
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false, true);
changes |= localChanges;
if (delete)
deleteList.AddRange(localDeleteList);
@ -168,8 +178,8 @@ public partial class ModCreator(
if (!changes)
return;
_saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
}
@ -177,7 +187,7 @@ public partial class ModCreator(
/// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
/// If delete is true, the files are deleted afterwards.
/// </summary>
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete)
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault)
{
var deleteList = new List<string>();
var oldSize = option.Manipulations.Count;
@ -194,7 +204,7 @@ public partial class ModCreator(
if (!file.Exists)
continue;
var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName),
var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName),
Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
@ -207,7 +217,7 @@ public partial class ModCreator(
if (!file.Exists)
continue;
var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
@ -223,7 +233,11 @@ public partial class ModCreator(
}
DeleteDeleteList(deleteList, delete);
return (oldSize < option.Manipulations.Count, deleteList);
var changes = oldSize < option.Manipulations.Count;
if (deleteDefault && !Config.KeepDefaultMetaChanges)
changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, option.Manipulations);
return (changes, deleteList);
}
/// <summary>
@ -250,7 +264,7 @@ public partial class ModCreator(
group.Priority = priority;
group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods.Select(s => s.Clone(group)));
_saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break;
}
case GroupType.Single:
@ -260,7 +274,7 @@ public partial class ModCreator(
group.Priority = priority;
group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group)));
_saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break;
}
}
@ -277,7 +291,8 @@ public partial class ModCreator(
foreach (var (_, gamePath, file) in list)
mod.Files.TryAdd(gamePath, file);
IncorporateMetaChanges(mod, baseFolder, true);
IncorporateMetaChanges(mod, baseFolder, true, true);
return mod;
}
@ -288,15 +303,15 @@ public partial class ModCreator(
internal void CreateDefaultFiles(DirectoryInfo directory)
{
var mod = new Mod(directory);
ReloadMod(mod, false, out _);
ReloadMod(mod, false, false, out _);
foreach (var file in mod.FindUnusedFiles())
{
if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath))
mod.Default.Files.TryAdd(gamePath, file);
}
IncorporateMetaChanges(mod.Default, directory, true);
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
IncorporateMetaChanges(mod.Default, directory, true, true);
saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
}
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
@ -333,7 +348,7 @@ public partial class ModCreator(
{
var mod = new Mod(baseDir);
var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList();
var files = saveService.FileNames.GetOptionGroupFiles(mod).ToList();
var idx = 0;
var reorder = false;
foreach (var groupFile in files)

View file

@ -0,0 +1,104 @@
using OtterGui.Classes;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Mods;
/// <summary>
/// Triggered whenever the selected mod changes
/// <list type="number">
/// <item>Parameter is the old selected mod. </item>
/// <item>Parameter is the new selected mod </item>
/// </list>
/// </summary>
public class ModSelection : EventWrapper<Mod?, Mod?, ModSelection.Priority>
{
private readonly ActiveCollections _collections;
private readonly EphemeralConfig _config;
private readonly CommunicatorService _communicator;
public ModSelection(CommunicatorService communicator, ModManager mods, ActiveCollections collections, EphemeralConfig config)
: base(nameof(ModSelection))
{
_communicator = communicator;
_collections = collections;
_config = config;
if (_config.LastModPath.Length > 0)
SelectMod(mods.FirstOrDefault(m => string.Equals(m.Identifier, config.LastModPath, StringComparison.OrdinalIgnoreCase)));
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection);
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection);
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection);
}
public ModSettings Settings { get; private set; } = ModSettings.Empty;
public ModCollection Collection { get; private set; } = ModCollection.Empty;
public Mod? Mod { get; private set; }
public void SelectMod(Mod? mod)
{
if (mod == Mod)
return;
var oldMod = Mod;
Mod = mod;
OnCollectionChange(CollectionType.Current, null, _collections.Current, string.Empty);
Invoke(oldMod, Mod);
_config.LastModPath = mod?.ModPath.Name ?? string.Empty;
_config.Save();
}
protected override void Dispose(bool _)
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnInheritanceChange);
_communicator.ModSettingChanged.Unsubscribe(OnSettingChange);
}
private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _2)
{
if (type is CollectionType.Current && oldCollection != newCollection)
UpdateSettings();
}
private void OnSettingChange(ModCollection collection, ModSettingChange _1, Mod? mod, Setting _2, int _3, bool _4)
{
if (collection == _collections.Current && mod == Mod)
UpdateSettings();
}
private void OnInheritanceChange(ModCollection collection, bool arg2)
{
if (collection == _collections.Current)
UpdateSettings();
}
private void UpdateSettings()
{
if (Mod == null)
{
Settings = ModSettings.Empty;
Collection = ModCollection.Empty;
}
else
{
(var settings, Collection) = _collections.Current[Mod.Index];
Settings = settings ?? ModSettings.Empty;
}
}
public enum Priority
{
/// <seealso cref="UI.ModsTab.ModPanel.OnSelectionChange"/>
ModPanel = 0,
/// <seealso cref="Editor.ModMerger.OnSelectionChange"/>
ModMerger = 0,
}
}

View file

@ -97,7 +97,7 @@ public class TemporaryMod : IMod
defaultMod.Manipulations.UnionWith(manips);
saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
modManager.AddMod(dir);
modManager.AddMod(dir, false);
Penumbra.Log.Information(
$"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}.");
}

View file

@ -114,7 +114,7 @@ public class Penumbra : IDalamudPlugin
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>()!;
_communicatorService.ChangedItemHover.Subscribe(it =>
{
if (it is IdentifiedItem)
if (it is IdentifiedItem { Item.Id.IsItem: true })
ImGui.TextUnformatted("Left Click to create an item link in chat.");
}, ChangedItemHover.Priority.Link);

View file

@ -86,11 +86,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" />
<!-- This reference is only there to silence a vulnerability warning caused by transitive inclusion of a lower version through PeNet and System.Security.Cryptography.Pkcs. -->
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
<PackageReference Include="EmbedIO" Version="3.5.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0030" />
<PackageReference Include="SharpCompress" Version="0.37.2" />
<PackageReference Include="SharpGLTF.Core" Version="1.0.1" />
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.1" />
<PackageReference Include="PeNet" Version="4.0.5" />
</ItemGroup>

View file

@ -242,7 +242,7 @@ public class MigrationManager(Configuration config) : IService
return;
}
var path = Path.Combine(directory, reader.Entry.Key);
var path = Path.Combine(directory, reader.Entry.Key!);
using var s = new MemoryStream();
using var e = reader.OpenEntryStream();
e.CopyTo(s);

View file

@ -281,7 +281,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
if (newDir == null)
return;
_modManager.AddMod(newDir);
_modManager.AddMod(newDir, false);
var mod = _modManager[^1];
if (!_swapData.WriteMod(_modManager, mod, mod.Default,
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))

View file

@ -61,7 +61,13 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil
}
protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate()
=> Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId.Id)
.ThenBy(kvp => kvp.Key.GenderRace)
.ThenBy(kvp => kvp.Key.Slot)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
=> Editor.Eqdp.Count;
private static bool DrawIdentifierInput(ref EqdpIdentifier identifier)
{

View file

@ -59,7 +59,13 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
}
protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate()
=> Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Eqp
.OrderBy(kvp => kvp.Key.SetId.Id)
.ThenBy(kvp => kvp.Key.Slot)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
=> Editor.Eqp.Count;
private static bool DrawIdentifierInput(ref EqpIdentifier identifier)
{

View file

@ -58,7 +58,14 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
}
protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate()
=> Editor.Est.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Est
.OrderBy(kvp => kvp.Key.SetId.Id)
.ThenBy(kvp => kvp.Key.GenderRace)
.ThenBy(kvp => kvp.Key.Slot)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
=> Editor.Est.Count;
private static bool DrawIdentifierInput(ref EstIdentifier identifier)
{

View file

@ -47,7 +47,13 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me
}
protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate()
=> Editor.GlobalEqp.Select(identifier => (identifier, (byte)0));
=> Editor.GlobalEqp
.OrderBy(identifier => identifier.Type)
.ThenBy(identifier => identifier.Condition.Id)
.Select(identifier => (identifier, (byte)0));
protected override int Count
=> Editor.GlobalEqp.Count;
private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier)
{

View file

@ -57,7 +57,12 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
}
protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate()
=> Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Gmp
.OrderBy(kvp => kvp.Key.SetId.Id)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
=> Editor.Gmp.Count;
private static bool DrawIdentifierInput(ref GmpIdentifier identifier)
{

View file

@ -140,7 +140,17 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate()
=> Editor.Imc.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Imc
.OrderBy(kvp => kvp.Key.ObjectType)
.ThenBy(kvp => kvp.Key.PrimaryId.Id)
.ThenBy(kvp => kvp.Key.EquipSlot)
.ThenBy(kvp => kvp.Key.BodySlot)
.ThenBy(kvp => kvp.Key.SecondaryId.Id)
.ThenBy(kvp => kvp.Key.Variant.Id)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
=> Editor.Imc.Count;
public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110)
{
@ -149,18 +159,18 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
if (ret)
{
var equipSlot = type switch
var (equipSlot, secondaryId) = type switch
{
ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head,
ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head,
ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears,
_ => EquipSlot.Unknown,
ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0),
ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0),
_ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId),
};
identifier = identifier with
{
ObjectType = type,
EquipSlot = equipSlot,
SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId,
SecondaryId = secondaryId,
};
}

View file

@ -41,12 +41,14 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
using var id = ImUtf8.PushId((int)Identifier.Type);
DrawNew();
foreach (var ((identifier, entry), idx) in Enumerate().WithIndex())
{
id.Push(idx);
DrawEntry(identifier, entry);
id.Pop();
}
var height = ImUtf8.FrameHeightSpacing;
var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY());
var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count);
ImGuiClip.DrawEndDummy(remainder, height);
void DrawLine((TIdentifier Identifier, TEntry Value) pair)
=> DrawEntry(pair.Identifier, pair.Value);
}
public abstract ReadOnlySpan<byte> Label { get; }
@ -57,6 +59,7 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
protected abstract void DrawEntry(TIdentifier identifier, TEntry entry);
protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate();
protected abstract int Count { get; }
/// <summary>

View file

@ -58,7 +58,13 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
}
protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate()
=> Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value));
=> Editor.Rsp
.OrderBy(kvp => kvp.Key.SubRace)
.ThenBy(kvp => kvp.Key.Attribute)
.Select(kvp => (kvp.Key, kvp.Value));
protected override int Count
=> Editor.Rsp.Count;
private static bool DrawIdentifierInput(ref RspIdentifier identifier)
{

View file

@ -1,8 +1,10 @@
using System.Linq;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
@ -144,22 +146,20 @@ public partial class ModEditWindow
private static string DrawFileTooltip(FileRegistry registry, ColorId color)
{
(string, int) GetMulti()
{
var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray();
return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length);
}
var (text, groupCount) = color switch
{
ColorId.ConflictingMod => (string.Empty, 0),
ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1),
ColorId.ConflictingMod => (null, 0),
ColorId.NewMod => ([registry.SubModUsage[0].Item1.GetName()], 1),
ColorId.InheritedMod => GetMulti(),
_ => (string.Empty, 0),
_ => (null, 0),
};
if (text.Length > 0 && ImGui.IsItemHovered())
ImGui.SetTooltip(text);
if (text != null && ImGui.IsItemHovered())
{
using var tt = ImUtf8.Tooltip();
using var c = ImRaii.DefaultColors();
ImUtf8.Text(string.Join('\n', text));
}
return (groupCount, registry.SubModUsage.Count) switch
@ -169,6 +169,12 @@ public partial class ModEditWindow
(1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)",
_ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)",
};
(IEnumerable<string>, int) GetMulti()
{
var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray();
return (groups.Select(g => g.Key.GetName()), groups.Length);
}
}
private void DrawSelectable(FileRegistry registry)

View file

@ -16,21 +16,21 @@ public partial class ModEditWindow
private void DrawMetaTab()
{
using var tab = ImRaii.TabItem("Meta Manipulations");
using var tab = ImUtf8.TabItem("Meta Manipulations"u8);
if (!tab)
return;
DrawOptionSelectHeader();
var setsEqual = !_editor.MetaEditor.Changes;
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
var tt = setsEqual ? "No changes staged."u8 : "Apply the currently staged changes to the option."u8;
ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
if (ImUtf8.ButtonEx("Apply Changes"u8, tt, Vector2.Zero, setsEqual))
_editor.MetaEditor.Apply(_editor.Option!);
ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual))
tt = setsEqual ? "No changes staged."u8 : "Revert all currently staged changes."u8;
if (ImUtf8.ButtonEx("Revert Changes"u8, tt, Vector2.Zero, setsEqual))
_editor.MetaEditor.Load(_editor.Mod!, _editor.Option!);
ImGui.SameLine();
@ -40,8 +40,11 @@ public partial class ModEditWindow
ImGui.SameLine();
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor);
ImGui.SameLine();
if (ImGui.Button("Write as TexTools Files"))
if (ImUtf8.Button("Write as TexTools Files"u8))
_metaFileManager.WriteAllTexToolsMeta(Mod!);
ImGui.SameLine();
if (ImUtf8.ButtonEx("Remove All Default-Values", "Delete any entries from all lists that set the value to its default value."u8))
_editor.MetaEditor.DeleteDefaultValues();
using var child = ImRaii.Child("##meta", -Vector2.One, true);
if (!child)

View file

@ -85,7 +85,7 @@ public partial class ModEditWindow
ImGuiUtil.SelectableHelpMarker(newDesc);
}
}
}
private void RedrawOnSaveBox()
{
@ -128,7 +128,8 @@ public partial class ModEditWindow
? "This saves the texture in place. This is not revertible."
: $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save.";
var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0);
if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2,
tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs))
{
@ -141,17 +142,18 @@ public partial class ModEditWindow
if (ImGui.Button("Save as TEX", buttonSize2))
OpenSaveAsDialog(".tex");
if (ImGui.Button("Export as PNG", buttonSize2))
if (ImGui.Button("Export as TGA", buttonSize3))
OpenSaveAsDialog(".tga");
ImGui.SameLine();
if (ImGui.Button("Export as PNG", buttonSize3))
OpenSaveAsDialog(".png");
ImGui.SameLine();
if (ImGui.Button("Export as DDS", buttonSize2))
if (ImGui.Button("Export as DDS", buttonSize3))
OpenSaveAsDialog(".dds");
ImGui.NewLine();
var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy;
var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0);
if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3,
"This converts the texture to BC7 format in place. This is not revertible.",
!canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB))
@ -226,7 +228,8 @@ public partial class ModEditWindow
private void OpenSaveAsDialog(string defaultExtension)
{
var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path);
_fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension,
_fileDialog.OpenSavePicker("Save Texture as TEX, DDS, PNG or TGA...", "Textures{.png,.dds,.tex,.tga},.tex,.dds,.png,.tga", fileName,
defaultExtension,
(a, b) =>
{
if (a)
@ -329,5 +332,6 @@ public partial class ModEditWindow
".png",
".dds",
".tex",
".tga",
};
}

View file

@ -8,6 +8,7 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@ -36,8 +37,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
{
private const string WindowBaseLabel = "###SubModEdit";
public readonly MigrationManager MigrationManager;
private readonly PerformanceTracker _performance;
private readonly ModEditor _editor;
private readonly Configuration _config;
@ -53,34 +52,68 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
private Vector2 _iconSize = Vector2.Zero;
private bool _allowReduplicate;
public Mod? Mod { get; private set; }
public Mod? Mod { get; private set; }
public bool IsLoading
{
get
{
lock (_lock)
{
return _editor.IsLoading || _loadingMod is { IsCompleted: false };
}
}
}
private readonly object _lock = new();
private Task? _loadingMod;
private void AppendTask(Action run)
{
lock (_lock)
{
if (_loadingMod == null || _loadingMod.IsCompleted)
_loadingMod = Task.Run(run);
else
_loadingMod = _loadingMod.ContinueWith(_ => run());
}
}
public void ChangeMod(Mod mod)
{
if (mod == Mod)
return;
_editor.LoadMod(mod, -1, 0);
Mod = mod;
SizeConstraints = new WindowSizeConstraints
WindowName = $"{mod.Name} (LOADING){WindowBaseLabel}";
AppendTask(() =>
{
MinimumSize = new Vector2(1240, 600),
MaximumSize = 4000 * Vector2.One,
};
_selectedFiles.Clear();
_modelTab.Reset();
_materialTab.Reset();
_shaderPackageTab.Reset();
_itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings);
UpdateModels();
_forceTextureStartPath = true;
_editor.LoadMod(mod, -1, 0).Wait();
Mod = mod;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(1240, 600),
MaximumSize = 4000 * Vector2.One,
};
_selectedFiles.Clear();
_modelTab.Reset();
_materialTab.Reset();
_shaderPackageTab.Reset();
_itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings);
UpdateModels();
_forceTextureStartPath = true;
});
}
public void ChangeOption(IModDataContainer? subMod)
{
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
_editor.LoadOption(groupIdx, dataIdx);
AppendTask(() =>
{
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
_editor.LoadOption(groupIdx, dataIdx).Wait();
});
}
public void UpdateModels()
@ -94,6 +127,9 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
public override void PreDraw()
{
if (IsLoading)
return;
using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow);
var sb = new StringBuilder(256);
@ -146,13 +182,16 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
public override void OnClose()
{
_left.Dispose();
_right.Dispose();
_materialTab.Reset();
_modelTab.Reset();
_shaderPackageTab.Reset();
_config.Ephemeral.AdvancedEditingOpen = false;
_config.Ephemeral.Save();
AppendTask(() =>
{
_left.Dispose();
_right.Dispose();
_materialTab.Reset();
_modelTab.Reset();
_shaderPackageTab.Reset();
});
}
public override void Draw()
@ -165,6 +204,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
_config.Ephemeral.Save();
}
if (IsLoading)
{
var radius = 100 * ImUtf8.GlobalScale;
var thickness = (int) (20 * ImUtf8.GlobalScale);
var offsetX = ImGui.GetContentRegionAvail().X / 2 - radius;
var offsetY = ImGui.GetContentRegionAvail().Y / 2 - radius;
ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(offsetX, offsetY));
ImUtf8.Spinner("##spinner"u8, radius, thickness, ImGui.GetColorU32(ImGuiCol.Text));
return;
}
using var tabBar = ImRaii.TabBar("##tabs");
if (!tabBar)
return;
@ -407,14 +457,14 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
_editor.Option is DefaultSubMod))
{
_editor.LoadOption(-1, 0);
_editor.LoadOption(-1, 0).Wait();
ret = true;
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
{
_editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
_editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait();
ret = true;
}
@ -432,7 +482,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
if (ImGui.Selectable(option.GetFullName(), option == _editor.Option))
{
var (groupIdx, dataIdx) = option.GetDataIndices();
_editor.LoadOption(groupIdx, dataIdx);
_editor.LoadOption(groupIdx, dataIdx).Wait();
ret = true;
}
}
@ -587,7 +637,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework,
MetaDrawers metaDrawers, MigrationManager migrationManager,
MtrlTabFactory mtrlTabFactory)
MtrlTabFactory mtrlTabFactory, ModSelection selection)
: base(WindowBaseLabel)
{
_performance = performance;
@ -604,7 +654,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
_models = models;
_fileDialog = fileDialog;
_framework = framework;
MigrationManager = migrationManager;
_metaDrawers = metaDrawers;
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
@ -622,6 +671,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
_quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
if (IsOpen && selection.Mod != null)
ChangeMod(selection.Mod);
}
public void Dispose()

View file

@ -5,24 +5,24 @@ using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Interop.PathResolving;
using Penumbra.Mods;
using Penumbra.UI.CollectionTab;
using Penumbra.UI.ModsTab;
namespace Penumbra.UI.Classes;
public class CollectionSelectHeader : IUiService
{
private readonly CollectionCombo _collectionCombo;
private readonly ActiveCollections _activeCollections;
private readonly TutorialService _tutorial;
private readonly ModFileSystemSelector _selector;
private readonly CollectionResolver _resolver;
private readonly CollectionCombo _collectionCombo;
private readonly ActiveCollections _activeCollections;
private readonly TutorialService _tutorial;
private readonly ModSelection _selection;
private readonly CollectionResolver _resolver;
public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector,
public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection,
CollectionResolver resolver)
{
_tutorial = tutorial;
_selector = selector;
_selection = selection;
_resolver = resolver;
_activeCollections = collectionManager.Active;
_collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList());
@ -115,7 +115,7 @@ public class CollectionSelectHeader : IUiService
private (ModCollection?, string, string, bool) GetInheritedCollectionInfo()
{
var collection = _selector.Selected == null ? null : _selector.SelectedSettingCollection;
var collection = _selection.Mod == null ? null : _selection.Collection;
return CheckCollection(collection, true) switch
{
CollectionState.Unavailable => (null, "Not Inherited",

View file

@ -4,7 +4,6 @@ using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.Widget;
using OtterGui.Widgets;
using OtterGuiInternal.Utility;
using Penumbra.GameData.Structs;
using Penumbra.Mods.Groups;

View file

@ -9,6 +9,8 @@ using OtterGui.Filesystem;
using OtterGui.FileSystem.Selector;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Text.Widget;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -25,7 +27,6 @@ namespace Penumbra.UI.ModsTab;
public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>, IUiService
{
private readonly CommunicatorService _communicator;
private readonly MessageService _messager;
private readonly Configuration _config;
private readonly FileDialogService _fileDialog;
private readonly ModManager _modManager;
@ -33,15 +34,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
private readonly TutorialService _tutorial;
private readonly ModImportManager _modImportManager;
private readonly IDragDropManager _dragDrop;
private readonly ModSearchStringSplitter Filter = new();
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
private readonly ModSearchStringSplitter _filter = new();
private readonly ModSelection _selection;
public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog,
MessageService messager, ModImportManager modImportManager, IDragDropManager dragDrop)
MessageService messager, ModImportManager modImportManager, IDragDropManager dragDrop, ModSelection selection)
: base(fileSystem, keyState, Penumbra.Log, HandleException, allowMultipleSelection: true)
{
_communicator = communicator;
@ -50,9 +48,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
_config = config;
_tutorial = tutorial;
_fileDialog = fileDialog;
_messager = messager;
_modImportManager = modImportManager;
_dragDrop = dragDrop;
_selection = selection;
// @formatter:off
SubscribeRightClickFolder(EnableDescendants, 10);
@ -78,22 +76,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
// @formatter:on
SetFilterTooltip();
SelectionChanged += OnSelectionChange;
if (_config.Ephemeral.LastModPath.Length > 0)
{
var mod = _modManager.FirstOrDefault(m
=> string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase));
if (mod != null)
SelectByValue(mod);
}
if (_selection.Mod != null)
SelectByValue(_selection.Mod);
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector);
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector);
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector);
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystemSelector);
_communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection, ModDiscoveryStarted.Priority.ModFileSystemSelector);
_communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection, ModDiscoveryFinished.Priority.ModFileSystemSelector);
OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, "");
SetFilterDirty();
SelectionChanged += OnSelectionChanged;
}
public void SetRenameSearchPath(RenameField value)
@ -190,7 +182,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName);
if (newDir != null)
{
_modManager.AddMod(newDir);
_modManager.AddMod(newDir, false);
_newModName = string.Empty;
}
}
@ -449,12 +441,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited)
{
if (collection != _collectionManager.Active.Current)
return;
SetFilterDirty();
if (mod == Selected)
OnSelectionChange(Selected, Selected, default);
if (collection == _collectionManager.Active.Current)
SetFilterDirty();
}
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
@ -473,41 +461,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
private void OnInheritanceChange(ModCollection collection, bool _)
{
if (collection != _collectionManager.Active.Current)
return;
SetFilterDirty();
OnSelectionChange(Selected, Selected, default);
if (collection == _collectionManager.Active.Current)
SetFilterDirty();
}
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _)
{
if (collectionType is not CollectionType.Current || oldCollection == newCollection)
return;
SetFilterDirty();
OnSelectionChange(Selected, Selected, default);
}
private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2)
{
if (newSelection == null)
{
SelectedSettings = ModSettings.Empty;
SelectedSettingCollection = ModCollection.Empty;
}
else
{
(var settings, SelectedSettingCollection) = _collectionManager.Active.Current[newSelection.Index];
SelectedSettings = settings ?? ModSettings.Empty;
}
var name = newSelection?.Identifier ?? string.Empty;
if (name != _config.Ephemeral.LastModPath)
{
_config.Ephemeral.LastModPath = name;
_config.Ephemeral.Save();
}
if (collectionType is CollectionType.Current && oldCollection != newCollection)
SetFilterDirty();
}
// Keep selections across rediscoveries if possible.
@ -530,6 +491,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
_lastSelectedDirectory = string.Empty;
}
private void OnSelectionChanged(Mod? oldSelection, Mod? newSelection, in ModState state)
=> _selection.SelectMod(newSelection);
#endregion
#region Filters
@ -567,7 +531,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
/// <summary> Appropriately identify and set the string filter and its type. </summary>
protected override bool ChangeFilter(string filterValue)
{
Filter.Parse(filterValue);
_filter.Parse(filterValue);
return true;
}
@ -597,7 +561,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
{
state = default;
return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|| !Filter.IsVisible(f);
|| !_filter.IsVisible(f);
}
return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state);
@ -605,7 +569,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
/// <summary> Apply the string filters. </summary>
private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod)
=> !Filter.IsVisible(leaf);
=> !_filter.IsVisible(leaf);
/// <summary> Only get the text color for a mod if no filters are set. </summary>
private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection)
@ -741,8 +705,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
ImGui.GetStyle().ItemSpacing with { Y = 3 * UiHelpers.Scale });
var flags = (int)_stateFilter;
if (ImGui.Checkbox("Everything", ref everything))
{
@ -751,12 +713,19 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
}
ImGui.Dummy(new Vector2(0, 5 * UiHelpers.Scale));
foreach (ModFilter flag in Enum.GetValues(typeof(ModFilter)))
foreach (var (onFlag, offFlag, name) in ModFilterExtensions.TriStatePairs)
{
if (ImGui.CheckboxFlags(flag.ToName(), ref flags, (int)flag))
{
_stateFilter = (ModFilter)flags;
if (TriStateCheckbox.Instance.Draw(name, ref _stateFilter, onFlag, offFlag))
SetFilterDirty();
}
foreach (var group in ModFilterExtensions.Groups)
{
ImGui.Separator();
foreach (var (flag, name) in group)
{
if (ImUtf8.Checkbox(name, ref _stateFilter, flag))
SetFilterDirty();
}
}

View file

@ -29,29 +29,28 @@ public static class ModFilterExtensions
{
public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 20) - 1);
public static string ToName(this ModFilter filter)
=> filter switch
{
ModFilter.Enabled => "Enabled",
ModFilter.Disabled => "Disabled",
ModFilter.Favorite => "Favorite",
ModFilter.NotFavorite => "No Favorite",
ModFilter.NoConflict => "No Conflicts",
ModFilter.SolvedConflict => "Solved Conflicts",
ModFilter.UnsolvedConflict => "Unsolved Conflicts",
ModFilter.HasNoMetaManipulations => "No Meta Manipulations",
ModFilter.HasMetaManipulations => "Meta Manipulations",
ModFilter.HasNoFileSwaps => "No File Swaps",
ModFilter.HasFileSwaps => "File Swaps",
ModFilter.HasNoConfig => "No Configuration",
ModFilter.HasConfig => "Configuration",
ModFilter.HasNoFiles => "No Files",
ModFilter.HasFiles => "Files",
ModFilter.IsNew => "Newly Imported",
ModFilter.NotNew => "Not Newly Imported",
ModFilter.Inherited => "Inherited Configuration",
ModFilter.Uninherited => "Own Configuration",
ModFilter.Undefined => "Not Configured",
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null),
};
public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
[
(ModFilter.Enabled, ModFilter.Disabled, "Enabled"),
(ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"),
(ModFilter.Favorite, ModFilter.NotFavorite, "Favorite"),
(ModFilter.HasConfig, ModFilter.HasNoConfig, "Has Options"),
(ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"),
(ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"),
(ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"),
];
public static IReadOnlyList<IReadOnlyList<(ModFilter Filter, string Name)>> Groups =
[
[
(ModFilter.NoConflict, "Has No Conflicts"),
(ModFilter.SolvedConflict, "Has Solved Conflicts"),
(ModFilter.UnsolvedConflict, "Has Unsolved Conflicts"),
],
[
(ModFilter.Undefined, "Not Configured"),
(ModFilter.Inherited, "Inherited Configuration"),
(ModFilter.Uninherited, "Own Configuration"),
],
];
}

View file

@ -10,22 +10,23 @@ namespace Penumbra.UI.ModsTab;
public class ModPanel : IDisposable, IUiService
{
private readonly MultiModPanel _multiModPanel;
private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow;
private readonly ModPanelHeader _header;
private readonly ModPanelTabBar _tabs;
private bool _resetCursor;
private readonly MultiModPanel _multiModPanel;
private readonly ModSelection _selection;
private readonly ModEditWindow _editWindow;
private readonly ModPanelHeader _header;
private readonly ModPanelTabBar _tabs;
private bool _resetCursor;
public ModPanel(IDalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs,
public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModEditWindow editWindow, ModPanelTabBar tabs,
MultiModPanel multiModPanel, CommunicatorService communicator)
{
_selector = selector;
_editWindow = editWindow;
_tabs = tabs;
_multiModPanel = multiModPanel;
_header = new ModPanelHeader(pi, communicator);
_selector.SelectionChanged += OnSelectionChange;
_selection = selection;
_editWindow = editWindow;
_tabs = tabs;
_multiModPanel = multiModPanel;
_header = new ModPanelHeader(pi, communicator);
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel);
OnSelectionChange(null, _selection.Mod);
}
public void Draw()
@ -52,17 +53,17 @@ public class ModPanel : IDisposable, IUiService
public void Dispose()
{
_selector.SelectionChanged -= OnSelectionChange;
_selection.Unsubscribe(OnSelectionChange);
_header.Dispose();
}
private bool _valid;
private Mod _mod = null!;
private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _)
private void OnSelectionChange(Mod? old, Mod? mod)
{
_resetCursor = true;
if (mod == null || _selector.Selected == null)
if (mod == null || _selection.Mod == null)
{
_editWindow.IsOpen = false;
_valid = false;
@ -73,7 +74,7 @@ public class ModPanel : IDisposable, IUiService
_editWindow.ChangeMod(mod);
_valid = true;
_mod = mod;
_header.UpdateModData(_mod);
_header.ChangeMod(_mod);
_tabs.Settings.Reset();
_tabs.Edit.Reset();
}

View file

@ -18,7 +18,8 @@ public class ModPanelHeader : IDisposable
private readonly IFontHandle _nameFont;
private readonly CommunicatorService _communicator;
private float _lastPreSettingsHeight = 0;
private float _lastPreSettingsHeight;
private bool _dirty = true;
public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator)
{
@ -33,6 +34,7 @@ public class ModPanelHeader : IDisposable
/// </summary>
public void Draw()
{
UpdateModData();
var height = ImGui.GetContentRegionAvail().Y;
var maxHeight = 3 * height / 4;
using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers
@ -49,16 +51,25 @@ public class ModPanelHeader : IDisposable
_lastPreSettingsHeight = ImGui.GetCursorPosY();
}
public void ChangeMod(Mod mod)
{
_mod = mod;
_dirty = true;
}
/// <summary>
/// Update all mod header data. Should someone change frame padding or item spacing,
/// or his default font, this will break, but he will just have to select a different mod to restore.
/// </summary>
public void UpdateModData(Mod mod)
private void UpdateModData()
{
if (!_dirty)
return;
_dirty = false;
_lastPreSettingsHeight = 0;
_mod = mod;
// Name
var name = $" {mod.Name} ";
var name = $" {_mod.Name} ";
if (name != _modName)
{
using var f = _nameFont.Push();
@ -67,16 +78,16 @@ public class ModPanelHeader : IDisposable
}
// Author
if (mod.Author != _modAuthor)
if (_mod.Author != _modAuthor)
{
var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}";
_modAuthor = mod.Author.Text;
var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}";
_modAuthor = _mod.Author.Text;
_modAuthorWidth = ImGui.CalcTextSize(author).X;
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
}
// Version
var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty;
var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty;
if (version != _modVersion)
{
_modVersion = version;
@ -84,9 +95,9 @@ public class ModPanelHeader : IDisposable
}
// Website
if (_modWebsite != mod.Website)
if (_modWebsite != _mod.Website)
{
_modWebsite = mod.Website;
_modWebsite = _mod.Website;
_websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp);
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
@ -253,7 +264,6 @@ public class ModPanelHeader : IDisposable
{
const ModDataChangeType relevantChanges =
ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version;
if ((changeType & relevantChanges) != 0)
UpdateModData(mod);
_dirty = (changeType & relevantChanges) != 0;
}
}

View file

@ -3,9 +3,9 @@ using OtterGui.Raii;
using OtterGui;
using OtterGui.Services;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.UI.Classes;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Mods.Settings;
@ -16,16 +16,14 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelSettingsTab(
CollectionManager collectionManager,
ModManager modManager,
ModFileSystemSelector selector,
ModSelection selection,
TutorialService tutorial,
CommunicatorService communicator,
ModGroupDrawer modGroupDrawer)
: ITab, IUiService
{
private bool _inherited;
private ModSettings _settings = null!;
private ModCollection _collection = null!;
private int? _currentPriority;
private bool _inherited;
private int? _currentPriority;
public ReadOnlySpan<byte> Label
=> "Settings"u8;
@ -42,12 +40,10 @@ public class ModPanelSettingsTab(
if (!child)
return;
_settings = selector.SelectedSettings;
_collection = selector.SelectedSettingCollection;
_inherited = _collection != collectionManager.Active.Current;
_inherited = selection.Collection != collectionManager.Active.Current;
DrawInheritedWarning();
UiHelpers.DefaultLineSpace();
communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier);
communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier);
DrawEnabledInput();
tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods);
ImGui.SameLine();
@ -55,11 +51,11 @@ public class ModPanelSettingsTab(
tutorial.OpenTutorial(BasicTutorialSteps.Priority);
DrawRemoveSettings();
communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier);
communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier);
modGroupDrawer.Draw(selector.Selected!, _settings);
modGroupDrawer.Draw(selection.Mod!, selection.Settings);
UiHelpers.DefaultLineSpace();
communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier);
communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier);
}
/// <summary> Draw a big red bar if the current setting is inherited. </summary>
@ -70,8 +66,8 @@ public class ModPanelSettingsTab(
using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg);
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width))
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false);
if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width))
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false);
ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"
+ "You can also just change any setting, which will copy the settings with the single setting changed to the current selection.");
@ -80,12 +76,12 @@ public class ModPanelSettingsTab(
/// <summary> Draw a checkbox for the enabled status of the mod. </summary>
private void DrawEnabledInput()
{
var enabled = _settings.Enabled;
var enabled = selection.Settings.Enabled;
if (!ImGui.Checkbox("Enabled", ref enabled))
return;
modManager.SetKnown(selector.Selected!);
collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled);
modManager.SetKnown(selection.Mod!);
collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled);
}
/// <summary>
@ -95,15 +91,16 @@ public class ModPanelSettingsTab(
private void DrawPriorityInput()
{
using var group = ImRaii.Group();
var priority = _currentPriority ?? _settings.Priority.Value;
var settings = selection.Settings;
var priority = _currentPriority ?? settings.Priority.Value;
ImGui.SetNextItemWidth(50 * UiHelpers.Scale);
if (ImGui.InputInt("##Priority", ref priority, 0, 0))
_currentPriority = priority;
if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue)
{
if (_currentPriority != _settings.Priority.Value)
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!,
if (_currentPriority != settings.Priority.Value)
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!,
new ModPriority(_currentPriority.Value));
_currentPriority = null;
@ -120,13 +117,13 @@ public class ModPanelSettingsTab(
private void DrawRemoveSettings()
{
const string text = "Inherit Settings";
if (_inherited || _settings == ModSettings.Empty)
if (_inherited || selection.Settings == ModSettings.Empty)
return;
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
if (ImGui.Button(text))
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true);
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true);
ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n"
+ "If no inherited collection has settings for this mod, it will be disabled.");

View file

@ -1,5 +1,4 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
@ -43,7 +42,6 @@ using Penumbra.Api.IpcTester;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.Tabs.Debug;
@ -721,7 +719,8 @@ public class DebugTab : Window, ITab, IUiService
if (!tree)
continue;
using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
using var table = Table("##table", data.Colors.Length + data.Scalars.Length,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
continue;

View file

@ -82,8 +82,7 @@ public class ModsTab(
+ $"{selector.SortMode.Name} Sort Mode\n"
+ $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
+ $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n");
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n");
}
}

View file

@ -816,13 +816,13 @@ public class SettingsTab : ITab, IUiService
if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero,
"Try to compress all files in your root directory. This will take a while.",
_compactor.MassCompactRunning || !_modManager.Valid))
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K);
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, true);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero,
"Try to decompress all files in your root directory. This will take a while.",
_compactor.MassCompactRunning || !_modManager.Valid))
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None);
_compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, true);
if (_compactor.MassCompactRunning)
{

View file

@ -4,11 +4,11 @@
"net8.0-windows7.0": {
"EmbedIO": {
"type": "Direct",
"requested": "[3.4.3, )",
"resolved": "3.4.3",
"contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==",
"requested": "[3.5.2, )",
"resolved": "3.5.2",
"contentHash": "YU4j+3XvuO8/VPkNf7KWOF1TpMhnyVhXnPsG1mvnDhTJ9D5BZOFXVDvCpE/SkQ1AJ0Aa+dXOVSW3ntgmLL7aJg==",
"dependencies": {
"Unosquare.Swan.Lite": "3.0.0"
"Unosquare.Swan.Lite": "3.1.0"
}
},
"PeNet": {
@ -23,23 +23,26 @@
},
"SharpCompress": {
"type": "Direct",
"requested": "[0.33.0, )",
"resolved": "0.33.0",
"contentHash": "FlHfpTAADzaSlVCBF33iKJk9UhOr3Xj+r5LXbW2GzqYr0SrhiOf6shLX2LC2fqs7g7d+YlwKbBXqWFtb+e7icw=="
"requested": "[0.37.2, )",
"resolved": "0.37.2",
"contentHash": "cFBpTct57aubLQXkdqMmgP8GGTFRh7fnRWP53lgE/EYUpDZJ27SSvTkdjB4OYQRZ20SJFpzczUquKLbt/9xkhw==",
"dependencies": {
"ZstdSharp.Port": "0.8.0"
}
},
"SharpGLTF.Core": {
"type": "Direct",
"requested": "[1.0.0-alpha0030, )",
"resolved": "1.0.0-alpha0030",
"contentHash": "HVL6PcrM0H/uEk96nRZfhtPeYvSFGHnni3g1aIckot2IWVp0jLMH5KWgaWfsatEz4Yds3XcdSLUWmJZivDBUPA=="
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w=="
},
"SharpGLTF.Toolkit": {
"type": "Direct",
"requested": "[1.0.0-alpha0030, )",
"resolved": "1.0.0-alpha0030",
"contentHash": "nsoJWAFhXgEky9bVCY0zLeZVDx+S88u7VjvuebvMb6dJiNyFOGF6FrrMHiJe+x5pcVBxxlc3VoXliBF7r/EqYA==",
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==",
"dependencies": {
"SharpGLTF.Runtime": "1.0.0-alpha0030"
"SharpGLTF.Runtime": "1.0.1"
}
},
"SixLabors.ImageSharp": {
@ -48,10 +51,16 @@
"resolved": "3.1.5",
"contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g=="
},
"System.Formats.Asn1": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2023.3.0",
"contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA=="
"resolved": "2024.2.0",
"contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw=="
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
@ -73,17 +82,12 @@
},
"SharpGLTF.Runtime": {
"type": "Transitive",
"resolved": "1.0.0-alpha0030",
"contentHash": "Ysn+fyj9EVXj6mfG0BmzSTBGNi/QvcnTrMd54dBMOlI/TsMRvnOY3JjTn0MpeH2CgHXX4qogzlDt4m+rb3n4Og==",
"resolved": "1.0.1",
"contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==",
"dependencies": {
"SharpGLTF.Core": "1.0.0-alpha0030"
"SharpGLTF.Core": "1.0.1"
}
},
"System.Formats.Asn1": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "8.0.0",
@ -99,16 +103,21 @@
},
"Unosquare.Swan.Lite": {
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==",
"resolved": "3.1.0",
"contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==",
"dependencies": {
"System.ValueTuple": "4.5.0"
}
},
"ZstdSharp.Port": {
"type": "Transitive",
"resolved": "0.8.0",
"contentHash": "Z62eNBIu8E8YtbqlMy57tK3dV1+m2b9NhPeaYovB5exmLKvrGCqOhJTzrEUH5VyUWU6vwX3c1XHJGhW5HVs8dA=="
},
"ottergui": {
"type": "Project",
"dependencies": {
"JetBrains.Annotations": "[2023.3.0, )",
"JetBrains.Annotations": "[2024.2.0, )",
"Microsoft.Extensions.DependencyInjection": "[8.0.0, )"
}
},
@ -122,7 +131,7 @@
"type": "Project",
"dependencies": {
"OtterGui": "[1.0.0, )",
"Penumbra.Api": "[5.2.0, )",
"Penumbra.Api": "[5.3.0, )",
"Penumbra.String": "[1.0.4, )"
}
},

View file

@ -6,7 +6,7 @@
"Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra",
"AssemblyVersion": "1.2.1.1",
"TestingAssemblyVersion": "1.2.1.1",
"TestingAssemblyVersion": "1.2.1.2",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 10,
@ -19,7 +19,7 @@
"LoadRequiredState": 2,
"LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
}