Files
ClubPenguinClient/MareSynchronos/Services/DalamudUtilService.cs
2023-05-04 01:31:21 +02:00

495 lines
19 KiB
C#

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 FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Runtime.CompilerServices;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace MareSynchronos.Services;
public class DalamudUtilService : IHostedService
{
private readonly List<uint> _classJobIdsIgnoredForPets = new() { 30 };
private readonly ClientState _clientState;
private readonly Condition _condition;
private readonly Framework _framework;
private readonly GameGui _gameGui;
private readonly ILogger<DalamudUtilService> _logger;
private readonly MareMediator _mediator;
private readonly ObjectTable _objectTable;
private readonly PerformanceCollectorService _performanceCollector;
private uint? _classJobId = 0;
private DateTime _delayedFrameworkUpdateCheck = DateTime.Now;
private string _lastGlobalBlockPlayer = string.Empty;
private Dictionary<string, (string Name, nint Address)> _playerCharas = new(StringComparer.Ordinal);
private bool _sentBetweenAreas = false;
public DalamudUtilService(ILogger<DalamudUtilService> 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<Lumina.Excel.GeneratedSheets.World>(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 IsAnythingDrawing { get; private set; } = false;
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<Dictionary<ushort, string>> WorldData { get; private set; }
public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference)
{
EnsureIsOnFramework();
return _objectTable.CreateObjectReference(reference);
}
public async Task<Dalamud.Game.ClientState.Objects.Types.GameObject?> 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<IntPtr> 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<bool> 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<IntPtr> 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<IntPtr> 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<string> GetPlayerNameAsync()
{
return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false);
}
public async Task<string> GetPlayerNameHashedAsync()
{
return await RunOnFrameworkThread(() => (GetPlayerName() + GetWorldId()).GetHash256()).ConfigureAwait(false);
}
public IntPtr GetPlayerPointer()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
}
public async Task<IntPtr> GetPlayerPointerAsync()
{
return await RunOnFrameworkThread(GetPlayerPointer).ConfigureAwait(false);
}
public uint GetWorldId()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer!.HomeWorld.Id;
}
public async Task<uint> 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<bool> IsObjectPresentAsync(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
{
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
}
public async Task RunOnFrameworkThread(Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
await _performanceCollector.LogPerformance(this, "RunOnFramework:Act/" + fileName + ">" + callerMember + ":" + callerLineNumber, async () =>
{
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();
}).ConfigureAwait(false);
}
public async Task<T> RunOnFrameworkThread<T>(Func<T> func, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
return await _performanceCollector.LogPerformance(this, "RunOnFramework:Func<" + typeof(T) + ">/" + fileName + ">" + callerMember + ":" + callerLineNumber, async () =>
{
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();
}).ConfigureAwait(false);
}
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 unsafe void CheckCharacterForDrawing(PlayerCharacter p)
{
if (!IsAnythingDrawing)
{
var gameObj = (GameObject*)p.Address;
var drawObj = gameObj->DrawObject;
var playerName = p.Name.ToString();
bool isDrawing = false;
if ((nint)drawObj != IntPtr.Zero)
{
isDrawing = gameObj->RenderFlags == 0b100000000000;
if (!isDrawing)
{
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
if (!isDrawing)
{
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
if (isDrawing)
{
if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal))
{
_lastGlobalBlockPlayer = playerName;
_logger.LogTrace("Global draw block: START => {name} (HasModelFilesInSlotLoaded)", playerName);
}
}
}
else
{
if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal))
{
_lastGlobalBlockPlayer = playerName;
_logger.LogTrace("Global draw block: START => {name} (HasModelInSlotLoaded)", playerName);
}
}
}
else
{
if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal))
{
_lastGlobalBlockPlayer = playerName;
_logger.LogTrace("Global draw block: START => {name} (RenderFlags)", playerName);
}
}
}
IsAnythingDrawing |= isDrawing;
}
}
private void FrameworkOnUpdate(Framework framework)
{
_performanceCollector.LogPerformance(this, "FrameworkOnUpdate", FrameworkOnUpdateInternal);
}
private unsafe void FrameworkOnUpdateInternal()
{
if (_clientState.LocalPlayer == null)
{
return;
}
if (_clientState.LocalPlayer?.IsDead ?? false)
{
return;
}
IsAnythingDrawing = false;
_playerCharas = _performanceCollector.LogPerformance(this, "ObjTableToCharas",
() => _objectTable.OfType<PlayerCharacter>().Where(o => o.ObjectIndex < 240)
.ToDictionary(p => p.GetHash256(), p =>
{
CheckCharacterForDrawing(p);
return (p.Name.ToString(), p.Address);
}, StringComparer.Ordinal));
if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer))
{
_logger.LogTrace("Global draw block: END => {name}", _lastGlobalBlockPlayer);
_lastGlobalBlockPlayer = string.Empty;
}
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;
}
}