Files
LightlessClient/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs
2026-01-17 22:31:50 +01:00

506 lines
16 KiB
C#

using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Handlers;
/// <summary>
/// Game object handler for managing game object state and updates
/// </summary>
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;
private readonly Lock _frameworkUpdateGate = new();
private bool _frameworkUpdateSubscribed;
private byte _classJob = 0;
private Task? _delayedZoningTask;
private bool _haltProcessing = false;
private CancellationTokenSource _zoningCts = new();
/// <summary>
/// Constructor for GameObjectHandler
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="performanceCollector">Performance Collector</param>
/// <param name="mediator">Lightless Mediator</param>
/// <param name="dalamudUtil">Dalamud Utilties Service</param>
/// <param name="objectKind">Object kind of Object</param>
/// <param name="getAddress">Get Adress</param>
/// <param name="objectTable">Object table of Dalamud</param>
/// <param name="ownedObject">Object is owned by user</param>
public GameObjectHandler(
ILogger<GameObjectHandler> logger,
PerformanceCollectorService performanceCollector,
LightlessMediator mediator,
DalamudUtilService dalamudUtil,
ObjectKind objectKind,
Func<IntPtr> getAddress,
IObjectTable objectTable,
bool ownedObject = true) : base(logger, mediator)
{
_performanceCollector = performanceCollector;
ObjectKind = objectKind;
_dalamudUtil = dalamudUtil;
_objectTable = objectTable;
_getAddress = () =>
{
_dalamudUtil.EnsureIsOnFramework();
return getAddress.Invoke();
};
_isOwnedObject = ownedObject;
Name = string.Empty;
if (ownedObject)
{
Mediator.Subscribe<TransientResourceChangedMessage>(this, msg =>
{
if (_delayedZoningTask?.IsCompleted ?? true)
{
if (msg.Address != Address) return;
Mediator.Publish(new CreateCacheForObjectMessage(this));
}
});
}
EnableFrameworkUpdates();
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ZoneSwitchEnd());
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ => ZoneSwitchStart());
Mediator.Subscribe<CutsceneStartMessage>(this, _ => _haltProcessing = true);
Mediator.Subscribe<CutsceneEndMessage>(this, _ =>
{
_haltProcessing = false;
ZoneSwitchEnd();
});
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, msg =>
{
if (msg.Address == Address) _haltProcessing = true;
});
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, msg =>
{
if (msg.Address == Address) _haltProcessing = false;
});
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
_dalamudUtil.EnsureIsOnFramework();
CheckAndUpdateObject(allowPublish: true);
}
/// <summary>
/// Draw Condition Enum
/// </summary>
public enum DrawCondition
{
None,
ObjectZero,
DrawObjectZero,
RenderFlags,
ModelInSlotLoaded,
ModelFilesInSlotLoaded
}
// Properties
public IntPtr Address { get; private set; }
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
public byte Gender { get; private set; }
public string Name { get; private set; }
public uint EntityId { get; private set; } = uint.MaxValue;
public ObjectKind ObjectKind { get; }
public byte RaceId { get; private set; }
public byte TribeId { get; private set; }
private byte[] CustomizeData { get; set; } = new byte[26];
private IntPtr DrawObjectAddress { get; set; }
/// <summary>
/// Act on framework thread after ensuring no draw condition
/// </summary>
/// <param name="act">Action of Character</param>
/// <param name="token">Cancellation Token</param>
/// <returns>Task Completion</returns>
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 ICharacter chara)
{
act.Invoke(chara);
}
return false;
}).ConfigureAwait(false))
{
await Task.Delay(250, token).ConfigureAwait(false);
}
}
/// <summary>
/// Compare Name And Throw if not equal
/// </summary>
/// <param name="name">Name that will be compared to Object Handler.</param>
/// <exception cref="InvalidOperationException">Not equal if thrown</exception>
public void CompareNameAndThrow(string name)
{
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Player name not equal to requested name, pointer invalid");
}
if (Address == IntPtr.Zero)
{
throw new InvalidOperationException("Player pointer is zero, pointer invalid");
}
}
/// <summary>
/// Gets the game object from the address
/// </summary>
/// <returns>Gane object</returns>
public IGameObject? GetGameObject()
{
return _dalamudUtil.CreateGameObject(Address);
}
/// <summary>
/// Invalidate the object handler
/// </summary>
public void Invalidate()
{
Address = IntPtr.Zero;
DrawObjectAddress = IntPtr.Zero;
EntityId = uint.MaxValue;
_haltProcessing = false;
}
/// <summary>
/// Refresh the object handler state
/// </summary>
public void Refresh()
{
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
}
/// <summary>
/// Is Being Drawn Run On Framework Asyncronously
/// </summary>
/// <returns>Object is being run in framework</returns>
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
{
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
}
/// <summary>
/// Override ToString method for GameObjectHandler
/// </summary>
/// <returns>String</returns>
public override string ToString()
{
var owned = _isOwnedObject ? "Self" : "Other";
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
}
/// <summary>
/// Try Get Object By Address from Object Table
/// </summary>
/// <param name="address">Object address</param>
/// <returns>Game Object of adress</returns>
private IGameObject? TryGetObjectByAddress(nint address)
{
if (address == nint.Zero) return null;
// Search object table
foreach (var obj in _objectTable)
{
if (obj is null) continue;
if (obj.Address == address)
return obj;
}
return null;
}
/// <summary>
/// Checks and updates the object state
/// </summary>
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
/// <summary>
/// Checks and updates the object state with option to allow publish
/// </summary>
/// <param name="allowPublish">Allows to publish the object</param>
private void CheckAndUpdateObject(bool allowPublish)
{
var prevAddr = Address;
var prevDrawObj = DrawObjectAddress;
string? nameString = null;
Address = _getAddress();
IGameObject? obj = null;
ICharacter? chara = null;
if (Address != nint.Zero)
{
// Try get object
obj = TryGetObjectByAddress(Address);
if (obj is not null)
{
EntityId = obj.EntityId;
DrawObjectAddress = Address;
// Name update
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 = nint.Zero;
EntityId = uint.MaxValue;
}
// Update draw condition
CurrentDrawCondition = IsBeingDrawnSafe(obj, chara);
if (_haltProcessing || !allowPublish) return;
// Determine differences
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
bool addrDiff = Address != prevAddr;
// Name change check
bool nameChange = false;
if (nameString is not null)
{
nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
if (nameChange) Name = nameString;
}
// Customize data change check
bool customizeDiff = false;
if (chara is not null)
{
// Class job change check
var classJob = chara.ClassJob.RowId;
if (classJob != _classJob)
{
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
_classJob = (byte)classJob;
Mediator.Publish(new ClassJobChangedMessage(this));
}
// Customize data comparison
customizeDiff = CompareAndUpdateCustomizeData(chara.Customize);
// Census update publish
if (_isOwnedObject && ObjectKind == ObjectKind.Player && chara.Customize.Length > (int)CustomizeIndex.Tribe)
{
var gender = chara.Customize[(int)CustomizeIndex.Gender];
var raceId = chara.Customize[(int)CustomizeIndex.Race];
var tribeId = chara.Customize[(int)CustomizeIndex.Tribe];
if (gender != Gender || raceId != RaceId || tribeId != TribeId)
{
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
Gender = gender;
RaceId = raceId;
TribeId = tribeId;
}
}
}
if ((addrDiff || drawObjDiff || customizeDiff || nameChange) && _isOwnedObject)
{
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
Mediator.Publish(new CreateCacheForObjectMessage(this));
}
else if (addrDiff || drawObjDiff)
{
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));
}
}
/// <summary>
/// Is object being drawn safe check
/// </summary>
/// <param name="obj">Object thats being checked</param>
/// <param name="chara">Character of the object</param>
/// <returns>Draw Condition of character</returns>
private DrawCondition IsBeingDrawnSafe(IGameObject? obj, ICharacter? chara)
{
// Object zero check
if (Address == nint.Zero) return DrawCondition.ObjectZero;
if (obj is null) return DrawCondition.DrawObjectZero;
// Draw Object check
if (chara is not null && (chara.Customize is null || chara.Customize.Length == 0))
return DrawCondition.DrawObjectZero;
return DrawCondition.None;
}
/// <summary>
/// Compare and update customize data of character
/// </summary>
/// <param name="customizeData">Customize+ data of object</param>
/// <returns>Successfully applied or not</returns>
private bool CompareAndUpdateCustomizeData(ReadOnlySpan<byte> customizeData)
{
bool hasChanges = false;
// Resize if needed
var len = Math.Min(customizeData.Length, CustomizeData.Length);
for (int i = 0; i < len; i++)
{
var data = customizeData[i];
if (CustomizeData[i] != data)
{
CustomizeData[i] = data;
hasChanges = true;
}
}
return hasChanges;
}
/// <summary>
/// Framework update method
/// </summary>
private void FrameworkUpdate()
{
try
{
var zoningDelayActive = !(_delayedZoningTask?.IsCompleted ?? true);
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}", () => CheckAndUpdateObject(allowPublish: !zoningDelayActive));
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this);
}
}
/// <summary>
/// Is object being drawn check
/// </summary>
/// <returns>Is being drawn</returns>
private bool IsBeingDrawn()
{
EnsureLatestObjectState();
if (_dalamudUtil.IsAnythingDrawing)
{
Logger.LogTrace("[{this}] IsBeingDrawn, Global draw block", this);
return true;
}
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, CurrentDrawCondition);
return CurrentDrawCondition != DrawCondition.None;
}
/// <summary>
/// Ensures the latest object state
/// </summary>
private void EnsureLatestObjectState()
{
if (_haltProcessing || !_frameworkUpdateSubscribed)
{
CheckAndUpdateObject();
}
}
/// <summary>
/// Enables framework updates for the object handler
/// </summary>
private void EnableFrameworkUpdates()
{
lock (_frameworkUpdateGate)
{
if (_frameworkUpdateSubscribed)
{
return;
}
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
_frameworkUpdateSubscribed = true;
}
}
/// <summary>
/// Zone switch end handling
/// </summary>
private void ZoneSwitchEnd()
{
if (!_isOwnedObject) return;
try
{
_zoningCts?.CancelAfter(2500);
}
catch (ObjectDisposedException)
{
// ignore canelled after disposed
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Zoning CTS cancel issue");
}
}
/// <summary>
/// Zone switch start handling
/// </summary>
private void ZoneSwitchStart()
{
if (!_isOwnedObject) return;
_zoningCts = new();
Logger.LogDebug("[{obj}] Starting Delay After Zoning", this);
_delayedZoningTask = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(120), _zoningCts.Token).ConfigureAwait(false);
}
catch
{
// ignore cancelled
}
finally
{
Logger.LogDebug("[{this}] Delay after zoning complete", this);
_zoningCts.Dispose();
}
}, _zoningCts.Token);
}
}