hopefully it's fine now?

This commit is contained in:
2025-12-24 07:43:23 +09:00
parent 2eba5a1f30
commit 7d151dac2b
24 changed files with 1101 additions and 352 deletions

View File

@@ -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);
}
}