mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add file types to Resource Tree and require Ctrl+Shift for some quick imports
This commit is contained in:
parent
4a00d82921
commit
514b0e7f30
7 changed files with 224 additions and 33 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7
|
||||
Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7
|
||||
|
|
@ -22,6 +22,13 @@ internal partial record ResolveContext
|
|||
private static bool IsEquipmentSlot(uint slotIndex)
|
||||
=> slotIndex is < 5 or 16 or 17;
|
||||
|
||||
private unsafe Variant Variant
|
||||
=> ModelType switch
|
||||
{
|
||||
ModelType.Monster => (byte)((Monster*)CharacterBase)->Variant,
|
||||
_ => Equipment.Variant,
|
||||
};
|
||||
|
||||
private Utf8GamePath ResolveModelPath()
|
||||
{
|
||||
// Correctness:
|
||||
|
|
@ -92,7 +99,7 @@ internal partial record ResolveContext
|
|||
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
||||
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
|
||||
ModelType.Monster => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||
_ => ResolveMaterialPathNative(mtrlFileName),
|
||||
};
|
||||
}
|
||||
|
|
@ -100,7 +107,7 @@ internal partial record ResolveContext
|
|||
[SkipLocalsInit]
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
Span<byte> mirroredFileName = stackalloc byte[32];
|
||||
|
|
@ -141,31 +148,16 @@ internal partial record ResolveContext
|
|||
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
|
||||
}
|
||||
|
||||
private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
||||
{
|
||||
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)
|
||||
private unsafe ImcEntry ResolveImcData(ResourceHandle* imc)
|
||||
{
|
||||
var imcFileData = imc->GetDataSpan();
|
||||
if (imcFileData.IsEmpty)
|
||||
{
|
||||
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);
|
||||
if (!exists)
|
||||
return variant.Id;
|
||||
|
||||
return entry.MaterialId;
|
||||
return ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), Variant, out _);
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,15 @@ internal unsafe partial record ResolveContext(
|
|||
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)
|
||||
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);
|
||||
|
||||
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)
|
||||
return null;
|
||||
|
|
@ -315,6 +383,9 @@ internal unsafe partial record ResolveContext(
|
|||
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
|
||||
if (skpNode != null)
|
||||
node.Children.Add(skpNode);
|
||||
var phybNode = CreateNodeFromPhyb(phybHandle, partialSkeletonIndex);
|
||||
if (phybNode != null)
|
||||
node.Children.Add(phybNode);
|
||||
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||
|
||||
return node;
|
||||
|
|
@ -338,6 +409,24 @@ internal unsafe partial record ResolveContext(
|
|||
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)
|
||||
{
|
||||
var path = gamePath.Path.Split((byte)'/');
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ public class ResourceNode : ICloneable
|
|||
public readonly WeakReference<Mod> Mod = new(null!);
|
||||
public string? ModRelativePath;
|
||||
public CiByteString AdditionalData;
|
||||
public bool ForceInternal;
|
||||
public bool ForceProtected;
|
||||
public readonly ulong Length;
|
||||
public readonly List<ResourceNode> Children;
|
||||
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
|
||||
=> 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)
|
||||
{
|
||||
|
|
@ -67,6 +74,8 @@ public class ResourceNode : ICloneable
|
|||
Mod = other.Mod;
|
||||
ModRelativePath = other.ModRelativePath;
|
||||
AdditionalData = other.AdditionalData;
|
||||
ForceInternal = other.ForceInternal;
|
||||
ForceProtected = other.ForceProtected;
|
||||
Length = other.Length;
|
||||
Children = other.Children;
|
||||
ResolveContext = other.ResolveContext;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Physics;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
|
@ -10,6 +12,7 @@ using Penumbra.UI;
|
|||
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
||||
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
|
||||
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||
|
||||
namespace Penumbra.Interop.ResourceTree;
|
||||
|
||||
|
|
@ -74,6 +77,18 @@ public class ResourceTree
|
|||
|
||||
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)
|
||||
{
|
||||
var slotContext = modelType switch
|
||||
|
|
@ -100,7 +115,8 @@ public class ResourceTree
|
|||
}
|
||||
|
||||
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 (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);
|
||||
|
||||
|
|
@ -140,6 +158,10 @@ public class ResourceTree
|
|||
|
||||
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)
|
||||
{
|
||||
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
|
||||
|
|
@ -154,7 +176,7 @@ public class ResourceTree
|
|||
}
|
||||
|
||||
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 (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;
|
||||
}
|
||||
|
|
@ -216,6 +240,7 @@ public class ResourceTree
|
|||
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
|
||||
if (legacyDecalNode != null)
|
||||
{
|
||||
legacyDecalNode.ForceProtected = !hasLegacyDecal;
|
||||
if (globalContext.WithUiData)
|
||||
{
|
||||
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);
|
||||
if (eidNode != null)
|
||||
|
|
@ -242,7 +267,9 @@ public class ResourceTree
|
|||
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ internal static class StructExtensions
|
|||
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)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
|
|
@ -45,6 +51,12 @@ internal static class StructExtensions
|
|||
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)
|
||||
=> str == null ? CiByteString.Empty : new CiByteString(str).Clone();
|
||||
|
||||
|
|
|
|||
|
|
@ -110,8 +110,11 @@ public partial class ModEditWindow
|
|||
_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,
|
||||
$"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();
|
||||
_quickImportActions.Remove((resourceNode.GamePath, writable));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue