Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it.

This commit is contained in:
Ottermandias 2025-08-13 16:50:26 +02:00
parent 9aff388e21
commit a7246b9d98
8 changed files with 187 additions and 52 deletions

@ -1 +1 @@
Subproject commit 0eaf7655123bd6502456e93d6ae9593249d3f792
Subproject commit 3ea61642a05403fb2b64032112ff674b387825b3

@ -1 +1 @@
Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a
Subproject commit 2cf59c61494a01fd14aecf925e7dc6325a7374ac

View file

@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi
if (!_framework.IsInFrameworkUpdateThread)
Penumbra.Log.Warning(
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
try
{

View file

@ -0,0 +1,119 @@
using Dalamud.Game;
using Dalamud.Plugin.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.String;
namespace Penumbra.Interop.Processing;
public sealed class PbdFilePostProcessor : IFilePostProcessor
{
private readonly IFileAllocator _allocator;
private byte[] _epbdData;
private unsafe delegate* unmanaged<ResourceHandle*, void> _loadEpbdData;
public ResourceType Type
=> ResourceType.Pbd;
public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner)
{
_allocator = allocator;
_epbdData = SetEpbdData(dataManager);
_loadEpbdData = (delegate* unmanaged<ResourceHandle*, void>)scanner.ScanText(Sigs.LoadEpbdData);
}
public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan<byte> additionalData)
{
if (_epbdData.Length is 0)
return;
if (resource->LoadState is not LoadState.Success)
{
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} failed load ({resource->LoadState}).");
return;
}
var (data, length) = resource->GetData();
if (length is 0 || data == nint.Zero)
{
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} succeeded load but has no data.");
return;
}
var span = new ReadOnlySpan<byte>((void*)data, (int)resource->FileSize);
var reader = new PackReader(span);
if (reader.HasData)
{
Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {originalGamePath} with EPBD data.");
return;
}
var newData = AppendData(span);
fixed (byte* ptr = newData)
{
// Set the appended data and the actual file size, then re-load the EPBD data via game function call.
if (resource->SetData((nint)ptr, newData.Length))
{
resource->FileSize = (uint)newData.Length;
resource->CsHandle.FileSize2 = (uint)newData.Length;
resource->CsHandle.FileSize3 = (uint)newData.Length;
_loadEpbdData(resource);
// Free original data.
_allocator.Release((void*)data, length);
Penumbra.Log.Verbose($"[ResourceLoader] Loaded {originalGamePath} from file and appended default EPBD data.");
}
else
{
Penumbra.Log.Warning(
$"[ResourceLoader] Failed to append EPBD data to custom PBD at {originalGamePath}.");
}
}
}
/// <summary> Combine the given data with the default PBD data using the game's file allocator. </summary>
private unsafe ReadOnlySpan<byte> AppendData(ReadOnlySpan<byte> data)
{
// offset has to be set, otherwise not called.
var newLength = data.Length + _epbdData.Length;
var memory = _allocator.Allocate(newLength);
var span = new Span<byte>(memory, newLength);
data.CopyTo(span);
_epbdData.CopyTo(span[data.Length..]);
return span;
}
/// <summary> Fetch the default EPBD data from the .pbd file of the game's installation. </summary>
private static byte[] SetEpbdData(IDataManager dataManager)
{
try
{
var file = dataManager.GetFile(GamePaths.Pbd.Path);
if (file is null || file.Data.Length is 0)
{
Penumbra.Log.Warning("Default PBD file has no data.");
return [];
}
ReadOnlySpan<byte> span = file.Data;
var reader = new PackReader(span);
if (!reader.HasData)
{
Penumbra.Log.Warning("Default PBD file has no EPBD section.");
return [];
}
var offset = span.Length - (int)reader.PackLength;
var ret = span[offset..];
Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}.");
return ret.ToArray();
}
catch (Exception ex)
{
Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}");
return [];
}
}
}

View file

@ -8,6 +8,7 @@ using OtterGui.Compression;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.Mods.Editor;
using Penumbra.Services;
@ -80,7 +81,7 @@ public class FileEditor<T>(
private Exception? _currentException;
private bool _changed;
private string _defaultPath = string.Empty;
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
private bool _inInput;
private Utf8GamePath _defaultPathUtf8;
private bool _isDefaultPathUtf8Valid;

View file

@ -1236,16 +1236,12 @@ public class DebugTab : Window, ITab, IUiService
}
public static unsafe void DrawCopyableAddress(ReadOnlySpan<byte> label, void* address)
{
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
{
if (ImUtf8.Selectable($"0x{(nint)address:X16} {label}"))
ImUtf8.SetClipboardText($"0x{(nint)address:X16}");
}
ImUtf8.HoverTooltip("Click to copy address to clipboard."u8);
}
=> DrawCopyableAddress(label, (nint)address);
public static unsafe void DrawCopyableAddress(ReadOnlySpan<byte> label, nint address)
=> DrawCopyableAddress(label, (void*)address);
{
Penumbra.Dynamis.DrawPointer(address);
ImUtf8.SameLineInner();
ImUtf8.Text(label);
}
}

View file

@ -27,14 +27,31 @@ public unsafe class GlobalVariablesDrawer(
return;
var actionManager = (ActionTimelineManager**)ActionTimelineManager.Instance();
DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address);
DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address);
DebugTab.DrawCopyableAddress("ScheduleManagement"u8, ScheduleManagement.Instance());
DebugTab.DrawCopyableAddress("ActionTimelineManager*"u8, actionManager);
DebugTab.DrawCopyableAddress("ActionTimelineManager"u8, actionManager != null ? *actionManager : null);
DebugTab.DrawCopyableAddress("SchedulerResourceManagement*"u8, scheduler.Address);
DebugTab.DrawCopyableAddress("SchedulerResourceManagement"u8, scheduler.Address != null ? *scheduler.Address : null);
DebugTab.DrawCopyableAddress("Device"u8, Device.Instance());
using (ImUtf8.Group())
{
Penumbra.Dynamis.DrawPointer(characterUtility.Address);
Penumbra.Dynamis.DrawPointer(residentResources.Address);
Penumbra.Dynamis.DrawPointer(ScheduleManagement.Instance());
Penumbra.Dynamis.DrawPointer(actionManager);
Penumbra.Dynamis.DrawPointer(actionManager != null ? *actionManager : null);
Penumbra.Dynamis.DrawPointer(scheduler.Address);
Penumbra.Dynamis.DrawPointer(scheduler.Address != null ? *scheduler.Address : null);
Penumbra.Dynamis.DrawPointer(Device.Instance());
}
ImGui.SameLine();
using (ImUtf8.Group())
{
ImUtf8.Text("CharacterUtility"u8);
ImUtf8.Text("ResidentResourceManager"u8);
ImUtf8.Text("ScheduleManagement"u8);
ImUtf8.Text("ActionTimelineManager*"u8);
ImUtf8.Text("ActionTimelineManager"u8);
ImUtf8.Text("SchedulerResourceManagement*"u8);
ImUtf8.Text("SchedulerResourceManagement"u8);
ImUtf8.Text("Device"u8);
}
DrawCharacterUtility();
DrawResidentResources();
DrawSchedulerResourcesMap();
@ -63,7 +80,7 @@ public unsafe class GlobalVariablesDrawer(
var resource = characterUtility.Address->Resource(idx);
ImUtf8.DrawTableColumn($"[{idx}]");
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}");
Penumbra.Dynamis.DrawPointer(resource);
if (resource == null)
{
ImGui.TableNextRow();
@ -74,25 +91,12 @@ public unsafe class GlobalVariablesDrawer(
ImGui.TableNextColumn();
var data = (nint)resource->CsHandle.GetData();
var length = resource->CsHandle.GetLength();
if (ImUtf8.Selectable($"0x{data:X}"))
if (data != nint.Zero && length > 0)
ImUtf8.SetClipboardText(string.Join("\n",
new ReadOnlySpan<byte>((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2"))));
ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8);
Penumbra.Dynamis.DrawPointer(data);
ImUtf8.DrawTableColumn(length.ToString());
ImGui.TableNextColumn();
if (intern.Value != -1)
{
ImUtf8.Selectable($"0x{characterUtility.DefaultResource(intern).Address:X}");
if (ImGui.IsItemClicked())
ImUtf8.SetClipboardText(string.Join("\n",
new ReadOnlySpan<byte>((byte*)characterUtility.DefaultResource(intern).Address,
characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2"))));
ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8);
Penumbra.Dynamis.DrawPointer(characterUtility.DefaultResource(intern).Address);
ImUtf8.DrawTableColumn($"{characterUtility.DefaultResource(intern).Size}");
}
else
@ -122,7 +126,7 @@ public unsafe class GlobalVariablesDrawer(
var resource = residentResources.Address->ResourceList[idx];
ImUtf8.DrawTableColumn($"[{idx}]");
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}");
Penumbra.Dynamis.DrawPointer(resource);
if (resource == null)
{
ImGui.TableNextRow();
@ -133,12 +137,7 @@ public unsafe class GlobalVariablesDrawer(
ImGui.TableNextColumn();
var data = (nint)resource->CsHandle.GetData();
var length = resource->CsHandle.GetLength();
if (ImUtf8.Selectable($"0x{data:X}"))
if (data != nint.Zero && length > 0)
ImUtf8.SetClipboardText(string.Join("\n",
new ReadOnlySpan<byte>((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2"))));
ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8);
Penumbra.Dynamis.DrawPointer(data);
ImUtf8.DrawTableColumn(length.ToString());
}
}
@ -184,15 +183,15 @@ public unsafe class GlobalVariablesDrawer(
ImUtf8.DrawTableColumn($"{resource->Consumers}");
ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}");
Penumbra.Dynamis.DrawPointer(resource);
ImGui.TableNextColumn();
var resourceHandle = *((ResourceHandle**)resource + 3);
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}");
Penumbra.Dynamis.DrawPointer(resourceHandle);
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span);
ImGui.TableNextColumn();
uint dataLength = 0;
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}");
Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength));
ImUtf8.DrawTableColumn($"{dataLength}");
++_shownResourcesMap;
}
@ -233,15 +232,15 @@ public unsafe class GlobalVariablesDrawer(
ImUtf8.DrawTableColumn($"{resource->Consumers}");
ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}");
Penumbra.Dynamis.DrawPointer(resource);
ImGui.TableNextColumn();
var resourceHandle = *((ResourceHandle**)resource + 3);
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}");
Penumbra.Dynamis.DrawPointer(resourceHandle);
ImGui.TableNextColumn();
ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span);
ImGui.TableNextColumn();
uint dataLength = 0;
ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}");
Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength));
ImUtf8.DrawTableColumn($"{dataLength}");
++_shownResourcesList;
}

View file

@ -58,6 +58,19 @@
"resolved": "3.1.11",
"contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw=="
},
"FlatSharp.Compiler": {
"type": "Transitive",
"resolved": "7.9.0",
"contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A=="
},
"FlatSharp.Runtime": {
"type": "Transitive",
"resolved": "7.9.0",
"contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==",
"dependencies": {
"System.Memory": "4.5.5"
}
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2024.3.0",
@ -94,6 +107,11 @@
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "8.0.1",
@ -133,8 +151,10 @@
"penumbra.gamedata": {
"type": "Project",
"dependencies": {
"FlatSharp.Compiler": "[7.9.0, )",
"FlatSharp.Runtime": "[7.9.0, )",
"OtterGui": "[1.0.0, )",
"Penumbra.Api": "[5.6.1, )",
"Penumbra.Api": "[5.10.0, )",
"Penumbra.String": "[1.0.6, )"
}
},