diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index ffe2f72d..55b392ba 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -15,6 +15,9 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr IdentifiedCollectionCache = 0, + + /// + DrawObjectState = 0, } public CharacterDestructor(HookManager hooks) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 5e413fe2..28a0dd8d 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -15,6 +15,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject = []; @@ -23,21 +24,24 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _gameState.LastGameObject; public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, - CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework) + CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework, CharacterDestructor characterDestructor) { _objects = objects; _createCharacterBase = createCharacterBase; _weaponReload = weaponReload; _characterBaseDestructor = characterBaseDestructor; _gameState = gameState; + _characterDestructor = characterDestructor; framework.RunOnFrameworkThread(InitializeDrawObjects); _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.DrawObjectState); } + public bool ContainsKey(nint key) => _drawObjectToGameObject.ContainsKey(key); @@ -68,6 +72,36 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary + /// Seems like sometimes the draw object of a game object is destroyed in frames after the original game object is already destroyed. + /// So protect against outdated game object pointers in the dictionary. + /// + private unsafe void OnCharacterDestructor(Character* a) + { + if (a is null) + return; + + var character = (nint)a; + var delete = stackalloc nint[5]; + var current = 0; + foreach (var (drawObject, (gameObject, _)) in _drawObjectToGameObject) + { + if (gameObject != character) + continue; + + delete[current++] = drawObject; + if (current is 4) + break; + } + + for (var ptr = delete; *ptr != nint.Zero; ++ptr) + { + _drawObjectToGameObject.Remove(*ptr, out var pair); + Penumbra.Log.Excessive($"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject:X}, {pair.IsChild})."); + } } private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 32abeb41..5e1612eb 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -90,7 +90,6 @@ public class PenumbraChangelog : IUiService .RegisterEntry("The EQP entry previously named Unknown 4 was renamed to 'Hide Glove Cuffs'.") .RegisterEntry("Fixed the changed item identification for EST changes.") .RegisterEntry("Fixed clipping issues in the changed items panel when no grouping was active."); - private static void Add1_3_5_0(Changelog log)