diff --git a/Penumbra.GameData b/Penumbra.GameData index e8220a0a..ec35e664 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e8220a0a74e9480330e98ed7ca462353434b9649 +Subproject commit ec35e66499eb388b4e7917e4fae4615218d33335 diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs new file mode 100644 index 00000000..14486e21 --- /dev/null +++ b/Penumbra/Meta/ImcChecker.cs @@ -0,0 +1,36 @@ +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta; + +public class ImcChecker(MetaFileManager metaFileManager) +{ + public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); + + private readonly Dictionary _cachedDefaultEntries = new(); + + public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) + { + if (_cachedDefaultEntries.TryGetValue(identifier, out var entry)) + return entry; + + try + { + var e = ImcFile.GetDefault(metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + entry = new CachedEntry(e, true, entryExists); + } + catch (Exception) + { + entry = new CachedEntry(default, false, false); + } + + if (storeCache) + _cachedDefaultEntries.Add(identifier, entry); + return entry; + } + + public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) + => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, + imcManip.EquipSlot, imcManip.BodySlot), storeCache); +} diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 9b123df1..fef86520 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -15,6 +15,8 @@ public readonly record struct ImcIdentifier( EquipSlot EquipSlot, BodySlot BodySlot) : IMetaIdentifier, IComparable { + public static readonly ImcIdentifier Default = new(EquipSlot.Body, 1, (Variant)1); + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, ushort variant) : this(primaryId, (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue), slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, @@ -25,6 +27,9 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } + public ImcManipulation ToManipulation(ImcEntry entry) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = ObjectType switch @@ -137,9 +142,12 @@ public readonly record struct ImcIdentifier( return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); } - public static ImcIdentifier? FromJson(JObject jObj) + public static ImcIdentifier? FromJson(JObject? jObj) { - var objectType = jObj["PrimaryId"]?.ToObject() ?? ObjectType.Unknown; + if (jObj == null) + return null; + + var objectType = jObj["ObjectType"]?.ToObject() ?? ObjectType.Unknown; var primaryId = new PrimaryId(jObj["PrimaryId"]?.ToObject() ?? 0); var variant = jObj["Variant"]?.ToObject() ?? 0; if (variant > byte.MaxValue) @@ -178,12 +186,12 @@ public readonly record struct ImcIdentifier( public JObject AddToJson(JObject jObj) { - jObj["ObjectType"] = ObjectType.ToString(); - jObj["PrimaryId"] = PrimaryId.Id; - jObj["PrimaryId"] = SecondaryId.Id; - jObj["Variant"] = Variant.Id; - jObj["EquipSlot"] = EquipSlot.ToString(); - jObj["BodySlot"] = BodySlot.ToString(); + jObj["ObjectType"] = ObjectType.ToString(); + jObj["PrimaryId"] = PrimaryId.Id; + jObj["SecondaryId"] = SecondaryId.Id; + jObj["Variant"] = Variant.Id; + jObj["EquipSlot"] = EquipSlot.ToString(); + jObj["BodySlot"] = BodySlot.ToString(); return jObj; } } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 45295990..945aab04 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -12,171 +12,96 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct ImcManipulation : IMetaManipulation { - public ImcEntry Entry { get; private init; } - public PrimaryId PrimaryId { get; private init; } - public PrimaryId SecondaryId { get; private init; } - public Variant Variant { get; private init; } + [JsonIgnore] + public ImcIdentifier Identifier { get; private init; } + + public ImcEntry Entry { get; private init; } + + + public PrimaryId PrimaryId + => Identifier.PrimaryId; + + public SecondaryId SecondaryId + => Identifier.SecondaryId; + + public Variant Variant + => Identifier.Variant; [JsonConverter(typeof(StringEnumConverter))] - public ObjectType ObjectType { get; private init; } + public ObjectType ObjectType + => Identifier.ObjectType; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot EquipSlot { get; private init; } + public EquipSlot EquipSlot + => Identifier.EquipSlot; [JsonConverter(typeof(StringEnumConverter))] - public BodySlot BodySlot { get; private init; } + public BodySlot BodySlot + => Identifier.BodySlot; public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) + : this(new ImcIdentifier(equipSlot, primaryId, variant), entry) + { } + + public ImcManipulation(ImcIdentifier identifier, ImcEntry entry) { - Entry = entry; - PrimaryId = primaryId; - Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - SecondaryId = 0; - ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; - EquipSlot = equipSlot; - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; + Identifier = identifier; + Entry = entry; } + // Variants were initially ushorts but got shortened to bytes. // There are still some manipulations around that have values > 255 for variant, // so we change the unused value to something nonsensical in that case, just so they do not compare equal, // and clamp the variant to 255. [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, PrimaryId secondaryId, ushort variant, + internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry) { - Entry = entry; - ObjectType = objectType; - PrimaryId = primaryId; - Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - - if (objectType is ObjectType.Accessory or ObjectType.Equipment) + Entry = entry; + var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); + Identifier = objectType switch { - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; - SecondaryId = 0; - EquipSlot = equipSlot; - } - else if (objectType is ObjectType.DemiHuman) - { - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; - SecondaryId = secondaryId; - EquipSlot = equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot; - } - else - { - BodySlot = bodySlot; - SecondaryId = secondaryId; - EquipSlot = variant > byte.MaxValue ? EquipSlot.All : EquipSlot.Unknown; - } + ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, + variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, + equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, + bodySlot), + }; } public ImcManipulation Copy(ImcEntry entry) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant.Id, EquipSlot, entry); + => new(Identifier, entry); public override string ToString() - => ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" - : $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}"; + => Identifier.ToString(); public bool Equals(ImcManipulation other) - => PrimaryId == other.PrimaryId - && Variant == other.Variant - && SecondaryId == other.SecondaryId - && ObjectType == other.ObjectType - && EquipSlot == other.EquipSlot - && BodySlot == other.BodySlot; + => Identifier == other.Identifier; public override bool Equals(object? obj) => obj is ImcManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine(PrimaryId, Variant, SecondaryId, (int)ObjectType, (int)EquipSlot, (int)BodySlot); + => Identifier.GetHashCode(); public int CompareTo(ImcManipulation other) - { - var o = ObjectType.CompareTo(other.ObjectType); - if (o != 0) - return o; - - var i = PrimaryId.Id.CompareTo(other.PrimaryId.Id); - if (i != 0) - return i; - - if (ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - var e = EquipSlot.CompareTo(other.EquipSlot); - return e != 0 ? e : Variant.Id.CompareTo(other.Variant.Id); - } - - if (ObjectType is ObjectType.DemiHuman) - { - var e = EquipSlot.CompareTo(other.EquipSlot); - if (e != 0) - return e; - } - - var s = SecondaryId.Id.CompareTo(other.SecondaryId.Id); - if (s != 0) - return s; - - var b = BodySlot.CompareTo(other.BodySlot); - return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); - } + => Identifier.CompareTo(other.Identifier); public MetaIndex FileIndex() - => (MetaIndex)(-1); + => Identifier.FileIndex(); public Utf8GamePath GamePath() - { - return 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), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId), out var p) ? p : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), - }; - } + => Identifier.GamePath(); public bool Apply(ImcFile file) => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); public bool Validate(bool withMaterial) { - switch (ObjectType) - { - case ObjectType.Accessory: - case ObjectType.Equipment: - if (BodySlot is not BodySlot.Unknown) - return false; - if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) - return false; - if (SecondaryId != 0) - return false; - - break; - case ObjectType.DemiHuman: - if (BodySlot is not BodySlot.Unknown) - return false; - if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) - return false; - - break; - default: - if (!Enum.IsDefined(BodySlot)) - return false; - if (EquipSlot is not EquipSlot.Unknown) - return false; - if (!Enum.IsDefined(ObjectType)) - return false; - - break; - } + if (!Identifier.Validate()) + return false; if (withMaterial && Entry.MaterialId == 0) return false; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index cd99396b..40fceb07 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -27,6 +27,7 @@ public unsafe class MetaFileManager internal readonly ValidityChecker ValidityChecker; internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; + internal readonly ImcChecker ImcChecker; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, @@ -40,6 +41,7 @@ public unsafe class MetaFileManager ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; + ImcChecker = new ImcChecker(this); interop.InitializeFromAttributes(this); } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index cf228889..e0d70aa6 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,25 +1,21 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab.Groups; -using Penumbra.Util; namespace Penumbra.Mods.Groups; public class ImcModGroup(Mod mod) : IModGroup { - public const int DisabledIndex = 60; - public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = string.Empty; @@ -33,33 +29,35 @@ public class ImcModGroup(Mod mod) : IModGroup public ModPriority Priority { get; set; } = ModPriority.Default; public Setting DefaultSettings { get; set; } = Setting.Zero; - public PrimaryId PrimaryId; - public SecondaryId SecondaryId; - public ObjectType ObjectType; - public BodySlot BodySlot; - public EquipSlot EquipSlot; - public Variant Variant; - - public ImcEntry DefaultEntry; + public ImcIdentifier Identifier; + public ImcEntry DefaultEntry; public FullPath? FindBestMatch(Utf8GamePath gamePath) => null; - private bool _canBeDisabled = false; + private bool _canBeDisabled; public bool CanBeDisabled { - get => _canBeDisabled; + get => OptionData.Any(m => m.IsDisableSubMod); set { _canBeDisabled = value; if (!value) + { + OptionData.RemoveAll(m => m.IsDisableSubMod); DefaultSettings = FixSetting(DefaultSettings); + } + else + { + if (!OptionData.Any(m => m.IsDisableSubMod)) + OptionData.Add(ImcSubMod.DisableSubMod(this)); + } } } public bool DefaultDisabled - => _canBeDisabled && DefaultSettings.HasFlag(DisabledIndex); + => IsDisabled(DefaultSettings); public IModOption? AddOption(string name, string description = "") { @@ -86,7 +84,7 @@ public class ImcModGroup(Mod mod) : IModGroup => []; public bool IsOption - => CanBeDisabled || OptionData.Count > 0; + => OptionData.Count > 0; public int GetIndex() => ModGroup.GetIndex(this); @@ -94,6 +92,128 @@ 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, + Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); + + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + if (IsDisabled(setting)) + return; + + var mask = GetCurrentMask(setting); + var imc = GetManip(mask); + manipulations.Add(imc); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => Identifier.AddChangedItems(identifier, changedItems); + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << OptionData.Count) - 1)); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + var jObj = Identifier.AddToJson(new JObject()); + jWriter.WritePropertyName(nameof(Identifier)); + jObj.WriteTo(jWriter); + jWriter.WritePropertyName(nameof(DefaultEntry)); + serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + if (option.IsDisableSubMod) + { + jWriter.WritePropertyName(nameof(option.IsDisableSubMod)); + jWriter.WriteValue(true); + } + else + { + jWriter.WritePropertyName(nameof(option.AttributeMask)); + jWriter.WriteValue(option.AttributeMask); + } + + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => (0, 0, 1); + + public static ImcModGroup? Load(Mod mod, JObject json) + { + var options = json["Options"]; + var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); + var ret = new ImcModGroup(mod) + { + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + }; + if (ret.Name.Length == 0) + return null; + + if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0) + { + Penumbra.Messager.NotificationMessage($"Could not add IMC group {ret.Name} because the associated IMC Entry is invalid.", + NotificationType.Warning); + return null; + } + + var rollingMask = ret.DefaultEntry.AttributeMask; + if (options != null) + foreach (var child in options.Children()) + { + var subMod = new ImcSubMod(ret, child); + + if (subMod.IsDisableSubMod) + ret._canBeDisabled = true; + + if (subMod.IsDisableSubMod && ret.OptionData.FirstOrDefault(m => m.IsDisableSubMod) is { } disable) + { + Penumbra.Messager.NotificationMessage( + $"Could not add IMC option {subMod.Name} to {ret.Name} because it already contains {disable.Name} as disable option.", + NotificationType.Warning); + } + else if ((subMod.AttributeMask & rollingMask) != 0) + { + Penumbra.Messager.NotificationMessage( + $"Could not add IMC option {subMod.Name} to {ret.Name} because it contains attributes already in use.", + NotificationType.Warning); + } + else + { + rollingMask |= subMod.AttributeMask; + ret.OptionData.Add(subMod); + } + } + + ret.Identifier = identifier.Value; + ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero; + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + return ret; + } + + private bool IsDisabled(Setting setting) + { + if (!CanBeDisabled) + return false; + + var idx = OptionData.IndexOf(m => m.IsDisableSubMod); + 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."); + return false; + } + private ushort GetCurrentMask(Setting setting) { var mask = DefaultEntry.AttributeMask; @@ -108,101 +228,4 @@ public class ImcModGroup(Mod mod) : IModGroup return mask; } - - private ushort GetFullMask() - => GetCurrentMask(Setting.AllBits(63)); - - public ImcManipulation GetManip(ushort mask) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, - DefaultEntry with { AttributeMask = mask }); - - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - { - if (CanBeDisabled && setting.HasFlag(DisabledIndex)) - return; - - var mask = GetCurrentMask(setting); - var imc = GetManip(mask); - manipulations.Add(imc); - } - - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.MetaChangedItems(changedItems, GetManip(0)); - - public Setting FixSetting(Setting setting) - => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); - - public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) - { - ModSaveGroup.WriteJsonBase(jWriter, this); - jWriter.WritePropertyName(nameof(ObjectType)); - jWriter.WriteValue(ObjectType.ToString()); - jWriter.WritePropertyName(nameof(BodySlot)); - jWriter.WriteValue(BodySlot.ToString()); - jWriter.WritePropertyName(nameof(EquipSlot)); - jWriter.WriteValue(EquipSlot.ToString()); - jWriter.WritePropertyName(nameof(PrimaryId)); - jWriter.WriteValue(PrimaryId.Id); - jWriter.WritePropertyName(nameof(SecondaryId)); - jWriter.WriteValue(SecondaryId.Id); - jWriter.WritePropertyName(nameof(Variant)); - jWriter.WriteValue(Variant.Id); - jWriter.WritePropertyName(nameof(DefaultEntry)); - serializer.Serialize(jWriter, DefaultEntry); - jWriter.WritePropertyName("Options"); - jWriter.WriteStartArray(); - foreach (var option in OptionData) - { - jWriter.WriteStartObject(); - SubMod.WriteModOption(jWriter, option); - jWriter.WritePropertyName(nameof(option.AttributeMask)); - jWriter.WriteValue(option.AttributeMask); - jWriter.WriteEndObject(); - } - - jWriter.WriteEndArray(); - } - - public (int Redirections, int Swaps, int Manips) GetCounts() - => (0, 0, 1); - - public static ImcModGroup? Load(Mod mod, JObject json) - { - var options = json["Options"]; - var ret = new ImcModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - ObjectType = json[nameof(ObjectType)]?.ToObject() ?? ObjectType.Unknown, - BodySlot = json[nameof(BodySlot)]?.ToObject() ?? BodySlot.Unknown, - EquipSlot = json[nameof(EquipSlot)]?.ToObject() ?? EquipSlot.Unknown, - PrimaryId = new PrimaryId(json[nameof(PrimaryId)]?.ToObject() ?? 0), - SecondaryId = new SecondaryId(json[nameof(SecondaryId)]?.ToObject() ?? 0), - Variant = new Variant(json[nameof(Variant)]?.ToObject() ?? 0), - CanBeDisabled = json[nameof(CanBeDisabled)]?.ToObject() ?? false, - DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), - }; - if (ret.Name.Length == 0) - return null; - - if (options != null) - foreach (var child in options.Children()) - { - var subMod = new ImcSubMod(ret, child); - ret.OptionData.Add(subMod); - } - - if (!new ImcManipulation(ret.ObjectType, ret.BodySlot, ret.PrimaryId, ret.SecondaryId.Id, ret.Variant.Id, ret.EquipSlot, - ret.DefaultEntry).Validate(true)) - { - Penumbra.Messager.NotificationMessage($"Could not add IMC group because the associated IMC Entry is invalid.", - NotificationType.Warning); - return null; - } - - ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); - return ret; - } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 20021d29..f9fd532f 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -7,7 +7,6 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; namespace Penumbra.Mods.Manager.OptionEditor; @@ -15,13 +14,13 @@ 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, ImcManipulation manip, 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; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, manip, maxPriority); + var group = CreateGroup(mod, newName, identifier, defaultEntry, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -97,19 +96,14 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ }; - private static ImcModGroup CreateGroup(Mod mod, string newName, ImcManipulation manip, ModPriority priority, + private static ImcModGroup CreateGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { Name = newName, Priority = priority, - ObjectType = manip.ObjectType, - EquipSlot = manip.EquipSlot, - BodySlot = manip.BodySlot, - PrimaryId = manip.PrimaryId, - SecondaryId = manip.SecondaryId.Id, - Variant = manip.Variant, - DefaultEntry = manip.Entry, + Identifier = identifier, + DefaultEntry = defaultEntry, }; protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 969ad3fa..e1db0ccf 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -246,7 +246,7 @@ public class ModGroupEditor( { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), _ => null, }; diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index 7f46bc95..c5c8f002 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -12,13 +12,23 @@ public class ImcSubMod(ImcModGroup group) : IModOption : this(group) { SubMod.LoadOptionData(json, this); - AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); + AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); + IsDisableSubMod = json[nameof(IsDisableSubMod)]?.ToObject() ?? false; } + public static ImcSubMod DisableSubMod(ImcModGroup group) + => new(group) + { + Name = "Disable", + AttributeMask = 0, + IsDisableSubMod = true, + }; + public Mod Mod => Group.Mod; public ushort AttributeMask; + public bool IsDisableSubMod { get; private init; } Mod IModOption.Mod => Mod; diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 19ae31a2..0c6648ba 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -101,7 +101,8 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(p => p.GetRequiredService().ImcChecker); private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 99889360..68933c9e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -19,9 +19,6 @@ public partial class ModEditWindow private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string PrimaryIdTooltip = - "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; private const string EquipSlotTooltip = "Equip Slot"; private const string ModelRaceTooltip = "Model Race"; @@ -316,7 +313,7 @@ public partial class ModEditWindow private static class ImcRow { - private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; private static float IdWidth => 80 * UiHelpers.Scale; @@ -324,75 +321,60 @@ public partial class ModEditWindow private static float SmallIdWidth => 45 * UiHelpers.Scale; - /// Convert throwing to null-return if the file does not exist. - private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc) - { - try - { - return ImcFile.GetDefault(metaFileManager, imc.GamePath(), imc.EquipSlot, imc.Variant, out _); - } - catch - { - return null; - } - } - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(metaFileManager, _new); - var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; - defaultEntry ??= new ImcEntry(); + var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); + var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); + var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); + var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry.Value)); + editor.MetaEditor.Add(manip); // Identifier ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _new); + var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _new); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. - if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _new); + if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _new); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _new); + change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); ImGui.TableNextColumn(); - if (_new.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _new, 70); + if (_newIdentifier.ObjectType is ObjectType.DemiHuman) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); else ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); if (change) - _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); + defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; // Values using var disabled = ImRaii.Disabled(); - - var entry = defaultEntry.Value; ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(entry, ref entry, false); + ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(entry, ref entry, false); + ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(entry, ref entry, false); + ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(entry, ref entry, false); + ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(entry, ref entry, false); + ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(entry, ref entry); + ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); } public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) @@ -439,10 +421,9 @@ public partial class ModEditWindow using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry(); + var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; var newEntry = meta.Entry; - - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); ImGui.SameLine(); changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 2d80d3df..3ac10cd0 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -5,7 +5,6 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -17,20 +16,20 @@ namespace Penumbra.UI.ModsTab.Groups; public class AddGroupDrawer : IUiService { private string _groupName = string.Empty; - private bool _groupNameValid = false; + private bool _groupNameValid; - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; - private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; + private ImcIdentifier _imcIdentifier = ImcIdentifier.Default; + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly ImcChecker _imcChecker; + private readonly ModManager _modManager; - public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker) { - _metaManager = metaManager; _modManager = modManager; + _imcChecker = imcChecker; UpdateEntry(); } @@ -61,7 +60,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -74,35 +73,35 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } private void DrawImcInput(float width) { - var change = ImcManipulationDrawer.DrawObjectType(ref _imcManip, width); + var change = ImcManipulationDrawer.DrawObjectType(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcManip, width); - if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcIdentifier, width); + if (_imcIdentifier.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); } - else if (_imcManip.ObjectType is ObjectType.DemiHuman) + else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); } else { - change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); } if (change) @@ -125,8 +124,8 @@ public class AddGroupDrawer : IUiService : "Add a new multi selection option group to this mod."u8, width, !_groupNameValid || _entryInvalid)) { - _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry); + _groupName = string.Empty; _groupNameValid = false; } @@ -142,20 +141,7 @@ public class AddGroupDrawer : IUiService private void UpdateEntry() { - try - { - _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, - out _entryExists); - _imcFileExists = true; - } - catch (Exception) - { - _defaultEntry = new ImcEntry(); - _imcFileExists = false; - _entryExists = false; - } - - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); - _entryInvalid = !_imcManip.Validate(true); + (_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false); + _entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists; } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 2418c5cb..045149c9 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,10 +1,8 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Text; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; @@ -16,59 +14,75 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr { public void Draw() { + var identifier = group.Identifier; + var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var entry = group.DefaultEntry; + var changes = false; + + ImUtf8.TextFramed(identifier.ToString(), 0, editor.AvailableWidth, borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + using (ImUtf8.Group()) { - ImUtf8.Text("Object Type"u8); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text("Slot"u8); - ImUtf8.Text("Primary ID"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text("Secondary ID"); - ImUtf8.Text("Variant"u8); - ImUtf8.TextFrameAligned("Material ID"u8); - ImUtf8.TextFrameAligned("Material Animation ID"u8); - ImUtf8.TextFrameAligned("Decal ID"u8); ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + changes |= ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref entry, true); + } + + ImGui.SameLine(0, editor.PriorityWidth); + using (ImUtf8.Group()) + { + ImUtf8.TextFrameAligned("Material Animation ID"u8); ImUtf8.TextFrameAligned("Sound ID"u8); ImUtf8.TextFrameAligned("Can Be Disabled"u8); - ImUtf8.TextFrameAligned("Default Attributes"u8); } ImGui.SameLine(); + using (ImUtf8.Group()) + { + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref entry, true); + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); + } + + if (changes) + editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry); + + ImGui.Dummy(Vector2.Zero); + DrawOptions(); var attributeCache = new ImcAttributeCache(group); + DrawNewOption(attributeCache); + ImGui.Dummy(Vector2.Zero); + using (ImUtf8.Group()) { - ImUtf8.Text(group.ObjectType.ToName()); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text(group.EquipSlot.ToName()); - ImUtf8.Text($"{group.PrimaryId.Id}"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text($"{group.SecondaryId.Id}"); - ImUtf8.Text($"{group.Variant.Id}"); - - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); - - var canBeDisabled = group.CanBeDisabled; - if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) - editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); - - var defaultDisabled = group.DefaultDisabled; - ImUtf8.SameLineInner(); - if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) - editor.ModManager.OptionEditor.ChangeModGroupDefaultOption(group, - group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); - - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + ImUtf8.TextFrameAligned("Default Attributes"u8); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + ImUtf8.TextFrameAligned(option.Name); } + ImUtf8.SameLineInner(); + using (ImUtf8.Group()) + { + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + } + private void DrawOptions() + { foreach (var (option, optionIdx) in group.OptionData.WithIndex()) { using var id = ImRaii.PushId(optionIdx); @@ -83,56 +97,51 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImUtf8.SameLineInner(); editor.DrawOptionDescription(option); - ImUtf8.SameLineInner(); - editor.DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + editor.OptionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); - } - - DrawNewOption(attributeCache); - return; - - static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) - { - for (var i = 0; i < ImcEntry.NumAttributes; ++i) + if (!option.IsDisableSubMod) { - using var id = ImRaii.PushId(i); - var value = (mask & 1 << i) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) - { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) - { - if (data is ImcModGroup g) - editor.ChangeDefaultAttribute(g, cache, i, value); - else - editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); - } - } - - ImUtf8.HoverTooltip($"{(char)('A' + i)}"); - if (i != 9) - ImUtf8.SameLineInner(); + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); } } } private void DrawNewOption(in ImcAttributeCache cache) { - if (cache.LowestUnsetMask == 0) - return; - - var name = editor.DrawNewOptionBase(group, group.Options.Count); + var dis = cache.LowestUnsetMask == 0; + var name = editor.DrawNewOptionBase(group, group.Options.Count); var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + var tt = dis + ? "No Free Attribute Slots for New Options..."u8 + : validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, !validName || dis)) { editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); editor.NewOptionName = null; } } + + private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & (1 << i)) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip("ABCDEFGHIJ"u8.Slice(i, 1)); + if (i != 9) + ImUtf8.SameLineInner(); + } + } } diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e7d70922..e8a27a74 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -7,6 +7,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -22,11 +23,16 @@ public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService + DescriptionEditPopup descriptionPopup, + ImcChecker imcChecker) : IUiService { - private static ReadOnlySpan DragDropLabel - => "##DragOption"u8; + private static ReadOnlySpan AcrossGroupsLabel + => "##DragOptionAcross"u8; + private static ReadOnlySpan InsideGroupLabel + => "##DragOptionInside"u8; + + internal readonly ImcChecker ImcChecker = imcChecker; internal readonly ModManager ModManager = modManager; internal readonly Queue ActionQueue = new(); @@ -50,6 +56,7 @@ public sealed class ModGroupEditDrawer( private IModGroup? _dragDropGroup; private IModOption? _dragDropOption; + private bool _draggingAcross; public void Draw(Mod mod) { @@ -292,32 +299,30 @@ public sealed class ModGroupEditDrawer( [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Source(IModOption option) { - if (option.Group is not ITexToolsGroup) - return; - using var source = ImUtf8.DragDropSource(); if (!source) return; - if (!DragDropSource.SetPayload(DragDropLabel)) + var across = option.Group is ITexToolsGroup; + + if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel)) { _dragDropGroup = option.Group; _dragDropOption = option; + _draggingAcross = across; } - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}..."); } private void Target(IModGroup group, int optionIdx) { - if (group is not ITexToolsGroup) - return; - - if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + if (_dragDropGroup != group + && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) return; using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + if (!target.Success || !DragDropTarget.CheckPayload(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) return; if (_dragDropGroup != null && _dragDropOption != null) @@ -342,6 +347,7 @@ public sealed class ModGroupEditDrawer( _dragDropGroup = null; _dragDropOption = null; + _draggingAcross = false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index 5873119e..c14652ac 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -1,8 +1,6 @@ using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using OtterGui.Text.HelperObjects; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -12,79 +10,78 @@ namespace Penumbra.UI.ModsTab; public static class ImcManipulationDrawer { - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); ImUtf8.HoverTooltip("Object Type"u8); if (ret) { var equipSlot = type switch { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, _ => EquipSlot.Unknown, }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); + identifier = identifier with + { + EquipSlot = equipSlot, + SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + }; } return ret; } - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + identifier.PrimaryId.Id <= 1); ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + "This should generally not be left <= 1 unless you explicitly want that."u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); + identifier = identifier with { PrimaryId = newId }; return ret; } - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); ImUtf8.HoverTooltip("Secondary ID"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); + identifier = identifier with { SecondaryId = newId }; return ret; } - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); ImUtf8.HoverTooltip("Variant ID"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); + identifier = identifier with { Variant = (byte)newId }; return ret; } - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) { bool ret; EquipSlot slot; - switch (manip.ObjectType) + switch (identifier.ObjectType) { case ObjectType.Equipment: case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); break; case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); break; default: return false; } ImUtf8.HoverTooltip("Equip Slot"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); + identifier = identifier with { EquipSlot = slot }; return ret; }