From ef1bbb6d9d8443791a0c4d534bf373f0e490e966 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:40:12 +0200 Subject: [PATCH 01/12] I don't know what I'm doing --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 3 +- Penumbra/Import/Models/Export/MeshExporter.cs | 19 +++++----- Penumbra/Import/Models/Import/MeshImporter.cs | 13 ++++--- .../Import/Models/Import/ModelImporter.cs | 34 +++++++++--------- .../LiveColorTablePreviewer.cs | 7 ++-- .../ModEditWindow.Materials.ColorTable.cs | 35 ++++++++++--------- .../ModEditWindow.Materials.MtrlTab.cs | 9 ++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 9 files changed, 65 insertions(+), 59 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fe9d563d..845d1f99 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fe9d563d9845630673cf098f7a6bfbd26e600fb4 +Subproject commit 845d1f99a752f4d23288a316e42d4bfa32fa987f diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 2fa4e1b2..73a5e725 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -1,5 +1,6 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using SharpGLTF.Materials; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; @@ -102,7 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation + private readonly struct ProcessCharacterNormalOperation(Image normal, ColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); public Image BaseColor { get; } = new(normal.Width, normal.Height); diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index d3ca87dc..f372f665 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -3,6 +3,7 @@ using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.IO; @@ -55,7 +56,7 @@ public class MeshExporter private readonly byte _lod; private readonly ushort _meshIndex; - private MdlStructs.MeshStruct XivMesh + private MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; private readonly MaterialBuilder _material; @@ -109,8 +110,8 @@ public class MeshExporter var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); - - foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) + // #TODO @ackwell maybe fix for V6 Models, I think this works fine. + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex)) @@ -238,19 +239,15 @@ public class MeshExporter { "targetNames", shapeNames }, }); - string[] attributes = []; - var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); + string[] attributes = []; + var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); if (maxAttribute < _mdl.Attributes.Length) - { attributes = Enumerable.Range(0, 32) .Where(index => ((attributeMask >> index) & 1) == 1) .Select(index => _mdl.Attributes[index]) .ToArray(); - } else - { _notifier.Warning("Invalid attribute data, ignoring."); - } return new MeshData { @@ -278,7 +275,7 @@ public class MeshExporter for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) { streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset[streamIndex]); + streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset(streamIndex)); } var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements @@ -315,7 +312,7 @@ public class MeshExporter MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1d4b223d..3a11cb04 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -1,5 +1,6 @@ using Lumina.Data.Parsing; using OtterGui; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -8,7 +9,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { public struct Mesh { - public MdlStructs.MeshStruct MeshStruct; + public MeshStruct MeshStruct; public List SubMeshStructs; public string? Material; @@ -69,10 +70,14 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) return new Mesh { - MeshStruct = new MdlStructs.MeshStruct + MeshStruct = new MeshStruct { - VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], - VertexBufferStride = _strides, + VertexBufferOffset1 = 0, + VertexBufferOffset2 = (uint)_streams[0].Count, + VertexBufferOffset3 = (uint)(_streams[0].Count + _streams[1].Count), + VertexBufferStride1 = _strides[0], + VertexBufferStride2 = _strides[1], + VertexBufferStride3 = _strides[2], VertexCount = _vertexCount, VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements .Select(element => element.Stream + 1) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 8f917b0e..eedd12ab 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,6 +1,7 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -14,10 +15,11 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) } // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", + RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] private static partial Regex MeshNameGroupingRegex(); - private readonly List _meshes = []; + private readonly List _meshes = []; private readonly List _subMeshes = []; private readonly List _materials = []; @@ -27,10 +29,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) private readonly List _indices = []; - private readonly List _bones = []; - private readonly List _boneTables = []; + private readonly List _bones = []; + private readonly List _boneTables = []; - private readonly BoundingBox _boundingBox = new BoundingBox(); + private readonly BoundingBox _boundingBox = new(); private readonly List _metaAttributes = []; @@ -95,9 +97,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) IndexBufferSize = (uint)indexBuffer.Length, }, ], - - Materials = [.. materials], - + Materials = [.. materials], BoundingBoxes = _boundingBox.ToStruct(), // TODO: Would be good to calculate all of this up the tree. @@ -132,9 +132,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) private void BuildMeshForGroup(IEnumerable subMeshNodes, int index) { // Record some offsets we'll be using later, before they get mutated with mesh values. - var subMeshOffset = _subMeshes.Count; - var vertexOffset = _vertexBuffer.Count; - var indexOffset = _indices.Count; + var subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}")); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); @@ -154,9 +154,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), BoneTableIndex = boneTableIndex, StartIndex = meshStartIndex, - VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset - .Select(offset => (uint)(offset + vertexOffset)) - .ToArray(), + VertexBufferOffset1 = (uint)(mesh.MeshStruct.VertexBufferOffset1 + vertexOffset), + VertexBufferOffset2 = (uint)(mesh.MeshStruct.VertexBufferOffset2 + vertexOffset), + VertexBufferOffset3 = (uint)(mesh.MeshStruct.VertexBufferOffset3 + vertexOffset), }); _boundingBox.Merge(mesh.BoundingBox); @@ -196,7 +196,8 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) // arrays, values is practically guaranteed to be the highest of the // group, so a failure on any of them will be a failure on it. if (_shapeValues.Count > ushort.MaxValue) - throw notifier.Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); + throw notifier.Exception( + $"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); } private ushort GetMaterialIndex(string materialName) @@ -216,6 +217,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) return (ushort)count; } + // #TODO @ackwell fix for V6 Models private ushort BuildBoneTable(List boneNames) { var boneIndices = new List(); @@ -238,7 +240,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); var boneTableIndex = _boneTables.Count; - _boneTables.Add(new MdlStructs.BoneTableStruct() + _boneTables.Add(new BoneTableStruct() { BoneIndex = boneIndicesArray, BoneCount = (byte)boneIndices.Count, diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 4d35e68a..f211e0bc 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; @@ -9,7 +8,7 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { public const int TextureWidth = 4; - public const int TextureHeight = MtrlFile.ColorTable.NumRows; + public const int TextureHeight = GameData.Files.MaterialStructs.ColorTable.NumUsedRows; public const int TextureLength = TextureWidth * TextureHeight * 4; private readonly IFramework _framework; @@ -17,7 +16,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private readonly Texture** _colorTableTexture; private readonly SafeTextureHandle _originalColorTableTexture; - private bool _updatePending; + private bool _updatePending; public Half[] ColorTable { get; } @@ -40,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - ColorTable = new Half[TextureLength]; + ColorTable = new Half[TextureLength]; _updatePending = true; framework.Update += OnFrameworkUpdate; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index a4e25f77..54c0eff6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.String.Functions; namespace Penumbra.UI.AdvancedWindow; @@ -74,7 +75,7 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + for (var i = 0; i < ColorTable.NumUsedRows; ++i) { ret |= DrawColorTableRow(tab, i, disabled); ImGui.TableNextRow(); @@ -115,8 +116,8 @@ public partial class ModEditWindow { var ret = false; if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId); + for (var i = 0; i < ColorTable.NumUsedRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); tab.UpdateColorTablePreview(); @@ -140,21 +141,21 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) + 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() + 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()); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } @@ -169,7 +170,7 @@ public partial class ModEditWindow } } - private static unsafe void ColorTableCopyClipboardButton(MtrlFile.ColorTable.Row row, MtrlFile.ColorDyeTable.Row dye) + private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Export this row to your clipboard.", false, true)) @@ -177,11 +178,11 @@ public partial class ModEditWindow try { - var data = new byte[MtrlFile.ColorTable.Row.Size + 2]; + var data = new byte[ColorTable.Row.Size + 2]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorTable.Row.Size, &dye, 2); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, 2); } var text = Convert.ToBase64String(data); @@ -217,15 +218,15 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != MtrlFile.ColorTable.Row.Size + 2 + if (data.Length != ColorTable.Row.Size + 2 || !tab.Mtrl.HasTable) return false; fixed (byte* ptr = data) { - tab.Mtrl.Table[rowIdx] = *(MtrlFile.ColorTable.Row*)ptr; + tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(MtrlFile.ColorDyeTable.Row*)(ptr + MtrlFile.ColorTable.Row.Size); + tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); } tab.UpdateColorTableRowPreview(rowIdx); @@ -451,7 +452,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, MtrlFile.ColorDyeTable.Row dye, float floatSize) + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) @@ -463,7 +464,7 @@ public partial class ModEditWindow 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); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); if (ret) tab.UpdateColorTableRowPreview(rowIdx); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index b4801f5f..9421493e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -8,6 +8,7 @@ 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; @@ -601,7 +602,7 @@ public partial class ModEditWindow var stm = _edit._stainService.StmFile; var dye = Mtrl.DyeTable[rowIdx]; if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); + row.ApplyDyeTemplate(dye, dyes, default); } if (HighlightedColorTableRow == rowIdx) @@ -628,12 +629,12 @@ public partial class ModEditWindow { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + for (var i = 0; i < ColorTable.NumUsedRows; ++i) { ref var row = ref rows[i]; var dye = Mtrl.DyeTable[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); + row.ApplyDyeTemplate(dye, dyes, default); } } @@ -647,7 +648,7 @@ public partial class ModEditWindow } } - private static void ApplyHighlight(ref MtrlFile.ColorTable.Row row, float time) + private static void ApplyHighlight(ref ColorTable.Row row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03f276ea..80b1a5d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -483,7 +483,7 @@ public partial class ModEditWindow if (table) { ImGuiUtil.DrawTableColumn("Version"); - ImGuiUtil.DrawTableColumn(_lastFile.Version.ToString()); + ImGuiUtil.DrawTableColumn($"0x{_lastFile.Version:X}"); ImGuiUtil.DrawTableColumn("Radius"); ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); From 624dd40d58bc3817c3fbd271564042c58ad72439 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:46:52 +0200 Subject: [PATCH 02/12] Handle writing. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 845d1f99..aff136e2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 845d1f99a752f4d23288a316e42d4bfa32fa987f +Subproject commit aff136e2ff79990989cbe1c518a79b7b83e294a5 From 75cfffeba73e80f970d617e7d36622e709e444c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:51:23 +0200 Subject: [PATCH 03/12] Oops. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index aff136e2..9208c9c2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit aff136e2ff79990989cbe1c518a79b7b83e294a5 +Subproject commit 9208c9c242244beeb3c1fb826582d72da09831af From 8fc7de64d9d23c9874861567dd6ada6a0f246d57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 17:55:28 +0200 Subject: [PATCH 04/12] Start group rework. --- Penumbra/Collections/Cache/CollectionCache.cs | 50 +++++-------------- .../Collections/Cache/CollectionModData.cs | 10 ++-- Penumbra/Mods/Editor/IMod.cs | 14 +++++- Penumbra/Mods/Mod.cs | 19 +++++++ Penumbra/Mods/Subclasses/IModGroup.cs | 4 ++ Penumbra/Mods/Subclasses/ISubMod.cs | 10 ++++ Penumbra/Mods/Subclasses/MultiModGroup.cs | 11 ++++ Penumbra/Mods/Subclasses/SingleModGroup.cs | 5 ++ Penumbra/Mods/TemporaryMod.cs | 28 ++++++++++- 9 files changed, 106 insertions(+), 45 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e1b32204..ded1dc73 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -2,12 +2,10 @@ using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; namespace Penumbra.Collections.Cache; @@ -231,37 +229,12 @@ public sealed class CollectionCache : IDisposable /// Add all files and possibly manipulations of a given mod according to its settings in this collection. internal void AddModSync(IMod mod, bool addMetaChanges) { - if (mod.Index >= 0) - { - var settings = _collection[mod.Index].Settings; - if (settings is not { Enabled: true }) - return; + var files = GetFiles(mod); + foreach (var (path, file) in files.FileRedirections) + AddFile(path, file, mod); - foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) - { - if (group.Count == 0) - continue; - - var config = settings.Settings[groupIndex]; - switch (group) - { - case SingleModGroup single: - AddSubMod(single[config.AsIndex], mod); - break; - case MultiModGroup multi: - { - foreach (var (option, _) in multi.WithIndex() - .Where(p => config.HasFlag(p.Index)) - .OrderByDescending(p => group.OptionPriority(p.Index))) - AddSubMod(option, mod); - - break; - } - } - } - } - - AddSubMod(mod.Default, mod); + foreach (var manip in files.Manipulations) + AddManipulation(manip, mod); if (addMetaChanges) { @@ -273,14 +246,15 @@ public sealed class CollectionCache : IDisposable } } - // Add all files and possibly manipulations of a specific submod - private void AddSubMod(ISubMod subMod, IMod parentMod) + private AppliedModData GetFiles(IMod mod) { - foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps)) - AddFile(path, file, parentMod); + if (mod.Index < 0) + return mod.GetData(); - foreach (var manip in subMod.Manipulations) - AddManipulation(manip, parentMod); + var settings = _collection[mod.Index].Settings; + return settings is not { Enabled: true } + ? AppliedModData.Empty + : mod.GetData(settings); } /// Invoke only if not in a full recalculation. diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index 3a3afad2..d0a3bc76 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -1,10 +1,12 @@ using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; +/// +/// Contains associations between a mod and the paths and meta manipulations affected by that mod. +/// public class CollectionModData { private readonly Dictionary, HashSet)> _data = new(); @@ -17,7 +19,7 @@ public class CollectionModData if (_data.Remove(mod, out var data)) return data; - return (Array.Empty(), Array.Empty()); + return ([], []); } public void AddPath(IMod mod, Utf8GamePath path) @@ -28,7 +30,7 @@ public class CollectionModData } else { - data = (new HashSet { path }, new HashSet()); + data = ([path], []); _data.Add(mod, data); } } @@ -41,7 +43,7 @@ public class CollectionModData } else { - data = (new HashSet(), new HashSet { manipulation }); + data = ([], [manipulation]); _data.Add(mod, data); } } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index d3bc19b0..8b5b65e1 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,15 +1,27 @@ using OtterGui.Classes; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Subclasses; +using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; +public record struct AppliedModData( + IReadOnlyCollection> FileRedirections, + IReadOnlyCollection Manipulations) +{ + public static readonly AppliedModData Empty = new([], []); +} + public interface IMod { LowerString Name { get; } - public int Index { get; } + public int Index { get; } public ModPriority Priority { get; } + public AppliedModData GetData(ModSettings? settings = null); + + public ISubMod Default { get; } public IReadOnlyList Groups { get; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index b7d1186d..3c996c8f 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,7 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.Collections.Cache; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; @@ -59,6 +61,23 @@ public sealed class Mod : IMod public readonly SubMod Default; public readonly List Groups = []; + public AppliedModData GetData(ModSettings? settings = null) + { + if (settings is not { Enabled: true }) + return AppliedModData.Empty; + + var dictRedirections = new Dictionary(TotalFileCount); + var setManips = new HashSet(TotalManipulations); + foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) + { + var config = settings.Settings[groupIndex]; + group.AddData(config, dictRedirections, setManips); + } + + ((ISubMod)Default).AddData(dictRedirections, setManips); + return new AppliedModData(dictRedirections, setManips); + } + ISubMod IMod.Default => Default; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 2daf31e6..57ef4e98 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -24,6 +26,8 @@ public interface IModGroup : IReadOnlyCollection public bool MoveOption(int optionIdxFrom, int optionIdxTo); public void UpdatePositions(int from = 0); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 29323c1d..e997e07d 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -14,6 +14,16 @@ public interface ISubMod public IReadOnlyDictionary FileSwaps { get; } public IReadOnlySet Manipulations { get; } + public void AddData(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } + public bool IsDefault { get; } public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 7479cd54..266d3037 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -5,6 +5,8 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -110,6 +112,15 @@ public sealed class MultiModGroup : IModGroup o.SetPosition(o.GroupIdx, i); } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) + { + if (setting.HasFlag(index)) + ((ISubMod)option.Mod).AddData(redirections, manipulations); + } + } + public Setting FixSetting(Setting setting) => new(setting.Value & ((1ul << Count) - 1)); } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 74769c7e..f797a709 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -3,6 +3,8 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -114,6 +116,9 @@ public sealed class SingleModGroup : IModGroup o.SetPosition(o.GroupIdx, i); } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + => this[setting.AsIndex].AddData(redirections, manipulations); + public Setting FixSetting(Setting setting) => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6be07881..8f27e201 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -20,6 +20,28 @@ public class TemporaryMod : IMod public readonly SubMod Default; + public AppliedModData GetData(ModSettings? settings = null) + { + Dictionary dict; + if (Default.FileSwapData.Count == 0) + { + dict = Default.FileData; + } + else if (Default.FileData.Count == 0) + { + dict = Default.FileSwapData; + } + else + { + // Need to ensure uniqueness. + dict = new Dictionary(Default.FileData.Count + Default.FileSwaps.Count); + foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) + dict.TryAdd(gamePath, file); + } + + return new AppliedModData(dict, Default.Manipulations); + } + ISubMod IMod.Default => Default; @@ -53,7 +75,8 @@ public class TemporaryMod : IMod dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", null, null); + $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", + null, null); var mod = new Mod(dir); var defaultMod = mod.Default; foreach (var (gamePath, fullPath) in collection.ResolvedFiles) @@ -86,7 +109,8 @@ public class TemporaryMod : IMod saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); - Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); + Penumbra.Log.Information( + $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } catch (Exception e) { From 9f4c6767f822be23632b39e3ab73792d19290ec3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 18:28:25 +0200 Subject: [PATCH 05/12] Remove ISubMod. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 7 +- Penumbra/Mods/Editor/FileRegistry.cs | 2 +- Penumbra/Mods/Editor/IMod.cs | 12 ++-- Penumbra/Mods/Editor/ModEditor.cs | 4 +- Penumbra/Mods/Editor/ModFileCollection.cs | 14 ++-- Penumbra/Mods/Editor/ModFileEditor.cs | 16 ++--- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 15 ++--- Penumbra/Mods/Manager/ModOptionEditor.cs | 15 ++--- Penumbra/Mods/Mod.cs | 13 ++-- Penumbra/Mods/ModCreator.cs | 8 +-- Penumbra/Mods/Subclasses/IModGroup.cs | 12 ++-- Penumbra/Mods/Subclasses/ISubMod.cs | 67 ------------------- Penumbra/Mods/Subclasses/ModSettings.cs | 35 +--------- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 +- Penumbra/Mods/Subclasses/SingleModGroup.cs | 4 +- Penumbra/Mods/Subclasses/SubMod.cs | 58 ++++++++++++++-- Penumbra/Mods/TemporaryMod.cs | 5 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 4 +- .../ModEditWindow.QuickImport.cs | 2 +- 23 files changed, 123 insertions(+), 184 deletions(-) delete mode 100644 Penumbra/Mods/Subclasses/ISubMod.cs diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 099b133c..f4b7d47e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -152,7 +152,7 @@ public partial class TexToolsImporter } // Iterate through all pages - var options = new List(); + var options = new List(); var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index c8530936..938199aa 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } - public void DeleteDuplicates(ModFileCollection files, Mod mod, ISubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) { if (!Worker.IsCompleted || _duplicates.Count == 0) return; @@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -86,8 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } else { - var sub = (SubMod)subMod; - sub.FileData = dict; + subMod.FileData = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 96d027b3..427c58ca 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -5,7 +5,7 @@ namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = []; + public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; public FullPath File { get; private init; } public Utf8RelPath RelPath { get; private init; } public long FileSize { get; private init; } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 8b5b65e1..c4c4be2f 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -6,8 +6,8 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; public record struct AppliedModData( - IReadOnlyCollection> FileRedirections, - IReadOnlyCollection Manipulations) + Dictionary FileRedirections, + HashSet Manipulations) { public static readonly AppliedModData Empty = new([], []); } @@ -19,14 +19,10 @@ public interface IMod public int Index { get; } public ModPriority Priority { get; } + public IReadOnlyList Groups { get; } + public AppliedModData GetData(ModSettings? settings = null); - - public ISubMod Default { get; } - public IReadOnlyList Groups { get; } - - public IEnumerable AllSubMods { get; } - // Cache public int TotalManipulations { get; } } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index b22aea17..d9781c06 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -29,7 +29,7 @@ public class ModEditor( public int OptionIdx { get; private set; } public IModGroup? Group { get; private set; } - public ISubMod? Option { get; private set; } + public SubMod? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); @@ -104,7 +104,7 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllOptions(Mod mod, Action action) { action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 2f8bdfb1..9dd78217 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public void UpdateAll(Mod mod, ISubMod option) + public void UpdateAll(Mod mod, SubMod option) { UpdateFiles(mod, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken()); } - public void UpdatePaths(Mod mod, ISubMod option) + public void UpdatePaths(Mod mod, SubMod option) => UpdatePaths(mod, option, true, new CancellationToken()); public void Clear() @@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable public void ClearMissingFiles() => _missing.Clear(); - public void RemoveUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Remove(gamePath); if (file != null) @@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable } } - public void RemoveUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - public void AddUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Add(gamePath); if (file == null) @@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable file.SubModUsage.Add((option, gamePath)); } - public void AddUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) @@ -154,7 +154,7 @@ public class ModFileCollection : IDisposable _usedPaths.Clear(); } - private void UpdatePaths(Mod mod, ISubMod option, bool clearRegistries, CancellationToken tok) + private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) { tok.ThrowIfCancellationRequested(); ClearPaths(clearRegistries, tok); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 30e97093..4bdf4b1b 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -30,16 +30,16 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu return num; } - public void Revert(Mod mod, ISubMod option) + public void Revert(Mod mod, SubMod option) { files.UpdateAll(mod, option); Changes = false; } /// Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths(Mod mod, ISubMod option) + public void RemoveMissingPaths(Mod mod, SubMod option) { - void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -61,7 +61,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// If path is empty, it will be deleted instead. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// - public bool SetGamePath(ISubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) { if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; @@ -84,7 +84,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. /// - public int AddPathsToSelected(ISubMod option, IEnumerable files1, int skipFolders = 0) + public int AddPathsToSelected(SubMod option, IEnumerable files1, int skipFolders = 0) { var failed = 0; foreach (var file in files1) @@ -111,7 +111,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(ISubMod option, IEnumerable files1) + public void RemovePathsFromSelected(SubMod option, IEnumerable files1) { foreach (var file in files1) { @@ -129,7 +129,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, ISubMod option, IEnumerable files1) + public void DeleteFiles(Mod mod, SubMod option, IEnumerable files1) { var deletions = 0; foreach (var file in files1) @@ -155,7 +155,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } - private bool CheckAgainstMissing(Mod mod, ISubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) { if (!files.Missing.Contains(file)) return true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 842b1bb3..25590c49 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -151,7 +151,7 @@ public class ModMerger : IDisposable MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) + private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) { var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 31aefdf5..a6218c6f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager) Changes = true; } - public void Load(Mod mod, ISubMod currentOption) + public void Load(Mod mod, SubMod currentOption) { OtherImcCount = 0; OtherEqpCount = 0; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index ada06264..0d5f05a9 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager) public IReadOnlyDictionary Swaps => _swaps; - public void Revert(ISubMod option) + public void Revert(SubMod option) { _swaps.SetTo(option.FileSwaps); Changes = false; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index e229738d..21b9ef2c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; @@ -15,14 +16,13 @@ public class ItemSwapContainer private readonly MetaFileManager _manager; private readonly ObjectIdentification _identifier; - private Dictionary _modRedirections = []; - private HashSet _modManipulations = []; + private AppliedModData _appliedModData = AppliedModData.Empty; public IReadOnlyDictionary ModRedirections - => _modRedirections; + => _appliedModData.FileRedirections; public IReadOnlySet ModManipulations - => _modManipulations; + => _appliedModData.Manipulations; public readonly List Swaps = []; @@ -97,12 +97,11 @@ public class ItemSwapContainer Clear(); if (mod == null || mod.Index < 0) { - _modRedirections = []; - _modManipulations = []; + _appliedModData = AppliedModData.Empty; } else { - (_modRedirections, _modManipulations) = ModSettings.GetResolveData(mod, settings); + _appliedModData = ModSettings.GetResolveData(mod, settings); } } @@ -120,7 +119,7 @@ public class ItemSwapContainer private Func MetaResolver(ModCollection? collection) { - var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _modManipulations; + var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _appliedModData.Manipulations; return m => set.TryGetValue(m, out var a) ? a : m; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 9efb8a3f..07c6f38e 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -262,15 +262,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add an existing option to a given group with default priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option) + public void AddOption(Mod mod, int groupIdx, SubMod option) => AddOption(mod, groupIdx, option, ModPriority.Default); /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option, ModPriority priority) + public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) { - if (option is not SubMod o) - return; - var group = mod.Groups[groupIdx]; switch (group) { @@ -280,12 +277,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: - o.SetPosition(groupIdx, s.Count); - s.OptionData.Add(o); + option.SetPosition(groupIdx, s.Count); + s.OptionData.Add(option); break; case MultiModGroup m: - o.SetPosition(groupIdx, m.Count); - m.PrioritizedOptions.Add((o, priority)); + option.SetPosition(groupIdx, m.Count); + m.PrioritizedOptions.Add((option, priority)); break; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 3c996c8f..25f3c510 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -32,6 +32,9 @@ public sealed class Mod : IMod public ModPriority Priority => ModPriority.Default; + IReadOnlyList IMod.Groups + => Groups; + internal Mod(DirectoryInfo modPath) { ModPath = modPath; @@ -74,18 +77,12 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - ((ISubMod)Default).AddData(dictRedirections, setManips); + Default.AddData(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } - ISubMod IMod.Default - => Default; - - IReadOnlyList IMod.Groups - => Groups; - public IEnumerable AllSubMods - => Groups.SelectMany(o => o).OfType().Prepend(Default); + => Groups.SelectMany(o => o).Prepend(Default); public List FindUnusedFiles() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 2bcdd3b1..661dd6fb 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, new ModPriority(idx)))); + group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -269,7 +269,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, } /// Create the data for a given sub mod from its data and the folder it is based on. - public ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) { var list = optionFolder.EnumerateNonHiddenFiles() .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) @@ -288,7 +288,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, } /// Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod(string name) + internal static SubMod CreateEmptySubMod(string name) => new SubMod(null!) // Mod is irrelevant here, only used for saving. { Name = name, diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 57ef4e98..3f363542 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -6,7 +6,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IReadOnlyCollection +public interface IModGroup : IReadOnlyCollection { public const int MaxMultiOptions = 63; @@ -18,7 +18,7 @@ public interface IModGroup : IReadOnlyCollection public ModPriority OptionPriority(Index optionIdx); - public ISubMod this[Index idx] { get; } + public SubMod this[Index idx] { get; } public bool IsOption { get; } @@ -37,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; - private readonly ISubMod? _defaultMod; + private readonly SubMod? _defaultMod; private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) @@ -59,7 +59,7 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, ISubMod @default, bool onlyAscii) + public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; @@ -91,7 +91,7 @@ public readonly struct ModSaveGroup : ISavable j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) { - ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch + SubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch { GroupType.Multi => _group.OptionPriority(idx), _ => null, @@ -103,7 +103,7 @@ public readonly struct ModSaveGroup : ISavable } else { - ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); + SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); } } } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs deleted file mode 100644 index e997e07d..00000000 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.Subclasses; - -public interface ISubMod -{ - public string Name { get; } - public string FullName { get; } - public string Description { get; } - - public IReadOnlyDictionary Files { get; } - public IReadOnlyDictionary FileSwaps { get; } - public IReadOnlySet Manipulations { get; } - - public void AddData(Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); - } - - public bool IsDefault { get; } - - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(Name)); - j.WriteValue(mod.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(mod.Description); - if (priority != null) - { - j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value.Value); - } - - j.WritePropertyName(nameof(mod.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.Manipulations)); - serializer.Serialize(j, mod.Manipulations); - j.WriteEndObject(); - } -} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 380b242c..81a3bb41 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -2,6 +2,7 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.String.Classes; @@ -34,44 +35,14 @@ public class ModSettings }; // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. - public static (Dictionary, HashSet) GetResolveData(Mod mod, ModSettings? settings) + public static AppliedModData GetResolveData(Mod mod, ModSettings? settings) { if (settings == null) settings = DefaultSettings(mod); else settings.Settings.FixSize(mod); - var dict = new Dictionary(); - var set = new HashSet(); - - foreach (var (group, index) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) - { - if (group.Type is GroupType.Single) - { - if (group.Count > 0) - AddOption(group[settings.Settings[index].AsIndex]); - } - else - { - foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index))) - { - if (settings.Settings[index].HasFlag(optionIdx)) - AddOption(option); - } - } - } - - AddOption(mod.Default); - return (dict, set); - - void AddOption(ISubMod option) - { - foreach (var (path, file) in option.Files.Concat(option.FileSwaps)) - dict.TryAdd(path, file); - - foreach (var manip in option.Manipulations) - set.Add(manip); - } + return mod.GetData(settings); } // Automatically react to changes in a mods available options. diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 266d3037..1600072e 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -24,7 +24,7 @@ public sealed class MultiModGroup : IModGroup public ModPriority OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; - public ISubMod this[Index idx] + public SubMod this[Index idx] => PrioritizedOptions[idx].Mod; public bool IsOption @@ -36,7 +36,7 @@ public sealed class MultiModGroup : IModGroup public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -117,7 +117,7 @@ public sealed class MultiModGroup : IModGroup foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) { if (setting.HasFlag(index)) - ((ISubMod)option.Mod).AddData(redirections, manipulations); + option.Mod.AddData(redirections, manipulations); } } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index f797a709..2d49fd1f 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -24,7 +24,7 @@ public sealed class SingleModGroup : IModGroup public ModPriority OptionPriority(Index _) => Priority; - public ISubMod this[Index idx] + public SubMod this[Index idx] => OptionData[idx]; public bool IsOption @@ -34,7 +34,7 @@ public sealed class SingleModGroup : IModGroup public int Count => OptionData.Count; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => OptionData.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 4f35cd33..386910e5 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -15,7 +16,7 @@ namespace Penumbra.Mods.Subclasses; /// Nothing is checked for existence or validity when loading. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// -public sealed class SubMod : ISubMod +public sealed class SubMod { public string Name { get; set; } = "Default"; @@ -29,7 +30,17 @@ public sealed class SubMod : ISubMod internal int OptionIdx { get; private set; } public bool IsDefault - => GroupIdx < 0; + => GroupIdx < 0; + + public void AddData(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } public Dictionary FileData = []; public Dictionary FileSwapData = []; @@ -60,8 +71,8 @@ public sealed class SubMod : ISubMod ManipulationData.Clear(); // Every option has a name, but priorities are only relevant for multi group options. - Name = json[nameof(ISubMod.Name)]?.ToObject() ?? string.Empty; - Description = json[nameof(ISubMod.Description)]?.ToObject() ?? string.Empty; + Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + Description = json[nameof(Description)]?.ToObject() ?? string.Empty; priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; var files = (JObject?)json[nameof(Files)]; @@ -104,4 +115,43 @@ public sealed class SubMod : ISubMod } } } + + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) + { + j.WriteStartObject(); + j.WritePropertyName(nameof(Name)); + j.WriteValue(mod.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(mod.Description); + if (priority != null) + { + j.WritePropertyName(nameof(IModGroup.Priority)); + j.WriteValue(priority.Value.Value); + } + + j.WritePropertyName(nameof(mod.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in mod.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(mod.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in mod.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(mod.Manipulations)); + serializer.Serialize(j, mod.Manipulations); + j.WriteEndObject(); + } } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 8f27e201..41c1211f 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -39,12 +39,9 @@ public class TemporaryMod : IMod dict.TryAdd(gamePath, file); } - return new AppliedModData(dict, Default.Manipulations); + return new AppliedModData(dict, Default.ManipulationData); } - ISubMod IMod.Default - => Default; - public IReadOnlyList Groups => Array.Empty(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index c8db7770..f765b47e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -192,7 +192,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -228,7 +228,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, FileRegistry registry, ISubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 10956deb..4ecacece 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -227,7 +227,7 @@ public partial class ModEditWindow return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod, bool replaceNonAscii) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; From 2d5afde61274f3602f15252eb64efbcb899c5cae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:02:30 +0200 Subject: [PATCH 06/12] Fix group priority writing. --- Penumbra/Mods/Subclasses/IModGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 3f363542..7554f6dc 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -82,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName(nameof(_group.Description)); j.WriteValue(_group.Description); j.WritePropertyName(nameof(_group.Priority)); - j.WriteValue(_group.Priority); + j.WriteValue(_group.Priority.Value); j.WritePropertyName(nameof(Type)); j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); From f86f29b44a4d3dc78929107a9c99538b0d314d6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:03:50 +0200 Subject: [PATCH 07/12] Some fixes. --- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +-- Penumbra/Mods/Subclasses/IModGroup.cs | 33 ++++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 07c6f38e..9d942574 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -158,10 +158,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { var group = mod.Groups[groupIdx]; var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) + if (option.Description == newDescription) return; - s.Description = newDescription; + option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 7554f6dc..38f070b3 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -72,8 +72,9 @@ public readonly struct ModSaveGroup : ISavable public void Save(StreamWriter writer) { - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; if (_groupIdx >= 0) { j.WriteStartObject(); @@ -87,19 +88,25 @@ public readonly struct ModSaveGroup : ISavable j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); j.WriteValue(_group.DefaultSettings.Value); - j.WritePropertyName("Options"); - j.WriteStartArray(); - for (var idx = 0; idx < _group.Count; ++idx) + switch (_group) { - SubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch - { - GroupType.Multi => _group.OptionPriority(idx), - _ => null, - }); + case SingleModGroup single: + j.WritePropertyName("Options"); + j.WriteStartArray(); + foreach (var option in single.OptionData) + SubMod.WriteSubMod(j, serializer, option, _basePath, null); + j.WriteEndArray(); + j.WriteEndObject(); + break; + case MultiModGroup multi: + j.WritePropertyName("Options"); + j.WriteStartArray(); + foreach (var (option, priority) in multi.PrioritizedOptions) + SubMod.WriteSubMod(j, serializer, option, _basePath, priority); + j.WriteEndArray(); + j.WriteEndObject(); + break; } - - j.WriteEndArray(); - j.WriteEndObject(); } else { From b99a809eba50019fad9aa7e1b25ce73c5ebca6fa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:26:12 +0200 Subject: [PATCH 08/12] Remove OptionPriority from general option groups. --- Penumbra/Mods/Subclasses/IModGroup.cs | 4 ++-- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 ++++-- Penumbra/Mods/Subclasses/SingleModGroup.cs | 6 ++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 13 ++++++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 10 +++++++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 38f070b3..96d7c6b7 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -16,7 +16,7 @@ public interface IModGroup : IReadOnlyCollection public ModPriority Priority { get; } public Setting DefaultSettings { get; set; } - public ModPriority OptionPriority(Index optionIdx); + public FullPath? FindBestMatch(Utf8GamePath gamePath); public SubMod this[Index idx] { get; } @@ -37,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; - private readonly SubMod? _defaultMod; + private readonly SubMod? _defaultMod; private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 1600072e..02ae07f4 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -21,8 +21,10 @@ public sealed class MultiModGroup : IModGroup public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public ModPriority OptionPriority(Index idx) - => PrioritizedOptions[idx].Priority; + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => PrioritizedOptions.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) + .FirstOrDefault(); public SubMod this[Index idx] => PrioritizedOptions[idx].Mod; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 2d49fd1f..b854d2b1 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -21,8 +21,10 @@ public sealed class SingleModGroup : IModGroup public readonly List OptionData = []; - public ModPriority OptionPriority(Index _) - => Priority; + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => OptionData + .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) + .FirstOrDefault(); public SubMod this[Index idx] => OptionData[idx]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6cf24f62..a70da628 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -540,14 +540,17 @@ public partial class ModEditWindow : Window, IDisposable return currentFile.Value; if (Mod != null) - foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority) - .SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value)) - .Append(Mod.Default)) + { + foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority)) { - if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) - return value; + if (option.FindBestMatch(path) is { } fullPath) + return fullPath; } + if (Mod.Default.Files.TryGetValue(path, out var value) || Mod.Default.FileSwaps.TryGetValue(path, out value)) + return value; + } + return new FullPath(path); } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 80af7b15..b002dedd 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -532,10 +532,10 @@ public class ModPanelEditTab( panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); - if (group.Type != GroupType.Multi) + if (group is not MultiModGroup multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, 50 * UiHelpers.Scale)) panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); @@ -613,7 +613,11 @@ public class ModPanelEditTab( var sourceGroup = panel._mod.Groups[sourceGroupIdx]; var currentCount = group.Count; var option = sourceGroup[sourceOption]; - var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); + var priority = sourceGroup switch + { + MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, + _ => ModPriority.Default, + }; panel._delayedActions.Enqueue(() => { panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); From c276f922a53fc5798a9ae71673614a54bc88d9df Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Apr 2024 18:22:03 +0200 Subject: [PATCH 09/12] Update API. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 0c8578cf..590629df 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 0c8578cfa12bf0591ed204fd89b30b66719f678f +Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a From b34114400fab2f83b119ddb1ac2d8c2ff7e4a708 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:09:53 +0200 Subject: [PATCH 10/12] Fix Havok ANSI / UTF8 Issue. --- Penumbra/Import/Models/HavokConverter.cs | 10 ++++------ Penumbra/Import/Models/SkeletonConverter.cs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 89f9ac4f..dc9d3e6a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -71,8 +71,7 @@ public static unsafe class HavokConverter /// Path to a file on the filesystem. private static hkResource* Read(string filePath) { - var path = Marshal.StringToHGlobalAnsi(filePath); - + var path = Encoding.UTF8.GetBytes(filePath); var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; @@ -81,8 +80,7 @@ public static unsafe class HavokConverter loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); // TODO: probably can use LoadFromBuffer for this. - var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); - return resource; + return hkSerializeUtil.LoadFromFile(path, null, loadOptions); } /// Serializes an hkResource* to a temporary file. @@ -94,9 +92,9 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); + var path = Encoding.UTF8.GetBytes(tempFile); var oStream = new hkOstream(); - oStream.Ctor((byte*)path); + oStream.Ctor(path); var result = stackalloc hkResult[1]; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 7058a159..25e74332 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -84,9 +84,8 @@ public static class SkeletonConverter .Where(n => n.NodeType != XmlNodeType.Comment) .Select(n => { - var text = n.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this I mean seriously - return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); + var text = n.InnerText.AsSpan().Trim()[1..]; + return BitConverter.Int32BitsToSingle(int.Parse(text, NumberStyles.HexNumber)); }) .ToArray(); From e21c9fb6d1d71e0f952d416a8d1b0e817e450826 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:11:09 +0200 Subject: [PATCH 11/12] Fix some IPC stuff. --- Penumbra/Api/IpcProviders.cs | 5 +++-- Penumbra/Api/IpcTester/UiIpcTester.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 21fe0a7c..ebf71176 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -62,6 +62,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ApiVersion.Provider(pi, api), new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility + new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), @@ -99,9 +100,9 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui), - IpcSubscribers.PreSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui), - IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), ]; diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index d95b79b8..a2c36938 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -32,9 +32,9 @@ public class UiIpcTester : IUiService, IDisposable { _pi = pi; PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); - PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod); - PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PostSettingsPanelDraw = IpcSubscribers.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); PreSettingsTabBar.Disable(); @@ -76,7 +76,7 @@ public class UiIpcTester : IUiService, IDisposable if (!table) return; - IpcTester.DrawIntro(IpcSubscribers.PostSettingsPanelDraw.Label, "Last Drawn Mod"); + IpcTester.DrawIntro(IpcSubscribers.PostSettingsDraw.Label, "Last Drawn Mod"); ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip"); From 792a04337f31f2c53ff562c3d8116821192fe58e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:50:09 +0200 Subject: [PATCH 12/12] Add a try-catch when scanning for mods. --- Penumbra/Mods/Manager/ModManager.cs | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 40585520..d912e292 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,3 +1,4 @@ +using System.Security.AccessControl; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -311,22 +312,31 @@ public sealed class ModManager : ModStorage, IDisposable /// private void ScanMods() { - var options = new ParallelOptions() + try { - MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), - }; - var queue = new ConcurrentQueue(); - Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => - { - var mod = Creator.LoadMod(dir, false); - if (mod != null) - queue.Enqueue(mod); - }); + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Creator.LoadMod(dir, false); + if (mod != null) + queue.Enqueue(mod); + }); - foreach (var mod in queue) + foreach (var mod in queue) + { + mod.Index = Count; + Mods.Add(mod); + } + } + catch (Exception ex) { - mod.Index = Count; - Mods.Add(mod); + Valid = false; + _communicator.ModDirectoryChanged.Invoke(BasePath.FullName, false); + Penumbra.Log.Error($"Could not scan for mods:\n{ex}"); } } }