Added debug information regarding minions
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -110,6 +110,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton(gameGui);
|
||||
services.AddSingleton(gameInteropProvider);
|
||||
services.AddSingleton(addonLifecycle);
|
||||
services.AddSingleton(objectTable);
|
||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||
|
||||
// Core singletons
|
||||
|
||||
@@ -508,7 +508,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
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)
|
||||
return (null, 0);
|
||||
@@ -520,6 +523,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
}
|
||||
|
||||
var ownerId = ResolveOwnerId(gameObject);
|
||||
|
||||
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
||||
if (localPlayerAddress == nint.Zero)
|
||||
return (null, ownerId);
|
||||
@@ -531,9 +535,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||
{
|
||||
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
||||
if (expectedMinionOrMount != nint.Zero
|
||||
&& (nint)gameObject == expectedMinionOrMount
|
||||
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
|
||||
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
|
||||
{
|
||||
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
||||
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
||||
@@ -543,20 +545,16 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
if (objectKind != DalamudObjectKind.BattleNpc)
|
||||
return (null, ownerId);
|
||||
|
||||
if (ownerId != localEntityId)
|
||||
if (ownerId != 0 && ownerId != localEntityId)
|
||||
return (null, ownerId);
|
||||
|
||||
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
||||
if (expectedPet != nint.Zero
|
||||
&& (nint)gameObject == expectedPet
|
||||
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
||||
return (LightlessObjectKind.Pet, ownerId);
|
||||
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
|
||||
return (LightlessObjectKind.Pet, ownerId != 0 ? ownerId : localEntityId);
|
||||
|
||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
||||
if (expectedCompanion != nint.Zero
|
||||
&& (nint)gameObject == expectedCompanion
|
||||
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
||||
return (LightlessObjectKind.Companion, ownerId);
|
||||
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
|
||||
return (LightlessObjectKind.Companion, ownerId != 0 ? ownerId : localEntityId);
|
||||
|
||||
return (null, ownerId);
|
||||
}
|
||||
@@ -584,21 +582,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
return nint.Zero;
|
||||
|
||||
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)
|
||||
{
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
|
||||
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||
{
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
var resolvedOwner = ResolveOwnerId(candidate);
|
||||
|
||||
if (resolvedOwner == ownerEntityId || resolvedOwner == 0)
|
||||
return candidateAddress;
|
||||
}
|
||||
}
|
||||
|
||||
if (ownerEntityId == 0)
|
||||
return nint.Zero;
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
||||
@@ -1219,21 +1305,19 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
|
||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return false;
|
||||
if (address == nint.Zero) return false;
|
||||
|
||||
var gameObject = (GameObject*)address;
|
||||
if (gameObject == null)
|
||||
return false;
|
||||
if (gameObject == null) return false;
|
||||
|
||||
var drawObject = gameObject->DrawObject;
|
||||
if (drawObject == null)
|
||||
return false;
|
||||
if (drawObject == null) return false;
|
||||
|
||||
if ((ulong)gameObject->RenderFlags == 2048)
|
||||
return false;
|
||||
|
||||
var characterBase = (CharacterBase*)drawObject;
|
||||
|
||||
if (characterBase->HasModelInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -1688,6 +1688,46 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
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.TextUnformatted("Syncshell Memberships");
|
||||
if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0)
|
||||
|
||||
Reference in New Issue
Block a user