From 2a0e6ce1aa61b8d882364812b3a929cc50697fa3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 02:48:11 +1100 Subject: [PATCH 01/13] WIP .mdl updates --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 113 ++++++++++++++++-- 1 file changed, 101 insertions(+), 12 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b95ba393..001e1c78 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,6 +1,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -10,28 +12,115 @@ public partial class ModEditWindow { private readonly FileEditor _modelTab; + private static List _submeshAttributeTagWidgets = new(); + private static bool DrawModelPanel(MdlFile file, bool disabled) { - var ret = false; - for (var i = 0; i < file.Materials.Length; ++i) + var submeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_submeshAttributeTagWidgets.Count != submeshTotal) { - using var id = ImRaii.PushId(i); - var tmp = file.Materials[i]; - if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - && tmp.Length > 0 - && tmp != file.Materials[i]) - { - file.Materials[i] = tmp; - ret = true; - } + _submeshAttributeTagWidgets.Clear(); + _submeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, submeshTotal).Select(_ => new TagButtons()) + ); } + var ret = false; + + for (var i = 0; i < file.Meshes.Length; ++i) + ret |= DrawMeshDetails(file, i, disabled); + ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } + private static bool DrawMeshDetails(MdlFile file, int meshIndex, bool disabled) + { + if (!ImGui.CollapsingHeader($"Mesh {meshIndex}")) + return false; + + using var id = ImRaii.PushId(meshIndex); + + var mesh = file.Meshes[meshIndex]; + + var ret = false; + + // Mesh material. + var temp = file.Materials[mesh.MaterialIndex]; + if ( + ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && temp.Length > 0 + && temp != file.Materials[mesh.MaterialIndex] + ) { + file.Materials[mesh.MaterialIndex] = temp; + ret = true; + } + + // Submeshes. + for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) + ret |= DrawSubMeshDetails(file, mesh.SubMeshIndex + submeshOffset, disabled); + + return ret; + } + + private static bool DrawSubMeshDetails(MdlFile file, int submeshIndex, bool disabled) + { + using var id = ImRaii.PushId(submeshIndex); + + var submesh = file.SubMeshes[submeshIndex]; + var widget = _submeshAttributeTagWidgets[submeshIndex]; + + var attributes = Enumerable + .Range(0, 32) + .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) + .Select(index => file.Attributes[index]) + .ToArray(); + + UiHelpers.DefaultLineSpace(); + var tagIndex = widget.Draw($"Submesh {submeshIndex} Attributes", "", attributes, out var editedAttribute, !disabled); + if (tagIndex >= 0) + { + // Eagerly remove the edited attribute from the attribute mask. + if (tagIndex < attributes.Length) + { + var previousAttributeIndex = file.Attributes.IndexOf(attributes[tagIndex]); + submesh.AttributeIndexMask &= ~(1u << previousAttributeIndex); + + // If no other submeshes use this attribute, remove it. + var usages = file.SubMeshes + .Where(submesh => ((submesh.AttributeIndexMask >> previousAttributeIndex) & 1) == 1) + .Count(); + if (usages <= 1) + { + // TODO THIS BLOWS UP ALL OTHER INDICES BEYOND WHAT WE JUST REMOVED - I NEED TO VIRTUALISE THIS SHIT + // file.Attributes = file.Attributes.RemoveItems(previousAttributeIndex); + } + } + + // If there's a new or edited name, add it to the mask, and the attribute list if it's not already known. + if (editedAttribute != "") + { + var attributeIndex = file.Attributes.IndexOf(editedAttribute); + if (attributeIndex == -1) + { + file.Attributes.AddItem(editedAttribute); + attributeIndex = file.Attributes.Length - 1; + } + submesh.AttributeIndexMask |= 1u << attributeIndex; + } + + file.SubMeshes[submeshIndex] = submesh; + + return true; + } + + ImGui.SameLine(); + ImGui.Text($"{Convert.ToString(submesh.AttributeIndexMask, 2)}"); + + return false; + } + private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 49b63d2208bf8b4c62bc02f83a7b447c951d8410 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 03:32:44 +1100 Subject: [PATCH 02/13] Draw the rest of the owl --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 001e1c78..8edc4082 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -71,56 +72,73 @@ public partial class ModEditWindow var submesh = file.SubMeshes[submeshIndex]; var widget = _submeshAttributeTagWidgets[submeshIndex]; - var attributes = Enumerable - .Range(0, 32) - .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) - .Select(index => file.Attributes[index]) - .ToArray(); + var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); UiHelpers.DefaultLineSpace(); var tagIndex = widget.Draw($"Submesh {submeshIndex} Attributes", "", attributes, out var editedAttribute, !disabled); if (tagIndex >= 0) { - // Eagerly remove the edited attribute from the attribute mask. - if (tagIndex < attributes.Length) - { - var previousAttributeIndex = file.Attributes.IndexOf(attributes[tagIndex]); - submesh.AttributeIndexMask &= ~(1u << previousAttributeIndex); - - // If no other submeshes use this attribute, remove it. - var usages = file.SubMeshes - .Where(submesh => ((submesh.AttributeIndexMask >> previousAttributeIndex) & 1) == 1) - .Count(); - if (usages <= 1) - { - // TODO THIS BLOWS UP ALL OTHER INDICES BEYOND WHAT WE JUST REMOVED - I NEED TO VIRTUALISE THIS SHIT - // file.Attributes = file.Attributes.RemoveItems(previousAttributeIndex); - } - } - - // If there's a new or edited name, add it to the mask, and the attribute list if it's not already known. - if (editedAttribute != "") - { - var attributeIndex = file.Attributes.IndexOf(editedAttribute); - if (attributeIndex == -1) - { - file.Attributes.AddItem(editedAttribute); - attributeIndex = file.Attributes.Length - 1; - } - submesh.AttributeIndexMask |= 1u << attributeIndex; - } - - file.SubMeshes[submeshIndex] = submesh; + EditSubmeshAttribute( + file, + submeshIndex, + tagIndex < attributes.Length ? attributes[tagIndex] : null, + editedAttribute != "" ? editedAttribute : null + ); return true; } - ImGui.SameLine(); - ImGui.Text($"{Convert.ToString(submesh.AttributeIndexMask, 2)}"); - return false; } + private static void EditSubmeshAttribute(MdlFile file, int changedSubmeshIndex, string? old, string? new_) + { + // Build a hydrated view of all attributes in the model + var submeshAttributes = file.SubMeshes + .Select(submesh => HydrateAttributes(file, submesh.AttributeIndexMask).ToList()) + .ToArray(); + + // Make changes to the submesh we're actually editing here. + var changedSubmesh = submeshAttributes[changedSubmeshIndex]; + + if (old != null) + changedSubmesh.Remove(old); + + if (new_ != null) + changedSubmesh.Add(new_); + + // Re-serialize all the attributes. + var allAttributes = new List(); + foreach (var (attributes, submeshIndex) in submeshAttributes.WithIndex()) + { + var mask = 0u; + + foreach (var attribute in attributes) + { + var attributeIndex = allAttributes.IndexOf(attribute); + if (attributeIndex == -1) + { + allAttributes.Add(attribute); + attributeIndex = allAttributes.Count() - 1; + } + + mask |= 1u << attributeIndex; + } + + file.SubMeshes[submeshIndex].AttributeIndexMask = mask; + } + + file.Attributes = allAttributes.ToArray(); + } + + private static IEnumerable HydrateAttributes(MdlFile file, uint mask) + { + return Enumerable + .Range(0, 32) + .Where(index => ((mask >> index) & 1) == 1) + .Select(index => file.Attributes[index]); + } + private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 8ba20218c620cef270d4cfcdf0ccb1591d5b5ef0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 03:35:17 +1100 Subject: [PATCH 03/13] whoops --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 8edc4082..b41dbf0c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,9 +1,7 @@ using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; From 27123f2a640ee93908344e0351b3080a2808ca42 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 19:00:06 +1100 Subject: [PATCH 04/13] Inline submesh UI, fix visual offset --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b41dbf0c..d43ae55b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -58,35 +58,32 @@ public partial class ModEditWindow // Submeshes. for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) - ret |= DrawSubMeshDetails(file, mesh.SubMeshIndex + submeshOffset, disabled); - - return ret; - } - - private static bool DrawSubMeshDetails(MdlFile file, int submeshIndex, bool disabled) - { - using var id = ImRaii.PushId(submeshIndex); - - var submesh = file.SubMeshes[submeshIndex]; - var widget = _submeshAttributeTagWidgets[submeshIndex]; - - var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); - - UiHelpers.DefaultLineSpace(); - var tagIndex = widget.Draw($"Submesh {submeshIndex} Attributes", "", attributes, out var editedAttribute, !disabled); - if (tagIndex >= 0) { - EditSubmeshAttribute( - file, - submeshIndex, - tagIndex < attributes.Length ? attributes[tagIndex] : null, - editedAttribute != "" ? editedAttribute : null - ); + using var submeshId = ImRaii.PushId(submeshOffset); - return true; + var submeshIndex = mesh.SubMeshIndex + submeshOffset; + + var submesh = file.SubMeshes[submeshIndex]; + var widget = _submeshAttributeTagWidgets[submeshIndex]; + + var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); + + UiHelpers.DefaultLineSpace(); + var tagIndex = widget.Draw($"Submesh {submeshOffset} Attributes", "", attributes, out var editedAttribute, !disabled); + if (tagIndex >= 0) + { + EditSubmeshAttribute( + file, + submeshIndex, + tagIndex < attributes.Length ? attributes[tagIndex] : null, + editedAttribute != "" ? editedAttribute : null + ); + + ret = true; + } } - return false; + return ret; } private static void EditSubmeshAttribute(MdlFile file, int changedSubmeshIndex, string? old, string? new_) From f04b2959891252453310a1eb9bec5c80ac7dba3a Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 19:06:55 +1100 Subject: [PATCH 05/13] Scaffold tab file --- .../ModEditWindow.Models.MdlTab.cs | 20 +++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 6 ++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs new file mode 100644 index 00000000..aeae20cc --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -0,0 +1,20 @@ +using Penumbra.GameData.Files; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private class MdlTab : IWritable + { + public readonly MdlFile Mdl; + + public MdlTab(byte[] bytes) + { + Mdl = new MdlFile(bytes); + } + + public bool Valid => Mdl.Valid; + + public byte[] Write() => Mdl.Write(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index d43ae55b..bcfc77ad 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -9,12 +9,14 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly FileEditor _modelTab; + private readonly FileEditor _modelTab; private static List _submeshAttributeTagWidgets = new(); - private static bool DrawModelPanel(MdlFile file, bool disabled) + private static bool DrawModelPanel(MdlTab tab, bool disabled) { + var file = tab.Mdl; + var submeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); if (_submeshAttributeTagWidgets.Count != submeshTotal) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index db9201ca..365c4a4a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -584,8 +584,8 @@ public partial class ModEditWindow : Window, IDisposable _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); - _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); + _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlTab(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); From 28246244cd04c37689278a2c13259fc17508bf68 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 20:25:01 +1100 Subject: [PATCH 06/13] Move persitence logic to tab file --- .../ModEditWindow.Models.MdlTab.cs | 87 +++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 69 ++------------- 2 files changed, 96 insertions(+), 60 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index aeae20cc..3ba39543 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; +using OtterGui; using Penumbra.GameData.Files; namespace Penumbra.UI.AdvancedWindow; @@ -8,9 +10,94 @@ public partial class ModEditWindow { public readonly MdlFile Mdl; + private List _materials; + private List[] _attributes; + public MdlTab(byte[] bytes) { Mdl = new MdlFile(bytes); + + _materials = Mdl.Meshes.Select(mesh => Mdl.Materials[mesh.MaterialIndex]).ToList(); + _attributes = HydrateAttributes(Mdl); + } + + private List[] HydrateAttributes(MdlFile mdl) + { + return mdl.SubMeshes.Select(submesh => + Enumerable.Range(0,32) + .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) + .Select(index => mdl.Attributes[index]) + .ToList() + ).ToArray(); + } + + public string GetMeshMaterial(int meshIndex) => _materials[meshIndex]; + + public void SetMeshMaterial(int meshIndex, string materialPath) + { + _materials[meshIndex] = materialPath; + + PersistMaterials(); + } + + private void PersistMaterials() + { + var allMaterials = new List(); + + foreach (var (material, meshIndex) in _materials.WithIndex()) + { + var materialIndex = allMaterials.IndexOf(material); + if (materialIndex == -1) + { + allMaterials.Add(material); + materialIndex = allMaterials.Count() - 1; + } + + Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + } + + Mdl.Materials = allMaterials.ToArray(); + } + + public IReadOnlyCollection GetSubmeshAttributes(int submeshIndex) => _attributes[submeshIndex]; + + public void UpdateSubmeshAttribute(int submeshIndex, string? old, string? new_) + { + var attributes = _attributes[submeshIndex]; + + if (old != null) + attributes.Remove(old); + + if (new_ != null) + attributes.Add(new_); + + PersistAttributes(); + } + + private void PersistAttributes() + { + var allAttributes = new List(); + + foreach (var (attributes, submeshIndex) in _attributes.WithIndex()) + { + var mask = 0u; + + foreach (var attribute in attributes) + { + var attributeIndex = allAttributes.IndexOf(attribute); + if (attributeIndex == -1) + { + allAttributes.Add(attribute); + attributeIndex = allAttributes.Count() - 1; + } + + mask |= 1u << attributeIndex; + } + + Mdl.SubMeshes[submeshIndex].AttributeIndexMask = mask; + } + + Mdl.Attributes = allAttributes.ToArray(); } public bool Valid => Mdl.Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bcfc77ad..0a4c9c1b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -29,32 +29,33 @@ public partial class ModEditWindow var ret = false; for (var i = 0; i < file.Meshes.Length; ++i) - ret |= DrawMeshDetails(file, i, disabled); + ret |= DrawMeshDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } - private static bool DrawMeshDetails(MdlFile file, int meshIndex, bool disabled) + private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) { if (!ImGui.CollapsingHeader($"Mesh {meshIndex}")) return false; using var id = ImRaii.PushId(meshIndex); + var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; var ret = false; // Mesh material. - var temp = file.Materials[mesh.MaterialIndex]; + var temp = tab.GetMeshMaterial(meshIndex); if ( ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) && temp.Length > 0 - && temp != file.Materials[mesh.MaterialIndex] + && temp != tab.GetMeshMaterial(meshIndex) ) { - file.Materials[mesh.MaterialIndex] = temp; + tab.SetMeshMaterial(meshIndex, temp); ret = true; } @@ -64,20 +65,16 @@ public partial class ModEditWindow using var submeshId = ImRaii.PushId(submeshOffset); var submeshIndex = mesh.SubMeshIndex + submeshOffset; - - var submesh = file.SubMeshes[submeshIndex]; var widget = _submeshAttributeTagWidgets[submeshIndex]; - - var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); + var attributes = tab.GetSubmeshAttributes(submeshIndex); UiHelpers.DefaultLineSpace(); var tagIndex = widget.Draw($"Submesh {submeshOffset} Attributes", "", attributes, out var editedAttribute, !disabled); if (tagIndex >= 0) { - EditSubmeshAttribute( - file, + tab.UpdateSubmeshAttribute( submeshIndex, - tagIndex < attributes.Length ? attributes[tagIndex] : null, + tagIndex < attributes.Count() ? attributes.ElementAt(tagIndex) : null, editedAttribute != "" ? editedAttribute : null ); @@ -88,54 +85,6 @@ public partial class ModEditWindow return ret; } - private static void EditSubmeshAttribute(MdlFile file, int changedSubmeshIndex, string? old, string? new_) - { - // Build a hydrated view of all attributes in the model - var submeshAttributes = file.SubMeshes - .Select(submesh => HydrateAttributes(file, submesh.AttributeIndexMask).ToList()) - .ToArray(); - - // Make changes to the submesh we're actually editing here. - var changedSubmesh = submeshAttributes[changedSubmeshIndex]; - - if (old != null) - changedSubmesh.Remove(old); - - if (new_ != null) - changedSubmesh.Add(new_); - - // Re-serialize all the attributes. - var allAttributes = new List(); - foreach (var (attributes, submeshIndex) in submeshAttributes.WithIndex()) - { - var mask = 0u; - - foreach (var attribute in attributes) - { - var attributeIndex = allAttributes.IndexOf(attribute); - if (attributeIndex == -1) - { - allAttributes.Add(attribute); - attributeIndex = allAttributes.Count() - 1; - } - - mask |= 1u << attributeIndex; - } - - file.SubMeshes[submeshIndex].AttributeIndexMask = mask; - } - - file.Attributes = allAttributes.ToArray(); - } - - private static IEnumerable HydrateAttributes(MdlFile file, uint mask) - { - return Enumerable - .Range(0, 32) - .Where(index => ((mask >> index) & 1) == 1) - .Select(index => file.Attributes[index]); - } - private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 7ef50f7bb4767441387db5c9af1c71ac30511c28 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 20:28:29 +1100 Subject: [PATCH 07/13] Add material list to further content --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0a4c9c1b..1960614a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -135,6 +135,13 @@ public partial class ModEditWindow } } + using (var materials = ImRaii.TreeNode("Materials", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (materials) + foreach (var material in file.Materials) + ImRaii.TreeNode(material, ImGuiTreeNodeFlags.Leaf).Dispose(); + } + using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) From 17e6838422965a83056726a1388cdaaacc1f12a8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 20:52:14 +1100 Subject: [PATCH 08/13] Swap to tree nodes for more compact UX --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1960614a..7e2f8f5f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -28,8 +28,9 @@ public partial class ModEditWindow var ret = false; - for (var i = 0; i < file.Meshes.Length; ++i) - ret |= DrawMeshDetails(tab, i, disabled); + if (ImGui.CollapsingHeader($"{file.Meshes.Length} Meshes###meshes")) + for (var i = 0; i < file.Meshes.Length; ++i) + ret |= DrawMeshDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); @@ -38,7 +39,8 @@ public partial class ModEditWindow private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) { - if (!ImGui.CollapsingHeader($"Mesh {meshIndex}")) + using var meshNode = ImRaii.TreeNode($"Mesh {meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); + if (!meshNode) return false; using var id = ImRaii.PushId(meshIndex); From 72f57d292b074d710f7c3325773669a1e6b1f6c7 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 22 Dec 2023 03:34:20 +1100 Subject: [PATCH 09/13] group meshes by lod --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 7e2f8f5f..c4c86e24 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -28,15 +28,31 @@ public partial class ModEditWindow var ret = false; - if (ImGui.CollapsingHeader($"{file.Meshes.Length} Meshes###meshes")) - for (var i = 0; i < file.Meshes.Length; ++i) - ret |= DrawMeshDetails(tab, i, disabled); + if (ImGui.CollapsingHeader($"Meshes ({file.Meshes.Length})###meshes")) + for (var i = 0; i < file.LodCount; ++i) + ret |= DrawLodDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } + private static bool DrawLodDetails(MdlTab tab, int lodIndex, bool disabled) + { + using var lodNode = ImRaii.TreeNode($"LOD {lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); + if (!lodNode) + return false; + + var lod = tab.Mdl.Lods[lodIndex]; + + var ret = false; + + for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) + ret |= DrawMeshDetails(tab, lod.MeshIndex + meshOffset, disabled); + + return ret; + } + private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) { using var meshNode = ImRaii.TreeNode($"Mesh {meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); From 829016a1c4b236b5516e92f84101a6ea3ade3a5a Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 22 Dec 2023 23:16:23 +1100 Subject: [PATCH 10/13] Spike improved UI --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 124 +++++++++++++++--- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index c4c86e24..0095ece8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -9,6 +10,8 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private const int MdlMaterialMaximum = 4; + private readonly FileEditor _modelTab; private static List _submeshAttributeTagWidgets = new(); @@ -28,18 +31,88 @@ public partial class ModEditWindow var ret = false; + ret |= DrawModelMaterialDetails(tab, disabled); + if (ImGui.CollapsingHeader($"Meshes ({file.Meshes.Length})###meshes")) for (var i = 0; i < file.LodCount; ++i) - ret |= DrawLodDetails(tab, i, disabled); + ret |= DrawModelLodDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } - private static bool DrawLodDetails(MdlTab tab, int lodIndex, bool disabled) + private static bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { - using var lodNode = ImRaii.TreeNode($"LOD {lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); + if (!ImGui.CollapsingHeader("Materials")) + return false; + + var materials = tab.Mdl.Materials; + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return false; + + ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); + ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + + var inputFlags = ImGuiInputTextFlags.None; + if (disabled) + inputFlags |= ImGuiInputTextFlags.ReadOnly; + + for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text($"Material #{materialIndex + 1}"); + + var temp = materials[materialIndex]; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + + ImGui.TableNextColumn(); + var todoDelete = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !ImGui.GetIO().KeyCtrl, true); + } + + if (materials.Length < MdlMaterialMaximum) + { + ImGui.TableNextColumn(); + + // todo: persist + var temp = ""; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + + // todo: flesh out this validation + var validName = temp != ""; + ImGui.TableNextColumn(); + var todoAdd = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true); + } + + // for (var index = 0; index < MdlMaterialMaximum; index++) + // { + // var temp = ""; + // ImGui.InputText($"Material {index}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + // } + + // var temp = tab.GetMeshMaterial(meshIndex); + // if ( + // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + // && temp.Length > 0 + // && temp != tab.GetMeshMaterial(meshIndex) + // ) { + // tab.SetMeshMaterial(meshIndex, temp); + // ret = true; + // } + return false; + } + + private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) + { + using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); if (!lodNode) return false; @@ -48,18 +121,24 @@ public partial class ModEditWindow var ret = false; for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) - ret |= DrawMeshDetails(tab, lod.MeshIndex + meshOffset, disabled); + ret |= DrawModelMeshDetails(tab, lod.MeshIndex + meshOffset, disabled); return ret; } - private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) + private static bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled) { - using var meshNode = ImRaii.TreeNode($"Mesh {meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); + using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); if (!meshNode) return false; using var id = ImRaii.PushId(meshIndex); + using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return false; + + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("field", ImGuiTableColumnFlags.WidthStretch, 1); var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; @@ -67,14 +146,23 @@ public partial class ModEditWindow var ret = false; // Mesh material. - var temp = tab.GetMeshMaterial(meshIndex); - if ( - ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - && temp.Length > 0 - && temp != tab.GetMeshMaterial(meshIndex) - ) { - tab.SetMeshMaterial(meshIndex, temp); - ret = true; + // var temp = tab.GetMeshMaterial(meshIndex); + // if ( + // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + // && temp.Length > 0 + // && temp != tab.GetMeshMaterial(meshIndex) + // ) { + // tab.SetMeshMaterial(meshIndex, temp); + // ret = true; + // } + ImGui.TableNextColumn(); + ImGui.Text("Material"); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + using (var materialCombo = ImRaii.Combo("##material", tab.GetMeshMaterial(meshIndex))) + { + // todo } // Submeshes. @@ -83,11 +171,15 @@ public partial class ModEditWindow using var submeshId = ImRaii.PushId(submeshOffset); var submeshIndex = mesh.SubMeshIndex + submeshOffset; + + ImGui.TableNextColumn(); + ImGui.Text($"Attributes #{submeshOffset}"); + + ImGui.TableNextColumn(); var widget = _submeshAttributeTagWidgets[submeshIndex]; var attributes = tab.GetSubmeshAttributes(submeshIndex); - UiHelpers.DefaultLineSpace(); - var tagIndex = widget.Draw($"Submesh {submeshOffset} Attributes", "", attributes, out var editedAttribute, !disabled); + var tagIndex = widget.Draw("", "", attributes, out var editedAttribute, !disabled); if (tagIndex >= 0) { tab.UpdateSubmeshAttribute( From a581495c7ea15058779c578acc51e12f247d1515 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 22 Dec 2023 23:49:50 +1100 Subject: [PATCH 11/13] Flesh out material wiring --- .../ModEditWindow.Models.MdlTab.cs | 57 ++++++--------- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 69 +++++++++++-------- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 3ba39543..5fac4f5e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using OtterGui; +using Penumbra.GameData; using Penumbra.GameData.Files; namespace Penumbra.UI.AdvancedWindow; @@ -10,53 +11,37 @@ public partial class ModEditWindow { public readonly MdlFile Mdl; - private List _materials; private List[] _attributes; public MdlTab(byte[] bytes) { Mdl = new MdlFile(bytes); - - _materials = Mdl.Meshes.Select(mesh => Mdl.Materials[mesh.MaterialIndex]).ToList(); - _attributes = HydrateAttributes(Mdl); + _attributes = PopulateAttributes(); } - private List[] HydrateAttributes(MdlFile mdl) + public void RemoveMaterial(int materialIndex) { - return mdl.SubMeshes.Select(submesh => - Enumerable.Range(0,32) - .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) - .Select(index => mdl.Attributes[index]) - .ToList() - ).ToArray(); - } - - public string GetMeshMaterial(int meshIndex) => _materials[meshIndex]; - - public void SetMeshMaterial(int meshIndex, string materialPath) - { - _materials[meshIndex] = materialPath; - - PersistMaterials(); - } - - private void PersistMaterials() - { - var allMaterials = new List(); - - foreach (var (material, meshIndex) in _materials.WithIndex()) + // Meshes using the removed material are redirected to material 0, and those after the index are corrected. + for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++) { - var materialIndex = allMaterials.IndexOf(material); - if (materialIndex == -1) - { - allMaterials.Add(material); - materialIndex = allMaterials.Count() - 1; - } - - Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + var mesh = Mdl.Meshes[meshIndex]; + if (mesh.MaterialIndex == materialIndex) + mesh.MaterialIndex = 0; + else if (mesh.MaterialIndex > materialIndex) + mesh.MaterialIndex -= 1; } - Mdl.Materials = allMaterials.ToArray(); + Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex); + } + + private List[] PopulateAttributes() + { + return Mdl.SubMeshes.Select(submesh => + Enumerable.Range(0,32) + .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) + .Select(index => Mdl.Attributes[index]) + .ToList() + ).ToArray(); } public IReadOnlyCollection GetSubmeshAttributes(int submeshIndex) => _attributes[submeshIndex]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0095ece8..fe3ca644 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -14,6 +15,7 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; + private static string _modelNewMaterial = string.Empty; private static List _submeshAttributeTagWidgets = new(); private static bool DrawModelPanel(MdlTab tab, bool disabled) @@ -47,12 +49,13 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Materials")) return false; - var materials = tab.Mdl.Materials; - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return false; + var ret = false; + var materials = tab.Mdl.Materials; + ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); @@ -63,6 +66,8 @@ public partial class ModEditWindow for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) { + using var id = ImRaii.PushId(materialIndex); + ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Text($"Material #{materialIndex + 1}"); @@ -70,44 +75,52 @@ public partial class ModEditWindow var temp = materials[materialIndex]; ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); - + if ( + ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags) + && temp.Length > 0 + && temp != materials[materialIndex] + ) { + materials[materialIndex] = temp; + ret = true; + } + ImGui.TableNextColumn(); - var todoDelete = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !ImGui.GetIO().KeyCtrl, true); + + // Need to have at least one material. + if (materials.Length <= 1) + continue; + + if (ImGuiUtil.DrawDisabledButton( + FontAwesomeIcon.Trash.ToIconString(), + UiHelpers.IconButtonSize, + "Delete this material.\nAny meshes targeting this material will be updated to use material #1.\nHold Control while clicking to delete.", + disabled || !ImGui.GetIO().KeyCtrl, + true + )) { + tab.RemoveMaterial(materialIndex); + ret = true; + } } if (materials.Length < MdlMaterialMaximum) { ImGui.TableNextColumn(); - // todo: persist - var temp = ""; ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); - // todo: flesh out this validation - var validName = temp != ""; + var validName = _modelNewMaterial != ""; ImGui.TableNextColumn(); - var todoAdd = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true)) + { + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; + ret = true; + } } - // for (var index = 0; index < MdlMaterialMaximum; index++) - // { - // var temp = ""; - // ImGui.InputText($"Material {index}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); - // } - - // var temp = tab.GetMeshMaterial(meshIndex); - // if ( - // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - // && temp.Length > 0 - // && temp != tab.GetMeshMaterial(meshIndex) - // ) { - // tab.SetMeshMaterial(meshIndex, temp); - // ret = true; - // } - return false; + return ret; } private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) @@ -160,7 +173,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - using (var materialCombo = ImRaii.Combo("##material", tab.GetMeshMaterial(meshIndex))) + using (var materialCombo = ImRaii.Combo("##material", "TODO material")) { // todo } From b22470ac79bf3f89bf3a4d671427d3aada448588 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 23 Dec 2023 00:12:59 +1100 Subject: [PATCH 12/13] Finish up mesh material combos --- .../ModEditWindow.Models.MdlTab.cs | 13 ++++----- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 5fac4f5e..f488c987 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; @@ -24,11 +23,13 @@ public partial class ModEditWindow // Meshes using the removed material are redirected to material 0, and those after the index are corrected. for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++) { - var mesh = Mdl.Meshes[meshIndex]; - if (mesh.MaterialIndex == materialIndex) - mesh.MaterialIndex = 0; - else if (mesh.MaterialIndex > materialIndex) - mesh.MaterialIndex -= 1; + var newIndex = Mdl.Meshes[meshIndex].MaterialIndex; + if (newIndex == materialIndex) + newIndex = 0; + else if (newIndex > materialIndex) + newIndex -= 1; + + Mdl.Meshes[meshIndex].MaterialIndex = newIndex; } Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index fe3ca644..92953ae4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -158,27 +158,28 @@ public partial class ModEditWindow var ret = false; - // Mesh material. - // var temp = tab.GetMeshMaterial(meshIndex); - // if ( - // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - // && temp.Length > 0 - // && temp != tab.GetMeshMaterial(meshIndex) - // ) { - // tab.SetMeshMaterial(meshIndex, temp); - // ret = true; - // } + // Mesh material ImGui.TableNextColumn(); ImGui.Text("Material"); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - using (var materialCombo = ImRaii.Combo("##material", "TODO material")) + using (var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex])) { - // todo + if (materialCombo) + { + foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex()) + { + if (ImGui.Selectable(material, mesh.MaterialIndex == materialIndex)) + { + file.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + ret = true; + } + } + } } - // Submeshes. + // Submeshes for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) { using var submeshId = ImRaii.PushId(submeshOffset); From a001fcf24ff4333dbdd1bb496caa8f4b4d28ec7a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Dec 2023 15:18:43 +0100 Subject: [PATCH 13/13] Some cleanup. --- .../ModEditWindow.Models.MdlTab.cs | 64 +++-- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 246 +++++++++--------- 2 files changed, 166 insertions(+), 144 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f488c987..4986963f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -10,24 +10,33 @@ public partial class ModEditWindow { public readonly MdlFile Mdl; - private List[] _attributes; + private readonly List[] _attributes; public MdlTab(byte[] bytes) { - Mdl = new MdlFile(bytes); - _attributes = PopulateAttributes(); + Mdl = new MdlFile(bytes); + _attributes = CreateAttributes(Mdl); } + /// + public bool Valid + => Mdl.Valid; + + /// + public byte[] Write() + => Mdl.Write(); + + /// Remove the material given by the index. + /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) { - // Meshes using the removed material are redirected to material 0, and those after the index are corrected. for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++) { var newIndex = Mdl.Meshes[meshIndex].MaterialIndex; if (newIndex == materialIndex) newIndex = 0; else if (newIndex > materialIndex) - newIndex -= 1; + --newIndex; Mdl.Meshes[meshIndex].MaterialIndex = newIndex; } @@ -35,36 +44,41 @@ public partial class ModEditWindow Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex); } - private List[] PopulateAttributes() - { - return Mdl.SubMeshes.Select(submesh => - Enumerable.Range(0,32) - .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) - .Select(index => Mdl.Attributes[index]) - .ToList() + /// Create a list of attributes per sub mesh. + private static List[] CreateAttributes(MdlFile mdl) + => mdl.SubMeshes.Select(s => Enumerable.Range(0, 32) + .Where(idx => ((s.AttributeIndexMask >> idx) & 1) == 1) + .Select(idx => mdl.Attributes[idx]) + .ToList() ).ToArray(); - } - public IReadOnlyCollection GetSubmeshAttributes(int submeshIndex) => _attributes[submeshIndex]; + /// Obtain the attributes associated with a sub mesh by its index. + public IReadOnlyList GetSubMeshAttributes(int subMeshIndex) + => _attributes[subMeshIndex]; - public void UpdateSubmeshAttribute(int submeshIndex, string? old, string? new_) + /// Remove or add attributes from a sub mesh by its index. + /// The index of the sub mesh to update. + /// If non-null, remove this attribute. + /// If non-null, add this attribute. + public void UpdateSubMeshAttribute(int subMeshIndex, string? old, string? @new) { - var attributes = _attributes[submeshIndex]; + var attributes = _attributes[subMeshIndex]; if (old != null) attributes.Remove(old); - if (new_ != null) - attributes.Add(new_); + if (@new != null) + attributes.Add(@new); PersistAttributes(); } + /// Apply changes to attributes to the file in memory. private void PersistAttributes() { var allAttributes = new List(); - foreach (var (attributes, submeshIndex) in _attributes.WithIndex()) + foreach (var (attributes, subMeshIndex) in _attributes.WithIndex()) { var mask = 0u; @@ -74,20 +88,16 @@ public partial class ModEditWindow if (attributeIndex == -1) { allAttributes.Add(attribute); - attributeIndex = allAttributes.Count() - 1; + attributeIndex = allAttributes.Count - 1; } mask |= 1u << attributeIndex; } - Mdl.SubMeshes[submeshIndex].AttributeIndexMask = mask; + Mdl.SubMeshes[subMeshIndex].AttributeIndexMask = mask; } - Mdl.Attributes = allAttributes.ToArray(); + Mdl.Attributes = [.. allAttributes]; } - - public bool Valid => Mdl.Valid; - - public byte[] Write() => Mdl.Write(); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 92953ae4..25bb012a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -15,19 +15,19 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; - private static string _modelNewMaterial = string.Empty; - private static List _submeshAttributeTagWidgets = new(); + private string _modelNewMaterial = string.Empty; + private readonly List _subMeshAttributeTagWidgets = []; - private static bool DrawModelPanel(MdlTab tab, bool disabled) + private bool DrawModelPanel(MdlTab tab, bool disabled) { var file = tab.Mdl; - var submeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_submeshAttributeTagWidgets.Count != submeshTotal) + var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_subMeshAttributeTagWidgets.Count != subMeshTotal) { - _submeshAttributeTagWidgets.Clear(); - _submeshAttributeTagWidgets.AddRange( - Enumerable.Range(0, submeshTotal).Select(_ => new TagButtons()) + _subMeshAttributeTagWidgets.Clear(); + _subMeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) ); } @@ -44,93 +44,92 @@ public partial class ModEditWindow return !disabled && ret; } - private static bool DrawModelMaterialDetails(MdlTab tab, bool disabled) + private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { if (!ImGui.CollapsingHeader("Materials")) return false; - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + using var table = ImRaii.Table(string.Empty, disabled ? 2 : 3, ImGuiTableFlags.SizingFixedFit); if (!table) return false; - var ret = false; + var ret = false; var materials = tab.Mdl.Materials; - ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); - ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - - var inputFlags = ImGuiInputTextFlags.None; - if (disabled) - inputFlags |= ImGuiInputTextFlags.ReadOnly; + ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); + ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); + if (!disabled) + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) - { - using var id = ImRaii.PushId(materialIndex); + ret |= DrawMaterialRow(tab, disabled, materials, materialIndex, inputFlags); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text($"Material #{materialIndex + 1}"); + if (materials.Length >= MdlMaterialMaximum || disabled) + return ret; - var temp = materials[materialIndex]; - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - if ( - ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags) - && temp.Length > 0 - && temp != materials[materialIndex] - ) { - materials[materialIndex] = temp; - ret = true; - } + ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); + var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/'; + ImGui.TableNextColumn(); + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) + return ret; - // Need to have at least one material. - if (materials.Length <= 1) - continue; - - if (ImGuiUtil.DrawDisabledButton( - FontAwesomeIcon.Trash.ToIconString(), - UiHelpers.IconButtonSize, - "Delete this material.\nAny meshes targeting this material will be updated to use material #1.\nHold Control while clicking to delete.", - disabled || !ImGui.GetIO().KeyCtrl, - true - )) { - tab.RemoveMaterial(materialIndex); - ret = true; - } - } - - if (materials.Length < MdlMaterialMaximum) - { - ImGui.TableNextColumn(); - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); - - var validName = _modelNewMaterial != ""; - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true)) - { - tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); - _modelNewMaterial = string.Empty; - ret = true; - } - } - - return ret; + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; + return true; } - private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) + private bool DrawMaterialRow(MdlTab tab, bool disabled, string[] materials, int materialIndex, ImGuiInputTextFlags inputFlags) { - using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); + using var id = ImRaii.PushId(materialIndex); + var ret = false; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Material #{materialIndex + 1}"); + + var temp = materials[materialIndex]; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags) + && temp.Length > 0 + && temp != materials[materialIndex] + ) + { + materials[materialIndex] = temp; + ret = true; + } + + if (disabled) + return ret; + + ImGui.TableNextColumn(); + + // Need to have at least one material. + if (materials.Length <= 1) + return ret; + + var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1."; + var modifierActive = _config.DeleteModModifier.IsActive(); + if (!modifierActive) + tt += $"\nHold {_config.DeleteModModifier} to delete."; + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true)) + return ret; + + tab.RemoveMaterial(materialIndex); + return true; + } + + private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) + { + using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen); if (!lodNode) return false; var lod = tab.Mdl.Lods[lodIndex]; - var ret = false; for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) @@ -139,76 +138,89 @@ public partial class ModEditWindow return ret; } - private static bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled) + private bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled) { - using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); + using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen); if (!meshNode) return false; - using var id = ImRaii.PushId(meshIndex); + using var id = ImRaii.PushId(meshIndex); using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); if (!table) return false; - ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); ImGui.TableSetupColumn("field", ImGuiTableColumnFlags.WidthStretch, 1); var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; - var ret = false; // Mesh material ImGui.TableNextColumn(); - ImGui.Text("Material"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Material"); ImGui.TableNextColumn(); + var ret = DrawMaterialCombo(tab, meshIndex, disabled); + + // Sub meshes + for (var subMeshOffset = 0; subMeshOffset < mesh.SubMeshCount; subMeshOffset++) + ret |= DrawSubMeshAttributes(tab, meshIndex, disabled, subMeshOffset); + + return ret; + } + + private bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled) + { + var mesh = tab.Mdl.Meshes[meshIndex]; + using var _ = ImRaii.Disabled(disabled); ImGui.SetNextItemWidth(-1); - using (var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex])) + using var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex]); + + if (!materialCombo) + return false; + + var ret = false; + foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex()) { - if (materialCombo) - { - foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex()) - { - if (ImGui.Selectable(material, mesh.MaterialIndex == materialIndex)) - { - file.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; - ret = true; - } - } - } - } + if (!ImGui.Selectable(material, mesh.MaterialIndex == materialIndex)) + continue; - // Submeshes - for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) - { - using var submeshId = ImRaii.PushId(submeshOffset); - - var submeshIndex = mesh.SubMeshIndex + submeshOffset; - - ImGui.TableNextColumn(); - ImGui.Text($"Attributes #{submeshOffset}"); - - ImGui.TableNextColumn(); - var widget = _submeshAttributeTagWidgets[submeshIndex]; - var attributes = tab.GetSubmeshAttributes(submeshIndex); - - var tagIndex = widget.Draw("", "", attributes, out var editedAttribute, !disabled); - if (tagIndex >= 0) - { - tab.UpdateSubmeshAttribute( - submeshIndex, - tagIndex < attributes.Count() ? attributes.ElementAt(tagIndex) : null, - editedAttribute != "" ? editedAttribute : null - ); - - ret = true; - } + tab.Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + ret = true; } return ret; } + private bool DrawSubMeshAttributes(MdlTab tab, int meshIndex, bool disabled, int subMeshOffset) + { + using var _ = ImRaii.PushId(subMeshOffset); + + var mesh = tab.Mdl.Meshes[meshIndex]; + var subMeshIndex = mesh.SubMeshIndex + subMeshOffset; + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Attributes #{subMeshOffset + 1}"); + + ImGui.TableNextColumn(); + var widget = _subMeshAttributeTagWidgets[subMeshIndex]; + var attributes = tab.GetSubMeshAttributes(subMeshIndex); + + var tagIndex = widget.Draw(string.Empty, string.Empty, attributes, + out var editedAttribute, !disabled); + if (tagIndex < 0) + return false; + + var oldName = tagIndex < attributes.Count ? attributes[tagIndex] : null; + var newName = editedAttribute.Length > 0 ? editedAttribute : null; + tab.UpdateSubMeshAttribute(subMeshIndex, oldName, newName); + + return true; + } + private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content"))