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),
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.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),
];

View file

@ -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");

View file

@ -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
/// <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)
{
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);
}
/// <summary> Invoke only if not in a full recalculation. </summary>

View file

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

View file

@ -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<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> BaseColor { get; } = new(normal.Width, normal.Height);

View file

@ -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<ushort, int>();
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
@ -316,7 +313,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(),

View file

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

View file

@ -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<Node> nodes, IoNotifier notifier)
{
public struct Mesh
{
public MdlStructs.MeshStruct MeshStruct;
public MeshStruct MeshStruct;
public List<MdlStructs.SubmeshStruct> SubMeshStructs;
public string? Material;
@ -69,10 +70,14 @@ public class MeshImporter(IEnumerable<Node> 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)

View file

@ -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<MdlStructs.MeshStruct> _meshes = [];
private readonly List<MeshStruct> _meshes = [];
private readonly List<MdlStructs.SubmeshStruct> _subMeshes = [];
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<string> _bones = [];
private readonly List<MdlStructs.BoneTableStruct> _boneTables = [];
private readonly List<string> _bones = [];
private readonly List<BoneTableStruct> _boneTables = [];
private readonly BoundingBox _boundingBox = new BoundingBox();
private readonly BoundingBox _boundingBox = new();
private readonly List<string> _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<Node> 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<string> boneNames)
{
var boneIndices = new List<ushort>();
@ -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,

View file

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

View file

@ -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;

View file

@ -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));
}
}

View file

@ -5,7 +5,7 @@ namespace Penumbra.Mods.Editor;
public class FileRegistry : IEquatable<FileRegistry>
{
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; }

View file

@ -1,19 +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(
Dictionary<Utf8GamePath, FullPath> FileRedirections,
HashSet<MetaManipulation> 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 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
public int TotalManipulations { get; }

View file

@ -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();
/// <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);
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 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);

View file

@ -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;
}
/// <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))
.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.
/// </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)
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.
/// </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;
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>
public void RemovePathsFromSelected(ISubMod option, IEnumerable<FileRegistry> files1)
public void RemovePathsFromSelected(SubMod option, IEnumerable<FileRegistry> 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>
public void DeleteFiles(Mod mod, ISubMod option, IEnumerable<FileRegistry> files1)
public void DeleteFiles(Mod mod, SubMod option, IEnumerable<FileRegistry> 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;

View file

@ -151,7 +151,7 @@ public class ModMerger : IDisposable
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 swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

View file

@ -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;

View file

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

View file

@ -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<Utf8GamePath, FullPath> _modRedirections = [];
private HashSet<MetaManipulation> _modManipulations = [];
private AppliedModData _appliedModData = AppliedModData.Empty;
public IReadOnlyDictionary<Utf8GamePath, FullPath> ModRedirections
=> _modRedirections;
=> _appliedModData.FileRedirections;
public IReadOnlySet<MetaManipulation> ModManipulations
=> _modManipulations;
=> _appliedModData.Manipulations;
public readonly List<Swap> 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<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;
}

View file

@ -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
/// </summary>
private void ScanMods()
{
var options = new ParallelOptions()
try
{
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2),
};
var queue = new ConcurrentQueue<Mod>();
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<Mod>();
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}");
}
}
}

View file

@ -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);
}
@ -262,15 +262,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
}
/// <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);
/// <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];
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;
}

View file

@ -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;
@ -30,6 +32,9 @@ public sealed class Mod : IMod
public ModPriority Priority
=> ModPriority.Default;
IReadOnlyList<IModGroup> IMod.Groups
=> Groups;
internal Mod(DirectoryInfo modPath)
{
ModPath = modPath;
@ -59,14 +64,25 @@ public sealed class Mod : IMod
public readonly SubMod Default;
public readonly List<IModGroup> Groups = [];
ISubMod IMod.Default
=> Default;
public AppliedModData GetData(ModSettings? settings = null)
{
if (settings is not { Enabled: true })
return AppliedModData.Empty;
IReadOnlyList<IModGroup> IMod.Groups
=> Groups;
var dictRedirections = new Dictionary<Utf8GamePath, FullPath>(TotalFileCount);
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
=> Groups.SelectMany(o => o).OfType<SubMod>().Prepend(Default);
=> Groups.SelectMany(o => o).Prepend(Default);
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>
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)
{
@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
Priority = priority,
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));
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>
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,
}
/// <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.
{
Name = name,

View file

@ -1,10 +1,12 @@
using Newtonsoft.Json;
using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses;
public interface IModGroup : IReadOnlyCollection<ISubMod>
public interface IModGroup : IReadOnlyCollection<SubMod>
{
public const int MaxMultiOptions = 63;
@ -14,9 +16,9 @@ public interface IModGroup : IReadOnlyCollection<ISubMod>
public ModPriority Priority { get; }
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; }
@ -24,6 +26,8 @@ public interface IModGroup : IReadOnlyCollection<ISubMod>
public bool MoveOption(int optionIdxFrom, int optionIdxTo);
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>
public Setting FixSetting(Setting setting);
}
@ -33,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)
@ -55,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;
@ -68,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();
@ -78,28 +83,34 @@ 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));
j.WriteValue(_group.DefaultSettings.Value);
j.WritePropertyName("Options");
j.WriteStartArray();
for (var idx = 0; idx < _group.Count; ++idx)
switch (_group)
{
ISubMod.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
{
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 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<Utf8GamePath, FullPath>, HashSet<MetaManipulation>) 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<Utf8GamePath, FullPath>();
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);
}
return mod.GetData(settings);
}
// Automatically react to changes in a mods available options.

View file

@ -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;
@ -19,10 +21,12 @@ 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 ISubMod this[Index idx]
public SubMod this[Index idx]
=> PrioritizedOptions[idx].Mod;
public bool IsOption
@ -34,7 +38,7 @@ public sealed class MultiModGroup : IModGroup
public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = [];
public IEnumerator<ISubMod> GetEnumerator()
public IEnumerator<SubMod> GetEnumerator()
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
@ -110,6 +114,15 @@ public sealed class MultiModGroup : IModGroup
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)
=> new(setting.Value & ((1ul << Count) - 1));
}

View file

@ -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;
@ -19,10 +21,12 @@ public sealed class SingleModGroup : IModGroup
public readonly List<SubMod> 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 ISubMod this[Index idx]
public SubMod this[Index idx]
=> OptionData[idx];
public bool IsOption
@ -32,7 +36,7 @@ public sealed class SingleModGroup : IModGroup
public int Count
=> OptionData.Count;
public IEnumerator<ISubMod> GetEnumerator()
public IEnumerator<SubMod> GetEnumerator()
=> OptionData.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
@ -114,6 +118,9 @@ public sealed class SingleModGroup : IModGroup
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)
=> 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 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.
/// </summary>
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<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> 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>() ?? string.Empty;
Description = json[nameof(ISubMod.Description)]?.ToObject<string>() ?? string.Empty;
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty;
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? 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();
}
}

View file

@ -20,8 +20,27 @@ public class TemporaryMod : IMod
public readonly SubMod Default;
ISubMod IMod.Default
=> Default;
public AppliedModData GetData(ModSettings? settings = null)
{
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
=> Array.Empty<IModGroup>();
@ -53,7 +72,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 +106,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)
{

View file

@ -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();

View file

@ -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<MtrlFile.ColorTable>())
if (data.Length < Marshal.SizeOf<ColorTable>())
return false;
ref var rows = ref tab.Mtrl.Table;
fixed (void* ptr = data, output = &rows)
{
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<MtrlFile.ColorTable>());
if (data.Length >= Marshal.SizeOf<MtrlFile.ColorTable>() + Marshal.SizeOf<MtrlFile.ColorDyeTable>()
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<ColorTable>());
if (data.Length >= Marshal.SizeOf<ColorTable>() + Marshal.SizeOf<ColorDyeTable>()
&& tab.Mtrl.HasDyeTable)
{
ref var dyeRows = ref tab.Mtrl.DyeTable;
fixed (void* output2 = &dyeRows)
{
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<MtrlFile.ColorTable>(),
Marshal.SizeOf<MtrlFile.ColorDyeTable>());
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<ColorTable>(),
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,
"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);

View file

@ -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();

View file

@ -503,7 +503,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");

View file

@ -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;

View file

@ -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);
}

View file

@ -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);