hopefully it's fine now?
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
@@ -31,13 +32,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
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 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 readonly OwnedObjectTracker _ownedTracker = new();
|
||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
||||
|
||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||
@@ -55,21 +61,29 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
IGameInteropProvider interop,
|
||||
IObjectTable objectTable,
|
||||
IClientState clientState,
|
||||
ICondition condition,
|
||||
LightlessMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_interop = interop;
|
||||
_objectTable = objectTable;
|
||||
_clientState = clientState;
|
||||
_condition = condition;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
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> PlayerDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||
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 bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||
@@ -113,6 +127,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
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;
|
||||
@@ -207,7 +222,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||
if (isLoaded)
|
||||
if (!IsZoning && isLoaded)
|
||||
return;
|
||||
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
@@ -297,10 +312,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
DisposeHooks();
|
||||
_activePlayers.Clear();
|
||||
_gposePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
_pendingHashResolutions.Clear();
|
||||
_ownedTracker.Reset();
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -336,7 +354,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_onCompanionTerminateHook.Enable();
|
||||
|
||||
_hooksActive = true;
|
||||
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||
_logger.LogTrace("ActorObjectService hooks enabled.");
|
||||
}
|
||||
|
||||
private Task WarmupExistingActors()
|
||||
@@ -350,36 +368,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private unsafe void OnCharacterInitialized(Character* chara)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onInitializeHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
||||
QueueTrack((GameObject*)chara);
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterTerminated(Character* chara)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onTerminateHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character terminate.");
|
||||
}
|
||||
QueueUntrack(address);
|
||||
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
||||
}
|
||||
|
||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
QueueUntrack(address);
|
||||
try
|
||||
{
|
||||
return _onDestructorHook!.Original(chara, freeMemory);
|
||||
@@ -416,7 +419,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
_logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
@@ -534,7 +537,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
RemoveDescriptor(descriptor);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
_logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
@@ -558,10 +561,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
if (!seen.Add(address))
|
||||
continue;
|
||||
|
||||
if (_activePlayers.ContainsKey(address))
|
||||
var gameObject = (GameObject*)address;
|
||||
if (_activePlayers.TryGetValue(address, out var existing))
|
||||
{
|
||||
RefreshDescriptorIfNeeded(existing, gameObject);
|
||||
continue;
|
||||
}
|
||||
|
||||
TrackGameObject((GameObject*)address);
|
||||
TrackGameObject(gameObject);
|
||||
}
|
||||
|
||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||
@@ -574,6 +581,50 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
_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);
|
||||
_ownedTracker.OnDescriptorRemoved(existing);
|
||||
|
||||
_activePlayers[updated.Address] = updated;
|
||||
IndexDescriptor(updated);
|
||||
_ownedTracker.OnDescriptorAdded(updated);
|
||||
UpdatePendingHashResolutions(updated);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||
@@ -605,30 +656,15 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onCompanionInitializeHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
||||
QueueTrack((GameObject*)companion);
|
||||
}
|
||||
|
||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||
{
|
||||
var address = (nint)companion;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onCompanionTerminateHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion terminate.");
|
||||
}
|
||||
QueueUntrack(address);
|
||||
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
||||
}
|
||||
|
||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||
@@ -655,6 +691,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_activePlayers[descriptor.Address] = descriptor;
|
||||
IndexDescriptor(descriptor);
|
||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||
UpdatePendingHashResolutions(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
@@ -662,21 +699,42 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
RemoveDescriptorFromIndexes(descriptor);
|
||||
_ownedTracker.OnDescriptorRemoved(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 playerDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
.ToArray();
|
||||
var ownedDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.OwnedKind is not null)
|
||||
.ToArray();
|
||||
var playerAddresses = new nint[playerDescriptors.Length];
|
||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||
playerAddresses[i] = playerDescriptors[i].Address;
|
||||
|
||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||
var nextGeneration = Snapshot.Generation + 1;
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
@@ -694,6 +752,24 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_ = _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
|
||||
@@ -725,7 +801,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (hadHooks)
|
||||
{
|
||||
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||
_logger.LogTrace("ActorObjectService hooks disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,6 +846,89 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
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 static unsafe bool IsObjectFullyLoaded(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
@@ -783,13 +942,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
if (drawObject == null)
|
||||
return false;
|
||||
|
||||
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
||||
if ((ulong)gameObject->RenderFlags == 2048)
|
||||
return false;
|
||||
|
||||
var characterBase = (CharacterBase*)drawObject;
|
||||
if (characterBase == null)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
@@ -925,14 +1081,27 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user