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 Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta;
|
||||
|
|
@ -61,6 +62,26 @@ public struct EstCache : IDisposable
|
|||
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()
|
||||
{
|
||||
_estFaceFile?.Reset();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Interop.Structs;
|
||||
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)
|
||||
=> _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>
|
||||
private void ApplyStoredManipulations()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@ public unsafe class PathState : IDisposable
|
|||
private readonly ResolvePathHooks _demiHuman;
|
||||
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
|
||||
=> _resolveData.Values;
|
||||
|
||||
public bool InInternalResolve
|
||||
=> _internalResolve.Value != 0u;
|
||||
|
||||
public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop)
|
||||
{
|
||||
interop.InitializeFromAttributes(this);
|
||||
|
|
@ -55,6 +59,7 @@ public unsafe class PathState : IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
_resolveData.Dispose();
|
||||
_internalResolve.Dispose();
|
||||
_human.Dispose();
|
||||
_weapon.Dispose();
|
||||
_demiHuman.Dispose();
|
||||
|
|
@ -80,7 +85,10 @@ public unsafe class PathState : IDisposable
|
|||
if (path == nint.Zero)
|
||||
return path;
|
||||
|
||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
||||
if (!InInternalResolve)
|
||||
{
|
||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +98,37 @@ public unsafe class PathState : IDisposable
|
|||
if (path == nint.Zero)
|
||||
return path;
|
||||
|
||||
_resolveData.Value = data;
|
||||
if (!InInternalResolve)
|
||||
{
|
||||
_resolveData.Value = data;
|
||||
}
|
||||
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)
|
||||
{
|
||||
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
||||
using var eqdp = slotIndex > 9
|
||||
using var eqdp = slotIndex > 9 || _parent.InInternalResolve
|
||||
? DisposableContainer.Empty
|
||||
: _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4);
|
||||
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)
|
||||
{
|
||||
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
||||
if (_parent.InInternalResolve)
|
||||
{
|
||||
return DisposableContainer.Empty;
|
||||
}
|
||||
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.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 OtterGui;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
|
@ -12,23 +13,29 @@ using Penumbra.String;
|
|||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI;
|
||||
using static Penumbra.Interop.Structs.CharacterBaseUtility;
|
||||
using static Penumbra.Interop.Structs.ModelResourceHandleUtility;
|
||||
using static Penumbra.Interop.Structs.StructExtensions;
|
||||
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||
|
||||
namespace Penumbra.Interop.ResourceTree;
|
||||
|
||||
internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache,
|
||||
int Skeleton, bool WithUiData)
|
||||
internal record GlobalResolveContext(IObjectIdentifier Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData)
|
||||
{
|
||||
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
||||
|
||||
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex, EquipSlot slot, CharacterArmor equipment)
|
||||
=> new(this, characterBase, slotIndex, slot, equipment);
|
||||
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
||||
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 unsafe ModelType ModelType
|
||||
=> CharacterBase.Value->GetModelType();
|
||||
|
||||
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath)
|
||||
{
|
||||
if (resourceHandle == null)
|
||||
|
|
@ -46,35 +53,33 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
|||
if (resourceHandle == null)
|
||||
return null;
|
||||
|
||||
Utf8GamePath path;
|
||||
if (dx11)
|
||||
{
|
||||
var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/');
|
||||
if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3)
|
||||
return null;
|
||||
|
||||
if (gamePath[lastDirectorySeparator + 1] != (byte)'-' || gamePath[lastDirectorySeparator + 2] != (byte)'-')
|
||||
{
|
||||
Span<byte> prefixed = stackalloc byte[gamePath.Length + 2];
|
||||
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
||||
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
||||
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
||||
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
||||
Span<byte> prefixed = stackalloc byte[260];
|
||||
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
||||
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
||||
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
||||
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
||||
|
||||
if (!Utf8GamePath.FromSpan(prefixed, out var tmp))
|
||||
return null;
|
||||
if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp))
|
||||
return null;
|
||||
|
||||
gamePath = tmp.Path.Clone();
|
||||
}
|
||||
path = tmp.Clone();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure the game path is owned, otherwise stale trees could cause crashes (access violations) or other memory safety issues.
|
||||
if (!gamePath.IsOwned)
|
||||
gamePath = gamePath.Clone();
|
||||
}
|
||||
|
||||
if (!Utf8GamePath.FromByteString(gamePath, out var path))
|
||||
return null;
|
||||
if (!Utf8GamePath.FromByteString(gamePath, out path))
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl)
|
||||
public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, Utf8GamePath imcPath)
|
||||
{
|
||||
if (mdl == null || mdl->ModelResourceHandle == null)
|
||||
return null;
|
||||
var mdlResource = mdl->ModelResourceHandle;
|
||||
|
||||
if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path))
|
||||
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;
|
||||
|
||||
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++)
|
||||
{
|
||||
var mtrl = mdl->Materials[i];
|
||||
var mtrlNode = CreateNodeFromMaterial(mtrl);
|
||||
var mtrl = mdl->Materials[i];
|
||||
if (mtrl == null)
|
||||
continue;
|
||||
|
||||
var mtrlFileName = GetMaterialFileNameBySlot(mdlResource, (uint)i);
|
||||
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imcPath, mtrlFileName));
|
||||
if (mtrlNode != null)
|
||||
{
|
||||
if (Global.WithUiData)
|
||||
|
|
@ -173,7 +183,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
|||
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)
|
||||
{
|
||||
|
|
@ -200,8 +210,6 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
|||
if (mtrl == null || mtrl->MaterialResourceHandle == null)
|
||||
return null;
|
||||
|
||||
var path = Utf8GamePath.Empty; // TODO
|
||||
|
||||
var resource = mtrl->MaterialResourceHandle;
|
||||
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
||||
return cached;
|
||||
|
|
@ -265,8 +273,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
|||
if (sklb == null || sklb->SkeletonResourceHandle == null)
|
||||
return null;
|
||||
|
||||
if (!Utf8GamePath.FromByteString(ResolveSklbPath(CharacterBase, partialSkeletonIndex), out var path))
|
||||
return null;
|
||||
var path = ResolveSkeletonPath(partialSkeletonIndex);
|
||||
|
||||
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
|
||||
return cached;
|
||||
|
|
@ -288,8 +295,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
|||
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
|
||||
return null;
|
||||
|
||||
if (!Utf8GamePath.FromByteString(ResolveSkpPath(CharacterBase, partialSkeletonIndex), out var path))
|
||||
return null;
|
||||
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
|
||||
|
||||
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached))
|
||||
return cached;
|
||||
|
|
@ -305,43 +311,6 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBas
|
|||
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)
|
||||
{
|
||||
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.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI;
|
||||
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
||||
|
||||
|
|
@ -64,25 +65,13 @@ public class ResourceTree
|
|||
CustomizeData = character->DrawData.CustomizeData;
|
||||
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
||||
|
||||
var genericContext = globalContext.CreateContext(model, 0xFFFFFFFFu, EquipSlot.Unknown, default);
|
||||
|
||||
var eid = (ResourceHandle*)model->EID;
|
||||
var eidNode = genericContext.CreateNodeFromEid(eid);
|
||||
if (eidNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
eidNode.FallbackName = "EID";
|
||||
Nodes.Add(eidNode);
|
||||
}
|
||||
var genericContext = globalContext.CreateContext(model);
|
||||
|
||||
for (var i = 0; i < model->SlotCount; ++i)
|
||||
{
|
||||
var slotContext = globalContext.CreateContext(
|
||||
model,
|
||||
(uint)i,
|
||||
i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown,
|
||||
i < equipment.Length ? equipment[i] : default
|
||||
);
|
||||
var slotContext = i < equipment.Length
|
||||
? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i])
|
||||
: globalContext.CreateContext(model, (uint)i);
|
||||
|
||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||
|
|
@ -94,7 +83,7 @@ public class ResourceTree
|
|||
}
|
||||
|
||||
var mdl = model->Models[i];
|
||||
var mdlNode = slotContext.CreateNodeFromRenderModel(mdl);
|
||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty);
|
||||
if (mdlNode != null)
|
||||
{
|
||||
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)
|
||||
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 subObjectNodes = new List<ResourceNode>();
|
||||
var weaponIndex = 0;
|
||||
var weaponNodes = new List<ResourceNode>();
|
||||
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
||||
{
|
||||
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
||||
continue;
|
||||
var subObject = (CharacterBase*)baseSubObject;
|
||||
|
||||
var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null;
|
||||
var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc.";
|
||||
if (subObject->GetModelType() != CharacterBase.ModelType.Weapon)
|
||||
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.
|
||||
var slot = weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown;
|
||||
var equipment = weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default;
|
||||
var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand;
|
||||
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 eid = (ResourceHandle*)subObject->EID;
|
||||
var eidNode = genericContext.CreateNodeFromEid(eid);
|
||||
if (eidNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
eidNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, EID";
|
||||
Nodes.Add(eidNode);
|
||||
}
|
||||
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
||||
|
||||
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 imcNode = slotContext.CreateNodeFromImc(imc);
|
||||
if (imcNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}";
|
||||
subObjectNodes.Add(imcNode);
|
||||
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
|
||||
weaponNodes.Add(imcNode);
|
||||
}
|
||||
|
||||
var mdl = subObject->Models[i];
|
||||
var mdlNode = slotContext.CreateNodeFromRenderModel(mdl);
|
||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty);
|
||||
if (mdlNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}";
|
||||
subObjectNodes.Add(mdlNode);
|
||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||
weaponNodes.Add(mdlNode);
|
||||
}
|
||||
}
|
||||
|
||||
AddSkeleton(subObjectNodes, genericContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
|
||||
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, ");
|
||||
|
||||
++subObjectIndex;
|
||||
if (weapon != null)
|
||||
++weaponIndex;
|
||||
++weaponIndex;
|
||||
}
|
||||
Nodes.InsertRange(0, subObjectNodes);
|
||||
Nodes.InsertRange(0, weaponNodes);
|
||||
}
|
||||
|
||||
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 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)
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
|
|
@ -18,9 +17,10 @@ public class ResourceTreeFactory
|
|||
private readonly IdentifierService _identifier;
|
||||
private readonly Configuration _config;
|
||||
private readonly ActorService _actors;
|
||||
private readonly PathState _pathState;
|
||||
|
||||
public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
|
||||
Configuration config, ActorService actors)
|
||||
Configuration config, ActorService actors, PathState pathState)
|
||||
{
|
||||
_gameData = gameData;
|
||||
_objects = objects;
|
||||
|
|
@ -28,6 +28,7 @@ public class ResourceTreeFactory
|
|||
_identifier = identifier;
|
||||
_config = config;
|
||||
_actors = actors;
|
||||
_pathState = pathState;
|
||||
}
|
||||
|
||||
private TreeBuildCache CreateTreeBuildCache()
|
||||
|
|
@ -87,13 +88,17 @@ public class ResourceTreeFactory
|
|||
var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId;
|
||||
var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related,
|
||||
networked, collectionResolveData.ModCollection.Name);
|
||||
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache,
|
||||
((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUiData) != 0);
|
||||
tree.LoadResources(globalContext);
|
||||
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, collectionResolveData.ModCollection,
|
||||
cache, (flags & Flags.WithUiData) != 0);
|
||||
using (var _ = _pathState.EnterInternalResolve())
|
||||
{
|
||||
tree.LoadResources(globalContext);
|
||||
}
|
||||
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
|
||||
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)
|
||||
ResolveUiData(tree);
|
||||
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))
|
||||
continue;
|
||||
|
||||
IReadOnlyCollection<Utf8GamePath> resolvedList = resolvedSet;
|
||||
if (resolvedList.Count > 1)
|
||||
{
|
||||
var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList);
|
||||
if (filteredList.Count > 0)
|
||||
resolvedList = filteredList;
|
||||
}
|
||||
|
||||
if (resolvedList.Count != 1)
|
||||
if (resolvedSet.Count != 1)
|
||||
{
|
||||
Penumbra.Log.Debug(
|
||||
$"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
|
||||
foreach (var gamePath in resolvedList)
|
||||
$"Found {resolvedSet.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
|
||||
foreach (var gamePath in resolvedSet)
|
||||
Penumbra.Log.Debug($"Game path: {gamePath}");
|
||||
}
|
||||
|
||||
node.PossibleGamePaths = resolvedList.ToArray();
|
||||
node.PossibleGamePaths = resolvedSet.ToArray();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
=> slot switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ public class Penumbra : IDalamudPlugin
|
|||
_communicatorService = _services.GetRequiredService<CommunicatorService>();
|
||||
_services.GetRequiredService<ResourceService>(); // 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();
|
||||
using (var t = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ public static class ServiceManager
|
|||
.AddSingleton<CreateFileWHook>()
|
||||
.AddSingleton<ResidentResourceManager>()
|
||||
.AddSingleton<FontReloader>()
|
||||
.AddSingleton<RedrawService>();
|
||||
.AddSingleton<RedrawService>()
|
||||
.AddSingleton<ModelResourceHandleUtility>();
|
||||
|
||||
private static IServiceCollection AddConfiguration(this IServiceCollection services)
|
||||
=> services.AddTransient<ConfigMigrationService>()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue