All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
1252 lines
41 KiB
C#
1252 lines
41 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);
|
|
});
|
|
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearTrackingState());
|
|
}
|
|
|
|
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();
|
|
ClearTrackingState();
|
|
_mediator.UnsubscribeAll(this);
|
|
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;
|
|
|
|
if (ownerEntityId == 0)
|
|
return nint.Zero;
|
|
|
|
var playerObject = (GameObject*)localPlayerAddress;
|
|
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
|
if (candidateAddress == nint.Zero)
|
|
return nint.Zero;
|
|
|
|
var candidate = (GameObject*)candidateAddress;
|
|
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
|
return candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
|
? candidateAddress
|
|
: 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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
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.");
|
|
}
|
|
}
|
|
|
|
private void ClearTrackingState()
|
|
{
|
|
_activePlayers.Clear();
|
|
_gposePlayers.Clear();
|
|
_actorsByHash.Clear();
|
|
_actorsByName.Clear();
|
|
_pendingHashResolutions.Clear();
|
|
lock (_playerRelatedHandlerLock)
|
|
{
|
|
_playerRelatedHandlers.Clear();
|
|
}
|
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
|
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
|
_nextRefreshAllowed = DateTime.MinValue;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|