Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: defnotken <itsdefnotken@gmail.com> Reviewed-on: #131
1295 lines
42 KiB
C#
1295 lines
42 KiB
C#
using System.Collections.Concurrent;
|
|
using Dalamud.Game.ClientState.Conditions;
|
|
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.PlayerData.Handlers;
|
|
using LightlessSync.Services.Mediator;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
|
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
|
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
|
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|
|
|
namespace LightlessSync.Services.ActorTracking;
|
|
|
|
public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorSubscriber
|
|
{
|
|
public readonly record struct ActorDescriptor(
|
|
string Name,
|
|
string HashedContentId,
|
|
nint Address,
|
|
ushort ObjectIndex,
|
|
bool IsLocalPlayer,
|
|
bool IsInGpose,
|
|
DalamudObjectKind ObjectKind,
|
|
LightlessObjectKind? OwnedKind,
|
|
uint OwnerEntityId);
|
|
|
|
private readonly ILogger<ActorObjectService> _logger;
|
|
private readonly IFramework _framework;
|
|
private readonly IGameInteropProvider _interop;
|
|
private readonly IObjectTable _objectTable;
|
|
private readonly IClientState _clientState;
|
|
private readonly ICondition _condition;
|
|
private readonly LightlessMediator _mediator;
|
|
private readonly object _playerRelatedHandlerLock = new();
|
|
private readonly HashSet<GameObjectHandler> _playerRelatedHandlers = [];
|
|
|
|
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
|
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
|
|
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
|
private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
|
|
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
|
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
|
|
|
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
|
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
|
private Hook<Character.Delegates.Dtor>? _onDestructorHook;
|
|
private Hook<Companion.Delegates.OnInitialize>? _onCompanionInitializeHook;
|
|
private Hook<Companion.Delegates.Terminate>? _onCompanionTerminateHook;
|
|
|
|
private bool _hooksActive;
|
|
private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
|
|
private DateTime _nextRefreshAllowed = DateTime.MinValue;
|
|
|
|
public ActorObjectService(
|
|
ILogger<ActorObjectService> logger,
|
|
IFramework framework,
|
|
IGameInteropProvider interop,
|
|
IObjectTable objectTable,
|
|
IClientState clientState,
|
|
ICondition condition,
|
|
LightlessMediator mediator)
|
|
{
|
|
_logger = logger;
|
|
_framework = framework;
|
|
_interop = interop;
|
|
_objectTable = objectTable;
|
|
_clientState = clientState;
|
|
_condition = condition;
|
|
_mediator = mediator;
|
|
|
|
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
|
{
|
|
if (!msg.OwnedObject) return;
|
|
lock (_playerRelatedHandlerLock)
|
|
{
|
|
_playerRelatedHandlers.Add(msg.GameObjectHandler);
|
|
}
|
|
RefreshTrackedActors(force: true);
|
|
});
|
|
_mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
|
{
|
|
if (!msg.OwnedObject) return;
|
|
lock (_playerRelatedHandlerLock)
|
|
{
|
|
_playerRelatedHandlers.Remove(msg.GameObjectHandler);
|
|
}
|
|
RefreshTrackedActors(force: true);
|
|
});
|
|
}
|
|
|
|
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
|
|
|
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
|
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
|
|
|
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
|
|
|
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
|
|
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
|
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
|
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
|
public LightlessMediator Mediator => _mediator;
|
|
|
|
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;
|
|
|
|
if (!_actorsByName.TryGetValue(name, out var entries) || entries.IsEmpty)
|
|
return false;
|
|
|
|
ActorDescriptor? best = null;
|
|
foreach (var candidate in entries.Values)
|
|
{
|
|
if (!ValidateDescriptorThreadSafe(candidate))
|
|
continue;
|
|
|
|
if (best is null || IsBetterNameMatch(candidate, best.Value))
|
|
{
|
|
best = candidate;
|
|
}
|
|
}
|
|
|
|
if (best is { } selected)
|
|
{
|
|
descriptor = selected;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
public bool HooksActive => _hooksActive;
|
|
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
|
|
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 ownedDescriptors = OwnedDescriptors;
|
|
for (var i = 0; i < ownedDescriptors.Count; i++)
|
|
{
|
|
var descriptor = ownedDescriptors[i];
|
|
if (descriptor.ObjectIndex != objectIndex)
|
|
continue;
|
|
|
|
if (descriptor.OwnedKind is { } resolvedKind)
|
|
{
|
|
ownedKind = resolvedKind;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address)
|
|
{
|
|
var ownedSnapshot = Snapshot.OwnedObjects;
|
|
address = kind switch
|
|
{
|
|
LightlessObjectKind.Player => ownedSnapshot.LocalPlayer,
|
|
LightlessObjectKind.Pet => ownedSnapshot.LocalPet,
|
|
LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount,
|
|
LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion,
|
|
_ => nint.Zero
|
|
};
|
|
|
|
return address != nint.Zero;
|
|
}
|
|
|
|
public bool TryGetOwnedActor(uint ownerEntityId, LightlessObjectKind? kindFilter, out ActorDescriptor descriptor)
|
|
{
|
|
descriptor = default;
|
|
foreach (var candidate in _activePlayers.Values)
|
|
{
|
|
if (candidate.OwnerEntityId != ownerEntityId)
|
|
continue;
|
|
|
|
if (kindFilter.HasValue && candidate.OwnedKind != kindFilter)
|
|
continue;
|
|
|
|
descriptor = candidate;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetPlayerAddressByHash(string hash, out nint address)
|
|
{
|
|
if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
|
|
{
|
|
address = descriptor.Address;
|
|
return true;
|
|
}
|
|
|
|
address = nint.Zero;
|
|
return false;
|
|
}
|
|
|
|
public async Task<bool> WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default, int timeOutMs = 30000)
|
|
{
|
|
if (address == nint.Zero)
|
|
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
|
|
|
var timeoutAt = timeOutMs > 0 ? Environment.TickCount64 + timeOutMs : long.MaxValue;
|
|
while (true)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var loadState = await _framework.RunOnFrameworkThread(() => GetObjectLoadState(address)).ConfigureAwait(false);
|
|
if (!loadState.IsValid)
|
|
return false;
|
|
|
|
if (!IsZoning && loadState.IsLoaded)
|
|
return true;
|
|
|
|
if (Environment.TickCount64 >= timeoutAt)
|
|
return false;
|
|
|
|
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))
|
|
{
|
|
if (!TryGetLivePlayerHash(descriptor, out var liveHash))
|
|
{
|
|
UntrackGameObject(descriptor.Address);
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal))
|
|
{
|
|
UntrackGameObject(descriptor.Address);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool TryGetLivePlayerHash(ActorDescriptor descriptor, out string liveHash)
|
|
{
|
|
liveHash = string.Empty;
|
|
|
|
if (_objectTable.CreateObjectReference(descriptor.Address) is not IPlayerCharacter playerCharacter)
|
|
return false;
|
|
|
|
return DalamudUtilService.TryGetHashedCID(playerCharacter, out liveHash);
|
|
}
|
|
|
|
public void RefreshTrackedActors(bool force = false)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if (!force && _hooksActive)
|
|
{
|
|
if (now < _nextRefreshAllowed)
|
|
return;
|
|
|
|
_nextRefreshAllowed = now + SnapshotRefreshInterval;
|
|
}
|
|
|
|
if (_framework.IsInFrameworkUpdateThread)
|
|
{
|
|
RefreshTrackedActorsInternal();
|
|
}
|
|
else
|
|
{
|
|
_ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
|
|
}
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
InitializeHooks();
|
|
var warmupTask = WarmupExistingActors();
|
|
return warmupTask;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to initialize ActorObjectService hooks, falling back to empty cache.");
|
|
DisposeHooks();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
DisposeHooks();
|
|
_activePlayers.Clear();
|
|
_gposePlayers.Clear();
|
|
_actorsByHash.Clear();
|
|
_actorsByName.Clear();
|
|
_pendingHashResolutions.Clear();
|
|
_mediator.UnsubscribeAll(this);
|
|
lock (_playerRelatedHandlerLock)
|
|
{
|
|
_playerRelatedHandlers.Clear();
|
|
}
|
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
|
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private unsafe void InitializeHooks()
|
|
{
|
|
if (_hooksActive)
|
|
return;
|
|
|
|
_onInitializeHook = _interop.HookFromAddress<Character.Delegates.OnInitialize>(
|
|
(nint)Character.StaticVirtualTablePointer->OnInitialize,
|
|
OnCharacterInitialized);
|
|
|
|
_onTerminateHook = _interop.HookFromAddress<Character.Delegates.Terminate>(
|
|
(nint)Character.StaticVirtualTablePointer->Terminate,
|
|
OnCharacterTerminated);
|
|
|
|
_onDestructorHook = _interop.HookFromAddress<Character.Delegates.Dtor>(
|
|
(nint)Character.StaticVirtualTablePointer->Dtor,
|
|
OnCharacterDisposed);
|
|
|
|
_onCompanionInitializeHook = _interop.HookFromAddress<Companion.Delegates.OnInitialize>(
|
|
(nint)Companion.StaticVirtualTablePointer->OnInitialize,
|
|
OnCompanionInitialized);
|
|
|
|
_onCompanionTerminateHook = _interop.HookFromAddress<Companion.Delegates.Terminate>(
|
|
(nint)Companion.StaticVirtualTablePointer->Terminate,
|
|
OnCompanionTerminated);
|
|
|
|
_onInitializeHook.Enable();
|
|
_onTerminateHook.Enable();
|
|
_onDestructorHook.Enable();
|
|
_onCompanionInitializeHook.Enable();
|
|
_onCompanionTerminateHook.Enable();
|
|
|
|
_hooksActive = true;
|
|
_logger.LogTrace("ActorObjectService hooks enabled.");
|
|
}
|
|
|
|
private Task WarmupExistingActors()
|
|
{
|
|
return _framework.RunOnFrameworkThread(() =>
|
|
{
|
|
RefreshTrackedActorsInternal();
|
|
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
|
});
|
|
}
|
|
|
|
private unsafe void OnCharacterInitialized(Character* chara)
|
|
{
|
|
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
|
QueueTrack((GameObject*)chara);
|
|
}
|
|
|
|
private unsafe void OnCharacterTerminated(Character* chara)
|
|
{
|
|
var address = (nint)chara;
|
|
QueueUntrack(address);
|
|
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
|
}
|
|
|
|
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
|
{
|
|
var address = (nint)chara;
|
|
QueueUntrack(address);
|
|
try
|
|
{
|
|
return _onDestructorHook!.Original(chara, freeMemory);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error invoking original character destructor.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private unsafe void TrackGameObject(GameObject* gameObject)
|
|
{
|
|
if (gameObject == null)
|
|
return;
|
|
|
|
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
|
|
|
if (!IsSupportedObjectKind(objectKind))
|
|
return;
|
|
|
|
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
|
|
return;
|
|
|
|
if (descriptor.ObjectKind != DalamudObjectKind.Player && descriptor.OwnedKind is null)
|
|
return;
|
|
|
|
if (_activePlayers.TryGetValue(descriptor.Address, out var existing))
|
|
{
|
|
RemoveDescriptor(existing);
|
|
}
|
|
|
|
AddDescriptor(descriptor);
|
|
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
_logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
|
descriptor.Name,
|
|
descriptor.Address,
|
|
descriptor.ObjectIndex,
|
|
descriptor.OwnedKind?.ToString() ?? "<none>",
|
|
descriptor.IsLocalPlayer,
|
|
descriptor.IsInGpose);
|
|
}
|
|
|
|
_mediator.Publish(new ActorTrackedMessage(descriptor));
|
|
}
|
|
|
|
private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
|
|
{
|
|
if (gameObject == null)
|
|
return null;
|
|
|
|
var address = (nint)gameObject;
|
|
string name = string.Empty;
|
|
ushort objectIndex = gameObject->ObjectIndex;
|
|
bool isInGpose = objectIndex >= 200;
|
|
bool isLocal = _objectTable.LocalPlayer?.Address == address;
|
|
string hashedCid = string.Empty;
|
|
|
|
IPlayerCharacter? resolvedPlayer = null;
|
|
if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter)
|
|
{
|
|
resolvedPlayer = playerCharacter;
|
|
name = playerCharacter.Name.TextValue ?? string.Empty;
|
|
objectIndex = playerCharacter.ObjectIndex;
|
|
isInGpose = objectIndex >= 200;
|
|
isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address;
|
|
}
|
|
else
|
|
{
|
|
name = gameObject->NameString ?? string.Empty;
|
|
}
|
|
|
|
if (objectKind == DalamudObjectKind.Player)
|
|
{
|
|
if (resolvedPlayer == null || !DalamudUtilService.TryGetHashedCID(resolvedPlayer, out hashedCid))
|
|
{
|
|
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
|
|
}
|
|
}
|
|
|
|
var (ownedKind, ownerEntityId) = DetermineOwnedKind(gameObject, objectKind, isLocal);
|
|
|
|
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
|
}
|
|
|
|
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
|
{
|
|
if (gameObject == null)
|
|
return (null, 0);
|
|
|
|
if (objectKind == DalamudObjectKind.Player)
|
|
{
|
|
var entityId = ((Character*)gameObject)->EntityId;
|
|
return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
|
|
}
|
|
|
|
var ownerId = ResolveOwnerId(gameObject);
|
|
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
|
if (localPlayerAddress == nint.Zero)
|
|
return (null, ownerId);
|
|
|
|
var localEntityId = ((Character*)localPlayerAddress)->EntityId;
|
|
if (localEntityId == 0)
|
|
return (null, ownerId);
|
|
|
|
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
|
{
|
|
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
|
if (expectedMinionOrMount != nint.Zero
|
|
&& (nint)gameObject == expectedMinionOrMount
|
|
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
|
|
{
|
|
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
|
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
|
}
|
|
}
|
|
|
|
if (objectKind != DalamudObjectKind.BattleNpc)
|
|
return (null, ownerId);
|
|
|
|
if (ownerId != localEntityId)
|
|
return (null, ownerId);
|
|
|
|
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
|
if (expectedPet != nint.Zero
|
|
&& (nint)gameObject == expectedPet
|
|
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
|
return (LightlessObjectKind.Pet, ownerId);
|
|
|
|
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
|
if (expectedCompanion != nint.Zero
|
|
&& (nint)gameObject == expectedCompanion
|
|
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
|
return (LightlessObjectKind.Companion, ownerId);
|
|
|
|
return (null, ownerId);
|
|
}
|
|
|
|
private bool IsPlayerRelatedOwnedAddress(nint address, LightlessObjectKind expectedKind)
|
|
{
|
|
if (address == nint.Zero)
|
|
return false;
|
|
|
|
lock (_playerRelatedHandlerLock)
|
|
{
|
|
foreach (var handler in _playerRelatedHandlers)
|
|
{
|
|
if (handler.Address == address && handler.ObjectKind == expectedKind)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
{
|
|
if (localPlayerAddress == nint.Zero)
|
|
return nint.Zero;
|
|
|
|
var playerObject = (GameObject*)localPlayerAddress;
|
|
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
|
if (ownerEntityId == 0)
|
|
return nint.Zero;
|
|
|
|
if (candidateAddress != nint.Zero)
|
|
{
|
|
var candidate = (GameObject*)candidateAddress;
|
|
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
|
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
|
{
|
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
return candidateAddress;
|
|
}
|
|
}
|
|
|
|
foreach (var obj in _objectTable)
|
|
{
|
|
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
continue;
|
|
|
|
if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
|
continue;
|
|
|
|
var candidate = (GameObject*)obj.Address;
|
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
return obj.Address;
|
|
}
|
|
|
|
return nint.Zero;
|
|
}
|
|
|
|
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
{
|
|
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
|
return nint.Zero;
|
|
|
|
var manager = CharacterManager.Instance();
|
|
if (manager != null)
|
|
{
|
|
var candidate = (nint)manager->LookupPetByOwnerObject((BattleChara*)localPlayerAddress);
|
|
if (candidate != nint.Zero)
|
|
{
|
|
var candidateObj = (GameObject*)candidate;
|
|
if (IsPetMatch(candidateObj, ownerEntityId))
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
foreach (var obj in _objectTable)
|
|
{
|
|
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
continue;
|
|
|
|
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
continue;
|
|
|
|
var candidate = (GameObject*)obj.Address;
|
|
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
continue;
|
|
|
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
return obj.Address;
|
|
}
|
|
|
|
return nint.Zero;
|
|
}
|
|
|
|
private unsafe nint GetCompanionAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
{
|
|
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
|
return nint.Zero;
|
|
|
|
var manager = CharacterManager.Instance();
|
|
if (manager != null)
|
|
{
|
|
var candidate = (nint)manager->LookupBuddyByOwnerObject((BattleChara*)localPlayerAddress);
|
|
if (candidate != nint.Zero)
|
|
{
|
|
var candidateObj = (GameObject*)candidate;
|
|
if (IsCompanionMatch(candidateObj, ownerEntityId))
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
foreach (var obj in _objectTable)
|
|
{
|
|
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
continue;
|
|
|
|
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
continue;
|
|
|
|
var candidate = (GameObject*)obj.Address;
|
|
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
|
|
continue;
|
|
|
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
return obj.Address;
|
|
}
|
|
|
|
return nint.Zero;
|
|
}
|
|
|
|
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
|
{
|
|
if (candidate == null)
|
|
return false;
|
|
|
|
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
|
|
return false;
|
|
|
|
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
return false;
|
|
|
|
return ResolveOwnerId(candidate) == ownerEntityId;
|
|
}
|
|
|
|
private static unsafe bool IsCompanionMatch(GameObject* candidate, uint ownerEntityId)
|
|
{
|
|
if (candidate == null)
|
|
return false;
|
|
|
|
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
|
|
return false;
|
|
|
|
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
|
|
return false;
|
|
|
|
return ResolveOwnerId(candidate) == ownerEntityId;
|
|
}
|
|
|
|
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
|
{
|
|
if (gameObject == null)
|
|
return 0;
|
|
|
|
if (gameObject->OwnerId != 0)
|
|
return gameObject->OwnerId;
|
|
|
|
var character = (Character*)gameObject;
|
|
if (character == null)
|
|
return 0;
|
|
|
|
if (character->CompanionOwnerId != 0)
|
|
return character->CompanionOwnerId;
|
|
|
|
var parent = character->GetParentCharacter();
|
|
return parent != null ? parent->EntityId : 0;
|
|
}
|
|
|
|
private void UntrackGameObject(nint address)
|
|
{
|
|
if (address == nint.Zero)
|
|
return;
|
|
|
|
if (_activePlayers.TryRemove(address, out var descriptor))
|
|
{
|
|
RemoveDescriptor(descriptor);
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
_logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
|
descriptor.Name,
|
|
descriptor.Address,
|
|
descriptor.ObjectIndex,
|
|
descriptor.OwnedKind?.ToString() ?? "<none>");
|
|
}
|
|
|
|
_mediator.Publish(new ActorUntrackedMessage(descriptor));
|
|
}
|
|
}
|
|
|
|
private unsafe void RefreshTrackedActorsInternal()
|
|
{
|
|
var addresses = EnumerateActiveCharacterAddresses();
|
|
HashSet<nint> seen = new(addresses.Count);
|
|
|
|
foreach (var address in addresses)
|
|
{
|
|
if (address == nint.Zero)
|
|
continue;
|
|
|
|
if (!seen.Add(address))
|
|
continue;
|
|
|
|
var gameObject = (GameObject*)address;
|
|
if (_activePlayers.TryGetValue(address, out var existing))
|
|
{
|
|
RefreshDescriptorIfNeeded(existing, gameObject);
|
|
continue;
|
|
}
|
|
|
|
TrackGameObject(gameObject);
|
|
}
|
|
|
|
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
|
foreach (var staleAddress in stale)
|
|
{
|
|
UntrackGameObject(staleAddress);
|
|
}
|
|
|
|
if (_hooksActive)
|
|
{
|
|
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
|
}
|
|
|
|
if (_clientState.IsGPosing)
|
|
{
|
|
RefreshGposeActorsInternal();
|
|
}
|
|
else if (!_gposePlayers.IsEmpty)
|
|
{
|
|
_gposePlayers.Clear();
|
|
PublishGposeSnapshot();
|
|
}
|
|
}
|
|
|
|
private unsafe void RefreshDescriptorIfNeeded(ActorDescriptor existing, GameObject* gameObject)
|
|
{
|
|
if (gameObject == null)
|
|
return;
|
|
|
|
if (existing.ObjectKind != DalamudObjectKind.Player || !string.IsNullOrEmpty(existing.HashedContentId))
|
|
return;
|
|
|
|
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
|
if (!IsSupportedObjectKind(objectKind))
|
|
return;
|
|
|
|
if (BuildDescriptor(gameObject, objectKind) is not { } updated)
|
|
return;
|
|
|
|
if (string.IsNullOrEmpty(updated.HashedContentId))
|
|
return;
|
|
|
|
ReplaceDescriptor(existing, updated);
|
|
_mediator.Publish(new ActorTrackedMessage(updated));
|
|
}
|
|
|
|
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
|
|
{
|
|
RemoveDescriptorFromIndexes(existing);
|
|
_activePlayers[updated.Address] = updated;
|
|
IndexDescriptor(updated);
|
|
UpdatePendingHashResolutions(updated);
|
|
PublishSnapshot();
|
|
}
|
|
|
|
private void IndexDescriptor(ActorDescriptor descriptor)
|
|
{
|
|
if (!string.IsNullOrEmpty(descriptor.HashedContentId))
|
|
{
|
|
_actorsByHash[descriptor.HashedContentId] = descriptor;
|
|
}
|
|
|
|
if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name))
|
|
{
|
|
var bucket = _actorsByName.GetOrAdd(descriptor.Name, _ => new ConcurrentDictionary<nint, ActorDescriptor>());
|
|
bucket[descriptor.Address] = descriptor;
|
|
}
|
|
}
|
|
|
|
private static bool IsBetterNameMatch(ActorDescriptor candidate, ActorDescriptor current)
|
|
{
|
|
if (!candidate.IsInGpose && current.IsInGpose)
|
|
return true;
|
|
if (candidate.IsInGpose && !current.IsInGpose)
|
|
return false;
|
|
|
|
return candidate.ObjectIndex < current.ObjectIndex;
|
|
}
|
|
|
|
private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor)
|
|
=> _activePlayers.TryGetValue(address, out descriptor);
|
|
|
|
private unsafe void OnCompanionInitialized(Companion* companion)
|
|
{
|
|
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
|
QueueTrack((GameObject*)companion);
|
|
}
|
|
|
|
private unsafe void OnCompanionTerminated(Companion* companion)
|
|
{
|
|
var address = (nint)companion;
|
|
QueueUntrack(address);
|
|
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
|
}
|
|
|
|
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
|
{
|
|
if (!string.IsNullOrEmpty(descriptor.HashedContentId))
|
|
{
|
|
_actorsByHash.TryRemove(descriptor.HashedContentId, out _);
|
|
}
|
|
|
|
if (descriptor.ObjectKind == DalamudObjectKind.Player
|
|
&& !string.IsNullOrEmpty(descriptor.Name)
|
|
&& _actorsByName.TryGetValue(descriptor.Name, out var bucket))
|
|
{
|
|
bucket.TryRemove(descriptor.Address, out _);
|
|
if (bucket.IsEmpty)
|
|
{
|
|
_actorsByName.TryRemove(descriptor.Name, out _);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddDescriptor(ActorDescriptor descriptor)
|
|
{
|
|
_activePlayers[descriptor.Address] = descriptor;
|
|
IndexDescriptor(descriptor);
|
|
UpdatePendingHashResolutions(descriptor);
|
|
PublishSnapshot();
|
|
}
|
|
|
|
private void RemoveDescriptor(ActorDescriptor descriptor)
|
|
{
|
|
RemoveDescriptorFromIndexes(descriptor);
|
|
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
|
PublishSnapshot();
|
|
}
|
|
|
|
private void UpdatePendingHashResolutions(ActorDescriptor descriptor)
|
|
{
|
|
if (descriptor.ObjectKind != DalamudObjectKind.Player)
|
|
{
|
|
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(descriptor.HashedContentId))
|
|
{
|
|
_pendingHashResolutions[descriptor.Address] = 1;
|
|
return;
|
|
}
|
|
|
|
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
|
}
|
|
|
|
private void PublishSnapshot()
|
|
{
|
|
var descriptors = _activePlayers.Values.ToArray();
|
|
var playerCount = 0;
|
|
var ownedCount = 0;
|
|
var companionCount = 0;
|
|
|
|
foreach (var descriptor in descriptors)
|
|
{
|
|
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
|
playerCount++;
|
|
|
|
if (descriptor.OwnedKind is not null)
|
|
ownedCount++;
|
|
|
|
if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
|
companionCount++;
|
|
}
|
|
|
|
var playerDescriptors = new ActorDescriptor[playerCount];
|
|
var ownedDescriptors = new ActorDescriptor[ownedCount];
|
|
var playerAddresses = new nint[playerCount];
|
|
var renderedCompanions = new nint[companionCount];
|
|
var ownedAddresses = new nint[ownedCount];
|
|
var ownedMap = new Dictionary<nint, LightlessObjectKind>(ownedCount);
|
|
nint localPlayer = nint.Zero;
|
|
nint localPet = nint.Zero;
|
|
nint localMinionOrMount = nint.Zero;
|
|
nint localCompanion = nint.Zero;
|
|
|
|
var playerIndex = 0;
|
|
var ownedIndex = 0;
|
|
var companionIndex = 0;
|
|
|
|
foreach (var descriptor in descriptors)
|
|
{
|
|
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
|
{
|
|
playerDescriptors[playerIndex] = descriptor;
|
|
playerAddresses[playerIndex] = descriptor.Address;
|
|
playerIndex++;
|
|
}
|
|
|
|
if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
|
{
|
|
renderedCompanions[companionIndex] = descriptor.Address;
|
|
companionIndex++;
|
|
}
|
|
|
|
if (descriptor.OwnedKind is not { } ownedKind)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ownedDescriptors[ownedIndex] = descriptor;
|
|
ownedAddresses[ownedIndex] = descriptor.Address;
|
|
ownedMap[descriptor.Address] = ownedKind;
|
|
|
|
switch (ownedKind)
|
|
{
|
|
case LightlessObjectKind.Player:
|
|
localPlayer = descriptor.Address;
|
|
break;
|
|
case LightlessObjectKind.Pet:
|
|
localPet = descriptor.Address;
|
|
break;
|
|
case LightlessObjectKind.MinionOrMount:
|
|
localMinionOrMount = descriptor.Address;
|
|
break;
|
|
case LightlessObjectKind.Companion:
|
|
localCompanion = descriptor.Address;
|
|
break;
|
|
}
|
|
|
|
ownedIndex++;
|
|
}
|
|
|
|
var ownedSnapshot = new OwnedObjectSnapshot(
|
|
playerAddresses,
|
|
renderedCompanions,
|
|
ownedAddresses,
|
|
ownedMap,
|
|
localPlayer,
|
|
localPet,
|
|
localMinionOrMount,
|
|
localCompanion);
|
|
var nextGeneration = Snapshot.Generation + 1;
|
|
var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
|
Volatile.Write(ref _snapshot, snapshot);
|
|
}
|
|
|
|
private void QueueFrameworkUpdate(Action action)
|
|
{
|
|
if (action == null)
|
|
return;
|
|
|
|
if (_framework.IsInFrameworkUpdateThread)
|
|
{
|
|
action();
|
|
return;
|
|
}
|
|
|
|
_ = _framework.RunOnFrameworkThread(action);
|
|
}
|
|
|
|
private void ExecuteOriginal(Action action, string errorMessage)
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, errorMessage);
|
|
}
|
|
}
|
|
|
|
private unsafe void QueueTrack(GameObject* gameObject)
|
|
=> QueueFrameworkUpdate(() => TrackGameObject(gameObject));
|
|
|
|
private void QueueUntrack(nint address)
|
|
=> QueueFrameworkUpdate(() => UntrackGameObject(address));
|
|
|
|
private void DisposeHooks()
|
|
{
|
|
var hadHooks = _hooksActive
|
|
|| _onInitializeHook is not null
|
|
|| _onTerminateHook is not null
|
|
|| _onDestructorHook is not null
|
|
|| _onCompanionInitializeHook is not null
|
|
|| _onCompanionTerminateHook is not null;
|
|
|
|
_onInitializeHook?.Disable();
|
|
_onTerminateHook?.Disable();
|
|
_onDestructorHook?.Disable();
|
|
_onCompanionInitializeHook?.Disable();
|
|
_onCompanionTerminateHook?.Disable();
|
|
|
|
_onInitializeHook?.Dispose();
|
|
_onTerminateHook?.Dispose();
|
|
_onDestructorHook?.Dispose();
|
|
_onCompanionInitializeHook?.Dispose();
|
|
_onCompanionTerminateHook?.Dispose();
|
|
|
|
_onInitializeHook = null;
|
|
_onTerminateHook = null;
|
|
_onDestructorHook = null;
|
|
_onCompanionInitializeHook = null;
|
|
_onCompanionTerminateHook = null;
|
|
|
|
_hooksActive = false;
|
|
|
|
if (hadHooks)
|
|
{
|
|
_logger.LogTrace("ActorObjectService hooks disabled.");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DisposeHooks();
|
|
_mediator.UnsubscribeAll(this);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private static bool IsSupportedObjectKind(DalamudObjectKind objectKind) =>
|
|
objectKind is DalamudObjectKind.Player
|
|
or DalamudObjectKind.BattleNpc
|
|
or DalamudObjectKind.Companion
|
|
or DalamudObjectKind.MountType;
|
|
|
|
private static unsafe List<nint> EnumerateActiveCharacterAddresses()
|
|
{
|
|
var results = new List<nint>(64);
|
|
var manager = GameObjectManager.Instance();
|
|
if (manager == null)
|
|
return results;
|
|
|
|
const int objectLimit = 200;
|
|
|
|
unsafe
|
|
{
|
|
for (var i = 0; i < objectLimit; i++)
|
|
{
|
|
Pointer<GameObject> objPtr = manager->Objects.IndexSorted[i];
|
|
var obj = objPtr.Value;
|
|
if (obj == null)
|
|
continue;
|
|
|
|
var objectKind = (DalamudObjectKind)obj->ObjectKind;
|
|
if (!IsSupportedObjectKind(objectKind))
|
|
continue;
|
|
|
|
results.Add((nint)obj);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private unsafe void RefreshGposeActorsInternal()
|
|
{
|
|
var addresses = EnumerateGposeCharacterAddresses();
|
|
HashSet<nint> seen = new(addresses.Count);
|
|
|
|
foreach (var address in addresses)
|
|
{
|
|
if (address == nint.Zero)
|
|
continue;
|
|
|
|
if (!seen.Add(address))
|
|
continue;
|
|
|
|
if (_gposePlayers.ContainsKey(address))
|
|
continue;
|
|
|
|
TrackGposeObject((GameObject*)address);
|
|
}
|
|
|
|
var stale = _gposePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
|
foreach (var staleAddress in stale)
|
|
{
|
|
UntrackGposeObject(staleAddress);
|
|
}
|
|
|
|
PublishGposeSnapshot();
|
|
}
|
|
|
|
private unsafe void TrackGposeObject(GameObject* gameObject)
|
|
{
|
|
if (gameObject == null)
|
|
return;
|
|
|
|
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
|
if (objectKind != DalamudObjectKind.Player)
|
|
return;
|
|
|
|
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
|
|
return;
|
|
|
|
if (!descriptor.IsInGpose)
|
|
return;
|
|
|
|
_gposePlayers[descriptor.Address] = descriptor;
|
|
}
|
|
|
|
private void UntrackGposeObject(nint address)
|
|
{
|
|
if (address == nint.Zero)
|
|
return;
|
|
|
|
_gposePlayers.TryRemove(address, out _);
|
|
}
|
|
|
|
private void PublishGposeSnapshot()
|
|
{
|
|
var gposeDescriptors = _gposePlayers.Values.ToArray();
|
|
var gposeAddresses = new nint[gposeDescriptors.Length];
|
|
for (var i = 0; i < gposeDescriptors.Length; i++)
|
|
gposeAddresses[i] = gposeDescriptors[i].Address;
|
|
|
|
var nextGeneration = CurrentGposeSnapshot.Generation + 1;
|
|
var snapshot = new GposeSnapshot(gposeDescriptors, gposeAddresses, nextGeneration);
|
|
Volatile.Write(ref _gposeSnapshot, snapshot);
|
|
}
|
|
|
|
private List<nint> EnumerateGposeCharacterAddresses()
|
|
{
|
|
var results = new List<nint>(16);
|
|
foreach (var obj in _objectTable)
|
|
{
|
|
if (obj.ObjectKind != DalamudObjectKind.Player)
|
|
continue;
|
|
|
|
if (obj.ObjectIndex < 200)
|
|
continue;
|
|
|
|
results.Add(obj.Address);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private LoadState GetObjectLoadState(nint address)
|
|
{
|
|
if (address == nint.Zero)
|
|
return LoadState.Invalid;
|
|
|
|
var obj = _objectTable.CreateObjectReference(address);
|
|
if (obj is null || obj.Address != address)
|
|
return LoadState.Invalid;
|
|
|
|
return new LoadState(true, IsObjectFullyLoaded(address));
|
|
}
|
|
|
|
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 ((ulong)gameObject->RenderFlags == 2048)
|
|
return false;
|
|
|
|
var characterBase = (CharacterBase*)drawObject;
|
|
if (characterBase->HasModelInSlotLoaded != 0)
|
|
return false;
|
|
|
|
if (characterBase->HasModelFilesInSlotLoaded != 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
|
{
|
|
public static LoadState Invalid => new(false, false);
|
|
}
|
|
|
|
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<ActorDescriptor> OwnedDescriptors,
|
|
IReadOnlyList<nint> PlayerAddresses,
|
|
OwnedObjectSnapshot OwnedObjects,
|
|
int Generation)
|
|
{
|
|
public static ActorSnapshot Empty { get; } = new(
|
|
Array.Empty<ActorDescriptor>(),
|
|
Array.Empty<ActorDescriptor>(),
|
|
Array.Empty<nint>(),
|
|
OwnedObjectSnapshot.Empty,
|
|
0);
|
|
}
|
|
|
|
private sealed record GposeSnapshot(
|
|
IReadOnlyList<ActorDescriptor> GposeDescriptors,
|
|
IReadOnlyList<nint> GposeAddresses,
|
|
int Generation)
|
|
{
|
|
public static GposeSnapshot Empty { get; } = new(
|
|
Array.Empty<ActorDescriptor>(),
|
|
Array.Empty<nint>(),
|
|
0);
|
|
}
|
|
}
|