This commit is contained in:
2025-12-28 05:24:12 +09:00
parent 1632258c4f
commit 8f32b375dd
27 changed files with 3040 additions and 482 deletions

View File

@@ -1,6 +1,5 @@
using System.Collections.Concurrent;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.Interop;
@@ -10,6 +9,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
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;
@@ -41,7 +42,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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;
@@ -151,15 +151,16 @@ public sealed class ActorObjectService : IHostedService, IDisposable
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
{
ownedKind = default;
var ownedSnapshot = OwnedObjects;
foreach (var (address, kind) in ownedSnapshot)
var ownedDescriptors = OwnedDescriptors;
for (var i = 0; i < ownedDescriptors.Count; i++)
{
if (!TryGetDescriptor(address, out var descriptor))
var descriptor = ownedDescriptors[i];
if (descriptor.ObjectIndex != objectIndex)
continue;
if (descriptor.ObjectIndex == objectIndex)
if (descriptor.OwnedKind is { } resolvedKind)
{
ownedKind = kind;
ownedKind = resolvedKind;
return true;
}
}
@@ -316,7 +317,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_actorsByHash.Clear();
_actorsByName.Clear();
_pendingHashResolutions.Clear();
_ownedTracker.Reset();
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
return Task.CompletedTask;
@@ -481,50 +481,196 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
}
if (isLocalPlayer)
{
var entityId = ((Character*)gameObject)->EntityId;
return (LightlessObjectKind.Player, entityId);
}
var ownerId = ResolveOwnerId(gameObject);
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
if (localPlayerAddress == nint.Zero)
return (null, ownerId);
if (_objectTable.LocalPlayer is not { } localPlayer)
return (null, 0);
var localEntityId = ((Character*)localPlayerAddress)->EntityId;
if (localEntityId == 0)
return (null, ownerId);
var ownerId = gameObject->OwnerId;
if (ownerId == 0)
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
{
var character = (Character*)gameObject;
if (character != null)
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
{
ownerId = character->CompanionOwnerId;
if (ownerId == 0)
{
var parent = character->GetParentCharacter();
if (parent != null)
{
ownerId = parent->EntityId;
}
}
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
}
}
if (ownerId == 0 || ownerId != localPlayer.EntityId)
if (objectKind != DalamudObjectKind.BattleNpc)
return (null, ownerId);
var ownedKind = objectKind switch
{
DalamudObjectKind.MountType => LightlessObjectKind.MinionOrMount,
DalamudObjectKind.Companion => LightlessObjectKind.MinionOrMount,
DalamudObjectKind.BattleNpc => gameObject->BattleNpcSubKind switch
{
BattleNpcSubKind.Buddy => LightlessObjectKind.Companion,
BattleNpcSubKind.Pet => LightlessObjectKind.Pet,
_ => (LightlessObjectKind?)null,
},
_ => (LightlessObjectKind?)null,
};
if (ownerId != localEntityId)
return (null, ownerId);
return (ownedKind, ownerId);
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
return (LightlessObjectKind.Pet, ownerId);
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
return (LightlessObjectKind.Companion, ownerId);
return (null, ownerId);
}
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 (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)
return candidateAddress;
}
}
if (ownerEntityId == 0)
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 candidateAddress;
}
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)
@@ -618,11 +764,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
{
RemoveDescriptorFromIndexes(existing);
_ownedTracker.OnDescriptorRemoved(existing);
_activePlayers[updated.Address] = updated;
IndexDescriptor(updated);
_ownedTracker.OnDescriptorAdded(updated);
UpdatePendingHashResolutions(updated);
PublishSnapshot();
}
@@ -690,7 +833,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{
_activePlayers[descriptor.Address] = descriptor;
IndexDescriptor(descriptor);
_ownedTracker.OnDescriptorAdded(descriptor);
UpdatePendingHashResolutions(descriptor);
PublishSnapshot();
}
@@ -698,7 +840,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private void RemoveDescriptor(ActorDescriptor descriptor)
{
RemoveDescriptorFromIndexes(descriptor);
_ownedTracker.OnDescriptorRemoved(descriptor);
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
PublishSnapshot();
}
@@ -722,17 +863,90 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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 descriptors = _activePlayers.Values.ToArray();
var playerCount = 0;
var ownedCount = 0;
var companionCount = 0;
var ownedSnapshot = _ownedTracker.CreateSnapshot();
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);
@@ -955,109 +1169,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return true;
}
private sealed class OwnedObjectTracker
{
private readonly HashSet<nint> _renderedPlayers = new();
private readonly HashSet<nint> _renderedCompanions = new();
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
private nint _localPlayerAddress = nint.Zero;
private nint _localPetAddress = nint.Zero;
private nint _localMinionMountAddress = nint.Zero;
private nint _localCompanionAddress = nint.Zero;
public void OnDescriptorAdded(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Add(descriptor.Address);
if (descriptor.IsLocalPlayer)
_localPlayerAddress = descriptor.Address;
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Add(descriptor.Address);
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects[descriptor.Address] = ownedKind;
switch (ownedKind)
{
case LightlessObjectKind.Player:
_localPlayerAddress = descriptor.Address;
break;
case LightlessObjectKind.Pet:
_localPetAddress = descriptor.Address;
break;
case LightlessObjectKind.MinionOrMount:
_localMinionMountAddress = descriptor.Address;
break;
case LightlessObjectKind.Companion:
_localCompanionAddress = descriptor.Address;
break;
}
}
}
public void OnDescriptorRemoved(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Remove(descriptor.Address);
if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
_localPlayerAddress = nint.Zero;
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Remove(descriptor.Address);
if (_localCompanionAddress == descriptor.Address)
_localCompanionAddress = nint.Zero;
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects.Remove(descriptor.Address);
switch (ownedKind)
{
case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
_localPlayerAddress = nint.Zero;
break;
case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
_localPetAddress = nint.Zero;
break;
case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
_localMinionMountAddress = nint.Zero;
break;
case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
_localCompanionAddress = nint.Zero;
break;
}
}
}
public OwnedObjectSnapshot CreateSnapshot()
=> new(
_renderedPlayers.ToArray(),
_renderedCompanions.ToArray(),
_ownedObjects.Keys.ToArray(),
new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
_localPlayerAddress,
_localPetAddress,
_localMinionMountAddress,
_localCompanionAddress);
public void Reset()
{
_renderedPlayers.Clear();
_renderedCompanions.Clear();
_ownedObjects.Clear();
_localPlayerAddress = nint.Zero;
_localPetAddress = nint.Zero;
_localMinionMountAddress = nint.Zero;
_localCompanionAddress = nint.Zero;
}
}
private sealed record OwnedObjectSnapshot(
IReadOnlyList<nint> RenderedPlayers,
IReadOnlyList<nint> RenderedCompanions,