diff --git a/Penumbra.Api b/Penumbra.Api index 86249598..759a8e9d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1 +Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0 diff --git a/Penumbra.GameData b/Penumbra.GameData index ee6c6faa..f2734d54 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a +Subproject commit f2734d543d9b2debecb8feb6d6fa928801eb2bcb diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index ff393aaf..04299187 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -36,7 +36,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : collection = ModCollection.Empty; if (collection.HasCache) - return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject()); Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded."); return []; diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 60b00d37..790121d5 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -134,6 +134,6 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public Dictionary GetChangedItems(string modDirectory, string modName) => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject()) : []; } diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index cf3cd8f2..515874c0 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -1,7 +1,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; @@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - private void OnChangedItemClick(MouseButton button, object? data) + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) { if (ChangedItemClicked == null) return; - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); ChangedItemClicked.Invoke(button, type, id); } - private void OnChangedItemHover(object? data) + private void OnChangedItemHover(IIdentifiedObjectData? data) { if (ChangedItemTooltip == null) return; - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); ChangedItemTooltip.Invoke(type, id); } } diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 026fabbc..1d516eba 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -8,7 +8,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Data; using ImGuiClip = OtterGui.ImGuiClip; namespace Penumbra.Api.IpcTester; @@ -17,10 +17,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService { private int _objectIdx; private string _collectionIdString = string.Empty; - private Guid? _collectionId = null; - private bool _allowCreation = true; - private bool _allowDeletion = true; - private ApiCollectionType _type = ApiCollectionType.Yourself; + private Guid? _collectionId; + private bool _allowCreation = true; + private bool _allowDeletion = true; + private ApiCollectionType _type = ApiCollectionType.Yourself; private Dictionary _collections = []; private (string, ChangedItemType, uint)[] _changedItems = []; @@ -116,7 +116,7 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty)); _changedItems = items.Select(kvp => { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value); + var (type, id) = kvp.Value.ToApiObject(); return (kvp.Key, type, id); }).ToArray(); ImGui.OpenPopup("Changed Item List"); @@ -130,9 +130,9 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService if (!p) return; - using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) + using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) { - if (t) + if (table) ImGuiClip.ClippedDraw(_changedItems, t => { ImGuiUtil.DrawTableColumn(t.Item1); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 4755840e..abc0dff8 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Util; +using Penumbra.GameData.Data; namespace Penumbra.Collections.Cache; @@ -18,14 +19,14 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// public sealed class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly CustomResourceCache CustomResources; - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, IIdentifiedObjectData?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly CustomResourceCache CustomResources; + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -41,7 +42,7 @@ public sealed class CollectionCache : IDisposable private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, object?)> ChangedItems + public IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems { get { @@ -412,7 +413,7 @@ public sealed class CollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = _manager.MetaFileManager.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { @@ -421,8 +422,8 @@ public sealed class CollectionCache : IDisposable if (!_changedItems.TryGetValue(name, out var data)) _changedItems.Add(name, (new SingleArray(mod), obj)); else if (!data.Item1.Contains(mod)) - _changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj); - else if (obj is int x && data.Item2 is int y) + _changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); + else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y) _changedItems[name] = (data.Item1, x + y); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 983509a4..0b38dde8 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,8 +1,8 @@ using OtterGui.Classes; using Penumbra.Mods; -using Penumbra.Meta.Files; using Penumbra.String.Classes; using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; using Penumbra.Mods.Editor; namespace Penumbra.Collections; @@ -46,8 +46,8 @@ public partial class ModCollection internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); - internal IReadOnlyDictionary, object?)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, object?)>(); + internal IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData?)>(); internal IEnumerable> AllConflicts => _cache?.AllConflicts ?? Array.Empty>(); diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 554e2221..1aac4454 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Api.Api; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; namespace Penumbra.Communication; @@ -11,7 +12,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 2dcced35..4e72b558 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api.Api; +using Penumbra.GameData.Data; namespace Penumbra.Communication; @@ -9,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 7457c910..24ab466b 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -23,24 +23,24 @@ public class EphemeralConfig : ISavable, IDisposable, IService [JsonIgnore] private readonly ModPathChanged _modPathChanged; - public int Version { get; set; } = Configuration.Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; - public bool DebugSeparateWindow { get; set; } = false; - public int TutorialStep { get; set; } = 0; - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public bool OnlyAddMatchingResources { get; set; } = true; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; - public TabType SelectedTab { get; set; } = TabType.Settings; - public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; - public bool FixMainWindow { get; set; } = false; - public string LastModPath { get; set; } = string.Empty; - public bool AdvancedEditingOpen { get; set; } = false; - public bool ForceRedrawOnFileChange { get; set; } = false; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public bool DebugSeparateWindow { get; set; } = false; + public int TutorialStep { get; set; } = 0; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public bool OnlyAddMatchingResources { get; set; } = true; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; + public bool FixMainWindow { get; set; } = false; + public string LastModPath { get; set; } = string.Empty; + public bool AdvancedEditingOpen { get; set; } = false; + public bool ForceRedrawOnFileChange { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 85b3284a..43324516 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Text.HelperObjects; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -8,26 +9,33 @@ using Penumbra.Meta.Manipulations; using Penumbra.String; using Penumbra.String.Classes; using static Penumbra.Interop.Structs.StructExtensions; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; internal partial record ResolveContext { + private static bool IsEquipmentOrAccessorySlot(uint slotIndex) + => slotIndex is < 10 or 16 or 17; + + private static bool IsEquipmentSlot(uint slotIndex) + => slotIndex is < 5 or 16 or 17; + private Utf8GamePath ResolveModelPath() { // Correctness: // Resolving a model path through the game's code can use EQDP metadata for human equipment models. return ModelType switch { - ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(), - _ => ResolveModelPathNative(), + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) => ResolveEquipmentModelPath(), + _ => ResolveModelPathNative(), }; } private Utf8GamePath ResolveEquipmentModelPath() { - var path = SlotIndex < 5 + var path = IsEquipmentSlot(SlotIndex) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; @@ -39,7 +47,7 @@ internal partial record ResolveContext private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { var slotIndex = slot.ToIndex(); - if (slotIndex >= 10 || ModelType != ModelType.Human) + if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human) return GenderRace.MidlanderMale; var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId; @@ -80,7 +88,7 @@ internal partial record ResolveContext // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { - ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), @@ -95,7 +103,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -109,31 +117,25 @@ internal partial record ResolveContext if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; - // MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand - if (setIdHigh is 3 or 16 or 18 or 26) + // Some offhands share materials with the corresponding mainhand + if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) { - var setIdLow = Equipment.Set.Id % 100; - if (setIdLow > 50) - { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - var mirroredSetId = (ushort)(Equipment.Set.Id - 50); + Span mirroredFileName = stackalloc byte[32]; + mirroredFileName = mirroredFileName[..fileName.Length]; + fileName.CopyTo(mirroredFileName); + WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId.Id); - Span mirroredFileName = stackalloc byte[32]; - mirroredFileName = mirroredFileName[..fileName.Length]; - fileName.CopyTo(mirroredFileName); - WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); - Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); + if (weaponPosition >= 0) + WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId.Id); - var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); - if (weaponPosition >= 0) - WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); - - return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; - } + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); @@ -144,7 +146,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -175,13 +177,21 @@ internal partial record ResolveContext var baseDirectory = modelPath[..modelPosition]; - baseDirectory.CopyTo(materialPathBuffer); - "/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]); - WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant); - materialPathBuffer[baseDirectory.Length + 15] = (byte)'/'; - mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]); + var writer = new SpanTextWriter(materialPathBuffer); + writer.Append(baseDirectory); + writer.Append("/material/v"u8); + WriteZeroPaddedNumber(ref writer, 4, variant); + writer.Append((byte)'/'); + writer.Append(mtrlFileName); + writer.EnsureNullTerminated(); - return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)]; + return materialPathBuffer[..writer.Position]; + } + + private static void WriteZeroPaddedNumber(ref SpanTextWriter writer, int width, ushort number) + { + WriteZeroPaddedNumber(writer.GetRemainingSpan()[..width], number); + writer.Advance(width); } private static void WriteZeroPaddedNumber(Span destination, ushort number) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 41485d75..b99ee235 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,9 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using OtterGui; +using OtterGui.Text.HelperObjects; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Data; @@ -16,7 +16,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; using static Penumbra.Interop.Structs.StructExtensions; -using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; namespace Penumbra.Interop.ResourceTree; @@ -29,25 +29,25 @@ internal record GlobalResolveContext( { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, + public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } internal unsafe partial record ResolveContext( GlobalResolveContext Global, - Pointer CharacterBasePointer, + Pointer CharacterBasePointer, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { - public CharacterBase* CharacterBase + public CharaBase* CharacterBase => CharacterBasePointer.Value; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ModelType ModelType + private CharaBase.ModelType ModelType => CharacterBase->GetModelType(); private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) @@ -75,11 +75,14 @@ internal unsafe partial record ResolveContext( if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3) return null; - Span prefixed = stackalloc byte[260]; - gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); - prefixed[lastDirectorySeparator + 1] = (byte)'-'; - prefixed[lastDirectorySeparator + 2] = (byte)'-'; - gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + Span prefixed = stackalloc byte[CharaBase.PathBufferSize]; + + var writer = new SpanTextWriter(prefixed); + writer.Append(gamePath.Span[..(lastDirectorySeparator + 1)]); + writer.Append((byte)'-'); + writer.Append((byte)'-'); + writer.Append(gamePath.Span[(lastDirectorySeparator + 1)..]); + writer.EnsureNullTerminated(); if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) return null; @@ -350,7 +353,7 @@ internal unsafe partial record ResolveContext( _ => string.Empty, } + item.Name; - return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag()); } var dataFromPath = GuessUiDataFromPath(gamePath); @@ -358,8 +361,8 @@ internal unsafe partial record ResolveContext( return dataFromPath; return isEquipment - ? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) - : new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + ? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag()) + : new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath) @@ -367,13 +370,13 @@ internal unsafe partial record ResolveContext( foreach (var obj in Global.Identifier.Identify(gamePath.ToString())) { var name = obj.Key; - if (name.StartsWith("Customization:")) + if (obj.Value is IdentifiedCustomization) name = name[14..].Trim(); if (name != "Unknown") - return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } - return new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index de43a874..6ab48325 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,7 +1,7 @@ using Penumbra.Api.Enums; using Penumbra.String; using Penumbra.String.Classes; -using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; +using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; @@ -9,7 +9,7 @@ public class ResourceNode : ICloneable { public string? Name; public string? FallbackName; - public ChangedItemIcon Icon; + public ChangedItemIconFlag IconFlag; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -51,7 +51,7 @@ public class ResourceNode : ICloneable { Name = other.Name; FallbackName = other.FallbackName; - Icon = other.Icon; + IconFlag = other.IconFlag; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; @@ -79,7 +79,7 @@ public class ResourceNode : ICloneable public void SetUiData(UiData uiData) { Name = uiData.Name; - Icon = uiData.Icon; + IconFlag = uiData.IconFlag; } public void PrependName(string prefix) @@ -88,9 +88,9 @@ public class ResourceNode : ICloneable Name = prefix + Name; } - public readonly record struct UiData(string? Name, ChangedItemIcon Icon) + public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag) { public UiData PrependName(string prefix) - => Name == null ? this : new UiData(prefix + Name, Icon); + => Name == null ? this : new UiData(prefix + Name, IconFlag); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 6663fb40..38f6fe97 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -9,6 +9,7 @@ using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; +using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -44,8 +45,8 @@ public class ResourceTree PlayerRelated = playerRelated; CollectionName = collectionName; AnonymizedCollectionName = anonymizedCollectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Nodes = []; + FlatNodes = []; } public void ProcessPostfix(Action action) @@ -59,13 +60,13 @@ public class ResourceTree var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; var modelType = model->GetModelType(); - var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; + var human = modelType == ModelType.Human ? (Human*)model : null; var equipment = modelType switch { - CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan( + ModelType.Human => new ReadOnlySpan(&human->Head, 12), + ModelType.DemiHuman => new ReadOnlySpan( Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), - _ => ReadOnlySpan.Empty, + _ => [], }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; @@ -75,9 +76,18 @@ public class ResourceTree for (var i = 0u; i < model->SlotCount; ++i) { - var slotContext = i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) - : globalContext.CreateContext(model, i); + var slotContext = modelType switch + { + 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), + }, + _ => i < equipment.Length + ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + : globalContext.CreateContext(model, i), + }; var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); @@ -117,7 +127,7 @@ public class ResourceTree var subObject = (CharacterBase*)baseSubObject; - if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) + if (subObject->GetModelType() != ModelType.Weapon) continue; var weapon = (Weapon*)subObject; @@ -174,7 +184,7 @@ public class ResourceTree { pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(pbdNode); @@ -192,7 +202,7 @@ public class ResourceTree { decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; - decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + decalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(decalNode); @@ -209,7 +219,7 @@ public class ResourceTree { legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; - legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(legacyDecalNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 22025dd6..48690e98 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -67,7 +67,7 @@ internal static class ResourceTreeApiHelper continue; var fullPath = node.FullPath.ToPath(); - resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon))); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)node.IconFlag.ToApiIcon())); } } @@ -106,7 +106,7 @@ internal static class ResourceTreeApiHelper var ret = new JObject { [nameof(ResourceNodeDto.Type)] = new JValue(node.Type), - [nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)), + [nameof(ResourceNodeDto.Icon)] = new JValue(node.IconFlag.ToApiIcon()), [nameof(ResourceNodeDto.Name)] = node.Name, [nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), [nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(), diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 3f856bd2..3a804d0c 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 5d37aac8..f758126c 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 2955dba4..cfe9b7d4 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -24,7 +24,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { switch (Slot) { diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 2b88d962..ec59762b 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = Type switch { diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index a6fcf58b..1f41adfb 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 5707ffca..d1668a4d 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -18,7 +18,7 @@ public enum MetaManipulationType : byte public interface IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); public MetaIndex FileIndex(); diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index d4887fe2..1b2492ee 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 73d1d7e5..2d73ec7f 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,4 +1,3 @@ -using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -9,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 9327ced9..c5654019 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -45,7 +45,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 7b0eb094..d42804ba 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -121,7 +121,7 @@ public class ImcModGroup(Mod mod) : IModGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index ee27d534..9cf7e6a3 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -126,7 +126,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index cc606f42..723cd5b1 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -111,7 +111,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 2c292a14..1a2f2798 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -186,7 +186,7 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, 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 meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, @@ -255,7 +255,8 @@ public static class EquipmentSwap { items = identifier.Identify(slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType() + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) + .Select(kvp => kvp.Value).OfType().Select(i => i.Item) .ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 16f06de2..488e3dc1 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -100,7 +101,7 @@ public sealed class Mod : IMod } // Cache - public readonly SortedList ChangedItems = new(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ea74987..dbe06803 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,8 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Dalamud.Plugin.Services; +using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; @@ -109,16 +111,17 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { _services.GetService(); + var itemSheet = _services.GetService().GetExcelSheet()!; _communicatorService.ChangedItemHover.Subscribe(it => { - if (it is (Item, FullEquipType)) + if (it is IdentifiedItem) ImGui.TextUnformatted("Left Click to create an item link in chat."); }, ChangedItemHover.Priority.Link); _communicatorService.ChangedItemClick.Subscribe((button, it) => { - if (button == MouseButton.Left && it is (Item item, FullEquipType type)) - Messager.LinkItem(item); + if (button == MouseButton.Left && it is IdentifiedItem item && itemSheet.GetRow(item.Item.ItemId.Id) is { } i) + Messager.LinkItem(i); }, ChangedItemClick.Priority.Link); } diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 70b05a73..5ba57cf4 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -115,7 +115,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; - _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() + _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() ?? _config.Ephemeral.ChangedItemFilter; _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; _config.Ephemeral.Save(); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index da2daeb7..b75c5aef 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -135,7 +135,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService : FilterComboCache<(EquipItem Item, bool InMod)>(() => { var list = data.ByType[type]; - if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type)) + if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)) return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); return list.Select(i => (i, false)).ToList(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs new file mode 100644 index 00000000..cb04dc0a --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -0,0 +1,538 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.String.Functions; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + + private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled) + { + if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + ColorTableCopyAllClipboardButton(tab.Mtrl); + ImGui.SameLine(); + var ret = ColorTablePasteAllClipboardButton(tab, disabled); + if (!disabled) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= ColorTableDyeableCheckbox(tab); + } + + var hasDyeTable = tab.Mtrl.HasDyeTable; + if (hasDyeTable) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= DrawPreviewDye(tab, disabled); + } + + using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!table) + return false; + + ImGui.TableNextColumn(); + ImGui.TableHeader(string.Empty); + ImGui.TableNextColumn(); + ImGui.TableHeader("Row"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Diffuse"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Specular"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Emissive"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Gloss"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Tile"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Repeat"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Skew"); + if (hasDyeTable) + { + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye Preview"); + } + + for (var i = 0; i < ColorTable.NumRows; ++i) + { + ret |= DrawColorTableRow(tab, i, disabled); + ImGui.TableNextRow(); + } + + return ret; + } + + + private static void ColorTableCopyAllClipboardButton(MtrlFile file) + { + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + return; + + try + { + var data1 = file.Table.AsBytes(); + var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan.Empty; + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); + } + catch + { + // ignored + } + } + + private bool DrawPreviewDye(MtrlTab tab, bool disabled) + { + var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; + var tt = dyeId == 0 + ? "Select a preview dye first." + : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) + { + var ret = false; + if (tab.Mtrl.HasDyeTable) + for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); + + tab.UpdateColorTablePreview(); + + return ret; + } + + ImGui.SameLine(); + var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; + if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) + tab.UpdateColorTablePreview(); + return false; + } + + private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) + { + if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) + || !tab.Mtrl.HasTable) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + if (data.Length < Marshal.SizeOf()) + return false; + + ref var rows = ref tab.Mtrl.Table; + fixed (void* ptr = data, output = &rows) + { + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + && tab.Mtrl.HasDyeTable) + { + ref var dyeRows = ref tab.Mtrl.DyeTable; + fixed (void* output2 = &dyeRows) + { + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); + } + } + } + + tab.UpdateColorTablePreview(); + + return true; + } + catch + { + return false; + } + } + + [SkipLocalsInit] + private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye) + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true)) + return; + + try + { + Span data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size]; + fixed (byte* ptr = data) + { + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size); + } + + var text = Convert.ToBase64String(data); + ImGui.SetClipboardText(text); + } + catch + { + // ignored + } + } + + private static bool ColorTableDyeableCheckbox(MtrlTab tab) + { + var dyeable = tab.Mtrl.HasDyeTable; + var ret = ImGui.Checkbox("Dyeable", ref dyeable); + + if (ret) + { + tab.Mtrl.HasDyeTable = dyeable; + tab.UpdateColorTablePreview(); + } + + return ret; + } + + private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled) + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size + || !tab.Mtrl.HasTable) + return false; + + fixed (byte* ptr = data) + { + tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr; + if (tab.Mtrl.HasDyeTable) + tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size); + } + + tab.UpdateColorTableRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + } + + private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled) + { + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true); + + if (ImGui.IsItemHovered()) + tab.HighlightColorTableRow(rowIdx); + else if (tab.HighlightedColorTableRow == rowIdx) + tab.CancelColorTableHighlight(); + } + + private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) + { + static bool FixFloat(ref float val, float current) + { + val = (float)(Half)val; + return val != current; + } + + using var id = ImRaii.PushId(rowIdx); + ref var row = ref tab.Mtrl.Table[rowIdx]; + var hasDye = tab.Mtrl.HasDyeTable; + ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; + var floatSize = 70 * UiHelpers.Scale; + var intSize = 45 * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(row, dye); + ImGui.SameLine(); + var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); + ImGui.SameLine(); + ColorTableHighlightButton(tab, rowIdx, disabled); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => + { + tab.Mtrl.Table[rowIdx].Diffuse = c; + tab.UpdateColorTableRowPreview(rowIdx); + }); + if (hasDye) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => + { + tab.Mtrl.DyeTable[rowIdx].Diffuse = b; + tab.UpdateColorTableRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => + { + tab.Mtrl.Table[rowIdx].Specular = c; + tab.UpdateColorTableRowPreview(rowIdx); + }); + ImGui.SameLine(); + var tmpFloat = row.SpecularStrength; + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.SpecularStrength)) + { + row.SpecularStrength = tmpFloat; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); + + if (hasDye) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => + { + tab.Mtrl.DyeTable[rowIdx].Specular = b; + tab.UpdateColorTableRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => + { + tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; + tab.UpdateColorTableRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => + { + tab.Mtrl.Table[rowIdx].Emissive = c; + tab.UpdateColorTableRowPreview(rowIdx); + }); + if (hasDye) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => + { + tab.Mtrl.DyeTable[rowIdx].Emissive = b; + tab.UpdateColorTableRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); + } + + ImGui.TableNextColumn(); + tmpFloat = row.GlossStrength; + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") + && FixFloat(ref tmpFloat, row.GlossStrength)) + { + row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); + if (hasDye) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => + { + tab.Mtrl.DyeTable[rowIdx].Gloss = b; + tab.UpdateColorTableRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); + } + + ImGui.TableNextColumn(); + int tmpInt = row.TileSet; + ImGui.SetNextItemWidth(intSize); + if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) + { + row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialRepeat.X; + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) + { + row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); + ImGui.SameLine(); + tmpFloat = row.MaterialRepeat.Y; + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) + { + row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialSkew.X; + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) + { + row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.SameLine(); + tmpFloat = row.MaterialSkew.Y; + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) + { + row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); + + if (hasDye) + { + ImGui.TableNextColumn(); + if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dye.Template = _stainService.TemplateCombo.CurrentSelection; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); + } + + + return ret; + } + + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) + { + var stain = _stainService.StainCombo.CurrentSelection.Key; + if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) + return false; + + var values = entry[(int)stain]; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); + + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); + + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); + if (ret) + tab.UpdateColorTableRowPreview(rowIdx); + + ImGui.SameLine(); + ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); + ImGui.SameLine(); + ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); + ImGui.SameLine(); + ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); + ImGui.SameLine(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); + + return ret; + } + + private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") + { + var ret = false; + var inputSqrt = PseudoSqrtRgb(input); + var tmp = inputSqrt; + if (ImGui.ColorEdit3(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) + { + setter(PseudoSquareRgb(tmp)); + ret = true; + } + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImGui.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText(center, textColor, letter); + } + + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); + + return ret; + } + + // Functions to deal with squared RGB values without making negatives useless. + + private static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : x * x; + + private static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + private static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + private static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + internal static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + private static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs new file mode 100644 index 00000000..b95eca9d --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -0,0 +1,783 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.MaterialPreview; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.UI.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private sealed class MtrlTab : IWritable, IDisposable + { + private const int ShpkPrefixLength = 16; + + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + + private readonly ModEditWindow _edit; + public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; + + private string[]? _shpkNames; + + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public JObject? AssociatedShpkDevkit; + + public readonly string LoadedBaseDevkitPathName; + public readonly JObject? AssociatedBaseDevkit; + + // Shader Key State + public readonly + List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> + Values)> ShaderKeys = new(16); + + public readonly HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; + + // Textures & Samplers + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); + + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + + // Material Constants + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> + Constants)> Constants = new(16); + + // Live-Previewers + public readonly List MaterialPreviewers = new(4); + public readonly List ColorTablePreviewers = new(4); + public int HighlightedColorTableRow = -1; + public readonly Stopwatch HighlightTime = new(); + + public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) + return FullPath.Empty; + + return _edit.FindBestMatch(defaultGamePath); + } + + public string[] GetShpkNames() + { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + + public void LoadShpk(FullPath path) + { + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + + try + { + LoadedShpkPath = path; + var data = LoadedShpkPath.IsRooted + ? File.ReadAllBytes(LoadedShpkPath.FullName) + : _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + LoadedShpkPathName = path.ToPath(); + } + catch (Exception e) + { + LoadedShpkPath = FullPath.Empty; + LoadedShpkPathName = string.Empty; + AssociatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + } + + if (LoadedShpkPath.InternalName.IsEmpty) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } + + UpdateShaderKeys(); + Update(); + } + + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) + { + try + { + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); + + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; + } + } + + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class + => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class + { + if (devkit == null) + return null; + + try + { + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) + { + var selector = BuildSelector(data!["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; + } + + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } + + private void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + foreach (var key in AssociatedShpk.MaterialKeys) + { + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => + { + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); + + return ($"0x{value:X8}", value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); + } + else + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); + } + + private void UpdateShaders() + { + VertexShaders.Clear(); + PixelShaders.Clear(); + if (AssociatedShpk == null) + { + ShadersKnown = false; + } + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = + BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + foreach (var systemKeySelector in systemKeySelectors) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) + { + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + foreach (var pass in node.Value.Passes) + { + VertexShaders.Add((int)pass.VertexShader); + PixelShaders.Add((int)pass.PixelShader); + } + else + ShadersKnown = false; + } + } + } + } + + var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); + var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); + + VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + } + + private void UpdateTextures() + { + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.HasTable) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.HasTable) + SamplerIds.Add(TableSamplerId); + } + + foreach (var samplerId in SamplerIds) + { + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (shpkSampler is not { Slot: 2 }) + continue; + + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + dkData?.Description ?? string.Empty, !hasDkLabel)); + } + + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.HasTable = true; + } + + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); + + TextureLabelWidth = 50f * UiHelpers.Scale; + + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + } + + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, + ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + } + + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; + } + + private void UpdateConstants() + { + static List FindOrAddGroup(List<(string, List)> groups, string name) + { + foreach (var (groupName, group) in groups) + { + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; + } + + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; + } + + Constants.Clear(); + if (AssociatedShpk == null) + { + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) + { + var values = Mtrl.GetConstantValues(constant); + for (var i = 0; i < values.Length; i += 4) + { + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, + FloatConstantEditor.Default)); + } + } + } + else + { + var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + if ((shpkConstant.ByteSize & 0x3) != 0) + continue; + + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) + foreach (var dkConstant in dkData) + { + var offset = (int)dkConstant.Offset; + var length = values.Length - offset; + if (dkConstant.Length.HasValue) + length = Math.Min(length, (int)dkConstant.Length.Value); + if (length <= 0) + continue; + + var editor = dkConstant.CreateEditor(); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } + + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(complement:true)) + { + if ((shpkConstant.ByteOffset & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset >> 2; + for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 4, end); + if (rangeEnd > rangeStart) + fcGroup.Add(( + $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + for (var i = start; i < end; i += 4) + { + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, + FloatConstantEditor.Default)); + } + } + } + } + } + + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => + { + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; + + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme + foreach (var (_, group) in Constants) + { + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); + } + } + + public unsafe void BindToMaterialInstances() + { + UnbindFromMaterialInstances(); + + var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), + FilePath); + + var foundMaterials = new HashSet(); + foreach (var materialInfo in instances) + { + var material = materialInfo.GetDrawObjectMaterial(_edit._objects); + if (foundMaterials.Contains((nint)material)) + continue; + + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateMaterialPreview(); + + if (!Mtrl.HasTable) + return; + + foreach (var materialInfo in instances) + { + try + { + ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateColorTablePreview(); + } + + private void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorTablePreviewers) + previewer.Dispose(); + ColorTablePreviewers.Clear(); + } + + private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0;) + { + var previewer = MaterialPreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorTablePreviewers.Count; i-- > 0;) + { + var previewer = ColorTablePreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorTablePreviewers.RemoveAt(i); + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetShaderPackageFlags(shPkFlags); + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetMaterialParameter(parameterCrc, offset, value); + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetSamplerFlags(samplerCrc, samplerFlags); + } + + private void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValues(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + public void HighlightColorTableRow(int rowIdx) + { + var oldRowIdx = HighlightedColorTableRow; + + if (HighlightedColorTableRow != rowIdx) + { + HighlightedColorTableRow = rowIdx; + HighlightTime.Restart(); + } + + if (oldRowIdx >= 0) + UpdateColorTableRowPreview(oldRowIdx); + if (rowIdx >= 0) + UpdateColorTableRowPreview(rowIdx); + } + + public void CancelColorTableHighlight() + { + var rowIdx = HighlightedColorTableRow; + + HighlightedColorTableRow = -1; + HighlightTime.Reset(); + + if (rowIdx >= 0) + UpdateColorTableRowPreview(rowIdx); + } + + public void UpdateColorTableRowPreview(int rowIdx) + { + if (ColorTablePreviewers.Count == 0) + return; + + if (!Mtrl.HasTable) + return; + + var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]); + if (Mtrl.HasDyeTable) + { + var stm = _edit._stainService.StmFile; + var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]); + if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) + row.ApplyDyeTemplate(dye, dyes); + } + + if (HighlightedColorTableRow == rowIdx) + ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorTablePreviewers) + { + row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() + .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); + previewer.ScheduleUpdate(); + } + } + + public void UpdateColorTablePreview() + { + if (ColorTablePreviewers.Count == 0) + return; + + if (!Mtrl.HasTable) + return; + + var rows = new LegacyColorTable(Mtrl.Table); + var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); + if (Mtrl.HasDyeTable) + { + var stm = _edit._stainService.StmFile; + var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) + { + ref var row = ref rows[i]; + var dye = dyeRows[i]; + if (stm.TryGetValue(dye.Template, stainId, out var dyes)) + row.ApplyDyeTemplate(dye, dyes); + } + } + + if (HighlightedColorTableRow >= 0) + ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorTablePreviewers) + { + // TODO: Dawntrail + rows.AsHalves().CopyTo(previewer.ColorTable); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyHighlight(ref LegacyColorTableRow row, float time) + { + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = ColorId.InGameHighlight.Value(); + var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); + + row.Diffuse = Vector3.Zero; + row.Specular = Vector3.Zero; + row.Emissive = color * color; + } + + public void Update() + { + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); + } + + public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) + { + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + LoadShpk(FindAssociatedShpk(out _, out _)); + if (writable) + { + _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); + BindToMaterialInstances(); + } + } + + public unsafe void Dispose() + { + UnbindFromMaterialInstances(); + if (Writable) + _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); + } + + // TODO Readd ShadersKnown + public bool Valid + => (true || ShadersKnown) && Mtrl.Valid; + + public byte[] Write() + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds); + + return output.Write(); + } + + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = new(); + } + + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + Integer = 1, + Color = 2, + Enum = 3, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public float Value = 0; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = Array.Empty(); + + public IConstantEditor? CreateEditor() + => Type switch + { + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, + Unit), + DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Factor, Bias, Unit), + DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), + DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, + value => (value.Label, value.Value, value.Description))), + _ => FloatConstantEditor.Default, + }; + + private static int? ToInteger(float? value) + => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index e2776b2f..a991c948 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -26,7 +26,7 @@ public class ResourceTreeViewer private readonly Dictionary _filterCache; private TreeCategory _categoryFilter; - private ChangedItemDrawer.ChangedItemIcon _typeFilter; + private ChangedItemIconFlag _typeFilter; private string _nameFilter; private string _nodeFilter; @@ -47,7 +47,7 @@ public class ResourceTreeViewer _filterCache = []; _categoryFilter = AllCategories; - _typeFilter = ChangedItemDrawer.AllFlags; + _typeFilter = ChangedItemFlagExtensions.AllFlags; _nameFilter = string.Empty; _nodeFilter = string.Empty; } @@ -184,13 +184,13 @@ public class ResourceTreeViewer }); private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, - ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + ChangedItemIconFlag parentFilterIconFlag) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon) + bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) { if (!_typeFilter.HasFlag(filterIcon)) return false; @@ -204,12 +204,12 @@ public class ResourceTreeViewer || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); } - NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) { if (node.Internal && !debugMode) return NodeVisibility.Hidden; - var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon; + var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; if (MatchesFilter(node, filterIcon)) return NodeVisibility.Visible; @@ -222,7 +222,7 @@ public class ResourceTreeViewer return NodeVisibility.Hidden; } - NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) { if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) { @@ -240,13 +240,13 @@ public class ResourceTreeViewer { var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); - var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon); + var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIconFlag); if (visibility == NodeVisibility.Hidden) continue; using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfTransparentText(), resourceNode.Internal); - var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; + var filterIcon = resourceNode.IconFlag != 0 ? resourceNode.IconFlag : parentFilterIconFlag; using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); @@ -277,7 +277,7 @@ public class ResourceTreeViewer ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } - _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + _changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && unfoldable) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 72bfa266..af9782d5 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -3,72 +3,24 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Lumina.Data.Files; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; +using Penumbra.GameData.Data; using Penumbra.Services; using Penumbra.UI.Classes; -using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; public class ChangedItemDrawer : IDisposable, IUiService { - [Flags] - public enum ChangedItemIcon : uint - { - Head = 0x00_00_01, - Body = 0x00_00_02, - Hands = 0x00_00_04, - Legs = 0x00_00_08, - Feet = 0x00_00_10, - Ears = 0x00_00_20, - Neck = 0x00_00_40, - Wrists = 0x00_00_80, - Finger = 0x00_01_00, - Monster = 0x00_02_00, - Demihuman = 0x00_04_00, - Customization = 0x00_08_00, - Action = 0x00_10_00, - Mainhand = 0x00_20_00, - Offhand = 0x00_40_00, - Unknown = 0x00_80_00, - Emote = 0x01_00_00, - } + private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray(); - private static readonly ChangedItemIcon[] Order = - [ - ChangedItemIcon.Head, - ChangedItemIcon.Body, - ChangedItemIcon.Hands, - ChangedItemIcon.Legs, - ChangedItemIcon.Feet, - ChangedItemIcon.Ears, - ChangedItemIcon.Neck, - ChangedItemIcon.Wrists, - ChangedItemIcon.Finger, - ChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand, - ChangedItemIcon.Customization, - ChangedItemIcon.Action, - ChangedItemIcon.Emote, - ChangedItemIcon.Monster, - ChangedItemIcon.Demihuman, - ChangedItemIcon.Unknown, - ]; - - private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray(); - - public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIcon slot) + public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIconFlag slot) { // Handle numeric cases before TryParse because numbers // are not logical otherwise. @@ -77,15 +29,15 @@ public class ChangedItemDrawer : IDisposable, IUiService // We assume users will use 1-based index, but if they enter 0, just use the first. if (idx == 0) { - slot = Order[0]; + slot = ChangedItemFlagExtensions.Order[0]; return true; } // Use 1-based index. --idx; - if (idx >= 0 && idx < Order.Length) + if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count) { - slot = Order[idx]; + slot = ChangedItemFlagExtensions.Order[idx]; return true; } } @@ -94,13 +46,13 @@ public class ChangedItemDrawer : IDisposable, IUiService return false; } - public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot) + public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot) { if (TryParseIndex(lowerInput, out slot)) return true; slot = 0; - foreach (var (item, flag) in LowerNames.Zip(Order)) + foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order)) { if (item.Contains(lowerInput, StringComparison.Ordinal)) slot |= flag; @@ -109,15 +61,11 @@ public class ChangedItemDrawer : IDisposable, IUiService return slot != 0; } - public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; - public static readonly int NumCategories = Order.Length; - public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; - private readonly Configuration _config; - private readonly ExcelSheet _items; - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - private float _smallestIconWidth; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth; public static Vector2 TypeFilterIconSize => new(2 * ImGui.GetTextLineHeight()); @@ -125,7 +73,6 @@ public class ChangedItemDrawer : IDisposable, IUiService public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { - _items = gameData.GetExcelSheet()!; uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); _communicator = communicator; _config = config; @@ -139,18 +86,19 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, object? data, LowerString filter) - => (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) - && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data))); + public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) + => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags + || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) + && (filter.IsEmpty || !data.IsFilteredOut(name, filter)); /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(string name, object? data) - => DrawCategoryIcon(GetCategoryIcon(name, data)); + public void DrawCategoryIcon(IIdentifiedObjectData? data) + => DrawCategoryIcon(data.GetIcon().ToFlag()); - public void DrawCategoryIcon(ChangedItemIcon iconType) + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) { var height = ImGui.GetFrameHeight(); - if (!_icons.TryGetValue(iconType, out var icon)) + if (!_icons.TryGetValue(iconFlagType, out var icon)) { ImGui.Dummy(new Vector2(height)); return; @@ -162,18 +110,18 @@ public class ChangedItemDrawer : IDisposable, IUiService using var tt = ImRaii.Tooltip(); ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); - ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0); + ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } } /// /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item Id in grey if requested. + /// Also draw the item ID in grey if requested. /// - public void DrawChangedItem(string name, object? data) + public void DrawChangedItem(string name, IIdentifiedObjectData? data) { - name = ChangedItemName(name, data); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) + name = data?.ToName(name) ?? name; + using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) { var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) @@ -182,31 +130,34 @@ public class ChangedItemDrawer : IDisposable, IUiService ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, Convert(data)); + _communicator.ChangedItemClick.Invoke(ret, data); } if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) { // We can not be sure that any subscriber actually prints something in any case. // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using var group = ImRaii.Group(); - _communicator.ChangedItemHover.Invoke(Convert(data)); - group.Dispose(); + using var tt = ImRaii.Tooltip(); + using (ImRaii.Group()) + { + _communicator.ChangedItemHover.Invoke(data); + } + if (ImGui.GetItemRectSize() == Vector2.Zero) ImGui.TextUnformatted("No actions available."); } } /// Draw the model information, right-justified. - public void DrawModelData(object? data) + public static void DrawModelData(IIdentifiedObjectData? data) { - if (!GetChangedItemObject(data, out var text)) + var additionalData = data?.AdditionalData ?? string.Empty; + if (additionalData.Length == 0) return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); + ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); } /// Draw a header line with the different icon types to filter them. @@ -224,7 +175,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) + public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter) { var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); @@ -232,16 +183,38 @@ public class ChangedItemDrawer : IDisposable, IUiService using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter) + foreach (var iconType in ChangedItemFlagExtensions.Order) { - var ret = false; - var icon = _icons[type]; - var flag = typeFilter.HasFlag(type); + ret |= DrawIcon(iconType, ref typeFilter); + ImGui.SameLine(); + } + + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); + ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, + typeFilter switch + { + 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), + ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f), + _ => new Vector4(0.5f, 0.5f, 1f, 1f), + }); + if (ImGui.IsItemClicked()) + { + typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags; + ret = true; + } + + return ret; + + bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter) + { + var localRet = false; + var icon = _icons[type]; + var flag = typeFilter.HasFlag(type); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { typeFilter = flag ? typeFilter & ~type : typeFilter | type; - ret = true; + localRet = true; } using var popup = ImRaii.ContextPopupItem(type.ToString()); @@ -249,7 +222,7 @@ public class ChangedItemDrawer : IDisposable, IUiService if (ImGui.MenuItem("Enable Only This")) { typeFilter = type; - ret = true; + localRet = true; ImGui.CloseCurrentPopup(); } @@ -258,165 +231,13 @@ public class ChangedItemDrawer : IDisposable, IUiService using var tt = ImRaii.Tooltip(); ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); - ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); + ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } - return ret; - } - - foreach (var iconType in Order) - { - ret |= DrawIcon(iconType, ref typeFilter); - ImGui.SameLine(); - } - - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); - ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, - typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : - typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); - if (ImGui.IsItemClicked()) - { - typeFilter = typeFilter == AllFlags ? 0 : AllFlags; - ret = true; - } - - return ret; - } - - /// Obtain the icon category corresponding to a changed item. - internal static ChangedItemIcon GetCategoryIcon(string name, object? obj) - { - var iconType = ChangedItemIcon.Unknown; - switch (obj) - { - case EquipItem it: - iconType = GetCategoryIcon(it.Type.ToSlot()); - break; - case ModelChara m: - iconType = (CharacterBase.ModelType)m.Type switch - { - CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman, - CharacterBase.ModelType.Monster => ChangedItemIcon.Monster, - _ => ChangedItemIcon.Unknown, - }; - break; - default: - { - if (name.StartsWith("Action: ")) - iconType = ChangedItemIcon.Action; - else if (name.StartsWith("Emote: ")) - iconType = ChangedItemIcon.Emote; - else if (name.StartsWith("Customization: ")) - iconType = ChangedItemIcon.Customization; - break; - } - } - - return iconType; - } - - internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => ChangedItemIcon.Mainhand, - EquipSlot.OffHand => ChangedItemIcon.Offhand, - EquipSlot.Head => ChangedItemIcon.Head, - EquipSlot.Body => ChangedItemIcon.Body, - EquipSlot.Hands => ChangedItemIcon.Hands, - EquipSlot.Legs => ChangedItemIcon.Legs, - EquipSlot.Feet => ChangedItemIcon.Feet, - EquipSlot.Ears => ChangedItemIcon.Ears, - EquipSlot.Neck => ChangedItemIcon.Neck, - EquipSlot.Wrists => ChangedItemIcon.Wrists, - EquipSlot.RFinger => ChangedItemIcon.Finger, - _ => ChangedItemIcon.Unknown, - }; - - /// Return more detailed object information in text, if it exists. - private static bool GetChangedItemObject(object? obj, out string text) - { - switch (obj) - { - case EquipItem it: - text = it.ModelString; - return true; - case ModelChara m: - text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; - return true; - default: - text = string.Empty; - return false; + return localRet; } } - /// We need to transform the internal EquipItem type to the Lumina Item type for API-events. - private object? Convert(object? data) - { - if (data is EquipItem it) - return (_items.GetRow(it.ItemId.Id), it.Type); - - return data; - } - - private static string ToDescription(ChangedItemIcon icon) - => icon switch - { - ChangedItemIcon.Head => EquipSlot.Head.ToName(), - ChangedItemIcon.Body => EquipSlot.Body.ToName(), - ChangedItemIcon.Hands => EquipSlot.Hands.ToName(), - ChangedItemIcon.Legs => EquipSlot.Legs.ToName(), - ChangedItemIcon.Feet => EquipSlot.Feet.ToName(), - ChangedItemIcon.Ears => EquipSlot.Ears.ToName(), - ChangedItemIcon.Neck => EquipSlot.Neck.ToName(), - ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(), - ChangedItemIcon.Finger => "Ring", - ChangedItemIcon.Monster => "Monster", - ChangedItemIcon.Demihuman => "Demi-Human", - ChangedItemIcon.Customization => "Customization", - ChangedItemIcon.Action => "Action", - ChangedItemIcon.Emote => "Emote", - ChangedItemIcon.Mainhand => "Weapon (Mainhand)", - ChangedItemIcon.Offhand => "Weapon (Offhand)", - _ => "Other", - }; - - internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon) - => icon switch - { - ChangedItemIcon.Head => ApiChangedItemIcon.Head, - ChangedItemIcon.Body => ApiChangedItemIcon.Body, - ChangedItemIcon.Hands => ApiChangedItemIcon.Hands, - ChangedItemIcon.Legs => ApiChangedItemIcon.Legs, - ChangedItemIcon.Feet => ApiChangedItemIcon.Feet, - ChangedItemIcon.Ears => ApiChangedItemIcon.Ears, - ChangedItemIcon.Neck => ApiChangedItemIcon.Neck, - ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists, - ChangedItemIcon.Finger => ApiChangedItemIcon.Finger, - ChangedItemIcon.Monster => ApiChangedItemIcon.Monster, - ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman, - ChangedItemIcon.Customization => ApiChangedItemIcon.Customization, - ChangedItemIcon.Action => ApiChangedItemIcon.Action, - ChangedItemIcon.Emote => ApiChangedItemIcon.Emote, - ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand, - ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown, - _ => ApiChangedItemIcon.None, - }; - - /// Apply Changed Item Counters to the Name if necessary. - private static string ChangedItemName(string name, object? data) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; - - /// Add filterable information to the string. - private static string ChangedItemFilterName(string name, object? data) - => data switch - { - int counter => $"{counter} Files Manipulating {name}s", - EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", - ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", - _ => name, - }; - /// Initialize the icons. private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) { @@ -425,30 +246,30 @@ public class ChangedItemDrawer : IDisposable, IUiService if (!equipTypeIcons.Valid) return false; - void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex) + void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex) { if (tex != null) _icons.Add(icon, tex); } - Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); - Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); - Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); - Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); - Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); - Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); - Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); - Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); - Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); - Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); - Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062042_hr1.tex")!)); - Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062041_hr1.tex")!)); - Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); - Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); - Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider)); - Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider)); - Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); + Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); + Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); + Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); + Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); + Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); + Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); + Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); + Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); + Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); + Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); + Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); + Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062044_hr1.tex")!)); + Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); + Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062045_hr1.tex")!)); + Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); + Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider)); + Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider)); + Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); _smallestIconWidth = _icons.Values.Min(i => i.Width); @@ -487,6 +308,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon"); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, + "Penumbra.EmoteItemIcon"); } } diff --git a/Penumbra/UI/ChangedItemIconFlag.cs b/Penumbra/UI/ChangedItemIconFlag.cs new file mode 100644 index 00000000..fc7073f2 --- /dev/null +++ b/Penumbra/UI/ChangedItemIconFlag.cs @@ -0,0 +1,122 @@ +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; + +namespace Penumbra.UI; + +[Flags] +public enum ChangedItemIconFlag : uint +{ + Head = 0x00_00_01, + Body = 0x00_00_02, + Hands = 0x00_00_04, + Legs = 0x00_00_08, + Feet = 0x00_00_10, + Ears = 0x00_00_20, + Neck = 0x00_00_40, + Wrists = 0x00_00_80, + Finger = 0x00_01_00, + Monster = 0x00_02_00, + Demihuman = 0x00_04_00, + Customization = 0x00_08_00, + Action = 0x00_10_00, + Mainhand = 0x00_20_00, + Offhand = 0x00_40_00, + Unknown = 0x00_80_00, + Emote = 0x01_00_00, +} + +public static class ChangedItemFlagExtensions +{ + public static readonly IReadOnlyList Order = + [ + ChangedItemIconFlag.Head, + ChangedItemIconFlag.Body, + ChangedItemIconFlag.Hands, + ChangedItemIconFlag.Legs, + ChangedItemIconFlag.Feet, + ChangedItemIconFlag.Ears, + ChangedItemIconFlag.Neck, + ChangedItemIconFlag.Wrists, + ChangedItemIconFlag.Finger, + ChangedItemIconFlag.Mainhand, + ChangedItemIconFlag.Offhand, + ChangedItemIconFlag.Customization, + ChangedItemIconFlag.Action, + ChangedItemIconFlag.Emote, + ChangedItemIconFlag.Monster, + ChangedItemIconFlag.Demihuman, + ChangedItemIconFlag.Unknown, + ]; + + public const ChangedItemIconFlag AllFlags = (ChangedItemIconFlag)0x01FFFF; + public static readonly int NumCategories = Order.Count; + public const ChangedItemIconFlag DefaultFlags = AllFlags & ~ChangedItemIconFlag.Offhand; + + public static string ToDescription(this ChangedItemIconFlag iconFlag) + => iconFlag switch + { + ChangedItemIconFlag.Head => EquipSlot.Head.ToName(), + ChangedItemIconFlag.Body => EquipSlot.Body.ToName(), + ChangedItemIconFlag.Hands => EquipSlot.Hands.ToName(), + ChangedItemIconFlag.Legs => EquipSlot.Legs.ToName(), + ChangedItemIconFlag.Feet => EquipSlot.Feet.ToName(), + ChangedItemIconFlag.Ears => EquipSlot.Ears.ToName(), + ChangedItemIconFlag.Neck => EquipSlot.Neck.ToName(), + ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToName(), + ChangedItemIconFlag.Finger => "Ring", + ChangedItemIconFlag.Monster => "Monster", + ChangedItemIconFlag.Demihuman => "Demi-Human", + ChangedItemIconFlag.Customization => "Customization", + ChangedItemIconFlag.Action => "Action", + ChangedItemIconFlag.Emote => "Emote", + ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)", + ChangedItemIconFlag.Offhand => "Weapon (Offhand)", + _ => "Other", + }; + + public static ChangedItemIcon ToApiIcon(this ChangedItemIconFlag iconFlag) + => iconFlag switch + { + ChangedItemIconFlag.Head => ChangedItemIcon.Head, + ChangedItemIconFlag.Body => ChangedItemIcon.Body, + ChangedItemIconFlag.Hands => ChangedItemIcon.Hands, + ChangedItemIconFlag.Legs => ChangedItemIcon.Legs, + ChangedItemIconFlag.Feet => ChangedItemIcon.Feet, + ChangedItemIconFlag.Ears => ChangedItemIcon.Ears, + ChangedItemIconFlag.Neck => ChangedItemIcon.Neck, + ChangedItemIconFlag.Wrists => ChangedItemIcon.Wrists, + ChangedItemIconFlag.Finger => ChangedItemIcon.Finger, + ChangedItemIconFlag.Monster => ChangedItemIcon.Monster, + ChangedItemIconFlag.Demihuman => ChangedItemIcon.Demihuman, + ChangedItemIconFlag.Customization => ChangedItemIcon.Customization, + ChangedItemIconFlag.Action => ChangedItemIcon.Action, + ChangedItemIconFlag.Emote => ChangedItemIcon.Emote, + ChangedItemIconFlag.Mainhand => ChangedItemIcon.Mainhand, + ChangedItemIconFlag.Offhand => ChangedItemIcon.Offhand, + ChangedItemIconFlag.Unknown => ChangedItemIcon.Unknown, + _ => ChangedItemIcon.None, + }; + + public static ChangedItemIconFlag ToFlag(this ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Unknown => ChangedItemIconFlag.Unknown, + ChangedItemIcon.Head => ChangedItemIconFlag.Head, + ChangedItemIcon.Body => ChangedItemIconFlag.Body, + ChangedItemIcon.Hands => ChangedItemIconFlag.Hands, + ChangedItemIcon.Legs => ChangedItemIconFlag.Legs, + ChangedItemIcon.Feet => ChangedItemIconFlag.Feet, + ChangedItemIcon.Ears => ChangedItemIconFlag.Ears, + ChangedItemIcon.Neck => ChangedItemIconFlag.Neck, + ChangedItemIcon.Wrists => ChangedItemIconFlag.Wrists, + ChangedItemIcon.Finger => ChangedItemIconFlag.Finger, + ChangedItemIcon.Mainhand => ChangedItemIconFlag.Mainhand, + ChangedItemIcon.Offhand => ChangedItemIconFlag.Offhand, + ChangedItemIcon.Customization => ChangedItemIconFlag.Customization, + ChangedItemIcon.Monster => ChangedItemIconFlag.Monster, + ChangedItemIcon.Demihuman => ChangedItemIconFlag.Demihuman, + ChangedItemIcon.Action => ChangedItemIconFlag.Action, + ChangedItemIcon.Emote => ChangedItemIconFlag.Emote, + _ => ChangedItemIconFlag.Unknown, + }; +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 55405313..42689efb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -550,7 +550,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector)selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); var height = ImGui.GetFrameHeightWithSpacing(); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(height); @@ -32,15 +33,15 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem ImGuiClip.DrawEndDummy(remainder, height); } - private bool CheckFilter((string Name, object? Data) kvp) + private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); - private void DrawChangedItem((string Name, object? Data) kvp) + private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + drawer.DrawCategoryIcon(kvp.Data); ImGui.SameLine(); drawer.DrawChangedItem(kvp.Name, kvp.Data); - drawer.DrawModelData(kvp.Data); + ChangedItemDrawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs index e7550eea..1eff1919 100644 --- a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -19,16 +19,16 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter { - public string Needle { get; init; } - public ModSearchType Type { get; init; } - public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; } + public string Needle { get; init; } + public ModSearchType Type { get; init; } + public ChangedItemIconFlag IconFlagFilter { get; init; } public bool Contains(Entry other) { if (Type != other.Type) return false; if (Type is ModSearchType.Category) - return IconFilter == other.IconFilter; + return IconFlagFilter == other.IconFlagFilter; return Needle.Contains(other.Needle); } @@ -77,7 +77,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), ModSearchType.Category => leaf.Value.ChangedItems.Any(p - => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0), + => ((p.Value?.Icon.ToFlag() ?? ChangedItemIconFlag.Unknown) & entry.IconFlagFilter) != 0), _ => true, }; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 2aeaaea0..256b0d79 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -6,6 +6,7 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; +using Penumbra.GameData.Data; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -66,22 +67,22 @@ public class ChangedItemsTab( } /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, object?)> item) + private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData?)> item) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, object?)> item) + private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData?)> item) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Value.Item2); ImGui.SameLine(); drawer.DrawChangedItem(item.Key, item.Value.Item2); ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - drawer.DrawModelData(item.Value.Item2); + ChangedItemDrawer.DrawModelData(item.Value.Item2); } private void DrawModColumn(SingleArray mods) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 41ca8d6e..27c7f2ed 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -432,7 +432,7 @@ public class SettingsTab : ITab, IUiService _config.HideChangedItemFilters = v; if (v) { - _config.Ephemeral.ChangedItemFilter = ChangedItemDrawer.AllFlags; + _config.Ephemeral.ChangedItemFilter = ChangedItemFlagExtensions.AllFlags; _config.Ephemeral.Save(); } }); diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index cb43ac06..5bd3f77c 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -10,7 +10,7 @@ namespace Penumbra.Util; public static class IdentifierExtensions { public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, - IDictionary changedItems) + IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); @@ -19,25 +19,25 @@ public static class IdentifierExtensions manip.AddChangedItems(identifier, changedItems); } - public static void RemoveMachinistOffhands(this SortedList changedItems) + public static void RemoveMachinistOffhands(this SortedList changedItems) { for (var i = 0; i < changedItems.Count; i++) { { var value = changedItems.Values[i]; - if (value is EquipItem { Type: FullEquipType.GunOff }) + if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff }) changedItems.RemoveAt(i--); } } } - public static void RemoveMachinistOffhands(this SortedList, object?)> changedItems) + public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData?)> changedItems) { for (var i = 0; i < changedItems.Count; i++) { { var value = changedItems.Values[i].Item2; - if (value is EquipItem { Type: FullEquipType.GunOff }) + if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff }) changedItems.RemoveAt(i--); } }