Split parts of GameFunctions into ChatFunctions; add some logging
This commit is contained in:
parent
7140fdf025
commit
8227f9af43
192
Questionable/ChatFunctions.cs
Normal file
192
Questionable/ChatFunctions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,13 +244,19 @@ 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
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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")}");
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user