Merged Cake and Abel branched into 2.0.3 (#131)

Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #131
This commit was merged in pull request #131.
This commit is contained in:
2026-01-05 00:45:14 +00:00
parent e0b8070aa8
commit 30717ba200
67 changed files with 13247 additions and 802 deletions

View File

@@ -6,6 +6,7 @@ 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;
@@ -16,7 +17,7 @@ using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.Services.ActorTracking;
public sealed class ActorObjectService : IHostedService, IDisposable
public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorSubscriber
{
public readonly record struct ActorDescriptor(
string Name,
@@ -36,6 +37,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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();
@@ -71,6 +74,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_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];
@@ -84,6 +106,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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)
@@ -213,18 +236,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return false;
}
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
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 isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
if (!IsZoning && isLoaded)
return;
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);
}
@@ -317,6 +347,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_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;
@@ -493,7 +528,9 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
{
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
if (expectedMinionOrMount != nint.Zero
&& (nint)gameObject == expectedMinionOrMount
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
{
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
@@ -507,16 +544,37 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return (null, ownerId);
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
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)
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)
@@ -524,20 +582,20 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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 (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId)
if (ResolveOwnerId(candidate) == ownerEntityId)
return candidateAddress;
}
}
if (ownerEntityId == 0)
return candidateAddress;
foreach (var obj in _objectTable)
{
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
@@ -551,7 +609,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return obj.Address;
}
return candidateAddress;
return nint.Zero;
}
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
@@ -1022,6 +1080,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
public void Dispose()
{
DisposeHooks();
_mediator.UnsubscribeAll(this);
GC.SuppressFinalize(this);
}
@@ -1143,6 +1202,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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)
@@ -1169,6 +1240,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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,