Removed unsafe handling of game object and owned object
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -11,6 +12,7 @@ public class GameObjectHandlerFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
|
||||
@@ -18,12 +20,14 @@ public class GameObjectHandlerFactory
|
||||
ILoggerFactory loggerFactory,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
LightlessMediator lightlessMediator,
|
||||
IServiceProvider serviceProvider)
|
||||
IServiceProvider serviceProvider,
|
||||
IObjectTable objectTable)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_serviceProvider = serviceProvider;
|
||||
_objectTable = objectTable;
|
||||
}
|
||||
|
||||
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||
@@ -36,6 +40,7 @@ public class GameObjectHandlerFactory
|
||||
dalamudUtilService,
|
||||
objectKind,
|
||||
getAddressFunc,
|
||||
_objectTable,
|
||||
isWatched)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
|
||||
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly Func<IntPtr> _getAddress;
|
||||
private readonly bool _isOwnedObject;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
@@ -24,7 +23,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
private CancellationTokenSource _zoningCts = new();
|
||||
|
||||
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
|
||||
LightlessMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool ownedObject = true) : base(logger, mediator)
|
||||
LightlessMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, IObjectTable objectTable, bool ownedObject = true) : base(logger, mediator)
|
||||
{
|
||||
_performanceCollector = performanceCollector;
|
||||
ObjectKind = objectKind;
|
||||
@@ -82,6 +81,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
});
|
||||
|
||||
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||
_objectTable = objectTable;
|
||||
|
||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||
}
|
||||
@@ -110,14 +110,14 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
private ushort[] MainHandData { get; set; } = new ushort[3];
|
||||
private ushort[] OffHandData { get; set; } = new ushort[3];
|
||||
|
||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<ICharacter> act, CancellationToken token)
|
||||
{
|
||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
EnsureLatestObjectState();
|
||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||
if (gameObj is ICharacter chara)
|
||||
{
|
||||
act.Invoke(chara);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
}
|
||||
}
|
||||
|
||||
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
|
||||
public IGameObject? GetGameObject()
|
||||
{
|
||||
return _dalamudUtil.CreateGameObject(Address);
|
||||
}
|
||||
@@ -169,9 +169,22 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||
}
|
||||
|
||||
private IGameObject? TryGetObjectByAddress(nint address)
|
||||
{
|
||||
if (address == nint.Zero) return null;
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null) continue;
|
||||
if (obj.Address == address)
|
||||
return obj;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
||||
|
||||
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
||||
private void CheckAndUpdateObject(bool allowPublish)
|
||||
{
|
||||
var prevAddr = Address;
|
||||
var prevDrawObj = DrawObjectAddress;
|
||||
@@ -179,127 +192,118 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
Address = _getAddress();
|
||||
|
||||
if (Address != IntPtr.Zero)
|
||||
{
|
||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
|
||||
EntityId = gameObject->EntityId;
|
||||
IGameObject? obj = null;
|
||||
ICharacter? chara = null;
|
||||
|
||||
var chara = (Character*)Address;
|
||||
nameString = chara->GameObject.NameString;
|
||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||
Name = nameString;
|
||||
if (Address != nint.Zero)
|
||||
{
|
||||
obj = TryGetObjectByAddress(Address);
|
||||
|
||||
if (obj is not null)
|
||||
{
|
||||
EntityId = obj.EntityId;
|
||||
|
||||
DrawObjectAddress = Address;
|
||||
|
||||
nameString = obj.Name.TextValue ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||
Name = nameString;
|
||||
|
||||
chara = obj as ICharacter;
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawObjectAddress = nint.Zero;
|
||||
EntityId = uint.MaxValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
DrawObjectAddress = nint.Zero;
|
||||
EntityId = uint.MaxValue;
|
||||
}
|
||||
|
||||
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||
CurrentDrawCondition = IsBeingDrawnSafe(obj, chara);
|
||||
|
||||
if (_haltProcessing || !allowPublish) return;
|
||||
|
||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||
bool addrDiff = Address != prevAddr;
|
||||
|
||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||
bool nameChange = false;
|
||||
if (nameString is not null)
|
||||
{
|
||||
var chara = (Character*)Address;
|
||||
var drawObj = (DrawObject*)DrawObjectAddress;
|
||||
var objType = drawObj->Object.GetObjectType();
|
||||
var isHuman = objType == ObjectType.CharacterBase
|
||||
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
||||
|
||||
nameString ??= ((Character*)Address)->GameObject.NameString;
|
||||
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||
nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||
if (nameChange) Name = nameString;
|
||||
}
|
||||
|
||||
bool equipDiff = false;
|
||||
bool customizeDiff = false;
|
||||
|
||||
if (isHuman)
|
||||
if (chara is not null)
|
||||
{
|
||||
var classJob = chara.ClassJob.RowId;
|
||||
if (classJob != _classJob)
|
||||
{
|
||||
var classJob = chara->CharacterData.ClassJob;
|
||||
if (classJob != _classJob)
|
||||
{
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
}
|
||||
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||
|
||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
else
|
||||
{
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = (byte)classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
}
|
||||
|
||||
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
|
||||
customizeDiff = CompareAndUpdateCustomizeData(chara.Customize);
|
||||
|
||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player && chara.Customize.Length > (int)CustomizeIndex.Tribe)
|
||||
{
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
return;
|
||||
}
|
||||
var gender = chara.Customize[(int)CustomizeIndex.Gender];
|
||||
var raceId = chara.Customize[(int)CustomizeIndex.Race];
|
||||
var tribeId = chara.Customize[(int)CustomizeIndex.Tribe];
|
||||
|
||||
bool customizeDiff = false;
|
||||
|
||||
if (isHuman)
|
||||
{
|
||||
var gender = ((Human*)drawObj)->Customize.Sex;
|
||||
var raceId = ((Human*)drawObj)->Customize.Race;
|
||||
var tribeId = ((Human*)drawObj)->Customize.Tribe;
|
||||
|
||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||
if (gender != Gender || raceId != RaceId || tribeId != TribeId)
|
||||
{
|
||||
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
||||
Gender = gender;
|
||||
RaceId = raceId;
|
||||
TribeId = tribeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||
}
|
||||
else
|
||||
{
|
||||
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||
{
|
||||
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
||||
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||
}
|
||||
if ((addrDiff || drawObjDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||
{
|
||||
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
||||
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||
}
|
||||
else if (addrDiff || drawObjDiff)
|
||||
{
|
||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||
if (Address == nint.Zero)
|
||||
CurrentDrawCondition = DrawCondition.ObjectZero;
|
||||
else if (DrawObjectAddress == nint.Zero)
|
||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
|
||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||
private DrawCondition IsBeingDrawnSafe(IGameObject? obj, ICharacter? chara)
|
||||
{
|
||||
if (Address == nint.Zero) return DrawCondition.ObjectZero;
|
||||
if (obj is null) return DrawCondition.DrawObjectZero;
|
||||
|
||||
if (chara is not null && (chara.Customize is null || chara.Customize.Length == 0))
|
||||
return DrawCondition.DrawObjectZero;
|
||||
|
||||
return DrawCondition.None;
|
||||
}
|
||||
|
||||
private bool CompareAndUpdateCustomizeData(ReadOnlySpan<byte> customizeData)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
|
||||
for (int i = 0; i < customizeData.Length; i++)
|
||||
var len = Math.Min(customizeData.Length, CustomizeData.Length);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var data = customizeData[i];
|
||||
if (CustomizeData[i] != data)
|
||||
@@ -312,48 +316,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
for (int i = 0; i < EquipSlotData.Length; i++)
|
||||
{
|
||||
var data = equipSlotData[i];
|
||||
if (EquipSlotData[i] != data)
|
||||
{
|
||||
EquipSlotData[i] = data;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||
MainHandData[0] = weapon->ModelSetId;
|
||||
hasChanges |= weapon->Variant != MainHandData[1];
|
||||
MainHandData[1] = weapon->Variant;
|
||||
hasChanges |= weapon->SecondaryId != MainHandData[2];
|
||||
MainHandData[2] = weapon->SecondaryId;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||
OffHandData[0] = weapon->ModelSetId;
|
||||
hasChanges |= weapon->Variant != OffHandData[1];
|
||||
OffHandData[1] = weapon->Variant;
|
||||
hasChanges |= weapon->SecondaryId != OffHandData[2];
|
||||
OffHandData[2] = weapon->SecondaryId;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
try
|
||||
@@ -403,24 +365,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||
{
|
||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
|
||||
var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags;
|
||||
if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags;
|
||||
|
||||
if (ObjectKind == ObjectKind.Player)
|
||||
{
|
||||
var modelInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelInSlotLoaded != 0);
|
||||
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
|
||||
var modelFilesInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelFilesInSlotLoaded != 0);
|
||||
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
|
||||
}
|
||||
|
||||
return DrawCondition.None;
|
||||
}
|
||||
|
||||
private void ZoneSwitchEnd()
|
||||
{
|
||||
if (!_isOwnedObject) return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
@@ -7,11 +8,15 @@ using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
|
||||
internal sealed class OwnedObjectHandler
|
||||
{
|
||||
// Debug information for owned object resolution
|
||||
internal readonly record struct OwnedResolveDebug(
|
||||
DateTime? ResolvedAtUtc,
|
||||
nint Address,
|
||||
@@ -26,12 +31,15 @@ internal sealed class OwnedObjectHandler
|
||||
private OwnedResolveDebug _minionResolveDebug = OwnedResolveDebug.Empty;
|
||||
public OwnedResolveDebug MinionResolveDebug => _minionResolveDebug;
|
||||
|
||||
// Dependencies
|
||||
private readonly ILogger _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly GameObjectHandlerFactory _handlerFactory;
|
||||
private readonly IpcManager _ipc;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private IObjectTable _objectTable;
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
// Timeouts for fully loaded checks
|
||||
private const int _fullyLoadedTimeoutMsPlayer = 30000;
|
||||
private const int _fullyLoadedTimeoutMsOther = 5000;
|
||||
|
||||
@@ -51,6 +59,18 @@ internal sealed class OwnedObjectHandler
|
||||
_objectTable = objectTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the specified changes to the owned object of the given kind.
|
||||
/// </summary>
|
||||
/// <param name="applicationId">Application ID of the Character Object</param>
|
||||
/// <param name="kind">Object Kind of the given object</param>
|
||||
/// <param name="changes">Changes of the object</param>
|
||||
/// <param name="data">Data of the object</param>
|
||||
/// <param name="playerHandler">Owner of the object</param>
|
||||
/// <param name="penumbraCollection">Collection if needed</param>
|
||||
/// <param name="customizeIds">Customizing identications for the object</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
/// <returns>Successfully applied or not</returns>
|
||||
public async Task<bool> ApplyAsync(
|
||||
Guid applicationId,
|
||||
ObjectKind kind,
|
||||
@@ -61,9 +81,11 @@ internal sealed class OwnedObjectHandler
|
||||
Dictionary<ObjectKind, Guid?> customizeIds,
|
||||
CancellationToken token)
|
||||
{
|
||||
// Validate player handler
|
||||
if (playerHandler.Address == nint.Zero)
|
||||
return false;
|
||||
|
||||
// Create handler for owned object
|
||||
var handler = await CreateHandlerAsync(kind, playerHandler, token).ConfigureAwait(false);
|
||||
if (handler is null || handler.Address == nint.Zero)
|
||||
return false;
|
||||
@@ -72,17 +94,20 @@ internal sealed class OwnedObjectHandler
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Determine if we have file replacements for this kind
|
||||
bool hasFileReplacements =
|
||||
kind != ObjectKind.Player
|
||||
&& data.FileReplacements.TryGetValue(kind, out var repls)
|
||||
&& repls is { Count: > 0 };
|
||||
|
||||
// Determine if we should assign a Penumbra collection
|
||||
bool shouldAssignCollection =
|
||||
kind != ObjectKind.Player
|
||||
&& hasFileReplacements
|
||||
&& penumbraCollection != Guid.Empty
|
||||
&& _ipc.Penumbra.APIAvailable;
|
||||
|
||||
// Determine if only IPC-only changes are being made for player
|
||||
bool isPlayerIpcOnly =
|
||||
kind == ObjectKind.Player
|
||||
&& changes.Count > 0
|
||||
@@ -91,17 +116,21 @@ internal sealed class OwnedObjectHandler
|
||||
or PlayerChanges.PetNames
|
||||
or PlayerChanges.Heels);
|
||||
|
||||
// Wait for drawing to complete
|
||||
await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false);
|
||||
|
||||
// Determine timeouts
|
||||
var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000;
|
||||
var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? _fullyLoadedTimeoutMsPlayer : _fullyLoadedTimeoutMsOther;
|
||||
|
||||
// Wait for drawing to complete
|
||||
await _dalamudUtil
|
||||
.WaitWhileCharacterIsDrawing(_logger, handler, applicationId, drawTimeoutMs, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (handler.Address != nint.Zero)
|
||||
{
|
||||
// Wait for fully loaded
|
||||
var loaded = await _actorObjectService
|
||||
.WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs)
|
||||
.ConfigureAwait(false);
|
||||
@@ -115,8 +144,10 @@ internal sealed class OwnedObjectHandler
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Assign Penumbra collection if needed
|
||||
if (shouldAssignCollection)
|
||||
{
|
||||
// Get object index
|
||||
var objIndex = await _dalamudUtil
|
||||
.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
|
||||
.ConfigureAwait(false);
|
||||
@@ -127,6 +158,7 @@ internal sealed class OwnedObjectHandler
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assign collection
|
||||
await _ipc.Penumbra
|
||||
.AssignTemporaryCollectionAsync(_logger, penumbraCollection, objIndex.Value)
|
||||
.ConfigureAwait(false);
|
||||
@@ -134,10 +166,12 @@ internal sealed class OwnedObjectHandler
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
// Apply each change
|
||||
foreach (var change in changes.OrderBy(c => (int)c))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Handle each change type
|
||||
switch (change)
|
||||
{
|
||||
case PlayerChanges.Customize:
|
||||
@@ -180,11 +214,13 @@ internal sealed class OwnedObjectHandler
|
||||
}
|
||||
}
|
||||
|
||||
// Await all tasks for change applications
|
||||
if (tasks.Count > 0)
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Determine if redraw is needed
|
||||
bool needsRedraw =
|
||||
_ipc.Penumbra.APIAvailable
|
||||
&& (
|
||||
@@ -196,9 +232,11 @@ internal sealed class OwnedObjectHandler
|
||||
|| changes.Contains(PlayerChanges.Customize)
|
||||
);
|
||||
|
||||
// Skip redraw for player if only IPC-only changes were made
|
||||
if (isPlayerIpcOnly)
|
||||
needsRedraw = false;
|
||||
|
||||
// Perform redraw if needed
|
||||
if (needsRedraw && _ipc.Penumbra.APIAvailable)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
@@ -219,8 +257,18 @@ internal sealed class OwnedObjectHandler
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a GameObjectHandler for the owned object of the specified kind.
|
||||
/// </summary>
|
||||
/// <param name="kind">Object kind of the handler</param>
|
||||
/// <param name="playerHandler">Owner of the given object</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
/// <returns>Handler for the GameObject with the handler</returns>
|
||||
private async Task<GameObjectHandler?> CreateHandlerAsync(ObjectKind kind, GameObjectHandler playerHandler, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Debug info setter
|
||||
void SetMinionDebug(string stage, string? failure, nint addr = default, ushort? objIndex = null)
|
||||
{
|
||||
if (kind != ObjectKind.MinionOrMount)
|
||||
@@ -234,9 +282,11 @@ internal sealed class OwnedObjectHandler
|
||||
failure);
|
||||
}
|
||||
|
||||
// Direct return for player
|
||||
if (kind == ObjectKind.Player)
|
||||
return playerHandler;
|
||||
|
||||
// First, try direct retrieval via Dalamud API
|
||||
var playerPtr = playerHandler.Address;
|
||||
if (playerPtr == nint.Zero)
|
||||
{
|
||||
@@ -244,6 +294,7 @@ internal sealed class OwnedObjectHandler
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try direct retrieval
|
||||
nint ownedPtr = kind switch
|
||||
{
|
||||
ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false),
|
||||
@@ -252,20 +303,29 @@ internal sealed class OwnedObjectHandler
|
||||
_ => nint.Zero
|
||||
};
|
||||
|
||||
// If that fails, scan the object table for owned objects
|
||||
var stage = ownedPtr != nint.Zero ? "direct" : "direct_miss";
|
||||
|
||||
// Owner ID based scan
|
||||
if (ownedPtr == nint.Zero)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Get owner entity ID
|
||||
var ownerEntityId = playerHandler.EntityId;
|
||||
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
|
||||
{
|
||||
ownerEntityId = await _dalamudUtil.RunOnFrameworkThread(() => ReadEntityIdUnsafe(playerPtr))
|
||||
// Read unsafe
|
||||
ownerEntityId = await _dalamudUtil
|
||||
.RunOnFrameworkThread(() => ReadEntityIdSafe(playerHandler))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (ownerEntityId != 0 && ownerEntityId != uint.MaxValue)
|
||||
{
|
||||
ownedPtr = await _dalamudUtil.RunOnFrameworkThread(() => FindOwnedByOwnerIdUnsafe(kind, ownerEntityId))
|
||||
// Scan for owned object
|
||||
ownedPtr = await _dalamudUtil
|
||||
.RunOnFrameworkThread(() => FindOwnedByOwnerIdSafe(kind, ownerEntityId))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
stage = ownedPtr != nint.Zero ? "owner_scan" : "owner_scan_miss";
|
||||
@@ -282,6 +342,9 @@ internal sealed class OwnedObjectHandler
|
||||
return null;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Create handler
|
||||
var handler = await _handlerFactory.Create(kind, () => ownedPtr, isWatched: false).ConfigureAwait(false);
|
||||
if (handler is null || handler.Address == nint.Zero)
|
||||
{
|
||||
@@ -289,83 +352,102 @@ internal sealed class OwnedObjectHandler
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get object index for debug
|
||||
ushort? objIndex = await _dalamudUtil.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SetMinionDebug(stage, null, handler.Address, objIndex);
|
||||
SetMinionDebug(stage, failure: 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;
|
||||
}
|
||||
/// <summary>
|
||||
/// Entity ID reader with safety checks.
|
||||
/// </summary>
|
||||
/// <param name="playerHandler">Handler of the Object</param>
|
||||
/// <returns>Entity Id</returns>
|
||||
private static uint ReadEntityIdSafe(GameObjectHandler playerHandler) => playerHandler.GetGameObject()?.EntityId ?? 0;
|
||||
|
||||
private unsafe nint FindOwnedByOwnerIdUnsafe(ObjectKind kind, uint ownerEntityId)
|
||||
/// <summary>
|
||||
/// Finds an owned object by scanning the object table for the specified owner entity ID.
|
||||
/// </summary>
|
||||
/// <param name="kind">Object kind to find of owned object</param>
|
||||
/// <param name="ownerEntityId">Owner Id</param>
|
||||
/// <returns>Object Id</returns>
|
||||
private nint FindOwnedByOwnerIdSafe(ObjectKind kind, uint ownerEntityId)
|
||||
{
|
||||
// Validate owner ID
|
||||
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
|
||||
return nint.Zero;
|
||||
|
||||
// Scan object table
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
// Validate object
|
||||
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)
|
||||
// Check owner ID match
|
||||
if (obj.OwnerId != ownerEntityId)
|
||||
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)
|
||||
// Check kind match
|
||||
if (!IsOwnedKindMatch(obj, kind))
|
||||
continue;
|
||||
|
||||
var resolvedOwner = ResolveOwnerIdUnsafe(go);
|
||||
if (resolvedOwner == ownerEntityId)
|
||||
return addr;
|
||||
return obj.Address;
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private static unsafe uint ResolveOwnerIdUnsafe(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject)
|
||||
/// <summary>
|
||||
/// Determines if the given object matches the specified owned kind.
|
||||
/// </summary>
|
||||
/// <param name="obj">Game Object</param>
|
||||
/// <param name="kind">Object Kind</param>
|
||||
/// <returns></returns>
|
||||
private static bool IsOwnedKindMatch(IGameObject obj, ObjectKind kind) => kind switch
|
||||
{
|
||||
if (gameObject == null) return 0;
|
||||
// Match minion or mount
|
||||
ObjectKind.MinionOrMount =>
|
||||
obj.ObjectKind is DalamudObjectKind.MountType
|
||||
or DalamudObjectKind.Companion,
|
||||
|
||||
if (gameObject->OwnerId != 0)
|
||||
return gameObject->OwnerId;
|
||||
// Match pet
|
||||
ObjectKind.Pet =>
|
||||
obj.ObjectKind == DalamudObjectKind.BattleNpc
|
||||
&& obj is IBattleNpc bnPet
|
||||
&& bnPet.BattleNpcKind == BattleNpcSubKind.Pet,
|
||||
|
||||
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;
|
||||
}
|
||||
// Match companion
|
||||
ObjectKind.Companion =>
|
||||
obj.ObjectKind == DalamudObjectKind.BattleNpc
|
||||
&& obj is IBattleNpc bnBuddy
|
||||
&& bnBuddy.BattleNpcKind == BattleNpcSubKind.Chocobo,
|
||||
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Applies Customize Plus data to the specified object.
|
||||
/// </summary>
|
||||
/// <param name="address">Object Address</param>
|
||||
/// <param name="customizeData">Data of the Customize+ that has to be applied</param>
|
||||
/// <param name="kind">Object Kind</param>
|
||||
/// <param name="customizeIds">Customize+ Ids</param>
|
||||
/// <returns>Task</returns>
|
||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
|
||||
{
|
||||
customizeIds[kind] = await _ipc.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts Customize Plus changes for the specified object.
|
||||
/// </summary>
|
||||
/// <param name="customizeId">Customize+ Id</param>
|
||||
/// <param name="kind">Object Id</param>
|
||||
/// <param name="customizeIds">List of Customize+ ids</param>
|
||||
/// <returns></returns>
|
||||
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
|
||||
{
|
||||
if (!customizeId.HasValue)
|
||||
|
||||
@@ -2885,6 +2885,62 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
TryHandleVisibilityUpdate();
|
||||
}
|
||||
|
||||
private void KickOwnedObjectRetryFromTracked()
|
||||
{
|
||||
if (!IsVisible || IsPaused() || !CanApplyNow() || _charaHandler is null || _charaHandler.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
var data = _cachedData ?? LastReceivedCharacterData ?? _pairStateCache.TryLoad(Ident);
|
||||
if (data is null)
|
||||
return;
|
||||
|
||||
static HashSet<PlayerChanges> BuildOwnedChanges(CharacterData d, ObjectKind k)
|
||||
{
|
||||
var set = new HashSet<PlayerChanges>();
|
||||
|
||||
if (d.FileReplacements.TryGetValue(k, out var repls) && repls is { Count: > 0 })
|
||||
set.Add(PlayerChanges.ModFiles);
|
||||
|
||||
if (d.GlamourerData.TryGetValue(k, out var g) && !string.IsNullOrEmpty(g))
|
||||
set.Add(PlayerChanges.Glamourer);
|
||||
|
||||
if (d.CustomizePlusData.TryGetValue(k, out var c) && !string.IsNullOrEmpty(c))
|
||||
set.Add(PlayerChanges.Customize);
|
||||
|
||||
if (set.Count > 0)
|
||||
set.Add(PlayerChanges.ForcedRedraw);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
var kinds = new[] { ObjectKind.MinionOrMount, ObjectKind.Pet, ObjectKind.Companion };
|
||||
|
||||
lock (_ownedRetryGate)
|
||||
{
|
||||
foreach (var k in kinds)
|
||||
{
|
||||
if (!HasAppearanceDataForKind(data, k))
|
||||
continue;
|
||||
|
||||
var changes = BuildOwnedChanges(data, k);
|
||||
if (changes.Count == 0)
|
||||
continue;
|
||||
|
||||
_pendingOwnedChanges[k] = changes;
|
||||
}
|
||||
|
||||
if (_pendingOwnedChanges.Count == 0)
|
||||
return;
|
||||
|
||||
_ownedRetryCts = _ownedRetryCts?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
if (_ownedRetryTask.IsCompleted)
|
||||
_ownedRetryTask = Task.Run(() => OwnedObjectRetryLoopAsync(_ownedRetryCts.Token), CancellationToken.None);
|
||||
}
|
||||
|
||||
Logger.LogDebug("{handler}: Kicked owned-object retry from ActorTracked (pending: {pending})",
|
||||
GetLogIdentifier(), string.Join(", ", _pendingOwnedChanges.Keys));
|
||||
}
|
||||
|
||||
private void TryHandleVisibilityUpdate()
|
||||
{
|
||||
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
||||
@@ -3386,6 +3442,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
|
||||
ScheduleOwnedObjectRetry(ownedKind, changes);
|
||||
|
||||
KickOwnedObjectRetryFromTracked();
|
||||
}
|
||||
|
||||
private static HashSet<PlayerChanges> BuildOwnedChangeSetForKind(CharacterData data, ObjectKind kind)
|
||||
|
||||
Reference in New Issue
Block a user