diff --git a/Questionable/ChatFunctions.cs b/Questionable/ChatFunctions.cs new file mode 100644 index 000000000..5d7abf3f6 --- /dev/null +++ b/Questionable/ChatFunctions.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Client.System.String; +using Lumina.Excel.GeneratedSheets; +using Microsoft.Extensions.Logging; +using Questionable.Model.V1; + +namespace Questionable; + +internal sealed unsafe class ChatFunctions +{ + private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4); + + private readonly ReadOnlyDictionary _emoteCommands; + + private readonly GameFunctions _gameFunctions; + private readonly ITargetManager _targetManager; + private readonly ILogger _logger; + private readonly ProcessChatBoxDelegate _processChatBox; + private readonly delegate* unmanaged _sanitiseString; + + public ChatFunctions(ISigScanner sigScanner, IDataManager dataManager, GameFunctions gameFunctions, + ITargetManager targetManager, ILogger logger) + { + _gameFunctions = gameFunctions; + _targetManager = targetManager; + _logger = logger; + _processChatBox = + Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText(Signatures.SendChat)); + _sanitiseString = + (delegate* unmanaged)sigScanner.ScanText(Signatures.SanitiseString); + + _emoteCommands = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .Where(x => x.TextCommand != null && x.TextCommand.Value != null) + .Select(x => (x.RowId, Command: x.TextCommand.Value!.Command?.ToString())) + .Where(x => x.Command != null && x.Command.StartsWith('/')) + .ToDictionary(x => (EEmote)x.RowId, x => x.Command!) + .AsReadOnly(); + } + + /// + /// + /// Send a given message to the chat box. This can send chat to the server. + /// + /// + /// This method is unsafe. This method does no checking on your input and + /// may send content to the server that the normal client could not. You must + /// verify what you're sending and handle content and length to properly use + /// this. + /// + /// + /// Message to send + /// If the signature for this function could not be found + private void SendMessageUnsafe(byte[] message) + { + var uiModule = (IntPtr)Framework.Instance()->GetUiModule(); + + using var payload = new ChatPayload(message); + var mem1 = Marshal.AllocHGlobal(400); + Marshal.StructureToPtr(payload, mem1, false); + + _processChatBox(uiModule, mem1, IntPtr.Zero, 0); + + Marshal.FreeHGlobal(mem1); + } + + /// + /// + /// Send a given message to the chat box. This can send chat to the server. + /// + /// + /// This method is slightly less unsafe than . It + /// will throw exceptions for certain inputs that the client can't normally send, + /// but it is still possible to make mistakes. Use with caution. + /// + /// + /// message to send + /// If is empty, longer than 500 bytes in UTF-8, or contains invalid characters. + /// If the signature for this function could not be found + private void SendMessage(string message) + { + _logger.LogDebug("Attempting to send chat message '{Message}'", message); + var bytes = Encoding.UTF8.GetBytes(message); + if (bytes.Length == 0) + throw new ArgumentException("message is empty", nameof(message)); + + if (bytes.Length > 500) + throw new ArgumentException("message is longer than 500 bytes", nameof(message)); + + if (message.Length != SanitiseText(message).Length) + throw new ArgumentException("message contained invalid characters", nameof(message)); + + SendMessageUnsafe(bytes); + } + + /// + /// + /// Sanitises a string by removing any invalid input. + /// + /// + /// The result of this method is safe to use with + /// , provided that it is not empty or too + /// long. + /// + /// + /// text to sanitise + /// sanitised text + /// If the signature for this function could not be found + private string SanitiseText(string text) + { + var uText = Utf8String.FromString(text); + + _sanitiseString(uText, 0x27F, IntPtr.Zero); + var sanitised = uText->ToString(); + + uText->Dtor(); + IMemorySpace.Free(uText); + + return sanitised; + } + + public void ExecuteCommand(string command) + { + if (!command.StartsWith('/')) + return; + + SendMessage(command); + } + + public void UseEmote(uint dataId, EEmote emote) + { + GameObject? gameObject = _gameFunctions.FindObjectByDataId(dataId); + if (gameObject != null) + { + _targetManager.Target = gameObject; + ExecuteCommand($"{_emoteCommands[emote]} motion"); + } + } + + public void UseEmote(EEmote emote) + { + ExecuteCommand($"{_emoteCommands[emote]} motion"); + } + + private static class Signatures + { + internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"; + internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D"; + } + + [StructLayout(LayoutKind.Explicit)] + [SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")] + private readonly struct ChatPayload : IDisposable + { + [FieldOffset(0)] private readonly IntPtr textPtr; + + [FieldOffset(16)] private readonly ulong textLen; + + [FieldOffset(8)] private readonly ulong unk1; + + [FieldOffset(24)] private readonly ulong unk2; + + internal ChatPayload(byte[] stringBytes) + { + textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); + Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length); + Marshal.WriteByte(textPtr + stringBytes.Length, 0); + + textLen = (ulong)(stringBytes.Length + 1); + + unk1 = 64; + unk2 = 0; + } + + public void Dispose() + { + Marshal.FreeHGlobal(textPtr); + } + } +} diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index cc732962e..dc9ce459b 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -26,17 +26,19 @@ internal sealed class MovementController : IDisposable private readonly NavmeshIpc _navmeshIpc; private readonly IClientState _clientState; private readonly GameFunctions _gameFunctions; + private readonly ChatFunctions _chatFunctions; private readonly ICondition _condition; private readonly ILogger _logger; private CancellationTokenSource? _cancellationTokenSource; private Task>? _pathfindTask; public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions, - ICondition condition, ILogger logger) + ChatFunctions chatFunctions, ICondition condition, ILogger logger) { _navmeshIpc = navmeshIpc; _clientState = clientState; _gameFunctions = gameFunctions; + _chatFunctions = chatFunctions; _condition = condition; _logger = logger; } @@ -199,7 +201,7 @@ internal sealed class MovementController : IDisposable if (InputManager.IsAutoRunning()) { _logger.LogInformation("Turning off auto-move"); - _gameFunctions.ExecuteCommand("/automove off"); + _chatFunctions.ExecuteCommand("/automove off"); } Destination = new DestinationData(dataId, to, stopDistance ?? (DefaultStopDistance - 0.2f), fly, sprint, @@ -257,7 +259,7 @@ internal sealed class MovementController : IDisposable if (InputManager.IsAutoRunning()) { _logger.LogInformation("Turning off auto-move [stop]"); - _gameFunctions.ExecuteCommand("/automove off"); + _chatFunctions.ExecuteCommand("/automove off"); } } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 71b91a193..9c919258c 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -20,7 +20,8 @@ internal sealed class QuestRegistry private readonly Dictionary _quests = new(); - public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager, ILogger logger) + public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager, + ILogger logger) { _pluginInterface = pluginInterface; _dataManager = dataManager; diff --git a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs index 31ec90d9e..ae275285c 100644 --- a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs @@ -16,7 +16,8 @@ namespace Questionable.Controller.Steps.BaseFactory; internal static class WaitAtEnd { - internal sealed class Factory(IServiceProvider serviceProvider, IClientState clientState, ICondition condition) : ITaskFactory + internal sealed class Factory(IServiceProvider serviceProvider, IClientState clientState, ICondition condition) + : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -31,8 +32,10 @@ internal static class WaitAtEnd switch (step.InteractionType) { case EInteractionType.Combat: - var notInCombat = new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)"); - return [ + var notInCombat = + new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)"); + return + [ serviceProvider.GetRequiredService(), notInCombat, serviceProvider.GetRequiredService(), @@ -71,7 +74,8 @@ internal static class WaitAtEnd if (step.TerritoryId != step.TargetTerritoryId) { // interaction moves to a different territory - waitInteraction = new WaitConditionTask(() => clientState.TerritoryType == step.TargetTerritoryId, + waitInteraction = new WaitConditionTask( + () => clientState.TerritoryType == step.TargetTerritoryId, $"Wait(tp to territory: {step.TargetTerritoryId})"); } else diff --git a/Questionable/Controller/Steps/BaseTasks/MountTask.cs b/Questionable/Controller/Steps/BaseTasks/MountTask.cs index 5e91484fb..e600aac98 100644 --- a/Questionable/Controller/Steps/BaseTasks/MountTask.cs +++ b/Questionable/Controller/Steps/BaseTasks/MountTask.cs @@ -59,7 +59,9 @@ internal sealed class MountTask( return false; } - logger.LogInformation("Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...", distance, _territoryId); + logger.LogInformation( + "Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...", + distance, _territoryId); } else logger.LogInformation("Want to use mount, trying (in territory {Id})...", _territoryId); diff --git a/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs b/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs index 761f87b5b..54be937af 100644 --- a/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs +++ b/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs @@ -20,6 +20,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger log if (condition[ConditionFlag.InFlight]) { gameFunctions.Unmount(); + _continueAt = DateTime.Now.AddSeconds(1); return true; } @@ -41,7 +42,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger log else _unmountTriggered = gameFunctions.Unmount(); - _continueAt = DateTime.Now.AddSeconds(0.5); + _continueAt = DateTime.Now.AddSeconds(1); return ETaskResult.StillRunning; } diff --git a/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs b/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs index 02e7d5fa4..bbe4d3b4f 100644 --- a/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs +++ b/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs @@ -45,7 +45,8 @@ internal static class AetherCurrent return true; } - logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", AetherCurrentId, DataId); + logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", AetherCurrentId, + DataId); return false; } diff --git a/Questionable/Controller/Steps/InteractionFactory/Emote.cs b/Questionable/Controller/Steps/InteractionFactory/Emote.cs index 8ecb4f585..fa53a94b6 100644 --- a/Questionable/Controller/Steps/InteractionFactory/Emote.cs +++ b/Questionable/Controller/Steps/InteractionFactory/Emote.cs @@ -35,7 +35,7 @@ internal static class Emote => throw new InvalidOperationException(); } - internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask + internal sealed class UseOnObject(ChatFunctions chatFunctions) : AbstractDelayedTask { public EEmote Emote { get; set; } public uint DataId { get; set; } @@ -49,14 +49,14 @@ internal static class Emote protected override bool StartInternal() { - gameFunctions.UseEmote(DataId, Emote); + chatFunctions.UseEmote(DataId, Emote); return true; } public override string ToString() => $"Emote({Emote} on {DataId})"; } - internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask + internal sealed class Use(ChatFunctions chatFunctions) : AbstractDelayedTask { public EEmote Emote { get; set; } @@ -68,7 +68,7 @@ internal static class Emote protected override bool StartInternal() { - gameFunctions.UseEmote(Emote); + chatFunctions.UseEmote(Emote); return true; } diff --git a/Questionable/Controller/Steps/InteractionFactory/Interact.cs b/Questionable/Controller/Steps/InteractionFactory/Interact.cs index 7a2b5f3c5..db1d30ab7 100644 --- a/Questionable/Controller/Steps/InteractionFactory/Interact.cs +++ b/Questionable/Controller/Steps/InteractionFactory/Interact.cs @@ -66,7 +66,7 @@ internal static class Interact { _needsUnmount = true; gameFunctions.Unmount(); - _continueAt = DateTime.Now.AddSeconds(0.5); + _continueAt = DateTime.Now.AddSeconds(1); return true; } @@ -90,7 +90,7 @@ internal static class Interact if (condition[ConditionFlag.Mounted]) { gameFunctions.Unmount(); - _continueAt = DateTime.Now.AddSeconds(0.5); + _continueAt = DateTime.Now.AddSeconds(1); return ETaskResult.StillRunning; } else diff --git a/Questionable/Controller/Steps/InteractionFactory/Say.cs b/Questionable/Controller/Steps/InteractionFactory/Say.cs index 7f82e9e13..377225252 100644 --- a/Questionable/Controller/Steps/InteractionFactory/Say.cs +++ b/Questionable/Controller/Steps/InteractionFactory/Say.cs @@ -19,7 +19,8 @@ internal static class Say ArgumentNullException.ThrowIfNull(step.ChatMessage); - string? excelString = gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key); + string? excelString = + gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key); ArgumentNullException.ThrowIfNull(excelString); var unmount = serviceProvider.GetRequiredService(); @@ -31,7 +32,7 @@ internal static class Say => throw new InvalidOperationException(); } - internal sealed class UseChat(GameFunctions gameFunctions) : AbstractDelayedTask + internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTask { public string ChatMessage { get; set; } = null!; @@ -43,7 +44,7 @@ internal static class Say protected override bool StartInternal() { - gameFunctions.ExecuteCommand($"/say {ChatMessage}"); + chatFunctions.ExecuteCommand($"/say {ChatMessage}"); return true; } diff --git a/Questionable/Controller/Steps/InteractionFactory/UseItem.cs b/Questionable/Controller/Steps/InteractionFactory/UseItem.cs index 323d4a791..2f752b1a4 100644 --- a/Questionable/Controller/Steps/InteractionFactory/UseItem.cs +++ b/Questionable/Controller/Steps/InteractionFactory/UseItem.cs @@ -95,7 +95,8 @@ internal static class UseItem if (itemCount == _itemCount) { // TODO Better handling for game-provided errors, i.e. reacting to the 'Could not use' messages. UseItem() is successful in this case (and returns 0) - logger.LogInformation("Attempted to use vesper bay aetheryte ticket, but it didn't consume an item - reattempting next frame"); + logger.LogInformation( + "Attempted to use vesper bay aetheryte ticket, but it didn't consume an item - reattempting next frame"); _usedItem = false; return ETaskResult.StillRunning; } diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index d1feca247..a1e974924 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; @@ -17,9 +14,6 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; -using FFXIVClientStructs.FFXIV.Client.System.Framework; -using FFXIVClientStructs.FFXIV.Client.System.Memory; -using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; @@ -41,18 +35,7 @@ namespace Questionable; internal sealed unsafe class GameFunctions { - private static class Signatures - { - internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"; - internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D"; - } - - private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4); - - private readonly ProcessChatBoxDelegate _processChatBox; - private readonly delegate* unmanaged _sanitiseString; private readonly ReadOnlyDictionary _territoryToAetherCurrentCompFlgSet; - private readonly ReadOnlyDictionary _emoteCommands; private readonly ReadOnlyDictionary _contentFinderConditionToContentId; private readonly IDataManager _dataManager; @@ -65,9 +48,15 @@ internal sealed unsafe class GameFunctions private readonly Configuration _configuration; private readonly ILogger _logger; - public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, - ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry, - IGameGui gameGui, Configuration configuration, ILogger logger) + public GameFunctions(IDataManager dataManager, + IObjectTable objectTable, + ITargetManager targetManager, + ICondition condition, + IClientState clientState, + QuestRegistry questRegistry, + IGameGui gameGui, + Configuration configuration, + ILogger logger) { _dataManager = dataManager; _objectTable = objectTable; @@ -78,23 +67,12 @@ internal sealed unsafe class GameFunctions _gameGui = gameGui; _configuration = configuration; _logger = logger; - _processChatBox = - Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText(Signatures.SendChat)); - _sanitiseString = - (delegate* unmanaged)sigScanner.ScanText(Signatures.SanitiseString); _territoryToAetherCurrentCompFlgSet = dataManager.GetExcelSheet()! .Where(x => x.RowId > 0) .Where(x => x.Unknown32 > 0) .ToDictionary(x => (ushort)x.RowId, x => x.Unknown32) .AsReadOnly(); - _emoteCommands = dataManager.GetExcelSheet()! - .Where(x => x.RowId > 0) - .Where(x => x.TextCommand != null && x.TextCommand.Value != null) - .Select(x => (x.RowId, Command: x.TextCommand.Value!.Command?.ToString())) - .Where(x => x.Command != null && x.Command.StartsWith('/')) - .ToDictionary(x => (EEmote)x.RowId, x => x.Command!) - .AsReadOnly(); _contentFinderConditionToContentId = dataManager.GetExcelSheet()! .Where(x => x.RowId > 0 && x.Content > 0) .ToDictionary(x => x.RowId, x => x.Content) @@ -258,6 +236,7 @@ internal sealed unsafe class GameFunctions public bool TeleportAetheryte(uint aetheryteId) { + _logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId); if (IsAetheryteUnlocked(aetheryteId, out var subIndex)) { if (aetheryteId == PlayerState.Instance()->HomeAetheryteId && @@ -265,12 +244,18 @@ internal sealed unsafe class GameFunctions { ReturnRequestedAt = DateTime.Now; if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8)) + { + _logger.LogInformation("Using 'return' for home aetheryte"); return true; + } } if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0) + { // fallback if return isn't available or (more likely) on a different aetheryte + _logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId); return Telepo.Instance()->Teleport(aetheryteId, subIndex); + } } return false; @@ -299,134 +284,6 @@ internal sealed unsafe class GameFunctions playerState->IsAetherCurrentUnlocked(aetherCurrentId); } - public void ExecuteCommand(string command) - { - if (!command.StartsWith('/')) - return; - - SendMessage(command); - } - - #region SendMessage - - /// - /// - /// Send a given message to the chat box. This can send chat to the server. - /// - /// - /// This method is unsafe. This method does no checking on your input and - /// may send content to the server that the normal client could not. You must - /// verify what you're sending and handle content and length to properly use - /// this. - /// - /// - /// Message to send - /// If the signature for this function could not be found - private void SendMessageUnsafe(byte[] message) - { - var uiModule = (IntPtr)Framework.Instance()->GetUiModule(); - - using var payload = new ChatPayload(message); - var mem1 = Marshal.AllocHGlobal(400); - Marshal.StructureToPtr(payload, mem1, false); - - _processChatBox(uiModule, mem1, IntPtr.Zero, 0); - - Marshal.FreeHGlobal(mem1); - } - - /// - /// - /// Send a given message to the chat box. This can send chat to the server. - /// - /// - /// This method is slightly less unsafe than . It - /// will throw exceptions for certain inputs that the client can't normally send, - /// but it is still possible to make mistakes. Use with caution. - /// - /// - /// message to send - /// If is empty, longer than 500 bytes in UTF-8, or contains invalid characters. - /// If the signature for this function could not be found - public void SendMessage(string message) - { - var bytes = Encoding.UTF8.GetBytes(message); - if (bytes.Length == 0) - { - throw new ArgumentException("message is empty", nameof(message)); - } - - if (bytes.Length > 500) - { - throw new ArgumentException("message is longer than 500 bytes", nameof(message)); - } - - if (message.Length != SanitiseText(message).Length) - { - throw new ArgumentException("message contained invalid characters", nameof(message)); - } - - SendMessageUnsafe(bytes); - } - - /// - /// - /// Sanitises a string by removing any invalid input. - /// - /// - /// The result of this method is safe to use with - /// , provided that it is not empty or too - /// long. - /// - /// - /// text to sanitise - /// sanitised text - /// If the signature for this function could not be found - public string SanitiseText(string text) - { - var uText = Utf8String.FromString(text); - - _sanitiseString(uText, 0x27F, IntPtr.Zero); - var sanitised = uText->ToString(); - - uText->Dtor(); - IMemorySpace.Free(uText); - - return sanitised; - } - - [StructLayout(LayoutKind.Explicit)] - [SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")] - private readonly struct ChatPayload : IDisposable - { - [FieldOffset(0)] private readonly IntPtr textPtr; - - [FieldOffset(16)] private readonly ulong textLen; - - [FieldOffset(8)] private readonly ulong unk1; - - [FieldOffset(24)] private readonly ulong unk2; - - internal ChatPayload(byte[] stringBytes) - { - textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); - Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length); - Marshal.WriteByte(textPtr + stringBytes.Length, 0); - - textLen = (ulong)(stringBytes.Length + 1); - - unk1 = 64; - unk2 = 0; - } - - public void Dispose() - { - Marshal.FreeHGlobal(textPtr); - } - } - - #endregion - public GameObject? FindObjectByDataId(uint dataId) { foreach (var gameObject in _objectTable) @@ -496,21 +353,6 @@ internal sealed unsafe class GameFunctions return false; } - public void UseEmote(uint dataId, EEmote emote) - { - GameObject? gameObject = FindObjectByDataId(dataId); - if (gameObject != null) - { - _targetManager.Target = gameObject; - ExecuteCommand($"{_emoteCommands[emote]} motion"); - } - } - - public void UseEmote(EEmote emote) - { - ExecuteCommand($"{_emoteCommands[emote]} motion"); - } - public bool IsObjectAtPosition(uint dataId, Vector3 position, float distance) { GameObject? gameObject = FindObjectByDataId(dataId); @@ -559,6 +401,7 @@ internal sealed unsafe class GameFunctions { if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId) == 0) { + _logger.LogDebug("Attempting to use preferred mount..."); if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId)) { _logger.LogInformation("Using preferred mount"); @@ -572,6 +415,7 @@ internal sealed unsafe class GameFunctions { if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0) { + _logger.LogDebug("Attempting to use mount roulette..."); if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9)) { _logger.LogInformation("Using mount roulette"); @@ -592,8 +436,14 @@ internal sealed unsafe class GameFunctions if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0) { - _logger.LogInformation("Unmounting..."); - return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); + _logger.LogDebug("Attempting to unmount..."); + if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23)) + { + _logger.LogInformation("Unmounted"); + return true; + } + + return false; } else { diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index c1e5969df..86ac9f004 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -1,7 +1,7 @@  net8.0-windows - 0.18 + 0.19 12 enable true @@ -23,10 +23,10 @@ - + - + @@ -58,8 +58,8 @@ - - - + + + diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index a4f2f9b22..f2d4835a3 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -65,6 +65,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration()); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -95,7 +96,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); - serviceCollection.AddTaskWithFactory(); + serviceCollection + .AddTaskWithFactory(); serviceCollection .AddTaskWithFactory