Split parts of GameFunctions into ChatFunctions; add some logging v0.19
authorLiza Carvelli <liza@carvel.li>
Thu, 20 Jun 2024 19:55:48 +0000 (21:55 +0200)
committerLiza Carvelli <liza@carvel.li>
Thu, 20 Jun 2024 19:56:22 +0000 (21:56 +0200)
16 files changed:
Questionable/ChatFunctions.cs [new file with mode: 0644]
Questionable/Controller/MovementController.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs
Questionable/Controller/Steps/BaseTasks/MountTask.cs
Questionable/Controller/Steps/BaseTasks/UnmountTask.cs
Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs
Questionable/Controller/Steps/InteractionFactory/Emote.cs
Questionable/Controller/Steps/InteractionFactory/Interact.cs
Questionable/Controller/Steps/InteractionFactory/Say.cs
Questionable/Controller/Steps/InteractionFactory/UseItem.cs
Questionable/GameFunctions.cs
Questionable/Questionable.csproj
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigWindow.cs
Questionable/Windows/QuestWindow.cs

diff --git a/Questionable/ChatFunctions.cs b/Questionable/ChatFunctions.cs
new file mode 100644 (file)
index 0000000..5d7abf3
--- /dev/null
@@ -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<EEmote, string> _emoteCommands;
+
+    private readonly GameFunctions _gameFunctions;
+    private readonly ITargetManager _targetManager;
+    private readonly ILogger<ChatFunctions> _logger;
+    private readonly ProcessChatBoxDelegate _processChatBox;
+    private readonly delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString;
+
+    public ChatFunctions(ISigScanner sigScanner, IDataManager dataManager, GameFunctions gameFunctions,
+        ITargetManager targetManager, ILogger<ChatFunctions> logger)
+    {
+        _gameFunctions = gameFunctions;
+        _targetManager = targetManager;
+        _logger = logger;
+        _processChatBox =
+            Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
+        _sanitiseString =
+            (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sigScanner.ScanText(Signatures.SanitiseString);
+
+        _emoteCommands = dataManager.GetExcelSheet<Emote>()!
+            .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();
+    }
+
+    /// <summary>
+    /// <para>
+    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
+    /// </para>
+    /// <para>
+    /// <b>This method is unsafe.</b> 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.
+    /// </para>
+    /// </summary>
+    /// <param name="message">Message to send</param>
+    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
+    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);
+    }
+
+    /// <summary>
+    /// <para>
+    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
+    /// </para>
+    /// <para>
+    /// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. 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.
+    /// </para>
+    /// </summary>
+    /// <param name="message">message to send</param>
+    /// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
+    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
+    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);
+    }
+
+    /// <summary>
+    /// <para>
+    /// Sanitises a string by removing any invalid input.
+    /// </para>
+    /// <para>
+    /// The result of this method is safe to use with
+    /// <see cref="SendMessage"/>, provided that it is not empty or too
+    /// long.
+    /// </para>
+    /// </summary>
+    /// <param name="text">text to sanitise</param>
+    /// <returns>sanitised text</returns>
+    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
+    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);
+        }
+    }
+}
index cc732962ee5235a0841fb52d0beac310d4f283d8..dc9ce459bdb9e798f603b92e9811edda8619ed2a 100644 (file)
@@ -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<MovementController> _logger;
     private CancellationTokenSource? _cancellationTokenSource;
     private Task<List<Vector3>>? _pathfindTask;
 
     public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions,
-        ICondition condition, ILogger<MovementController> logger)
+        ChatFunctions chatFunctions, ICondition condition, ILogger<MovementController> 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");
         }
     }
 
index 71b91a193eda3f8d6c8873609e785b378b9f0b36..9c919258c66dfefe70d81b16e56c4c2a8e8940ab 100644 (file)
@@ -20,7 +20,8 @@ internal sealed class QuestRegistry
 
     private readonly Dictionary<ushort, Quest> _quests = new();
 
-    public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager, ILogger<QuestRegistry> logger)
+    public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager,
+        ILogger<QuestRegistry> logger)
     {
         _pluginInterface = pluginInterface;
         _dataManager = dataManager;
index 31ec90d9e334bd833c48e320b949769cfb761bcc..ae275285c85b8ed3bdbe2debe6f0dafe6fed1a94 100644 (file)
@@ -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<ITask> 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<WaitDelay>(),
                         notInCombat,
                         serviceProvider.GetRequiredService<WaitDelay>(),
@@ -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
index 5e91484fb5371547aca6925c20ac811727e347bb..e600aac98caf43e06db66c78e6b1617d94169026 100644 (file)
@@ -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);
index 761f87b5b378896318f2f4e964fae17e05c3e550..54be937af80726383f077e796c9008904fa6c382 100644 (file)
@@ -20,6 +20,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> 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<UnmountTask> log
             else
                 _unmountTriggered = gameFunctions.Unmount();
 
-            _continueAt = DateTime.Now.AddSeconds(0.5);
+            _continueAt = DateTime.Now.AddSeconds(1);
             return ETaskResult.StillRunning;
         }
 
index 02e7d5fa4186983158aca2069eae56a524814d00..bbe4d3b4f075810283cb96d0e1dca1c7955cbc72 100644 (file)
@@ -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;
         }
 
index 8ecb4f585d8f95ab3c0ca65c0ea341ef8f60033d..fa53a94b6c404c6e37c562ebdd8693cdf3d75c53 100644 (file)
@@ -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;
         }
 
index 7a2b5f3c5b4a411781d42ea2bebe29e9bb4f308f..db1d30ab7168884bdb45a0b020c93372e3705a7a 100644 (file)
@@ -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
index 7f82e9e13d7d5be93c36b0648bde3fb23ede393c..3772252521691176bfea14795bf0fbe42b032cec 100644 (file)
@@ -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<UnmountTask>();
@@ -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;
         }
 
index 323d4a791c9a5892177f6d5fa6660dc058c7571f..2f752b1a4b9fde8ea168d6bf144c12951fa30413 100644 (file)
@@ -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;
                 }
index d1feca24744a2aaab1f8cb04e1730cf8bb3b822a..a1e974924fc9b34dd3651587c2b64409e902b340 100644 (file)
@@ -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<Utf8String*, int, IntPtr, void> _sanitiseString;
     private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
-    private readonly ReadOnlyDictionary<EEmote, string> _emoteCommands;
     private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId;
 
     private readonly IDataManager _dataManager;
@@ -65,9 +48,15 @@ internal sealed unsafe class GameFunctions
     private readonly Configuration _configuration;
     private readonly ILogger<GameFunctions> _logger;
 
-    public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
-        ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
-        IGameGui gameGui, Configuration configuration, ILogger<GameFunctions> logger)
+    public GameFunctions(IDataManager dataManager,
+        IObjectTable objectTable,
+        ITargetManager targetManager,
+        ICondition condition,
+        IClientState clientState,
+        QuestRegistry questRegistry,
+        IGameGui gameGui,
+        Configuration configuration,
+        ILogger<GameFunctions> logger)
     {
         _dataManager = dataManager;
         _objectTable = objectTable;
@@ -78,23 +67,12 @@ internal sealed unsafe class GameFunctions
         _gameGui = gameGui;
         _configuration = configuration;
         _logger = logger;
-        _processChatBox =
-            Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
-        _sanitiseString =
-            (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sigScanner.ScanText(Signatures.SanitiseString);
 
         _territoryToAetherCurrentCompFlgSet = dataManager.GetExcelSheet<TerritoryType>()!
             .Where(x => x.RowId > 0)
             .Where(x => x.Unknown32 > 0)
             .ToDictionary(x => (ushort)x.RowId, x => x.Unknown32)
             .AsReadOnly();
-        _emoteCommands = dataManager.GetExcelSheet<Emote>()!
-            .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<ContentFinderCondition>()!
             .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
-
-    /// <summary>
-    /// <para>
-    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
-    /// </para>
-    /// <para>
-    /// <b>This method is unsafe.</b> 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.
-    /// </para>
-    /// </summary>
-    /// <param name="message">Message to send</param>
-    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
-    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);
-    }
-
-    /// <summary>
-    /// <para>
-    /// Send a given message to the chat box. <b>This can send chat to the server.</b>
-    /// </para>
-    /// <para>
-    /// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. 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.
-    /// </para>
-    /// </summary>
-    /// <param name="message">message to send</param>
-    /// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
-    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
-    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);
-    }
-
-    /// <summary>
-    /// <para>
-    /// Sanitises a string by removing any invalid input.
-    /// </para>
-    /// <para>
-    /// The result of this method is safe to use with
-    /// <see cref="SendMessage"/>, provided that it is not empty or too
-    /// long.
-    /// </para>
-    /// </summary>
-    /// <param name="text">text to sanitise</param>
-    /// <returns>sanitised text</returns>
-    /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
-    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
         {
index c1e5969df9beabbc566a55376fa1ddd12872ccd3..86ac9f004a8ea3f312f0043f65bb6d24e5e88e03 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>net8.0-windows</TargetFramework>
-        <Version>0.18</Version>
+        <Version>0.19</Version>
         <LangVersion>12</LangVersion>
         <Nullable>enable</Nullable>
         <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1" />
+        <PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1"/>
         <PackageReference Include="DalamudPackager" Version="2.1.12"/>
         <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" ExcludeAssets="runtime"/>
-        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
         <PackageReference Include="System.Text.Json" Version="8.0.3"/>
     </ItemGroup>
 
@@ -58,8 +58,8 @@
     </ItemGroup>
 
     <ItemGroup>
-      <ProjectReference Include="..\LLib\LLib.csproj" />
-        <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
-        <ProjectReference Include="..\QuestPaths\QuestPaths.csproj" />
+        <ProjectReference Include="..\LLib\LLib.csproj"/>
+        <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
+        <ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
     </ItemGroup>
 </Project>
index a4f2f9b22e9caa36698a606eb651fca0a33e934e..f2d4835a37cb56cabbcdeedde2af30a55cbe9742 100644 (file)
@@ -65,6 +65,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
 
         serviceCollection.AddSingleton<GameFunctions>();
+        serviceCollection.AddSingleton<ChatFunctions>();
         serviceCollection.AddSingleton<AetheryteData>();
         serviceCollection.AddSingleton<TerritoryData>();
         serviceCollection.AddSingleton<NavmeshIpc>();
@@ -95,7 +96,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
         serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>();
         serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>();
-        serviceCollection.AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready, SinglePlayerDuty.RestoreYesAlready>();
+        serviceCollection
+            .AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready,
+                SinglePlayerDuty.RestoreYesAlready>();
 
         serviceCollection
             .AddTaskWithFactory<WaitAtEnd.Factory,
index b820e7e5921cb575a59380f30d9e25b51b78e48c..4262f39cc4b191ca6aff5a19be734cdff7b78b90 100644 (file)
@@ -20,7 +20,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
     private readonly string[] _mountNames;
 
     private readonly string[] _grandCompanyNames =
-        ["None (manually pick quest)", "Maelstrom", "Twin Adder"/*, "Immortal Flames"*/];
+        ["None (manually pick quest)", "Maelstrom", "Twin Adder" /*, "Immortal Flames"*/];
 
     [SuppressMessage("Performance", "CA1861", Justification = "One time initialization")]
     public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager)
index eb65373be6fa81f34467181d33d4a4caa2e05096..665a158b836f2a4efe18a1732951444a46414cef 100644 (file)
@@ -30,6 +30,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
     private readonly MovementController _movementController;
     private readonly QuestController _questController;
     private readonly GameFunctions _gameFunctions;
+    private readonly ChatFunctions _chatFunctions;
     private readonly IClientState _clientState;
     private readonly IFramework _framework;
     private readonly ITargetManager _targetManager;
@@ -43,6 +44,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
         MovementController movementController,
         QuestController questController,
         GameFunctions gameFunctions,
+        ChatFunctions chatFunctions,
         IClientState clientState,
         IFramework framework,
         ITargetManager targetManager,
@@ -57,6 +59,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
         _movementController = movementController;
         _questController = questController;
         _gameFunctions = gameFunctions;
+        _chatFunctions = chatFunctions;
         _clientState = clientState;
         _framework = framework;
         _targetManager = targetManager;
@@ -325,7 +328,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
         if (ImGui.Button("Move to Flag"))
         {
             _movementController.Destination = null;
-            _gameFunctions.ExecuteCommand(
+            _chatFunctions.ExecuteCommand(
                 $"/vnav {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}");
         }