Add file types to Resource Tree and require Ctrl+Shift for some quick imports

This commit is contained in:
Exter-N 2025-02-27 00:10:24 +01:00
parent 4a00d82921
commit 514b0e7f30
7 changed files with 224 additions and 33 deletions

@ -1 +1 @@
Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7 Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7

View file

@ -22,6 +22,13 @@ internal partial record ResolveContext
private static bool IsEquipmentSlot(uint slotIndex) private static bool IsEquipmentSlot(uint slotIndex)
=> slotIndex is < 5 or 16 or 17; => slotIndex is < 5 or 16 or 17;
private unsafe Variant Variant
=> ModelType switch
{
ModelType.Monster => (byte)((Monster*)CharacterBase)->Variant,
_ => Equipment.Variant,
};
private Utf8GamePath ResolveModelPath() private Utf8GamePath ResolveModelPath()
{ {
// Correctness: // Correctness:
@ -92,7 +99,7 @@ internal partial record ResolveContext
=> 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),
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), ModelType.Monster => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
_ => ResolveMaterialPathNative(mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName),
}; };
} }
@ -100,7 +107,7 @@ internal partial record ResolveContext
[SkipLocalsInit] [SkipLocalsInit]
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{ {
var variant = ResolveMaterialVariant(imc, Equipment.Variant); var variant = ResolveImcData(imc).MaterialId;
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
@ -118,9 +125,9 @@ internal partial record ResolveContext
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;
// Some offhands share materials with the corresponding mainhand // Some offhands share materials with the corresponding mainhand
if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) if (ItemData.AdaptOffhandImc(Equipment.Set, out var mirroredSetId))
{ {
var variant = ResolveMaterialVariant(imc, Equipment.Variant); var variant = ResolveImcData(imc).MaterialId;
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> mirroredFileName = stackalloc byte[32]; Span<byte> mirroredFileName = stackalloc byte[32];
@ -141,31 +148,16 @@ internal partial record ResolveContext
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
} }
private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) private unsafe ImcEntry ResolveImcData(ResourceHandle* imc)
{
var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
}
private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant)
{ {
var imcFileData = imc->GetDataSpan(); var imcFileData = imc->GetDataSpan();
if (imcFileData.IsEmpty) if (imcFileData.IsEmpty)
{ {
Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span"); Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span");
return variant.Id; return default;
} }
var entry = ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), variant, out var exists); return ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), Variant, out _);
if (!exists)
return variant.Id;
return entry.MaterialId;
} }
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant, private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant,
@ -317,4 +309,52 @@ internal partial record ResolveContext
var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex); var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
} }
private Utf8GamePath ResolvePhysicsModulePath(uint partialSkeletonIndex)
{
// Correctness and Safety:
// Resolving a physics module 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 => ResolveHumanPhysicsModulePath(partialSkeletonIndex),
_ => ResolvePhysicsModulePathNative(partialSkeletonIndex),
};
}
private Utf8GamePath ResolveHumanPhysicsModulePath(uint partialSkeletonIndex)
{
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
if (set == 0)
return Utf8GamePath.Empty;
var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolvePhysicsModulePathNative(uint partialSkeletonIndex)
{
var path = CharacterBase->ResolvePhybPathAsByteString(partialSkeletonIndex);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
{
var animation = ResolveImcData(imc).MaterialAnimationId;
if (animation == 0)
return Utf8GamePath.Empty;
var path = CharacterBase->ResolveMaterialPapPathAsByteString(SlotIndex, animation);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveDecalPath(ResourceHandle* imc)
{
var decal = ResolveImcData(imc).DecalId;
if (decal == 0)
return Utf8GamePath.Empty;
var path = GamePaths.Equipment.Decal.Path(decal);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
} }

View file

@ -180,7 +180,15 @@ internal unsafe partial record ResolveContext(
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
} }
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, Utf8GamePath gamePath)
{
if (tex == null)
return null;
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath);
}
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle)
{ {
if (mdl == null || mdl->ModelResourceHandle == null) if (mdl == null || mdl->ModelResourceHandle == null)
return null; return null;
@ -210,6 +218,14 @@ internal unsafe partial record ResolveContext(
} }
} }
var decalNode = CreateNodeFromDecal(decalHandle, imc);
if (null != decalNode)
node.Children.Add(decalNode);
var mpapNode = CreateNodeFromMaterialPap(mpapHandle, imc);
if (null != mpapNode)
node.Children.Add(mpapNode);
Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node);
return node; return node;
@ -301,7 +317,59 @@ internal unsafe partial record ResolveContext(
} }
} }
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc)
{
if (decalHandle == null)
return null;
var path = ResolveDecalPath(imc);
if (path.IsEmpty)
return null;
var node = CreateNodeFromTex(decalHandle, path)!;
if (Global.WithUiData)
node.FallbackName = "Decal";
return node;
}
public ResourceNode? CreateNodeFromMaterialPap(ResourceHandle* mpapHandle, ResourceHandle* imc)
{
if (mpapHandle == null)
return null;
var path = ResolveMaterialAnimationPath(imc);
if (path.IsEmpty)
return null;
if (Global.Nodes.TryGetValue((path, (nint)mpapHandle), out var cached))
return cached;
var node = CreateNode(ResourceType.Pap, 0, mpapHandle, path);
if (Global.WithUiData)
node.FallbackName = "Material Animation";
return node;
}
public ResourceNode? CreateNodeFromMaterialSklb(SkeletonResourceHandle* sklbHandle)
{
if (sklbHandle == null)
return null;
if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path))
return null;
if (Global.Nodes.TryGetValue((path, (nint)sklbHandle), out var cached))
return cached;
var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, path);
node.ForceInternal = true;
return node;
}
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex)
{ {
if (sklb == null || sklb->SkeletonResourceHandle == null) if (sklb == null || sklb->SkeletonResourceHandle == null)
return null; return null;
@ -315,6 +383,9 @@ internal unsafe partial record ResolveContext(
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
if (skpNode != null) if (skpNode != null)
node.Children.Add(skpNode); node.Children.Add(skpNode);
var phybNode = CreateNodeFromPhyb(phybHandle, partialSkeletonIndex);
if (phybNode != null)
node.Children.Add(phybNode);
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
return node; return node;
@ -338,6 +409,24 @@ internal unsafe partial record ResolveContext(
return node; return node;
} }
private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex)
{
if (phybHandle == null)
return null;
var path = ResolvePhysicsModulePath(partialSkeletonIndex);
if (Global.Nodes.TryGetValue((path, (nint)phybHandle), out var cached))
return cached;
var node = CreateNode(ResourceType.Phyb, 0, phybHandle, path, false);
if (Global.WithUiData)
node.FallbackName = "Physics Module";
Global.Nodes.Add((path, (nint)phybHandle), node);
return node;
}
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
{ {
var path = gamePath.Path.Split((byte)'/'); var path = gamePath.Path.Split((byte)'/');

View file

@ -21,6 +21,8 @@ public class ResourceNode : ICloneable
public readonly WeakReference<Mod> Mod = new(null!); public readonly WeakReference<Mod> Mod = new(null!);
public string? ModRelativePath; public string? ModRelativePath;
public CiByteString AdditionalData; public CiByteString AdditionalData;
public bool ForceInternal;
public bool ForceProtected;
public readonly ulong Length; public readonly ulong Length;
public readonly List<ResourceNode> Children; public readonly List<ResourceNode> Children;
internal ResolveContext? ResolveContext; internal ResolveContext? ResolveContext;
@ -37,8 +39,13 @@ public class ResourceNode : ICloneable
} }
} }
/// <summary> Whether to treat the file as internal (hide from user unless debug mode is on). </summary>
public bool Internal public bool Internal
=> Type is ResourceType.Eid or ResourceType.Imc; => ForceInternal || Type is ResourceType.Eid or ResourceType.Imc;
/// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary>
public bool Protected
=> ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd;
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
{ {
@ -67,6 +74,8 @@ public class ResourceNode : ICloneable
Mod = other.Mod; Mod = other.Mod;
ModRelativePath = other.ModRelativePath; ModRelativePath = other.ModRelativePath;
AdditionalData = other.AdditionalData; AdditionalData = other.AdditionalData;
ForceInternal = other.ForceInternal;
ForceProtected = other.ForceProtected;
Length = other.Length; Length = other.Length;
Children = other.Children; Children = other.Children;
ResolveContext = other.ResolveContext; ResolveContext = other.ResolveContext;

View file

@ -1,7 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Physics;
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 FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -10,6 +12,7 @@ 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; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
@ -74,6 +77,18 @@ public class ResourceTree
var genericContext = globalContext.CreateContext(model); var genericContext = globalContext.CreateContext(model);
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : [];
var decalArray = modelType switch
{
ModelType.Human => human->SlotDecalsSpan,
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
ModelType.Weapon => [((Weapon*)model)->Decal],
ModelType.Monster => [((Monster*)model)->Decal],
_ => [],
};
for (var i = 0u; i < model->SlotCount; ++i) for (var i = 0u; i < model->SlotCount; ++i)
{ {
var slotContext = modelType switch var slotContext = modelType switch
@ -100,7 +115,8 @@ public class ResourceTree
} }
var mdl = model->Models[i]; var mdl = model->Models[i];
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
i < mpapArray.Length ? mpapArray[(int)i].Value : null);
if (mdlNode != null) if (mdlNode != null)
{ {
if (globalContext.WithUiData) if (globalContext.WithUiData)
@ -109,7 +125,9 @@ public class ResourceTree
} }
} }
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton); AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule);
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940));
AddWeapons(globalContext, model); AddWeapons(globalContext, model);
@ -140,6 +158,10 @@ public class ResourceTree
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948);
var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
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 slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
@ -154,7 +176,7 @@ public class ResourceTree
} }
var mdl = subObject->Models[i]; var mdl = subObject->Models[i];
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null);
if (mdlNode != null) if (mdlNode != null)
{ {
if (globalContext.WithUiData) if (globalContext.WithUiData)
@ -163,7 +185,9 @@ public class ResourceTree
} }
} }
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, "); AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, ");
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), $"Weapon #{weaponIndex}, ");
++weaponIndex; ++weaponIndex;
} }
@ -216,6 +240,7 @@ public class ResourceTree
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath); var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
if (legacyDecalNode != null) if (legacyDecalNode != null)
{ {
legacyDecalNode.ForceProtected = !hasLegacyDecal;
if (globalContext.WithUiData) if (globalContext.WithUiData)
{ {
legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode = legacyDecalNode.Clone();
@ -227,7 +252,7 @@ public class ResourceTree
} }
} }
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "") private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, string prefix = "")
{ {
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
if (eidNode != null) if (eidNode != null)
@ -242,7 +267,9 @@ public class ResourceTree
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
{ {
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i); // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null;
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i);
if (sklbNode != null) if (sklbNode != null)
{ {
if (context.Global.WithUiData) if (context.Global.WithUiData)
@ -251,4 +278,15 @@ public class ResourceTree
} }
} }
} }
private unsafe void AddMaterialAnimationSkeleton(List<ResourceNode> nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, string prefix = "")
{
var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle);
if (sklbNode == null)
return;
if (context.Global.WithUiData)
sklbNode.FallbackName = $"{prefix}Material Animation Skeleton";
nodes.Add(sklbNode);
}
} }

View file

@ -33,6 +33,12 @@ internal static class StructExtensions
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
} }
public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId)
{
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId));
}
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
{ {
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
@ -45,6 +51,12 @@ internal static class StructExtensions
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
} }
public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
{
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
}
private static unsafe CiByteString ToOwnedByteString(byte* str) private static unsafe CiByteString ToOwnedByteString(byte* str)
=> str == null ? CiByteString.Empty : new CiByteString(str).Clone(); => str == null ? CiByteString.Empty : new CiByteString(str).Clone();

View file

@ -110,8 +110,11 @@ public partial class ModEditWindow
_quickImportActions.Add((resourceNode.GamePath, writable), quickImport); _quickImportActions.Add((resourceNode.GamePath, writable), quickImport);
} }
var canQuickImport = quickImport.CanExecute;
var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive());
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize,
$"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) $"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}",
!quickImportEnabled, true))
{ {
quickImport.Execute(); quickImport.Execute();
_quickImportActions.Remove((resourceNode.GamePath, writable)); _quickImportActions.Remove((resourceNode.GamePath, writable));