From a7246b9d98392db549ec6fd408d430843664e48b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Aug 2025 16:50:26 +0200 Subject: [PATCH] Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../PostProcessing/PreBoneDeformerReplacer.cs | 2 +- .../Processing/PbdFilePostProcessor.cs | 119 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/FileEditor.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 16 +-- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 73 ++++++----- Penumbra/packages.lock.json | 22 +++- 8 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 Penumbra/Interop/Processing/PbdFilePostProcessor.cs diff --git a/OtterGui b/OtterGui index 0eaf7655..3ea61642 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0eaf7655123bd6502456e93d6ae9593249d3f792 +Subproject commit 3ea61642a05403fb2b64032112ff674b387825b3 diff --git a/Penumbra.GameData b/Penumbra.GameData index 2f5e9013..2cf59c61 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a +Subproject commit 2cf59c61494a01fd14aecf925e7dc6325a7374ac diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 30e643c7..51af5813 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -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 { diff --git a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs new file mode 100644 index 00000000..69f2ecd5 --- /dev/null +++ b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs @@ -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 _loadEpbdData; + + public ResourceType Type + => ResourceType.Pbd; + + public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner) + { + _allocator = allocator; + _epbdData = SetEpbdData(dataManager); + _loadEpbdData = (delegate* unmanaged)scanner.ScanText(Sigs.LoadEpbdData); + } + + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan 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((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}."); + } + } + } + + /// Combine the given data with the default PBD data using the game's file allocator. + private unsafe ReadOnlySpan AppendData(ReadOnlySpan data) + { + // offset has to be set, otherwise not called. + var newLength = data.Length + _epbdData.Length; + var memory = _allocator.Allocate(newLength); + var span = new Span(memory, newLength); + data.CopyTo(span); + _epbdData.CopyTo(span[data.Length..]); + return span; + } + + /// Fetch the default EPBD data from the .pbd file of the game's installation. + 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 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 []; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index a0305619..424bc56f 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -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( 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; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b9e36d80..d41dd25a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1236,16 +1236,12 @@ public class DebugTab : Window, ITab, IUiService } public static unsafe void DrawCopyableAddress(ReadOnlySpan 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 label, nint address) - => DrawCopyableAddress(label, (void*)address); + { + Penumbra.Dynamis.DrawPointer(address); + ImUtf8.SameLineInner(); + ImUtf8.Text(label); + } } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 7af33a36..f0ab1125 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -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*)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*)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*)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; } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 778f776e..7499bffa 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -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, )" } },