Added debug information regarding minions

This commit is contained in:
cake
2026-01-15 06:11:07 +01:00
parent 9fcbd68ca2
commit f6a5c85c2d
9 changed files with 894 additions and 142 deletions

View File

@@ -1,22 +1,37 @@
using Microsoft.Extensions.Logging; using Dalamud.Plugin.Services;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
using LightlessSync.Interop.Ipc; using Microsoft.Extensions.Logging;
using LightlessSync.PlayerData.Pairs;
namespace LightlessSync.PlayerData.Handlers; namespace LightlessSync.PlayerData.Handlers;
internal sealed class OwnedObjectHandler internal sealed class OwnedObjectHandler
{ {
internal readonly record struct OwnedResolveDebug(
DateTime? ResolvedAtUtc,
nint Address,
ushort? ObjectIndex,
string Stage,
string? FailureReason)
{
public string? AddressHex => Address == nint.Zero ? null : $"0x{Address:X}";
public static OwnedResolveDebug Empty => new(null, nint.Zero, null, string.Empty, null);
}
private OwnedResolveDebug _minionResolveDebug = OwnedResolveDebug.Empty;
public OwnedResolveDebug MinionResolveDebug => _minionResolveDebug;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly GameObjectHandlerFactory _handlerFactory; private readonly GameObjectHandlerFactory _handlerFactory;
private readonly IpcManager _ipc; private readonly IpcManager _ipc;
private readonly ActorObjectService _actorObjectService; private readonly ActorObjectService _actorObjectService;
private IObjectTable _objectTable;
private const int _fullyLoadedTimeoutMsPlayer = 30000; private const int _fullyLoadedTimeoutMsPlayer = 30000;
private const int _fullyLoadedTimeoutMsOther = 5000; private const int _fullyLoadedTimeoutMsOther = 5000;
@@ -25,13 +40,15 @@ internal sealed class OwnedObjectHandler
DalamudUtilService dalamudUtil, DalamudUtilService dalamudUtil,
GameObjectHandlerFactory handlerFactory, GameObjectHandlerFactory handlerFactory,
IpcManager ipc, IpcManager ipc,
ActorObjectService actorObjectService) ActorObjectService actorObjectService,
IObjectTable objectTable)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_handlerFactory = handlerFactory; _handlerFactory = handlerFactory;
_ipc = ipc; _ipc = ipc;
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_objectTable = objectTable;
} }
public async Task<bool> ApplyAsync( public async Task<bool> ApplyAsync(
@@ -204,10 +221,29 @@ internal sealed class OwnedObjectHandler
private async Task<GameObjectHandler?> CreateHandlerAsync(ObjectKind kind, GameObjectHandler playerHandler, CancellationToken token) private async Task<GameObjectHandler?> CreateHandlerAsync(ObjectKind kind, GameObjectHandler playerHandler, CancellationToken token)
{ {
void SetMinionDebug(string stage, string? failure, nint addr = default, ushort? objIndex = null)
{
if (kind != ObjectKind.MinionOrMount)
return;
_minionResolveDebug = new OwnedResolveDebug(
DateTime.UtcNow,
addr,
objIndex,
stage,
failure);
}
if (kind == ObjectKind.Player) if (kind == ObjectKind.Player)
return playerHandler; return playerHandler;
var playerPtr = playerHandler.Address; var playerPtr = playerHandler.Address;
if (playerPtr == nint.Zero)
{
SetMinionDebug("player_ptr_zero", "playerHandler.Address == 0");
return null;
}
nint ownedPtr = kind switch nint ownedPtr = kind switch
{ {
ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false), ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false),
@@ -216,12 +252,115 @@ internal sealed class OwnedObjectHandler
_ => nint.Zero _ => nint.Zero
}; };
if (ownedPtr == nint.Zero) var stage = ownedPtr != nint.Zero ? "direct" : "direct_miss";
return null;
return await _handlerFactory.Create(kind, () => ownedPtr, isWatched: false).ConfigureAwait(false); if (ownedPtr == nint.Zero)
{
var ownerEntityId = playerHandler.EntityId;
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
{
ownerEntityId = await _dalamudUtil.RunOnFrameworkThread(() => ReadEntityIdUnsafe(playerPtr))
.ConfigureAwait(false);
}
if (ownerEntityId != 0 && ownerEntityId != uint.MaxValue)
{
ownedPtr = await _dalamudUtil.RunOnFrameworkThread(() => FindOwnedByOwnerIdUnsafe(kind, ownerEntityId))
.ConfigureAwait(false);
stage = ownedPtr != nint.Zero ? "owner_scan" : "owner_scan_miss";
}
else
{
stage = "owner_id_unavailable";
}
}
if (ownedPtr == nint.Zero)
{
SetMinionDebug(stage, "ownedPtr == 0");
return null;
}
var handler = await _handlerFactory.Create(kind, () => ownedPtr, isWatched: false).ConfigureAwait(false);
if (handler is null || handler.Address == nint.Zero)
{
SetMinionDebug(stage, "handlerFactory returned null/zero", ownedPtr);
return null;
}
ushort? objIndex = await _dalamudUtil.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
.ConfigureAwait(false);
SetMinionDebug(stage, null, handler.Address, objIndex);
return handler;
} }
private static unsafe uint ReadEntityIdUnsafe(nint playerPtr)
{
if (playerPtr == nint.Zero) return 0;
var ch = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)playerPtr;
return ch != null ? ch->EntityId : 0;
}
private unsafe nint FindOwnedByOwnerIdUnsafe(ObjectKind kind, uint ownerEntityId)
{
foreach (var obj in _objectTable)
{
if (obj is null || obj.Address == nint.Zero)
continue;
var addr = obj.Address;
var go = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)addr;
if (go == null)
continue;
var ok = kind switch
{
ObjectKind.MinionOrMount =>
obj.ObjectKind is Dalamud.Game.ClientState.Objects.Enums.ObjectKind.MountType
or Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Companion,
ObjectKind.Pet =>
obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.BattleNpc
&& go->BattleNpcSubKind == FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind.Pet,
ObjectKind.Companion =>
obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.BattleNpc
&& go->BattleNpcSubKind == FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind.Buddy,
_ => false
};
if (!ok)
continue;
var resolvedOwner = ResolveOwnerIdUnsafe(go);
if (resolvedOwner == ownerEntityId)
return addr;
}
return nint.Zero;
}
private static unsafe uint ResolveOwnerIdUnsafe(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject)
{
if (gameObject == null) return 0;
if (gameObject->OwnerId != 0)
return gameObject->OwnerId;
var character = (FFXIVClientStructs.FFXIV.Client.Game.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 async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds) private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
{ {
customizeIds[kind] = await _ipc.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false); customizeIds[kind] = await _ipc.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);

View File

@@ -1,43 +1,61 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary> /// <summary>
/// orchestrates the lifecycle of a paired character /// orchestrates the lifecycle of a paired character
/// </summary> /// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{ {
new string Ident { get; } new string Ident { get; }
bool Initialized { get; } bool Initialized { get; }
bool IsVisible { get; } bool IsVisible { get; }
bool ScheduledForDeletion { get; set; } bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; } CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; } long LastAppliedDataBytes { get; }
new string? PlayerName { get; } new string? PlayerName { get; }
string PlayerNameHash { get; } string PlayerNameHash { get; }
uint PlayerCharacterId { get; } uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; } DateTime? LastDataReceivedAt { get; }
DateTime? LastSuccessfulApplyAt { get; } DateTime? LastApplyAttemptAt { get; }
string? LastFailureReason { get; } DateTime? LastSuccessfulApplyAt { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; } string? LastFailureReason { get; }
bool IsDownloading { get; } IReadOnlyList<string> LastBlockingConditions { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; } bool IsApplying { get; }
bool PendingModReapply { get; } bool IsDownloading { get; }
bool ModApplyDeferred { get; } int PendingDownloadCount { get; }
int MissingCriticalMods { get; } int ForbiddenDownloadCount { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; } bool PendingModReapply { get; }
DateTime? InvisibleSinceUtc { get; } bool ModApplyDeferred { get; }
DateTime? VisibilityEvictionDueAtUtc { get; } int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
string? MinionAddressHex { get; }
ushort? MinionObjectIndex { get; }
DateTime? MinionResolvedAtUtc { get; }
string? MinionResolveStage { get; }
string? MinionResolveFailureReason { get; }
bool MinionPendingRetry { get; }
IReadOnlyList<string> MinionPendingRetryChanges { get; }
bool MinionHasAppearanceData { get; }
Guid OwnedPenumbraCollectionId { get; }
bool NeedsCollectionRebuildDebug { get; }
void Initialize(); void Initialize();
void ApplyData(CharacterData data); void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false); void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache(); bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data); void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading); void SetUploading(bool uploading);
void SetPaused(bool paused); void SetPaused(bool paused);
} }

View File

@@ -244,6 +244,17 @@ public class Pair
handler.ModApplyDeferred, handler.ModApplyDeferred,
handler.MissingCriticalMods, handler.MissingCriticalMods,
handler.MissingNonCriticalMods, handler.MissingNonCriticalMods,
handler.MissingForbiddenMods); handler.MissingForbiddenMods,
handler.MinionAddressHex,
handler.MinionObjectIndex,
handler.MinionResolvedAtUtc,
handler.MinionResolveStage,
handler.MinionResolveFailureReason,
handler.MinionPendingRetry,
handler.MinionPendingRetryChanges,
handler.MinionHasAppearanceData,
handler.OwnedPenumbraCollectionId,
handler.NeedsCollectionRebuildDebug);
} }
} }

View File

@@ -21,28 +21,50 @@ public sealed record PairDebugInfo(
bool ModApplyDeferred, bool ModApplyDeferred,
int MissingCriticalMods, int MissingCriticalMods,
int MissingNonCriticalMods, int MissingNonCriticalMods,
int MissingForbiddenMods) int MissingForbiddenMods,
string? MinionAddressHex,
ushort? MinionObjectIndex,
DateTime? MinionResolvedAtUtc,
string? MinionResolveStage,
string? MinionResolveFailureReason,
bool MinionPendingRetry,
IReadOnlyList<string> MinionPendingRetryChanges,
bool MinionHasAppearanceData,
Guid OwnedPenumbraCollectionId,
bool NeedsCollectionRebuild)
{ {
public static PairDebugInfo Empty { get; } = new( public static PairDebugInfo Empty { get; } = new(
false, HasHandler: false,
false, HandlerInitialized: false,
false, HandlerVisible: false,
false, HandlerScheduledForDeletion: false,
null, LastDataReceivedAt: null,
null, LastApplyAttemptAt: null,
null, LastSuccessfulApplyAt: null,
null, InvisibleSinceUtc: null,
null, VisibilityEvictionDueAtUtc: null,
null, VisibilityEvictionRemainingSeconds: null,
null, LastFailureReason: null,
Array.Empty<string>(), BlockingConditions: [],
false, IsApplying: false,
false, IsDownloading: false,
0, PendingDownloadCount: 0,
0, ForbiddenDownloadCount: 0,
false, PendingModReapply: false,
false, ModApplyDeferred: false,
0, MissingCriticalMods: 0,
0, MissingNonCriticalMods: 0,
0); MissingForbiddenMods: 0,
MinionAddressHex: null,
MinionObjectIndex: null,
MinionResolvedAtUtc: null,
MinionResolveStage: null,
MinionResolveFailureReason: null,
MinionPendingRetry: false,
MinionPendingRetryChanges: [],
MinionHasAppearanceData: false,
OwnedPenumbraCollectionId: Guid.Empty,
NeedsCollectionRebuild: false);
} }

View File

@@ -1,6 +1,5 @@
using System.Collections.Concurrent; using Dalamud.Plugin.Services;
using System.Diagnostics; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Dalamud.Plugin.Services;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
@@ -21,9 +20,11 @@ using LightlessSync.Utils;
using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer; using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
@@ -133,6 +134,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private ushort _lastKnownObjectIndex = ushort.MaxValue; private ushort _lastKnownObjectIndex = ushort.MaxValue;
private string? _lastKnownName; private string? _lastKnownName;
private readonly object _ownedReapplyGate = new();
private DateTime _nextOwnedReapplyUtc = DateTime.MinValue;
private static readonly TimeSpan OwnedReapplyThrottle = TimeSpan.FromSeconds(1);
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc; public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc; public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
public string Ident { get; } public string Ident { get; }
@@ -198,6 +203,54 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
public bool IsDownloading => _downloadManager.IsDownloading; public bool IsDownloading => _downloadManager.IsDownloading;
public int PendingDownloadCount => _downloadManager.CurrentDownloads.Count; public int PendingDownloadCount => _downloadManager.CurrentDownloads.Count;
public int ForbiddenDownloadCount => _downloadManager.ForbiddenTransfers.Count; public int ForbiddenDownloadCount => _downloadManager.ForbiddenTransfers.Count;
public string? MinionAddressHex => _ownedObjectHandler.MinionResolveDebug.AddressHex;
public ushort? MinionObjectIndex => _ownedObjectHandler.MinionResolveDebug.ObjectIndex;
public DateTime? MinionResolvedAtUtc => _ownedObjectHandler.MinionResolveDebug.ResolvedAtUtc;
public string? MinionResolveStage => string.IsNullOrEmpty(_ownedObjectHandler.MinionResolveDebug.Stage) ? null : _ownedObjectHandler.MinionResolveDebug.Stage;
public string? MinionResolveFailureReason => _ownedObjectHandler.MinionResolveDebug.FailureReason;
public bool MinionPendingRetry
{
get
{
lock (_ownedRetryGate)
return _pendingOwnedChanges.ContainsKey(ObjectKind.MinionOrMount);
}
}
public IReadOnlyList<string> MinionPendingRetryChanges
{
get
{
lock (_ownedRetryGate)
{
if (_pendingOwnedChanges.TryGetValue(ObjectKind.MinionOrMount, out var set))
return set.Select(s => s.ToString()).ToArray();
return Array.Empty<string>();
}
}
}
public bool MinionHasAppearanceData
{
get
{
var data = _cachedData ?? LastReceivedCharacterData ?? _pairStateCache.TryLoad(Ident);
return data is not null && HasAppearanceDataForKind(data, ObjectKind.MinionOrMount);
}
}
public Guid OwnedPenumbraCollectionId
{
get
{
lock (_collectionGate)
return _penumbraOwnedCollection;
}
}
public bool NeedsCollectionRebuildDebug => _needsCollectionRebuild;
public PairHandlerAdapter( public PairHandlerAdapter(
ILogger<PairHandlerAdapter> logger, ILogger<PairHandlerAdapter> logger,
@@ -210,6 +263,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
PluginWarningNotificationService pluginWarningNotificationManager, PluginWarningNotificationService pluginWarningNotificationManager,
DalamudUtilService dalamudUtil, DalamudUtilService dalamudUtil,
IFramework framework, IFramework framework,
IObjectTable objectTable,
ActorObjectService actorObjectService, ActorObjectService actorObjectService,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager, FileCacheManager fileDbManager,
@@ -247,7 +301,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_tempCollectionJanitor = tempCollectionJanitor; _tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer; _modelAnalyzer = modelAnalyzer;
_configService = configService; _configService = configService;
_ownedObjectHandler = new OwnedObjectHandler(Logger, _dalamudUtil, _gameObjectHandlerFactory, _ipcManager, _actorObjectService); _ownedObjectHandler = new OwnedObjectHandler(Logger, _dalamudUtil, _gameObjectHandlerFactory, _ipcManager, _actorObjectService, objectTable);
} }
public void Initialize() public void Initialize()
@@ -1304,6 +1358,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private async Task OwnedObjectRetryLoopAsync(CancellationToken token) private async Task OwnedObjectRetryLoopAsync(CancellationToken token)
{ {
var delay = OwnedRetryInitialDelay; var delay = OwnedRetryInitialDelay;
try try
{ {
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
@@ -1318,11 +1373,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
lock (_ownedRetryGate) lock (_ownedRetryGate)
{ {
if (_pendingOwnedChanges.Count == 0) if (_pendingOwnedChanges.Count == 0)
{
return; return;
}
pending = _pendingOwnedChanges.ToDictionary(kvp => kvp.Key, kvp => new HashSet<PlayerChanges>(kvp.Value)); pending = _pendingOwnedChanges.ToDictionary(
kvp => kvp.Key,
kvp => new HashSet<PlayerChanges>(kvp.Value));
} }
if (!IsVisible || IsPaused() || !CanApplyNow() || PlayerCharacter == nint.Zero || _charaHandler is null) if (!IsVisible || IsPaused() || !CanApplyNow() || PlayerCharacter == nint.Zero || _charaHandler is null)
@@ -1347,16 +1402,62 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
continue; continue;
} }
bool anyApplied = false; token.ThrowIfCancellationRequested();
foreach (var entry in pending)
var ownedPending = pending
.Where(k => k.Key != ObjectKind.Player)
.ToList();
if (ownedPending.Count == 0)
{ {
return;
}
var needsOwnedCollection =
_ipcManager.Penumbra.APIAvailable
&& ownedPending.Any(e =>
e.Value.Contains(PlayerChanges.ModFiles)
&& sanitized.FileReplacements.TryGetValue(e.Key, out var repls)
&& repls is { Count: > 0 });
Guid ownedCollection = Guid.Empty;
if (needsOwnedCollection)
{
ownedCollection = EnsureOwnedPenumbraCollection();
if (ownedCollection == Guid.Empty)
{
await Task.Delay(delay, token).ConfigureAwait(false);
delay = IncreaseRetryDelay(delay);
continue;
}
await TryRefreshOwnedCollectionModsAsync(ownedCollection, sanitized, token).ConfigureAwait(false);
}
bool anyApplied = false;
foreach (var entry in ownedPending)
{
token.ThrowIfCancellationRequested();
if (!HasAppearanceDataForKind(sanitized, entry.Key)) if (!HasAppearanceDataForKind(sanitized, entry.Key))
{ {
ClearOwnedObjectRetry(entry.Key); ClearOwnedObjectRetry(entry.Key);
continue; continue;
} }
var applied = await ApplyCustomizationDataAsync(Guid.NewGuid(), entry, sanitized, token).ConfigureAwait(false); var applied = await _ownedObjectHandler.ApplyAsync(
Guid.NewGuid(),
entry.Key,
entry.Value,
sanitized,
_charaHandler,
ownedCollection,
_customizeIds,
token)
.ConfigureAwait(false);
if (applied) if (applied)
{ {
ClearOwnedObjectRetry(entry.Key); ClearOwnedObjectRetry(entry.Key);
@@ -1385,6 +1486,95 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
} }
private async Task TryRefreshOwnedCollectionModsAsync(
Guid ownedCollection,
CharacterData sanitized,
CancellationToken token)
{
if (ownedCollection == Guid.Empty)
return;
if (!_ipcManager.Penumbra.APIAvailable)
return;
static bool IsOwnedKind(ObjectKind k) =>
k is ObjectKind.MinionOrMount or ObjectKind.Pet or ObjectKind.Companion;
var ownedGamePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var ownedFileSwaps = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in sanitized.FileReplacements)
{
if (!IsOwnedKind(kvp.Key))
continue;
foreach (var repl in kvp.Value)
{
if (!string.IsNullOrEmpty(repl.FileSwapPath))
{
foreach (var gp in repl.GamePaths)
{
if (!string.IsNullOrEmpty(gp))
ownedFileSwaps[gp] = repl.FileSwapPath!;
}
continue;
}
foreach (var gp in repl.GamePaths)
{
if (!string.IsNullOrEmpty(gp))
ownedGamePaths.Add(gp);
}
}
}
if (ownedGamePaths.Count == 0 && ownedFileSwaps.Count == 0)
return;
token.ThrowIfCancellationRequested();
Dictionary<(string GamePath, string? Hash), string>? resolved = null;
if (_lastAppliedModdedPaths is not null && _lastAppliedModdedPaths.Count > 0 && HasValidCachedModdedPaths())
{
resolved = _lastAppliedModdedPaths;
}
else
{
_ = TryCalculateModdedDictionary(Guid.NewGuid(), sanitized, out var recomputed, token);
resolved = recomputed;
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(resolved, resolved.Comparer);
}
token.ThrowIfCancellationRequested();
var ownedMods = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var kv in resolved)
{
var gp = kv.Key.GamePath;
if (ownedGamePaths.Contains(gp))
ownedMods[gp] = kv.Value;
}
foreach (var kv in ownedFileSwaps)
ownedMods[kv.Key] = kv.Value;
if (ownedMods.Count == 0)
return;
var refreshId = Guid.NewGuid();
Logger.LogDebug("[{appId}] Refreshing OWNED temp collection mods ({count} paths) for {handler}",
refreshId, ownedMods.Count, GetLogIdentifier());
await _ipcManager.Penumbra
.SetTemporaryModsAsync(Logger, refreshId, ownedCollection, ownedMods, scope: "OwnedRetryRefresh")
.ConfigureAwait(false);
}
private static TimeSpan IncreaseRetryDelay(TimeSpan delay) private static TimeSpan IncreaseRetryDelay(TimeSpan delay)
{ {
var nextMs = Math.Min(delay.TotalMilliseconds * 2, OwnedRetryMaxDelay.TotalMilliseconds); var nextMs = Math.Min(delay.TotalMilliseconds * 2, OwnedRetryMaxDelay.TotalMilliseconds);
@@ -1674,7 +1864,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
ObjectKind.Player => _charaHandler!, ObjectKind.Player => _charaHandler!,
ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanionPtr(ptr), isWatched: false).ConfigureAwait(false), ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanionPtr(ptr), isWatched: false).ConfigureAwait(false),
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMountPtr(ptr), isWatched: false).ConfigureAwait(false), ObjectKind.MinionOrMount => await CreateMinionOrMountHandlerAsync(token).ConfigureAwait(false),
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPetPtr(ptr), isWatched: false).ConfigureAwait(false), ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPetPtr(ptr), isWatched: false).ConfigureAwait(false),
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key) _ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
}; };
@@ -1777,8 +1967,36 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
await Task.WhenAll(tasks).ConfigureAwait(false); await Task.WhenAll(tasks).ConfigureAwait(false);
if (!isIpcOnly && needsRedraw && _ipcManager.Penumbra.APIAvailable) if (!isIpcOnly && needsRedraw && _ipcManager.Penumbra.APIAvailable)
{
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
if (handler.ObjectKind == ObjectKind.Player)
{
var fullyLoaded = await _actorObjectService
.WaitForFullyLoadedAsync(handler.Address, token, FullyLoadedTimeoutMsPlayer)
.ConfigureAwait(false);
if (!fullyLoaded)
{
Logger.LogDebug("[{applicationId}] Timed out waiting for PLAYER {handler} to fully load, skipping customization apply",
applicationId, handler);
return false;
}
}
else
{
var ready = await WaitForNonPlayerDrawableAsync(handler.Address, token, timeoutMs: FullyLoadedTimeoutMsOther)
.ConfigureAwait(false);
if (!ready)
{
Logger.LogDebug("[{applicationId}] Timed out waiting for OWNED {handler} to become drawable, skipping (will retry)",
applicationId, handler);
return false;
}
}
}
return true; return true;
} }
finally finally
@@ -1787,6 +2005,97 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
} }
private async Task<GameObjectHandler> CreateMinionOrMountHandlerAsync(CancellationToken token)
{
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => nint.Zero, isWatched: false).ConfigureAwait(false);
var ownedPtr = await ResolveMinionOrMountAddressAsync(_charaHandler, token).ConfigureAwait(false);
return await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => ownedPtr, isWatched: false).ConfigureAwait(false);
}
private static async Task<bool> WaitForNonPlayerDrawableAsync(nint address, CancellationToken token, int timeoutMs)
{
var until = Environment.TickCount64 + timeoutMs;
while (Environment.TickCount64 < until)
{
token.ThrowIfCancellationRequested();
if (IsNonPlayerDrawable(address))
return true;
await Task.Delay(100, token).ConfigureAwait(false);
}
return false;
}
private static unsafe bool IsNonPlayerDrawable(nint address)
{
if (address == nint.Zero)
return false;
var go = (GameObject*)address;
if (go == null)
return false;
if (go->DrawObject == null)
return false;
if ((ulong)go->RenderFlags == 2048)
return false;
return true;
}
private async Task<IReadOnlyList<nint>> ResolveMinionOrMountCandidatesAsync(GameObjectHandler playerHandler, CancellationToken token)
{
if (playerHandler is null || playerHandler.Address == nint.Zero)
return [];
return await _dalamudUtil.RunOnFrameworkThread(() =>
{
var ownerEntityId = playerHandler.EntityId;
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
return Array.Empty<nint>();
var objIndex = playerHandler.GetGameObject()?.ObjectIndex ?? (ushort)0;
return _actorObjectService.GetMinionOrMountCandidates(ownerEntityId, objIndex);
}).ConfigureAwait(false);
}
private async Task<nint> ResolveMinionOrMountAddressAsync(GameObjectHandler playerHandler, CancellationToken token)
{
if (playerHandler is null || playerHandler.Address == nint.Zero)
return nint.Zero;
var ownerEntityId = playerHandler.EntityId;
if (ownerEntityId != 0 && ownerEntityId != uint.MaxValue)
{
var owned = await _dalamudUtil.RunOnFrameworkThread(() =>
{
return _actorObjectService.TryFindOwnedObject(
ownerEntityId,
ObjectKind.MinionOrMount,
out var addr)
? addr
: nint.Zero;
}).ConfigureAwait(false);
if (owned != nint.Zero)
return owned;
}
try
{
return await _dalamudUtil.GetMinionOrMountAsync(playerHandler.Address).ConfigureAwait(false);
}
catch
{
return nint.Zero;
}
}
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData) private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
{ {
@@ -2137,9 +2446,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
kvp.Key != ObjectKind.Player kvp.Key != ObjectKind.Player
&& kvp.Value.Contains(PlayerChanges.ModFiles)); && kvp.Value.Contains(PlayerChanges.ModFiles));
var wantsOwnedCollectionAssignNow = var wantsOwnedCollectionAssignNow = needsOwnedCollectionAssign;
needsOwnedCollectionAssign
&& updateModdedPaths;
Guid ownedAssignCollection = Guid.Empty; Guid ownedAssignCollection = Guid.Empty;
if (wantsOwnedCollectionAssignNow) if (wantsOwnedCollectionAssignNow)
@@ -2187,38 +2494,47 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
var ownedPtr = await ResolveOtherOwnedPtrAsync(kvp.Key, handlerForApply.Address).ConfigureAwait(false); IReadOnlyList<nint> ownedPtrs;
if (ownedPtr != nint.Zero)
{
using var ownedHandler = await _gameObjectHandlerFactory
.Create(kvp.Key, () => ownedPtr, isWatched: false)
.ConfigureAwait(false);
if (ownedHandler.Address != nint.Zero) if (kvp.Key == ObjectKind.MinionOrMount)
ownedPtrs = await ResolveMinionOrMountCandidatesAsync(handlerForApply, token).ConfigureAwait(false);
else
ownedPtrs = new[] { await ResolveOtherOwnedPtrAsync(kvp.Key, handlerForApply.Address).ConfigureAwait(false) };
ownedPtrs = ownedPtrs.Where(p => p != nint.Zero).Distinct().ToArray();
if (ownedPtrs.Count > 0)
{
foreach (var ptr in ownedPtrs)
{ {
using var ownedHandler = await _gameObjectHandlerFactory
.Create(kvp.Key, () => ptr, isWatched: false)
.ConfigureAwait(false);
if (ownedHandler.Address == nint.Zero)
continue;
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => var objIndex = await _dalamudUtil.RunOnFrameworkThread(() =>
{ {
var go = ownedHandler.GetGameObject(); var go = ownedHandler.GetGameObject();
return go?.ObjectIndex; return go?.ObjectIndex;
}).ConfigureAwait(false); }).ConfigureAwait(false);
if (objIndex.HasValue) if (!objIndex.HasValue)
{ continue;
await _ipcManager.Penumbra
.AssignTemporaryCollectionAsync(Logger, ownedCollectionSnapshot, objIndex.Value)
.ConfigureAwait(false);
await _ipcManager.Penumbra await _ipcManager.Penumbra
.RedrawAsync(Logger, ownedHandler, Guid.NewGuid(), token) .AssignTemporaryCollectionAsync(Logger, ownedCollectionSnapshot, objIndex.Value)
.ConfigureAwait(false); .ConfigureAwait(false);
Logger.LogDebug( await _ipcManager.Penumbra
"Assigned OWNED temp collection {collection} to owned object {kind} for {handler} and redrew", .RedrawAsync(Logger, ownedHandler, Guid.NewGuid(), token)
ownedCollectionSnapshot, kvp.Key, GetLogIdentifier()); .ConfigureAwait(false);
break; Logger.LogDebug("Assigned OWNED temp collection {collection} to {kind} candidate idx={idx} for {handler}",
} ownedCollectionSnapshot, kvp.Key, objIndex.Value, GetLogIdentifier());
} }
break;
} }
await Task.Delay(delay, token).ConfigureAwait(false); await Task.Delay(delay, token).ConfigureAwait(false);
@@ -2421,6 +2737,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
// Final redraw // Final redraw
await _ipcManager.Penumbra.RedrawAsync(Logger, handlerForApply, _applicationId, token).ConfigureAwait(false); await _ipcManager.Penumbra.RedrawAsync(Logger, handlerForApply, _applicationId, token).ConfigureAwait(false);
if (handlerForApply.Address != nint.Zero)
{
await _actorObjectService
.WaitForFullyLoadedAsync(handlerForApply.Address, token, FullyLoadedTimeoutMsPlayer)
.ConfigureAwait(false);
}
// Cache last applied modded paths // Cache last applied modded paths
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer); _lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer);
} }
@@ -3026,18 +3349,104 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{ {
if (!TryResolveDescriptorHash(descriptor, out var hashedCid)) if (TryResolveDescriptorHash(descriptor, out var hashedCid)
&& string.Equals(hashedCid, Ident, StringComparison.Ordinal))
{
if (descriptor.Address == nint.Zero)
return;
UpdateLastKnownActor(descriptor);
RefreshTrackedHandler(descriptor);
QueueActorInitialization(descriptor);
return;
}
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return; return;
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal)) var ownerId = descriptor.OwnerEntityId;
if (ownerId == 0 || ownerId != _charaHandler.EntityId)
return; return;
if (descriptor.Address == nint.Zero) if (!TryMapOwnedKind(descriptor, out var ownedKind))
return; return;
UpdateLastKnownActor(descriptor); var data = _cachedData
RefreshTrackedHandler(descriptor); ?? LastReceivedCharacterData
QueueActorInitialization(descriptor); ?? _pairStateCache.TryLoad(Ident);
if (data is null)
return;
if (!HasAppearanceDataForKind(data, ownedKind))
return;
var changes = BuildOwnedChangeSetForKind(data, ownedKind);
if (changes.Count == 0)
return;
ScheduleOwnedObjectRetry(ownedKind, changes);
}
private static HashSet<PlayerChanges> BuildOwnedChangeSetForKind(CharacterData data, ObjectKind kind)
{
var changes = new HashSet<PlayerChanges>();
if (data.FileReplacements.TryGetValue(kind, out var repls) && repls is { Count: > 0 })
changes.Add(PlayerChanges.ModFiles);
if (data.GlamourerData.TryGetValue(kind, out var glamourer) && !string.IsNullOrEmpty(glamourer))
changes.Add(PlayerChanges.Glamourer);
if (data.CustomizePlusData.TryGetValue(kind, out var customize) && !string.IsNullOrEmpty(customize))
changes.Add(PlayerChanges.Customize);
return changes;
}
private static unsafe bool TryMapOwnedKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind kind)
{
kind = default;
switch (descriptor.ObjectKind)
{
case DalamudObjectKind.MountType:
kind = ObjectKind.MinionOrMount;
return true;
case DalamudObjectKind.Companion:
kind = ObjectKind.Companion;
return true;
case DalamudObjectKind.BattleNpc:
{
if (descriptor.Address == nint.Zero)
return false;
var go = (GameObject*)descriptor.Address;
if (go == null)
return false;
var subKind = go->BattleNpcSubKind;
if (subKind == BattleNpcSubKind.Pet)
{
kind = ObjectKind.Pet;
return true;
}
if (subKind == BattleNpcSubKind.Buddy)
{
kind = ObjectKind.Companion;
return true;
}
return false;
}
default:
return false;
}
} }
private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor) private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor)
@@ -3124,26 +3533,50 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
{ {
if (!TryResolveDescriptorHash(descriptor, out var hashedCid)) if (TryResolveDescriptorHash(descriptor, out var hashedCid))
{ {
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
return;
if (_charaHandler is null || _charaHandler.Address == nint.Zero) if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return; return;
if (descriptor.Address != _charaHandler.Address) if (descriptor.Address != _charaHandler.Address)
return; return;
}
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal)) HandleVisibilityLoss(logChange: false);
{
return; return;
} }
if (_charaHandler is null || _charaHandler.Address == nint.Zero) if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return; return;
if (descriptor.Address != _charaHandler.Address) var localEntityId = _charaHandler.EntityId;
return; if (localEntityId != 0 && localEntityId != uint.MaxValue
&& descriptor.OwnerEntityId != 0
&& descriptor.OwnerEntityId == localEntityId)
{
switch (descriptor.ObjectKind)
{
case DalamudObjectKind.MountType:
ClearOwnedObjectRetry(ObjectKind.MinionOrMount);
return;
HandleVisibilityLoss(logChange: false); case DalamudObjectKind.Companion:
ClearOwnedObjectRetry(ObjectKind.Companion);
return;
case DalamudObjectKind.BattleNpc:
ClearOwnedObjectRetry(ObjectKind.Pet);
ClearOwnedObjectRetry(ObjectKind.Companion);
return;
}
}
if (descriptor.Address == _charaHandler.Address)
{
HandleVisibilityLoss(logChange: false);
}
} }
private static bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid) private static bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)

View File

@@ -40,6 +40,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer; private readonly XivDataAnalyzer _modelAnalyzer;
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly IObjectTable _objectTable;
public PairHandlerAdapterFactory( public PairHandlerAdapterFactory(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@@ -63,7 +64,8 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
PairPerformanceMetricsCache pairPerformanceMetricsCache, PairPerformanceMetricsCache pairPerformanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor, PenumbraTempCollectionJanitor tempCollectionJanitor,
XivDataAnalyzer modelAnalyzer, XivDataAnalyzer modelAnalyzer,
LightlessConfigService configService) LightlessConfigService configService,
IObjectTable objectTable)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_mediator = mediator; _mediator = mediator;
@@ -87,6 +89,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_tempCollectionJanitor = tempCollectionJanitor; _tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer; _modelAnalyzer = modelAnalyzer;
_configService = configService; _configService = configService;
_objectTable = objectTable;
} }
public IPairHandlerAdapter Create(string ident) public IPairHandlerAdapter Create(string ident)
@@ -105,6 +108,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_pluginWarningNotificationManager, _pluginWarningNotificationManager,
dalamudUtilService, dalamudUtilService,
_framework, _framework,
_objectTable,
actorObjectService, actorObjectService,
_lifetime, _lifetime,
_fileCacheManager, _fileCacheManager,

View File

@@ -110,6 +110,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton(gameGui); services.AddSingleton(gameGui);
services.AddSingleton(gameInteropProvider); services.AddSingleton(gameInteropProvider);
services.AddSingleton(addonLifecycle); services.AddSingleton(addonLifecycle);
services.AddSingleton(objectTable);
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder); services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
// Core singletons // Core singletons

View File

@@ -508,7 +508,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId); 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) private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(
GameObject* gameObject,
DalamudObjectKind objectKind,
bool isLocalPlayer)
{ {
if (gameObject == null) if (gameObject == null)
return (null, 0); return (null, 0);
@@ -520,6 +523,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
} }
var ownerId = ResolveOwnerId(gameObject); var ownerId = ResolveOwnerId(gameObject);
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero; var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
if (localPlayerAddress == nint.Zero) if (localPlayerAddress == nint.Zero)
return (null, ownerId); return (null, ownerId);
@@ -531,9 +535,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion) if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
{ {
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId); var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
if (expectedMinionOrMount != nint.Zero if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
&& (nint)gameObject == expectedMinionOrMount
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
{ {
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId; var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
return (LightlessObjectKind.MinionOrMount, resolvedOwner); return (LightlessObjectKind.MinionOrMount, resolvedOwner);
@@ -543,20 +545,16 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
if (objectKind != DalamudObjectKind.BattleNpc) if (objectKind != DalamudObjectKind.BattleNpc)
return (null, ownerId); return (null, ownerId);
if (ownerId != localEntityId) if (ownerId != 0 && ownerId != localEntityId)
return (null, ownerId); return (null, ownerId);
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId); var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
if (expectedPet != nint.Zero if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
&& (nint)gameObject == expectedPet return (LightlessObjectKind.Pet, ownerId != 0 ? ownerId : localEntityId);
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
return (LightlessObjectKind.Pet, ownerId);
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId); var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
if (expectedCompanion != nint.Zero if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
&& (nint)gameObject == expectedCompanion return (LightlessObjectKind.Companion, ownerId != 0 ? ownerId : localEntityId);
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
return (LightlessObjectKind.Companion, ownerId);
return (null, ownerId); return (null, ownerId);
} }
@@ -584,21 +582,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
return nint.Zero; return nint.Zero;
var playerObject = (GameObject*)localPlayerAddress; var playerObject = (GameObject*)localPlayerAddress;
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
if (ownerEntityId == 0)
return nint.Zero;
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
if (candidateAddress != nint.Zero) if (candidateAddress != nint.Zero)
{ {
var candidate = (GameObject*)candidateAddress; var candidate = (GameObject*)candidateAddress;
var candidateKind = (DalamudObjectKind)candidate->ObjectKind; var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion) if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
{ {
if (ResolveOwnerId(candidate) == ownerEntityId) var resolvedOwner = ResolveOwnerId(candidate);
if (resolvedOwner == ownerEntityId || resolvedOwner == 0)
return candidateAddress; return candidateAddress;
} }
} }
if (ownerEntityId == 0)
return nint.Zero;
foreach (var obj in _objectTable) foreach (var obj in _objectTable)
{ {
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress) if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
@@ -615,6 +617,90 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
return nint.Zero; return nint.Zero;
} }
public unsafe bool TryFindOwnedObject(uint ownerEntityId, LightlessObjectKind kind, out nint address)
{
address = nint.Zero;
if (ownerEntityId == 0) return false;
foreach (var addr in EnumerateActiveCharacterAddresses())
{
if (addr == nint.Zero) continue;
var go = (GameObject*)addr;
var ok = (DalamudObjectKind)go->ObjectKind;
switch (kind)
{
case LightlessObjectKind.MinionOrMount:
if (ok is DalamudObjectKind.MountType or DalamudObjectKind.Companion
&& ResolveOwnerId(go) == ownerEntityId)
{
address = addr;
return true;
}
break;
case LightlessObjectKind.Pet:
if (ok == DalamudObjectKind.BattleNpc
&& go->BattleNpcSubKind == BattleNpcSubKind.Pet
&& ResolveOwnerId(go) == ownerEntityId)
{
address = addr;
return true;
}
break;
case LightlessObjectKind.Companion:
if (ok == DalamudObjectKind.BattleNpc
&& go->BattleNpcSubKind == BattleNpcSubKind.Buddy
&& ResolveOwnerId(go) == ownerEntityId)
{
address = addr;
return true;
}
break;
}
}
return false;
}
public unsafe IReadOnlyList<nint> GetMinionOrMountCandidates(uint ownerEntityId, ushort preferredPlayerIndex)
{
var results = new List<(nint Ptr, int Score)>(4);
var manager = GameObjectManager.Instance();
if (manager == null || ownerEntityId == 0)
return Array.Empty<nint>();
const int objectLimit = 200;
for (var i = 0; i < objectLimit; i++)
{
var obj = manager->Objects.IndexSorted[i].Value;
if (obj == null)
continue;
var kind = (DalamudObjectKind)obj->ObjectKind;
if (kind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
continue;
var owner = ResolveOwnerId(obj);
if (owner != ownerEntityId)
continue;
var idx = obj->ObjectIndex;
var score = Math.Abs(idx - (preferredPlayerIndex + 1));
if (obj->DrawObject == null) score += 50;
results.Add(((nint)obj, score));
}
return results
.OrderBy(r => r.Score)
.Select(r => r.Ptr)
.ToArray();
}
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId) private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
{ {
if (localPlayerAddress == nint.Zero || ownerEntityId == 0) if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
@@ -1219,21 +1305,19 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
private static unsafe bool IsObjectFullyLoaded(nint address) private static unsafe bool IsObjectFullyLoaded(nint address)
{ {
if (address == nint.Zero) if (address == nint.Zero) return false;
return false;
var gameObject = (GameObject*)address; var gameObject = (GameObject*)address;
if (gameObject == null) if (gameObject == null) return false;
return false;
var drawObject = gameObject->DrawObject; var drawObject = gameObject->DrawObject;
if (drawObject == null) if (drawObject == null) return false;
return false;
if ((ulong)gameObject->RenderFlags == 2048) if ((ulong)gameObject->RenderFlags == 2048)
return false; return false;
var characterBase = (CharacterBase*)drawObject; var characterBase = (CharacterBase*)drawObject;
if (characterBase->HasModelInSlotLoaded != 0) if (characterBase->HasModelInSlotLoaded != 0)
return false; return false;

View File

@@ -1688,6 +1688,46 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndTable(); ImGui.EndTable();
} }
ImGui.Separator();
ImGui.TextUnformatted("Owned / Minion-Mount");
if (ImGui.BeginTable("##pairDebugOwnedMinion", 2, ImGuiTableFlags.SizingStretchProp))
{
DrawPairPropertyRow("Owned Temp Collection", debugInfo.OwnedPenumbraCollectionId == Guid.Empty
? "n/a"
: debugInfo.OwnedPenumbraCollectionId.ToString());
DrawPairPropertyRow("Needs Collection Rebuild", FormatBool(debugInfo.NeedsCollectionRebuild));
DrawPairPropertyRow("Minion Ptr", string.IsNullOrEmpty(debugInfo.MinionAddressHex)
? "n/a"
: debugInfo.MinionAddressHex);
DrawPairPropertyRow("Minion ObjectIndex", debugInfo.MinionObjectIndex.HasValue
? debugInfo.MinionObjectIndex.Value.ToString(CultureInfo.InvariantCulture)
: "n/a");
DrawPairPropertyRow("Minion Resolved At", FormatTimestamp(debugInfo.MinionResolvedAtUtc));
DrawPairPropertyRow("Minion Resolve Stage", string.IsNullOrEmpty(debugInfo.MinionResolveStage)
? "n/a"
: debugInfo.MinionResolveStage);
DrawPairPropertyRow("Minion Resolve Failure", string.IsNullOrEmpty(debugInfo.MinionResolveFailureReason)
? "n/a"
: debugInfo.MinionResolveFailureReason);
DrawPairPropertyRow("Minion Pending Retry", FormatBool(debugInfo.MinionPendingRetry));
var retryChanges = debugInfo.MinionPendingRetryChanges is { Count: > 0 }
? string.Join(", ", debugInfo.MinionPendingRetryChanges)
: "n/a";
DrawPairPropertyRow("Minion Pending Changes", retryChanges);
DrawPairPropertyRow("Minion Has Appearance Data", FormatBool(debugInfo.MinionHasAppearanceData));
ImGui.EndTable();
}
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted("Syncshell Memberships"); ImGui.TextUnformatted("Syncshell Memberships");
if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0) if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0)