mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Merge branch 'rt-rework'
This commit is contained in:
commit
806561b95a
34 changed files with 803 additions and 507 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit f39a716ad4f908c301d497728ede047ee6bd61c0
|
Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f
|
||||||
|
|
@ -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,18 @@ 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);
|
||||||
|
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (mtrlHandle == null)
|
if (mtrlHandle == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a resource handle");
|
throw new InvalidOperationException("Material doesn't have a resource handle");
|
||||||
|
|
||||||
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures;
|
var colorSetTextures = DrawObject->ColorTableTextures;
|
||||||
if (colorSetTextures == null)
|
if (colorSetTextures == null)
|
||||||
throw new InvalidOperationException("Draw object doesn't have color table textures");
|
throw new InvalidOperationException("Draw object doesn't have color table textures");
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
textureSize[1] = TextureHeight;
|
textureSize[1] = TextureHeight;
|
||||||
|
|
||||||
using var texture =
|
using var texture =
|
||||||
new SafeTextureHandle(Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7), false);
|
new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false);
|
||||||
if (texture.IsInvalid)
|
if (texture.IsInvalid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
{
|
{
|
||||||
fixed (Half* colorTable = _colorTable)
|
fixed (Half* colorTable = _colorTable)
|
||||||
{
|
{
|
||||||
success = Structs.TextureUtility.InitializeContents(texture.Texture, colorTable);
|
success = texture.Texture->InitializeContents(colorTable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
|
||||||
if (!base.IsStillValid())
|
if (!base.IsStillValid())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures;
|
var colorSetTextures = DrawObject->ColorTableTextures;
|
||||||
if (colorSetTextures == null)
|
if (colorSetTextures == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
if (mtrlHandle == null)
|
if (mtrlHandle == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a resource handle");
|
throw new InvalidOperationException("Material doesn't have a resource handle");
|
||||||
|
|
||||||
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
|
var shpkHandle = mtrlHandle->ShaderPackageResourceHandle;
|
||||||
if (shpkHandle == null)
|
if (shpkHandle == null)
|
||||||
throw new InvalidOperationException("Material doesn't have a ShPk resource handle");
|
throw new InvalidOperationException("Material doesn't have a ShPk resource handle");
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
|
||||||
if (!CheckValidity())
|
if (!CheckValidity())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags;
|
Material->ShaderFlags = shPkFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
|
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var mtrlHandle = material->MaterialResourceHandle;
|
var mtrlHandle = material->MaterialResourceHandle;
|
||||||
var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle);
|
var path = ResolveContext.GetResourceHandlePath(&mtrlHandle->ResourceHandle);
|
||||||
if (path == needle)
|
if (path == needle)
|
||||||
result.Add(new MaterialInfo(index, type, i, j));
|
result.Add(new MaterialInfo(index, type, i, j));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,6 @@ public unsafe class MetaState : IDisposable
|
||||||
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
|
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
|
||||||
{
|
{
|
||||||
var races = race.Dependencies();
|
var races = race.Dependencies();
|
||||||
if (races.Length == 0)
|
|
||||||
return DisposableContainer.Empty;
|
|
||||||
|
|
||||||
var equipmentEnumerable = equipment
|
var equipmentEnumerable = equipment
|
||||||
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))
|
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,14 @@ public unsafe class PathState : IDisposable
|
||||||
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;
|
||||||
|
|
||||||
|
if (!InInternalResolve)
|
||||||
|
{
|
||||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
_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;
|
||||||
|
|
||||||
|
if (!InInternalResolve)
|
||||||
|
{
|
||||||
_resolveData.Value = data;
|
_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),
|
||||||
|
|
|
||||||
312
Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
Normal file
312
Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
|
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, ResourceHandle* imc, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
// Safety and correctness:
|
||||||
|
// Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata.
|
||||||
|
return ModelType switch
|
||||||
|
{
|
||||||
|
ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
|
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
|
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
|
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
|
_ => ResolveMaterialPathNative(mtrlFileName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
||||||
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
|
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
||||||
|
|
||||||
|
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
var setIdHigh = Equipment.Set.Id / 100;
|
||||||
|
// All MCH (20??) weapons' materials C are one and the same
|
||||||
|
if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c')
|
||||||
|
return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty;
|
||||||
|
|
||||||
|
// MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand
|
||||||
|
if (setIdHigh is 3 or 16 or 18 or 26)
|
||||||
|
{
|
||||||
|
var setIdLow = Equipment.Set.Id % 100;
|
||||||
|
if (setIdLow > 50)
|
||||||
|
{
|
||||||
|
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
||||||
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
|
var mirroredSetId = (ushort)(Equipment.Set.Id - 50);
|
||||||
|
|
||||||
|
Span<byte> mirroredFileName = stackalloc byte[32];
|
||||||
|
mirroredFileName = mirroredFileName[..fileName.Length];
|
||||||
|
fileName.CopyTo(mirroredFileName);
|
||||||
|
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId);
|
||||||
|
|
||||||
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
|
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
|
||||||
|
|
||||||
|
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
|
||||||
|
if (weaponPosition >= 0)
|
||||||
|
WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId);
|
||||||
|
|
||||||
|
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
// TODO: Submit this (Monster->Variant) to ClientStructs
|
||||||
|
var variant = ResolveMaterialVariant(imc, ((byte*)CharacterBase.Value)[0x8F4]);
|
||||||
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
|
Span<byte> pathBuffer = stackalloc byte[260];
|
||||||
|
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
||||||
|
|
||||||
|
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant)
|
||||||
|
{
|
||||||
|
var imcFileData = imc->GetDataSpan();
|
||||||
|
if (imcFileData.IsEmpty)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"IMC resource handle with path {GetResourceHandlePath(imc, false)} doesn't have a valid data span");
|
||||||
|
return variant.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists);
|
||||||
|
if (!exists)
|
||||||
|
return variant.Id;
|
||||||
|
|
||||||
|
return entry.MaterialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant, ReadOnlySpan<byte> mtrlFileName)
|
||||||
|
{
|
||||||
|
var modelPosition = modelPath.IndexOf("/model/"u8);
|
||||||
|
if (modelPosition < 0)
|
||||||
|
return Span<byte>.Empty;
|
||||||
|
|
||||||
|
var baseDirectory = modelPath[..modelPosition];
|
||||||
|
|
||||||
|
baseDirectory.CopyTo(materialPathBuffer);
|
||||||
|
"/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]);
|
||||||
|
WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant);
|
||||||
|
materialPathBuffer[baseDirectory.Length + 15] = (byte)'/';
|
||||||
|
mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]);
|
||||||
|
|
||||||
|
return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,160 +1,189 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
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;
|
||||||
using Penumbra.Interop.Structs;
|
|
||||||
using Penumbra.String;
|
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.ModelResourceHandleUtility;
|
||||||
|
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<nint, ResourceNode> Nodes = new(128);
|
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
||||||
|
|
||||||
public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment)
|
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
||||||
=> new(Identifier, TreeBuildCache, Skeleton, WithUiData, Nodes, slot, equipment);
|
EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, WeaponType weaponType = default)
|
||||||
|
=> new(this, characterBase, slotIndex, slot, equipment, weaponType);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData,
|
internal partial record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBase> CharacterBase, uint SlotIndex,
|
||||||
Dictionary<nint, ResourceNode> Nodes, EquipSlot Slot, CharacterArmor Equipment)
|
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 ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal)
|
private unsafe ModelType ModelType
|
||||||
{
|
=> CharacterBase.Value->GetModelType();
|
||||||
if (Nodes.TryGetValue((nint)resourceHandle, out var cached))
|
|
||||||
return cached;
|
|
||||||
|
|
||||||
|
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath)
|
||||||
|
{
|
||||||
|
if (resourceHandle == null)
|
||||||
|
return null;
|
||||||
if (gamePath.IsEmpty)
|
if (gamePath.IsEmpty)
|
||||||
return null;
|
return null;
|
||||||
if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false))
|
if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal);
|
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11)
|
private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11)
|
||||||
{
|
{
|
||||||
if (Nodes.TryGetValue((nint)resourceHandle, out var cached))
|
if (resourceHandle == null)
|
||||||
return cached;
|
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];
|
||||||
{
|
|
||||||
Span<byte> prefixed = stackalloc byte[gamePath.Length + 2];
|
|
||||||
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
||||||
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
||||||
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
||||||
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
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
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(gamePath, out var path))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
|
||||||
Utf8GamePath gamePath, bool @internal)
|
|
||||||
{
|
{
|
||||||
|
// 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 path))
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||||
|
Utf8GamePath gamePath)
|
||||||
|
{
|
||||||
|
if (resourceHandle == null)
|
||||||
|
throw new ArgumentNullException(nameof(resourceHandle));
|
||||||
|
|
||||||
|
if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
return CreateNode(type, objectAddress, resourceHandle, gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||||
|
Utf8GamePath gamePath, bool autoAdd = true)
|
||||||
|
{
|
||||||
|
if (resourceHandle == null)
|
||||||
|
throw new ArgumentNullException(nameof(resourceHandle));
|
||||||
|
|
||||||
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty;
|
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty;
|
||||||
|
|
||||||
var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), @internal, this)
|
var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this)
|
||||||
{
|
{
|
||||||
GamePath = gamePath,
|
GamePath = gamePath,
|
||||||
FullPath = fullPath,
|
FullPath = fullPath,
|
||||||
};
|
};
|
||||||
if (resourceHandle != null)
|
if (autoAdd)
|
||||||
Nodes.Add((nint)resourceHandle, node);
|
Global.Nodes.Add((gamePath, (nint)resourceHandle), node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal)
|
public unsafe ResourceNode? CreateNodeFromEid(ResourceHandle* eid)
|
||||||
{
|
{
|
||||||
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
|
if (eid == null)
|
||||||
if (fullPath.InternalName.IsEmpty)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this)
|
if (!Utf8GamePath.FromByteString(ResolveEidPath(CharacterBase), out var path))
|
||||||
{
|
return null;
|
||||||
FullPath = fullPath,
|
|
||||||
};
|
return GetOrCreateNode(ResourceType.Eid, 0, eid, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
|
public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
|
||||||
{
|
{
|
||||||
if (Nodes.TryGetValue((nint)imc, out var cached))
|
if (imc == null)
|
||||||
return cached;
|
|
||||||
|
|
||||||
var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true);
|
|
||||||
if (node == null)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
Nodes.Add((nint)imc, node);
|
if (!Utf8GamePath.FromByteString(ResolveImcPath(CharacterBase, SlotIndex), out var path))
|
||||||
|
return null;
|
||||||
|
|
||||||
return node;
|
return GetOrCreateNode(ResourceType.Imc, 0, imc, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex)
|
public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
|
||||||
{
|
{
|
||||||
if (Nodes.TryGetValue((nint)tex, out var cached))
|
if (tex == null)
|
||||||
return cached;
|
return null;
|
||||||
|
|
||||||
var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false);
|
if (!Utf8GamePath.FromString(gamePath, out var path))
|
||||||
if (node != null)
|
return null;
|
||||||
Nodes.Add((nint)tex, node);
|
|
||||||
|
|
||||||
return node;
|
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl)
|
public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc)
|
||||||
{
|
{
|
||||||
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
|
if (mdl == null || mdl->ModelResourceHandle == null)
|
||||||
|
return null;
|
||||||
|
var mdlResource = mdl->ModelResourceHandle;
|
||||||
|
|
||||||
|
if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)mdlResource), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false);
|
var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdlResource->ResourceHandle, path, false);
|
||||||
if (node == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
for (var i = 0; i < mdl->MaterialCount; i++)
|
for (var i = 0; i < mdl->MaterialCount; i++)
|
||||||
{
|
{
|
||||||
var mtrl = (Material*)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, imc, mtrlFileName));
|
||||||
if (mtrlNode != null)
|
if (mtrlNode != null)
|
||||||
{
|
{
|
||||||
if (WithUiData)
|
if (Global.WithUiData)
|
||||||
mtrlNode.FallbackName = $"Material #{i}";
|
mtrlNode.FallbackName = $"Material #{i}";
|
||||||
node.Children.Add(mtrlNode);
|
node.Children.Add(mtrlNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Nodes.Add((nint)mdl->ResourceHandle, node);
|
Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node);
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|
@ -169,55 +198,55 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet<uint> alreadyVisitedSamplerIds)
|
static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet<uint> alreadyVisitedSamplerIds)
|
||||||
=> mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p)
|
=> mtrl->TexturesSpan.FindFirst(p => p.Texture == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p)
|
||||||
? p.Id
|
? p.Id
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id)
|
static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id)
|
||||||
=> new ReadOnlySpan<ShaderPackageUtility.Sampler>(shpk->Samplers, shpk->SamplerCount).FindFirst(s => s.Id == id, out var s)
|
=> shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s)
|
||||||
? s.Crc
|
? s.CRC
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (mtrl == null)
|
if (mtrl == null || mtrl->MaterialResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var resource = mtrl->ResourceHandle;
|
var resource = mtrl->MaterialResourceHandle;
|
||||||
if (Nodes.TryGetValue((nint)resource, out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false);
|
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
|
||||||
if (node == null)
|
if (node == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false);
|
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName));
|
||||||
if (shpkNode != null)
|
if (shpkNode != null)
|
||||||
{
|
{
|
||||||
if (WithUiData)
|
if (Global.WithUiData)
|
||||||
shpkNode.Name = "Shader Package";
|
shpkNode.Name = "Shader Package";
|
||||||
node.Children.Add(shpkNode);
|
node.Children.Add(shpkNode);
|
||||||
}
|
}
|
||||||
var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
|
var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
|
||||||
var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
||||||
|
|
||||||
var alreadyProcessedSamplerIds = new HashSet<uint>();
|
var alreadyProcessedSamplerIds = new HashSet<uint>();
|
||||||
for (var i = 0; i < resource->NumTex; i++)
|
for (var i = 0; i < resource->TextureCount; i++)
|
||||||
{
|
{
|
||||||
var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false,
|
var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)),
|
||||||
resource->TexIsDX11(i));
|
resource->Textures[i].IsDX11);
|
||||||
if (texNode == null)
|
if (texNode == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (WithUiData)
|
if (Global.WithUiData)
|
||||||
{
|
{
|
||||||
string? name = null;
|
string? name = null;
|
||||||
if (shpk != null)
|
if (shpk != null)
|
||||||
{
|
{
|
||||||
var index = GetTextureIndex(mtrl, resource->TexSpace[i].Flags, alreadyProcessedSamplerIds);
|
var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds);
|
||||||
uint? samplerId;
|
uint? samplerId;
|
||||||
if (index != 0x001F)
|
if (index != 0x001F)
|
||||||
samplerId = mtrl->Textures[index].Id;
|
samplerId = mtrl->Textures[index].Id;
|
||||||
else
|
else
|
||||||
samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle, alreadyProcessedSamplerIds);
|
samplerId = GetTextureSamplerId(mtrl, resource->Textures[i].TextureResourceHandle, alreadyProcessedSamplerIds);
|
||||||
if (samplerId.HasValue)
|
if (samplerId.HasValue)
|
||||||
{
|
{
|
||||||
alreadyProcessedSamplerIds.Add(samplerId.Value);
|
alreadyProcessedSamplerIds.Add(samplerId.Value);
|
||||||
|
|
@ -234,94 +263,61 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
|
||||||
node.Children.Add(texNode);
|
node.Children.Add(texNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Nodes.Add((nint)resource, node);
|
Global.Nodes.Add((path, (nint)resource), node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb)
|
public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
if (sklb->SkeletonResourceHandle == null)
|
if (sklb == null || sklb->SkeletonResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached))
|
var path = ResolveSkeletonPath(partialSkeletonIndex);
|
||||||
|
|
||||||
|
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false);
|
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
|
||||||
if (node != null)
|
if (node != null)
|
||||||
{
|
{
|
||||||
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb);
|
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
|
||||||
if (skpNode != null)
|
if (skpNode != null)
|
||||||
node.Children.Add(skpNode);
|
node.Children.Add(skpNode);
|
||||||
Nodes.Add((nint)sklb->SkeletonResourceHandle, node);
|
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb)
|
private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
if (sklb->SkeletonParameterResourceHandle == null)
|
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached))
|
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
|
||||||
|
|
||||||
|
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached))
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true);
|
var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false);
|
||||||
if (node != null)
|
if (node != null)
|
||||||
{
|
{
|
||||||
if (WithUiData)
|
if (Global.WithUiData)
|
||||||
node.FallbackName = "Skeleton Parameters";
|
node.FallbackName = "Skeleton Parameters";
|
||||||
Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node);
|
Global.Nodes.Add((path, (nint)sklb->SkeletonParameterResourceHandle), node);
|
||||||
}
|
}
|
||||||
|
|
||||||
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{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);
|
||||||
// Weapons intentionally left out.
|
// Weapons intentionally left out.
|
||||||
var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment";
|
var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment";
|
||||||
if (isEquipment)
|
if (isEquipment)
|
||||||
foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot()))
|
foreach (var item in Global.Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot()))
|
||||||
{
|
{
|
||||||
var name = Slot switch
|
var name = Slot switch
|
||||||
{
|
{
|
||||||
|
|
@ -344,7 +340,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
|
||||||
|
|
||||||
internal ResourceNode.UiData GuessUIDataFromPath(Utf8GamePath gamePath)
|
internal ResourceNode.UiData GuessUIDataFromPath(Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
foreach (var obj in Identifier.Identify(gamePath.ToString()))
|
foreach (var obj in Global.Identifier.Identify(gamePath.ToString()))
|
||||||
{
|
{
|
||||||
var name = obj.Key;
|
var name = obj.Key;
|
||||||
if (name.StartsWith("Customization:"))
|
if (name.StartsWith("Customization:"))
|
||||||
|
|
@ -362,16 +358,16 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
|
||||||
return i >= 0 && i < array.Length ? array[i] : null;
|
return i >= 0 && i < array.Length ? array[i] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle)
|
internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true)
|
||||||
{
|
{
|
||||||
if (handle == null)
|
if (handle == null)
|
||||||
return ByteString.Empty;
|
return ByteString.Empty;
|
||||||
|
|
||||||
var name = handle->FileName();
|
var name = handle->FileName.AsByteString();
|
||||||
if (name.IsEmpty)
|
if (name.IsEmpty)
|
||||||
return ByteString.Empty;
|
return ByteString.Empty;
|
||||||
|
|
||||||
if (name[0] == (byte)'|')
|
if (stripPrefix && name[0] == (byte)'|')
|
||||||
{
|
{
|
||||||
var pos = name.IndexOf((byte)'|', 1);
|
var pos = name.IndexOf((byte)'|', 1);
|
||||||
if (pos < 0)
|
if (pos < 0)
|
||||||
|
|
@ -388,6 +384,6 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
|
||||||
if (handle == null)
|
if (handle == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return ResourceHandle.GetLength(handle);
|
return handle->GetLength();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ public class ResourceNode : ICloneable
|
||||||
public Utf8GamePath[] PossibleGamePaths;
|
public Utf8GamePath[] PossibleGamePaths;
|
||||||
public FullPath FullPath;
|
public FullPath FullPath;
|
||||||
public readonly ulong Length;
|
public readonly ulong Length;
|
||||||
public readonly bool Internal;
|
|
||||||
public readonly List<ResourceNode> Children;
|
public readonly List<ResourceNode> Children;
|
||||||
internal ResolveContext? ResolveContext;
|
internal ResolveContext? ResolveContext;
|
||||||
|
|
||||||
|
|
@ -31,14 +30,16 @@ public class ResourceNode : ICloneable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, bool @internal, ResolveContext? resolveContext)
|
public bool Internal
|
||||||
|
=> Type is ResourceType.Eid or ResourceType.Imc;
|
||||||
|
|
||||||
|
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
ObjectAddress = objectAddress;
|
ObjectAddress = objectAddress;
|
||||||
ResourceHandle = resourceHandle;
|
ResourceHandle = resourceHandle;
|
||||||
PossibleGamePaths = Array.Empty<Utf8GamePath>();
|
PossibleGamePaths = Array.Empty<Utf8GamePath>();
|
||||||
Length = length;
|
Length = length;
|
||||||
Internal = @internal;
|
|
||||||
Children = new List<ResourceNode>();
|
Children = new List<ResourceNode>();
|
||||||
ResolveContext = resolveContext;
|
ResolveContext = resolveContext;
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +55,6 @@ public class ResourceNode : ICloneable
|
||||||
PossibleGamePaths = other.PossibleGamePaths;
|
PossibleGamePaths = other.PossibleGamePaths;
|
||||||
FullPath = other.FullPath;
|
FullPath = other.FullPath;
|
||||||
Length = other.Length;
|
Length = other.Length;
|
||||||
Internal = other.Internal;
|
|
||||||
Children = other.Children;
|
Children = other.Children;
|
||||||
ResolveContext = other.ResolveContext;
|
ResolveContext = other.ResolveContext;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.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;
|
||||||
|
|
||||||
|
|
@ -50,21 +53,28 @@ public class ResourceTree
|
||||||
{
|
{
|
||||||
var character = (Character*)GameObjectAddress;
|
var character = (Character*)GameObjectAddress;
|
||||||
var model = (CharacterBase*)DrawObjectAddress;
|
var model = (CharacterBase*)DrawObjectAddress;
|
||||||
var equipment = new ReadOnlySpan<CharacterArmor>(&character->DrawData.Head, 10);
|
var modelType = model->GetModelType();
|
||||||
// var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 );
|
var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null;
|
||||||
|
var equipment = modelType switch
|
||||||
|
{
|
||||||
|
CharacterBase.ModelType.Human => new ReadOnlySpan<CharacterArmor>(&human->Head, 10),
|
||||||
|
CharacterBase.ModelType.DemiHuman => new ReadOnlySpan<CharacterArmor>(&character->DrawData.Head, 10),
|
||||||
|
_ => ReadOnlySpan<CharacterArmor>.Empty,
|
||||||
|
};
|
||||||
ModelId = character->CharacterData.ModelCharaId;
|
ModelId = character->CharacterData.ModelCharaId;
|
||||||
CustomizeData = character->DrawData.CustomizeData;
|
CustomizeData = character->DrawData.CustomizeData;
|
||||||
RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace)((Human*)model)->RaceSexId : GenderRace.Unknown;
|
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
||||||
|
|
||||||
|
var genericContext = globalContext.CreateContext(model);
|
||||||
|
|
||||||
for (var i = 0; i < model->SlotCount; ++i)
|
for (var i = 0; i < model->SlotCount; ++i)
|
||||||
{
|
{
|
||||||
var context = globalContext.CreateContext(
|
var slotContext = i < equipment.Length
|
||||||
i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown,
|
? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i])
|
||||||
i < equipment.Length ? equipment[i] : default
|
: globalContext.CreateContext(model, (uint)i);
|
||||||
);
|
|
||||||
|
|
||||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||||
var imcNode = context.CreateNodeFromImc(imc);
|
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||||
if (imcNode != null)
|
if (imcNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
|
|
@ -72,8 +82,8 @@ public class ResourceTree
|
||||||
Nodes.Add(imcNode);
|
Nodes.Add(imcNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdl = (RenderModel*)model->Models[i];
|
var mdl = model->Models[i];
|
||||||
var mdlNode = context.CreateNodeFromRenderModel(mdl);
|
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
||||||
if (mdlNode != null)
|
if (mdlNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
|
|
@ -82,62 +92,74 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton);
|
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton);
|
||||||
|
|
||||||
if (model->GetModelType() == CharacterBase.ModelType.Human)
|
AddWeapons(globalContext, model);
|
||||||
AddHumanResources(globalContext, (HumanExt*)model);
|
|
||||||
|
if (human != null)
|
||||||
|
AddHumanResources(globalContext, human);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human)
|
private unsafe void AddWeapons(GlobalResolveContext globalContext, CharacterBase* model)
|
||||||
{
|
{
|
||||||
var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject;
|
var weaponIndex = 0;
|
||||||
if (firstSubObject != null)
|
var weaponNodes = new List<ResourceNode>();
|
||||||
|
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
||||||
{
|
{
|
||||||
var subObjectNodes = new List<ResourceNode>();
|
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
||||||
var subObject = firstSubObject;
|
continue;
|
||||||
var subObjectIndex = 0;
|
var subObject = (CharacterBase*)baseSubObject;
|
||||||
do
|
|
||||||
{
|
if (subObject->GetModelType() != CharacterBase.ModelType.Weapon)
|
||||||
var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null;
|
continue;
|
||||||
var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc.";
|
var weapon = (Weapon*)subObject;
|
||||||
var subObjectContext = globalContext.CreateContext(
|
|
||||||
weapon != null ? EquipSlot.MainHand : EquipSlot.Unknown,
|
// This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it.
|
||||||
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, weaponType);
|
||||||
|
|
||||||
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, weaponType);
|
||||||
|
|
||||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||||
var imcNode = subObjectContext.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 = (RenderModel*)subObject->Models[i];
|
var mdl = subObject->Models[i];
|
||||||
var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl);
|
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
||||||
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, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
|
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, ");
|
||||||
|
|
||||||
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
|
++weaponIndex;
|
||||||
++subObjectIndex;
|
}
|
||||||
} while (subObject != null && subObject != firstSubObject);
|
Nodes.InsertRange(0, weaponNodes);
|
||||||
|
|
||||||
Nodes.InsertRange(0, subObjectNodes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var context = globalContext.CreateContext(EquipSlot.Unknown, default);
|
private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human)
|
||||||
|
{
|
||||||
|
var genericContext = globalContext.CreateContext(&human->CharacterBase);
|
||||||
|
|
||||||
var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal);
|
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
|
||||||
|
var decalPath = decalId != 0
|
||||||
|
? GamePaths.Human.Decal.FaceDecalPath(decalId)
|
||||||
|
: GamePaths.Tex.TransparentPath;
|
||||||
|
var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath);
|
||||||
if (decalNode != null)
|
if (decalNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
|
|
@ -149,7 +171,11 @@ public class ResourceTree
|
||||||
Nodes.Add(decalNode);
|
Nodes.Add(decalNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal);
|
var hasLegacyDecal = (human->Customize[(int)CustomizeIndex.FaceFeatures] & 0x80) != 0;
|
||||||
|
var legacyDecalPath = hasLegacyDecal
|
||||||
|
? GamePaths.Human.Decal.LegacyDecalPath
|
||||||
|
: GamePaths.Tex.TransparentPath;
|
||||||
|
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
|
||||||
if (legacyDecalNode != null)
|
if (legacyDecalNode != null)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
|
|
@ -162,17 +188,25 @@ 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;
|
||||||
|
|
||||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||||
{
|
{
|
||||||
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]);
|
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i);
|
||||||
if (sklbNode != null)
|
if (sklbNode != null)
|
||||||
{
|
{
|
||||||
if (context.WithUiData)
|
if (context.Global.WithUiData)
|
||||||
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
||||||
nodes.Add(sklbNode);
|
nodes.Add(sklbNode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
using (var _ = _pathState.EnterInternalResolve())
|
||||||
|
{
|
||||||
tree.LoadResources(globalContext);
|
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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
using Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.SafeHandles;
|
namespace Penumbra.Interop.SafeHandles;
|
||||||
|
|
||||||
|
|
@ -18,7 +17,7 @@ public unsafe class SafeTextureHandle : SafeHandle
|
||||||
throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported");
|
throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported");
|
||||||
|
|
||||||
if (incRef && handle != null)
|
if (incRef && handle != null)
|
||||||
TextureUtility.IncRef(handle);
|
handle->IncRef();
|
||||||
SetHandle((nint)handle);
|
SetHandle((nint)handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +42,7 @@ public unsafe class SafeTextureHandle : SafeHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handle != 0)
|
if (handle != 0)
|
||||||
TextureUtility.DecRef((Texture*)handle);
|
((Texture*)handle)->DecRef();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using Dalamud.Hooking;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
|
|
@ -73,19 +74,19 @@ public sealed unsafe class SkinFixer : IDisposable
|
||||||
public ulong GetAndResetSlowPathCallDelta()
|
public ulong GetAndResetSlowPathCallDelta()
|
||||||
=> Interlocked.Exchange(ref _slowPathCallDelta, 0);
|
=> Interlocked.Exchange(ref _slowPathCallDelta, 0);
|
||||||
|
|
||||||
private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource)
|
private static bool IsSkinMaterial(MaterialResourceHandle* mtrlResource)
|
||||||
{
|
{
|
||||||
if (mtrlResource == null)
|
if (mtrlResource == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString);
|
var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkName);
|
||||||
return SkinShpkName.SequenceEqual(shpkName);
|
return SkinShpkName.SequenceEqual(shpkName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
|
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
|
||||||
{
|
{
|
||||||
var mtrl = (Structs.MtrlResource*)mtrlResourceHandle;
|
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
|
||||||
var shpk = mtrl->ShpkResourceHandle;
|
var shpk = mtrl->ShaderPackageResourceHandle;
|
||||||
if (shpk == null)
|
if (shpk == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -109,7 +110,7 @@ public sealed unsafe class SkinFixer : IDisposable
|
||||||
return _onRenderMaterialHook.Original(human, param);
|
return _onRenderMaterialHook.Original(human, param);
|
||||||
|
|
||||||
var material = param->Model->Materials[param->MaterialIndex];
|
var material = param->Model->Materials[param->MaterialIndex];
|
||||||
var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle;
|
var mtrlResource = material->MaterialResourceHandle;
|
||||||
if (!IsSkinMaterial(mtrlResource))
|
if (!IsSkinMaterial(mtrlResource))
|
||||||
return _onRenderMaterialHook.Original(human, param);
|
return _onRenderMaterialHook.Original(human, param);
|
||||||
|
|
||||||
|
|
@ -124,7 +125,7 @@ public sealed unsafe class SkinFixer : IDisposable
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShpkResourceHandle;
|
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle;
|
||||||
return _onRenderMaterialHook.Original(human, param);
|
return _onRenderMaterialHook.Original(human, param);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
|
||||||
public unsafe struct CharacterBaseExt
|
|
||||||
{
|
|
||||||
[FieldOffset(0x0)]
|
|
||||||
public CharacterBase CharacterBase;
|
|
||||||
|
|
||||||
[FieldOffset(0x258)]
|
|
||||||
public Texture** ColorTableTextures;
|
|
||||||
}
|
|
||||||
62
Penumbra/Interop/Structs/CharacterBaseUtility.cs
Normal file
62
Penumbra/Interop/Structs/CharacterBaseUtility.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
// TODO submit these to ClientStructs
|
||||||
|
public static unsafe class CharacterBaseUtility
|
||||||
|
{
|
||||||
|
private const int PathBufferSize = 260;
|
||||||
|
|
||||||
|
private const uint ResolveSklbPathVf = 72;
|
||||||
|
private const uint ResolveMdlPathVf = 73;
|
||||||
|
private const uint ResolveSkpPathVf = 74;
|
||||||
|
private const uint ResolveImcPathVf = 81;
|
||||||
|
private const uint ResolveMtrlPathVf = 82;
|
||||||
|
private const uint ResolveEidPathVf = 85;
|
||||||
|
|
||||||
|
private static void* GetVFunc(CharacterBase* characterBase, uint vfIndex)
|
||||||
|
=> ((void**)characterBase->VTable)[vfIndex];
|
||||||
|
|
||||||
|
private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex)
|
||||||
|
{
|
||||||
|
var vFunc = (delegate* unmanaged<CharacterBase*, byte*, nint, byte*>)GetVFunc(characterBase, vfIndex);
|
||||||
|
var pathBuffer = stackalloc byte[PathBufferSize];
|
||||||
|
var path = vFunc(characterBase, pathBuffer, PathBufferSize);
|
||||||
|
return path != null ? new ByteString(path).Clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex)
|
||||||
|
{
|
||||||
|
var vFunc = (delegate* unmanaged<CharacterBase*, byte*, nint, uint, byte*>)GetVFunc(characterBase, vfIndex);
|
||||||
|
var pathBuffer = stackalloc byte[PathBufferSize];
|
||||||
|
var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex);
|
||||||
|
return path != null ? new ByteString(path).Clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex, byte* name)
|
||||||
|
{
|
||||||
|
var vFunc = (delegate* unmanaged<CharacterBase*, byte*, nint, uint, byte*, byte*>)GetVFunc(characterBase, vfIndex);
|
||||||
|
var pathBuffer = stackalloc byte[PathBufferSize];
|
||||||
|
var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex, name);
|
||||||
|
return path != null ? new ByteString(path).Clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteString? ResolveEidPath(CharacterBase* characterBase)
|
||||||
|
=> ResolvePath(characterBase, ResolveEidPathVf);
|
||||||
|
|
||||||
|
public static ByteString? ResolveImcPath(CharacterBase* characterBase, uint slotIndex)
|
||||||
|
=> ResolvePath(characterBase, ResolveImcPathVf, slotIndex);
|
||||||
|
|
||||||
|
public static ByteString? ResolveMdlPath(CharacterBase* characterBase, uint slotIndex)
|
||||||
|
=> ResolvePath(characterBase, ResolveMdlPathVf, slotIndex);
|
||||||
|
|
||||||
|
public static ByteString? ResolveMtrlPath(CharacterBase* characterBase, uint slotIndex, byte* mtrlFileName)
|
||||||
|
=> ResolvePath(characterBase, ResolveMtrlPathVf, slotIndex, mtrlFileName);
|
||||||
|
|
||||||
|
public static ByteString? ResolveSklbPath(CharacterBase* characterBase, uint partialSkeletonIndex)
|
||||||
|
=> ResolvePath(characterBase, ResolveSklbPathVf, partialSkeletonIndex);
|
||||||
|
|
||||||
|
public static ByteString? ResolveSkpPath(CharacterBase* characterBase, uint partialSkeletonIndex)
|
||||||
|
=> ResolvePath(characterBase, ResolveSkpPathVf, partialSkeletonIndex);
|
||||||
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit, Size = 0x70)]
|
|
||||||
public unsafe struct ConstantBuffer
|
|
||||||
{
|
|
||||||
[FieldOffset(0x20)]
|
|
||||||
public int Size;
|
|
||||||
|
|
||||||
[FieldOffset(0x24)]
|
|
||||||
public int Flags;
|
|
||||||
|
|
||||||
[FieldOffset(0x28)]
|
|
||||||
private void* _maybeSourcePointer;
|
|
||||||
|
|
||||||
public bool TryGetBuffer(out Span<float> buffer)
|
|
||||||
{
|
|
||||||
if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null)
|
|
||||||
{
|
|
||||||
buffer = new Span<float>(_maybeSourcePointer, Size >> 2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buffer = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
|
||||||
public unsafe struct HumanExt
|
|
||||||
{
|
|
||||||
[FieldOffset(0x0)]
|
|
||||||
public Human Human;
|
|
||||||
|
|
||||||
[FieldOffset(0x0)]
|
|
||||||
public CharacterBaseExt CharacterBase;
|
|
||||||
|
|
||||||
[FieldOffset(0x9E8)]
|
|
||||||
public ResourceHandle* Decal;
|
|
||||||
|
|
||||||
[FieldOffset(0x9F0)]
|
|
||||||
public ResourceHandle* LegacyBodyDecal;
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit, Size = 0x40)]
|
|
||||||
public unsafe struct Material
|
|
||||||
{
|
|
||||||
[FieldOffset(0x10)]
|
|
||||||
public MtrlResource* ResourceHandle;
|
|
||||||
|
|
||||||
[FieldOffset(0x18)]
|
|
||||||
public uint ShaderPackageFlags;
|
|
||||||
|
|
||||||
[FieldOffset(0x20)]
|
|
||||||
public uint* ShaderKeys;
|
|
||||||
|
|
||||||
public int ShaderKeyCount
|
|
||||||
=> (int)((uint*)Textures - ShaderKeys);
|
|
||||||
|
|
||||||
[FieldOffset(0x28)]
|
|
||||||
public ConstantBuffer* MaterialParameter;
|
|
||||||
|
|
||||||
[FieldOffset(0x30)]
|
|
||||||
public TextureEntry* Textures;
|
|
||||||
|
|
||||||
[FieldOffset(0x38)]
|
|
||||||
public ushort TextureCount;
|
|
||||||
|
|
||||||
public Texture* Texture(int index)
|
|
||||||
=> Textures[index].ResourceHandle->KernelTexture;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
|
|
||||||
public struct TextureEntry
|
|
||||||
{
|
|
||||||
[FieldOffset(0x00)]
|
|
||||||
public uint Id;
|
|
||||||
|
|
||||||
[FieldOffset(0x08)]
|
|
||||||
public TextureResourceHandle* ResourceHandle;
|
|
||||||
|
|
||||||
[FieldOffset(0x10)]
|
|
||||||
public uint SamplerFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlySpan<TextureEntry> TextureSpan
|
|
||||||
=> new(Textures, TextureCount);
|
|
||||||
}
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
|
||||||
public unsafe struct MtrlResource
|
|
||||||
{
|
|
||||||
[FieldOffset(0x00)]
|
|
||||||
public ResourceHandle Handle;
|
|
||||||
|
|
||||||
[FieldOffset(0xC8)]
|
|
||||||
public ShaderPackageResourceHandle* ShpkResourceHandle;
|
|
||||||
|
|
||||||
[FieldOffset(0xD0)]
|
|
||||||
public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list.
|
|
||||||
|
|
||||||
[FieldOffset(0xE0)]
|
|
||||||
public byte* StringList;
|
|
||||||
|
|
||||||
[FieldOffset(0xF8)]
|
|
||||||
public ushort ShpkOffset;
|
|
||||||
|
|
||||||
[FieldOffset(0xFA)]
|
|
||||||
public byte NumTex;
|
|
||||||
|
|
||||||
public byte* ShpkString
|
|
||||||
=> StringList + ShpkOffset;
|
|
||||||
|
|
||||||
public byte* TexString(int idx)
|
|
||||||
=> StringList + TexSpace[idx].PathOffset;
|
|
||||||
|
|
||||||
public bool TexIsDX11(int idx)
|
|
||||||
=> TexSpace[idx].Flags >= 0x8000;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
|
|
||||||
public struct TextureEntry
|
|
||||||
{
|
|
||||||
[FieldOffset(0x00)]
|
|
||||||
public TextureResourceHandle* ResourceHandle;
|
|
||||||
|
|
||||||
[FieldOffset(0x08)]
|
|
||||||
public ushort PathOffset;
|
|
||||||
|
|
||||||
[FieldOffset(0x0A)]
|
|
||||||
public ushort Flags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,9 @@ namespace Penumbra.Interop.Structs;
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
public unsafe struct RenderModel
|
public unsafe struct RenderModel
|
||||||
{
|
{
|
||||||
|
[FieldOffset(0)]
|
||||||
|
public Model Model;
|
||||||
|
|
||||||
[FieldOffset(0x18)]
|
[FieldOffset(0x18)]
|
||||||
public RenderModel* PreviousModel;
|
public RenderModel* PreviousModel;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData;
|
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
using CsHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
namespace Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
|
@ -14,24 +12,8 @@ public unsafe struct TextureResourceHandle
|
||||||
[FieldOffset(0x0)]
|
[FieldOffset(0x0)]
|
||||||
public ResourceHandle Handle;
|
public ResourceHandle Handle;
|
||||||
|
|
||||||
[FieldOffset(0x38)]
|
|
||||||
public IntPtr Unk;
|
|
||||||
|
|
||||||
[FieldOffset(0x118)]
|
|
||||||
public Texture* KernelTexture;
|
|
||||||
|
|
||||||
[FieldOffset(0x20)]
|
|
||||||
public IntPtr NewKernelTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
|
||||||
public unsafe struct ShaderPackageResourceHandle
|
|
||||||
{
|
|
||||||
[FieldOffset(0x0)]
|
[FieldOffset(0x0)]
|
||||||
public ResourceHandle Handle;
|
public CsHandle.TextureResourceHandle CsHandle;
|
||||||
|
|
||||||
[FieldOffset(0xB0)]
|
|
||||||
public ShaderPackage* ShaderPackage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LoadState : byte
|
public enum LoadState : byte
|
||||||
|
|
@ -59,27 +41,14 @@ public unsafe struct ResourceHandle
|
||||||
public ulong DataLength;
|
public ulong DataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public const int SsoSize = 15;
|
public readonly ByteString FileName()
|
||||||
|
=> CsHandle.FileName.AsByteString();
|
||||||
|
|
||||||
public byte* FileNamePtr()
|
public readonly bool GamePath(out Utf8GamePath path)
|
||||||
{
|
=> Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), out path);
|
||||||
if (FileNameLength > SsoSize)
|
|
||||||
return FileNameData;
|
|
||||||
|
|
||||||
fixed (byte** name = &FileNameData)
|
[FieldOffset(0x00)]
|
||||||
{
|
public CsHandle.ResourceHandle CsHandle;
|
||||||
return (byte*)name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ByteString FileName()
|
|
||||||
=> ByteString.FromByteStringUnsafe(FileNamePtr(), FileNameLength, true);
|
|
||||||
|
|
||||||
public ReadOnlySpan<byte> FileNameAsSpan()
|
|
||||||
=> new(FileNamePtr(), FileNameLength);
|
|
||||||
|
|
||||||
public bool GamePath(out Utf8GamePath path)
|
|
||||||
=> Utf8GamePath.FromSpan(FileNameAsSpan(), out path);
|
|
||||||
|
|
||||||
[FieldOffset(0x00)]
|
[FieldOffset(0x00)]
|
||||||
public void** VTable;
|
public void** VTable;
|
||||||
|
|
@ -90,18 +59,9 @@ public unsafe struct ResourceHandle
|
||||||
[FieldOffset(0x0C)]
|
[FieldOffset(0x0C)]
|
||||||
public ResourceType FileType;
|
public ResourceType FileType;
|
||||||
|
|
||||||
[FieldOffset(0x10)]
|
|
||||||
public uint Id;
|
|
||||||
|
|
||||||
[FieldOffset(0x28)]
|
[FieldOffset(0x28)]
|
||||||
public uint FileSize;
|
public uint FileSize;
|
||||||
|
|
||||||
[FieldOffset(0x2C)]
|
|
||||||
public uint FileSize2;
|
|
||||||
|
|
||||||
[FieldOffset(0x34)]
|
|
||||||
public uint FileSize3;
|
|
||||||
|
|
||||||
[FieldOffset(0x48)]
|
[FieldOffset(0x48)]
|
||||||
public byte* FileNameData;
|
public byte* FileNameData;
|
||||||
|
|
||||||
|
|
@ -114,13 +74,6 @@ public unsafe struct ResourceHandle
|
||||||
[FieldOffset(0xAC)]
|
[FieldOffset(0xAC)]
|
||||||
public uint RefCount;
|
public uint RefCount;
|
||||||
|
|
||||||
// May return null.
|
|
||||||
public static byte* GetData(ResourceHandle* handle)
|
|
||||||
=> ((delegate* unmanaged< ResourceHandle*, byte* >)handle->VTable[Offsets.ResourceHandleGetDataVfunc])(handle);
|
|
||||||
|
|
||||||
public static ulong GetLength(ResourceHandle* handle)
|
|
||||||
=> ((delegate* unmanaged< ResourceHandle*, ulong >)handle->VTable[Offsets.ResourceHandleGetLengthVfunc])(handle);
|
|
||||||
|
|
||||||
|
|
||||||
// Only use these if you know what you are doing.
|
// Only use these if you know what you are doing.
|
||||||
// Those are actually only sure to be accessible for DefaultResourceHandles.
|
// Those are actually only sure to be accessible for DefaultResourceHandles.
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
public static class ShaderPackageUtility
|
|
||||||
{
|
|
||||||
[StructLayout(LayoutKind.Explicit, Size = 0xC)]
|
|
||||||
public unsafe struct Sampler
|
|
||||||
{
|
|
||||||
[FieldOffset(0x0)]
|
|
||||||
public uint Crc;
|
|
||||||
|
|
||||||
[FieldOffset(0x4)]
|
|
||||||
public uint Id;
|
|
||||||
|
|
||||||
[FieldOffset(0xA)]
|
|
||||||
public ushort Slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
Penumbra/Interop/Structs/StructExtensions.cs
Normal file
24
Penumbra/Interop/Structs/StructExtensions.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
using FFXIVClientStructs.STD;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
internal static class StructExtensions
|
||||||
|
{
|
||||||
|
// TODO submit this to ClientStructs
|
||||||
|
public static unsafe ReadOnlySpan<byte> AsSpan(in this StdString str)
|
||||||
|
{
|
||||||
|
if (str.Length < 16)
|
||||||
|
{
|
||||||
|
fixed (StdString* pStr = &str)
|
||||||
|
{
|
||||||
|
return new(pStr->Buffer, (int)str.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return new(str.BufferPtr, (int)str.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe ByteString AsByteString(in this StdString str)
|
||||||
|
=> ByteString.FromSpanUnsafe(str.AsSpan(), true);
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
public unsafe class TextureUtility
|
|
||||||
{
|
|
||||||
public TextureUtility(IGameInteropProvider interop)
|
|
||||||
=> interop.InitializeFromAttributes(this);
|
|
||||||
|
|
||||||
|
|
||||||
[Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")]
|
|
||||||
private static nint _textureCreate2D = nint.Zero;
|
|
||||||
|
|
||||||
[Signature("E9 ?? ?? ?? ?? 8B 02 25")]
|
|
||||||
private static nint _textureInitializeContents = nint.Zero;
|
|
||||||
|
|
||||||
public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk)
|
|
||||||
=> ((delegate* unmanaged<Device*, int*, byte, uint, uint, uint, Texture*>)_textureCreate2D)(device, size, mipLevel, textureFormat,
|
|
||||||
flags, unk);
|
|
||||||
|
|
||||||
public static bool InitializeContents(Texture* texture, void* contents)
|
|
||||||
=> ((delegate* unmanaged<Texture*, void*, bool>)_textureInitializeContents)(texture, contents);
|
|
||||||
|
|
||||||
public static void IncRef(Texture* texture)
|
|
||||||
=> ((delegate* unmanaged<Texture*, void>)(*(void***)texture)[2])(texture);
|
|
||||||
|
|
||||||
public static void DecRef(Texture* texture)
|
|
||||||
=> ((delegate* unmanaged<Texture*, void>)(*(void***)texture)[3])(texture);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -161,11 +168,19 @@ public unsafe class ImcFile : MetaBaseFile
|
||||||
if (file == null)
|
if (file == null)
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
|
|
||||||
fixed (byte* ptr = file.Data)
|
return GetEntry(file.Data, slot, variantIdx, out exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImcEntry GetEntry(ReadOnlySpan<byte> imcFileData, EquipSlot slot, Variant variantIdx, out bool exists)
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = imcFileData)
|
||||||
{
|
{
|
||||||
var entry = VariantPtr(ptr, PartIndex(slot), variantIdx);
|
var entry = VariantPtr(ptr, PartIndex(slot), variantIdx);
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
|
{
|
||||||
|
exists = false;
|
||||||
return new ImcEntry();
|
return new ImcEntry();
|
||||||
|
}
|
||||||
|
|
||||||
exists = true;
|
exists = true;
|
||||||
return *entry;
|
return *entry;
|
||||||
|
|
|
||||||
|
|
@ -76,7 +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<TextureUtility>(); // 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))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ public static class ServiceManager
|
||||||
.AddSingleton<ResidentResourceManager>()
|
.AddSingleton<ResidentResourceManager>()
|
||||||
.AddSingleton<FontReloader>()
|
.AddSingleton<FontReloader>()
|
||||||
.AddSingleton<RedrawService>()
|
.AddSingleton<RedrawService>()
|
||||||
.AddSingleton<TextureUtility>();
|
.AddSingleton<ModelResourceHandleUtility>();
|
||||||
|
|
||||||
private static IServiceCollection AddConfiguration(this IServiceCollection services)
|
private static IServiceCollection AddConfiguration(this IServiceCollection services)
|
||||||
=> services.AddTransient<ConfigMigrationService>()
|
=> services.AddTransient<ConfigMigrationService>()
|
||||||
|
|
|
||||||
|
|
@ -713,8 +713,8 @@ public class DebugTab : Window, ITab
|
||||||
|
|
||||||
UiHelpers.Text(resource);
|
UiHelpers.Text(resource);
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
var data = (nint)ResourceHandle.GetData(resource);
|
var data = (nint)resource->CsHandle.GetData();
|
||||||
var length = ResourceHandle.GetLength(resource);
|
var length = resource->CsHandle.GetLength();
|
||||||
if (ImGui.Selectable($"0x{data:X}"))
|
if (ImGui.Selectable($"0x{data:X}"))
|
||||||
if (data != nint.Zero && length > 0)
|
if (data != nint.Zero && length > 0)
|
||||||
ImGui.SetClipboardText(string.Join("\n",
|
ImGui.SetClipboardText(string.Join("\n",
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,10 @@ public class ResourceTab : ITab
|
||||||
UiHelpers.Text(resource);
|
UiHelpers.Text(resource);
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
{
|
{
|
||||||
var data = Interop.Structs.ResourceHandle.GetData(resource);
|
var data = resource->CsHandle.GetData();
|
||||||
if (data != null)
|
if (data != null)
|
||||||
{
|
{
|
||||||
var length = (int)Interop.Structs.ResourceHandle.GetLength(resource);
|
var length = (int)resource->CsHandle.GetLength();
|
||||||
ImGui.SetClipboardText(string.Join(" ",
|
ImGui.SetClipboardText(string.Join(" ",
|
||||||
new ReadOnlySpan<byte>(data, length).ToArray().Select(b => b.ToString("X2"))));
|
new ReadOnlySpan<byte>(data, length).ToArray().Select(b => b.ToString("X2"))));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,18 @@ public static class UiHelpers
|
||||||
public static unsafe void Text(byte* s, int length)
|
public static unsafe void Text(byte* s, int length)
|
||||||
=> ImGuiNative.igTextUnformatted(s, s + length);
|
=> ImGuiNative.igTextUnformatted(s, s + length);
|
||||||
|
|
||||||
|
/// <summary> Draw text given by a byte span. </summary>
|
||||||
|
public static unsafe void Text(ReadOnlySpan<byte> s)
|
||||||
|
{
|
||||||
|
fixed (byte* pS = s)
|
||||||
|
{
|
||||||
|
Text(pS, s.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> Draw the name of a resource file. </summary>
|
/// <summary> Draw the name of a resource file. </summary>
|
||||||
public static unsafe void Text(ResourceHandle* resource)
|
public static unsafe void Text(ResourceHandle* resource)
|
||||||
=> Text(resource->FileName().Path, resource->FileNameLength);
|
=> Text(resource->CsHandle.FileName.AsSpan());
|
||||||
|
|
||||||
/// <summary> Draw a ByteString as a selectable. </summary>
|
/// <summary> Draw a ByteString as a selectable. </summary>
|
||||||
public static unsafe bool Selectable(ByteString s, bool selected)
|
public static unsafe bool Selectable(ByteString s, bool selected)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue