2025-08-22 02:19:48 +01:00
|
|
|
|
using Dalamud.Game.ClientState.Conditions;
|
|
|
|
|
using Dalamud.Game.ClientState.Objects;
|
|
|
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
|
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
|
|
|
using Dalamud.Plugin.Services;
|
|
|
|
|
using Dalamud.Utility;
|
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
|
|
|
using Lumina.Excel.Sheets;
|
|
|
|
|
using MareSynchronos.API.Dto.CharaData;
|
|
|
|
|
using MareSynchronos.Interop;
|
|
|
|
|
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 System.Text;
|
|
|
|
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
|
|
|
|
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
|
|
|
|
|
|
|
|
|
|
namespace MareSynchronos.Services;
|
|
|
|
|
|
|
|
|
|
public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|
|
|
|
{
|
|
|
|
|
public struct PlayerCharacter
|
|
|
|
|
{
|
|
|
|
|
public uint ObjectId;
|
|
|
|
|
public string Name;
|
|
|
|
|
public uint HomeWorldId;
|
|
|
|
|
public nint Address;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private struct PlayerInfo
|
|
|
|
|
{
|
|
|
|
|
public PlayerCharacter Character;
|
|
|
|
|
public string Hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly List<uint> _classJobIdsIgnoredForPets = [30];
|
|
|
|
|
private readonly IClientState _clientState;
|
|
|
|
|
private readonly ICondition _condition;
|
|
|
|
|
private readonly IDataManager _gameData;
|
|
|
|
|
private readonly BlockedCharacterHandler _blockedCharacterHandler;
|
|
|
|
|
private readonly IFramework _framework;
|
|
|
|
|
private readonly IGameGui _gameGui;
|
|
|
|
|
private readonly IToastGui _toastGui;
|
|
|
|
|
private readonly ILogger<DalamudUtilService> _logger;
|
|
|
|
|
private readonly IObjectTable _objectTable;
|
|
|
|
|
private readonly PerformanceCollectorService _performanceCollector;
|
|
|
|
|
private uint? _classJobId = 0;
|
|
|
|
|
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
|
|
|
|
private string _lastGlobalBlockPlayer = string.Empty;
|
|
|
|
|
private string _lastGlobalBlockReason = string.Empty;
|
|
|
|
|
private ushort _lastZone = 0;
|
|
|
|
|
private readonly Dictionary<string, PlayerCharacter> _playerCharas = new(StringComparer.Ordinal);
|
|
|
|
|
private readonly List<string> _notUpdatedCharas = [];
|
|
|
|
|
private bool _sentBetweenAreas = false;
|
|
|
|
|
private static readonly Dictionary<uint, PlayerInfo> _playerInfoCache = new();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
|
|
|
|
|
IGameGui gameGui, IToastGui toastGui,ICondition condition, IDataManager gameData, ITargetManager targetManager,
|
|
|
|
|
BlockedCharacterHandler blockedCharacterHandler, MareMediator mediator, PerformanceCollectorService performanceCollector)
|
|
|
|
|
{
|
|
|
|
|
_logger = logger;
|
|
|
|
|
_clientState = clientState;
|
|
|
|
|
_objectTable = objectTable;
|
|
|
|
|
_framework = framework;
|
|
|
|
|
_gameGui = gameGui;
|
|
|
|
|
_toastGui = toastGui;
|
|
|
|
|
_condition = condition;
|
|
|
|
|
_gameData = gameData;
|
|
|
|
|
_blockedCharacterHandler = blockedCharacterHandler;
|
|
|
|
|
Mediator = mediator;
|
|
|
|
|
_performanceCollector = performanceCollector;
|
|
|
|
|
WorldData = new(() =>
|
|
|
|
|
{
|
|
|
|
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
|
|
|
|
|
.Where(w => w.Name.ByteLength > 0 && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper((char)w.Name.Data.Span[0])))
|
|
|
|
|
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
|
|
|
|
});
|
|
|
|
|
UiColors = new(() =>
|
|
|
|
|
{
|
|
|
|
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.UIColor>(Dalamud.Game.ClientLanguage.English)!
|
|
|
|
|
.Where(x => x.RowId != 0 && !(x.RowId >= 500 && (x.Dark & 0xFFFFFF00) == 0))
|
|
|
|
|
.ToDictionary(x => (int)x.RowId);
|
|
|
|
|
});
|
|
|
|
|
TerritoryData = new(() =>
|
|
|
|
|
{
|
|
|
|
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
|
|
|
|
.Where(w => w.RowId != 0)
|
|
|
|
|
.ToDictionary(w => w.RowId, w =>
|
|
|
|
|
{
|
|
|
|
|
StringBuilder sb = new();
|
|
|
|
|
sb.Append(w.PlaceNameRegion.Value.Name);
|
|
|
|
|
if (w.PlaceName.ValueNullable != null)
|
|
|
|
|
{
|
|
|
|
|
sb.Append(" - ");
|
|
|
|
|
sb.Append(w.PlaceName.Value.Name);
|
|
|
|
|
}
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
MapData = new(() =>
|
|
|
|
|
{
|
|
|
|
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.Map>(Dalamud.Game.ClientLanguage.English)!
|
|
|
|
|
.Where(w => w.RowId != 0)
|
|
|
|
|
.ToDictionary(w => w.RowId, w =>
|
|
|
|
|
{
|
|
|
|
|
StringBuilder sb = new();
|
|
|
|
|
sb.Append(w.PlaceNameRegion.Value.Name);
|
|
|
|
|
if (w.PlaceName.ValueNullable != null)
|
|
|
|
|
{
|
|
|
|
|
sb.Append(" - ");
|
|
|
|
|
sb.Append(w.PlaceName.Value.Name);
|
|
|
|
|
}
|
|
|
|
|
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
|
|
|
|
|
{
|
|
|
|
|
sb.Append(" - ");
|
|
|
|
|
sb.Append(w.PlaceNameSub.Value.Name);
|
|
|
|
|
}
|
|
|
|
|
return (w, sb.ToString());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
|
|
|
|
{
|
|
|
|
|
if (clientState.IsPvP) return;
|
|
|
|
|
var ident = msg.Pair.GetPlayerNameHash();
|
|
|
|
|
_ = RunOnFrameworkThread(() =>
|
|
|
|
|
{
|
|
|
|
|
var addr = GetPlayerCharacterFromCachedTableByIdent(ident);
|
|
|
|
|
var pc = _clientState.LocalPlayer!;
|
|
|
|
|
var gobj = CreateGameObject(addr);
|
|
|
|
|
// Any further than roughly 55y is out of range for targetting
|
|
|
|
|
if (gobj != null && Vector3.Distance(pc.Position, gobj.Position) < 55.0f)
|
|
|
|
|
targetManager.Target = gobj;
|
|
|
|
|
else
|
|
|
|
|
_toastGui.ShowError("Player out of range.");
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
});
|
|
|
|
|
IsWine = Util.IsWine();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool IsWine { get; init; }
|
|
|
|
|
public unsafe GameObject* GposeTarget
|
|
|
|
|
{
|
|
|
|
|
get => TargetSystem.Instance()->GPoseTarget;
|
|
|
|
|
set => TargetSystem.Instance()->GPoseTarget = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private unsafe bool HasGposeTarget => GposeTarget != null;
|
|
|
|
|
private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex;
|
|
|
|
|
|
|
|
|
|
public async Task<IGameObject?> GetGposeTargetGameObjectAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!HasGposeTarget)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
return await _framework.RunOnFrameworkThread(() => _objectTable[GPoseTargetIdx]).ConfigureAwait(true);
|
|
|
|
|
}
|
|
|
|
|
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 IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
|
|
|
|
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
|
|
|
|
public bool IsInCombatOrPerforming { get; private set; } = false;
|
|
|
|
|
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
|
|
|
|
|
|
|
|
|
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
|
|
|
|
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
|
|
|
|
|
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
|
|
|
|
public Lazy<Dictionary<uint, (Lumina.Excel.Sheets.Map Map, string MapName)>> MapData { get; private set; }
|
|
|
|
|
|
|
|
|
|
public MareMediator Mediator { get; }
|
|
|
|
|
|
|
|
|
|
public Dalamud.Game.ClientState.Objects.Types.IGameObject? CreateGameObject(IntPtr reference)
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
return _objectTable.CreateObjectReference(reference);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Dalamud.Game.ClientState.Objects.Types.IGameObject?> 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.ICharacter? 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.ICharacter)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 async Task<ICharacter?> GetGposeCharacterFromObjectTableByNameAsync(string name, bool onlyGposeCharacters = false)
|
|
|
|
|
{
|
|
|
|
|
return await RunOnFrameworkThread(() => GetGposeCharacterFromObjectTableByName(name, onlyGposeCharacters)).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ICharacter? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false)
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
return (ICharacter?)_objectTable
|
|
|
|
|
.FirstOrDefault(i => (!onlyGposeCharacters || i.ObjectIndex >= 200) && string.Equals(i.Name.ToString(), name, StringComparison.Ordinal));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
|
|
|
|
{
|
|
|
|
|
return _objectTable.Where(o => o.ObjectIndex > 200 && o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player).Cast<ICharacter>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 async Task<IPlayerCharacter> GetPlayerCharacterAsync()
|
|
|
|
|
{
|
|
|
|
|
return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IPlayerCharacter GetPlayerCharacter()
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
return _clientState.LocalPlayer!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IntPtr GetPlayerCharacterFromCachedTableByName(string characterName)
|
|
|
|
|
{
|
|
|
|
|
foreach (var c in _playerCharas.Values)
|
|
|
|
|
{
|
|
|
|
|
if (c.Name.Equals(characterName, StringComparison.Ordinal))
|
|
|
|
|
return c.Address;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() + GetHomeWorldId()).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 GetHomeWorldId()
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
return _clientState.LocalPlayer!.HomeWorld.RowId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public uint GetWorldId()
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
return _clientState.LocalPlayer!.CurrentWorld.RowId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public unsafe LocationInfo GetMapData()
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
var agentMap = AgentMap.Instance();
|
|
|
|
|
var houseMan = HousingManager.Instance();
|
|
|
|
|
uint serverId = 0;
|
|
|
|
|
if (_clientState.LocalPlayer == null) serverId = 0;
|
|
|
|
|
else serverId = _clientState.LocalPlayer.CurrentWorld.RowId;
|
|
|
|
|
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
|
|
|
|
|
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
|
|
|
|
|
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
|
|
|
|
|
uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1);
|
|
|
|
|
uint houseId = 0;
|
|
|
|
|
var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot());
|
|
|
|
|
if (!houseMan->IsInside()) tempHouseId = 0;
|
|
|
|
|
if (tempHouseId < -1)
|
|
|
|
|
{
|
|
|
|
|
divisionId = tempHouseId == -127 ? 2 : (uint)1;
|
|
|
|
|
tempHouseId = 100;
|
|
|
|
|
}
|
|
|
|
|
if (tempHouseId == -1) tempHouseId = 0;
|
|
|
|
|
houseId = (uint)tempHouseId;
|
|
|
|
|
if (houseId != 0)
|
|
|
|
|
{
|
|
|
|
|
territoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
|
|
|
|
|
}
|
|
|
|
|
uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom());
|
|
|
|
|
|
|
|
|
|
return new LocationInfo()
|
|
|
|
|
{
|
|
|
|
|
ServerId = serverId,
|
|
|
|
|
MapId = mapId,
|
|
|
|
|
TerritoryId = territoryId,
|
|
|
|
|
DivisionId = divisionId,
|
|
|
|
|
WardId = wardId,
|
|
|
|
|
HouseId = houseId,
|
|
|
|
|
RoomId = roomId
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
var agentMap = AgentMap.Instance();
|
|
|
|
|
if (agentMap == null) return;
|
|
|
|
|
agentMap->OpenMapByMapId(map.RowId);
|
|
|
|
|
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<LocationInfo> GetMapDataAsync()
|
|
|
|
|
{
|
|
|
|
|
return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<uint> GetWorldIdAsync()
|
|
|
|
|
{
|
|
|
|
|
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<uint> GetHomeWorldIdAsync()
|
|
|
|
|
{
|
|
|
|
|
return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public unsafe bool IsGameObjectPresent(IntPtr key)
|
|
|
|
|
{
|
|
|
|
|
return _objectTable.Any(f => f.Address == key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.IGameObject? obj)
|
|
|
|
|
{
|
|
|
|
|
EnsureIsOnFramework();
|
|
|
|
|
return obj != null && obj.IsValid();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> IsObjectPresentAsync(Dalamud.Game.ClientState.Objects.Types.IGameObject? obj)
|
|
|
|
|
{
|
|
|
|
|
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task RunOnFrameworkThread(System.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)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Starting DalamudUtilService");
|
|
|
|
|
#pragma warning disable S2696 // Instance members should not write to "static" fields
|
2025-08-22 21:17:09 +01:00
|
|
|
|
Snowcloak.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate;
|
2025-08-22 02:19:48 +01:00
|
|
|
|
#pragma warning restore S2696
|
2025-08-22 21:17:09 +01:00
|
|
|
|
_framework.Update += Snowcloak.Plugin.Self.OnFrameworkUpdate;
|
2025-08-22 02:19:48 +01:00
|
|
|
|
if (IsLoggedIn)
|
|
|
|
|
{
|
|
|
|
|
_classJobId = _clientState.LocalPlayer!.ClassJob.RowId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Started DalamudUtilService");
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("Stopping {type}", GetType());
|
|
|
|
|
|
|
|
|
|
Mediator.UnsubscribeAll(this);
|
2025-08-22 21:17:09 +01:00
|
|
|
|
_framework.Update -= Snowcloak.Plugin.Self.OnFrameworkUpdate;
|
2025-08-22 02:19:48 +01:00
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
|
|
|
|
|
{
|
|
|
|
|
if (!_clientState.IsLoggedIn) return;
|
|
|
|
|
|
|
|
|
|
const int tick = 250;
|
|
|
|
|
int curWaitTime = 0;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
|
|
|
|
|
await Task.Delay(tick).ConfigureAwait(true);
|
|
|
|
|
curWaitTime += tick;
|
|
|
|
|
|
|
|
|
|
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"));
|
|
|
|
|
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.IGameObject? obj)
|
|
|
|
|
{
|
|
|
|
|
if (obj == null) return Vector2.Zero;
|
|
|
|
|
return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PlayerCharacter FindPlayerByNameHash(string ident)
|
|
|
|
|
{
|
|
|
|
|
_playerCharas.TryGetValue(ident, out var result);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private unsafe PlayerInfo GetPlayerInfo(DalamudGameObject chara)
|
|
|
|
|
{
|
|
|
|
|
uint id = chara.EntityId;
|
|
|
|
|
|
|
|
|
|
if (!_playerInfoCache.TryGetValue(id, out var info))
|
|
|
|
|
{
|
|
|
|
|
info.Character.ObjectId = id;
|
|
|
|
|
info.Character.Name = chara.Name.TextValue; // ?
|
|
|
|
|
info.Character.HomeWorldId = ((BattleChara*)chara.Address)->Character.HomeWorld;
|
|
|
|
|
info.Character.Address = chara.Address;
|
|
|
|
|
info.Hash = Crypto.GetHash256(info.Character.Name + info.Character.HomeWorldId.ToString());
|
|
|
|
|
_playerInfoCache[id] = info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.Character.Address = chara.Address;
|
|
|
|
|
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private unsafe void CheckCharacterForDrawing(PlayerCharacter p)
|
|
|
|
|
{
|
|
|
|
|
var gameObj = (GameObject*)p.Address;
|
|
|
|
|
var drawObj = gameObj->DrawObject;
|
|
|
|
|
var characterName = p.Name;
|
|
|
|
|
bool isDrawing = false;
|
|
|
|
|
bool isDrawingChanged = 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 && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
|
|
|
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
_lastGlobalBlockPlayer = characterName;
|
|
|
|
|
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
|
|
|
|
isDrawingChanged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
|
|
|
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
_lastGlobalBlockPlayer = characterName;
|
|
|
|
|
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
|
|
|
|
isDrawingChanged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
|
|
|
|
&& !string.Equals(_lastGlobalBlockReason, "RenderFlags", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
_lastGlobalBlockPlayer = characterName;
|
|
|
|
|
_lastGlobalBlockReason = "RenderFlags";
|
|
|
|
|
isDrawingChanged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isDrawingChanged)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("Global draw block: START => {name} ({reason})", characterName, _lastGlobalBlockReason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IsAnythingDrawing |= isDrawing;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FrameworkOnUpdate(IFramework framework)
|
|
|
|
|
{
|
|
|
|
|
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdate", FrameworkOnUpdateInternal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private unsafe void FrameworkOnUpdateInternal()
|
|
|
|
|
{
|
|
|
|
|
if (_clientState.LocalPlayer?.IsDead ?? false)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddMilliseconds(200);
|
|
|
|
|
|
|
|
|
|
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
|
|
|
|
{
|
|
|
|
|
IsAnythingDrawing = false;
|
|
|
|
|
_performanceCollector.LogPerformance(this, $"ObjTableToCharas",
|
|
|
|
|
() =>
|
|
|
|
|
{
|
|
|
|
|
if (_sentBetweenAreas)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_notUpdatedCharas.AddRange(_playerCharas.Keys);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 200; i += 2)
|
|
|
|
|
{
|
|
|
|
|
var chara = _objectTable[i];
|
|
|
|
|
if (chara == null || chara.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (_blockedCharacterHandler.IsCharacterBlocked(chara.Address, out bool firstTime) && firstTime)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("Skipping character {addr}, blocked/muted", chara.Address.ToString("X"));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var info = GetPlayerInfo(chara);
|
|
|
|
|
|
|
|
|
|
if (!IsAnythingDrawing)
|
|
|
|
|
CheckCharacterForDrawing(info.Character);
|
|
|
|
|
_notUpdatedCharas.Remove(info.Hash);
|
|
|
|
|
_playerCharas[info.Hash] = info.Character;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var notUpdatedChara in _notUpdatedCharas)
|
|
|
|
|
{
|
|
|
|
|
_playerCharas.Remove(notUpdatedChara);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_notUpdatedCharas.Clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("Global draw block: END => {name}", _lastGlobalBlockPlayer);
|
|
|
|
|
_lastGlobalBlockPlayer = string.Empty;
|
|
|
|
|
_lastGlobalBlockReason = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_clientState.IsGPosing && !IsInGpose)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Gpose start");
|
|
|
|
|
IsInGpose = true;
|
|
|
|
|
Mediator.Publish(new GposeStartMessage());
|
|
|
|
|
}
|
|
|
|
|
else if (!_clientState.IsGPosing && IsInGpose)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Gpose end");
|
|
|
|
|
IsInGpose = false;
|
|
|
|
|
Mediator.Publish(new GposeEndMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Combat/Performance start");
|
|
|
|
|
IsInCombatOrPerforming = true;
|
|
|
|
|
Mediator.Publish(new CombatOrPerformanceStartMessage());
|
|
|
|
|
Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming)));
|
|
|
|
|
}
|
|
|
|
|
else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Combat/Performance end");
|
|
|
|
|
IsInCombatOrPerforming = false;
|
|
|
|
|
Mediator.Publish(new CombatOrPerformanceEndMessage());
|
|
|
|
|
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Cutscene start");
|
|
|
|
|
IsInCutscene = true;
|
|
|
|
|
Mediator.Publish(new CutsceneStartMessage());
|
|
|
|
|
Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene)));
|
|
|
|
|
}
|
|
|
|
|
else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Cutscene end");
|
|
|
|
|
IsInCutscene = false;
|
|
|
|
|
Mediator.Publish(new CutsceneEndMessage());
|
|
|
|
|
Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsInCutscene)
|
|
|
|
|
{
|
|
|
|
|
Mediator.Publish(new CutsceneFrameworkUpdateMessage());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51])
|
|
|
|
|
{
|
|
|
|
|
var zone = _clientState.TerritoryType;
|
|
|
|
|
if (_lastZone != zone)
|
|
|
|
|
{
|
|
|
|
|
_lastZone = zone;
|
|
|
|
|
if (!_sentBetweenAreas)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Zone switch/Gpose start");
|
|
|
|
|
_sentBetweenAreas = true;
|
|
|
|
|
_playerInfoCache.Clear();
|
|
|
|
|
Mediator.Publish(new ZoneSwitchStartMessage());
|
|
|
|
|
Mediator.Publish(new HaltScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_sentBetweenAreas)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Zone switch/Gpose end");
|
|
|
|
|
_sentBetweenAreas = false;
|
|
|
|
|
Mediator.Publish(new ZoneSwitchEndMessage());
|
|
|
|
|
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var localPlayer = _clientState.LocalPlayer;
|
|
|
|
|
if (localPlayer != null)
|
|
|
|
|
{
|
|
|
|
|
_classJobId = localPlayer.ClassJob.RowId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
|
|
|
|
|
|
|
|
|
if (!IsInCombatOrPerforming)
|
|
|
|
|
Mediator.Publish(new FrameworkUpdateMessage());
|
|
|
|
|
|
|
|
|
|
if (isNormalFrameworkUpdate)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (localPlayer != null && !IsLoggedIn)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Logged in");
|
|
|
|
|
IsLoggedIn = true;
|
|
|
|
|
_lastZone = _clientState.TerritoryType;
|
|
|
|
|
Mediator.Publish(new DalamudLoginMessage());
|
|
|
|
|
}
|
|
|
|
|
else if (localPlayer == null && IsLoggedIn)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Logged out");
|
|
|
|
|
IsLoggedIn = false;
|
|
|
|
|
Mediator.Publish(new DalamudLogoutMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsInCombatOrPerforming)
|
|
|
|
|
Mediator.Publish(new FrameworkUpdateMessage());
|
|
|
|
|
|
|
|
|
|
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
|
|
|
|
|
|
|
|
|
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|