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.Enum;
using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Pairs;
using Microsoft.Extensions.Logging;
namespace LightlessSync.PlayerData.Handlers;
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 DalamudUtilService _dalamudUtil;
private readonly GameObjectHandlerFactory _handlerFactory;
private readonly IpcManager _ipc;
private readonly ActorObjectService _actorObjectService;
private IObjectTable _objectTable;
private const int _fullyLoadedTimeoutMsPlayer = 30000;
private const int _fullyLoadedTimeoutMsOther = 5000;
@@ -25,13 +40,15 @@ internal sealed class OwnedObjectHandler
DalamudUtilService dalamudUtil,
GameObjectHandlerFactory handlerFactory,
IpcManager ipc,
ActorObjectService actorObjectService)
ActorObjectService actorObjectService,
IObjectTable objectTable)
{
_logger = logger;
_dalamudUtil = dalamudUtil;
_handlerFactory = handlerFactory;
_ipc = ipc;
_actorObjectService = actorObjectService;
_objectTable = objectTable;
}
public async Task<bool> ApplyAsync(
@@ -204,10 +221,29 @@ internal sealed class OwnedObjectHandler
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)
return playerHandler;
var playerPtr = playerHandler.Address;
if (playerPtr == nint.Zero)
{
SetMinionDebug("player_ptr_zero", "playerHandler.Address == 0");
return null;
}
nint ownedPtr = kind switch
{
ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false),
@@ -216,12 +252,115 @@ internal sealed class OwnedObjectHandler
_ => nint.Zero
};
if (ownedPtr == nint.Zero)
return null;
var stage = ownedPtr != nint.Zero ? "direct" : "direct_miss";
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)
{
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>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{
new string Ident { get; }
bool Initialized { get; }
bool IsVisible { get; }
bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; }
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
bool PendingModReapply { get; }
bool ModApplyDeferred { get; }
int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{
new string Ident { get; }
bool Initialized { get; }
bool IsVisible { get; }
bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; }
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
bool PendingModReapply { get; }
bool ModApplyDeferred { 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 ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
}
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
}

View File

@@ -244,6 +244,17 @@ public class Pair
handler.ModApplyDeferred,
handler.MissingCriticalMods,
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,
int MissingCriticalMods,
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(
false,
false,
false,
false,
null,
null,
null,
null,
null,
null,
null,
Array.Empty<string>(),
false,
false,
0,
0,
false,
false,
0,
0,
0);
HasHandler: false,
HandlerInitialized: false,
HandlerVisible: false,
HandlerScheduledForDeletion: false,
LastDataReceivedAt: null,
LastApplyAttemptAt: null,
LastSuccessfulApplyAt: null,
InvisibleSinceUtc: null,
VisibilityEvictionDueAtUtc: null,
VisibilityEvictionRemainingSeconds: null,
LastFailureReason: null,
BlockingConditions: [],
IsApplying: false,
IsDownloading: false,
PendingDownloadCount: 0,
ForbiddenDownloadCount: 0,
PendingModReapply: false,
ModApplyDeferred: false,
MissingCriticalMods: 0,
MissingNonCriticalMods: 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 System.Diagnostics;
using Dalamud.Plugin.Services;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
@@ -21,9 +20,11 @@ using LightlessSync.Utils;
using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Pairs;
@@ -133,6 +134,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private ushort _lastKnownObjectIndex = ushort.MaxValue;
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? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
public string Ident { get; }
@@ -198,6 +203,54 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
public bool IsDownloading => _downloadManager.IsDownloading;
public int PendingDownloadCount => _downloadManager.CurrentDownloads.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(
ILogger<PairHandlerAdapter> logger,
@@ -210,6 +263,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
PluginWarningNotificationService pluginWarningNotificationManager,
DalamudUtilService dalamudUtil,
IFramework framework,
IObjectTable objectTable,
ActorObjectService actorObjectService,
IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager,
@@ -247,7 +301,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer;
_configService = configService;
_ownedObjectHandler = new OwnedObjectHandler(Logger, _dalamudUtil, _gameObjectHandlerFactory, _ipcManager, _actorObjectService);
_ownedObjectHandler = new OwnedObjectHandler(Logger, _dalamudUtil, _gameObjectHandlerFactory, _ipcManager, _actorObjectService, objectTable);
}
public void Initialize()
@@ -1304,6 +1358,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private async Task OwnedObjectRetryLoopAsync(CancellationToken token)
{
var delay = OwnedRetryInitialDelay;
try
{
while (!token.IsCancellationRequested)
@@ -1318,11 +1373,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
lock (_ownedRetryGate)
{
if (_pendingOwnedChanges.Count == 0)
{
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)
@@ -1347,16 +1402,62 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
continue;
}
bool anyApplied = false;
foreach (var entry in pending)
token.ThrowIfCancellationRequested();
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))
{
ClearOwnedObjectRetry(entry.Key);
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)
{
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)
{
var nextMs = Math.Min(delay.TotalMilliseconds * 2, OwnedRetryMaxDelay.TotalMilliseconds);
@@ -1674,7 +1864,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
ObjectKind.Player => _charaHandler!,
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),
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
};
@@ -1777,8 +1967,36 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
await Task.WhenAll(tasks).ConfigureAwait(false);
if (!isIpcOnly && needsRedraw && _ipcManager.Penumbra.APIAvailable)
{
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;
}
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)
{
@@ -2137,9 +2446,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
kvp.Key != ObjectKind.Player
&& kvp.Value.Contains(PlayerChanges.ModFiles));
var wantsOwnedCollectionAssignNow =
needsOwnedCollectionAssign
&& updateModdedPaths;
var wantsOwnedCollectionAssignNow = needsOwnedCollectionAssign;
Guid ownedAssignCollection = Guid.Empty;
if (wantsOwnedCollectionAssignNow)
@@ -2187,38 +2494,47 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
while (!token.IsCancellationRequested)
{
var ownedPtr = await ResolveOtherOwnedPtrAsync(kvp.Key, handlerForApply.Address).ConfigureAwait(false);
if (ownedPtr != nint.Zero)
{
using var ownedHandler = await _gameObjectHandlerFactory
.Create(kvp.Key, () => ownedPtr, isWatched: false)
.ConfigureAwait(false);
IReadOnlyList<nint> ownedPtrs;
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 go = ownedHandler.GetGameObject();
return go?.ObjectIndex;
}).ConfigureAwait(false);
if (objIndex.HasValue)
{
await _ipcManager.Penumbra
.AssignTemporaryCollectionAsync(Logger, ownedCollectionSnapshot, objIndex.Value)
.ConfigureAwait(false);
if (!objIndex.HasValue)
continue;
await _ipcManager.Penumbra
.RedrawAsync(Logger, ownedHandler, Guid.NewGuid(), token)
.ConfigureAwait(false);
await _ipcManager.Penumbra
.AssignTemporaryCollectionAsync(Logger, ownedCollectionSnapshot, objIndex.Value)
.ConfigureAwait(false);
Logger.LogDebug(
"Assigned OWNED temp collection {collection} to owned object {kind} for {handler} and redrew",
ownedCollectionSnapshot, kvp.Key, GetLogIdentifier());
await _ipcManager.Penumbra
.RedrawAsync(Logger, ownedHandler, Guid.NewGuid(), token)
.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);
@@ -2420,6 +2736,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
// Final redraw
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
_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)
{
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;
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
var ownerId = descriptor.OwnerEntityId;
if (ownerId == 0 || ownerId != _charaHandler.EntityId)
return;
if (descriptor.Address == nint.Zero)
if (!TryMapOwnedKind(descriptor, out var ownedKind))
return;
UpdateLastKnownActor(descriptor);
RefreshTrackedHandler(descriptor);
QueueActorInitialization(descriptor);
var data = _cachedData
?? LastReceivedCharacterData
?? _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)
@@ -3124,26 +3533,50 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
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)
return;
if (descriptor.Address != _charaHandler.Address)
return;
}
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
{
HandleVisibilityLoss(logChange: false);
return;
}
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return;
if (descriptor.Address != _charaHandler.Address)
return;
var localEntityId = _charaHandler.EntityId;
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)

View File

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