various 'improvements'

This commit is contained in:
2025-12-11 12:59:32 +09:00
parent 2e14fc2f8f
commit 6cf0e3daed
26 changed files with 3706 additions and 884 deletions

View File

@@ -1,27 +1,20 @@
using LightlessSync;
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using System.Collections.Concurrent;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using FFXIVClientStructs.Interop;
using System.Threading;
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.Services.ActorTracking;
public sealed unsafe class ActorObjectService : IHostedService, IDisposable
public sealed class ActorObjectService : IHostedService, IDisposable
{
public readonly record struct ActorDescriptor(
string Name,
@@ -38,25 +31,13 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
private readonly IFramework _framework;
private readonly IGameInteropProvider _interop;
private readonly IObjectTable _objectTable;
private readonly IClientState _clientState;
private readonly LightlessMediator _mediator;
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
private ActorDescriptor[] _playerCharacterSnapshot = Array.Empty<ActorDescriptor>();
private nint[] _playerAddressSnapshot = Array.Empty<nint>();
private readonly HashSet<nint> _renderedPlayers = new();
private readonly HashSet<nint> _renderedCompanions = new();
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
private nint[] _renderedPlayerSnapshot = Array.Empty<nint>();
private nint[] _renderedCompanionSnapshot = Array.Empty<nint>();
private nint[] _ownedObjectSnapshot = Array.Empty<nint>();
private IReadOnlyDictionary<nint, LightlessObjectKind> _ownedObjectMapSnapshot = new Dictionary<nint, LightlessObjectKind>();
private nint _localPlayerAddress = nint.Zero;
private nint _localPetAddress = nint.Zero;
private nint _localMinionMountAddress = nint.Zero;
private nint _localCompanionAddress = nint.Zero;
private readonly OwnedObjectTracker _ownedTracker = new();
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
@@ -80,16 +61,30 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
_framework = framework;
_interop = interop;
_objectTable = objectTable;
_clientState = clientState;
_mediator = mediator;
}
public IReadOnlyList<nint> PlayerAddresses => Volatile.Read(ref _playerAddressSnapshot);
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Volatile.Read(ref _playerCharacterSnapshot);
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
{
descriptor = default;
if (!_actorsByHash.TryGetValue(hash, out var candidate))
return false;
if (!ValidateDescriptorThreadSafe(candidate))
return false;
descriptor = candidate;
return true;
}
public bool TryGetPlayerByName(string name, out ActorDescriptor descriptor)
{
descriptor = default;
@@ -100,6 +95,9 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
ActorDescriptor? best = null;
foreach (var candidate in entries.Values)
{
if (!ValidateDescriptorThreadSafe(candidate))
continue;
if (best is null || IsBetterNameMatch(candidate, best.Value))
{
best = candidate;
@@ -115,23 +113,54 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return false;
}
public bool HooksActive => _hooksActive;
public IReadOnlyList<nint> RenderedPlayerAddresses => Volatile.Read(ref _renderedPlayerSnapshot);
public IReadOnlyList<nint> RenderedCompanionAddresses => Volatile.Read(ref _renderedCompanionSnapshot);
public IReadOnlyList<nint> OwnedObjectAddresses => Volatile.Read(ref _ownedObjectSnapshot);
public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Volatile.Read(ref _ownedObjectMapSnapshot);
public nint LocalPlayerAddress => Volatile.Read(ref _localPlayerAddress);
public nint LocalPetAddress => Volatile.Read(ref _localPetAddress);
public nint LocalMinionOrMountAddress => Volatile.Read(ref _localMinionMountAddress);
public nint LocalCompanionAddress => Volatile.Read(ref _localCompanionAddress);
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Snapshot.OwnedObjects.Map;
public nint LocalPlayerAddress => Snapshot.OwnedObjects.LocalPlayer;
public nint LocalPetAddress => Snapshot.OwnedObjects.LocalPet;
public nint LocalMinionOrMountAddress => Snapshot.OwnedObjects.LocalMinionOrMount;
public nint LocalCompanionAddress => Snapshot.OwnedObjects.LocalCompanion;
public bool TryGetOwnedKind(nint address, out LightlessObjectKind kind)
=> OwnedObjects.TryGetValue(address, out kind);
public bool TryGetOwnedActor(LightlessObjectKind kind, out ActorDescriptor descriptor)
{
descriptor = default;
if (!TryGetOwnedObject(kind, out var address))
return false;
return TryGetDescriptor(address, out descriptor);
}
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
{
ownedKind = default;
var ownedSnapshot = OwnedObjects;
foreach (var (address, kind) in ownedSnapshot)
{
if (!TryGetDescriptor(address, out var descriptor))
continue;
if (descriptor.ObjectIndex == objectIndex)
{
ownedKind = kind;
return true;
}
}
return false;
}
public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address)
{
var ownedSnapshot = Snapshot.OwnedObjects;
address = kind switch
{
LightlessObjectKind.Player => Volatile.Read(ref _localPlayerAddress),
LightlessObjectKind.Pet => Volatile.Read(ref _localPetAddress),
LightlessObjectKind.MinionOrMount => Volatile.Read(ref _localMinionMountAddress),
LightlessObjectKind.Companion => Volatile.Read(ref _localCompanionAddress),
LightlessObjectKind.Player => ownedSnapshot.LocalPlayer,
LightlessObjectKind.Pet => ownedSnapshot.LocalPet,
LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount,
LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion,
_ => nint.Zero
};
@@ -158,7 +187,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
public bool TryGetPlayerAddressByHash(string hash, out nint address)
{
if (TryGetActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
{
address = descriptor.Address;
return true;
@@ -168,6 +197,50 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return false;
}
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
{
if (address == nint.Zero)
throw new ArgumentException("Address cannot be zero.", nameof(address));
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
if (isLoaded)
return;
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
private bool ValidateDescriptorThreadSafe(ActorDescriptor descriptor)
{
if (_framework.IsInFrameworkUpdateThread)
return ValidateDescriptorInternal(descriptor);
return _framework.RunOnFrameworkThread(() => ValidateDescriptorInternal(descriptor)).GetAwaiter().GetResult();
}
private bool ValidateDescriptorInternal(ActorDescriptor descriptor)
{
if (descriptor.Address == nint.Zero)
return false;
if (descriptor.ObjectKind == DalamudObjectKind.Player &&
!string.IsNullOrEmpty(descriptor.HashedContentId))
{
var liveHash = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal))
{
UntrackGameObject(descriptor.Address);
return false;
}
}
return true;
}
public void RefreshTrackedActors(bool force = false)
{
var now = DateTime.UtcNow;
@@ -185,7 +258,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
}
else
{
_framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
_ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
}
}
@@ -211,23 +284,12 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
_activePlayers.Clear();
_actorsByHash.Clear();
_actorsByName.Clear();
Volatile.Write(ref _playerCharacterSnapshot, Array.Empty<ActorDescriptor>());
Volatile.Write(ref _playerAddressSnapshot, Array.Empty<nint>());
Volatile.Write(ref _renderedPlayerSnapshot, Array.Empty<nint>());
Volatile.Write(ref _renderedCompanionSnapshot, Array.Empty<nint>());
Volatile.Write(ref _ownedObjectSnapshot, Array.Empty<nint>());
Volatile.Write(ref _ownedObjectMapSnapshot, new Dictionary<nint, LightlessObjectKind>());
Volatile.Write(ref _localPlayerAddress, nint.Zero);
Volatile.Write(ref _localPetAddress, nint.Zero);
Volatile.Write(ref _localMinionMountAddress, nint.Zero);
Volatile.Write(ref _localCompanionAddress, nint.Zero);
_renderedPlayers.Clear();
_renderedCompanions.Clear();
_ownedObjects.Clear();
_ownedTracker.Reset();
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
return Task.CompletedTask;
}
private void InitializeHooks()
private unsafe void InitializeHooks()
{
if (_hooksActive)
return;
@@ -271,7 +333,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
});
}
private void OnCharacterInitialized(Character* chara)
private unsafe void OnCharacterInitialized(Character* chara)
{
try
{
@@ -285,7 +347,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
}
private void OnCharacterTerminated(Character* chara)
private unsafe void OnCharacterTerminated(Character* chara)
{
var address = (nint)chara;
QueueFrameworkUpdate(() => UntrackGameObject(address));
@@ -299,7 +361,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
}
}
private GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
{
var address = (nint)chara;
QueueFrameworkUpdate(() => UntrackGameObject(address));
@@ -314,7 +376,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
}
}
private void TrackGameObject(GameObject* gameObject)
private unsafe void TrackGameObject(GameObject* gameObject)
{
if (gameObject == null)
return;
@@ -332,14 +394,10 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
if (_activePlayers.TryGetValue(descriptor.Address, out var existing))
{
RemoveDescriptorFromIndexes(existing);
RemoveDescriptorFromCollections(existing);
RemoveDescriptor(existing);
}
_activePlayers[descriptor.Address] = descriptor;
IndexDescriptor(descriptor);
AddDescriptorToCollections(descriptor);
RebuildSnapshots();
AddDescriptor(descriptor);
if (_logger.IsEnabled(LogLevel.Debug))
{
@@ -355,16 +413,16 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
_mediator.Publish(new ActorTrackedMessage(descriptor));
}
private ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
{
if (gameObject == null)
return null;
var address = (nint)gameObject;
string name = string.Empty;
ushort objectIndex = (ushort)gameObject->ObjectIndex;
ushort objectIndex = gameObject->ObjectIndex;
bool isInGpose = objectIndex >= 200;
bool isLocal = _clientState.LocalPlayer?.Address == address;
bool isLocal = _objectTable.LocalPlayer?.Address == address;
string hashedCid = string.Empty;
if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter)
@@ -372,7 +430,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
name = playerCharacter.Name.TextValue ?? string.Empty;
objectIndex = playerCharacter.ObjectIndex;
isInGpose = objectIndex >= 200;
isLocal = playerCharacter.Address == _clientState.LocalPlayer?.Address;
isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address;
}
else
{
@@ -389,7 +447,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
}
private (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
{
if (gameObject == null)
return (null, 0);
@@ -406,7 +464,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return (LightlessObjectKind.Player, entityId);
}
if (_clientState.LocalPlayer is not { } localPlayer)
if (_objectTable.LocalPlayer is not { } localPlayer)
return (null, 0);
var ownerId = gameObject->OwnerId;
@@ -453,9 +511,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
if (_activePlayers.TryRemove(address, out var descriptor))
{
RemoveDescriptorFromIndexes(descriptor);
RemoveDescriptorFromCollections(descriptor);
RebuildSnapshots();
RemoveDescriptor(descriptor);
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
@@ -469,7 +525,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
}
}
private void RefreshTrackedActorsInternal()
private unsafe void RefreshTrackedActorsInternal()
{
var addresses = EnumerateActiveCharacterAddresses();
HashSet<nint> seen = new(addresses.Count);
@@ -524,7 +580,10 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return candidate.ObjectIndex < current.ObjectIndex;
}
private void OnCompanionInitialized(Companion* companion)
private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor)
=> _activePlayers.TryGetValue(address, out descriptor);
private unsafe void OnCompanionInitialized(Companion* companion)
{
try
{
@@ -538,7 +597,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
}
private void OnCompanionTerminated(Companion* companion)
private unsafe void OnCompanionTerminated(Companion* companion)
{
var address = (nint)companion;
QueueFrameworkUpdate(() => UntrackGameObject(address));
@@ -559,107 +618,46 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
_actorsByHash.TryRemove(descriptor.HashedContentId, out _);
}
if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name))
if (descriptor.ObjectKind == DalamudObjectKind.Player
&& !string.IsNullOrEmpty(descriptor.Name)
&& _actorsByName.TryGetValue(descriptor.Name, out var bucket))
{
if (_actorsByName.TryGetValue(descriptor.Name, out var bucket))
bucket.TryRemove(descriptor.Address, out _);
if (bucket.IsEmpty)
{
bucket.TryRemove(descriptor.Address, out _);
if (bucket.IsEmpty)
{
_actorsByName.TryRemove(descriptor.Name, out _);
}
_actorsByName.TryRemove(descriptor.Name, out _);
}
}
}
private void AddDescriptorToCollections(ActorDescriptor descriptor)
private void AddDescriptor(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Add(descriptor.Address);
if (descriptor.IsLocalPlayer)
{
Volatile.Write(ref _localPlayerAddress, descriptor.Address);
}
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Add(descriptor.Address);
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects[descriptor.Address] = ownedKind;
switch (ownedKind)
{
case LightlessObjectKind.Player:
Volatile.Write(ref _localPlayerAddress, descriptor.Address);
break;
case LightlessObjectKind.Pet:
Volatile.Write(ref _localPetAddress, descriptor.Address);
break;
case LightlessObjectKind.MinionOrMount:
Volatile.Write(ref _localMinionMountAddress, descriptor.Address);
break;
case LightlessObjectKind.Companion:
Volatile.Write(ref _localCompanionAddress, descriptor.Address);
break;
}
}
_activePlayers[descriptor.Address] = descriptor;
IndexDescriptor(descriptor);
_ownedTracker.OnDescriptorAdded(descriptor);
PublishSnapshot();
}
private void RemoveDescriptorFromCollections(ActorDescriptor descriptor)
private void RemoveDescriptor(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Remove(descriptor.Address);
if (descriptor.IsLocalPlayer && Volatile.Read(ref _localPlayerAddress) == descriptor.Address)
{
Volatile.Write(ref _localPlayerAddress, nint.Zero);
}
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Remove(descriptor.Address);
if (Volatile.Read(ref _localCompanionAddress) == descriptor.Address)
{
Volatile.Write(ref _localCompanionAddress, nint.Zero);
}
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects.Remove(descriptor.Address);
switch (ownedKind)
{
case LightlessObjectKind.Player when Volatile.Read(ref _localPlayerAddress) == descriptor.Address:
Volatile.Write(ref _localPlayerAddress, nint.Zero);
break;
case LightlessObjectKind.Pet when Volatile.Read(ref _localPetAddress) == descriptor.Address:
Volatile.Write(ref _localPetAddress, nint.Zero);
break;
case LightlessObjectKind.MinionOrMount when Volatile.Read(ref _localMinionMountAddress) == descriptor.Address:
Volatile.Write(ref _localMinionMountAddress, nint.Zero);
break;
case LightlessObjectKind.Companion when Volatile.Read(ref _localCompanionAddress) == descriptor.Address:
Volatile.Write(ref _localCompanionAddress, nint.Zero);
break;
}
}
RemoveDescriptorFromIndexes(descriptor);
_ownedTracker.OnDescriptorRemoved(descriptor);
PublishSnapshot();
}
private void RebuildSnapshots()
private void PublishSnapshot()
{
var playerDescriptors = _activePlayers.Values
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
.ToArray();
var playerAddresses = new nint[playerDescriptors.Length];
for (var i = 0; i < playerDescriptors.Length; i++)
playerAddresses[i] = playerDescriptors[i].Address;
Volatile.Write(ref _playerCharacterSnapshot, playerDescriptors);
Volatile.Write(ref _playerAddressSnapshot, playerDescriptors.Select(d => d.Address).ToArray());
Volatile.Write(ref _renderedPlayerSnapshot, _renderedPlayers.ToArray());
Volatile.Write(ref _renderedCompanionSnapshot, _renderedCompanions.ToArray());
Volatile.Write(ref _ownedObjectSnapshot, _ownedObjects.Keys.ToArray());
Volatile.Write(ref _ownedObjectMapSnapshot, new Dictionary<nint, LightlessObjectKind>(_ownedObjects));
var ownedSnapshot = _ownedTracker.CreateSnapshot();
var nextGeneration = Snapshot.Generation + 1;
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
Volatile.Write(ref _snapshot, snapshot);
}
private void QueueFrameworkUpdate(Action action)
@@ -673,7 +671,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return;
}
_framework.RunOnFrameworkThread(action);
_ = _framework.RunOnFrameworkThread(action);
}
private void DisposeHooks()
@@ -723,7 +721,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
or DalamudObjectKind.Companion
or DalamudObjectKind.MountType;
private static List<nint> EnumerateActiveCharacterAddresses()
private static unsafe List<nint> EnumerateActiveCharacterAddresses()
{
var results = new List<nint>(64);
var manager = GameObjectManager.Instance();
@@ -751,4 +749,170 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
return results;
}
private static unsafe bool IsObjectFullyLoaded(nint address)
{
if (address == nint.Zero)
return false;
var gameObject = (GameObject*)address;
if (gameObject == null)
return false;
var drawObject = gameObject->DrawObject;
if (drawObject == null)
return false;
if (gameObject->RenderFlags == 2048)
return false;
var characterBase = (CharacterBase*)drawObject;
if (characterBase == null)
return false;
if (characterBase->HasModelInSlotLoaded != 0)
return false;
if (characterBase->HasModelFilesInSlotLoaded != 0)
return false;
return true;
}
private sealed class OwnedObjectTracker
{
private readonly HashSet<nint> _renderedPlayers = new();
private readonly HashSet<nint> _renderedCompanions = new();
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
private nint _localPlayerAddress = nint.Zero;
private nint _localPetAddress = nint.Zero;
private nint _localMinionMountAddress = nint.Zero;
private nint _localCompanionAddress = nint.Zero;
public void OnDescriptorAdded(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Add(descriptor.Address);
if (descriptor.IsLocalPlayer)
_localPlayerAddress = descriptor.Address;
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Add(descriptor.Address);
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects[descriptor.Address] = ownedKind;
switch (ownedKind)
{
case LightlessObjectKind.Player:
_localPlayerAddress = descriptor.Address;
break;
case LightlessObjectKind.Pet:
_localPetAddress = descriptor.Address;
break;
case LightlessObjectKind.MinionOrMount:
_localMinionMountAddress = descriptor.Address;
break;
case LightlessObjectKind.Companion:
_localCompanionAddress = descriptor.Address;
break;
}
}
}
public void OnDescriptorRemoved(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Remove(descriptor.Address);
if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
_localPlayerAddress = nint.Zero;
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Remove(descriptor.Address);
if (_localCompanionAddress == descriptor.Address)
_localCompanionAddress = nint.Zero;
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects.Remove(descriptor.Address);
switch (ownedKind)
{
case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
_localPlayerAddress = nint.Zero;
break;
case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
_localPetAddress = nint.Zero;
break;
case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
_localMinionMountAddress = nint.Zero;
break;
case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
_localCompanionAddress = nint.Zero;
break;
}
}
}
public OwnedObjectSnapshot CreateSnapshot()
=> new(
_renderedPlayers.ToArray(),
_renderedCompanions.ToArray(),
_ownedObjects.Keys.ToArray(),
new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
_localPlayerAddress,
_localPetAddress,
_localMinionMountAddress,
_localCompanionAddress);
public void Reset()
{
_renderedPlayers.Clear();
_renderedCompanions.Clear();
_ownedObjects.Clear();
_localPlayerAddress = nint.Zero;
_localPetAddress = nint.Zero;
_localMinionMountAddress = nint.Zero;
_localCompanionAddress = nint.Zero;
}
}
private sealed record OwnedObjectSnapshot(
IReadOnlyList<nint> RenderedPlayers,
IReadOnlyList<nint> RenderedCompanions,
IReadOnlyList<nint> OwnedAddresses,
IReadOnlyDictionary<nint, LightlessObjectKind> Map,
nint LocalPlayer,
nint LocalPet,
nint LocalMinionOrMount,
nint LocalCompanion)
{
public static OwnedObjectSnapshot Empty { get; } = new(
Array.Empty<nint>(),
Array.Empty<nint>(),
Array.Empty<nint>(),
new Dictionary<nint, LightlessObjectKind>(),
nint.Zero,
nint.Zero,
nint.Zero,
nint.Zero);
}
private sealed record ActorSnapshot(
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
IReadOnlyList<nint> PlayerAddresses,
OwnedObjectSnapshot OwnedObjects,
int Generation)
{
public static ActorSnapshot Empty { get; } = new(
Array.Empty<ActorDescriptor>(),
Array.Empty<nint>(),
OwnedObjectSnapshot.Empty,
0);
}
}