diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 197de0bb..7595353f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,10 +5,15 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexSkinShpk = 83; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -97,8 +102,23 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexDecalTex * 8)] public TextureResourceHandle* DecalTexResource; + [FieldOffset(8 + IndexTileOrbArrayTex * 8)] + public TextureResourceHandle* TileOrbArrayTexResource; + + [FieldOffset(8 + IndexTileNormArrayTex * 8)] + public TextureResourceHandle* TileNormArrayTexResource; + [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] + public ResourceHandle* GudStmResource; + + [FieldOffset(8 + IndexLegacyStm * 8)] + public ResourceHandle* LegacyStmResource; + + [FieldOffset(8 + IndexSphereDArrayTex * 8)] + public TextureResourceHandle* SphereDArrayTexResource; + // not included resources have no known use case. } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 26b39229..50713968 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -6,19 +6,25 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; -using Penumbra.UI.AdvancedWindow; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.Services; public class StainService : IService { - public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { + // FIXME There might be a better way to handle that. + public int CurrentDyeChannel = 0; + protected override float GetFilterWidth() { var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; - if (stainCombo.CurrentSelection.Key == 0) + if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) return baseSize; return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; @@ -47,33 +53,73 @@ public class StainService : IService protected override bool DrawSelectable(int globalIdx, bool selected) { var ret = base.DrawSelectable(globalIdx, selected); - var selection = stainCombo.CurrentSelection.Key; + var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) return ret; ImGui.SameLine(); var frame = new Vector2(ImGui.GetTextLineHeight()); - ImGui.ColorButton("D", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Diffuse), 1), 0, frame); + ImGui.ColorButton("D", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("S", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Specular), 1), 0, frame); + ImGui.ColorButton("S", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("E", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Emissive), 1), 0, frame); + ImGui.ColorButton("E", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); return ret; } } - public readonly DictStain StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; - public readonly StainTemplateCombo TemplateCombo; + public const int ChannelCount = 2; - public StainService(IDataManager dataManager, DictStain stainData) + public readonly DictStain StainData; + public readonly FilterComboColors StainCombo1; + public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this? + public readonly StmFile LegacyStmFile; + public readonly StmFile GudStmFile; + public readonly StainTemplateCombo LegacyTemplateCombo; + public readonly StainTemplateCombo GudTemplateCombo; + + public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo = new FilterComboColors(140, MouseWheelType.None, - () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), - Penumbra.Log); - StmFile = new StmFile(dataManager); - TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); + StainData = stainData; + StainCombo1 = CreateStainCombo(); + StainCombo2 = CreateStainCombo(); + LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); + GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + + FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; + + LegacyTemplateCombo = new StainTemplateCombo(stainCombos, LegacyStmFile); + GudTemplateCombo = new StainTemplateCombo(stainCombos, GudStmFile); } + + /// Retrieves the instance for the given channel. Indexing is zero-based. + public FilterComboColors GetStainCombo(int channel) + => channel switch + { + 0 => StainCombo1, + 1 => StainCombo2, + _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)") + }; + + /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + { + if (stmResourceHandle != null) + { + var stmData = stmResourceHandle->CsHandle.GetDataSpan(); + if (stmData.Length > 0) + { + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from ResourceHandle 0x{(nint)stmResourceHandle:X}"); + return new StmFile(stmData); + } + } + + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from Lumina"); + return new StmFile(dataManager); + } + + private FilterComboColors CreateStainCombo() + => new(140, MouseWheelType.None, + () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + Penumbra.Log); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 3a64e556..ead02874 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,9 @@ using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -697,32 +700,48 @@ public class DebugTab : Window, ITab, IUiService if (!mainTree) return; - foreach (var (key, data) in _stains.StmFile.Entries) + using (var legacyTree = TreeNode("stainingtemplate.stm")) + { + if (legacyTree) + DrawStainTemplatesFile(_stains.LegacyStmFile); + } + + using (var gudTree = TreeNode("stainingtemplate_gud.stm")) + { + if (gudTree) + DrawStainTemplatesFile(_stains.GudStmFile); + } + } + + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + { + foreach (var (key, data) in stmFile.Entries) { using var tree = TreeNode($"Template {key}"); if (!tree) continue; - using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; - for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) + for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) { - var (r, g, b) = data.DiffuseEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + foreach (var list in data.Colors) + { + var color = list[i]; + ImGui.TableNextColumn(); + var frame = new Vector2(ImGui.GetTextLineHeight()); + ImGui.ColorButton("###color", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)color), 1), 0, frame); + ImGui.SameLine(); + ImGui.TextUnformatted($"{color.Red:F6} | {color.Green:F6} | {color.Blue:F6}"); + } - (r, g, b) = data.SpecularEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - (r, g, b) = data.EmissiveEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - var a = data.SpecularPowerEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); - - a = data.GlossEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); + foreach (var list in data.Scalars) + { + var scalar = list[i]; + ImGuiUtil.DrawTableColumn($"{scalar:F6}"); + } } } }