diff --git a/Penumbra.Api b/Penumbra.Api index bd56d828..47bd5424 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit bd56d82816b8366e19dddfb2dc7fd7f167e264ee +Subproject commit 47bd5424d04c667d0df1ac1dd1eeb3e50b476c2c diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index 7f70c6bf..74cde3a0 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable { private readonly CommunicatorService _communicator; private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; private readonly CutsceneService _cutsceneService; private readonly ResourceLoader _resourceLoader; public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService, - ResourceLoader resourceLoader) + ResourceLoader resourceLoader, DrawObjectState drawObjectState) { _communicator = communicator; _collectionResolver = collectionResolver; _cutsceneService = cutsceneService; _resourceLoader = resourceLoader; + _drawObjectState = drawObjectState; _resourceLoader.ResourceLoaded += OnResourceLoaded; _resourceLoader.PapRequested += OnPapRequested; _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); @@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public int GetCutsceneParentIndex(int actorIdx) => _cutsceneService.GetParentIndex(actorIdx); + public Func GetCutsceneParentIndexFunc() + { + var weakRef = new WeakReference(_cutsceneService); + return idx => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed."); + + return c.GetParentIndex(idx); + }; + } + + public Func GetGameObjectFromDrawObjectFunc() + { + var weakRef = new WeakReference(_drawObjectState); + return model => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed."); + + return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero; + }; + } + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) ? PenumbraApiEc.Success diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 47d44cfc..38125627 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 8; + public const int FeatureVersion = 9; public void Dispose() { diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index d54faa6c..f5a6c16d 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -40,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState), IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState), IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState), + IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState), + IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState), IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta), IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta), diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index f14abbff..02e1be54 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -89,10 +89,13 @@ public sealed unsafe class CollectionResolver( /// Identify the correct collection for a draw object. public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) { - var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject) + if (drawObject is null) + return DefaultCollection; + + Actor obj = drawObjectState.TryGetValue(drawObject, out var gameObject) ? gameObject.Item1 - : drawObjectState.LastGameObject); - return IdentifyCollection(obj, useCache); + : drawObjectState.LastGameObject; + return IdentifyCollection(obj.AsObject, useCache); } /// Get the default collection. diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 28a0dd8d..6f3e457c 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -9,7 +9,7 @@ using Penumbra.Interop.Hooks.Objects; namespace Penumbra.Interop.PathResolving; -public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService +public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService { private readonly ObjectManager _objects; private readonly CreateCharacterBase _createCharacterBase; @@ -18,7 +18,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject = []; + private readonly Dictionary _drawObjectToGameObject = []; public nint LastGameObject => _gameState.LastGameObject; @@ -41,11 +41,10 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject.ContainsKey(key); - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() => _drawObjectToGameObject.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -54,16 +53,28 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject.Count; - public bool TryGetValue(nint drawObject, out (nint, bool) gameObject) - => _drawObjectToGameObject.TryGetValue(drawObject, out gameObject); + public bool TryGetValue(Model drawObject, out (Actor, ObjectIndex, bool) gameObject) + { + if (!_drawObjectToGameObject.TryGetValue(drawObject, out gameObject)) + return false; - public (nint, bool) this[nint key] + var currentObject = _objects[gameObject.Item2]; + if (currentObject != gameObject.Item1) + { + Penumbra.Log.Warning($"[DrawObjectState] Stored association {drawObject} -> {gameObject.Item1} has index {gameObject.Item2}, which resolves to {currentObject}."); + return false; + } + + return true; + } + + public (Actor, ObjectIndex, bool) this[Model key] => _drawObjectToGameObject[key]; - public IEnumerable Keys + public IEnumerable Keys => _drawObjectToGameObject.Keys; - public IEnumerable<(nint, bool)> Values + public IEnumerable<(Actor, ObjectIndex, bool)> Values => _drawObjectToGameObject.Values; public unsafe void Dispose() @@ -87,20 +98,21 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary 0x{(nint)a:X} (actual: 0x{pair.GameObject:X}, {pair.IsChild})."); + Penumbra.Log.Excessive( + $"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject.Address:X}, {pair.IsChild})."); } } @@ -119,9 +131,9 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary @@ -137,12 +149,12 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionaryChildObject, gameObject, true, true); if (!iterate) return; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 79c7f2db..7f4c1b23 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,7 +21,6 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData.Data; -using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; @@ -190,7 +189,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 6629c126..9dd18ddd 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -569,29 +569,31 @@ public class DebugTab : Window, ITab, IUiService { if (drawTree) { - using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###DrawObjectResolverTable", 8, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState - .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) - .ThenBy(kvp => kvp.Value.Item2) - .ThenBy(kvp => kvp.Key)) + foreach (var (drawObject, (gameObjectPtr, idx, child)) in _drawObjectState + .OrderBy(kvp => kvp.Value.Item2.Index) + .ThenBy(kvp => kvp.Value.Item3) + .ThenBy(kvp => kvp.Key.Address)) { - var gameObject = (GameObject*)gameObjectPtr; ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"{drawObject}"); + ImUtf8.DrawTableColumn($"{gameObjectPtr.Index}"); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, gameObjectPtr.Index != idx)) + { + ImUtf8.DrawTableColumn($"{idx}"); + } - ImGuiUtil.CopyOnClickSelectable($"0x{drawObject:X}"); + ImUtf8.DrawTableColumn(child ? "Child"u8 : "Main"u8); ImGui.TableNextColumn(); - ImGui.TextUnformatted(gameObject->ObjectIndex.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(child ? "Child" : "Main"); - ImGui.TableNextColumn(); - var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString()); - ImGuiUtil.CopyOnClickSelectable(address); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(name); - ImGui.TableNextColumn(); - var collection = _collectionResolver.IdentifyCollection(gameObject, true); - ImGui.TextUnformatted(collection.ModCollection.Identity.Name); + ImUtf8.CopyOnClickSelectable($"{gameObjectPtr}"); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, _objects[idx] != gameObjectPtr)) + { + ImUtf8.DrawTableColumn($"{_objects[idx]}"); + } + ImUtf8.DrawTableColumn(gameObjectPtr.Utf8Name.Span); + var collection = _collectionResolver.IdentifyCollection(gameObjectPtr.AsObject, true); + ImUtf8.DrawTableColumn(collection.ModCollection.Identity.Name); } } }