mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'rt-more-files'
This commit is contained in:
commit
2413424c8a
7 changed files with 311 additions and 145 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,
|
||||
|
|
@ -256,7 +248,7 @@ internal partial record ResolveContext
|
|||
if (faceId < 201)
|
||||
faceId -= tribe switch
|
||||
{
|
||||
0xB when modelType == 4 => 100,
|
||||
0xB when modelType is 4 => 100,
|
||||
0xE | 0xF => 100,
|
||||
_ => 0,
|
||||
};
|
||||
|
|
@ -305,7 +297,7 @@ internal partial record ResolveContext
|
|||
private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex)
|
||||
{
|
||||
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||
if (set == 0)
|
||||
if (set.Id is 0)
|
||||
return Utf8GamePath.Empty;
|
||||
|
||||
var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set);
|
||||
|
|
@ -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.Id is 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 is 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 is 0)
|
||||
return Utf8GamePath.Empty;
|
||||
|
||||
var path = GamePaths.Equipment.Decal.Path(decal);
|
||||
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath)
|
||||
{
|
||||
if (resourceHandle == null)
|
||||
if (resourceHandle is null)
|
||||
return null;
|
||||
if (gamePath.IsEmpty)
|
||||
return null;
|
||||
|
|
@ -65,7 +65,7 @@ internal unsafe partial record ResolveContext(
|
|||
[SkipLocalsInit]
|
||||
private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11)
|
||||
{
|
||||
if (resourceHandle == null)
|
||||
if (resourceHandle is null)
|
||||
return null;
|
||||
|
||||
Utf8GamePath path;
|
||||
|
|
@ -105,7 +105,7 @@ internal unsafe partial record ResolveContext(
|
|||
private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||
Utf8GamePath gamePath)
|
||||
{
|
||||
if (resourceHandle == null)
|
||||
if (resourceHandle is null)
|
||||
throw new ArgumentNullException(nameof(resourceHandle));
|
||||
|
||||
if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached))
|
||||
|
|
@ -117,7 +117,7 @@ internal unsafe partial record ResolveContext(
|
|||
private ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||
Utf8GamePath gamePath, bool autoAdd = true)
|
||||
{
|
||||
if (resourceHandle == null)
|
||||
if (resourceHandle is null)
|
||||
throw new ArgumentNullException(nameof(resourceHandle));
|
||||
|
||||
var fileName = (ReadOnlySpan<byte>)resourceHandle->FileName.AsSpan();
|
||||
|
|
@ -141,7 +141,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
public ResourceNode? CreateNodeFromEid(ResourceHandle* eid)
|
||||
{
|
||||
if (eid == null)
|
||||
if (eid is null)
|
||||
return null;
|
||||
|
||||
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path))
|
||||
|
|
@ -152,7 +152,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
public ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
|
||||
{
|
||||
if (imc == null)
|
||||
if (imc is null)
|
||||
return null;
|
||||
|
||||
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path))
|
||||
|
|
@ -163,7 +163,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd)
|
||||
{
|
||||
if (pbd == null)
|
||||
if (pbd is null)
|
||||
return null;
|
||||
|
||||
return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath);
|
||||
|
|
@ -171,7 +171,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
|
||||
{
|
||||
if (tex == null)
|
||||
if (tex is null)
|
||||
return null;
|
||||
|
||||
if (!Utf8GamePath.FromString(gamePath, out var path))
|
||||
|
|
@ -180,9 +180,17 @@ 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 (mdl == null || mdl->ModelResourceHandle == null)
|
||||
if (tex is 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 is null || mdl->ModelResourceHandle is null)
|
||||
return null;
|
||||
|
||||
var mdlResource = mdl->ModelResourceHandle;
|
||||
|
|
@ -197,12 +205,12 @@ internal unsafe partial record ResolveContext(
|
|||
for (var i = 0; i < mdl->MaterialCount; i++)
|
||||
{
|
||||
var mtrl = mdl->Materials[i];
|
||||
if (mtrl == null)
|
||||
if (mtrl is null)
|
||||
continue;
|
||||
|
||||
var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i);
|
||||
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName));
|
||||
if (mtrlNode != null)
|
||||
if (mtrlNode is not null)
|
||||
{
|
||||
if (Global.WithUiData)
|
||||
mtrlNode.FallbackName = $"Material #{i}";
|
||||
|
|
@ -210,6 +218,12 @@ internal unsafe partial record ResolveContext(
|
|||
}
|
||||
}
|
||||
|
||||
if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode)
|
||||
node.Children.Add(decalNode);
|
||||
|
||||
if (CreateNodeFromMaterialPap(mpapHandle, imc) is { } mpapNode)
|
||||
node.Children.Add(mpapNode);
|
||||
|
||||
Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node);
|
||||
|
||||
return node;
|
||||
|
|
@ -217,7 +231,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path)
|
||||
{
|
||||
if (mtrl == null || mtrl->MaterialResourceHandle == null)
|
||||
if (mtrl is null || mtrl->MaterialResourceHandle is null)
|
||||
return null;
|
||||
|
||||
var resource = mtrl->MaterialResourceHandle;
|
||||
|
|
@ -226,15 +240,15 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
|
||||
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName));
|
||||
if (shpkNode != null)
|
||||
if (shpkNode is not null)
|
||||
{
|
||||
if (Global.WithUiData)
|
||||
shpkNode.Name = "Shader Package";
|
||||
node.Children.Add(shpkNode);
|
||||
}
|
||||
|
||||
var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
|
||||
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
||||
var shpkNames = Global.WithUiData && shpkNode is not null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
|
||||
var shpk = Global.WithUiData && shpkNode is not null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
||||
|
||||
var alreadyProcessedSamplerIds = new HashSet<uint>();
|
||||
for (var i = 0; i < resource->TextureCount; i++)
|
||||
|
|
@ -247,7 +261,7 @@ internal unsafe partial record ResolveContext(
|
|||
if (Global.WithUiData)
|
||||
{
|
||||
string? name = null;
|
||||
if (shpk != null)
|
||||
if (shpk is not null)
|
||||
{
|
||||
var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds);
|
||||
var samplerId = index != 0x001F
|
||||
|
|
@ -301,9 +315,61 @@ internal unsafe partial record ResolveContext(
|
|||
}
|
||||
}
|
||||
|
||||
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
||||
public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc)
|
||||
{
|
||||
if (sklb == null || sklb->SkeletonResourceHandle == null)
|
||||
if (decalHandle is 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 is 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 is 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 is null || sklb->SkeletonResourceHandle is null)
|
||||
return null;
|
||||
|
||||
var path = ResolveSkeletonPath(partialSkeletonIndex);
|
||||
|
|
@ -311,10 +377,11 @@ internal unsafe partial record ResolveContext(
|
|||
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
|
||||
return cached;
|
||||
|
||||
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
|
||||
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
|
||||
if (skpNode != null)
|
||||
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
|
||||
if (CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex) is { } skpNode)
|
||||
node.Children.Add(skpNode);
|
||||
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
|
||||
node.Children.Add(phybNode);
|
||||
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||
|
||||
return node;
|
||||
|
|
@ -322,7 +389,7 @@ internal unsafe partial record ResolveContext(
|
|||
|
||||
private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
||||
{
|
||||
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
|
||||
if (sklb is null || sklb->SkeletonParameterResourceHandle is null)
|
||||
return null;
|
||||
|
||||
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
|
||||
|
|
@ -338,11 +405,31 @@ internal unsafe partial record ResolveContext(
|
|||
return node;
|
||||
}
|
||||
|
||||
private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex)
|
||||
{
|
||||
if (phybHandle is 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)'/');
|
||||
// Weapons intentionally left out.
|
||||
var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8));
|
||||
var isEquipment = path.Count >= 2
|
||||
&& path[0].Span.SequenceEqual("chara"u8)
|
||||
&& (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8));
|
||||
if (isEquipment)
|
||||
foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot()))
|
||||
{
|
||||
|
|
@ -358,7 +445,7 @@ internal unsafe partial record ResolveContext(
|
|||
}
|
||||
|
||||
var dataFromPath = GuessUiDataFromPath(gamePath);
|
||||
if (dataFromPath.Name != null)
|
||||
if (dataFromPath.Name is not null)
|
||||
return dataFromPath;
|
||||
|
||||
return isEquipment
|
||||
|
|
@ -373,24 +460,13 @@ internal unsafe partial record ResolveContext(
|
|||
var name = obj.Key;
|
||||
if (obj.Value is IdentifiedCustomization)
|
||||
name = name[14..].Trim();
|
||||
if (name != "Unknown")
|
||||
if (name is not "Unknown")
|
||||
return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag());
|
||||
}
|
||||
|
||||
return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
||||
}
|
||||
|
||||
private static string? SafeGet(ReadOnlySpan<string> array, Index index)
|
||||
{
|
||||
var i = index.GetOffset(array.Length);
|
||||
return i >= 0 && i < array.Length ? array[i] : null;
|
||||
}
|
||||
|
||||
private static ulong GetResourceHandleLength(ResourceHandle* handle)
|
||||
{
|
||||
if (handle == null)
|
||||
return 0;
|
||||
|
||||
return handle->GetLength();
|
||||
}
|
||||
=> handle is null ? 0ul : handle->GetLength();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ public class ResourceNode : ICloneable
|
|||
public Utf8GamePath[] PossibleGamePaths;
|
||||
public FullPath FullPath;
|
||||
public PathStatus FullPathStatus;
|
||||
public bool ForceInternal;
|
||||
public bool ForceProtected;
|
||||
public string? ModName;
|
||||
public readonly WeakReference<Mod> Mod = new(null!);
|
||||
public string? ModRelativePath;
|
||||
|
|
@ -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,45 +12,39 @@ 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;
|
||||
|
||||
public class ResourceTree
|
||||
public class ResourceTree(
|
||||
string name,
|
||||
string anonymizedName,
|
||||
int gameObjectIndex,
|
||||
nint gameObjectAddress,
|
||||
nint drawObjectAddress,
|
||||
bool localPlayerRelated,
|
||||
bool playerRelated,
|
||||
bool networked,
|
||||
string collectionName,
|
||||
string anonymizedCollectionName)
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string AnonymizedName;
|
||||
public readonly int GameObjectIndex;
|
||||
public readonly nint GameObjectAddress;
|
||||
public readonly nint DrawObjectAddress;
|
||||
public readonly bool LocalPlayerRelated;
|
||||
public readonly bool PlayerRelated;
|
||||
public readonly bool Networked;
|
||||
public readonly string CollectionName;
|
||||
public readonly string AnonymizedCollectionName;
|
||||
public readonly List<ResourceNode> Nodes;
|
||||
public readonly HashSet<ResourceNode> FlatNodes;
|
||||
public readonly string Name = name;
|
||||
public readonly string AnonymizedName = anonymizedName;
|
||||
public readonly int GameObjectIndex = gameObjectIndex;
|
||||
public readonly nint GameObjectAddress = gameObjectAddress;
|
||||
public readonly nint DrawObjectAddress = drawObjectAddress;
|
||||
public readonly bool LocalPlayerRelated = localPlayerRelated;
|
||||
public readonly bool PlayerRelated = playerRelated;
|
||||
public readonly bool Networked = networked;
|
||||
public readonly string CollectionName = collectionName;
|
||||
public readonly string AnonymizedCollectionName = anonymizedCollectionName;
|
||||
public readonly List<ResourceNode> Nodes = [];
|
||||
public readonly HashSet<ResourceNode> FlatNodes = [];
|
||||
|
||||
public int ModelId;
|
||||
public CustomizeData CustomizeData;
|
||||
public GenderRace RaceCode;
|
||||
|
||||
public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress,
|
||||
bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName)
|
||||
{
|
||||
Name = name;
|
||||
AnonymizedName = anonymizedName;
|
||||
GameObjectIndex = gameObjectIndex;
|
||||
GameObjectAddress = gameObjectAddress;
|
||||
DrawObjectAddress = drawObjectAddress;
|
||||
LocalPlayerRelated = localPlayerRelated;
|
||||
Networked = networked;
|
||||
PlayerRelated = playerRelated;
|
||||
CollectionName = collectionName;
|
||||
AnonymizedCollectionName = anonymizedCollectionName;
|
||||
Nodes = [];
|
||||
FlatNodes = [];
|
||||
}
|
||||
|
||||
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action)
|
||||
{
|
||||
foreach (var node in Nodes)
|
||||
|
|
@ -70,10 +66,22 @@ public class ResourceTree
|
|||
};
|
||||
ModelId = character->ModelContainer.ModelCharaId;
|
||||
CustomizeData = character->DrawData.CustomizeData;
|
||||
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
||||
RaceCode = human is not null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
||||
|
||||
var genericContext = globalContext.CreateContext(model);
|
||||
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
|
||||
var mpapArray = mpapArrayPtr is not null ? 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
|
||||
|
|
@ -90,18 +98,17 @@ public class ResourceTree
|
|||
: globalContext.CreateContext(model, i),
|
||||
};
|
||||
|
||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||
if (imcNode != null)
|
||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||
if (slotContext.CreateNodeFromImc(imc) is { } imcNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
imcNode.FallbackName = $"IMC #{i}";
|
||||
Nodes.Add(imcNode);
|
||||
}
|
||||
|
||||
var mdl = model->Models[i];
|
||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
||||
if (mdlNode != null)
|
||||
var mdl = model->Models[i];
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
|
||||
i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Model #{i}";
|
||||
|
|
@ -109,11 +116,13 @@ 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);
|
||||
|
||||
if (human != null)
|
||||
if (human is not null)
|
||||
AddHumanResources(globalContext, human);
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +132,12 @@ public class ResourceTree
|
|||
var weaponNodes = new List<ResourceNode>();
|
||||
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
||||
{
|
||||
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
||||
if (baseSubObject->GetObjectType() is not FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
||||
continue;
|
||||
|
||||
var subObject = (CharacterBase*)baseSubObject;
|
||||
|
||||
if (subObject->GetModelType() != ModelType.Weapon)
|
||||
if (subObject->GetModelType() is not ModelType.Weapon)
|
||||
continue;
|
||||
|
||||
var weapon = (Weapon*)subObject;
|
||||
|
|
@ -140,22 +149,24 @@ 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 = mpapArrayPtr is not null ? 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);
|
||||
|
||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||
if (imcNode != null)
|
||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||
if (slotContext.CreateNodeFromImc(imc) is { } imcNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
|
||||
weaponNodes.Add(imcNode);
|
||||
}
|
||||
|
||||
var mdl = subObject->Models[i];
|
||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
||||
if (mdlNode != null)
|
||||
var mdl = subObject->Models[i];
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||
|
|
@ -163,7 +174,11 @@ 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;
|
||||
}
|
||||
|
|
@ -176,28 +191,25 @@ public class ResourceTree
|
|||
var genericContext = globalContext.CreateContext(&human->CharacterBase);
|
||||
|
||||
var cache = globalContext.Collection._cache;
|
||||
if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle))
|
||||
if (cache is not null
|
||||
&& cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)
|
||||
&& genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode)
|
||||
{
|
||||
var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle);
|
||||
if (pbdNode != null)
|
||||
if (globalContext.WithUiData)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
{
|
||||
pbdNode = pbdNode.Clone();
|
||||
pbdNode.FallbackName = "Racial Deformer";
|
||||
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(pbdNode);
|
||||
pbdNode = pbdNode.Clone();
|
||||
pbdNode.FallbackName = "Racial Deformer";
|
||||
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(pbdNode);
|
||||
}
|
||||
|
||||
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
|
||||
var decalPath = decalId != 0
|
||||
var decalPath = decalId is not 0
|
||||
? GamePaths.Human.Decal.FaceDecalPath(decalId)
|
||||
: GamePaths.Tex.TransparentPath;
|
||||
var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath);
|
||||
if (decalNode != null)
|
||||
if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
{
|
||||
|
|
@ -213,9 +225,9 @@ public class ResourceTree
|
|||
var legacyDecalPath = hasLegacyDecal
|
||||
? GamePaths.Human.Decal.LegacyDecalPath
|
||||
: GamePaths.Tex.TransparentPath;
|
||||
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
|
||||
if (legacyDecalNode != null)
|
||||
if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode)
|
||||
{
|
||||
legacyDecalNode.ForceProtected = !hasLegacyDecal;
|
||||
if (globalContext.WithUiData)
|
||||
{
|
||||
legacyDecalNode = legacyDecalNode.Clone();
|
||||
|
|
@ -227,7 +239,8 @@ 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,8 +255,9 @@ public class ResourceTree
|
|||
|
||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||
{
|
||||
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i);
|
||||
if (sklbNode != null)
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null;
|
||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode)
|
||||
{
|
||||
if (context.Global.WithUiData)
|
||||
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
||||
|
|
@ -251,4 +265,16 @@ public class ResourceTree
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void AddMaterialAnimationSkeleton(List<ResourceNode> nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle,
|
||||
string prefix = "")
|
||||
{
|
||||
var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle);
|
||||
if (sklbNode is 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Interop.ResourceTree;
|
||||
|
|
@ -43,7 +42,7 @@ public partial class ModEditWindow
|
|||
|
||||
private void DrawQuickImportTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("Import from Screen");
|
||||
using var tab = ImUtf8.TabItem("Import from Screen"u8);
|
||||
if (!tab)
|
||||
{
|
||||
_quickImportActions.Clear();
|
||||
|
|
@ -73,14 +72,14 @@ public partial class ModEditWindow
|
|||
else
|
||||
{
|
||||
var file = _gameData.GetFile(path);
|
||||
writable = file == null ? null : new RawGameFileWritable(file);
|
||||
writable = file is null ? null : new RawGameFileWritable(file);
|
||||
}
|
||||
|
||||
_quickImportWritables.Add(resourceNode.FullPath, writable);
|
||||
}
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.",
|
||||
resourceNode.FullPath.FullName.Length == 0 || writable == null, true))
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
|
||||
resourceNode.FullPath.FullName.Length is 0 || writable is null))
|
||||
{
|
||||
var fullPathStr = resourceNode.FullPath.FullName;
|
||||
var ext = resourceNode.PossibleGamePaths.Length == 1
|
||||
|
|
@ -110,15 +109,19 @@ public partial class ModEditWindow
|
|||
_quickImportActions.Add((resourceNode.GamePath, writable), quickImport);
|
||||
}
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize,
|
||||
$"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true))
|
||||
var canQuickImport = quickImport.CanExecute;
|
||||
var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive());
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.FileImport,
|
||||
$"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}",
|
||||
buttonSize,
|
||||
!quickImportEnabled))
|
||||
{
|
||||
quickImport.Execute();
|
||||
_quickImportActions.Remove((resourceNode.GamePath, writable));
|
||||
}
|
||||
}
|
||||
|
||||
private record class RawFileWritable(string Path) : IWritable
|
||||
private record RawFileWritable(string Path) : IWritable
|
||||
{
|
||||
public bool Valid
|
||||
=> true;
|
||||
|
|
@ -127,7 +130,7 @@ public partial class ModEditWindow
|
|||
=> File.ReadAllBytes(Path);
|
||||
}
|
||||
|
||||
private record class RawGameFileWritable(FileResource FileResource) : IWritable
|
||||
private record RawGameFileWritable(FileResource FileResource) : IWritable
|
||||
{
|
||||
public bool Valid
|
||||
=> true;
|
||||
|
|
@ -185,19 +188,19 @@ public partial class ModEditWindow
|
|||
public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file)
|
||||
{
|
||||
var editor = owner._editor;
|
||||
if (editor == null)
|
||||
if (editor is null)
|
||||
return new QuickImportAction(owner._editor, FallbackOptionName, gamePath);
|
||||
|
||||
var subMod = editor.Option!;
|
||||
var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName;
|
||||
if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes)
|
||||
if (gamePath.IsEmpty || file is null || editor.FileEditor.Changes)
|
||||
return new QuickImportAction(editor, optionName, gamePath);
|
||||
|
||||
if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath))
|
||||
return new QuickImportAction(editor, optionName, gamePath);
|
||||
|
||||
var mod = owner.Mod;
|
||||
if (mod == null)
|
||||
if (mod is null)
|
||||
return new QuickImportAction(editor, optionName, gamePath);
|
||||
|
||||
var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport);
|
||||
|
|
@ -232,7 +235,7 @@ public partial class ModEditWindow
|
|||
{
|
||||
var path = mod.ModPath;
|
||||
var subDirs = 0;
|
||||
if (subMod == null)
|
||||
if (subMod is null)
|
||||
return (path, subDirs);
|
||||
|
||||
var name = subMod.Name;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue