diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 38d080cc..09a9b7c4 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -160,7 +160,7 @@ public class TemporaryApi( /// Only returns true if all conversions are successful and distinct. /// private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out HashSet? manips) + [NotNullWhen(true)] out MetaDictionary? manips) { if (manipString.Length == 0) { @@ -174,7 +174,7 @@ public class TemporaryApi( return false; } - manips = new HashSet(manipArray!.Length); + manips = new MetaDictionary(manipArray!.Length); foreach (var manip in manipArray.Where(m => m.Validate())) { if (manips.Add(manip)) diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index aee2b447..cbb07436 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -43,7 +43,7 @@ public class TempModManager : IDisposable => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, - HashSet manips, ModPriority priority) + MetaDictionary manips, ModPriority priority) { var mod = GetOrCreateMod(tag, collection, priority, out var created); Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 2c01ce3f..8c5f27e5 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -8,11 +8,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqdpManipulation : IMetaManipulation +public readonly struct EqdpManipulation(EqdpIdentifier identifier, EqdpEntry entry) : IMetaManipulation { [JsonIgnore] - public EqdpIdentifier Identifier { get; private init; } - public EqdpEntry Entry { get; private init; } + public EqdpIdentifier Identifier { get; } = identifier; + + public EqdpEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public Gender Gender @@ -31,20 +32,18 @@ public readonly struct EqdpManipulation : IMetaManipulation [JsonConstructor] public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) - { - Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); - Entry = Eqdp.Mask(Slot) & entry; - } + : this(new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)), Eqdp.Mask(slot) & entry) + { } public EqdpManipulation Copy(EqdpManipulation entry) { if (entry.Slot != Slot) { var (bit1, bit2) = entry.Entry.ToBits(entry.Slot); - return new EqdpManipulation(Eqdp.FromSlotAndBits(Slot, bit1, bit2), Slot, Gender, Race, SetId); + return new EqdpManipulation(Identifier, Eqdp.FromSlotAndBits(Slot, bit1, bit2)); } - return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId); + return new EqdpManipulation(Identifier, entry.Entry); } public EqdpManipulation Copy(EqdpEntry entry) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 3bced096..eef21d12 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -9,12 +9,13 @@ using Penumbra.Util; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqpManipulation : IMetaManipulation +public readonly struct EqpManipulation(EqpIdentifier identifier, EqpEntry entry) : IMetaManipulation { - [JsonConverter(typeof(ForceNumericFlagEnumConverter))] - public EqpEntry Entry { get; private init; } + [JsonIgnore] + public EqpIdentifier Identifier { get; } = identifier; - public EqpIdentifier Identifier { get; private init; } + [JsonConverter(typeof(ForceNumericFlagEnumConverter))] + public EqpEntry Entry { get; } = entry; public PrimaryId SetId => Identifier.SetId; @@ -25,13 +26,11 @@ public readonly struct EqpManipulation : IMetaManipulation [JsonConstructor] public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) - { - Identifier = new EqpIdentifier(setId, slot); - Entry = Eqp.Mask(slot) & entry; - } + : this(new EqpIdentifier(setId, slot), Eqp.Mask(slot) & entry) + { } public EqpManipulation Copy(EqpEntry entry) - => new(entry, Slot, SetId); + => new(Identifier, entry); public override string ToString() => $"Eqp - {SetId} - {Slot}"; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index c3f9792f..09abbaa5 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -8,7 +8,7 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EstManipulation : IMetaManipulation +public readonly struct EstManipulation(EstIdentifier identifier, EstEntry entry) : IMetaManipulation { public static string ToName(EstType type) => type switch @@ -20,8 +20,9 @@ public readonly struct EstManipulation : IMetaManipulation _ => "unk", }; - public EstIdentifier Identifier { get; private init; } - public EstEntry Entry { get; private init; } + [JsonIgnore] + public EstIdentifier Identifier { get; } = identifier; + public EstEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public Gender Gender @@ -41,13 +42,11 @@ public readonly struct EstManipulation : IMetaManipulation [JsonConstructor] public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) - { - Entry = entry; - Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); - } + : this(new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)), entry) + { } public EstManipulation Copy(EstEntry entry) - => new(Gender, Race, Slot, SetId, entry); + => new(Identifier, entry); public override string ToString() diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index ada543dc..94c892e2 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -1,9 +1,11 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly struct GlobalEqpManipulation : IMetaManipulation +public readonly struct GlobalEqpManipulation : IMetaManipulation, IMetaIdentifier { public GlobalEqpType Type { get; init; } public PrimaryId Condition { get; init; } @@ -19,6 +21,28 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation() ?? (GlobalEqpType)100; + var condition = jObj[nameof(Condition)]?.ToObject() ?? 0; + var ret = new GlobalEqpManipulation + { + Type = type, + Condition = condition, + }; + return ret.Validate() ? ret : null; + } + public bool Equals(GlobalEqpManipulation other) => Type == other.Type @@ -45,6 +69,9 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { } + public MetaIndex FileIndex() - => (MetaIndex)(-1); + => MetaIndex.Eqp; } diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 0b2a9f4b..431f6325 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -6,24 +6,23 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct GmpManipulation : IMetaManipulation +public readonly struct GmpManipulation(GmpIdentifier identifier, GmpEntry entry) : IMetaManipulation { - public GmpIdentifier Identifier { get; private init; } + [JsonIgnore] + public GmpIdentifier Identifier { get; } = identifier; - public GmpEntry Entry { get; private init; } + public GmpEntry Entry { get; } = entry; public PrimaryId SetId => Identifier.SetId; [JsonConstructor] public GmpManipulation(GmpEntry entry, PrimaryId setId) - { - Entry = entry; - Identifier = new GmpIdentifier(setId); - } + : this(new GmpIdentifier(setId), entry) + { } public GmpManipulation Copy(GmpEntry entry) - => new(entry, SetId); + => new(Identifier, entry); public override string ToString() => $"Gmp - {SetId}"; diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs new file mode 100644 index 00000000..65252c5d --- /dev/null +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -0,0 +1,140 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; +using ImcEntry = Penumbra.GameData.Structs.ImcEntry; + +namespace Penumbra.Meta.Manipulations; + +[JsonConverter(typeof(Converter))] +public sealed class MetaDictionary : HashSet +{ + public MetaDictionary() + { } + + public MetaDictionary(int capacity) + : base(capacity) + { } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MetaDictionary? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + writer.WriteStartArray(); + foreach (var item in value) + { + writer.WriteStartObject(); + writer.WritePropertyName("Type"); + writer.WriteValue(item.ManipulationType.ToString()); + writer.WritePropertyName("Manipulation"); + serializer.Serialize(writer, item.Manipulation); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var dict = existingValue ?? []; + dict.Clear(); + var jObj = JArray.Load(reader); + foreach (var item in jObj) + { + var type = item["Type"]?.ToObject() ?? MetaManipulation.Type.Unknown; + if (type is MetaManipulation.Type.Unknown) + { + Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); + continue; + } + + if (item["Manipulation"] is not JObject manip) + { + Penumbra.Log.Warning($"Manipulation of type {type} does not contain manipulation data."); + continue; + } + + switch (type) + { + case MetaManipulation.Type.Imc: + { + var identifier = ImcIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new ImcManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); + break; + } + case MetaManipulation.Type.Eqdp: + { + var identifier = EqdpIdentifier.FromJson(manip); + var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EqdpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Eqp: + { + var identifier = EqpIdentifier.FromJson(manip); + var entry = (EqpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EqpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Est: + { + var identifier = EstIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EstManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EST Manipulation encountered."); + break; + } + case MetaManipulation.Type.Gmp: + { + var identifier = GmpIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new GmpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Rsp: + { + var identifier = RspIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new RspManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); + break; + } + case MetaManipulation.Type.GlobalEqp: + { + var identifier = GlobalEqpManipulation.FromJson(manip); + if (identifier.HasValue) + dict.Add(new MetaManipulation(identifier.Value)); + else + Penumbra.Log.Warning("Invalid Global EQP Manipulation encountered."); + break; + } + } + } + + return dict; + } + } +} diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 29cdfd71..ca7cb1c5 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,3 +1,4 @@ +using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -12,13 +13,31 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() - => throw new NotImplementedException(); + => MetaIndex.HumanCmp; public bool Validate() - => throw new NotImplementedException(); + => SubRace is not SubRace.Unknown + && Enum.IsDefined(SubRace) + && Attribute is not RspAttribute.NumAttributes + && Enum.IsDefined(Attribute); public JObject AddToJson(JObject jObj) - => throw new NotImplementedException(); + { + jObj["SubRace"] = SubRace.ToString(); + jObj["Attribute"] = Attribute.ToString(); + return jObj; + } + + public static RspIdentifier? FromJson(JObject? jObj) + { + if (jObj == null) + return null; + + var subRace = jObj["SubRace"]?.ToObject() ?? SubRace.Unknown; + var attribute = jObj["Attribute"]?.ToObject() ?? RspAttribute.NumAttributes; + var ret = new RspIdentifier(subRace, attribute); + return ret.Validate() ? ret : null; + } } [JsonConverter(typeof(Converter))] @@ -28,6 +47,9 @@ public readonly record struct RspEntry(float Value) : IComparisonOperators Value is >= MinValue and <= MaxValue; + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer) diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 04691c9f..e2282c41 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -7,10 +7,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct RspManipulation : IMetaManipulation +public readonly struct RspManipulation(RspIdentifier identifier, RspEntry entry) : IMetaManipulation { - public RspIdentifier Identifier { get; private init; } - public RspEntry Entry { get; private init; } + [JsonIgnore] + public RspIdentifier Identifier { get; } = identifier; + + public RspEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public SubRace SubRace @@ -22,13 +24,11 @@ public readonly struct RspManipulation : IMetaManipulation [JsonConstructor] public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) - { - Entry = entry; - Identifier = new RspIdentifier(subRace, attribute); - } + : this(new RspIdentifier(subRace, attribute), entry) + { } public RspManipulation Copy(RspEntry entry) - => new(SubRace, Attribute, entry); + => new(Identifier, entry); public override string ToString() => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; @@ -63,14 +63,5 @@ public readonly struct RspManipulation : IMetaManipulation } public bool Validate() - { - if (SubRace is SubRace.Unknown || !Enum.IsDefined(SubRace)) - return false; - if (!Enum.IsDefined(Attribute)) - return false; - if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue) - return false; - - return true; - } + => Identifier.Validate() && Entry.Validate(); } diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 1a234879..5a300a48 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = []; IMod IModDataContainer.Mod => Mod; diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 7f7ef4a6..1a89ec17 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -10,9 +10,9 @@ public interface IModDataContainer public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public MetaDictionary Manipulations { get; set; } public string GetName(); public string GetFullName(); diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 02d86af2..378f6dc8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -33,7 +33,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = []; public void AddDataTo(Dictionary redirections, HashSet manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6e6e72ab..e0a03c92 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -57,7 +57,7 @@ public class TemporaryMod : IMod public bool SetManipulation(MetaManipulation manip) => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); - public void SetAll(Dictionary dict, HashSet manips) + public void SetAll(Dictionary dict, MetaDictionary manips) { Default.Files = dict; Default.Manipulations = manips;