mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-29 11:59:26 +01:00
Merge remote-tracking branch 'upstream/master' into mdl-io-triage-6
This commit is contained in:
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
|
||||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue