Merge branch 'rt-dt'

This commit is contained in:
Ottermandias 2024-08-04 00:00:49 +02:00
commit a6ee4c96ea
3 changed files with 77 additions and 54 deletions

View file

@ -1,5 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Text.HelperObjects;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -8,26 +9,33 @@ using Penumbra.Meta.Manipulations;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using static Penumbra.Interop.Structs.StructExtensions; using static Penumbra.Interop.Structs.StructExtensions;
using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
internal partial record ResolveContext internal partial record ResolveContext
{ {
private static bool IsEquipmentOrAccessorySlot(uint slotIndex)
=> slotIndex is < 10 or 16 or 17;
private static bool IsEquipmentSlot(uint slotIndex)
=> slotIndex is < 5 or 16 or 17;
private Utf8GamePath ResolveModelPath() private Utf8GamePath ResolveModelPath()
{ {
// Correctness: // Correctness:
// Resolving a model path through the game's code can use EQDP metadata for human equipment models. // Resolving a model path through the game's code can use EQDP metadata for human equipment models.
return ModelType switch return ModelType switch
{ {
ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(), ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) => ResolveEquipmentModelPath(),
_ => ResolveModelPathNative(), _ => ResolveModelPathNative(),
}; };
} }
private Utf8GamePath ResolveEquipmentModelPath() private Utf8GamePath ResolveEquipmentModelPath()
{ {
var path = SlotIndex < 5 var path = IsEquipmentSlot(SlotIndex)
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot)
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
@ -39,7 +47,7 @@ internal partial record ResolveContext
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId)
{ {
var slotIndex = slot.ToIndex(); var slotIndex = slot.ToIndex();
if (slotIndex >= 10 || ModelType != ModelType.Human) if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human)
return GenderRace.MidlanderMale; return GenderRace.MidlanderMale;
var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId; var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId;
@ -80,7 +88,7 @@ internal partial record ResolveContext
// Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata.
return ModelType switch return ModelType switch
{ {
ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) && mtrlFileName[8] != (byte)'b'
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
@ -95,7 +103,7 @@ internal partial record ResolveContext
var variant = ResolveMaterialVariant(imc, Equipment.Variant); var variant = ResolveMaterialVariant(imc, Equipment.Variant);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> pathBuffer = stackalloc byte[260]; Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
@ -109,32 +117,26 @@ internal partial record ResolveContext
if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') 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; 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 // Some offhands share materials with the corresponding mainhand
if (setIdHigh is 3 or 16 or 18 or 26) if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId))
{
var setIdLow = Equipment.Set.Id % 100;
if (setIdLow > 50)
{ {
var variant = ResolveMaterialVariant(imc, Equipment.Variant); var variant = ResolveMaterialVariant(imc, Equipment.Variant);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
var mirroredSetId = (ushort)(Equipment.Set.Id - 50);
Span<byte> mirroredFileName = stackalloc byte[32]; Span<byte> mirroredFileName = stackalloc byte[32];
mirroredFileName = mirroredFileName[..fileName.Length]; mirroredFileName = mirroredFileName[..fileName.Length];
fileName.CopyTo(mirroredFileName); fileName.CopyTo(mirroredFileName);
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId.Id);
Span<byte> pathBuffer = stackalloc byte[260]; Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
if (weaponPosition >= 0) if (weaponPosition >= 0)
WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId.Id);
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
} }
}
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
} }
@ -144,7 +146,7 @@ internal partial record ResolveContext
var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> pathBuffer = stackalloc byte[260]; Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
@ -175,13 +177,21 @@ internal partial record ResolveContext
var baseDirectory = modelPath[..modelPosition]; var baseDirectory = modelPath[..modelPosition];
baseDirectory.CopyTo(materialPathBuffer); var writer = new SpanTextWriter(materialPathBuffer);
"/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]); writer.Append(baseDirectory);
WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant); writer.Append("/material/v"u8);
materialPathBuffer[baseDirectory.Length + 15] = (byte)'/'; WriteZeroPaddedNumber(ref writer, 4, variant);
mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]); writer.Append((byte)'/');
writer.Append(mtrlFileName);
writer.EnsureNullTerminated();
return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)]; return materialPathBuffer[..writer.Position];
}
private static void WriteZeroPaddedNumber(ref SpanTextWriter writer, int width, ushort number)
{
WriteZeroPaddedNumber(writer.GetRemainingSpan()[..width], number);
writer.Advance(width);
} }
private static void WriteZeroPaddedNumber(Span<byte> destination, ushort number) private static void WriteZeroPaddedNumber(Span<byte> destination, ushort number)

View file

@ -1,9 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop; using FFXIVClientStructs.Interop;
using OtterGui; using OtterGui;
using OtterGui.Text.HelperObjects;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
@ -16,7 +16,7 @@ using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI; using Penumbra.UI;
using static Penumbra.Interop.Structs.StructExtensions; using static Penumbra.Interop.Structs.StructExtensions;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
@ -29,25 +29,25 @@ internal record GlobalResolveContext(
{ {
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default)
=> new(this, characterBase, slotIndex, slot, equipment, secondaryId); => new(this, characterBase, slotIndex, slot, equipment, secondaryId);
} }
internal unsafe partial record ResolveContext( internal unsafe partial record ResolveContext(
GlobalResolveContext Global, GlobalResolveContext Global,
Pointer<CharacterBase> CharacterBasePointer, Pointer<CharaBase> CharacterBasePointer,
uint SlotIndex, uint SlotIndex,
EquipSlot Slot, EquipSlot Slot,
CharacterArmor Equipment, CharacterArmor Equipment,
SecondaryId SecondaryId) SecondaryId SecondaryId)
{ {
public CharacterBase* CharacterBase public CharaBase* CharacterBase
=> CharacterBasePointer.Value; => CharacterBasePointer.Value;
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
private ModelType ModelType private CharaBase.ModelType ModelType
=> CharacterBase->GetModelType(); => CharacterBase->GetModelType();
private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath)
@ -75,11 +75,14 @@ internal unsafe partial record ResolveContext(
if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3) if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3)
return null; return null;
Span<byte> prefixed = stackalloc byte[260]; Span<byte> prefixed = stackalloc byte[CharaBase.PathBufferSize];
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
prefixed[lastDirectorySeparator + 1] = (byte)'-'; var writer = new SpanTextWriter(prefixed);
prefixed[lastDirectorySeparator + 2] = (byte)'-'; writer.Append(gamePath.Span[..(lastDirectorySeparator + 1)]);
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); writer.Append((byte)'-');
writer.Append((byte)'-');
writer.Append(gamePath.Span[(lastDirectorySeparator + 1)..]);
writer.EnsureNullTerminated();
if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp))
return null; return null;

View file

@ -9,6 +9,7 @@ using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.UI; using Penumbra.UI;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
@ -44,8 +45,8 @@ public class ResourceTree
PlayerRelated = playerRelated; PlayerRelated = playerRelated;
CollectionName = collectionName; CollectionName = collectionName;
AnonymizedCollectionName = anonymizedCollectionName; AnonymizedCollectionName = anonymizedCollectionName;
Nodes = new List<ResourceNode>(); Nodes = [];
FlatNodes = new HashSet<ResourceNode>(); FlatNodes = [];
} }
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action) public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action)
@ -59,13 +60,13 @@ public class ResourceTree
var character = (Character*)GameObjectAddress; var character = (Character*)GameObjectAddress;
var model = (CharacterBase*)DrawObjectAddress; var model = (CharacterBase*)DrawObjectAddress;
var modelType = model->GetModelType(); var modelType = model->GetModelType();
var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; var human = modelType == ModelType.Human ? (Human*)model : null;
var equipment = modelType switch var equipment = modelType switch
{ {
CharacterBase.ModelType.Human => new ReadOnlySpan<CharacterArmor>(&human->Head, 10), ModelType.Human => new ReadOnlySpan<CharacterArmor>(&human->Head, 12),
CharacterBase.ModelType.DemiHuman => new ReadOnlySpan<CharacterArmor>( ModelType.DemiHuman => new ReadOnlySpan<CharacterArmor>(
Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10),
_ => ReadOnlySpan<CharacterArmor>.Empty, _ => [],
}; };
ModelId = character->CharacterData.ModelCharaId; ModelId = character->CharacterData.ModelCharaId;
CustomizeData = character->DrawData.CustomizeData; CustomizeData = character->DrawData.CustomizeData;
@ -75,9 +76,18 @@ public class ResourceTree
for (var i = 0u; i < model->SlotCount; ++i) for (var i = 0u; i < model->SlotCount; ++i)
{ {
var slotContext = i < equipment.Length var slotContext = modelType switch
{
ModelType.Human => i switch
{
< 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]),
16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]),
_ => globalContext.CreateContext(model, i),
},
_ => i < equipment.Length
? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i])
: globalContext.CreateContext(model, i); : globalContext.CreateContext(model, i),
};
var imc = (ResourceHandle*)model->IMCArray[i]; var imc = (ResourceHandle*)model->IMCArray[i];
var imcNode = slotContext.CreateNodeFromImc(imc); var imcNode = slotContext.CreateNodeFromImc(imc);
@ -117,7 +127,7 @@ public class ResourceTree
var subObject = (CharacterBase*)baseSubObject; var subObject = (CharacterBase*)baseSubObject;
if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) if (subObject->GetModelType() != ModelType.Weapon)
continue; continue;
var weapon = (Weapon*)subObject; var weapon = (Weapon*)subObject;