mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
ResourceTree: WIP - Path resolution
This commit is contained in:
parent
2852562a03
commit
fd163f8f66
13 changed files with 452 additions and 142 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit b141301c4ee65422d6802f3038c8f344911d4ae2
|
Subproject commit 1f274b41e3e703712deb83f3abd8727e10614ebe
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
|
|
@ -61,6 +62,26 @@ public struct EstCache : IDisposable
|
||||||
return manager.TemporarilySetFile(file, idx);
|
return manager.TemporarilySetFile(file, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly EstFile? GetEstFile(EstManipulation.EstType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
EstManipulation.EstType.Face => _estFaceFile,
|
||||||
|
EstManipulation.EstType.Hair => _estHairFile,
|
||||||
|
EstManipulation.EstType.Body => _estBodyFile,
|
||||||
|
EstManipulation.EstType.Head => _estHeadFile,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, SetId setId)
|
||||||
|
{
|
||||||
|
var file = GetEstFile(type);
|
||||||
|
return file != null
|
||||||
|
? file[genderRace, setId.Id]
|
||||||
|
: EstFile.GetDefault(manager, type, genderRace, setId);
|
||||||
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_estFaceFile?.Reset();
|
_estFaceFile?.Reset();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
|
|
@ -186,6 +187,23 @@ public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation,
|
||||||
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
|
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
|
||||||
=> _imcCache.GetImcFile(path, out file);
|
=> _imcCache.GetImcFile(path, out file);
|
||||||
|
|
||||||
|
public ImcEntry GetImcEntry(Utf8GamePath path, EquipSlot slot, Variant variantIdx, out bool exists)
|
||||||
|
=> GetImcFile(path, out var file)
|
||||||
|
? file.GetEntry(Meta.Files.ImcFile.PartIndex(slot), variantIdx, out exists)
|
||||||
|
: Meta.Files.ImcFile.GetDefault(_manager, path, slot, variantIdx, out exists);
|
||||||
|
|
||||||
|
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, SetId setId)
|
||||||
|
{
|
||||||
|
var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
|
||||||
|
if (eqdpFile != null)
|
||||||
|
return setId.Id < eqdpFile.Count ? eqdpFile[setId] : default;
|
||||||
|
else
|
||||||
|
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, setId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, SetId setId)
|
||||||
|
=> _estCache.GetEstEntry(_manager, type, genderRace, setId);
|
||||||
|
|
||||||
/// <summary> Use this when CharacterUtility becomes ready. </summary>
|
/// <summary> Use this when CharacterUtility becomes ready. </summary>
|
||||||
private void ApplyStoredManipulations()
|
private void ApplyStoredManipulations()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,15 @@ public unsafe class PathState : IDisposable
|
||||||
private readonly ResolvePathHooks _demiHuman;
|
private readonly ResolvePathHooks _demiHuman;
|
||||||
private readonly ResolvePathHooks _monster;
|
private readonly ResolvePathHooks _monster;
|
||||||
|
|
||||||
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
|
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
|
||||||
|
private readonly ThreadLocal<uint> _internalResolve = new(() => 0, false);
|
||||||
|
|
||||||
public IList<ResolveData> CurrentData
|
public IList<ResolveData> CurrentData
|
||||||
=> _resolveData.Values;
|
=> _resolveData.Values;
|
||||||
|
|
||||||
|
public bool InInternalResolve
|
||||||
|
=> _internalResolve.Value != 0u;
|
||||||
|
|
||||||
public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop)
|
public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop)
|
||||||
{
|
{
|
||||||
interop.InitializeFromAttributes(this);
|
interop.InitializeFromAttributes(this);
|
||||||
|
|
@ -55,6 +59,7 @@ public unsafe class PathState : IDisposable
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resolveData.Dispose();
|
_resolveData.Dispose();
|
||||||
|
_internalResolve.Dispose();
|
||||||
_human.Dispose();
|
_human.Dispose();
|
||||||
_weapon.Dispose();
|
_weapon.Dispose();
|
||||||
_demiHuman.Dispose();
|
_demiHuman.Dispose();
|
||||||
|
|
@ -80,7 +85,10 @@ public unsafe class PathState : IDisposable
|
||||||
if (path == nint.Zero)
|
if (path == nint.Zero)
|
||||||
return path;
|
return path;
|
||||||
|
|
||||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
if (!InInternalResolve)
|
||||||
|
{
|
||||||
|
_resolveData.Value = collection.ToResolveData(gameObject);
|
||||||
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +98,37 @@ public unsafe class PathState : IDisposable
|
||||||
if (path == nint.Zero)
|
if (path == nint.Zero)
|
||||||
return path;
|
return path;
|
||||||
|
|
||||||
_resolveData.Value = data;
|
if (!InInternalResolve)
|
||||||
|
{
|
||||||
|
_resolveData.Value = data;
|
||||||
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporarily disables metadata mod application and resolve data capture on the current thread. <para />
|
||||||
|
/// Must be called to prevent race conditions between Penumbra's internal path resolution (for example for Resource Trees) and the game's path resolution. <para />
|
||||||
|
/// Please note that this will make path resolution cases that depend on metadata incorrect.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns> A struct that will undo this operation when disposed. Best used with: <code>using (var _ = pathState.EnterInternalResolve()) { ... }</code> </returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public InternalResolveRaii EnterInternalResolve()
|
||||||
|
=> new(this);
|
||||||
|
|
||||||
|
public readonly ref struct InternalResolveRaii
|
||||||
|
{
|
||||||
|
private readonly ThreadLocal<uint> _internalResolve;
|
||||||
|
|
||||||
|
public InternalResolveRaii(PathState parent)
|
||||||
|
{
|
||||||
|
_internalResolve = parent._internalResolve;
|
||||||
|
++_internalResolve.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public readonly void Dispose()
|
||||||
|
{
|
||||||
|
--_internalResolve.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ public unsafe class ResolvePathHooks : IDisposable
|
||||||
private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
|
private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
|
||||||
{
|
{
|
||||||
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
||||||
using var eqdp = slotIndex > 9
|
using var eqdp = slotIndex > 9 || _parent.InInternalResolve
|
||||||
? DisposableContainer.Empty
|
? DisposableContainer.Empty
|
||||||
: _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4);
|
: _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4);
|
||||||
return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex));
|
return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex));
|
||||||
|
|
@ -176,6 +176,10 @@ public unsafe class ResolvePathHooks : IDisposable
|
||||||
private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data)
|
private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data)
|
||||||
{
|
{
|
||||||
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
||||||
|
if (_parent.InInternalResolve)
|
||||||
|
{
|
||||||
|
return DisposableContainer.Empty;
|
||||||
|
}
|
||||||
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face),
|
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face),
|
||||||
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body),
|
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body),
|
||||||
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair),
|
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair),
|
||||||
|
|
|
||||||
248
Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
Normal file
248
Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
using Penumbra.String;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
using static Penumbra.Interop.Structs.CharacterBaseUtility;
|
||||||
|
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
|
internal partial record ResolveContext
|
||||||
|
{
|
||||||
|
|
||||||
|
private Utf8GamePath ResolveModelPath()
|
||||||
|
{
|
||||||
|
// Correctness:
|
||||||
|
// Resolving a model path through the game's code can use EQDP metadata for human equipment models.
|
||||||
|
return ModelType switch
|
||||||
|
{
|
||||||
|
ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(),
|
||||||
|
_ => ResolveModelPathNative(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Utf8GamePath ResolveEquipmentModelPath()
|
||||||
|
{
|
||||||
|
var path = SlotIndex < 5
|
||||||
|
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot)
|
||||||
|
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot);
|
||||||
|
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe GenderRace ResolveModelRaceCode()
|
||||||
|
=> ResolveEqdpRaceCode(Slot, Equipment.Set);
|
||||||
|
|
||||||
|
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, SetId setId)
|
||||||
|
{
|
||||||
|
var slotIndex = slot.ToIndex();
|
||||||
|
if (slotIndex >= 10 || ModelType != ModelType.Human)
|
||||||
|
return GenderRace.MidlanderMale;
|
||||||
|
|
||||||
|
var characterRaceCode = (GenderRace)((Human*)CharacterBase.Value)->RaceSexId;
|
||||||
|
if (characterRaceCode == GenderRace.MidlanderMale)
|
||||||
|
return GenderRace.MidlanderMale;
|
||||||
|
|
||||||
|
var accessory = slotIndex >= 5;
|
||||||
|
if ((ushort)characterRaceCode % 10 != 1 && accessory)
|
||||||
|
return GenderRace.MidlanderMale;
|
||||||
|
|
||||||
|
var metaCache = Global.Collection.MetaCache;
|
||||||
|
if (metaCache == null)
|
||||||
|
return GenderRace.MidlanderMale;
|
||||||
|
|
||||||
|
var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, setId);
|
||||||
|
if (entry.ToBits(slot).Item2)
|
||||||
|
return characterRaceCode;
|
||||||
|
|
||||||
|
var fallbackRaceCode = characterRaceCode.Fallback();
|
||||||
|
if (fallbackRaceCode == GenderRace.MidlanderMale)
|
||||||
|
return GenderRace.MidlanderMale;
|
||||||
|
|
||||||
|
entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, setId);
|
||||||
|
if (entry.ToBits(slot).Item2)
|
||||||
|
return fallbackRaceCode;
|
||||||
|
|
||||||
|
return GenderRace.MidlanderMale;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveModelPathNative()
|
||||||
|
{
|
||||||
|
var path = ResolveMdlPath(CharacterBase, SlotIndex);
|
||||||
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
// Safety:
|
||||||
|
// Resolving a material path through the game's code can dereference null pointers for equipment materials.
|
||||||
|
return ModelType switch
|
||||||
|
{
|
||||||
|
ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName),
|
||||||
|
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName),
|
||||||
|
ModelType.Weapon => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName),
|
||||||
|
_ => ResolveMaterialPathNative(mtrlFileName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
var modelPathSpan = modelPath.Path.Span;
|
||||||
|
var baseDirectory = modelPathSpan[..modelPathSpan.IndexOf("/model/"u8)];
|
||||||
|
|
||||||
|
var variant = ResolveMaterialVariant(imcPath);
|
||||||
|
|
||||||
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
|
baseDirectory.CopyTo(pathBuffer);
|
||||||
|
"/material/v"u8.CopyTo(pathBuffer[baseDirectory.Length..]);
|
||||||
|
WriteZeroPaddedNumber(pathBuffer.Slice(baseDirectory.Length + 11, 4), variant);
|
||||||
|
pathBuffer[baseDirectory.Length + 15] = (byte)'/';
|
||||||
|
fileName.CopyTo(pathBuffer[(baseDirectory.Length + 16)..]);
|
||||||
|
|
||||||
|
return Utf8GamePath.FromSpan(pathBuffer[..(baseDirectory.Length + 16 + fileName.Length)], out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte ResolveMaterialVariant(Utf8GamePath imcPath)
|
||||||
|
{
|
||||||
|
var metaCache = Global.Collection.MetaCache;
|
||||||
|
if (metaCache == null)
|
||||||
|
return Equipment.Variant.Id;
|
||||||
|
|
||||||
|
var entry = metaCache.GetImcEntry(imcPath, Slot, Equipment.Variant, out var exists);
|
||||||
|
if (!exists)
|
||||||
|
return Equipment.Variant.Id;
|
||||||
|
|
||||||
|
return entry.MaterialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteZeroPaddedNumber(Span<byte> destination, ushort number)
|
||||||
|
{
|
||||||
|
for (var i = destination.Length; i-- > 0;)
|
||||||
|
{
|
||||||
|
destination[i] = (byte)('0' + number % 10);
|
||||||
|
number /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveMaterialPathNative(byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
ByteString? path;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
path = ResolveMtrlPath(CharacterBase, SlotIndex, mtrlFileName);
|
||||||
|
}
|
||||||
|
catch (AccessViolationException)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase.Value:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})");
|
||||||
|
return Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Utf8GamePath ResolveSkeletonPath(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
// Correctness and Safety:
|
||||||
|
// Resolving a skeleton path through the game's code can use EST metadata for human skeletons.
|
||||||
|
// Additionally, it can dereference null pointers for human equipment skeletons.
|
||||||
|
return ModelType switch
|
||||||
|
{
|
||||||
|
ModelType.Human => ResolveHumanSkeletonPath(partialSkeletonIndex),
|
||||||
|
_ => ResolveSkeletonPathNative(partialSkeletonIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveHumanSkeletonPath(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||||
|
if (set == 0)
|
||||||
|
return Utf8GamePath.Empty;
|
||||||
|
|
||||||
|
var path = GamePaths.Skeleton.Sklb.Path(raceCode, slot, set);
|
||||||
|
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanSkeletonData(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var human = (Human*)CharacterBase.Value;
|
||||||
|
var characterRaceCode = (GenderRace)human->RaceSexId;
|
||||||
|
switch (partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return (characterRaceCode, "base", 1);
|
||||||
|
case 1:
|
||||||
|
var faceId = human->FaceId;
|
||||||
|
var tribe = human->Customize[(int)CustomizeIndex.Tribe];
|
||||||
|
var modelType = human->Customize[(int)CustomizeIndex.ModelType];
|
||||||
|
if (faceId < 201)
|
||||||
|
{
|
||||||
|
faceId -= tribe switch
|
||||||
|
{
|
||||||
|
0xB when modelType == 4 => 100,
|
||||||
|
0xE | 0xF => 100,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Face, faceId);
|
||||||
|
case 2:
|
||||||
|
return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Hair, human->HairId);
|
||||||
|
case 3:
|
||||||
|
return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstManipulation.EstType.Head);
|
||||||
|
case 4:
|
||||||
|
return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstManipulation.EstType.Body);
|
||||||
|
default:
|
||||||
|
return (0, string.Empty, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type)
|
||||||
|
{
|
||||||
|
var human = (Human*)CharacterBase.Value;
|
||||||
|
var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()];
|
||||||
|
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, SetId set)
|
||||||
|
{
|
||||||
|
var metaCache = Global.Collection.MetaCache;
|
||||||
|
var skeletonSet = metaCache == null ? default : metaCache.GetEstEntry(type, raceCode, set);
|
||||||
|
return (raceCode, EstManipulation.ToName(type), skeletonSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var path = ResolveSklbPath(CharacterBase, partialSkeletonIndex);
|
||||||
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Utf8GamePath ResolveSkeletonParameterPath(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
// Correctness and Safety:
|
||||||
|
// Resolving a skeleton parameter path through the game's code can use EST metadata for human skeletons.
|
||||||
|
// Additionally, it can dereference null pointers for human equipment skeletons.
|
||||||
|
return ModelType switch
|
||||||
|
{
|
||||||
|
ModelType.Human => ResolveHumanSkeletonParameterPath(partialSkeletonIndex),
|
||||||
|
_ => ResolveSkeletonParameterPathNative(partialSkeletonIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||||
|
if (set == 0)
|
||||||
|
return Utf8GamePath.Empty;
|
||||||
|
|
||||||
|
var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set);
|
||||||
|
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveSkeletonParameterPathNative(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var path = ResolveSkpPath(CharacterBase, partialSkeletonIndex);
|
||||||
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using FFXIVClientStructs.Interop;
|
using FFXIVClientStructs.Interop;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.Collections;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
@ -12,23 +13,29 @@ using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI;
|
using Penumbra.UI;
|
||||||
using static Penumbra.Interop.Structs.CharacterBaseUtility;
|
using static Penumbra.Interop.Structs.CharacterBaseUtility;
|
||||||
|
using static Penumbra.Interop.Structs.ModelResourceHandleUtility;
|
||||||
using static Penumbra.Interop.Structs.StructExtensions;
|
using static Penumbra.Interop.Structs.StructExtensions;
|
||||||
|
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||||
|
|
||||||
namespace Penumbra.Interop.ResourceTree;
|
namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache,
|
internal record GlobalResolveContext(IObjectIdentifier Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData)
|
||||||
int Skeleton, bool WithUiData)
|
|
||||||
{
|
{
|
||||||
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
||||||
|
|
||||||
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex, EquipSlot slot, CharacterArmor equipment)
|
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
||||||
=> new(this, characterBase, slotIndex, slot, equipment);
|
EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, WeaponType weaponType = default)
|
||||||
|
=> new(this, characterBase, slotIndex, slot, equipment, weaponType);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBase> CharacterBase, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment)
|
internal partial record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBase> CharacterBase, uint SlotIndex,
|
||||||
|
EquipSlot Slot, CharacterArmor Equipment, WeaponType WeaponType)
|
||||||
{
|
{
|
||||||
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
|
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
|
||||||
|
|
||||||
|
private unsafe ModelType ModelType
|
||||||
|
=> CharacterBase.Value->GetModelType();
|
||||||
|
|
||||||
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath)
|
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath)
|
||||||
{
|
{
|
||||||
if (resourceHandle == null)
|
if (resourceHandle == null)
|
||||||
|
|
@ -46,35 +53,33 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
if (resourceHandle == null)
|
if (resourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
Utf8GamePath path;
|
||||||
if (dx11)
|
if (dx11)
|
||||||
{
|
{
|
||||||
var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/');
|
var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/');
|
||||||
if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3)
|
if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (gamePath[lastDirectorySeparator + 1] != (byte)'-' || gamePath[lastDirectorySeparator + 2] != (byte)'-')
|
Span<byte> prefixed = stackalloc byte[260];
|
||||||
{
|
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
||||||
Span<byte> prefixed = stackalloc byte[gamePath.Length + 2];
|
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
||||||
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
||||||
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
||||||
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
|
||||||
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
|
||||||
|
|
||||||
if (!Utf8GamePath.FromSpan(prefixed, out var tmp))
|
if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
gamePath = tmp.Path.Clone();
|
path = tmp.Clone();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Make sure the game path is owned, otherwise stale trees could cause crashes (access violations) or other memory safety issues.
|
// Make sure the game path is owned, otherwise stale trees could cause crashes (access violations) or other memory safety issues.
|
||||||
if (!gamePath.IsOwned)
|
if (!gamePath.IsOwned)
|
||||||
gamePath = gamePath.Clone();
|
gamePath = gamePath.Clone();
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(gamePath, out var path))
|
if (!Utf8GamePath.FromByteString(gamePath, out path))
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path);
|
return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path);
|
||||||
}
|
}
|
||||||
|
|
@ -143,23 +148,28 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
|
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl)
|
public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, Utf8GamePath imcPath)
|
||||||
{
|
{
|
||||||
if (mdl == null || mdl->ModelResourceHandle == null)
|
if (mdl == null || mdl->ModelResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
var mdlResource = mdl->ModelResourceHandle;
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path))
|
if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)mdlResource), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, path, false);
|
var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdlResource->ResourceHandle, path, false);
|
||||||
|
|
||||||
for (var i = 0; i < mdl->MaterialCount; i++)
|
for (var i = 0; i < mdl->MaterialCount; i++)
|
||||||
{
|
{
|
||||||
var mtrl = mdl->Materials[i];
|
var mtrl = mdl->Materials[i];
|
||||||
var mtrlNode = CreateNodeFromMaterial(mtrl);
|
if (mtrl == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var mtrlFileName = GetMaterialFileNameBySlot(mdlResource, (uint)i);
|
||||||
|
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imcPath, mtrlFileName));
|
||||||
if (mtrlNode != null)
|
if (mtrlNode != null)
|
||||||
{
|
{
|
||||||
if (Global.WithUiData)
|
if (Global.WithUiData)
|
||||||
|
|
@ -173,7 +183,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl)
|
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path)
|
||||||
{
|
{
|
||||||
static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet<uint> alreadyVisitedSamplerIds)
|
static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet<uint> alreadyVisitedSamplerIds)
|
||||||
{
|
{
|
||||||
|
|
@ -200,8 +210,6 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
if (mtrl == null || mtrl->MaterialResourceHandle == null)
|
if (mtrl == null || mtrl->MaterialResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var path = Utf8GamePath.Empty; // TODO
|
|
||||||
|
|
||||||
var resource = mtrl->MaterialResourceHandle;
|
var resource = mtrl->MaterialResourceHandle;
|
||||||
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
@ -265,8 +273,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
if (sklb == null || sklb->SkeletonResourceHandle == null)
|
if (sklb == null || sklb->SkeletonResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(ResolveSklbPath(CharacterBase, partialSkeletonIndex), out var path))
|
var path = ResolveSkeletonPath(partialSkeletonIndex);
|
||||||
return null;
|
|
||||||
|
|
||||||
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
@ -288,8 +295,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
|
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(ResolveSkpPath(CharacterBase, partialSkeletonIndex), out var path))
|
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
|
||||||
return null;
|
|
||||||
|
|
||||||
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
@ -305,43 +311,6 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal List<Utf8GamePath> FilterGamePaths(IReadOnlyCollection<Utf8GamePath> gamePaths)
|
|
||||||
{
|
|
||||||
var filtered = new List<Utf8GamePath>(gamePaths.Count);
|
|
||||||
foreach (var path in gamePaths)
|
|
||||||
{
|
|
||||||
// In doubt, keep the paths.
|
|
||||||
if (IsMatch(path.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
?? true)
|
|
||||||
filtered.Add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool? IsMatch(ReadOnlySpan<string> path)
|
|
||||||
=> SafeGet(path, 0) switch
|
|
||||||
{
|
|
||||||
"chara" => SafeGet(path, 1) switch
|
|
||||||
{
|
|
||||||
"accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Id:D4}"),
|
|
||||||
"equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Id:D4}"),
|
|
||||||
"monster" => SafeGet(path, 2) == $"m{Global.Skeleton:D4}",
|
|
||||||
"weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Id:D4}"),
|
|
||||||
_ => null,
|
|
||||||
},
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool? IsMatchEquipment(ReadOnlySpan<string> path, string equipmentDir)
|
|
||||||
=> SafeGet(path, 0) == equipmentDir
|
|
||||||
? SafeGet(path, 1) switch
|
|
||||||
{
|
|
||||||
"material" => SafeGet(path, 2) == $"v{Equipment.Variant.Id:D4}",
|
|
||||||
_ => null,
|
|
||||||
}
|
|
||||||
: false;
|
|
||||||
|
|
||||||
internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath)
|
internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries);
|
var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI;
|
using Penumbra.UI;
|
||||||
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
||||||
|
|
||||||
|
|
@ -64,25 +65,13 @@ public class ResourceTree
|
||||||
CustomizeData = character->DrawData.CustomizeData;
|
CustomizeData = character->DrawData.CustomizeData;
|
||||||
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(model, 0xFFFFFFFFu, EquipSlot.Unknown, default);
|
var genericContext = globalContext.CreateContext(model);
|
||||||
|
|
||||||
var eid = (ResourceHandle*)model->EID;
|
|
||||||
var eidNode = genericContext.CreateNodeFromEid(eid);
|
|
||||||
if (eidNode != null)
|
|
||||||
{
|
|
||||||
if (globalContext.WithUiData)
|
|
||||||
eidNode.FallbackName = "EID";
|
|
||||||
Nodes.Add(eidNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < model->SlotCount; ++i)
|
for (var i = 0; i < model->SlotCount; ++i)
|
||||||
{
|
{
|
||||||
var slotContext = globalContext.CreateContext(
|
var slotContext = i < equipment.Length
|
||||||
model,
|
? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i])
|
||||||
(uint)i,
|
: globalContext.CreateContext(model, (uint)i);
|
||||||
i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown,
|
|
||||||
i < equipment.Length ? equipment[i] : default
|
|
||||||
);
|
|
||||||
|
|
||||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||||
|
|
@ -94,7 +83,7 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdl = model->Models[i];
|
var mdl = model->Models[i];
|
||||||
var mdlNode = slotContext.CreateNodeFromRenderModel(mdl);
|
var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty);
|
||||||
if (mdlNode != null)
|
if (mdlNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
|
|
@ -103,77 +92,68 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(Nodes, genericContext, model->Skeleton);
|
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton);
|
||||||
|
|
||||||
AddSubObjects(globalContext, model);
|
AddWeapons(globalContext, model);
|
||||||
|
|
||||||
if (human != null)
|
if (human != null)
|
||||||
AddHumanResources(globalContext, human);
|
AddHumanResources(globalContext, human);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddSubObjects(GlobalResolveContext globalContext, CharacterBase* model)
|
private unsafe void AddWeapons(GlobalResolveContext globalContext, CharacterBase* model)
|
||||||
{
|
{
|
||||||
var subObjectIndex = 0;
|
var weaponIndex = 0;
|
||||||
var weaponIndex = 0;
|
var weaponNodes = new List<ResourceNode>();
|
||||||
var subObjectNodes = new List<ResourceNode>();
|
|
||||||
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
||||||
{
|
{
|
||||||
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
||||||
continue;
|
continue;
|
||||||
var subObject = (CharacterBase*)baseSubObject;
|
var subObject = (CharacterBase*)baseSubObject;
|
||||||
|
|
||||||
var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null;
|
if (subObject->GetModelType() != CharacterBase.ModelType.Weapon)
|
||||||
var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc.";
|
continue;
|
||||||
|
var weapon = (Weapon*)subObject;
|
||||||
|
|
||||||
// This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it.
|
// This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it.
|
||||||
var slot = weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown;
|
var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand;
|
||||||
var equipment = weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default;
|
var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown);
|
||||||
|
var weaponType = weapon->SecondaryId;
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment);
|
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
||||||
|
|
||||||
var eid = (ResourceHandle*)subObject->EID;
|
|
||||||
var eidNode = genericContext.CreateNodeFromEid(eid);
|
|
||||||
if (eidNode != null)
|
|
||||||
{
|
|
||||||
if (globalContext.WithUiData)
|
|
||||||
eidNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, EID";
|
|
||||||
Nodes.Add(eidNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < subObject->SlotCount; ++i)
|
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||||
{
|
{
|
||||||
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment);
|
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
|
||||||
|
|
||||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||||
if (imcNode != null)
|
if (imcNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}";
|
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
|
||||||
subObjectNodes.Add(imcNode);
|
weaponNodes.Add(imcNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdl = subObject->Models[i];
|
var mdl = subObject->Models[i];
|
||||||
var mdlNode = slotContext.CreateNodeFromRenderModel(mdl);
|
var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty);
|
||||||
if (mdlNode != null)
|
if (mdlNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}";
|
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||||
subObjectNodes.Add(mdlNode);
|
weaponNodes.Add(mdlNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(subObjectNodes, genericContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
|
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, ");
|
||||||
|
|
||||||
++subObjectIndex;
|
++weaponIndex;
|
||||||
if (weapon != null)
|
|
||||||
++weaponIndex;
|
|
||||||
}
|
}
|
||||||
Nodes.InsertRange(0, subObjectNodes);
|
Nodes.InsertRange(0, weaponNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human)
|
private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human)
|
||||||
{
|
{
|
||||||
var genericContext = globalContext.CreateContext(&human->CharacterBase, 0xFFFFFFFFu, EquipSlot.Unknown, default);
|
var genericContext = globalContext.CreateContext(&human->CharacterBase);
|
||||||
|
|
||||||
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
|
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
|
||||||
var decalPath = decalId != 0
|
var decalPath = decalId != 0
|
||||||
|
|
@ -208,8 +188,16 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, Skeleton* skeleton, string prefix = "")
|
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "")
|
||||||
{
|
{
|
||||||
|
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
||||||
|
if (eidNode != null)
|
||||||
|
{
|
||||||
|
if (context.Global.WithUiData)
|
||||||
|
eidNode.FallbackName = $"{prefix}EID";
|
||||||
|
Nodes.Add(eidNode);
|
||||||
|
}
|
||||||
|
|
||||||
if (skeleton == null)
|
if (skeleton == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
|
@ -18,9 +17,10 @@ public class ResourceTreeFactory
|
||||||
private readonly IdentifierService _identifier;
|
private readonly IdentifierService _identifier;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ActorService _actors;
|
private readonly ActorService _actors;
|
||||||
|
private readonly PathState _pathState;
|
||||||
|
|
||||||
public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
|
public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
|
||||||
Configuration config, ActorService actors)
|
Configuration config, ActorService actors, PathState pathState)
|
||||||
{
|
{
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
|
|
@ -28,6 +28,7 @@ public class ResourceTreeFactory
|
||||||
_identifier = identifier;
|
_identifier = identifier;
|
||||||
_config = config;
|
_config = config;
|
||||||
_actors = actors;
|
_actors = actors;
|
||||||
|
_pathState = pathState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TreeBuildCache CreateTreeBuildCache()
|
private TreeBuildCache CreateTreeBuildCache()
|
||||||
|
|
@ -87,13 +88,17 @@ public class ResourceTreeFactory
|
||||||
var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId;
|
var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId;
|
||||||
var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related,
|
var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related,
|
||||||
networked, collectionResolveData.ModCollection.Name);
|
networked, collectionResolveData.ModCollection.Name);
|
||||||
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache,
|
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, collectionResolveData.ModCollection,
|
||||||
((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUiData) != 0);
|
cache, (flags & Flags.WithUiData) != 0);
|
||||||
tree.LoadResources(globalContext);
|
using (var _ = _pathState.EnterInternalResolve())
|
||||||
|
{
|
||||||
|
tree.LoadResources(globalContext);
|
||||||
|
}
|
||||||
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
|
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
|
||||||
tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node));
|
tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node));
|
||||||
|
|
||||||
ResolveGamePaths(tree, collectionResolveData.ModCollection);
|
// This is currently unneeded as we can resolve all paths by querying the draw object:
|
||||||
|
// ResolveGamePaths(tree, collectionResolveData.ModCollection);
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
ResolveUiData(tree);
|
ResolveUiData(tree);
|
||||||
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null);
|
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null);
|
||||||
|
|
@ -128,23 +133,15 @@ public class ResourceTreeFactory
|
||||||
if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet))
|
if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
IReadOnlyCollection<Utf8GamePath> resolvedList = resolvedSet;
|
if (resolvedSet.Count != 1)
|
||||||
if (resolvedList.Count > 1)
|
|
||||||
{
|
|
||||||
var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList);
|
|
||||||
if (filteredList.Count > 0)
|
|
||||||
resolvedList = filteredList;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedList.Count != 1)
|
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug(
|
Penumbra.Log.Debug(
|
||||||
$"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
|
$"Found {resolvedSet.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
|
||||||
foreach (var gamePath in resolvedList)
|
foreach (var gamePath in resolvedSet)
|
||||||
Penumbra.Log.Debug($"Game path: {gamePath}");
|
Penumbra.Log.Debug($"Game path: {gamePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
node.PossibleGamePaths = resolvedList.ToArray();
|
node.PossibleGamePaths = resolvedSet.ToArray();
|
||||||
}
|
}
|
||||||
else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1)
|
else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
18
Penumbra/Interop/Structs/ModelResourceHandleUtility.cs
Normal file
18
Penumbra/Interop/Structs/ModelResourceHandleUtility.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
// TODO submit this to ClientStructs
|
||||||
|
public class ModelResourceHandleUtility
|
||||||
|
{
|
||||||
|
public ModelResourceHandleUtility(IGameInteropProvider interop)
|
||||||
|
=> interop.InitializeFromAttributes(this);
|
||||||
|
|
||||||
|
[Signature("E8 ?? ?? ?? ?? 44 8B CD 48 89 44 24")]
|
||||||
|
private static nint _getMaterialFileNameBySlot = nint.Zero;
|
||||||
|
|
||||||
|
public static unsafe byte* GetMaterialFileNameBySlot(ModelResourceHandle* handle, uint slot)
|
||||||
|
=> ((delegate* unmanaged<ModelResourceHandle*, uint, byte*>)_getMaterialFileNameBySlot)(handle, slot);
|
||||||
|
}
|
||||||
|
|
@ -65,6 +65,13 @@ public unsafe class ImcFile : MetaBaseFile
|
||||||
return ptr == null ? new ImcEntry() : *ptr;
|
return ptr == null ? new ImcEntry() : *ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImcEntry GetEntry(int partIdx, Variant variantIdx, out bool exists)
|
||||||
|
{
|
||||||
|
var ptr = VariantPtr(Data, partIdx, variantIdx);
|
||||||
|
exists = ptr != null;
|
||||||
|
return exists ? *ptr : new ImcEntry();
|
||||||
|
}
|
||||||
|
|
||||||
public static int PartIndex(EquipSlot slot)
|
public static int PartIndex(EquipSlot slot)
|
||||||
=> slot switch
|
=> slot switch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
_communicatorService = _services.GetRequiredService<CommunicatorService>();
|
_communicatorService = _services.GetRequiredService<CommunicatorService>();
|
||||||
_services.GetRequiredService<ResourceService>(); // Initialize because not required anywhere else.
|
_services.GetRequiredService<ResourceService>(); // Initialize because not required anywhere else.
|
||||||
_services.GetRequiredService<ModCacheManager>(); // Initialize because not required anywhere else.
|
_services.GetRequiredService<ModCacheManager>(); // Initialize because not required anywhere else.
|
||||||
|
_services.GetRequiredService<ModelResourceHandleUtility>(); // Initialize because not required anywhere else.
|
||||||
_collectionManager.Caches.CreateNecessaryCaches();
|
_collectionManager.Caches.CreateNecessaryCaches();
|
||||||
using (var t = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
|
using (var t = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,8 @@ public static class ServiceManager
|
||||||
.AddSingleton<CreateFileWHook>()
|
.AddSingleton<CreateFileWHook>()
|
||||||
.AddSingleton<ResidentResourceManager>()
|
.AddSingleton<ResidentResourceManager>()
|
||||||
.AddSingleton<FontReloader>()
|
.AddSingleton<FontReloader>()
|
||||||
.AddSingleton<RedrawService>();
|
.AddSingleton<RedrawService>()
|
||||||
|
.AddSingleton<ModelResourceHandleUtility>();
|
||||||
|
|
||||||
private static IServiceCollection AddConfiguration(this IServiceCollection services)
|
private static IServiceCollection AddConfiguration(this IServiceCollection services)
|
||||||
=> services.AddTransient<ConfigMigrationService>()
|
=> services.AddTransient<ConfigMigrationService>()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue