diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 33b366d3..7990122a 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -51,7 +51,7 @@ public readonly struct ImcCache : IDisposable try { if (!_imcFiles.TryGetValue(path, out var file)) - file = new ImcFile(manager, manip); + file = new ImcFile(manager, manip.Identifier); _imcManipulations[idx] = (manip, file); if (!manip.Apply(file)) diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index f062ae25..554cf848 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -110,7 +110,7 @@ public partial class TexToolsMeta var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, new ImcEntry()); - var def = new ImcFile(_metaFileManager, manip); + var def = new ImcFile(_metaFileManager, manip.Identifier); var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 03bdbd90..09bd2c12 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -133,7 +133,7 @@ public partial class TexToolsMeta { case MetaManipulation.Type.Imc: var allManips = manips.ToList(); - var baseFile = new ImcFile(manager, allManips[0].Imc); + var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier); foreach (var manip in allManips) manip.Imc.Apply(baseFile); diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 68d3f5b3..5d704cf8 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -9,20 +9,20 @@ namespace Penumbra.Meta.Files; public class ImcException : Exception { - public readonly ImcManipulation Manipulation; - public readonly string GamePath; + public readonly ImcIdentifier Identifier; + public readonly string GamePath; - public ImcException(ImcManipulation manip, Utf8GamePath path) + public ImcException(ImcIdentifier identifier, Utf8GamePath path) { - Manipulation = manip; - GamePath = path.ToString(); + Identifier = identifier; + GamePath = path.ToString(); } public override string Message => "Could not obtain default Imc File.\n" + " Either the default file does not exist (possibly for offhand files from TexTools) or the installation is corrupted.\n" + $" Game Path: {GamePath}\n" - + $" Manipulation: {Manipulation}"; + + $" Manipulation: {Identifier}"; } public unsafe class ImcFile : MetaBaseFile @@ -142,13 +142,14 @@ public unsafe class ImcFile : MetaBaseFile } } - public ImcFile(MetaFileManager manager, ImcManipulation manip) + public ImcFile(MetaFileManager manager, ImcIdentifier identifier) : base(manager, 0) { - Path = manip.GamePath(); - var file = manager.GameData.GetFile(Path.ToString()); + var path = identifier.GamePathString(); + Path = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; + var file = manager.GameData.GetFile(path); if (file == null) - throw new ImcException(manip, Path); + throw new ImcException(identifier, Path); fixed (byte* ptr = file.Data) { diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 14486e21..650919a3 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -4,11 +4,32 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; -public class ImcChecker(MetaFileManager metaFileManager) +public class ImcChecker { + private static readonly Dictionary VariantCounts = []; + private static MetaFileManager? _dataManager; + + + public static int GetVariantCount(ImcIdentifier identifier) + { + if (VariantCounts.TryGetValue(identifier, out var count)) + return count; + + count = GetFile(identifier)?.Count ?? 0; + VariantCounts[identifier] = count; + return count; + } + public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); private readonly Dictionary _cachedDefaultEntries = new(); + private readonly MetaFileManager _metaFileManager; + + public ImcChecker(MetaFileManager metaFileManager) + { + _metaFileManager = metaFileManager; + _dataManager = metaFileManager; + } public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) { @@ -17,7 +38,7 @@ public class ImcChecker(MetaFileManager metaFileManager) try { - var e = ImcFile.GetDefault(metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); entry = new CachedEntry(e, true, entryExists); } catch (Exception) @@ -33,4 +54,19 @@ public class ImcChecker(MetaFileManager metaFileManager) public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, imcManip.EquipSlot, imcManip.BodySlot), storeCache); + + private static ImcFile? GetFile(ImcIdentifier identifier) + { + if (_dataManager == null) + return null; + + try + { + return new ImcFile(_dataManager, identifier); + } + catch + { + return null; + } + } } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index fef86520..2a2f4c03 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -31,12 +31,16 @@ public readonly record struct ImcIdentifier( => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => AddChangedItems(identifier, changedItems, false); + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { - ObjectType.Equipment or ObjectType.Accessory => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, - Variant, - "a"), + ObjectType.Equipment when allVariants => GamePaths.Equipment.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Equipment => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Accessory when allVariants => GamePaths.Accessory.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Accessory => GamePaths.Accessory.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, "a"), @@ -49,24 +53,19 @@ public readonly record struct ImcIdentifier( identifier.Identify(changedItems, path); } - public Utf8GamePath GamePath() - { - return ObjectType switch + public string GamePathString() + => ObjectType switch { - ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), + ObjectType.Accessory => GamePaths.Accessory.Imc.Path(PrimaryId), + ObjectType.Equipment => GamePaths.Equipment.Imc.Path(PrimaryId), + ObjectType.DemiHuman => GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), + ObjectType.Monster => GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), + ObjectType.Weapon => GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), + _ => string.Empty, }; - } + + public Utf8GamePath GamePath() + => Utf8GamePath.FromString(GamePathString(), out var p) ? p : Utf8GamePath.Empty; public MetaIndex FileIndex() => (MetaIndex)(-1); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e0d70aa6..b336203d 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -31,6 +32,8 @@ public class ImcModGroup(Mod mod) : IModGroup public ImcIdentifier Identifier; public ImcEntry DefaultEntry; + public bool AllVariants; + public FullPath? FindBestMatch(Utf8GamePath gamePath) => null; @@ -39,7 +42,7 @@ public class ImcModGroup(Mod mod) : IModGroup public bool CanBeDisabled { - get => OptionData.Any(m => m.IsDisableSubMod); + get => _canBeDisabled; set { _canBeDisabled = value; @@ -92,8 +95,8 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcManipulation GetManip(ushort mask) - => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, Identifier.Variant.Id, + public ImcManipulation GetManip(ushort mask, Variant variant) + => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) @@ -102,12 +105,23 @@ public class ImcModGroup(Mod mod) : IModGroup return; var mask = GetCurrentMask(setting); - var imc = GetManip(mask); - manipulations.Add(imc); + if (AllVariants) + { + var count = ImcChecker.GetVariantCount(Identifier); + if (count == 0) + manipulations.Add(GetManip(mask, Identifier.Variant)); + else + for (var i = 0; i <= count; ++i) + manipulations.Add(GetManip(mask, (Variant)i)); + } + else + { + manipulations.Add(GetManip(mask, Identifier.Variant)); + } } public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => Identifier.AddChangedItems(identifier, changedItems); + => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) => new(setting.Value & ((1ul << OptionData.Count) - 1)); @@ -120,6 +134,8 @@ public class ImcModGroup(Mod mod) : IModGroup jObj.WriteTo(jWriter); jWriter.WritePropertyName(nameof(DefaultEntry)); serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName(nameof(AllVariants)); + jWriter.WriteValue(AllVariants); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) @@ -156,6 +172,7 @@ public class ImcModGroup(Mod mod) : IModGroup Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, }; if (ret.Name.Length == 0) return null; @@ -210,7 +227,7 @@ public class ImcModGroup(Mod mod) : IModGroup if (idx >= 0) return setting.HasFlag(idx); - Penumbra.Log.Warning($"A IMC Group should be able to be disabled, but does not contain a disable option."); + Penumbra.Log.Warning("A IMC Group should be able to be disabled, but does not contain a disable option."); return false; } diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 5a5181a5..ea4ef7b1 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -51,7 +51,7 @@ public static class EquipmentSwap var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); - var imcFileTo = new ImcFile(manager, imcManip); + var imcFileTo = new ImcFile(manager, imcManip.Identifier); var skipFemale = false; var skipMale = false; var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; @@ -121,7 +121,7 @@ public static class EquipmentSwap { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); - var imcFileTo = new ImcFile(manager, imcManip); + var imcFileTo = new ImcFile(manager, imcManip.Identifier); var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -250,7 +250,7 @@ public static class EquipmentSwap PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry); + var imc = new ImcFile(manager, entry.Identifier); EquipItem[] items; Variant[] variants; if (idFrom == idTo) diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index f9fd532f..4aae45a2 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -2,6 +2,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -14,7 +15,8 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ : ModOptionEditor(communicator, saveService, config), IService { /// Add a new, empty imc group with the given manipulation data. - public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, SaveType saveType = SaveType.ImmediateSync) + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, + SaveType saveType = SaveType.ImmediateSync) { if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) return null; @@ -78,6 +80,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, option.Mod, option.Group, option, null, -1); } + public void ChangeAllVariants(ImcModGroup group, bool allVariants, SaveType saveType = SaveType.Queue) + { + if (group.AllVariants == allVariants) + return; + + group.AllVariants = allVariants; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) { if (group.CanBeDisabled == canBeDisabled) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index b129d275..d346e05c 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -19,7 +19,13 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr var entry = group.DefaultEntry; var changes = false; - ImUtf8.TextFramed(identifier.ToString(), 0, editor.AvailableWidth, borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X; + ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + ImUtf8.SameLineInner(); + var allVariants = group.AllVariants; + if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) + editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); + ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); using (ImUtf8.Group()) {