Merge remote-tracking branch 'upstream/master' into mdl-io-triage-6

This commit is contained in:
ackwell 2024-04-25 21:30:38 +10:00
commit c1472d5f65
41 changed files with 350 additions and 315 deletions

@ -1 +1 @@
Subproject commit 0c8578cfa12bf0591ed204fd89b30b66719f678f Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a

@ -1 +1 @@
Subproject commit fe9d563d9845630673cf098f7a6bfbd26e600fb4 Subproject commit 9208c9c242244beeb3c1fb826582d72da09831af

View file

@ -62,6 +62,7 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.ApiVersion.Provider(pi, api), IpcSubscribers.ApiVersion.Provider(pi, api),
new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility
new FuncProvider<int>(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility
IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState),
IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState),
IpcSubscribers.ModDirectoryChanged.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.ChangedItemTooltip.Provider(pi, api.Ui),
IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui),
IpcSubscribers.PreSettingsTabBarDraw.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.PostEnabledDraw.Provider(pi, api.Ui),
IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui), IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
]; ];

View file

@ -32,9 +32,9 @@ public class UiIpcTester : IUiService, IDisposable
{ {
_pi = pi; _pi = pi;
PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod);
PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod);
PostEnabled = IpcSubscribers.PostEnabledDraw.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); ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip);
ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick);
PreSettingsTabBar.Disable(); PreSettingsTabBar.Disable();
@ -76,7 +76,7 @@ public class UiIpcTester : IUiService, IDisposable
if (!table) if (!table)
return; 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"); ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip"); IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip");

View file

@ -2,12 +2,10 @@ using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Api.Enums;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
namespace Penumbra.Collections.Cache; namespace Penumbra.Collections.Cache;
@ -231,37 +229,12 @@ public sealed class CollectionCache : IDisposable
/// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary> /// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary>
internal void AddModSync(IMod mod, bool addMetaChanges) internal void AddModSync(IMod mod, bool addMetaChanges)
{ {
if (mod.Index >= 0) var files = GetFiles(mod);
{ foreach (var (path, file) in files.FileRedirections)
var settings = _collection[mod.Index].Settings; AddFile(path, file, mod);
if (settings is not { Enabled: true })
return;
foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) foreach (var manip in files.Manipulations)
{ AddManipulation(manip, mod);
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);
if (addMetaChanges) if (addMetaChanges)
{ {
@ -273,14 +246,15 @@ public sealed class CollectionCache : IDisposable
} }
} }
// Add all files and possibly manipulations of a specific submod private AppliedModData GetFiles(IMod mod)
private void AddSubMod(ISubMod subMod, IMod parentMod)
{ {
foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps)) if (mod.Index < 0)
AddFile(path, file, parentMod); return mod.GetData();
foreach (var manip in subMod.Manipulations) var settings = _collection[mod.Index].Settings;
AddManipulation(manip, parentMod); return settings is not { Enabled: true }
? AppliedModData.Empty
: mod.GetData(settings);
} }
/// <summary> Invoke only if not in a full recalculation. </summary> /// <summary> Invoke only if not in a full recalculation. </summary>

View file

@ -1,10 +1,12 @@
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache; namespace Penumbra.Collections.Cache;
/// <summary>
/// Contains associations between a mod and the paths and meta manipulations affected by that mod.
/// </summary>
public class CollectionModData public class CollectionModData
{ {
private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new(); private readonly Dictionary<IMod, (HashSet<Utf8GamePath>, HashSet<MetaManipulation>)> _data = new();
@ -17,7 +19,7 @@ public class CollectionModData
if (_data.Remove(mod, out var data)) if (_data.Remove(mod, out var data))
return data; return data;
return (Array.Empty<Utf8GamePath>(), Array.Empty<MetaManipulation>()); return ([], []);
} }
public void AddPath(IMod mod, Utf8GamePath path) public void AddPath(IMod mod, Utf8GamePath path)
@ -28,7 +30,7 @@ public class CollectionModData
} }
else else
{ {
data = (new HashSet<Utf8GamePath> { path }, new HashSet<MetaManipulation>()); data = ([path], []);
_data.Add(mod, data); _data.Add(mod, data);
} }
} }
@ -41,7 +43,7 @@ public class CollectionModData
} }
else else
{ {
data = (new HashSet<Utf8GamePath>(), new HashSet<MetaManipulation> { manipulation }); data = ([], [manipulation]);
_data.Add(mod, data); _data.Add(mod, data);
} }
} }

View file

@ -1,5 +1,6 @@
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using SharpGLTF.Materials; using SharpGLTF.Materials;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced; 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. // 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. // 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<Rgba32> normal, MtrlFile.ColorTable table) : IRowOperation private readonly struct ProcessCharacterNormalOperation(Image<Rgba32> normal, ColorTable table) : IRowOperation
{ {
public Image<Rgba32> Normal { get; } = normal.Clone(); public Image<Rgba32> Normal { get; } = normal.Clone();
public Image<Rgba32> BaseColor { get; } = new(normal.Width, normal.Height); public Image<Rgba32> BaseColor { get; } = new(normal.Width, normal.Height);

View file

@ -3,6 +3,7 @@ using Lumina.Data.Parsing;
using Lumina.Extensions; using Lumina.Extensions;
using OtterGui; using OtterGui;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Geometry; using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.IO; using SharpGLTF.IO;
@ -55,7 +56,7 @@ public class MeshExporter
private readonly byte _lod; private readonly byte _lod;
private readonly ushort _meshIndex; private readonly ushort _meshIndex;
private MdlStructs.MeshStruct XivMesh private MeshStruct XivMesh
=> _mdl.Meshes[_meshIndex]; => _mdl.Meshes[_meshIndex];
private readonly MaterialBuilder _material; private readonly MaterialBuilder _material;
@ -109,8 +110,8 @@ public class MeshExporter
var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex];
var indexMap = new Dictionary<ushort, int>(); var indexMap = new Dictionary<ushort, int>();
// #TODO @ackwell maybe fix for V6 Models, I think this works fine.
foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex())
{ {
var boneName = _mdl.Bones[xivBoneIndex]; var boneName = _mdl.Bones[xivBoneIndex];
if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex)) if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex))
@ -238,19 +239,15 @@ public class MeshExporter
{ "targetNames", shapeNames }, { "targetNames", shapeNames },
}); });
string[] attributes = []; string[] attributes = [];
var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask);
if (maxAttribute < _mdl.Attributes.Length) if (maxAttribute < _mdl.Attributes.Length)
{
attributes = Enumerable.Range(0, 32) attributes = Enumerable.Range(0, 32)
.Where(index => ((attributeMask >> index) & 1) == 1) .Where(index => ((attributeMask >> index) & 1) == 1)
.Select(index => _mdl.Attributes[index]) .Select(index => _mdl.Attributes[index])
.ToArray(); .ToArray();
}
else else
{
_notifier.Warning("Invalid attribute data, ignoring."); _notifier.Warning("Invalid attribute data, ignoring.");
}
return new MeshData return new MeshData
{ {
@ -278,7 +275,7 @@ public class MeshExporter
for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++)
{ {
streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); 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 var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements
@ -316,7 +313,7 @@ public class MeshExporter
MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), 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.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.UByte4 => reader.ReadBytes(4), 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), reader.ReadByte() / 255f),
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), 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(), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(),

View file

@ -71,7 +71,7 @@ public static unsafe class HavokConverter
/// <param name="filePath"> Path to a file on the filesystem. </param> /// <param name="filePath"> Path to a file on the filesystem. </param>
private static hkResource* Read(string filePath) private static hkResource* Read(string filePath)
{ {
var path = Encoding.UTF8.GetBytes(filePath); var path = Encoding.UTF8.GetBytes(filePath);
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1];

View file

@ -1,5 +1,6 @@
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using OtterGui; using OtterGui;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Schema2; using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import; namespace Penumbra.Import.Models.Import;
@ -8,7 +9,7 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
{ {
public struct Mesh public struct Mesh
{ {
public MdlStructs.MeshStruct MeshStruct; public MeshStruct MeshStruct;
public List<MdlStructs.SubmeshStruct> SubMeshStructs; public List<MdlStructs.SubmeshStruct> SubMeshStructs;
public string? Material; public string? Material;
@ -69,10 +70,14 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
return new Mesh return new Mesh
{ {
MeshStruct = new MdlStructs.MeshStruct MeshStruct = new MeshStruct
{ {
VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], VertexBufferOffset1 = 0,
VertexBufferStride = _strides, VertexBufferOffset2 = (uint)_streams[0].Count,
VertexBufferOffset3 = (uint)(_streams[0].Count + _streams[1].Count),
VertexBufferStride1 = _strides[0],
VertexBufferStride2 = _strides[1],
VertexBufferStride3 = _strides[2],
VertexCount = _vertexCount, VertexCount = _vertexCount,
VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements
.Select(element => element.Stream + 1) .Select(element => element.Stream + 1)

View file

@ -1,6 +1,7 @@
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using OtterGui; using OtterGui;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.ModelStructs;
using SharpGLTF.Schema2; using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import; 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]+)?$" // 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 static partial Regex MeshNameGroupingRegex();
private readonly List<MdlStructs.MeshStruct> _meshes = []; private readonly List<MeshStruct> _meshes = [];
private readonly List<MdlStructs.SubmeshStruct> _subMeshes = []; private readonly List<MdlStructs.SubmeshStruct> _subMeshes = [];
private readonly List<string> _materials = []; private readonly List<string> _materials = [];
@ -27,10 +29,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
private readonly List<ushort> _indices = []; private readonly List<ushort> _indices = [];
private readonly List<string> _bones = []; private readonly List<string> _bones = [];
private readonly List<MdlStructs.BoneTableStruct> _boneTables = []; private readonly List<BoneTableStruct> _boneTables = [];
private readonly BoundingBox _boundingBox = new BoundingBox(); private readonly BoundingBox _boundingBox = new();
private readonly List<string> _metaAttributes = []; private readonly List<string> _metaAttributes = [];
@ -95,9 +97,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
IndexBufferSize = (uint)indexBuffer.Length, IndexBufferSize = (uint)indexBuffer.Length,
}, },
], ],
Materials = [.. materials],
Materials = [.. materials],
BoundingBoxes = _boundingBox.ToStruct(), BoundingBoxes = _boundingBox.ToStruct(),
// TODO: Would be good to calculate all of this up the tree. // 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<Node> subMeshNodes, int index) private void BuildMeshForGroup(IEnumerable<Node> subMeshNodes, int index)
{ {
// Record some offsets we'll be using later, before they get mutated with mesh values. // Record some offsets we'll be using later, before they get mutated with mesh values.
var subMeshOffset = _subMeshes.Count; var subMeshOffset = _subMeshes.Count;
var vertexOffset = _vertexBuffer.Count; var vertexOffset = _vertexBuffer.Count;
var indexOffset = _indices.Count; var indexOffset = _indices.Count;
var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}")); var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}"));
var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); 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), SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset),
BoneTableIndex = boneTableIndex, BoneTableIndex = boneTableIndex,
StartIndex = meshStartIndex, StartIndex = meshStartIndex,
VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset VertexBufferOffset1 = (uint)(mesh.MeshStruct.VertexBufferOffset1 + vertexOffset),
.Select(offset => (uint)(offset + vertexOffset)) VertexBufferOffset2 = (uint)(mesh.MeshStruct.VertexBufferOffset2 + vertexOffset),
.ToArray(), VertexBufferOffset3 = (uint)(mesh.MeshStruct.VertexBufferOffset3 + vertexOffset),
}); });
_boundingBox.Merge(mesh.BoundingBox); _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 // 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. // group, so a failure on any of them will be a failure on it.
if (_shapeValues.Count > ushort.MaxValue) 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) private ushort GetMaterialIndex(string materialName)
@ -216,6 +217,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
return (ushort)count; return (ushort)count;
} }
// #TODO @ackwell fix for V6 Models
private ushort BuildBoneTable(List<string> boneNames) private ushort BuildBoneTable(List<string> boneNames)
{ {
var boneIndices = new List<ushort>(); var boneIndices = new List<ushort>();
@ -238,7 +240,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count);
var boneTableIndex = _boneTables.Count; var boneTableIndex = _boneTables.Count;
_boneTables.Add(new MdlStructs.BoneTableStruct() _boneTables.Add(new BoneTableStruct()
{ {
BoneIndex = boneIndicesArray, BoneIndex = boneIndicesArray,
BoneCount = (byte)boneIndices.Count, BoneCount = (byte)boneIndices.Count,

View file

@ -152,7 +152,7 @@ public partial class TexToolsImporter
} }
// Iterate through all pages // Iterate through all pages
var options = new List<ISubMod>(); var options = new List<SubMod>();
var groupPriority = ModPriority.Default; var groupPriority = ModPriority.Default;
var groupNames = new HashSet<string>(); var groupNames = new HashSet<string>();
foreach (var page in modList.ModPackPages) foreach (var page in modList.ModPackPages)

View file

@ -1,6 +1,5 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Penumbra.GameData.Files;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Penumbra.Interop.SafeHandles; using Penumbra.Interop.SafeHandles;
@ -9,7 +8,7 @@ namespace Penumbra.Interop.MaterialPreview;
public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
{ {
public const int TextureWidth = 4; 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; public const int TextureLength = TextureWidth * TextureHeight * 4;
private readonly IFramework _framework; private readonly IFramework _framework;
@ -17,7 +16,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
private readonly Texture** _colorTableTexture; private readonly Texture** _colorTableTexture;
private readonly SafeTextureHandle _originalColorTableTexture; private readonly SafeTextureHandle _originalColorTableTexture;
private bool _updatePending; private bool _updatePending;
public Half[] ColorTable { get; } public Half[] ColorTable { get; }
@ -40,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (_originalColorTableTexture == null) if (_originalColorTableTexture == null)
throw new InvalidOperationException("Material doesn't have a color table"); throw new InvalidOperationException("Material doesn't have a color table");
ColorTable = new Half[TextureLength]; ColorTable = new Half[TextureLength];
_updatePending = true; _updatePending = true;
framework.Update += OnFrameworkUpdate; framework.Update += OnFrameworkUpdate;

View file

@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); 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) if (!Worker.IsCompleted || _duplicates.Count == 0)
return; return;
@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return; return;
void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx)
{ {
var changes = false; var changes = false;
var dict = subMod.Files.ToDictionary(kvp => kvp.Key, var dict = subMod.Files.ToDictionary(kvp => kvp.Key,
@ -86,8 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
} }
else else
{ {
var sub = (SubMod)subMod; subMod.FileData = dict;
sub.FileData = dict;
saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
} }
} }

View file

@ -5,7 +5,7 @@ namespace Penumbra.Mods.Editor;
public class FileRegistry : IEquatable<FileRegistry> public class FileRegistry : IEquatable<FileRegistry>
{ {
public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = []; public readonly List<(SubMod, Utf8GamePath)> SubModUsage = [];
public FullPath File { get; private init; } public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; } public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; } public long FileSize { get; private init; }

View file

@ -1,19 +1,27 @@
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Subclasses; using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Editor; namespace Penumbra.Mods.Editor;
public record struct AppliedModData(
Dictionary<Utf8GamePath, FullPath> FileRedirections,
HashSet<MetaManipulation> Manipulations)
{
public static readonly AppliedModData Empty = new([], []);
}
public interface IMod public interface IMod
{ {
LowerString Name { get; } LowerString Name { get; }
public int Index { get; } public int Index { get; }
public ModPriority Priority { get; } public ModPriority Priority { get; }
public ISubMod Default { get; } public IReadOnlyList<IModGroup> Groups { get; }
public IReadOnlyList<IModGroup> Groups { get; }
public IEnumerable<SubMod> AllSubMods { get; } public AppliedModData GetData(ModSettings? settings = null);
// Cache // Cache
public int TotalManipulations { get; } public int TotalManipulations { get; }

View file

@ -29,7 +29,7 @@ public class ModEditor(
public int OptionIdx { get; private set; } public int OptionIdx { get; private set; }
public IModGroup? Group { 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) public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0); => LoadMod(mod, -1, 0);
@ -104,7 +104,7 @@ public class ModEditor(
=> Clear(); => Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary> /// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public static void ApplyToAllOptions(Mod mod, Action<ISubMod, int, int> action) public static void ApplyToAllOptions(Mod mod, Action<SubMod, int, int> action)
{ {
action(mod.Default, -1, 0); action(mod.Default, -1, 0);
foreach (var (group, groupIdx) in mod.Groups.WithIndex()) foreach (var (group, groupIdx) in mod.Groups.WithIndex())

View file

@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable
public bool Ready { get; private set; } = true; 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()); UpdateFiles(mod, new CancellationToken());
UpdatePaths(mod, option, false, 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()); => UpdatePaths(mod, option, true, new CancellationToken());
public void Clear() public void Clear()
@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable
public void ClearMissingFiles() public void ClearMissingFiles()
=> _missing.Clear(); => _missing.Clear();
public void RemoveUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath)
{ {
_usedPaths.Remove(gamePath); _usedPaths.Remove(gamePath);
if (file != null) 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); => 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); _usedPaths.Add(gamePath);
if (file == null) if (file == null)
@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable
file.SubModUsage.Add((option, gamePath)); 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); => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
@ -154,7 +154,7 @@ public class ModFileCollection : IDisposable
_usedPaths.Clear(); _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(); tok.ThrowIfCancellationRequested();
ClearPaths(clearRegistries, tok); ClearPaths(clearRegistries, tok);

View file

@ -30,16 +30,16 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
return num; return num;
} }
public void Revert(Mod mod, ISubMod option) public void Revert(Mod mod, SubMod option)
{ {
files.UpdateAll(mod, option); files.UpdateAll(mod, option);
Changes = false; Changes = false;
} }
/// <summary> Remove all path redirections where the pointed-to file does not exist. </summary> /// <summary> Remove all path redirections where the pointed-to file does not exist. </summary>
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)) var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); .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 path is empty, it will be deleted instead.
/// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
/// </summary> /// </summary>
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) if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count)
return false; 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, /// Transform a set of files to the appropriate game paths with the given number of folders skipped,
/// and add them to the given option. /// and add them to the given option.
/// </summary> /// </summary>
public int AddPathsToSelected(ISubMod option, IEnumerable<FileRegistry> files1, int skipFolders = 0) public int AddPathsToSelected(SubMod option, IEnumerable<FileRegistry> files1, int skipFolders = 0)
{ {
var failed = 0; var failed = 0;
foreach (var file in files1) foreach (var file in files1)
@ -111,7 +111,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
} }
/// <summary> Remove all paths in the current option from the given files. </summary> /// <summary> Remove all paths in the current option from the given files. </summary>
public void RemovePathsFromSelected(ISubMod option, IEnumerable<FileRegistry> files1) public void RemovePathsFromSelected(SubMod option, IEnumerable<FileRegistry> files1)
{ {
foreach (var file in files1) foreach (var file in files1)
{ {
@ -129,7 +129,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
} }
/// <summary> Delete all given files from your filesystem </summary> /// <summary> Delete all given files from your filesystem </summary>
public void DeleteFiles(Mod mod, ISubMod option, IEnumerable<FileRegistry> files1) public void DeleteFiles(Mod mod, SubMod option, IEnumerable<FileRegistry> files1)
{ {
var deletions = 0; var deletions = 0;
foreach (var file in files1) 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)) if (!files.Missing.Contains(file))
return true; return true;

View file

@ -151,7 +151,7 @@ public class ModMerger : IDisposable
MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true);
} }
private void MergeIntoOption(IEnumerable<ISubMod> mergeOptions, SubMod option, bool fromFileToFile) private void MergeIntoOption(IEnumerable<SubMod> mergeOptions, SubMod option, bool fromFileToFile)
{ {
var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

View file

@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager)
Changes = true; Changes = true;
} }
public void Load(Mod mod, ISubMod currentOption) public void Load(Mod mod, SubMod currentOption)
{ {
OtherImcCount = 0; OtherImcCount = 0;
OtherEqpCount = 0; OtherEqpCount = 0;

View file

@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager)
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
=> _swaps; => _swaps;
public void Revert(ISubMod option) public void Revert(SubMod option)
{ {
_swaps.SetTo(option.FileSwaps); _swaps.SetTo(option.FileSwaps);
Changes = false; Changes = false;

View file

@ -5,6 +5,7 @@ using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses; using Penumbra.Mods.Subclasses;
@ -15,14 +16,13 @@ public class ItemSwapContainer
private readonly MetaFileManager _manager; private readonly MetaFileManager _manager;
private readonly ObjectIdentification _identifier; private readonly ObjectIdentification _identifier;
private Dictionary<Utf8GamePath, FullPath> _modRedirections = []; private AppliedModData _appliedModData = AppliedModData.Empty;
private HashSet<MetaManipulation> _modManipulations = [];
public IReadOnlyDictionary<Utf8GamePath, FullPath> ModRedirections public IReadOnlyDictionary<Utf8GamePath, FullPath> ModRedirections
=> _modRedirections; => _appliedModData.FileRedirections;
public IReadOnlySet<MetaManipulation> ModManipulations public IReadOnlySet<MetaManipulation> ModManipulations
=> _modManipulations; => _appliedModData.Manipulations;
public readonly List<Swap> Swaps = []; public readonly List<Swap> Swaps = [];
@ -97,12 +97,11 @@ public class ItemSwapContainer
Clear(); Clear();
if (mod == null || mod.Index < 0) if (mod == null || mod.Index < 0)
{ {
_modRedirections = []; _appliedModData = AppliedModData.Empty;
_modManipulations = [];
} }
else else
{ {
(_modRedirections, _modManipulations) = ModSettings.GetResolveData(mod, settings); _appliedModData = ModSettings.GetResolveData(mod, settings);
} }
} }
@ -120,7 +119,7 @@ public class ItemSwapContainer
private Func<MetaManipulation, MetaManipulation> MetaResolver(ModCollection? collection) private Func<MetaManipulation, MetaManipulation> 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; return m => set.TryGetValue(m, out var a) ? a : m;
} }

View file

@ -1,3 +1,4 @@
using System.Security.AccessControl;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Services; using Penumbra.Services;
@ -311,22 +312,31 @@ public sealed class ModManager : ModStorage, IDisposable
/// </summary> /// </summary>
private void ScanMods() private void ScanMods()
{ {
var options = new ParallelOptions() try
{ {
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), var options = new ParallelOptions()
}; {
var queue = new ConcurrentQueue<Mod>(); MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2),
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => };
{ var queue = new ConcurrentQueue<Mod>();
var mod = Creator.LoadMod(dir, false); Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
if (mod != null) {
queue.Enqueue(mod); 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; Valid = false;
Mods.Add(mod); _communicator.ModDirectoryChanged.Invoke(BasePath.FullName, false);
Penumbra.Log.Error($"Could not scan for mods:\n{ex}");
} }
} }
} }

View file

@ -158,10 +158,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
{ {
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
var option = group[optionIdx]; var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s) if (option.Description == newDescription)
return; return;
s.Description = newDescription; option.Description = newDescription;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
} }
@ -262,15 +262,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
} }
/// <summary> Add an existing option to a given group with default priority. </summary> /// <summary> Add an existing option to a given group with default priority. </summary>
public void AddOption(Mod mod, int groupIdx, ISubMod option) public void AddOption(Mod mod, int groupIdx, SubMod option)
=> AddOption(mod, groupIdx, option, ModPriority.Default); => AddOption(mod, groupIdx, option, ModPriority.Default);
/// <summary> Add an existing option to a given group with a given priority. </summary> /// <summary> Add an existing option to a given group with a given priority. </summary>
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]; var group = mod.Groups[groupIdx];
switch (group) 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."); + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return; return;
case SingleModGroup s: case SingleModGroup s:
o.SetPosition(groupIdx, s.Count); option.SetPosition(groupIdx, s.Count);
s.OptionData.Add(o); s.OptionData.Add(option);
break; break;
case MultiModGroup m: case MultiModGroup m:
o.SetPosition(groupIdx, m.Count); option.SetPosition(groupIdx, m.Count);
m.PrioritizedOptions.Add((o, priority)); m.PrioritizedOptions.Add((option, priority));
break; break;
} }

View file

@ -1,5 +1,7 @@
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Collections.Cache;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Subclasses; using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -30,6 +32,9 @@ public sealed class Mod : IMod
public ModPriority Priority public ModPriority Priority
=> ModPriority.Default; => ModPriority.Default;
IReadOnlyList<IModGroup> IMod.Groups
=> Groups;
internal Mod(DirectoryInfo modPath) internal Mod(DirectoryInfo modPath)
{ {
ModPath = modPath; ModPath = modPath;
@ -59,14 +64,25 @@ public sealed class Mod : IMod
public readonly SubMod Default; public readonly SubMod Default;
public readonly List<IModGroup> Groups = []; public readonly List<IModGroup> Groups = [];
ISubMod IMod.Default public AppliedModData GetData(ModSettings? settings = null)
=> Default; {
if (settings is not { Enabled: true })
return AppliedModData.Empty;
IReadOnlyList<IModGroup> IMod.Groups var dictRedirections = new Dictionary<Utf8GamePath, FullPath>(TotalFileCount);
=> Groups; var setManips = new HashSet<MetaManipulation>(TotalManipulations);
foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority))
{
var config = settings.Settings[groupIndex];
group.AddData(config, dictRedirections, setManips);
}
Default.AddData(dictRedirections, setManips);
return new AppliedModData(dictRedirections, setManips);
}
public IEnumerable<SubMod> AllSubMods public IEnumerable<SubMod> AllSubMods
=> Groups.SelectMany(o => o).OfType<SubMod>().Prepend(Default); => Groups.SelectMany(o => o).Prepend(Default);
public List<FullPath> FindUnusedFiles() public List<FullPath> FindUnusedFiles()
{ {

View file

@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
/// <summary> Create a file for an option group from given data. </summary> /// <summary> Create a file for an option group from given data. </summary>
public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name,
ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable<ISubMod> subMods) ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable<SubMod> subMods)
{ {
switch (type) switch (type)
{ {
@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
Priority = priority, Priority = priority,
DefaultSettings = defaultSettings, DefaultSettings = defaultSettings,
}; };
group.PrioritizedOptions.AddRange(subMods.OfType<SubMod>().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)); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
@ -269,7 +269,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
} }
/// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary> /// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary>
public ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option)
{ {
var list = optionFolder.EnumerateNonHiddenFiles() var list = optionFolder.EnumerateNonHiddenFiles()
.Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .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,
} }
/// <summary> Create an empty sub mod for single groups with None options. </summary> /// <summary> Create an empty sub mod for single groups with None options. </summary>
internal static ISubMod CreateEmptySubMod(string name) internal static SubMod CreateEmptySubMod(string name)
=> new SubMod(null!) // Mod is irrelevant here, only used for saving. => new SubMod(null!) // Mod is irrelevant here, only used for saving.
{ {
Name = name, Name = name,

View file

@ -1,10 +1,12 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
public interface IModGroup : IReadOnlyCollection<ISubMod> public interface IModGroup : IReadOnlyCollection<SubMod>
{ {
public const int MaxMultiOptions = 63; public const int MaxMultiOptions = 63;
@ -14,9 +16,9 @@ public interface IModGroup : IReadOnlyCollection<ISubMod>
public ModPriority Priority { get; } public ModPriority Priority { get; }
public Setting DefaultSettings { get; set; } public Setting DefaultSettings { get; set; }
public ModPriority OptionPriority(Index optionIdx); public FullPath? FindBestMatch(Utf8GamePath gamePath);
public ISubMod this[Index idx] { get; } public SubMod this[Index idx] { get; }
public bool IsOption { get; } public bool IsOption { get; }
@ -24,6 +26,8 @@ public interface IModGroup : IReadOnlyCollection<ISubMod>
public bool MoveOption(int optionIdxFrom, int optionIdxTo); public bool MoveOption(int optionIdxFrom, int optionIdxTo);
public void UpdatePositions(int from = 0); public void UpdatePositions(int from = 0);
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations);
/// <summary> Ensure that a value is valid for a group. </summary> /// <summary> Ensure that a value is valid for a group. </summary>
public Setting FixSetting(Setting setting); public Setting FixSetting(Setting setting);
} }
@ -33,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable
private readonly DirectoryInfo _basePath; private readonly DirectoryInfo _basePath;
private readonly IModGroup? _group; private readonly IModGroup? _group;
private readonly int _groupIdx; private readonly int _groupIdx;
private readonly ISubMod? _defaultMod; private readonly SubMod? _defaultMod;
private readonly bool _onlyAscii; private readonly bool _onlyAscii;
public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii)
@ -55,7 +59,7 @@ public readonly struct ModSaveGroup : ISavable
_onlyAscii = onlyAscii; _onlyAscii = onlyAscii;
} }
public ModSaveGroup(DirectoryInfo basePath, ISubMod @default, bool onlyAscii) public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii)
{ {
_basePath = basePath; _basePath = basePath;
_groupIdx = -1; _groupIdx = -1;
@ -68,8 +72,9 @@ public readonly struct ModSaveGroup : ISavable
public void Save(StreamWriter writer) public void Save(StreamWriter writer)
{ {
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; using var j = new JsonTextWriter(writer);
var serializer = new JsonSerializer { Formatting = Formatting.Indented }; j.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
if (_groupIdx >= 0) if (_groupIdx >= 0)
{ {
j.WriteStartObject(); j.WriteStartObject();
@ -78,28 +83,34 @@ public readonly struct ModSaveGroup : ISavable
j.WritePropertyName(nameof(_group.Description)); j.WritePropertyName(nameof(_group.Description));
j.WriteValue(_group.Description); j.WriteValue(_group.Description);
j.WritePropertyName(nameof(_group.Priority)); j.WritePropertyName(nameof(_group.Priority));
j.WriteValue(_group.Priority); j.WriteValue(_group.Priority.Value);
j.WritePropertyName(nameof(Type)); j.WritePropertyName(nameof(Type));
j.WriteValue(_group.Type.ToString()); j.WriteValue(_group.Type.ToString());
j.WritePropertyName(nameof(_group.DefaultSettings)); j.WritePropertyName(nameof(_group.DefaultSettings));
j.WriteValue(_group.DefaultSettings.Value); j.WriteValue(_group.DefaultSettings.Value);
j.WritePropertyName("Options"); switch (_group)
j.WriteStartArray();
for (var idx = 0; idx < _group.Count; ++idx)
{ {
ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch case SingleModGroup single:
{ j.WritePropertyName("Options");
GroupType.Multi => _group.OptionPriority(idx), j.WriteStartArray();
_ => null, 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 else
{ {
ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null);
} }
} }
} }

View file

@ -1,57 +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<Utf8GamePath, FullPath> Files { get; }
public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps { get; }
public IReadOnlySet<MetaManipulation> Manipulations { get; }
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();
}
}

View file

@ -2,6 +2,7 @@ using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.String.Classes; 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. // 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<Utf8GamePath, FullPath>, HashSet<MetaManipulation>) GetResolveData(Mod mod, ModSettings? settings) public static AppliedModData GetResolveData(Mod mod, ModSettings? settings)
{ {
if (settings == null) if (settings == null)
settings = DefaultSettings(mod); settings = DefaultSettings(mod);
else else
settings.Settings.FixSize(mod); settings.Settings.FixSize(mod);
var dict = new Dictionary<Utf8GamePath, FullPath>(); return mod.GetData(settings);
var set = new HashSet<MetaManipulation>();
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);
}
} }
// Automatically react to changes in a mods available options. // Automatically react to changes in a mods available options.

View file

@ -5,6 +5,8 @@ using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
@ -19,10 +21,12 @@ public sealed class MultiModGroup : IModGroup
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; } public Setting DefaultSettings { get; set; }
public ModPriority OptionPriority(Index idx) public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> PrioritizedOptions[idx].Priority; => 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 ISubMod this[Index idx] public SubMod this[Index idx]
=> PrioritizedOptions[idx].Mod; => PrioritizedOptions[idx].Mod;
public bool IsOption public bool IsOption
@ -34,7 +38,7 @@ public sealed class MultiModGroup : IModGroup
public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = [];
public IEnumerator<ISubMod> GetEnumerator() public IEnumerator<SubMod> GetEnumerator()
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); => PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
@ -110,6 +114,15 @@ public sealed class MultiModGroup : IModGroup
o.SetPosition(o.GroupIdx, i); o.SetPosition(o.GroupIdx, i);
} }
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{
foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority))
{
if (setting.HasFlag(index))
option.Mod.AddData(redirections, manipulations);
}
}
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> new(setting.Value & ((1ul << Count) - 1)); => new(setting.Value & ((1ul << Count) - 1));
} }

View file

@ -3,6 +3,8 @@ using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
@ -19,10 +21,12 @@ public sealed class SingleModGroup : IModGroup
public readonly List<SubMod> OptionData = []; public readonly List<SubMod> OptionData = [];
public ModPriority OptionPriority(Index _) public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> Priority; => OptionData
.SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file))
.FirstOrDefault();
public ISubMod this[Index idx] public SubMod this[Index idx]
=> OptionData[idx]; => OptionData[idx];
public bool IsOption public bool IsOption
@ -32,7 +36,7 @@ public sealed class SingleModGroup : IModGroup
public int Count public int Count
=> OptionData.Count; => OptionData.Count;
public IEnumerator<ISubMod> GetEnumerator() public IEnumerator<SubMod> GetEnumerator()
=> OptionData.GetEnumerator(); => OptionData.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
@ -114,6 +118,9 @@ public sealed class SingleModGroup : IModGroup
o.SetPosition(o.GroupIdx, i); o.SetPosition(o.GroupIdx, i);
} }
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> this[setting.AsIndex].AddData(redirections, manipulations);
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1)));
} }

View file

@ -1,3 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
@ -15,7 +16,7 @@ namespace Penumbra.Mods.Subclasses;
/// Nothing is checked for existence or validity when loading. /// 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. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
/// </summary> /// </summary>
public sealed class SubMod : ISubMod public sealed class SubMod
{ {
public string Name { get; set; } = "Default"; public string Name { get; set; } = "Default";
@ -29,7 +30,17 @@ public sealed class SubMod : ISubMod
internal int OptionIdx { get; private set; } internal int OptionIdx { get; private set; }
public bool IsDefault public bool IsDefault
=> GroupIdx < 0; => GroupIdx < 0;
public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> 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<Utf8GamePath, FullPath> FileData = []; public Dictionary<Utf8GamePath, FullPath> FileData = [];
public Dictionary<Utf8GamePath, FullPath> FileSwapData = []; public Dictionary<Utf8GamePath, FullPath> FileSwapData = [];
@ -60,8 +71,8 @@ public sealed class SubMod : ISubMod
ManipulationData.Clear(); ManipulationData.Clear();
// Every option has a name, but priorities are only relevant for multi group options. // Every option has a name, but priorities are only relevant for multi group options.
Name = json[nameof(ISubMod.Name)]?.ToObject<string>() ?? string.Empty; Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty;
Description = json[nameof(ISubMod.Description)]?.ToObject<string>() ?? string.Empty; Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default; priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
var files = (JObject?)json[nameof(Files)]; 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();
}
} }

View file

@ -20,8 +20,27 @@ public class TemporaryMod : IMod
public readonly SubMod Default; public readonly SubMod Default;
ISubMod IMod.Default public AppliedModData GetData(ModSettings? settings = null)
=> Default; {
Dictionary<Utf8GamePath, FullPath> 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<Utf8GamePath, FullPath>(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.ManipulationData);
}
public IReadOnlyList<IModGroup> Groups public IReadOnlyList<IModGroup> Groups
=> Array.Empty<IModGroup>(); => Array.Empty<IModGroup>();
@ -53,7 +72,8 @@ public class TemporaryMod : IMod
dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true);
var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files"));
modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, 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 mod = new Mod(dir);
var defaultMod = mod.Default; var defaultMod = mod.Default;
foreach (var (gamePath, fullPath) in collection.ResolvedFiles) foreach (var (gamePath, fullPath) in collection.ResolvedFiles)
@ -86,7 +106,8 @@ public class TemporaryMod : IMod
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
modManager.AddMod(dir); 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) catch (Exception e)
{ {

View file

@ -192,7 +192,7 @@ public partial class ModEditWindow
ImGuiUtil.RightAlign(rightText); 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); using var id = ImRaii.PushId(j);
ImGui.TableNextColumn(); 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 tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty;
var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight();

View file

@ -4,6 +4,7 @@ using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.String.Functions; using Penumbra.String.Functions;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -74,7 +75,7 @@ public partial class ModEditWindow
ImGui.TableHeader("Dye Preview"); 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); ret |= DrawColorTableRow(tab, i, disabled);
ImGui.TableNextRow(); ImGui.TableNextRow();
@ -115,8 +116,8 @@ public partial class ModEditWindow
{ {
var ret = false; var ret = false;
if (tab.Mtrl.HasDyeTable) if (tab.Mtrl.HasDyeTable)
for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) for (var i = 0; i < ColorTable.NumUsedRows; ++i)
ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId); ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0);
tab.UpdateColorTablePreview(); tab.UpdateColorTablePreview();
@ -140,21 +141,21 @@ public partial class ModEditWindow
{ {
var text = ImGui.GetClipboardText(); var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text); var data = Convert.FromBase64String(text);
if (data.Length < Marshal.SizeOf<MtrlFile.ColorTable>()) if (data.Length < Marshal.SizeOf<ColorTable>())
return false; return false;
ref var rows = ref tab.Mtrl.Table; ref var rows = ref tab.Mtrl.Table;
fixed (void* ptr = data, output = &rows) fixed (void* ptr = data, output = &rows)
{ {
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<MtrlFile.ColorTable>()); MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<ColorTable>());
if (data.Length >= Marshal.SizeOf<MtrlFile.ColorTable>() + Marshal.SizeOf<MtrlFile.ColorDyeTable>() if (data.Length >= Marshal.SizeOf<ColorTable>() + Marshal.SizeOf<ColorDyeTable>()
&& tab.Mtrl.HasDyeTable) && tab.Mtrl.HasDyeTable)
{ {
ref var dyeRows = ref tab.Mtrl.DyeTable; ref var dyeRows = ref tab.Mtrl.DyeTable;
fixed (void* output2 = &dyeRows) fixed (void* output2 = &dyeRows)
{ {
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<MtrlFile.ColorTable>(), MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<ColorTable>(),
Marshal.SizeOf<MtrlFile.ColorDyeTable>()); Marshal.SizeOf<ColorDyeTable>());
} }
} }
} }
@ -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, if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Export this row to your clipboard.", false, true)) "Export this row to your clipboard.", false, true))
@ -177,11 +178,11 @@ public partial class ModEditWindow
try try
{ {
var data = new byte[MtrlFile.ColorTable.Row.Size + 2]; var data = new byte[ColorTable.Row.Size + 2];
fixed (byte* ptr = data) fixed (byte* ptr = data)
{ {
MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorTable.Row.Size); MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size);
MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorTable.Row.Size, &dye, 2); MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, 2);
} }
var text = Convert.ToBase64String(data); var text = Convert.ToBase64String(data);
@ -217,15 +218,15 @@ public partial class ModEditWindow
{ {
var text = ImGui.GetClipboardText(); var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String(text); var data = Convert.FromBase64String(text);
if (data.Length != MtrlFile.ColorTable.Row.Size + 2 if (data.Length != ColorTable.Row.Size + 2
|| !tab.Mtrl.HasTable) || !tab.Mtrl.HasTable)
return false; return false;
fixed (byte* ptr = data) fixed (byte* ptr = data)
{ {
tab.Mtrl.Table[rowIdx] = *(MtrlFile.ColorTable.Row*)ptr; tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr;
if (tab.Mtrl.HasDyeTable) 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); tab.UpdateColorTableRowPreview(rowIdx);
@ -451,7 +452,7 @@ public partial class ModEditWindow
return ret; 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; var stain = _stainService.StainCombo.CurrentSelection.Key;
if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) 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()), var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Apply the selected dye to this row.", disabled, true); "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) if (ret)
tab.UpdateColorTableRowPreview(rowIdx); tab.UpdateColorTableRowPreview(rowIdx);

View file

@ -8,6 +8,7 @@ using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.MaterialPreview; using Penumbra.Interop.MaterialPreview;
@ -601,7 +602,7 @@ public partial class ModEditWindow
var stm = _edit._stainService.StmFile; var stm = _edit._stainService.StmFile;
var dye = Mtrl.DyeTable[rowIdx]; var dye = Mtrl.DyeTable[rowIdx];
if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) 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) if (HighlightedColorTableRow == rowIdx)
@ -628,12 +629,12 @@ public partial class ModEditWindow
{ {
var stm = _edit._stainService.StmFile; var stm = _edit._stainService.StmFile;
var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; 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]; ref var row = ref rows[i];
var dye = Mtrl.DyeTable[i]; var dye = Mtrl.DyeTable[i];
if (stm.TryGetValue(dye.Template, stainId, out var dyes)) 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 level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f;
var baseColor = ColorId.InGameHighlight.Value(); var baseColor = ColorId.InGameHighlight.Value();

View file

@ -503,7 +503,7 @@ public partial class ModEditWindow
if (table) if (table)
{ {
ImGuiUtil.DrawTableColumn("Version"); ImGuiUtil.DrawTableColumn("Version");
ImGuiUtil.DrawTableColumn(_lastFile.Version.ToString()); ImGuiUtil.DrawTableColumn($"0x{_lastFile.Version:X}");
ImGuiUtil.DrawTableColumn("Radius"); ImGuiUtil.DrawTableColumn("Radius");
ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture));
ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); ImGuiUtil.DrawTableColumn("Model Clip Out Distance");

View file

@ -227,7 +227,7 @@ public partial class ModEditWindow
return fileRegistry; 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 path = mod.ModPath;
var subDirs = 0; var subDirs = 0;

View file

@ -540,14 +540,17 @@ public partial class ModEditWindow : Window, IDisposable
return currentFile.Value; return currentFile.Value;
if (Mod != null) 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)) foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority))
.Append(Mod.Default))
{ {
if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) if (option.FindBestMatch(path) is { } fullPath)
return value; return fullPath;
} }
if (Mod.Default.Files.TryGetValue(path, out var value) || Mod.Default.FileSwaps.TryGetValue(path, out value))
return value;
}
return new FullPath(path); return new FullPath(path);
} }

View file

@ -532,10 +532,10 @@ public class ModPanelEditTab(
panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (group.Type != GroupType.Multi) if (group is not MultiModGroup multi)
return; 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)) 50 * UiHelpers.Scale))
panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
@ -613,7 +613,11 @@ public class ModPanelEditTab(
var sourceGroup = panel._mod.Groups[sourceGroupIdx]; var sourceGroup = panel._mod.Groups[sourceGroupIdx];
var currentCount = group.Count; var currentCount = group.Count;
var option = sourceGroup[sourceOption]; 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._delayedActions.Enqueue(() =>
{ {
panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption);