Split parts of GameFunctions into ChatFunctions; add some logging

This commit is contained in:
Liza 2024-06-20 21:55:48 +02:00
parent 7140fdf025
commit 8227f9af43
Signed by: liza
GPG Key ID: 7199F8D727D55F67
16 changed files with 267 additions and 206 deletions

View File

@ -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);
}
}
}

View File

@ -26,17 +26,19 @@ internal sealed class MovementController : IDisposable
private readonly NavmeshIpc _navmeshIpc; private readonly NavmeshIpc _navmeshIpc;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly ChatFunctions _chatFunctions;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly ILogger<MovementController> _logger; private readonly ILogger<MovementController> _logger;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private Task<List<Vector3>>? _pathfindTask; private Task<List<Vector3>>? _pathfindTask;
public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions, public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions,
ICondition condition, ILogger<MovementController> logger) ChatFunctions chatFunctions, ICondition condition, ILogger<MovementController> logger)
{ {
_navmeshIpc = navmeshIpc; _navmeshIpc = navmeshIpc;
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_chatFunctions = chatFunctions;
_condition = condition; _condition = condition;
_logger = logger; _logger = logger;
} }
@ -199,7 +201,7 @@ internal sealed class MovementController : IDisposable
if (InputManager.IsAutoRunning()) if (InputManager.IsAutoRunning())
{ {
_logger.LogInformation("Turning off auto-move"); _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, Destination = new DestinationData(dataId, to, stopDistance ?? (DefaultStopDistance - 0.2f), fly, sprint,
@ -257,7 +259,7 @@ internal sealed class MovementController : IDisposable
if (InputManager.IsAutoRunning()) if (InputManager.IsAutoRunning())
{ {
_logger.LogInformation("Turning off auto-move [stop]"); _logger.LogInformation("Turning off auto-move [stop]");
_gameFunctions.ExecuteCommand("/automove off"); _chatFunctions.ExecuteCommand("/automove off");
} }
} }

View File

@ -20,7 +20,8 @@ internal sealed class QuestRegistry
private readonly Dictionary<ushort, Quest> _quests = new(); 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; _pluginInterface = pluginInterface;
_dataManager = dataManager; _dataManager = dataManager;

View File

@ -16,7 +16,8 @@ namespace Questionable.Controller.Steps.BaseFactory;
internal static class WaitAtEnd 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) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -31,8 +32,10 @@ internal static class WaitAtEnd
switch (step.InteractionType) switch (step.InteractionType)
{ {
case EInteractionType.Combat: case EInteractionType.Combat:
var notInCombat = new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)"); var notInCombat =
return [ new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
return
[
serviceProvider.GetRequiredService<WaitDelay>(), serviceProvider.GetRequiredService<WaitDelay>(),
notInCombat, notInCombat,
serviceProvider.GetRequiredService<WaitDelay>(), serviceProvider.GetRequiredService<WaitDelay>(),
@ -71,7 +74,8 @@ internal static class WaitAtEnd
if (step.TerritoryId != step.TargetTerritoryId) if (step.TerritoryId != step.TargetTerritoryId)
{ {
// interaction moves to a different territory // 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})"); $"Wait(tp to territory: {step.TargetTerritoryId})");
} }
else else

View File

@ -59,7 +59,9 @@ internal sealed class MountTask(
return false; 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 else
logger.LogInformation("Want to use mount, trying (in territory {Id})...", _territoryId); logger.LogInformation("Want to use mount, trying (in territory {Id})...", _territoryId);

View File

@ -20,6 +20,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
if (condition[ConditionFlag.InFlight]) if (condition[ConditionFlag.InFlight])
{ {
gameFunctions.Unmount(); gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return true; return true;
} }
@ -41,7 +42,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
else else
_unmountTriggered = gameFunctions.Unmount(); _unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(1);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }

View File

@ -45,7 +45,8 @@ internal static class AetherCurrent
return true; 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; return false;
} }

View File

@ -35,7 +35,7 @@ internal static class Emote
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
} }
internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask internal sealed class UseOnObject(ChatFunctions chatFunctions) : AbstractDelayedTask
{ {
public EEmote Emote { get; set; } public EEmote Emote { get; set; }
public uint DataId { get; set; } public uint DataId { get; set; }
@ -49,14 +49,14 @@ internal static class Emote
protected override bool StartInternal() protected override bool StartInternal()
{ {
gameFunctions.UseEmote(DataId, Emote); chatFunctions.UseEmote(DataId, Emote);
return true; return true;
} }
public override string ToString() => $"Emote({Emote} on {DataId})"; 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; } public EEmote Emote { get; set; }
@ -68,7 +68,7 @@ internal static class Emote
protected override bool StartInternal() protected override bool StartInternal()
{ {
gameFunctions.UseEmote(Emote); chatFunctions.UseEmote(Emote);
return true; return true;
} }

View File

@ -66,7 +66,7 @@ internal static class Interact
{ {
_needsUnmount = true; _needsUnmount = true;
gameFunctions.Unmount(); gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(1);
return true; return true;
} }
@ -90,7 +90,7 @@ internal static class Interact
if (condition[ConditionFlag.Mounted]) if (condition[ConditionFlag.Mounted])
{ {
gameFunctions.Unmount(); gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(1);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
else else

View File

@ -19,7 +19,8 @@ internal static class Say
ArgumentNullException.ThrowIfNull(step.ChatMessage); 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); ArgumentNullException.ThrowIfNull(excelString);
var unmount = serviceProvider.GetRequiredService<UnmountTask>(); var unmount = serviceProvider.GetRequiredService<UnmountTask>();
@ -31,7 +32,7 @@ internal static class Say
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
} }
internal sealed class UseChat(GameFunctions gameFunctions) : AbstractDelayedTask internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTask
{ {
public string ChatMessage { get; set; } = null!; public string ChatMessage { get; set; } = null!;
@ -43,7 +44,7 @@ internal static class Say
protected override bool StartInternal() protected override bool StartInternal()
{ {
gameFunctions.ExecuteCommand($"/say {ChatMessage}"); chatFunctions.ExecuteCommand($"/say {ChatMessage}");
return true; return true;
} }

View File

@ -95,7 +95,8 @@ internal static class UseItem
if (itemCount == _itemCount) 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) // 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; _usedItem = false;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }

View File

@ -1,11 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects; 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.Control;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI; 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.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
@ -41,18 +35,7 @@ namespace Questionable;
internal sealed unsafe class GameFunctions 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<ushort, byte> _territoryToAetherCurrentCompFlgSet;
private readonly ReadOnlyDictionary<EEmote, string> _emoteCommands;
private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId; private readonly ReadOnlyDictionary<uint, ushort> _contentFinderConditionToContentId;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
@ -65,9 +48,15 @@ internal sealed unsafe class GameFunctions
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger<GameFunctions> _logger; private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, public GameFunctions(IDataManager dataManager,
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry, IObjectTable objectTable,
IGameGui gameGui, Configuration configuration, ILogger<GameFunctions> logger) ITargetManager targetManager,
ICondition condition,
IClientState clientState,
QuestRegistry questRegistry,
IGameGui gameGui,
Configuration configuration,
ILogger<GameFunctions> logger)
{ {
_dataManager = dataManager; _dataManager = dataManager;
_objectTable = objectTable; _objectTable = objectTable;
@ -78,23 +67,12 @@ internal sealed unsafe class GameFunctions
_gameGui = gameGui; _gameGui = gameGui;
_configuration = configuration; _configuration = configuration;
_logger = logger; _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>()! _territoryToAetherCurrentCompFlgSet = dataManager.GetExcelSheet<TerritoryType>()!
.Where(x => x.RowId > 0) .Where(x => x.RowId > 0)
.Where(x => x.Unknown32 > 0) .Where(x => x.Unknown32 > 0)
.ToDictionary(x => (ushort)x.RowId, x => x.Unknown32) .ToDictionary(x => (ushort)x.RowId, x => x.Unknown32)
.AsReadOnly(); .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>()! _contentFinderConditionToContentId = dataManager.GetExcelSheet<ContentFinderCondition>()!
.Where(x => x.RowId > 0 && x.Content > 0) .Where(x => x.RowId > 0 && x.Content > 0)
.ToDictionary(x => x.RowId, x => x.Content) .ToDictionary(x => x.RowId, x => x.Content)
@ -258,6 +236,7 @@ internal sealed unsafe class GameFunctions
public bool TeleportAetheryte(uint aetheryteId) public bool TeleportAetheryte(uint aetheryteId)
{ {
_logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
if (IsAetheryteUnlocked(aetheryteId, out var subIndex)) if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
{ {
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId && if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
@ -265,13 +244,19 @@ internal sealed unsafe class GameFunctions
{ {
ReturnRequestedAt = DateTime.Now; ReturnRequestedAt = DateTime.Now;
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8)) if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
{
_logger.LogInformation("Using 'return' for home aetheryte");
return true; return true;
} }
}
if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0) if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
{
// fallback if return isn't available or (more likely) on a different aetheryte // 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 Telepo.Instance()->Teleport(aetheryteId, subIndex);
} }
}
return false; return false;
} }
@ -299,134 +284,6 @@ internal sealed unsafe class GameFunctions
playerState->IsAetherCurrentUnlocked(aetherCurrentId); 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) public GameObject? FindObjectByDataId(uint dataId)
{ {
foreach (var gameObject in _objectTable) foreach (var gameObject in _objectTable)
@ -496,21 +353,6 @@ internal sealed unsafe class GameFunctions
return false; 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) public bool IsObjectAtPosition(uint dataId, Vector3 position, float distance)
{ {
GameObject? gameObject = FindObjectByDataId(dataId); GameObject? gameObject = FindObjectByDataId(dataId);
@ -559,6 +401,7 @@ internal sealed unsafe class GameFunctions
{ {
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId) == 0) 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)) if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId))
{ {
_logger.LogInformation("Using preferred mount"); _logger.LogInformation("Using preferred mount");
@ -572,6 +415,7 @@ internal sealed unsafe class GameFunctions
{ {
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0) if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{ {
_logger.LogDebug("Attempting to use mount roulette...");
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9)) if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9))
{ {
_logger.LogInformation("Using mount roulette"); _logger.LogInformation("Using mount roulette");
@ -592,8 +436,14 @@ internal sealed unsafe class GameFunctions
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0) if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
{ {
_logger.LogInformation("Unmounting..."); _logger.LogDebug("Attempting to unmount...");
return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23))
{
_logger.LogInformation("Unmounted");
return true;
}
return false;
} }
else else
{ {

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Version>0.18</Version> <Version>0.19</Version>
<LangVersion>12</LangVersion> <LangVersion>12</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -23,10 +23,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="DalamudPackager" Version="2.1.12"/>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" ExcludeAssets="runtime"/> <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"/> <PackageReference Include="System.Text.Json" Version="8.0.3"/>
</ItemGroup> </ItemGroup>
@ -58,8 +58,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LLib\LLib.csproj" /> <ProjectReference Include="..\LLib\LLib.csproj"/>
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" /> <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj" /> <ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -65,6 +65,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration()); serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
serviceCollection.AddSingleton<GameFunctions>(); serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<AetheryteData>(); serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<TerritoryData>(); serviceCollection.AddSingleton<TerritoryData>();
serviceCollection.AddSingleton<NavmeshIpc>(); serviceCollection.AddSingleton<NavmeshIpc>();
@ -95,7 +96,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>(); serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>(); serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>();
serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>(); serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>();
serviceCollection.AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready, SinglePlayerDuty.RestoreYesAlready>(); serviceCollection
.AddTaskWithFactory<SinglePlayerDuty.Factory, SinglePlayerDuty.DisableYesAlready,
SinglePlayerDuty.RestoreYesAlready>();
serviceCollection serviceCollection
.AddTaskWithFactory<WaitAtEnd.Factory, .AddTaskWithFactory<WaitAtEnd.Factory,

View File

@ -20,7 +20,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
private readonly string[] _mountNames; private readonly string[] _mountNames;
private readonly string[] _grandCompanyNames = 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")] [SuppressMessage("Performance", "CA1861", Justification = "One time initialization")]
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager) public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager)

View File

@ -30,6 +30,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
private readonly MovementController _movementController; private readonly MovementController _movementController;
private readonly QuestController _questController; private readonly QuestController _questController;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly ChatFunctions _chatFunctions;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly ITargetManager _targetManager; private readonly ITargetManager _targetManager;
@ -43,6 +44,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
MovementController movementController, MovementController movementController,
QuestController questController, QuestController questController,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ChatFunctions chatFunctions,
IClientState clientState, IClientState clientState,
IFramework framework, IFramework framework,
ITargetManager targetManager, ITargetManager targetManager,
@ -57,6 +59,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
_movementController = movementController; _movementController = movementController;
_questController = questController; _questController = questController;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_chatFunctions = chatFunctions;
_clientState = clientState; _clientState = clientState;
_framework = framework; _framework = framework;
_targetManager = targetManager; _targetManager = targetManager;
@ -325,7 +328,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
if (ImGui.Button("Move to Flag")) if (ImGui.Button("Move to Flag"))
{ {
_movementController.Destination = null; _movementController.Destination = null;
_gameFunctions.ExecuteCommand( _chatFunctions.ExecuteCommand(
$"/vnav {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}"); $"/vnav {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}");
} }