diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index d3ca884c0..9b42e1024 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -15,6 +15,8 @@ using Microsoft.Extensions.ObjectPool; using Serilog; +using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; + namespace Dalamud.Game.ClientState.Objects; /// @@ -36,16 +38,19 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable private readonly ObjectPool multiThreadedEnumerators = new DefaultObjectPoolProvider().Create(); - private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[64]; + private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; private long nextMultithreadedUsageWarnTime; [ServiceManager.ServiceConstructor] - private ObjectTable(ClientState clientState) + private unsafe ObjectTable(ClientState clientState) { this.clientState = clientState; - foreach (ref var e in this.cachedObjectTable.AsSpan()) - e = CachedEntry.CreateNew(); + + var nativeObjectTableAddress = (CSGameObject**)this.clientState.AddressResolver.ObjectTable; + for (var i = 0; i < this.cachedObjectTable.Length; i++) + this.cachedObjectTable[i] = new(nativeObjectTableAddress, i); + for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++) this.frameworkThreadEnumerators[i] = new(this, i); @@ -73,9 +78,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { _ = this.WarnMultithreadedUsage(); - if (index is >= ObjectTableLength or < 0) return null; - this.cachedObjectTable[index].Update(this.GetObjectAddressUnsafe(index)); - return this.cachedObjectTable[index].ActiveObject; + return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].Update(); } } @@ -87,24 +90,21 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable if (objectId is GameObject.InvalidGameObjectId or 0) return null; - foreach (var obj in this) + foreach (var e in this.cachedObjectTable) { - if (obj == null) - continue; - - if (obj.ObjectId == objectId) - return obj; + if (e.Update() is { } o && o.ObjectId == objectId) + return o; } return null; } /// - public nint GetObjectAddress(int index) + public unsafe nint GetObjectAddress(int index) { _ = this.WarnMultithreadedUsage(); - return index is < 0 or >= ObjectTableLength ? nint.Zero : this.GetObjectAddressUnsafe(index); + return index is < 0 or >= ObjectTableLength ? nint.Zero : (nint)this.cachedObjectTable[index].Address; } /// @@ -118,7 +118,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable if (address == nint.Zero) return null; - var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address; + var obj = (CSGameObject*)address; var objKind = (ObjectKind)obj->ObjectKind; return objKind switch { @@ -134,6 +134,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable }; } + [Api10ToDo("Use ThreadSafety.AssertMainThread() instead of this.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool WarnMultithreadedUsage() { @@ -154,56 +155,58 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe nint GetObjectAddressUnsafe(int index) => - *(nint*)(this.clientState.AddressResolver.ObjectTable + (8 * index)); - - private struct CachedEntry + /// Stores an object table entry, with preallocated concrete types. + internal readonly unsafe struct CachedEntry { - public GameObject? ActiveObject; - public PlayerCharacter PlayerCharacter; - public BattleNpc BattleNpc; - public Npc Npc; - public EventObj EventObj; - public GameObject GameObject; + private readonly CSGameObject** gameObjectPtrPtr; + private readonly PlayerCharacter playerCharacter; + private readonly BattleNpc battleNpc; + private readonly Npc npc; + private readonly EventObj eventObj; + private readonly GameObject gameObject; - public static CachedEntry CreateNew() => - new() - { - PlayerCharacter = new(nint.Zero), - BattleNpc = new(nint.Zero), - Npc = new(nint.Zero), - EventObj = new(nint.Zero), - GameObject = new(nint.Zero), - }; - - public unsafe void Update(nint address) + /// Initializes a new instance of the struct. + /// The object table that this entry should be pointing to. + /// The slot index inside the table. + public CachedEntry(CSGameObject** ownerTable, int slot) { - if (this.ActiveObject != null && address == this.ActiveObject.Address) - return; + this.gameObjectPtrPtr = ownerTable + slot; + this.playerCharacter = new(nint.Zero); + this.battleNpc = new(nint.Zero); + this.npc = new(nint.Zero); + this.eventObj = new(nint.Zero); + this.gameObject = new(nint.Zero); + } - if (address == nint.Zero) - { - this.ActiveObject = null; - return; - } + /// Gets the address of the underlying native object. May be null. + public CSGameObject* Address + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => *this.gameObjectPtrPtr; + } - var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address; - var objKind = (ObjectKind)obj->ObjectKind; - var activeObject = objKind switch + /// Updates and gets the wrapped game object pointed by this struct. + /// The pointed object, or null if no object exists at that slot. + public GameObject? Update() + { + var address = this.Address; + if (address is null) + return null; + + var activeObject = (ObjectKind)address->ObjectKind switch { - ObjectKind.Player => this.PlayerCharacter, - ObjectKind.BattleNpc => this.BattleNpc, - ObjectKind.EventNpc => this.Npc, - ObjectKind.Retainer => this.Npc, - ObjectKind.EventObj => this.EventObj, - ObjectKind.Companion => this.Npc, - ObjectKind.MountType => this.Npc, - ObjectKind.Ornament => this.Npc, - _ => this.GameObject, + ObjectKind.Player => this.playerCharacter, + ObjectKind.BattleNpc => this.battleNpc, + ObjectKind.EventNpc => this.npc, + ObjectKind.Retainer => this.npc, + ObjectKind.EventObj => this.eventObj, + ObjectKind.Companion => this.npc, + ObjectKind.MountType => this.npc, + ObjectKind.Ornament => this.npc, + _ => this.gameObject, }; - activeObject.Address = address; - this.ActiveObject = activeObject; + activeObject.Address = (nint)address; + return activeObject; } } } @@ -219,6 +222,7 @@ internal sealed partial class ObjectTable /// public IEnumerator GetEnumerator() { + // If something's trying to enumerate outside the framework thread, we use the ObjectPool. if (this.WarnMultithreadedUsage()) { // let's not @@ -227,6 +231,7 @@ internal sealed partial class ObjectTable return e; } + // If we're on the framework thread, see if there's an already allocated enumerator available for use. foreach (ref var x in this.frameworkThreadEnumerators.AsSpan()) { if (x is not null) @@ -238,6 +243,7 @@ internal sealed partial class ObjectTable } } + // No reusable enumerator is available; allocate a new temporary one. return new Enumerator(this, -1); } @@ -271,9 +277,7 @@ internal sealed partial class ObjectTable var cache = this.owner!.cachedObjectTable.AsSpan(); for (this.index++; this.index < ObjectTableLength; this.index++) { - this.owner!.cachedObjectTable[this.index].Update(this.owner!.GetObjectAddressUnsafe(this.index)); - - if (cache[this.index].ActiveObject is { } ao) + if (cache[this.index].Update() is { } ao) { this.Current = ao; return true; @@ -292,7 +296,7 @@ internal sealed partial class ObjectTable if (this.owner is not { } o) return; - if (this.index == -1) + if (this.slotId == -1) o.multiThreadedEnumerators.Return(this); else o.frameworkThreadEnumerators[this.slotId] = this; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 4aaf15bee..606bf03da 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -498,6 +498,9 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework /// public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; + /// + public TaskFactory FrameworkThreadTaskFactory => this.frameworkService.FrameworkThreadTaskFactory; + /// public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; diff --git a/Dalamud/Plugin/Services/IObjectTable.cs b/Dalamud/Plugin/Services/IObjectTable.cs index d029045fa..e0f671b3c 100644 --- a/Dalamud/Plugin/Services/IObjectTable.cs +++ b/Dalamud/Plugin/Services/IObjectTable.cs @@ -1,24 +1,27 @@ using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Utility; namespace Dalamud.Plugin.Services; /// /// This collection represents the currently spawned FFXIV game objects. /// +[Api10ToDo( + "Make it an IEnumerable instead. Skipping null objects make IReadOnlyCollection.Count yield incorrect values.")] public interface IObjectTable : IReadOnlyCollection { /// /// Gets the address of the object table. /// public nint Address { get; } - + /// /// Gets the length of the object table. /// public int Length { get; } - + /// /// Get an object at the specified spawn index. /// @@ -32,14 +35,14 @@ public interface IObjectTable : IReadOnlyCollection /// Object ID to find. /// A game object or null. public GameObject? SearchById(ulong objectId); - + /// /// Gets the address of the game object at the specified index of the object table. /// /// The index of the object. /// The memory address of the object. public nint GetObjectAddress(int index); - + /// /// Create a reference to an FFXIV game object. ///