Introduce Identifiers and strong entry types for each meta manipulation and use them in the manipulations.

This commit is contained in:
Ottermandias 2024-06-06 17:26:25 +02:00
parent ceed8531af
commit 2e9f184454
27 changed files with 533 additions and 163 deletions

View file

@ -48,33 +48,33 @@ public struct EstCache : IDisposable
} }
} }
public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type) public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type)
{ {
var (file, idx) = type switch var (file, idx) = type switch
{ {
EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst), EstType.Face => (_estFaceFile, MetaIndex.FaceEst),
EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst), EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst), EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst), EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
_ => (null, 0), _ => (null, 0),
}; };
return manager.TemporarilySetFile(file, idx); return manager.TemporarilySetFile(file, idx);
} }
private readonly EstFile? GetEstFile(EstManipulation.EstType type) private readonly EstFile? GetEstFile(EstType type)
{ {
return type switch return type switch
{ {
EstManipulation.EstType.Face => _estFaceFile, EstType.Face => _estFaceFile,
EstManipulation.EstType.Hair => _estHairFile, EstType.Hair => _estHairFile,
EstManipulation.EstType.Body => _estBodyFile, EstType.Body => _estBodyFile,
EstManipulation.EstType.Head => _estHeadFile, EstType.Head => _estHeadFile,
_ => null, _ => null,
}; };
} }
internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId) internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId)
{ {
var file = GetEstFile(type); var file = GetEstFile(type);
return file != null return file != null
@ -96,10 +96,10 @@ public struct EstCache : IDisposable
_estManipulations.AddOrReplace(m); _estManipulations.AddOrReplace(m);
var file = m.Slot switch var file = m.Slot switch
{ {
EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair), EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair),
EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face), EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face),
EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body), EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body),
EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head), EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head),
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
return m.Apply(file); return m.Apply(file);
@ -114,10 +114,10 @@ public struct EstCache : IDisposable
var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def); var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
var file = m.Slot switch var file = m.Slot switch
{ {
EstManipulation.EstType.Hair => _estHairFile!, EstType.Hair => _estHairFile!,
EstManipulation.EstType.Face => _estFaceFile!, EstType.Face => _estFaceFile!,
EstManipulation.EstType.Body => _estBodyFile!, EstType.Body => _estBodyFile!,
EstManipulation.EstType.Head => _estHeadFile!, EstType.Head => _estHeadFile!,
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
return manip.Apply(file); return manip.Apply(file);

View file

@ -188,7 +188,7 @@ public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation,
public MetaList.MetaReverter TemporarilySetCmpFile() public MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager); => _cmpCache.TemporarilySetFiles(_manager);
public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) public MetaList.MetaReverter TemporarilySetEstFile(EstType type)
=> _estCache.TemporarilySetFiles(_manager, type); => _estCache.TemporarilySetFiles(_manager, type);
public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor)
@ -208,7 +208,7 @@ public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation,
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId); return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId);
} }
internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId) internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId)
=> _estCache.GetEstEntry(_manager, type, genderRace, primaryId); => _estCache.GetEstEntry(_manager, type, genderRace, primaryId);
/// <summary> Use this when CharacterUtility becomes ready. </summary> /// <summary> Use this when CharacterUtility becomes ready. </summary>

View file

@ -112,7 +112,7 @@ public partial class ModCollection
=> _cache?.Meta.TemporarilySetCmpFile() => _cache?.Meta.TemporarilySetCmpFile()
?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp);
public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type)
=> _cache?.Meta.TemporarilySetEstFile(type) => _cache?.Meta.TemporarilySetEstFile(type)
?? utility.TemporarilyResetResource((MetaIndex)type); ?? utility.TemporarilyResetResource((MetaIndex)type);

View file

@ -63,16 +63,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
return info.ObjectType switch return info.ObjectType switch
{ {
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)],
ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)],
ObjectType.Equipment => [baseSkeleton], ObjectType.Equipment => [baseSkeleton],
ObjectType.Accessory => [baseSkeleton], ObjectType.Accessory => [baseSkeleton],
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton],
ObjectType.Character when info.BodySlot is BodySlot.Hair ObjectType.Character when info.BodySlot is BodySlot.Hair
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)],
ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear
=> [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)],
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)],
ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)],
@ -81,7 +81,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
}; };
} }
private string[] ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations)
{ {
// Try to find an EST entry from the manipulations provided. // Try to find an EST entry from the manipulations provided.
var (gender, race) = info.GenderRace.Split(); var (gender, race) = info.GenderRace.Split();
@ -96,13 +96,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
// Try to use an entry from provided manipulations, falling back to the current collection. // Try to use an entry from provided manipulations, falling back to the current collection.
var targetId = modEst?.Entry var targetId = modEst?.Entry
?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId)
?? 0; ?? EstEntry.Zero;
// If there's no entries, we can assume that there's no additional skeleton. // If there's no entries, we can assume that there's no additional skeleton.
if (targetId == 0) if (targetId == EstEntry.Zero)
return []; return [];
return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)];
} }
/// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary> /// <summary> Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. </summary>

View file

@ -75,14 +75,14 @@ public partial class TexToolsMeta
{ {
var gr = (GenderRace)reader.ReadUInt16(); var gr = (GenderRace)reader.ReadUInt16();
var id = reader.ReadUInt16(); var id = reader.ReadUInt16();
var value = reader.ReadUInt16(); var value = new EstEntry(reader.ReadUInt16());
var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch
{ {
(BodySlot.Face, _) => EstManipulation.EstType.Face, (BodySlot.Face, _) => EstType.Face,
(BodySlot.Hair, _) => EstManipulation.EstType.Hair, (BodySlot.Hair, _) => EstType.Hair,
(_, EquipSlot.Head) => EstManipulation.EstType.Head, (_, EquipSlot.Head) => EstType.Head,
(_, EquipSlot.Body) => EstManipulation.EstType.Body, (_, EquipSlot.Body) => EstType.Body,
_ => (EstManipulation.EstType)0, _ => (EstType)0,
}; };
if (!gr.IsValid() || type == 0) if (!gr.IsValid() || type == 0)
continue; continue;

View file

@ -66,7 +66,7 @@ public partial class TexToolsMeta
foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute); var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute);
b.Write(value); b.Write(value.Value);
} }
} }
@ -176,7 +176,7 @@ public partial class TexToolsMeta
{ {
b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race)); b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race));
b.Write(manip.Est.SetId.Id); b.Write(manip.Est.SetId.Id);
b.Write(manip.Est.Entry); b.Write(manip.Est.Entry.Value);
} }
break; break;
@ -239,10 +239,10 @@ public partial class TexToolsMeta
var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode(); var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode();
return manip.Slot switch return manip.Slot switch
{ {
EstManipulation.EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta",
EstManipulation.EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta",
EstManipulation.EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta",
EstManipulation.EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta",
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
} }

View file

@ -46,8 +46,8 @@ public partial class TexToolsMeta
void Add(RspAttribute attribute, float value) void Add(RspAttribute attribute, float value)
{ {
var def = CmpFile.GetDefault(manager, subRace, attribute); var def = CmpFile.GetDefault(manager, subRace, attribute);
if (keepDefault || value != def) if (keepDefault || value != def.Value)
ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, value)); ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value)));
} }
if (gender == 1) if (gender == 1)

View file

@ -212,10 +212,10 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
if (_parent.InInternalResolve) if (_parent.InInternalResolve)
return DisposableContainer.Empty; return DisposableContainer.Empty;
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face), return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Face),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body), data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Body),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair), data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Hair),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Head)); data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Head));
} }

View file

@ -250,30 +250,30 @@ internal partial record ResolveContext
_ => 0, _ => 0,
}; };
} }
return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Face, faceId); return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId);
case 2: case 2:
return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Hair, human->HairId); return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId);
case 3: case 3:
return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstManipulation.EstType.Head); return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head);
case 4: case 4:
return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstManipulation.EstType.Body); return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body);
default: default:
return (0, string.Empty, 0); return (0, string.Empty, 0);
} }
} }
private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstType type)
{ {
var human = (Human*)CharacterBase; var human = (Human*)CharacterBase;
var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()];
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set);
} }
private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, PrimaryId primary) private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary)
{ {
var metaCache = Global.Collection.MetaCache; var metaCache = Global.Collection.MetaCache;
var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default;
return (raceCode, EstManipulation.ToName(type), skeletonSet); return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId);
} }
private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex)

View file

@ -2,6 +2,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Functions; using Penumbra.String.Functions;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
@ -17,10 +18,10 @@ public sealed unsafe class CmpFile : MetaBaseFile
private const int RacialScalingStart = 0x2A800; private const int RacialScalingStart = 0x2A800;
public float this[SubRace subRace, RspAttribute attribute] public RspEntry this[SubRace subRace, RspAttribute attribute]
{ {
get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); get => *(RspEntry*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4);
set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; set => *(RspEntry*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value;
} }
public override void Reset() public override void Reset()
@ -39,10 +40,10 @@ public sealed unsafe class CmpFile : MetaBaseFile
Reset(); Reset();
} }
public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) public static RspEntry GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute)
{ {
var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address;
return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); return *(RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4);
} }
private static int ToRspIndex(SubRace subRace) private static int ToRspIndex(SubRace subRace)

View file

@ -34,26 +34,26 @@ public sealed unsafe class EstFile : MetaBaseFile
Removed, Removed,
} }
public ushort this[GenderRace genderRace, ushort setId] public EstEntry this[GenderRace genderRace, PrimaryId setId]
{ {
get get
{ {
var (idx, exists) = FindEntry(genderRace, setId); var (idx, exists) = FindEntry(genderRace, setId);
if (!exists) if (!exists)
return 0; return EstEntry.Zero;
return *(ushort*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx); return *(EstEntry*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx);
} }
set => SetEntry(genderRace, setId, value); set => SetEntry(genderRace, setId, value);
} }
private void InsertEntry(int idx, GenderRace genderRace, ushort setId, ushort skeletonId) private void InsertEntry(int idx, GenderRace genderRace, PrimaryId setId, EstEntry skeletonId)
{ {
if (Length < Size + EntryDescSize + EntrySize) if (Length < Size + EntryDescSize + EntrySize)
ResizeResources(Length + IncreaseSize); ResizeResources(Length + IncreaseSize);
var control = (Info*)(Data + 4); var control = (Info*)(Data + 4);
var entries = (ushort*)(control + Count); var entries = (EstEntry*)(control + Count);
for (var i = Count - 1; i >= idx; --i) for (var i = Count - 1; i >= idx; --i)
entries[i + 3] = entries[i]; entries[i + 3] = entries[i];
@ -94,10 +94,10 @@ public sealed unsafe class EstFile : MetaBaseFile
[StructLayout(LayoutKind.Sequential, Size = 4)] [StructLayout(LayoutKind.Sequential, Size = 4)]
private struct Info : IComparable<Info> private struct Info : IComparable<Info>
{ {
public readonly ushort SetId; public readonly PrimaryId SetId;
public readonly GenderRace GenderRace; public readonly GenderRace GenderRace;
public Info(GenderRace gr, ushort setId) public Info(GenderRace gr, PrimaryId setId)
{ {
GenderRace = gr; GenderRace = gr;
SetId = setId; SetId = setId;
@ -106,42 +106,42 @@ public sealed unsafe class EstFile : MetaBaseFile
public int CompareTo(Info other) public int CompareTo(Info other)
{ {
var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); var genderRaceComparison = GenderRace.CompareTo(other.GenderRace);
return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo(other.SetId); return genderRaceComparison != 0 ? genderRaceComparison : SetId.Id.CompareTo(other.SetId.Id);
} }
} }
private static (int, bool) FindEntry(ReadOnlySpan<Info> data, GenderRace genderRace, ushort setId) private static (int, bool) FindEntry(ReadOnlySpan<Info> data, GenderRace genderRace, PrimaryId setId)
{ {
var idx = data.BinarySearch(new Info(genderRace, setId)); var idx = data.BinarySearch(new Info(genderRace, setId));
return idx < 0 ? (~idx, false) : (idx, true); return idx < 0 ? (~idx, false) : (idx, true);
} }
private (int, bool) FindEntry(GenderRace genderRace, ushort setId) private (int, bool) FindEntry(GenderRace genderRace, PrimaryId setId)
{ {
var span = new ReadOnlySpan<Info>(Data + 4, Count); var span = new ReadOnlySpan<Info>(Data + 4, Count);
return FindEntry(span, genderRace, setId); return FindEntry(span, genderRace, setId);
} }
public EstEntryChange SetEntry(GenderRace genderRace, ushort setId, ushort skeletonId) public EstEntryChange SetEntry(GenderRace genderRace, PrimaryId setId, EstEntry skeletonId)
{ {
var (idx, exists) = FindEntry(genderRace, setId); var (idx, exists) = FindEntry(genderRace, setId);
if (exists) if (exists)
{ {
var value = *(ushort*)(Data + 4 * (Count + 1) + 2 * idx); var value = *(EstEntry*)(Data + 4 * (Count + 1) + 2 * idx);
if (value == skeletonId) if (value == skeletonId)
return EstEntryChange.Unchanged; return EstEntryChange.Unchanged;
if (skeletonId == 0) if (skeletonId == EstEntry.Zero)
{ {
RemoveEntry(idx); RemoveEntry(idx);
return EstEntryChange.Removed; return EstEntryChange.Removed;
} }
*(ushort*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId; *(EstEntry*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId;
return EstEntryChange.Changed; return EstEntryChange.Changed;
} }
if (skeletonId == 0) if (skeletonId == EstEntry.Zero)
return EstEntryChange.Unchanged; return EstEntryChange.Unchanged;
InsertEntry(idx, genderRace, setId, skeletonId); InsertEntry(idx, genderRace, setId, skeletonId);
@ -156,7 +156,7 @@ public sealed unsafe class EstFile : MetaBaseFile
MemoryUtility.MemSet(Data + length, 0, Length - length); MemoryUtility.MemSet(Data + length, 0, Length - length);
} }
public EstFile(MetaFileManager manager, EstManipulation.EstType estType) public EstFile(MetaFileManager manager, EstType estType)
: base(manager, (MetaIndex)estType) : base(manager, (MetaIndex)estType)
{ {
var length = DefaultData.Length; var length = DefaultData.Length;
@ -164,24 +164,24 @@ public sealed unsafe class EstFile : MetaBaseFile
Reset(); Reset();
} }
public ushort GetDefault(GenderRace genderRace, ushort setId) public EstEntry GetDefault(GenderRace genderRace, PrimaryId setId)
=> GetDefault(Manager, Index, genderRace, setId); => GetDefault(Manager, Index, genderRace, setId);
public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId) public static EstEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId)
{ {
var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address; var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address;
var count = *(int*)data; var count = *(int*)data;
var span = new ReadOnlySpan<Info>(data + 4, count); var span = new ReadOnlySpan<Info>(data + 4, count);
var (idx, found) = FindEntry(span, genderRace, primaryId.Id); var (idx, found) = FindEntry(span, genderRace, primaryId.Id);
if (!found) if (!found)
return 0; return EstEntry.Zero;
return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize); return *(EstEntry*)(data + 4 + count * EntryDescSize + idx * EntrySize);
} }
public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId) public static EstEntry GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId)
=> GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, primaryId); => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, primaryId);
public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, PrimaryId primaryId) public static EstEntry GetDefault(MetaFileManager manager, EstType estType, GenderRace genderRace, PrimaryId primaryId)
=> GetDefault(manager, (MetaIndex)estType, genderRace, primaryId); => GetDefault(manager, (MetaIndex)estType, genderRace, primaryId);
} }

View file

@ -0,0 +1,87 @@
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, GenderRace GenderRace)
: IMetaIdentifier, IComparable<EqdpIdentifier>
{
public ModelRace Race
=> GenderRace.Split().Item2;
public Gender Gender
=> GenderRace.Split().Item1;
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
=> identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot));
public MetaIndex FileIndex()
=> CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory());
public override string ToString()
=> $"Eqdp - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}";
public bool Validate()
{
var mask = Eqdp.Mask(Slot);
if (mask == 0)
return false;
if (FileIndex() == (MetaIndex)(-1))
return false;
// No check for set id.
return true;
}
public int CompareTo(EqdpIdentifier other)
{
var gr = GenderRace.CompareTo(other.GenderRace);
if (gr != 0)
return gr;
var set = SetId.Id.CompareTo(other.SetId.Id);
if (set != 0)
return set;
return Slot.CompareTo(other.Slot);
}
public static EqdpIdentifier? FromJson(JObject jObj)
{
var gender = jObj["Gender"]?.ToObject<Gender>() ?? Gender.Unknown;
var race = jObj["Race"]?.ToObject<ModelRace>() ?? ModelRace.Unknown;
var setId = new PrimaryId(jObj["SetId"]?.ToObject<ushort>() ?? 0);
var slot = jObj["Slot"]?.ToObject<EquipSlot>() ?? EquipSlot.Unknown;
var ret = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race));
return ret.Validate() ? ret : null;
}
public JObject AddToJson(JObject jObj)
{
var (gender, race) = GenderRace.Split();
jObj["Gender"] = gender.ToString();
jObj["Race"] = race.ToString();
jObj["SetId"] = SetId.Id.ToString();
jObj["Slot"] = Slot.ToString();
return jObj;
}
}
public readonly record struct InternalEqdpEntry(bool Model, bool Material)
{
private InternalEqdpEntry((bool, bool) val)
: this(val.Item1, val.Item2)
{ }
public InternalEqdpEntry(EqdpEntry entry, EquipSlot slot)
: this(entry.ToBits(slot))
{ }
public EqdpEntry ToEntry(EquipSlot slot)
=> Eqdp.FromSlotAndBits(slot, Model, Material);
}

View file

@ -10,27 +10,30 @@ namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EqdpManipulation : IMetaManipulation<EqdpManipulation> public readonly struct EqdpManipulation : IMetaManipulation<EqdpManipulation>
{ {
public EqdpEntry Entry { get; private init; } [JsonIgnore]
public EqdpIdentifier Identifier { get; private init; }
public EqdpEntry Entry { get; private init; }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public Gender Gender { get; private init; } public Gender Gender
=> Identifier.Gender;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public ModelRace Race { get; private init; } public ModelRace Race
=> Identifier.Race;
public PrimaryId SetId { get; private init; } public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public EquipSlot Slot { get; private init; } public EquipSlot Slot
=> Identifier.Slot;
[JsonConstructor] [JsonConstructor]
public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId)
{ {
Gender = gender; Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race));
Race = race; Entry = Eqdp.Mask(Slot) & entry;
SetId = setId;
Slot = slot;
Entry = Eqdp.Mask(Slot) & entry;
} }
public EqdpManipulation Copy(EqdpManipulation entry) public EqdpManipulation Copy(EqdpManipulation entry)

View file

@ -0,0 +1,72 @@
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable<EqpIdentifier>
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
=> identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot));
public MetaIndex FileIndex()
=> MetaIndex.Eqp;
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
public bool Validate()
{
var mask = Eqp.Mask(Slot);
if (mask == 0)
return false;
// No check for set id.
return true;
}
public int CompareTo(EqpIdentifier other)
{
var set = SetId.Id.CompareTo(other.SetId.Id);
if (set != 0)
return set;
return Slot.CompareTo(other.Slot);
}
public static EqpIdentifier? FromJson(JObject jObj)
{
var setId = new PrimaryId(jObj["SetId"]?.ToObject<ushort>() ?? 0);
var slot = jObj["Slot"]?.ToObject<EquipSlot>() ?? EquipSlot.Unknown;
var ret = new EqpIdentifier(setId, slot);
return ret.Validate() ? ret : null;
}
public JObject AddToJson(JObject jObj)
{
jObj["SetId"] = SetId.Id.ToString();
jObj["Slot"] = Slot.ToString();
return jObj;
}
}
public readonly record struct EqpEntryInternal(uint Value)
{
public EqpEntryInternal(EqpEntry entry, EquipSlot slot)
: this(GetValue(entry, slot))
{ }
public EqpEntry ToEntry(EquipSlot slot)
{
var (offset, mask) = Eqp.OffsetAndMask(slot);
return (EqpEntry)((ulong)Value << offset) & mask;
}
private static uint GetValue(EqpEntry entry, EquipSlot slot)
{
var (offset, mask) = Eqp.OffsetAndMask(slot);
return (uint)((ulong)(entry & mask) >> offset);
}
}

View file

@ -5,7 +5,6 @@ using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Util; using Penumbra.Util;
using SharpCompress.Common;
namespace Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Manipulations;
@ -15,17 +14,20 @@ public readonly struct EqpManipulation : IMetaManipulation<EqpManipulation>
[JsonConverter(typeof(ForceNumericFlagEnumConverter))] [JsonConverter(typeof(ForceNumericFlagEnumConverter))]
public EqpEntry Entry { get; private init; } public EqpEntry Entry { get; private init; }
public PrimaryId SetId { get; private init; } public EqpIdentifier Identifier { get; private init; }
public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public EquipSlot Slot { get; private init; } public EquipSlot Slot
=> Identifier.Slot;
[JsonConstructor] [JsonConstructor]
public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId)
{ {
Slot = slot; Identifier = new EqpIdentifier(setId, slot);
SetId = setId; Entry = Eqp.Mask(slot) & entry;
Entry = Eqp.Mask(slot) & entry;
} }
public EqpManipulation Copy(EqpEntry entry) public EqpManipulation Copy(EqpEntry entry)

View file

@ -0,0 +1,113 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public enum EstType : byte
{
Hair = MetaIndex.HairEst,
Face = MetaIndex.FaceEst,
Body = MetaIndex.BodyEst,
Head = MetaIndex.HeadEst,
}
public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, GenderRace GenderRace)
: IMetaIdentifier, IComparable<EstIdentifier>
{
public ModelRace Race
=> GenderRace.Split().Item2;
public Gender Gender
=> GenderRace.Split().Item1;
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
{
switch (Slot)
{
case EstType.Hair:
changedItems.TryAdd(
$"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair (Hair) {SetId}", null);
break;
case EstType.Face:
changedItems.TryAdd(
$"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face (Face) {SetId}", null);
break;
case EstType.Body:
identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Body));
break;
case EstType.Head:
identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Head));
break;
}
}
public MetaIndex FileIndex()
=> (MetaIndex)Slot;
public override string ToString()
=> $"Est - {SetId} - {Slot} - {GenderRace.ToName()}";
public bool Validate()
{
if (!Enum.IsDefined(Slot))
return false;
if (GenderRace is GenderRace.Unknown || !Enum.IsDefined(GenderRace))
return false;
// No known check for set id.
return true;
}
public int CompareTo(EstIdentifier other)
{
var gr = GenderRace.CompareTo(other.GenderRace);
if (gr != 0)
return gr;
var id = SetId.Id.CompareTo(other.SetId.Id);
return id != 0 ? id : Slot.CompareTo(other.Slot);
}
public static EstIdentifier? FromJson(JObject jObj)
{
var gender = jObj["Gender"]?.ToObject<Gender>() ?? Gender.Unknown;
var race = jObj["Race"]?.ToObject<ModelRace>() ?? ModelRace.Unknown;
var setId = new PrimaryId(jObj["SetId"]?.ToObject<ushort>() ?? 0);
var slot = jObj["Slot"]?.ToObject<EstType>() ?? 0;
var ret = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race));
return ret.Validate() ? ret : null;
}
public JObject AddToJson(JObject jObj)
{
var (gender, race) = GenderRace.Split();
jObj["Gender"] = gender.ToString();
jObj["Race"] = race.ToString();
jObj["SetId"] = SetId.Id.ToString();
jObj["Slot"] = Slot.ToString();
return jObj;
}
}
[JsonConverter(typeof(Converter))]
public readonly record struct EstEntry(ushort Value)
{
public static readonly EstEntry Zero = new(0);
public PrimaryId AsId
=> new(Value);
private class Converter : JsonConverter<EstEntry>
{
public override void WriteJson(JsonWriter writer, EstEntry value, JsonSerializer serializer)
=> serializer.Serialize(writer, value.Value);
public override EstEntry ReadJson(JsonReader reader, Type objectType, EstEntry existingValue, bool hasExistingValue,
JsonSerializer serializer)
=> new(serializer.Deserialize<ushort>(reader));
}
}

View file

@ -10,14 +10,6 @@ namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct EstManipulation : IMetaManipulation<EstManipulation> public readonly struct EstManipulation : IMetaManipulation<EstManipulation>
{ {
public enum EstType : byte
{
Hair = MetaIndex.HairEst,
Face = MetaIndex.FaceEst,
Body = MetaIndex.BodyEst,
Head = MetaIndex.HeadEst,
}
public static string ToName(EstType type) public static string ToName(EstType type)
=> type switch => type switch
{ {
@ -28,31 +20,33 @@ public readonly struct EstManipulation : IMetaManipulation<EstManipulation>
_ => "unk", _ => "unk",
}; };
public ushort Entry { get; private init; } // SkeletonIdx. public EstIdentifier Identifier { get; private init; }
public EstEntry Entry { get; private init; }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public Gender Gender { get; private init; } public Gender Gender
=> Identifier.Gender;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public ModelRace Race { get; private init; } public ModelRace Race
=> Identifier.Race;
public PrimaryId SetId { get; private init; } public PrimaryId SetId
=> Identifier.SetId;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public EstType Slot { get; private init; } public EstType Slot
=> Identifier.Slot;
[JsonConstructor] [JsonConstructor]
public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, ushort entry) public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry)
{ {
Entry = entry; Entry = entry;
Gender = gender; Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race));
Race = race;
SetId = setId;
Slot = slot;
} }
public EstManipulation Copy(ushort entry) public EstManipulation Copy(EstEntry entry)
=> new(Gender, Race, Slot, SetId, entry); => new(Gender, Race, Slot, SetId, entry);
@ -111,3 +105,5 @@ public readonly struct EstManipulation : IMetaManipulation<EstManipulation>
return true; return true;
} }
} }

View file

@ -0,0 +1,39 @@
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable<GmpIdentifier>
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
=> identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head));
public MetaIndex FileIndex()
=> MetaIndex.Gmp;
public override string ToString()
=> $"Gmp - {SetId}";
public bool Validate()
// No known conditions.
=> true;
public int CompareTo(GmpIdentifier other)
=> SetId.Id.CompareTo(other.SetId.Id);
public static GmpIdentifier? FromJson(JObject jObj)
{
var setId = new PrimaryId(jObj["SetId"]?.ToObject<ushort>() ?? 0);
var ret = new GmpIdentifier(setId);
return ret.Validate() ? ret : null;
}
public JObject AddToJson(JObject jObj)
{
jObj["SetId"] = SetId.Id.ToString();
return jObj;
}
}

View file

@ -8,14 +8,18 @@ namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct GmpManipulation : IMetaManipulation<GmpManipulation> public readonly struct GmpManipulation : IMetaManipulation<GmpManipulation>
{ {
public GmpEntry Entry { get; private init; } public GmpIdentifier Identifier { get; private init; }
public PrimaryId SetId { get; private init; }
public GmpEntry Entry { get; private init; }
public PrimaryId SetId
=> Identifier.SetId;
[JsonConstructor] [JsonConstructor]
public GmpManipulation(GmpEntry entry, PrimaryId setId) public GmpManipulation(GmpEntry entry, PrimaryId setId)
{ {
Entry = entry; Entry = entry;
SetId = setId; Identifier = new GmpIdentifier(setId);
} }
public GmpManipulation Copy(GmpEntry entry) public GmpManipulation Copy(GmpEntry entry)

View file

@ -0,0 +1,52 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Manipulations;
public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
=> changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null);
public MetaIndex FileIndex()
=> throw new NotImplementedException();
public bool Validate()
=> throw new NotImplementedException();
public JObject AddToJson(JObject jObj)
=> throw new NotImplementedException();
}
[JsonConverter(typeof(Converter))]
public readonly record struct RspEntry(float Value) : IComparisonOperators<RspEntry, RspEntry, bool>
{
public const float MinValue = 0.01f;
public const float MaxValue = 512f;
public static readonly RspEntry One = new(1f);
private class Converter : JsonConverter<RspEntry>
{
public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer)
=> serializer.Serialize(writer, value.Value);
public override RspEntry ReadJson(JsonReader reader, Type objectType, RspEntry existingValue, bool hasExistingValue,
JsonSerializer serializer)
=> new(serializer.Deserialize<float>(reader));
}
public static bool operator >(RspEntry left, RspEntry right)
=> left.Value > right.Value;
public static bool operator >=(RspEntry left, RspEntry right)
=> left.Value >= right.Value;
public static bool operator <(RspEntry left, RspEntry right)
=> left.Value < right.Value;
public static bool operator <=(RspEntry left, RspEntry right)
=> left.Value <= right.Value;
}

View file

@ -9,25 +9,25 @@ namespace Penumbra.Meta.Manipulations;
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct RspManipulation : IMetaManipulation<RspManipulation> public readonly struct RspManipulation : IMetaManipulation<RspManipulation>
{ {
public const float MinValue = 0.01f; public RspIdentifier Identifier { get; private init; }
public const float MaxValue = 512f; public RspEntry Entry { get; private init; }
public float Entry { get; private init; }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public SubRace SubRace { get; private init; } public SubRace SubRace
=> Identifier.SubRace;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public RspAttribute Attribute { get; private init; } public RspAttribute Attribute
=> Identifier.Attribute;
[JsonConstructor] [JsonConstructor]
public RspManipulation(SubRace subRace, RspAttribute attribute, float entry) public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry)
{ {
Entry = entry; Entry = entry;
SubRace = subRace; Identifier = new RspIdentifier(subRace, attribute);
Attribute = attribute;
} }
public RspManipulation Copy(float entry) public RspManipulation Copy(RspEntry entry)
=> new(SubRace, Attribute, entry); => new(SubRace, Attribute, entry);
public override string ToString() public override string ToString()
@ -68,7 +68,7 @@ public readonly struct RspManipulation : IMetaManipulation<RspManipulation>
return false; return false;
if (!Enum.IsDefined(Attribute)) if (!Enum.IsDefined(Attribute))
return false; return false;
if (Entry is < MinValue or > MaxValue) if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue)
return false; return false;
return true; return true;

View file

@ -126,9 +126,9 @@ public static class EquipmentSwap
var isAccessory = slot.IsAccessory(); var isAccessory = slot.IsAccessory();
var estType = slot switch var estType = slot switch
{ {
EquipSlot.Head => EstManipulation.EstType.Head, EquipSlot.Head => EstType.Head,
EquipSlot.Body => EstManipulation.EstType.Body, EquipSlot.Body => EstType.Body,
_ => (EstManipulation.EstType)0, _ => (EstType)0,
}; };
var skipFemale = false; var skipFemale = false;

View file

@ -133,23 +133,23 @@ public static class ItemSwap
} }
public static FileSwap CreatePhyb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstManipulation.EstType type, public static FileSwap CreatePhyb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type,
GenderRace race, ushort estEntry) GenderRace race, EstEntry estEntry)
{ {
var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry); var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId);
return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath);
} }
public static FileSwap CreateSklb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstManipulation.EstType type, public static FileSwap CreateSklb(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EstType type,
GenderRace race, ushort estEntry) GenderRace race, EstEntry estEntry)
{ {
var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry); var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId);
return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath);
} }
/// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks> /// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks>
public static MetaSwap? CreateEst(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, public static MetaSwap? CreateEst(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
Func<MetaManipulation, MetaManipulation> manips, EstManipulation.EstType type, Func<MetaManipulation, MetaManipulation> manips, EstType type,
GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl)
{ {
if (type == 0) if (type == 0)
@ -160,7 +160,7 @@ public static class ItemSwap
var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo));
var est = new MetaSwap(manips, fromDefault, toDefault); var est = new MetaSwap(manips, fromDefault, toDefault);
if (ownMdl && est.SwapApplied.Est.Entry >= 2) if (ownMdl && est.SwapApplied.Est.Entry.Value >= 2)
{ {
var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry);
est.ChildSwaps.Add(phyb); est.ChildSwaps.Add(phyb);

View file

@ -152,9 +152,9 @@ public class ItemSwapContainer
var mdl = CustomizationSwap.CreateMdl(manager, pathResolver, slot, race, from, to); var mdl = CustomizationSwap.CreateMdl(manager, pathResolver, slot, race, from, to);
var type = slot switch var type = slot switch
{ {
BodySlot.Hair => EstManipulation.EstType.Hair, BodySlot.Hair => EstType.Hair,
BodySlot.Face => EstManipulation.EstType.Face, BodySlot.Face => EstType.Face,
_ => (EstManipulation.EstType)0, _ => (EstType)0,
}; };
var metaResolver = MetaResolver(collection); var metaResolver = MetaResolver(collection);

View file

@ -453,7 +453,7 @@ public partial class ModEditWindow
private static class EstRow private static class EstRow
{ {
private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero);
private static float IdWidth private static float IdWidth
=> 100 * UiHelpers.Scale; => 100 * UiHelpers.Scale;
@ -510,7 +510,7 @@ public partial class ModEditWindow
// Values // Values
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f); IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f);
} }
public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize)
@ -538,9 +538,9 @@ public partial class ModEditWindow
// Values // Values
var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value,
out var entry, 0, ushort.MaxValue, 0.05f)) out var entry, 0, ushort.MaxValue, 0.05f))
editor.MetaEditor.Change(meta.Copy((ushort)entry)); editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry)));
} }
} }
@ -646,7 +646,7 @@ public partial class ModEditWindow
private static class RspRow private static class RspRow
{ {
private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One);
private static float FloatWidth private static float FloatWidth
=> 150 * UiHelpers.Scale; => 150 * UiHelpers.Scale;
@ -680,7 +680,8 @@ public partial class ModEditWindow
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth(FloatWidth); ImGui.SetNextItemWidth(FloatWidth);
ImGui.DragFloat("##rspValue", ref defaultEntry, 0f); var value = defaultEntry.Value;
ImGui.DragFloat("##rspValue", ref value, 0f);
} }
public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize)
@ -699,15 +700,15 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
// Values // Values
var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute); var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value;
var value = meta.Entry; var value = meta.Entry.Value;
ImGui.SetNextItemWidth(FloatWidth); ImGui.SetNextItemWidth(FloatWidth);
using var color = ImRaii.PushColor(ImGuiCol.FrameBg, using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(),
def != value); def != value);
if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspManipulation.MinValue, RspManipulation.MaxValue) if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue)
&& value is >= RspManipulation.MinValue and <= RspManipulation.MaxValue) && value is >= RspEntry.MinValue and <= RspEntry.MaxValue)
editor.MetaEditor.Change(meta.Copy(value)); editor.MetaEditor.Change(meta.Copy(new RspEntry(value)));
ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}");
} }

View file

@ -33,7 +33,7 @@ public static class Combos
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute, => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute,
RspAttributeExtensions.ToFullString, 0, 1); RspAttributeExtensions.ToFullString, 0, 1);
public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute, float unscaledWidth = 200) public static bool EstSlot(string label, EstType current, out EstType attribute, float unscaledWidth = 200)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute); => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute);
public static bool ImcType(string label, ObjectType current, out ObjectType type, float unscaledWidth = 110) public static bool ImcType(string label, ObjectType current, out ObjectType type, float unscaledWidth = 110)

View file

@ -51,18 +51,18 @@ public static class IdentifierExtensions
case MetaManipulation.Type.Est: case MetaManipulation.Type.Est:
switch (manip.Est.Slot) switch (manip.Est.Slot)
{ {
case EstManipulation.EstType.Hair: case EstType.Hair:
changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null);
break; break;
case EstManipulation.EstType.Face: case EstType.Face:
changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null);
break; break;
case EstManipulation.EstType.Body: case EstType.Body:
identifier.Identify(changedItems, identifier.Identify(changedItems,
GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race),
EquipSlot.Body)); EquipSlot.Body));
break; break;
case EstManipulation.EstType.Head: case EstType.Head:
identifier.Identify(changedItems, identifier.Identify(changedItems,
GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race),
EquipSlot.Head)); EquipSlot.Head));