diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b40b2538..1783c9a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Release/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c9e2909..4799cbed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Release/* diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 91361646..0718ded2 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -37,7 +37,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Debug/* diff --git a/OtterGui b/OtterGui index 07a00913..3e6b0857 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a +Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97 diff --git a/Penumbra.Api b/Penumbra.Api index 552246e5..97e9f427 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a +Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623 diff --git a/Penumbra.GameData b/Penumbra.GameData index 6c02858d..27cef2c1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6c02858d5a3d20100377dd127d1b85dbe82a4c44 +Subproject commit 27cef2c1b8ef8ce9b73bc658e03f543b5c7ef29d diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs index 93345053..e50b7a1b 100644 --- a/Penumbra/Api/Api/EditingApi.cs +++ b/Penumbra/Api/Api/EditingApi.cs @@ -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), diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index ce1a9def..6f3ed51e 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -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 GetPlayerMetaManipulationsAsync() + { + return Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false); + return CompressMetaManipulations(playerCollection); + }); + } + + public Task 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(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(&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(Stream stream, MetaCacheBase 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(); + } + } + } + + /// + /// 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. + /// + 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 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(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var eqpCount = r.ReadInt32(); + for (var i = 0; i < eqpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var eqdpCount = r.ReadInt32(); + for (var i = 0; i < eqdpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var estCount = r.ReadInt32(); + for (var i = 0; i < estCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var rspCount = r.ReadInt32(); + for (var i = 0; i < rspCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var gmpCount = r.ReadInt32(); + for (var i = 0; i < gmpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var globalEqpCount = r.ReadInt32(); + for (var i = 0; i < globalEqpCount; ++i) + { + var manip = r.Read(); + if (!manip.Validate() || !manips.TryAdd(manip)) + return false; + } + + return true; + } + + private static bool ConvertManipsV0(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + var json = Encoding.UTF8.GetString(data); + manips = JsonConvert.DeserializeObject(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)}"); } } diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 2acdf031..64e201be 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -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); } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index f02b0d94..516b4347 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -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; } - - /// - /// 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. - /// - 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; - } } diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 6d4f17b2..f6d1c9eb 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -9,7 +9,6 @@ using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; -using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -28,6 +27,8 @@ public class TemporaryIpcTester( { public Guid LastCreatedCollectionId = Guid.Empty; + private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9; + private Guid? _tempGuid; private string _tempCollectionName = string.Empty; private string _tempCollectionGuidName = string.Empty; @@ -48,9 +49,9 @@ public class TemporaryIpcTester( ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); - ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); - ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); + ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); + ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); @@ -102,7 +103,7 @@ public class TemporaryIpcTester( !collections.Storage.ByName(_tempModName, out var copyCollection)) && copyCollection is { HasCache: true }) { - var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); + var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); var manips = MetaApi.CompressMetaManipulations(copyCollection); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); } @@ -124,11 +125,11 @@ public class TemporaryIpcTester( public void DrawCollections() { - using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); + using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8); if (!collTree) return; - using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit); + using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -139,7 +140,7 @@ public class TemporaryIpcTester( var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) .FirstOrDefault() ?? "Unknown"; - if (ImGui.Button("Save##Collection")) + if (_debug && ImUtf8.Button("Save##Collection"u8)) TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character); using (ImRaii.PushFont(UiBuilder.MonoFont)) diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index 1c80b47d..60e782b5 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -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, IService +public class GlobalEqpCache : ReadWriteDictionary, IService { private readonly HashSet _doNotHideEarrings = []; private readonly HashSet _doNotHideNecklace = []; @@ -39,7 +40,7 @@ public class GlobalEqpCache : Dictionary, IService original |= EqpEntry.HeadShowHrothgarHat; if (_doNotHideEarrings.Contains(armor[5].Set)) - original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman; + original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura; if (_doNotHideNecklace.Contains(armor[6].Set)) original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 1a6924a9..05a94ac5 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -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(); diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/MetaCacheBase.cs similarity index 52% rename from Penumbra/Collections/Cache/IMetaCache.cs rename to Penumbra/Collections/Cache/MetaCacheBase.cs index fecc6f50..98a87e3f 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCacheBase.cs @@ -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(MetaFileManager manager, ModCollection collection) - : Dictionary + : ReadWriteDictionary 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.Default.Equals(pair.Entry, entry)) - return false; + if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.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(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(MetaFileManager manager protected virtual void RevertModInternal(TIdentifier identifier) { } - - protected virtual void Dispose(bool _) - { } } diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 95d4ac4d..2788177d 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -46,5 +46,8 @@ public sealed class CollectionChange() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index dbcf9e4a..30af2b20 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -23,5 +23,8 @@ public sealed class CollectionInheritanceChanged() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 7fda2f35..d4bf00be 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -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() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 62892473..121e6eed 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -36,12 +36,14 @@ public class MaterialExporter return material.Mtrl.ShaderPackage.Name switch { // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. - "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), - "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), - "hair.shpk" => BuildHair(material, name), - "iris.shpk" => BuildIris(material, name), - "skin.shpk" => BuildSkin(material, name), - _ => BuildFallback(material, name, notifier), + "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "charactertattoo.shpk" => BuildCharacterTattoo(material, name), + "hair.shpk" => BuildHair(material, name), + "iris.shpk" => BuildIris(material, name), + "skin.shpk" => BuildSkin(material, name), + _ => BuildFallback(material, name, notifier), }; } @@ -49,70 +51,65 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = new LegacyColorTable(material.Mtrl.Table!); + var table = new ColorTable(material.Mtrl.Table!); + var indexTexture = material.Textures[(TextureUsage)1449103320]; + var indexOperation = new ProcessCharacterIndexOperation(indexTexture, table); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, indexTexture.Bounds, in indexOperation); - var normal = material.Textures[TextureUsage.SamplerNormal]; + var normalTexture = material.Textures[TextureUsage.SamplerNormal]; + var normalOperation = new ProcessCharacterNormalOperation(normalTexture); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normalTexture.Bounds, in normalOperation); - var operation = new ProcessCharacterNormalOperation(normal, table); - ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation); + // Merge in opacity from the normal. + var baseColor = indexOperation.BaseColor; + MultiplyOperation.Execute(baseColor, normalOperation.BaseColorOpacity); - // Check if full textures are provided, and merge in if available. - var baseColor = operation.BaseColor; + // Check if a full diffuse is provided, and merge in if available. if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { - MultiplyOperation.Execute(diffuse, operation.BaseColor); + MultiplyOperation.Execute(diffuse, indexOperation.BaseColor); baseColor = diffuse; } - Image specular = operation.Specular; + var specular = indexOperation.Specular; if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { - MultiplyOperation.Execute(specularTexture, operation.Specular); + MultiplyOperation.Execute(specularTexture, indexOperation.Specular); specular = specularTexture; } // Pull further information from the mask. if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { - // Extract the red channel for "ambient occlusion". - maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); - maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => - { - for (var y = 0; y < maskAccessor.Height; y++) - { - var maskSpan = maskAccessor.GetRowSpan(y); - var baseColorSpan = baseColorAccessor.GetRowSpan(y); + var maskOperation = new ProcessCharacterMaskOperation(maskTexture); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, maskTexture.Bounds, in maskOperation); - for (var x = 0; x < maskSpan.Length; x++) - baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); - } - }); - // TODO: handle other textures stored in the mask? + // TODO: consider using the occusion gltf material property. + MultiplyOperation.Execute(baseColor, maskOperation.Occlusion); + + // Similar to base color's alpha, this is a pretty wasteful operation for a single channel. + MultiplyOperation.Execute(specular, maskOperation.SpecularFactor); } // Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture. var specularImage = BuildImage(specular, name, "specular"); return BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(operation.Normal, name, "normal")) - .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normalOperation.Normal, name, "normal")) + .WithEmissive(BuildImage(indexOperation.Emissive, name, "emissive"), Vector3.One, 1) .WithSpecularFactor(specularImage, 1) .WithSpecularColor(specularImage); } - // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. - // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - // TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore. - private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation + private readonly struct ProcessCharacterIndexOperation(Image index, ColorTable table) : IRowOperation { - public Image Normal { get; } = normal.Clone(); - public Image BaseColor { get; } = new(normal.Width, normal.Height); - public Image Specular { get; } = new(normal.Width, normal.Height); - public Image Emissive { get; } = new(normal.Width, normal.Height); + public Image BaseColor { get; } = new(index.Width, index.Height); + public Image Specular { get; } = new(index.Width, index.Height); + public Image Emissive { get; } = new(index.Width, index.Height); - private Buffer2D NormalBuffer - => Normal.Frames.RootFrame.PixelBuffer; + private Buffer2D IndexBuffer + => index.Frames.RootFrame.PixelBuffer; private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; @@ -125,66 +122,96 @@ public class MaterialExporter public void Invoke(int y) { - var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var indexSpan = IndexBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + for (var x = 0; x < indexSpan.Length; x++) + { + ref var indexPixel = ref indexSpan[x]; + + // Calculate and fetch the color table rows being used for this pixel. + var tablePair = (int) Math.Round(indexPixel.R / 17f); + var rowBlend = 1.0f - indexPixel.G / 255f; + + var prevRow = table[tablePair * 2]; + var nextRow = table[Math.Min(tablePair * 2 + 1, ColorTable.NumRows)]; + + // Lerp between table row values to fetch final pixel values for each subtexture. + var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend); + baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + + var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend); + specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); + + var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend); + emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); + } + } + } + + private readonly struct ProcessCharacterNormalOperation(Image normal) : IRowOperation + { + // TODO: Consider omitting the alpha channel here. + public Image Normal { get; } = normal.Clone(); + // TODO: We only really need the alpha here, however using A8 will result in the multiply later zeroing out the RGB channels. + public Image BaseColorOpacity { get; } = new(normal.Width, normal.Height); + + private Buffer2D NormalBuffer + => Normal.Frames.RootFrame.PixelBuffer; + + private Buffer2D BaseColorOpacityBuffer + => BaseColorOpacity.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) + { + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var baseColorOpacitySpan = BaseColorOpacityBuffer.DangerousGetRowSpan(y); + for (var x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; - // Table row data (.a) - var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = table[tableRow.Previous]; - var nextRow = table[tableRow.Next]; + baseColorOpacitySpan[x].FromVector4(Vector4.One); + baseColorOpacitySpan[x].A = normalPixel.B; - // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight); - baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); - baseColorSpan[x].A = normalPixel.B; - - // Specular (table) - var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight); - var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight); - specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); - - // Emissive (table) - var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight); - emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); - - // Normal (.rg) - // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. normalPixel.B = byte.MaxValue; normalPixel.A = byte.MaxValue; } } } - private static TableRow GetTableRowIndices(float input) + private readonly struct ProcessCharacterMaskOperation(Image mask) : IRowOperation { - // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + public Image Occlusion { get; } = new(mask.Width, mask.Height); + public Image SpecularFactor { get; } = new(mask.Width, mask.Height); - var stepped = MathF.Floor(smoothed + 0.5f); + private Buffer2D MaskBuffer + => mask.Frames.RootFrame.PixelBuffer; - return new TableRow + private Buffer2D OcclusionBuffer + => Occlusion.Frames.RootFrame.PixelBuffer; + + private Buffer2D SpecularFactorBuffer + => SpecularFactor.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) { - Stepped = (int)stepped, - Previous = (int)MathF.Floor(smoothed), - Next = (int)MathF.Ceiling(smoothed), - Weight = smoothed % 1, - }; - } + var maskSpan = MaskBuffer.DangerousGetRowSpan(y); + var occlusionSpan = OcclusionBuffer.DangerousGetRowSpan(y); + var specularFactorSpan = SpecularFactorBuffer.DangerousGetRowSpan(y); - private ref struct TableRow - { - public int Stepped; - public int Previous; - public int Next; - public float Weight; + for (var x = 0; x < maskSpan.Length; x++) + { + ref var maskPixel = ref maskSpan[x]; + + occlusionSpan[x].FromL8(new L8(maskPixel.B)); + + specularFactorSpan[x].FromVector4(Vector4.One); + specularFactorSpan[x].A = maskPixel.R; + } + } } private readonly struct MultiplyOperation @@ -218,6 +245,37 @@ public class MaterialExporter } } + private static readonly Vector4 DefaultTattooColor = new Vector4(38, 112, 102, 255) / new Vector4(255); + + private static MaterialBuilder BuildCharacterTattoo(Material material, string name) + { + var normal = material.Textures[TextureUsage.SamplerNormal]; + var baseColor = new Image(normal.Width, normal.Height); + + normal.ProcessPixelRows(baseColor, (normalAccessor, baseColorAccessor) => + { + for (var y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (var x = 0; x < normalSpan.Length; x++) + { + baseColorSpan[x].FromVector4(DefaultTattooColor); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].B = byte.MaxValue; + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(AlphaMode.BLEND); + } + // TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here. private static readonly Vector4 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); @@ -248,10 +306,11 @@ public class MaterialExporter for (var x = 0; x < normalSpan.Length; x++) { - var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f); - baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); + var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, normalSpan[x].B / 255f); + baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].A / 255f)); baseColorSpan[x].A = normalSpan[x].A; + normalSpan[x].B = byte.MaxValue; normalSpan[x].A = byte.MaxValue; } } @@ -269,26 +328,23 @@ public class MaterialExporter // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping separate for now. private static MaterialBuilder BuildIris(Material material, string name) { - var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + var baseColor = material.Textures[TextureUsage.SamplerDiffuse]; - mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + mask.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); - var baseColor = new Image(normal.Width, normal.Height); - normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + baseColor.ProcessPixelRows(mask, (baseColorAccessor, maskAccessor) => { - for (var y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < baseColor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); - for (var x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < baseColorSpan.Length; x++) { - baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f)); - baseColorSpan[x].A = normalSpan[x].A; - - normalSpan[x].A = byte.MaxValue; + var eyeColor = Vector4.Lerp(Vector4.One, DefaultEyeColor, maskSpan[x].B / 255f); + baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * eyeColor); } } }); @@ -314,21 +370,7 @@ public class MaterialExporter var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; var normal = material.Textures[TextureUsage.SamplerNormal]; - // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. - var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); - diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => - { - for (var y = 0; y < diffuseAccessor.Height; y++) - { - var diffuseSpan = diffuseAccessor.GetRowSpan(y); - var normalSpan = normalAccessor.GetRowSpan(y); - - for (var x = 0; x < diffuseSpan.Length; x++) - diffuseSpan[x].A = normalSpan[x].B; - } - }); - - // Clear the blue channel out of the normal now that we're done with it. + // The normal also stores the skin color influence (.b) and wetness mask (.a) - remove. normal.ProcessPixelRows(normalAccessor => { for (var y = 0; y < normalAccessor.Height; y++) @@ -336,7 +378,10 @@ public class MaterialExporter var normalSpan = normalAccessor.GetRowSpan(y); for (var x = 0; x < normalSpan.Length; x++) + { normalSpan[x].B = byte.MaxValue; + normalSpan[x].A = byte.MaxValue; + } } }); diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 219a046e..73160615 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -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(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() - { - { "targetNames", shapeNames }, - }); + meshBuilder.Extras = new JsonObject { ["targetNames"] = JsonSerializer.SerializeToNode(shapeNames) }; string[] attributes = []; var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); @@ -312,12 +311,10 @@ public class MeshExporter MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, - reader.ReadByte() / 255f), + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), - (float)reader.ReadHalf()), - + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.UShort4 => reader.ReadBytes(8), var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } @@ -445,7 +442,16 @@ public class MeshExporter private static Type GetSkinningType(IReadOnlyDictionary usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) - return typeof(VertexJoints4); + { + if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + { + return typeof(VertexJoints8); + } + else + { + return typeof(VertexJoints4); + } + } return typeof(VertexEmpty); } @@ -456,15 +462,17 @@ public class MeshExporter if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); - if (_skinningType == typeof(VertexJoints4)) + if (_skinningType == typeof(VertexJoints4) || _skinningType == typeof(VertexJoints8)) { if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); - var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); - - var bindings = Enumerable.Range(0, 4) + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indices = ToByteArray(indiciesData); + var weights = ToFloatArray(weightsData); + + var bindings = Enumerable.Range(0, indices.Length) .Select(bindingIndex => { // NOTE: I've not seen any files that throw this error that aren't completely broken. @@ -475,7 +483,13 @@ public class MeshExporter return (jointIndex, weights[bindingIndex]); }) .ToArray(); - return new VertexJoints4(bindings); + + return bindings.Length switch + { + 4 => new VertexJoints4(bindings), + 8 => new VertexJoints8(bindings), + _ => throw _notifier.Exception($"Invalid number of bone bindings {bindings.Length}.") + }; } throw _notifier.Exception($"Unknown skinning type {_skinningType}"); @@ -518,4 +532,13 @@ public class MeshExporter byte[] value => value, _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"), }; + + private static float[] ToFloatArray(object data) + => data switch + { + byte[] value => value.Select(x => x / 255f).ToArray(), + Vector4 v4 => new[] { v4.X, v4.Y, v4.Z, v4.W }, + _ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"), + }; } + diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 7a82e994..eff34d54 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -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> 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("_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 CustomAttributes => CustomNames; + + public IEnumerable 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> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_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 CustomAttributes => CustomNames; + + public IEnumerable 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> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_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 CustomAttributes => CustomNames; + + public IEnumerable 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)); } diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1df97907..6a46fb9f 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -194,24 +194,34 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); - var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); - - if (jointsAccessor == null || weightsAccessor == null) + var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); + var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); + var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); + var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); + + if (joints0Accessor == null || weights0Accessor == null) throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. - for (var i = 0; i < jointsAccessor.Count; i++) + for (var i = 0; i < joints0Accessor.Count; i++) { - var joints = jointsAccessor[i]; - var weights = weightsAccessor[i]; + var joints0 = joints0Accessor[i]; + var weights0 = weights0Accessor[i]; + var joints1 = joints1Accessor?[i]; + var weights1 = weights1Accessor?[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. - if (weights[index] == 0) - continue; + if (weights0[index] != 0) + { + usedJoints.Add((ushort)joints0[index]); + } - usedJoints.Add((ushort)joints[index]); + + if (joints1 != null && weights1 != null && weights1.Value[index] != 0) + { + usedJoints.Add((ushort)joints1.Value[index]); + } } } } diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index e81bb622..df08eea3 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -61,7 +61,7 @@ public class SubMeshImporter try { - _morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize>(); + _morphNames = node.Mesh.Extras["targetNames"].Deserialize>(); } catch { diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index af401ec1..a1c3246b 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -40,6 +40,7 @@ public class VertexAttribute MdlFile.VertexType.NByte4 => 4, MdlFile.VertexType.Half2 => 4, MdlFile.VertexType.Half4 => 8, + MdlFile.VertexType.UShort4 => 8, _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; @@ -121,87 +122,120 @@ public class VertexAttribute public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier) { - if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) return null; if (!accessors.ContainsKey("JOINTS_0")) throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); + if (accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) + { + if (!accessors.ContainsKey("JOINTS_1")) + throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute."); + } + var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, }; - var values = accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + var weights1 = weights1Accessor?.AsVector4Array(); return new VertexAttribute( element, - index => { - // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off - // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak - // the converted values to have the expected sum, preferencing values with minimal differences. - var originalValues = values[index]; - var byteValues = BuildNByte4(originalValues); - - var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); - while (adjustment != 0) - { - var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); - var closestIndex = Enumerable.Range(0, 4) - .Where(index => { - var byteValue = byteValues[index]; - if (adjustment < 0) return byteValue > 0; - if (adjustment > 0) return byteValue < 255; - return true; - }) - .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) - .MinBy(x => x.delta) - .index; - byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); - adjustment = 255 - byteValues.Select(value => (int)value).Sum(); - } - - return byteValues; - } + index => BuildBlendWeights(weights0[index], weights1?[index] ?? Vector4.Zero) ); } + + private static byte[] BuildBlendWeights(Vector4 v1, Vector4 v2) + { + var originalData = BuildUshort4(v1, v2); + var byteValues = new byte[originalData.Length]; + for (var i = 0; i < originalData.Length; i++) + { + byteValues[i] = (byte)Math.Round(originalData[i] * 255f); + } + + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off + // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak + // the converted values to have the expected sum, preferencing values with minimal differences. + var adjustment = 255 - byteValues.Sum(value => value); + while (adjustment != 0) + { + var closestIndex = Enumerable.Range(0, byteValues.Length) + .Where(i => adjustment switch + { + < 0 => byteValues[i] > 0, + > 0 => byteValues[i] < 255, + _ => true, + }) + .Select(index => (index, delta: Math.Abs(originalData[index] - (byteValues[index] * (1f / 255f))))) + .MinBy(x => x.delta) + .index; + byteValues[closestIndex] += (byte)Math.CopySign(1, adjustment); + adjustment = 255 - byteValues.Sum(value => value); + } + + return byteValues; + } public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { - if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor)) + if (!accessors.TryGetValue("JOINTS_0", out var joints0Accessor)) return null; - if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); - var element = new MdlStructs.VertexElement() + var joints0 = joints0Accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + accessors.TryGetValue("JOINTS_1", out var joints1Accessor); + accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor); + var element = new MdlStructs.VertexElement { Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, + Type = (byte)MdlFile.VertexType.UShort4, Usage = (byte)MdlFile.VertexUsage.BlendIndices, }; - var joints = jointsAccessor.AsVector4Array(); - var weights = weightsAccessor.AsVector4Array(); + var joints1 = joints1Accessor?.AsVector4Array(); + var weights1 = weights1Accessor?.AsVector4Array(); return new VertexAttribute( element, index => { - var gltfIndices = joints[index]; - var gltfWeights = weights[index]; + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var gltfIndices1 = joints1?[index]; + var gltfWeights1 = weights1?[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + + var v1 = Vector4.Zero; + if (gltfIndices1 != null && gltfWeights1 != null) + { + v1 = new Vector4( + gltfWeights1.Value.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.X], + gltfWeights1.Value.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Y], + gltfWeights1.Value.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Z], + gltfWeights1.Value.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.W] + ); + } - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); } ); } @@ -232,7 +266,7 @@ public class VertexAttribute var value = values[vertexIndex]; var delta = morphValues[morphIndex]?[vertexIndex]; - if (delta != null) + if (delta != null) value += delta.Value; return BuildSingle3(value); @@ -489,4 +523,11 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; + + private static float[] BuildUshort4(Vector4 v0, Vector4 v1) => + new[] + { + v0.X, v0.Y, v0.Z, v0.W, + v1.X, v1.Y, v1.Z, v1.W, + }; } diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index ba089662..fed06573 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -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) { diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index dea343c6..febbe179 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -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."); } diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index a069204c..f145f560 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -46,21 +46,28 @@ public partial class TexToolsImporter { ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; - ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); + if (_currentOptionIdx >= _currentNumOptions) + ImGui.ProgressBar(1f, size, $"Extracted {_currentNumOptions} Options"); + else + ImGui.ProgressBar(_currentOptionIdx / (float)_currentNumOptions, size, + $"Extracting Option {_currentOptionIdx + 1} / {_currentNumOptions}..."); + ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) ImGui.TextUnformatted( - $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); + $"Extracting Option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); } ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; - ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + if (_currentFileIdx >= _currentNumFiles) + ImGui.ProgressBar(1f, size, $"Extracted {_currentNumFiles} Files"); + else + ImGui.ProgressBar(_currentFileIdx / (float)_currentNumFiles, size, $"Extracting File {_currentFileIdx + 1} / {_currentNumFiles}..."); + ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + ImGui.TextUnformatted($"Extracting File {_currentFileName}..."); return false; } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 3ae1eda9..7bbb762e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -151,6 +151,7 @@ public partial class TexToolsImporter _currentGroupName = string.Empty; _currentOptionName = "Default"; ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList); + ++_currentOptionIdx; } // Iterate through all pages @@ -208,6 +209,7 @@ public partial class TexToolsImporter options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); + ++_currentOptionIdx; } } diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 98b87ac3..c1a22088 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -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( diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index ae4a39c0..220095c1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -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, diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c5207e94..ae0aabd9 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -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 diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index c83604e4..b0a65ac0 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -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(); diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index cc785d02..7118f8af 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -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? png = null; + Image? 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); - /// Load a .png file from drive using ImageSharp. - public BaseImage LoadPng(string path) + /// Load a supported file type from drive using ImageSharp. + public BaseImage LoadImageSharp(string path) { using var stream = File.OpenRead(path); return Image.Load(stream); } - /// Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. + /// Convert an existing image to ImageSharp. Does not create a deep copy of an existing ImageSharp file and just returns the existing one. 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", }; diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index b1b23f27..d55caf34 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -3,7 +3,10 @@ using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; namespace Penumbra.Interop.Hooks.Resources; @@ -212,30 +215,74 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + [StructLayout(LayoutKind.Explicit)] + private struct ChangedEquipData + { + [FieldOffset(0)] + public PrimaryId Model; + + [FieldOffset(2)] + public Variant Variant; + + [FieldOffset(8)] + public PrimaryId BonusModel; + + [FieldOffset(10)] + public Variant BonusVariant; + + [FieldOffset(20)] + public ushort VfxId; + + [FieldOffset(22)] + public GenderRace GenderRace; + } + private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex is <= 4 or >= 10) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + switch (slotIndex) + { + case <= 4: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + case <= 10: + { + // Enable vfxs for accessories + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var changedEquipData = ((Human*)drawObject)->ChangedEquipData; - // Enable vfxs for accessories - if (changedEquipData == null) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + ref var slot = ref changedEquipData[slotIndex]; - var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex); - var model = slot[0]; - var variant = slot[1]; - var vfxId = slot[4]; + if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (model == 0 || variant == 0 || vfxId == 0) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx\0", - out _)) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + *(ulong*)unkOutParam = 4; + return ResolvePath(drawObject, pathBuffer); + } + case 16: + { + // Enable vfxs for glasses + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - *(ulong*)unkOutParam = 4; - return ResolvePath(drawObject, pathBuffer); + ref var slot = ref changedEquipData[slotIndex - 6]; + + if (slot.BonusModel == 0 || slot.BonusVariant == 0 || slot.VfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/equipment/e{slot.BonusModel.Id:D4}/vfx/eff/ve{slot.VfxId:D4}.avfx\0", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + *(ulong*)unkOutParam = 4; + return ResolvePath(drawObject, pathBuffer); + } + default: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + } } private nint VFunc81(nint drawObject, int estType, nint unk) diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 61ccc95c..c459a67a 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -40,8 +40,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture.Texture == null) throw new InvalidOperationException("Material doesn't have a color table"); - Width = (int)_originalColorTableTexture.Texture->Width; - Height = (int)_originalColorTableTexture.Texture->Height; + Width = (int)_originalColorTableTexture.Texture->ActualWidth; + Height = (int)_originalColorTableTexture.Texture->ActualHeight; ColorTable = new Half[Width * Height * 4]; _updatePending = true; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 43324516..79f97881 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -36,13 +36,13 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); + ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) + : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } private GenderRace ResolveModelRaceCode() - => ResolveEqdpRaceCode(Slot, Equipment.Set); + => ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set); private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { @@ -161,7 +161,7 @@ internal partial record ResolveContext return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot.ToSlot(), variant, out var exists); if (!exists) return variant.Id; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index b99ee235..54612070 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -30,7 +30,7 @@ internal record GlobalResolveContext( public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, - EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) + FullEquipType slot = FullEquipType.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } @@ -38,7 +38,7 @@ internal unsafe partial record ResolveContext( GlobalResolveContext Global, Pointer CharacterBasePointer, uint SlotIndex, - EquipSlot Slot, + FullEquipType Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { @@ -340,19 +340,20 @@ internal unsafe partial record ResolveContext( internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) { - var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); + var path = gamePath.Path.Split((byte)'/'); // Weapons intentionally left out. - var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment"; + var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { - var name = Slot switch + var name = item.Name; + if (Slot is FullEquipType.Finger) + name = SlotIndex switch { - EquipSlot.RFinger => "R: ", - EquipSlot.LFinger => "L: ", - _ => string.Empty, - } - + item.Name; + 8 => "R: " + name, + 9 => "L: " + name, + _ => name, + }; return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag()); } @@ -361,7 +362,7 @@ internal unsafe partial record ResolveContext( return dataFromPath; return isEquipment - ? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag()) + ? new ResourceNode.UiData(Slot.ToName(), Slot.GetCategoryIcon().ToFlag()) : new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 85d12ce7..6c3e1ebe 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -7,20 +7,20 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceNode : ICloneable { - public string? Name; - public string? FallbackName; - public ChangedItemIconFlag IconFlag; - public readonly ResourceType Type; - public readonly nint ObjectAddress; - public readonly nint ResourceHandle; - public Utf8GamePath[] PossibleGamePaths; - public FullPath FullPath; - public string? ModName; - public string? ModRelativePath; - public CiByteString AdditionalData; - public readonly ulong Length; - public readonly List Children; - internal ResolveContext? ResolveContext; + public string? Name; + public string? FallbackName; + public ChangedItemIconFlag IconFlag; + public readonly ResourceType Type; + public readonly nint ObjectAddress; + public readonly nint ResourceHandle; + public Utf8GamePath[] PossibleGamePaths; + public FullPath FullPath; + public string? ModName; + public string? ModRelativePath; + public CiByteString AdditionalData; + public readonly ulong Length; + public readonly List Children; + internal ResolveContext? ResolveContext; public Utf8GamePath GamePath { @@ -53,7 +53,7 @@ public class ResourceNode : ICloneable { Name = other.Name; FallbackName = other.FallbackName; - IconFlag = other.IconFlag; + IconFlag = other.IconFlag; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; @@ -82,7 +82,7 @@ public class ResourceNode : ICloneable public void SetUiData(UiData uiData) { - Name = uiData.Name; + Name = uiData.Name; IconFlag = uiData.IconFlag; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 38f6fe97..89e0c62b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -80,12 +80,13 @@ public class ResourceTree { ModelType.Human => i switch { - < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]), - 16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]), - _ => globalContext.CreateContext(model, i), + < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]), + 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), + 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), + _ => globalContext.CreateContext(model, i), }, _ => i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + ? globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]) : globalContext.CreateContext(model, i), }; @@ -133,7 +134,7 @@ public class ResourceTree var weapon = (Weapon*)subObject; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. - var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; + var slot = weaponIndex > 0 ? FullEquipType.UnknownOffhand : FullEquipType.UnknownMainhand; var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1)); var weaponType = weapon->SecondaryId; @@ -184,7 +185,7 @@ public class ResourceTree { pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.IconFlag = ChangedItemIconFlag.Customization; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(pbdNode); @@ -202,7 +203,7 @@ public class ResourceTree { decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; - decalNode.IconFlag = ChangedItemIconFlag.Customization; + decalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(decalNode); @@ -219,7 +220,7 @@ public class ResourceTree { legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; - legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; + legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(legacyDecalNode); diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 4e3ff11b..a415c9b0 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -6,9 +6,9 @@ namespace Penumbra.Meta; public class ImcChecker { - private static readonly Dictionary VariantCounts = []; - private static MetaFileManager? _dataManager; - + private static readonly Dictionary VariantCounts = []; + private static MetaFileManager? _dataManager; + private static readonly ConcurrentDictionary GlobalCachedDefaultEntries = []; public static int GetVariantCount(ImcIdentifier identifier) { @@ -26,23 +26,20 @@ public class ImcChecker public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); - private readonly Dictionary _cachedDefaultEntries = new(); - private readonly MetaFileManager _metaFileManager; - public ImcChecker(MetaFileManager metaFileManager) - { - _metaFileManager = metaFileManager; - _dataManager = metaFileManager; - } + => _dataManager = metaFileManager; - public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) + public static CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) { - if (_cachedDefaultEntries.TryGetValue(identifier, out var entry)) + if (GlobalCachedDefaultEntries.TryGetValue(identifier, out var entry)) return entry; + if (_dataManager == null) + return new CachedEntry(default, false, false); + try { - var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + var e = ImcFile.GetDefault(_dataManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); entry = new CachedEntry(e, true, entryExists); } catch (Exception) @@ -51,7 +48,7 @@ public class ImcChecker } if (storeCache) - _cachedDefaultEntries.Add(identifier, entry); + GlobalCachedDefaultEntries.TryAdd(identifier, entry); return entry; } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 1b2492ee..cba6c379 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -68,9 +68,13 @@ public readonly record struct ImcIdentifier( => (MetaIndex)(-1); public override string ToString() - => ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}" - : $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}"; + => ObjectType switch + { + ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}", + ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}", + _ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}", + }; + public bool Validate() { @@ -102,6 +106,7 @@ public readonly record struct ImcIdentifier( return false; if (ItemData.AdaptOffhandImc(PrimaryId, out _)) return false; + break; } @@ -163,7 +168,7 @@ public readonly record struct ImcIdentifier( case ObjectType.DemiHuman: { var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); - var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var slot = jObj["EquipSlot"]?.ToObject() ?? EquipSlot.Unknown; ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown); break; } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 1093c6c5..70d4fd47 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -69,6 +69,7 @@ public class MetaDictionary public void Clear() { + Count = 0; _imc.Clear(); _eqp.Clear(); _eqdp.Clear(); diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index bcecf264..56a19766 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -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(); diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index cacb7f88..19ca7022 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -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(); + }); } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 241f5b3b..20423493 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -12,6 +12,7 @@ public class ModFileCollection : IDisposable, IService private readonly List _mdl = []; private readonly List _tex = []; private readonly List _shpk = []; + private readonly List _pbd = []; private readonly SortedSet _missing = []; private readonly HashSet _usedPaths = []; @@ -23,19 +24,22 @@ public class ModFileCollection : IDisposable, IService => Ready ? _usedPaths : []; public IReadOnlyList Available - => Ready ? _available : Array.Empty(); + => Ready ? _available : []; public IReadOnlyList Mtrl - => Ready ? _mtrl : Array.Empty(); + => Ready ? _mtrl : []; public IReadOnlyList Mdl - => Ready ? _mdl : Array.Empty(); + => Ready ? _mdl : []; public IReadOnlyList Tex - => Ready ? _tex : Array.Empty(); + => Ready ? _tex : []; public IReadOnlyList Shpk - => Ready ? _shpk : Array.Empty(); + => Ready ? _shpk : []; + + public IReadOnlyList Pbd + => Ready ? _pbd : []; public bool Ready { get; private set; } = true; @@ -128,6 +132,9 @@ public class ModFileCollection : IDisposable, IService case ".shpk": _shpk.Add(registry); break; + case ".pbd": + _pbd.Add(registry); + break; } } } @@ -139,6 +146,7 @@ public class ModFileCollection : IDisposable, IService _mdl.Clear(); _tex.Clear(); _shpk.Clear(); + _pbd.Clear(); } private void ClearPaths(bool clearRegistries, CancellationToken tok) diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 55e0e94e..3b765215 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -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); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index b059813b..e3eb5f54 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -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 Warnings = new List(); 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; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index bacf4122..64c585ea 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,12 +1,16 @@ 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) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -62,12 +66,111 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService Changes = false; } + public static bool DeleteDefaultValues(MetaFileManager metaFileManager, 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, this); + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, this); + groupEditor.SetManipulations(container, this); Changes = false; } } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 43cfc1ee..3e367a3b 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -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> paths = []; diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index c5654019..a6f6e20d 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -24,14 +24,21 @@ public interface IModGroup { public const int MaxMultiOptions = 32; - public Mod Mod { get; } - public string Name { get; set; } - public string Description { get; set; } - public string Image { get; set; } - public GroupType Type { get; } - public GroupDrawBehaviour Behaviour { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; set; } + public string Description { get; set; } + + /// Unused in Penumbra but for better TexTools interop. + public string Image { get; set; } + + public GroupType Type { get; } + public GroupDrawBehaviour Behaviour { get; } + public ModPriority Priority { get; set; } + + /// Unused in Penumbra but for better TexTools interop. + public int Page { get; set; } + + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); public IModOption? AddOption(string name, string description = ""); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index d42804ba..2a1854ed 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -29,11 +29,13 @@ public class ImcModGroup(Mod mod) : IModGroup => GroupDrawBehaviour.MultiSelection; public ModPriority Priority { get; set; } = ModPriority.Default; + public int Page { get; set; } public Setting DefaultSettings { get; set; } = Setting.Zero; public ImcIdentifier Identifier; public ImcEntry DefaultEntry; public bool AllVariants; + public bool OnlyAttributes; public FullPath? FindBestMatch(Utf8GamePath gamePath) @@ -96,28 +98,36 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcEntry GetEntry(ushort mask) - => DefaultEntry with { AttributeMask = mask }; + private ImcEntry GetEntry(Variant variant, ushort mask) + { + if (!OnlyAttributes) + return DefaultEntry with { AttributeMask = mask }; + + var defaultEntry = ImcChecker.GetDefaultEntry(Identifier with { Variant = variant }, true); + if (defaultEntry.VariantExists) + return defaultEntry.Entry with { AttributeMask = mask }; + + return DefaultEntry with { AttributeMask = mask }; + } public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; - var mask = GetCurrentMask(setting); - var entry = GetEntry(mask); + var mask = GetCurrentMask(setting); if (AllVariants) { var count = ImcChecker.GetVariantCount(Identifier); if (count == 0) - manipulations.TryAdd(Identifier, entry); + manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask)); else for (var i = 0; i <= count; ++i) - manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry); + manipulations.TryAdd(Identifier with { Variant = (Variant)i }, GetEntry((Variant)i, mask)); } else { - manipulations.TryAdd(Identifier, entry); + manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask)); } } @@ -137,6 +147,8 @@ public class ImcModGroup(Mod mod) : IModGroup serializer.Serialize(jWriter, DefaultEntry); jWriter.WritePropertyName(nameof(AllVariants)); jWriter.WriteValue(AllVariants); + jWriter.WritePropertyName(nameof(OnlyAttributes)); + jWriter.WriteValue(OnlyAttributes); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) @@ -169,14 +181,11 @@ public class ImcModGroup(Mod mod) : IModGroup var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); var ret = new ImcModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), - AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, + OnlyAttributes = json[nameof(OnlyAttributes)]?.ToObject() ?? false, }; - if (ret.Name.Length == 0) + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0) @@ -215,7 +224,6 @@ public class ImcModGroup(Mod mod) : IModGroup } ret.Identifier = identifier.Value; - ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero; ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index c465822b..bda70b54 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -1,4 +1,7 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; @@ -90,6 +93,8 @@ public readonly struct ModSaveGroup : ISavable jWriter.WriteValue(group.Description); jWriter.WritePropertyName(nameof(group.Image)); jWriter.WriteValue(group.Image); + jWriter.WritePropertyName(nameof(group.Page)); + jWriter.WriteValue(group.Page); jWriter.WritePropertyName(nameof(group.Priority)); jWriter.WriteValue(group.Priority.Value); jWriter.WritePropertyName(nameof(group.Type)); @@ -97,4 +102,16 @@ public readonly struct ModSaveGroup : ISavable jWriter.WritePropertyName(nameof(group.DefaultSettings)); jWriter.WriteValue(group.DefaultSettings.Value); } + + public static bool ReadJsonBase(JObject json, IModGroup group) + { + group.Name = json[nameof(IModGroup.Name)]?.ToObject() ?? string.Empty; + group.Description = json[nameof(IModGroup.Description)]?.ToObject() ?? string.Empty; + group.Image = json[nameof(IModGroup.Image)]?.ToObject() ?? string.Empty; + group.Page = json[nameof(IModGroup.Page)]?.ToObject() ?? 0; + group.Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + group.DefaultSettings = json[nameof(IModGroup.DefaultSettings)]?.ToObject() ?? Setting.Zero; + + return group.Name.Length > 0; + } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 9cf7e6a3..0c9aa805 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -28,6 +28,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public string Description { get; set; } = string.Empty; public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } + public int Page { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -66,15 +67,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public static MultiModGroup? Load(Mod mod, JObject json) { - var ret = new MultiModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - }; - if (ret.Name.Length == 0) + var ret = new MultiModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; var options = json["Options"]; @@ -105,6 +99,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup Name = Name, Description = Description, Priority = Priority, + Image = Image, + Page = Page, DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single))); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 723cd5b1..ab0c2d4f 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -26,6 +26,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public string Description { get; set; } = string.Empty; public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } + public int Page { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -62,15 +63,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public static SingleModGroup? Load(Mod mod, JObject json) { var options = json["Options"]; - var ret = new SingleModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - }; - if (ret.Name.Length == 0) + var ret = new SingleModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; if (options != null) @@ -91,6 +85,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup Name = Name, Description = Description, Priority = Priority, + Image = Image, + Page = Page, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i)))); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 1a2f2798..c7e43a26 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -32,26 +32,26 @@ public static class EquipmentSwap EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot()) throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); var mtrlVariantTo = imcEntry.MaterialId; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -94,7 +94,7 @@ public static class EquipmentSwap { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); if (slotFrom != slotTo) throw new ItemSwap.InvalidItemTypeException(); @@ -111,7 +111,7 @@ public static class EquipmentSwap { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); @@ -122,18 +122,18 @@ public static class EquipmentSwap { EquipSlot.Head => EstType.Head, EquipSlot.Body => EstType.Body, - _ => (EstType)0, + _ => (EstType)0, }; var skipFemale = false; - var skipMale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -148,7 +148,7 @@ public static class EquipmentSwap swaps.Add(eqdp); var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; - var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); + var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); } @@ -176,7 +176,6 @@ public static class EquipmentSwap return affectedItems; } - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); @@ -186,9 +185,9 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); - var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); - var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, eqdpFromDefault, eqdpToIdentifier, eqdpToDefault); @@ -217,7 +216,7 @@ public static class EquipmentSwap ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -242,13 +241,13 @@ public static class EquipmentSwap private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var imc = new ImcFile(manager, ident); + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); EquipItem[] items; - Variant[] variants; + Variant[] variants; if (idFrom == idTo) { - items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); + items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); variants = [variantFrom]; } else @@ -271,9 +270,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new GmpIdentifier(idFrom); - var manipToIdentifier = new GmpIdentifier(idTo); - var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -288,9 +287,9 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); @@ -329,7 +328,7 @@ public static class EquipmentSwap var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { @@ -347,9 +346,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new EqpIdentifier(idFrom, slot); - var manipToIdentifier = new EqpIdentifier(idTo, slot); - var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); - var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -381,7 +380,7 @@ public static class EquipmentSwap if (newFileName != fileName) { - fileName = newFileName; + fileName = newFileName; dataWasChanged = true; } @@ -406,13 +405,13 @@ public static class EquipmentSwap EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); - var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } @@ -430,8 +429,8 @@ public static class EquipmentSwap PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); - filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); + filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7a0467d0..162f823d 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -37,7 +37,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - saveService.ImmediateSave(new ModMeta(mod)); + saveService.ImmediateSaveSync(new ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) @@ -249,6 +249,17 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } + public void ResetModImportDate(Mod mod) + { + var newDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (mod.ImportDate == newDate) + return; + + mod.ImportDate = newDate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.ImportDate, mod, null); + } + public void ChangeModNote(Mod mod, string newNote) { if (mod.Note == newNote) diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index d984d374..22cc0c86 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -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; } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 59f8906e..bf1b6637 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -81,13 +81,13 @@ public sealed class ModManager : ModStorage, IDisposable, IService } /// Load a new mod and add it to the manager if successful. - 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(); 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); }); diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index c7eb7cc5..3e58c515 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -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; } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 4aae45a2..dc94c881 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -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; @@ -90,6 +89,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); } + public void ChangeOnlyAttributes(ImcModGroup group, bool onlyAttributes, SaveType saveType = SaveType.Queue) + { + if (group.OnlyAttributes == onlyAttributes) + return; + + group.OnlyAttributes = onlyAttributes; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) { if (group.CanBeDisabled == canBeDisabled) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 712630c6..7f18852d 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -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); } /// Forces a file save of the given container's group. 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)); /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary 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); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0f4972e3..1af9c1db 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -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,11 @@ 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) : IService { public readonly Configuration Config = config; @@ -34,7 +35,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 +47,7 @@ public partial class ModCreator( } /// Load a mod by its directory. - public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges) + public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges, bool deleteDefaultMetaChanges) { modPath.Refresh(); if (!modPath.Exists) @@ -56,7 +57,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 +66,29 @@ public partial class ModCreator( } /// Reload a mod from its mod path. - 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); + modDataChange |= 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, container.Manipulations)) + saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); + } + } return true; } @@ -89,13 +98,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 +115,13 @@ public partial class ModCreator( } if (changes) - _saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); } /// Load the default option for a given mod. 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 +166,7 @@ public partial class ModCreator( List 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 +177,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 +186,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. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -194,7 +203,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 +216,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 +232,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, option.Manipulations); + + return (changes, deleteList); } /// @@ -250,7 +263,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 +273,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 +290,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 +302,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)); } /// Return the name of a new valid directory based on the base directory and the given name. @@ -333,7 +347,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) diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs new file mode 100644 index 00000000..73d0272b --- /dev/null +++ b/Penumbra/Mods/ModSelection.cs @@ -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; + +/// +/// Triggered whenever the selected mod changes +/// +/// Parameter is the old selected mod. +/// Parameter is the new selected mod +/// +/// +public class ModSelection : EventWrapper +{ + 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 + { + /// + ModPanel = 0, + + /// + ModMerger = 0, + } +} diff --git a/Penumbra/Mods/Settings/ModPriority.cs b/Penumbra/Mods/Settings/ModPriority.cs index 993bd577..cf234c00 100644 --- a/Penumbra/Mods/Settings/ModPriority.cs +++ b/Penumbra/Mods/Settings/ModPriority.cs @@ -66,4 +66,10 @@ public readonly record struct ModPriority(int Value) : public int CompareTo(ModPriority other) => Value.CompareTo(other.Value); + + public const int HiddenMin = -84037; + public const int HiddenMax = HiddenMin + 1000; + + public bool IsHidden + => Value is > HiddenMin and < HiddenMax; } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e4049482..b5499624 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -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}."); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6f0b63ce..b6b19ef2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -114,7 +114,7 @@ public class Penumbra : IDalamudPlugin var itemSheet = _services.GetService().GetExcelSheet()!; _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); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 8e143e3c..9b613729 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -86,11 +86,13 @@ - + + + - - - + + + diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 9041fbd0..aa2d445e 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -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); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index b75c5aef..3f7f2f6c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -48,15 +48,16 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Glasses] = (new ItemSelector(itemService, selector, FullEquipType.Glasses), new ItemSelector(itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), // @formatter:on }; @@ -129,6 +130,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Ears, Tail, Weapon, + Glasses, } private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) @@ -158,14 +160,14 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private ModSettings? _modSettings; private bool _dirty; - private SwapType _lastTab = SwapType.Hair; - private Gender _currentGender = Gender.Male; - private ModelRace _currentRace = ModelRace.Midlander; - private int _targetId; - private int _sourceId; - private Exception? _loadException; - private EquipSlot _slotFrom = EquipSlot.Head; - private EquipSlot _slotTo = EquipSlot.Ears; + private SwapType _lastTab = SwapType.Hair; + private Gender _currentGender = Gender.Male; + private ModelRace _currentRace = ModelRace.Midlander; + private int _targetId; + private int _sourceId; + private Exception? _loadException; + private BetweenSlotTypes _slotFrom = BetweenSlotTypes.Hat; + private BetweenSlotTypes _slotTo = BetweenSlotTypes.Earrings; private string _newModName = string.Empty; private string _newGroupName = "Swaps"; @@ -200,18 +202,19 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Necklace: case SwapType.Bracelet: case SwapType.Ring: + case SwapType.Glasses: var values = _selectors[_lastTab]; if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); - break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item, _slotFrom, selectorFrom.CurrentSelection.Item, + _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom), + selectorFrom.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -264,7 +267,23 @@ public class ItemSwapTab : IDisposable, ITab, IUiService } private string CreateDescription() - => $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + { + switch (_lastTab) + { + case SwapType.Ears: + case SwapType.Face: + case SwapType.Hair: + case SwapType.Tail: + return + $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + case SwapType.BetweenSlots: + return + $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}."; + default: + return + $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}."; + } + } private void UpdateOption() { @@ -281,7 +300,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)) @@ -416,6 +435,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService DrawEquipmentSwap(SwapType.Necklace); DrawEquipmentSwap(SwapType.Bracelet); DrawEquipmentSwap(SwapType.Ring); + DrawEquipmentSwap(SwapType.Glasses); DrawAccessorySwap(); DrawHairSwap(); //DrawFaceSwap(); @@ -454,23 +474,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - using (var combo = ImRaii.Combo("##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName())) + using (var combo = ImRaii.Combo("##fromType", ToName(_slotFrom))) { if (combo) - foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head)) + foreach (var slot in Enum.GetValues()) { - if (!ImGui.Selectable(slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom) || slot == _slotFrom) + if (!ImGui.Selectable(ToName(slot), slot == _slotFrom) || slot == _slotFrom) continue; _dirty = true; _slotFrom = slot; if (slot == _slotTo) - _slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s); + _slotTo = AvailableToTypes.First(s => slot != s); } } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, + InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -480,12 +501,12 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - using (var combo = ImRaii.Combo("##toType", _slotTo.ToName())) + using (var combo = ImRaii.Combo("##toType", ToName(_slotTo))) { if (combo) - foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom)) + foreach (var slot in AvailableToTypes.Where(t => t != _slotFrom)) { - if (!ImGui.Selectable(slot.ToName(), slot == _slotTo) || slot == _slotTo) + if (!ImGui.Selectable(ToName(slot), slot == _slotTo) || slot == _slotTo) continue; _dirty = true; @@ -508,17 +529,18 @@ public class ItemSwapTab : IDisposable, ITab, IUiService .Select(i => i.Name))); } - private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) + private (string, string, ItemSelector) GetAccessorySelector(BetweenSlotTypes slot, bool source) { var (type, article1, article2) = slot switch { - EquipSlot.Head => (SwapType.Hat, "this", "it"), - EquipSlot.Ears => (SwapType.Earrings, "these", "them"), - EquipSlot.Neck => (SwapType.Necklace, "this", "it"), - EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"), - EquipSlot.RFinger => (SwapType.Ring, "this", "it"), - EquipSlot.LFinger => (SwapType.Ring, "this", "it"), - _ => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.Hat => (SwapType.Hat, "this", "it"), + BetweenSlotTypes.Earrings => (SwapType.Earrings, "these", "them"), + BetweenSlotTypes.Necklace => (SwapType.Necklace, "this", "it"), + BetweenSlotTypes.Bracelets => (SwapType.Bracelet, "these", "them"), + BetweenSlotTypes.RightRing => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.LeftRing => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.Glasses => (SwapType.Glasses, "these", "them"), + _ => (SwapType.Ring, "this", "it"), }; var (itemSelector, target, _, _) = _selectors[type]; return (article1, article2, source ? itemSelector : target); @@ -689,6 +711,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService SwapType.Necklace => "One of the selected necklaces does not seem to exist.", SwapType.Bracelet => "One of the selected bracelets does not seem to exist.", SwapType.Ring => "One of the selected rings does not seem to exist.", + SwapType.Glasses => "One of the selected glasses does not seem to exist.", SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", @@ -746,4 +769,44 @@ public class ItemSwapTab : IDisposable, ITab, IUiService UpdateOption(); _dirty = true; } + + private enum BetweenSlotTypes + { + Hat, + Earrings, + Necklace, + Bracelets, + RightRing, + LeftRing, + Glasses, + } + + private static EquipSlot ToEquipSlot(BetweenSlotTypes type) + => type switch + { + BetweenSlotTypes.Hat => EquipSlot.Head, + BetweenSlotTypes.Earrings => EquipSlot.Ears, + BetweenSlotTypes.Necklace => EquipSlot.Neck, + BetweenSlotTypes.Bracelets => EquipSlot.Wrists, + BetweenSlotTypes.RightRing => EquipSlot.RFinger, + BetweenSlotTypes.LeftRing => EquipSlot.LFinger, + BetweenSlotTypes.Glasses => BonusItemFlag.Glasses.ToEquipSlot(), + _ => EquipSlot.Unknown, + }; + + private static string ToName(BetweenSlotTypes type) + => type switch + { + BetweenSlotTypes.Hat => "Hat", + BetweenSlotTypes.Earrings => "Earrings", + BetweenSlotTypes.Necklace => "Necklace", + BetweenSlotTypes.Bracelets => "Bracelets", + BetweenSlotTypes.RightRing => "Right Ring", + BetweenSlotTypes.LeftRing => "Left Ring", + BetweenSlotTypes.Glasses => "Glasses", + _ => "Unknown", + }; + + private static readonly IReadOnlyList AvailableToTypes = + Enum.GetValues().Where(s => s is not BetweenSlotTypes.Hat).ToArray(); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 2d98c9d8..8aebaeec 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -67,7 +67,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService _textureSize = _firstNonNullTexture switch { null => Vector2.Zero, - _ => new Vector2(_firstNonNullTexture->CsHandle.Texture->Width, _firstNonNullTexture->CsHandle.Texture->Height).Contain(new Vector2(MaximumTextureSize)), + _ => new Vector2(_firstNonNullTexture->CsHandle.Texture->ActualWidth, _firstNonNullTexture->CsHandle.Texture->ActualHeight).Contain(new Vector2(MaximumTextureSize)), }; if (float.IsNaN(itemHeight)) @@ -192,10 +192,10 @@ public sealed unsafe class MaterialTemplatePickers : IUiService continue; var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; - var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + var size = new Vector2(texture->ActualWidth, texture->ActualHeight).Contain(itemSize); position += (itemSize - size) * 0.5f; ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, - new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + new Vector2(texture->ActualWidth / (float)texture->AllocatedWidth, texture->ActualHeight / (float)texture->AllocatedHeight)); } } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 5206ece8..f9baddbe 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -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) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 56c06bc9..51b14459 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -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) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 5c3c5df5..09075319 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -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) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 130831a0..1aa9060e 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -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) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 87ed21dc..9532d8e7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -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) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 58f626fc..c8310cf7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -30,7 +30,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } private void UpdateEntry() - => (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true); + => (Entry, _fileExists, _) = ImcChecker.GetDefaultEntry(Identifier, true); protected override void DrawNew() { @@ -54,7 +54,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile DrawMetaButtons(identifier, entry); DrawIdentifier(identifier); - var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; if (DrawEntry(defaultEntry, ref entry, true)) Editor.Changes |= Editor.Update(identifier, entry); } @@ -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, }; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 229526c4..75de20a7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -41,12 +41,14 @@ public abstract class MetaDrawer(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 Label { get; } @@ -57,6 +59,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); + protected abstract int Count { get; } /// diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index be02e321..87e8c5b8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -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) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs new file mode 100644 index 00000000..258e51ff --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -0,0 +1,354 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private readonly FileEditor _pbdTab; + private readonly PbdData _pbdData = new(); + + private bool DrawDeformerPanel(PbdTab tab, bool disabled) + { + _pbdData.Update(tab.File); + DrawGenderRaceSelector(tab); + ImGui.SameLine(); + DrawBoneSelector(); + ImGui.SameLine(); + return DrawBoneData(tab, disabled); + } + + private void DrawGenderRaceSelector(PbdTab tab) + { + using var group = ImUtf8.Group(); + var width = ImUtf8.CalcTextSize("Hellsguard - Female (Child)____0000"u8).X + 2 * ImGui.GetStyle().WindowPadding.X; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(width); + ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8); + } + + using var child = ImUtf8.Child("GenderRace"u8, + new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true); + if (!child) + return; + + var metaColor = ColorId.ItemId.Value(); + foreach (var (deformer, index) in tab.File.Deformers.WithIndex()) + { + var name = deformer.GenderRace.ToName(); + var raceCode = deformer.GenderRace.ToRaceCode(); + // No clipping necessary since this are not that many objects anyway. + if (!name.Contains(_pbdData.RaceCodeFilter) && !raceCode.Contains(_pbdData.RaceCodeFilter)) + continue; + + using var id = ImUtf8.PushId(index); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), deformer.RacialDeformer.IsEmpty); + if (ImUtf8.Selectable(name, deformer.GenderRace == _pbdData.SelectedRaceCode)) + { + _pbdData.SelectedRaceCode = deformer.GenderRace; + _pbdData.SelectedDeformer = deformer.RacialDeformer; + } + + ImGui.SameLine(); + color.Push(ImGuiCol.Text, metaColor); + ImUtf8.TextRightAligned(raceCode); + } + } + + private void DrawBoneSelector() + { + using var group = ImUtf8.Group(); + var width = 200 * ImUtf8.GlobalScale; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(width); + ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8); + } + + using var child = ImUtf8.Child("Bone"u8, + new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true); + if (!child) + return; + + if (_pbdData.SelectedDeformer == null) + return; + + if (_pbdData.SelectedDeformer.IsEmpty) + { + ImUtf8.Text(""u8); + } + else + { + var height = ImGui.GetTextLineHeightWithSpacing(); + var skips = ImGuiClip.GetNecessarySkips(height); + var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips, + b => b.Contains(_pbdData.BoneFilter), bone + => + { + if (ImUtf8.Selectable(bone, bone == _pbdData.SelectedBone)) + _pbdData.SelectedBone = bone; + }); + ImGuiClip.DrawEndDummy(remainder, height); + } + } + + private bool DrawBoneData(PbdTab tab, bool disabled) + { + using var child = ImUtf8.Child("Data"u8, + ImGui.GetContentRegionAvail() with { Y = ImGui.GetContentRegionMax().Y - ImGui.GetStyle().WindowPadding.Y }, true); + if (!child) + return false; + + if (_pbdData.SelectedBone == null) + return false; + + if (!_pbdData.SelectedDeformer!.DeformMatrices.TryGetValue(_pbdData.SelectedBone, out var matrix)) + return false; + + var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2; + var dummyHeight = ImGui.GetTextLineHeight() / 2; + var ret = DrawAddNewBone(tab, disabled, matrix, width); + + ImUtf8.Dummy(0, dummyHeight); + ImGui.Separator(); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawDeformerMatrix(disabled, matrix, width); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawCopyPasteButtons(disabled, matrix, width); + + + ImUtf8.Dummy(0, dummyHeight); + ImGui.Separator(); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawDecomposedData(disabled, matrix, width); + + return ret; + } + + private bool DrawAddNewBone(PbdTab tab, bool disabled, in TransformMatrix matrix, float width) + { + var ret = false; + ImUtf8.TextFrameAligned("Copy the values of the bone "u8); + ImGui.SameLine(0, 0); + using (ImRaii.PushColor(ImGuiCol.Text, ColorId.NewMod.Value())) + { + ImUtf8.TextFrameAligned(_pbdData.SelectedBone); + } + + ImGui.SameLine(0, 0); + ImUtf8.TextFrameAligned(" to a new bone of name"u8); + + var fullWidth = width * 4 + ImGui.GetStyle().ItemSpacing.X * 3; + ImGui.SetNextItemWidth(fullWidth); + ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8); + ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8); + ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X); + if (ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), + disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null)) + { + foreach (var deformer in tab.File.Deformers) + { + if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) + continue; + + if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) + && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) + && !newBoneMatrix.Equals(existingMatrix)) + Penumbra.Messager.AddMessage(new Notification( + $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", + NotificationType.Warning)); + else + ret = true; + } + + _pbdData.NewBoneName = string.Empty; + } + + if (ImUtf8.ButtonEx("Copy Values to Single New Bone Entry"u8, ""u8, new Vector2(fullWidth, 0), + disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedDeformer!.DeformMatrices.ContainsKey(_pbdData.NewBoneName))) + { + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.NewBoneName] = matrix; + ret = true; + _pbdData.NewBoneName = string.Empty; + } + + + return ret; + } + + private bool DrawDeformerMatrix(bool disabled, in TransformMatrix matrix, float width) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var _ = ImRaii.Disabled(disabled); + var ret = false; + for (var i = 0; i < 3; ++i) + { + for (var j = 0; j < 4; ++j) + { + using var id = ImUtf8.PushId(i * 4 + j); + ImGui.SetNextItemWidth(width); + var tmp = matrix[i, j]; + if (ImUtf8.InputScalar(""u8, ref tmp, "% 12.8f"u8)) + { + ret = true; + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = matrix.ChangeValue(i, j, tmp); + } + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + return ret; + } + + private bool DrawCopyPasteButtons(bool disabled, in TransformMatrix matrix, float width) + { + var size = new Vector2(width, 0); + if (ImUtf8.Button("Copy Values"u8, size)) + _pbdData.CopiedMatrix = matrix; + + ImGui.SameLine(); + + var ret = false; + if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue)) + { + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value; + ret = true; + } + + var modifier = _config.DeleteModModifier.IsActive(); + ImGui.SameLine(); + if (modifier) + { + if (ImUtf8.ButtonEx("Delete"u8, "Delete this bone entry."u8, size, disabled)) + { + ret |= _pbdData.SelectedDeformer!.DeformMatrices.Remove(_pbdData.SelectedBone!); + _pbdData.SelectedBone = null; + } + } + else + { + ImUtf8.ButtonEx("Delete"u8, $"Delete this bone entry. Hold {_config.DeleteModModifier} to delete.", size, true); + } + + return ret; + } + + private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width) + { + var ret = false; + + + if (!matrix.TryDecompose(out var scale, out var rotation, out var translation)) + return false; + + using (ImUtf8.Group()) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var _ = ImRaii.Disabled(disabled); + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleX"u8, ref scale.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleY"u8, ref scale.Y, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleZ"u8, ref scale.Z, "% 12.8f"u8); + + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationX"u8, ref translation.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationY"u8, ref translation.Y, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationZ"u8, ref translation.Z, "% 12.8f"u8); + + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationR"u8, ref rotation.W, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationI"u8, ref rotation.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationJ"u8, ref rotation.Y, "% 12.8f"u8); + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationK"u8, ref rotation.Z, "% 12.8f"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.TextFrameAligned("Scale"u8); + ImUtf8.TextFrameAligned("Translation"u8); + ImUtf8.TextFrameAligned("Rotation (Quaternion, rijk)"u8); + } + + if (ret) + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = TransformMatrix.Compose(scale, rotation, translation); + return ret; + } + + public class PbdTab(byte[] data, string filePath) : IWritable + { + public readonly string FilePath = filePath; + + public readonly PbdFile File = new(data); + + public bool Valid + => File.Valid; + + public byte[] Write() + => File.Write(); + } + + private class PbdData + { + public GenderRace SelectedRaceCode = GenderRace.Unknown; + public RacialDeformer? SelectedDeformer; + public string? SelectedBone; + public string NewBoneName = string.Empty; + public string BoneFilter = string.Empty; + public string RaceCodeFilter = string.Empty; + + public TransformMatrix? CopiedMatrix; + + public void Update(PbdFile file) + { + if (SelectedRaceCode is GenderRace.Unknown) + { + SelectedDeformer = null; + } + else + { + SelectedDeformer = file.Deformers.FirstOrDefault(p => p.GenderRace == SelectedRaceCode).RacialDeformer; + if (SelectedDeformer is null) + SelectedRaceCode = GenderRace.Unknown; + } + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index ffa7473d..b07633b6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -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, 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) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 3ec6a4d5..49eac96e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -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) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 490fa147..de088736 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -97,9 +97,7 @@ public partial class ModEditWindow private void DrawImportExport(MdlTab tab, bool disabled) { - // TODO: Enable when functional. - using var dawntrailDisabled = ImRaii.Disabled(); - if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true) + if (!ImGui.CollapsingHeader("Import / Export")) return; var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 652ecb49..c08e8a8e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -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", }; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 13458252..1a4065bb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -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; @@ -186,6 +236,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _itemSwapTab.DrawContent(); } + _pbdTab.Draw(); + DrawMissingFilesTab(); DrawMaterialReassignmentTab(); } @@ -407,14 +459,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 +484,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 +639,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 +656,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _models = models; _fileDialog = fileDialog; _framework = framework; - MigrationManager = migrationManager; _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, @@ -616,12 +667,18 @@ public partial class ModEditWindow : Window, IDisposable, IUiService () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new ShpkTab(_fileDialog, bytes, path)); + _pbdTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Deformers", ".pbd", + () => _editor.Files.Pbd, DrawDeformerPanel, + () => Mod?.ModPath.FullName ?? string.Empty, + (bytes, path, _) => new PbdTab(bytes, path)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; _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() diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index b5f0255c..bd62089f 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -45,9 +46,30 @@ public class ModMergeTab(ModMerger modMerger) : IUiService private void DrawMergeInto(float size) { - using var bigGroup = ImRaii.Group(); + using var bigGroup = ImRaii.Group(); + var minComboSize = 300 * ImGuiHelpers.GlobalScale; + var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X; + ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into "); + + using (ImRaii.Group()) + { + ImUtf8.Text("Merge "u8); + ImGui.SameLine(0, 0); + if (size - textSize < minComboSize) + { + ImUtf8.Text("selected mod"u8, ColorId.FolderLine.Value()); + ImUtf8.HoverTooltip(modMerger.MergeFromMod!.Name.Text); + } + else + { + ImUtf8.Text(modMerger.MergeFromMod!.Name.Text, ColorId.FolderLine.Value()); + } + + ImGui.SameLine(0, 0); + ImUtf8.Text(" into"u8); + } + ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 361094c4..3aff2ac9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -190,52 +190,6 @@ public class ResourceTreeViewer var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) - { - if (!_typeFilter.HasFlag(filterIcon)) - return false; - - if (_nodeFilter.Length == 0) - return true; - - return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); - } - - NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) - { - if (node.Internal && !debugMode) - return NodeVisibility.Hidden; - - var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; - if (MatchesFilter(node, filterIcon)) - return NodeVisibility.Visible; - - foreach (var child in node.Children) - { - if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) - return NodeVisibility.DescendentsOnly; - } - - return NodeVisibility.Hidden; - } - - NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) - { - if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) - { - visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); - _filterCache.Add(nodePathHash, visibility); - } - - return visibility; - } - - string GetAdditionalDataSuffix(CiByteString data) - => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; - foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); @@ -346,6 +300,54 @@ public class ResourceTreeViewer if (unfolded) DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); } + + return; + + string GetAdditionalDataSuffix(CiByteString data) + => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; + + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) + { + if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) + { + visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); + _filterCache.Add(nodePathHash, visibility); + } + + return visibility; + } + + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) + { + if (node.Internal && !debugMode) + return NodeVisibility.Hidden; + + var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; + if (MatchesFilter(node, filterIcon)) + return NodeVisibility.Visible; + + foreach (var child in node.Children) + { + if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) + return NodeVisibility.DescendentsOnly; + } + + return NodeVisibility.Hidden; + } + + bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) + { + if (!_typeFilter.HasFlag(filterIcon)) + return false; + + if (_nodeFilter.Length == 0) + return true; + + return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + } } [Flags] diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 41920d1c..48ac90d8 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -53,10 +53,42 @@ public class PenumbraChangelog : IUiService Add1_1_0_0(Changelog); Add1_1_1_0(Changelog); Add1_2_1_0(Changelog); + Add1_3_0_0(Changelog); } #region Changelogs + private static void Add1_3_0_0(Changelog log) + => log.NextVersion("Version 1.3.0.0") + + .RegisterHighlight("The textures tab in the advanced editing window can now import and export .tga files.") + .RegisterEntry("BC4 and BC6 textures can now also be imported.", 1) + .RegisterHighlight("Added item swapping from and to the Glasses slot.") + .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.", 1) + .RegisterEntry("The import date of a mod is now shown in the Edit Mod tab, and can be reset via button.") + .RegisterEntry("A button to open the file containing local mod data for a mod was also added.", 1) + .RegisterHighlight("IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") + .RegisterEntry("This allows keeping the material index of every IMC entry of a group, while setting the attributes.", 1) + .RegisterHighlight("Model Import/Export was fixed and re-enabled (thanks ackwell and ramen).") + .RegisterHighlight("Added a hack to allow bonus items (face wear, glasses) to have VFX.") + .RegisterEntry("Also fixed the hack that allowed accessories to have VFX not working anymore.", 1) + .RegisterHighlight("Added rudimentary options to edit PBD files in the advanced editing window.") + .RegisterEntry("Preparing the advanced editing window for a mod now does not freeze the game until it is ready.") + .RegisterEntry("Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") + .RegisterEntry("Added a button to the advanced editing window to remove all default-valued meta manipulations from a mod") + .RegisterEntry("Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", 1) + .RegisterEntry("Checkbox-based mod filters are now tri-state checkboxes instead of two disjoint checkboxes.") + .RegisterEntry("Paths from the resource logger can now be copied.") + .RegisterEntry("Silenced some redundant error logs when updating mods via Heliosphere.") + .RegisterEntry("Added 'Page' to imported mod data for TexTools interop. The value is not used in Penumbra, just persisted.") + .RegisterEntry("Updated all external dependencies.") + .RegisterEntry("Fixed issue with Demihuman IMC entries.") + .RegisterEntry("Fixed some off-by-one errors on the mod import window.") + .RegisterEntry("Fixed a race-condition concerning the first-time creation of mod-meta files.") + .RegisterEntry("Fixed an issue with long mod titles in the merge mods tab.") + .RegisterEntry("A bunch of other miscellaneous fixes."); + + private static void Add1_2_1_0(Changelog log) => log.NextVersion("Version 1.2.1.0") .RegisterHighlight("Penumbra is now released for Dawntrail!") diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 0f9b2518..3972e350 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -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", diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 689571f3..c30239bc 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -24,13 +24,11 @@ public class AddGroupDrawer : IUiService private bool _imcFileExists; private bool _entryExists; private bool _entryInvalid; - private readonly ImcChecker _imcChecker; private readonly ModManager _modManager; - public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker) + public AddGroupDrawer(ModManager modManager) { _modManager = modManager; - _imcChecker = imcChecker; UpdateEntry(); } @@ -142,7 +140,7 @@ public class AddGroupDrawer : IUiService private void UpdateEntry() { - (_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false); + (_defaultEntry, _imcFileExists, _entryExists) = ImcChecker.GetDefaultEntry(_imcIdentifier, false); _entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists; } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 9d1ab78a..786bb8ff 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -4,9 +4,9 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget; -using OtterGui.Widgets; using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; @@ -19,18 +19,25 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr public void Draw() { var identifier = group.Identifier; - var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; var entry = group.DefaultEntry; var changes = false; - var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X; + var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - ImUtf8.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight; ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + ImUtf8.SameLineInner(); var allVariants = group.AllVariants; if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); + ImGui.SameLine(); + var onlyAttributes = group.OnlyAttributes; + if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes)) + editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes); + ImUtf8.HoverTooltip("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8); + using (ImUtf8.Group()) { ImUtf8.TextFrameAligned("Material ID"u8); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 42689efb..0781312c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -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, 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 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 _selection.SelectMod(newSelection); + #endregion #region Filters @@ -567,7 +530,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. protected override bool ChangeFilter(string filterValue) { - Filter.Parse(filterValue); + _filter.Parse(filterValue); return true; } @@ -597,7 +560,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Apply the string filters. private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) - => !Filter.IsVisible(leaf); + => !_filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) @@ -741,8 +704,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector 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> 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"), + ], + ]; } diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index ee6fab1f..9d6ead62 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -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(); } diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index bee48068..bc18ac51 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -25,7 +25,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy => "Conflicts"u8; public bool IsVisible - => collectionManager.Active.Current.Conflicts(selector.Selected!).Count > 0; + => collectionManager.Active.Current.Conflicts(selector.Selected!).Any(c => !GetPriority(c).IsHidden); private readonly ConditionalWeakTable _expandedMods = []; @@ -58,7 +58,8 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy // Can not be null because otherwise the tab bar is never drawn. var mod = selector.Selected!; - foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) + foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).Where(c => !c.Mod2.Priority.IsHidden) + .OrderByDescending(GetPriority) .ThenBy(c => c.Mod2.Name.Lower).WithIndex()) { using var id = ImRaii.PushId(index); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 90d8fb74..1ccee2cb 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -7,6 +7,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; @@ -50,6 +51,8 @@ public class ModPanelEditTab( EditButtons(); EditRegularMeta(); UiHelpers.DefaultLineSpace(); + EditLocalData(); + UiHelpers.DefaultLineSpace(); if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X)) try @@ -182,6 +185,40 @@ public class ModPanelEditTab( DrawOpenDefaultMod(); } + private void EditLocalData() + { + DrawImportDate(); + DrawOpenLocalData(); + } + + private void DrawImportDate() + { + ImUtf8.TextFramed($"{DateTimeOffset.FromUnixTimeMilliseconds(_mod.ImportDate).ToLocalTime():yyyy/MM/dd HH:mm}", + ImGui.GetColorU32(ImGuiCol.FrameBg, 0.5f), new Vector2(UiHelpers.InputTextMinusButton3, 0)); + ImGui.SameLine(0, 3 * ImUtf8.GlobalScale); + + var canRefresh = config.DeleteModModifier.IsActive(); + var tt = canRefresh + ? "Reset the import date to the current date and time." + : $"Reset the import date to the current date and time.\nHold {config.DeleteModModifier} while clicking to refresh."; + + if (ImUtf8.IconButton(FontAwesomeIcon.Sync, tt, disabled: !canRefresh)) + modManager.DataEditor.ResetModImportDate(_mod); + ImUtf8.SameLineInner(); + ImUtf8.Text("Import Date"u8); + } + + private void DrawOpenLocalData() + { + var file = filenames.LocalDataFile(_mod); + var fileExists = File.Exists(file); + var tt = fileExists + ? "Open the local mod data file in the text editor of your choice."u8 + : "The local mod data file does not exist."u8; + if (ImUtf8.ButtonEx("Open Local Data"u8, tt, UiHelpers.InputTextWidth, !fileExists)) + Process.Start(new ProcessStartInfo(file) { UseShellExecute = true }); + } + private void DrawOpenDefaultMod() { var file = filenames.OptionGroupFile(_mod, -1, false); diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 6c974f9c..aafbffa6 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -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 /// 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; + } + /// /// 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. /// - 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; } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 7e3b8a95..8d889c3b 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -2,10 +2,11 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; +using OtterGui.Text; 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 +17,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 Label => "Settings"u8; @@ -42,12 +41,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 +52,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); } /// Draw a big red bar if the current setting is inherited. @@ -70,8 +67,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 +77,12 @@ public class ModPanelSettingsTab( /// Draw a checkbox for the enabled status of the mod. 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); } /// @@ -95,15 +92,19 @@ 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 (new ModPriority(priority).IsHidden) + ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + 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 +121,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."); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 33e301ae..2bb71b87 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; +using OtterGui.Text; using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; @@ -52,36 +53,41 @@ internal sealed class ResourceWatcherTable : Table private static unsafe void DrawByteString(CiByteString path, float length) { - Vector2 vec; - ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); - if (vec.X <= length) + if (path.IsEmpty) + return; + + var size = ImUtf8.CalcTextSize(path.Span); + var clicked = false; + if (size.X <= length) { - ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); + clicked = ImUtf8.Selectable(path.Span); } else { - var fileName = path.LastIndexOf((byte)'/'); - CiByteString shortPath; - if (fileName != -1) + var fileName = path.LastIndexOf((byte)'/'); + using (ImRaii.Group()) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); - using var font = ImRaii.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString()); - ImGui.SameLine(); - shortPath = path.Substring(fileName, path.Length - fileName); - } - else - { - shortPath = path; + CiByteString shortPath; + if (fileName != -1) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + clicked = ImUtf8.Selectable(FontAwesomeIcon.EllipsisH.ToIconString()); + ImUtf8.SameLineInner(); + shortPath = path.Substring(fileName, path.Length - fileName); + } + else + { + shortPath = path; + } + + clicked |= ImUtf8.Selectable(shortPath.Span, false, ImGuiSelectableFlags.AllowItemOverlap); } - ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length); - if (ImGui.IsItemClicked()) - ImGuiNative.igSetClipboardText(path.Path); - - if (ImGui.IsItemHovered()) - ImGuiNative.igSetTooltip(path.Path); + ImUtf8.HoverTooltip(path.Span); } + + if (clicked) + ImUtf8.SetClipboardText(path.Span); } private sealed class RecordTypeColumn : ColumnFlags diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5b82a523..7c6cd01e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -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; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 50fdc1d3..87338bdb 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -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"); } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 66b76406..19ba7343 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -820,13 +820,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) { diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 8e7106dd..5b868212 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -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, )" } }, diff --git a/repo.json b/repo.json index 5a274d73..cba274c8 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.1", + "AssemblyVersion": "1.3.0.0", + "TestingAssemblyVersion": "1.3.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "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", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ]