forked from liza/Questionable
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 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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")}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user