using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Numerics; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace MareSynchronos.Services; public class DalamudUtilService : IHostedService { private readonly List _classJobIdsIgnoredForPets = new() { 30 }; private readonly ClientState _clientState; private readonly Condition _condition; private readonly Framework _framework; private readonly GameGui _gameGui; private readonly ILogger _logger; private readonly MareMediator _mediator; private readonly ObjectTable _objectTable; private readonly PerformanceCollectorService _performanceCollector; private uint? _classJobId = 0; private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; private Dictionary _playerCharas = new(StringComparer.Ordinal); private bool _sentBetweenAreas = false; public DalamudUtilService(ILogger logger, ClientState clientState, ObjectTable objectTable, Framework framework, GameGui gameGui, Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollectorService performanceCollector) { _logger = logger; _clientState = clientState; _objectTable = objectTable; _framework = framework; _gameGui = gameGui; _condition = condition; _mediator = mediator; _performanceCollector = performanceCollector; WorldData = new(() => { return gameData.GetExcelSheet(Dalamud.ClientLanguage.English)! .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); } public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget; public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex]; public bool IsInCutscene { get; private set; } = false; public bool IsInGpose { get; private set; } = false; public bool IsLoggedIn { get; private set; } public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; public Lazy> WorldData { get; private set; } public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) { EnsureIsOnFramework(); return _objectTable.CreateObjectReference(reference); } public async Task CreateGameObjectAsync(IntPtr reference) { return await RunOnFrameworkThread(() => _objectTable.CreateObjectReference(reference)).ConfigureAwait(false); } public void EnsureIsOnFramework() { if (!_framework.IsInFrameworkUpdateThread) throw new InvalidOperationException("Can only be run on Framework"); } public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index) { EnsureIsOnFramework(); var objTableObj = _objectTable[index]; if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; } public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) { EnsureIsOnFramework(); var mgr = CharacterManager.Instance(); playerPointer ??= GetPlayerPointer(); if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero; return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer); } public async Task GetCompanionAsync(IntPtr? playerPointer = null) { return await RunOnFrameworkThread(() => GetCompanion(playerPointer)).ConfigureAwait(false); } public bool GetIsPlayerPresent() { EnsureIsOnFramework(); return _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); } public async Task GetIsPlayerPresentAsync() { return await RunOnFrameworkThread(GetIsPlayerPresent).ConfigureAwait(false); } public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null) { EnsureIsOnFramework(); playerPointer ??= GetPlayerPointer(); if (playerPointer == IntPtr.Zero) return IntPtr.Zero; return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1); } public async Task GetMinionOrMountAsync(IntPtr? playerPointer = null) { return await RunOnFrameworkThread(() => GetMinionOrMount(playerPointer)).ConfigureAwait(false); } public unsafe IntPtr GetPet(IntPtr? playerPointer = null) { EnsureIsOnFramework(); if (_classJobIdsIgnoredForPets.Contains(_classJobId ?? 0)) return IntPtr.Zero; var mgr = CharacterManager.Instance(); playerPointer ??= GetPlayerPointer(); if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero; return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer); } public async Task GetPetAsync(IntPtr? playerPointer = null) { return await RunOnFrameworkThread(() => GetPet(playerPointer)).ConfigureAwait(false); } public PlayerCharacter GetPlayerCharacter() { EnsureIsOnFramework(); return _clientState.LocalPlayer!; } public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName) { if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address; return IntPtr.Zero; } public string GetPlayerName() { EnsureIsOnFramework(); return _clientState.LocalPlayer?.Name.ToString() ?? "--"; } public async Task GetPlayerNameAsync() { return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false); } public async Task GetPlayerNameHashedAsync() { return await RunOnFrameworkThread(() => (GetPlayerName() + GetWorldId()).GetHash256()).ConfigureAwait(false); } public IntPtr GetPlayerPointer() { EnsureIsOnFramework(); return _clientState.LocalPlayer?.Address ?? IntPtr.Zero; } public uint GetWorldId() { EnsureIsOnFramework(); return _clientState.LocalPlayer!.HomeWorld.Id; } public async Task GetWorldIdAsync() { return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false); } public unsafe bool IsGameObjectPresent(IntPtr key) { return _objectTable.Any(f => f.Address == key); } public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) { EnsureIsOnFramework(); return obj != null && obj.IsValid(); } public async Task IsObjectPresentAsync(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) { return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false); } public async Task RunOnFrameworkThread(Action act) { if (!_framework.IsInFrameworkUpdateThread) { await _framework.RunOnFrameworkThread(act).ContinueWith((_) => Task.CompletedTask).ConfigureAwait(false); while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered { _logger.LogTrace("Still on framework"); await Task.Delay(1).ConfigureAwait(false); } } else act(); } public async Task RunOnFrameworkThread(Func func) { if (!_framework.IsInFrameworkUpdateThread) { var result = await _framework.RunOnFrameworkThread(func).ContinueWith((task) => task.Result).ConfigureAwait(false); while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered { _logger.LogTrace("Still on framework"); await Task.Delay(1).ConfigureAwait(false); } return result; } return func.Invoke(); } public Task StartAsync(CancellationToken cancellationToken) { _framework.Update += FrameworkOnUpdate; if (IsLoggedIn) { _classJobId = _clientState.LocalPlayer!.ClassJob.Id; } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogTrace("Stopping {type}", GetType()); _framework.Update -= FrameworkOnUpdate; return Task.CompletedTask; } public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null) { if (!_clientState.IsLoggedIn || handler.Address == IntPtr.Zero) return; logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler); const int tick = 250; int curWaitTime = 0; try { while ((!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut && await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something { logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); curWaitTime += tick; await Task.Delay(tick).ConfigureAwait(true); } logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime); } catch (NullReferenceException ex) { logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); } catch (AccessViolationException ex) { logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); } } public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000) { Thread.Sleep(500); var obj = (GameObject*)characterAddress; const int tick = 250; int curWaitTime = 0; _logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X")); // ReSharper disable once LoopVariableIsNeverChangedInsideLoop while (obj->RenderFlags != 0x00 && curWaitTime < timeOut) { _logger.LogTrace($"Waiting for gpose actor to finish drawing"); curWaitTime += tick; Thread.Sleep(tick); } Thread.Sleep(tick * 2); } public Vector2 WorldToScreen(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) { if (obj == null) return Vector2.Zero; return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero; } internal (string Name, nint Address) FindPlayerByNameHash(string ident) { _playerCharas.TryGetValue(ident, out var result); return result; } private void FrameworkOnUpdate(Framework framework) { _performanceCollector.LogPerformance(this, "FrameworkOnUpdate", FrameworkOnUpdateInternal); } private unsafe void FrameworkOnUpdateInternal() { if (_clientState.LocalPlayer?.IsDead ?? false) return; _playerCharas = _performanceCollector.LogPerformance(this, "ObjTableToCharas", () => _objectTable.OfType().Where(o => o.ObjectIndex < 240) .ToDictionary(p => p.GetHash256(), p => (p.Name.ToString(), p.Address), StringComparer.Ordinal)); if (GposeTarget != null && !IsInGpose) { _logger.LogDebug("Gpose start"); IsInGpose = true; _mediator.Publish(new GposeStartMessage()); } else if (GposeTarget == null && IsInGpose) { _logger.LogDebug("Gpose end"); IsInGpose = false; _mediator.Publish(new GposeEndMessage()); } if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene) { _logger.LogDebug("Cutscene start"); IsInCutscene = true; _mediator.Publish(new CutsceneStartMessage()); _mediator.Publish(new HaltScanMessage("Cutscene")); } else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene) { _logger.LogDebug("Cutscene end"); IsInCutscene = false; _mediator.Publish(new CutsceneEndMessage()); _mediator.Publish(new ResumeScanMessage("Cutscene")); } if (IsInCutscene) { _mediator.Publish(new CutsceneFrameworkUpdateMessage()); return; } if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]) { if (!_sentBetweenAreas) { _logger.LogDebug("Zone switch/Gpose start"); _sentBetweenAreas = true; _mediator.Publish(new ZoneSwitchStartMessage()); _mediator.Publish(new HaltScanMessage("Zone switch")); } return; } if (_sentBetweenAreas) { _logger.LogDebug("Zone switch/Gpose end"); _sentBetweenAreas = false; _mediator.Publish(new ZoneSwitchEndMessage()); _mediator.Publish(new ResumeScanMessage("Zone switch")); } _mediator.Publish(new FrameworkUpdateMessage()); if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; var localPlayer = _clientState.LocalPlayer; if (localPlayer != null && !IsLoggedIn) { _logger.LogDebug("Logged in"); IsLoggedIn = true; _mediator.Publish(new DalamudLoginMessage()); } else if (localPlayer == null && IsLoggedIn) { _logger.LogDebug("Logged out"); IsLoggedIn = false; _mediator.Publish(new DalamudLogoutMessage()); } if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) { var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; if (_classJobId != newclassJobId) { _classJobId = newclassJobId; _mediator.Publish(new ClassJobChangedMessage(_classJobId)); } } _mediator.Publish(new DelayedFrameworkUpdateMessage()); _delayedFrameworkUpdateCheck = DateTime.Now; } }