2024-05-25 21:51:37 +00:00
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Diagnostics.CodeAnalysis ;
using System.Linq ;
2024-05-26 19:45:26 +00:00
using System.Numerics ;
2024-05-25 21:51:37 +00:00
using System.Runtime.InteropServices ;
using System.Text ;
using Dalamud.Game ;
2024-05-27 19:54:34 +00:00
using Dalamud.Game.ClientState.Conditions ;
2024-05-26 19:45:26 +00:00
using Dalamud.Game.ClientState.Objects ;
2024-05-27 19:54:34 +00:00
using Dalamud.Game.ClientState.Objects.Types ;
2024-05-25 21:51:37 +00:00
using Dalamud.Plugin.Services ;
2024-05-28 20:24:06 +00:00
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions ;
2024-05-25 21:51:37 +00:00
using FFXIVClientStructs.FFXIV.Client.Game ;
2024-05-26 19:45:26 +00:00
using FFXIVClientStructs.FFXIV.Client.Game.Control ;
2024-05-27 19:54:34 +00:00
using FFXIVClientStructs.FFXIV.Client.Game.Object ;
2024-05-25 21:51:37 +00:00
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 Lumina.Excel.GeneratedSheets ;
2024-05-28 20:24:06 +00:00
using Questionable.Controller ;
2024-05-26 19:45:26 +00:00
using Questionable.Model.V1 ;
2024-05-27 19:54:34 +00:00
using BattleChara = FFXIVClientStructs . FFXIV . Client . Game . Character . BattleChara ;
using GameObject = Dalamud . Game . ClientState . Objects . Types . GameObject ;
2024-05-25 21:51:37 +00:00
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 ;
2024-05-27 19:54:34 +00:00
private readonly ReadOnlyDictionary < EEmote , string > _emoteCommands ;
2024-05-25 21:51:37 +00:00
2024-05-26 19:45:26 +00:00
private readonly IObjectTable _objectTable ;
private readonly ITargetManager _targetManager ;
2024-05-27 19:54:34 +00:00
private readonly ICondition _condition ;
2024-05-31 23:26:46 +00:00
private readonly IClientState _clientState ;
2024-05-26 19:45:26 +00:00
private readonly IPluginLog _pluginLog ;
2024-05-27 19:54:34 +00:00
public GameFunctions ( IDataManager dataManager , IObjectTable objectTable , ISigScanner sigScanner ,
2024-05-31 23:26:46 +00:00
ITargetManager targetManager , ICondition condition , IClientState clientState , IPluginLog pluginLog )
2024-05-25 21:51:37 +00:00
{
2024-05-26 19:45:26 +00:00
_objectTable = objectTable ;
_targetManager = targetManager ;
2024-05-27 19:54:34 +00:00
_condition = condition ;
2024-05-31 23:26:46 +00:00
_clientState = clientState ;
2024-05-26 19:45:26 +00:00
_pluginLog = pluginLog ;
2024-05-25 21:51:37 +00:00
_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 ( ) ;
2024-05-27 19:54:34 +00:00
_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 ( ) ;
2024-05-25 21:51:37 +00:00
}
2024-05-31 23:26:46 +00:00
// FIXME
public QuestController QuestController { private get ; set ; } = null ! ;
2024-05-28 20:24:06 +00:00
2024-05-26 19:45:26 +00:00
public ( ushort CurrentQuest , byte Sequence ) GetCurrentQuest ( )
2024-05-25 21:51:37 +00:00
{
2024-05-28 20:24:06 +00:00
ushort currentQuest ;
// if any quest that is currently tracked (i.e. in the to-do list) exists as mapped quest, we use that
var questManager = QuestManager . Instance ( ) ;
if ( questManager ! = null )
2024-05-25 21:51:37 +00:00
{
2024-05-28 20:24:06 +00:00
foreach ( var tracked in questManager - > TrackedQuestsSpan )
{
switch ( tracked . QuestType )
{
default :
continue ;
case 1 : // normal quest
currentQuest = questManager - > NormalQuestsSpan [ tracked . Index ] . QuestId ;
break ;
}
if ( QuestController . IsKnownQuest ( currentQuest ) )
return ( currentQuest , QuestManager . GetQuestSequence ( currentQuest ) ) ;
}
2024-05-25 21:51:37 +00:00
}
2024-05-28 20:24:06 +00:00
var scenarioTree = AgentScenarioTree . Instance ( ) ;
if ( scenarioTree = = null )
return default ;
2024-05-25 21:51:37 +00:00
if ( scenarioTree - > Data = = null )
2024-05-28 20:24:06 +00:00
return default ;
2024-05-25 21:51:37 +00:00
2024-05-28 20:24:06 +00:00
currentQuest = scenarioTree - > Data - > CurrentScenarioQuest ;
2024-05-25 21:51:37 +00:00
if ( currentQuest = = 0 )
2024-05-28 20:24:06 +00:00
return default ;
return ( currentQuest , QuestManager . GetQuestSequence ( currentQuest ) ) ;
}
public QuestWork ? GetQuestEx ( ushort questId )
{
QuestWork * questWork = QuestManager . Instance ( ) - > GetQuestById ( questId ) ;
return questWork ! = null ? * questWork : null ;
2024-05-26 19:45:26 +00:00
}
public bool IsAetheryteUnlocked ( uint aetheryteId , out byte subIndex )
{
2024-05-28 20:24:06 +00:00
subIndex = 0 ;
var uiState = UIState . Instance ( ) ;
return uiState ! = null & & uiState - > IsAetheryteUnlocked ( aetheryteId ) ;
/ *
2024-05-26 19:45:26 +00:00
var telepo = Telepo . Instance ( ) ;
if ( telepo = = null | | telepo - > UpdateAetheryteList ( ) = = null )
{
subIndex = 0 ;
return false ;
}
2024-05-27 19:54:34 +00:00
for ( ulong i = 0 ; i < telepo - > TeleportList . Size ( ) ; + + i )
2024-05-26 19:45:26 +00:00
{
var data = telepo - > TeleportList . Get ( i ) ;
if ( data . AetheryteId = = aetheryteId )
{
subIndex = data . SubIndex ;
return true ;
}
}
subIndex = 0 ;
return false ;
2024-05-28 20:24:06 +00:00
* /
2024-05-26 19:45:26 +00:00
}
public bool IsAetheryteUnlocked ( EAetheryteLocation aetheryteLocation )
= > IsAetheryteUnlocked ( ( uint ) aetheryteLocation , out _ ) ;
public bool TeleportAetheryte ( uint aetheryteId )
{
var status = ActionManager . Instance ( ) - > GetActionStatus ( ActionType . Action , 5 ) ;
if ( status ! = 0 )
return false ;
if ( IsAetheryteUnlocked ( aetheryteId , out var subIndex ) )
{
return Telepo . Instance ( ) - > Teleport ( aetheryteId , subIndex ) ;
}
return false ;
2024-05-25 21:51:37 +00:00
}
2024-05-26 19:45:26 +00:00
public bool TeleportAetheryte ( EAetheryteLocation aetheryteLocation )
= > TeleportAetheryte ( ( uint ) aetheryteLocation ) ;
2024-05-25 21:51:37 +00:00
public bool IsFlyingUnlocked ( ushort territoryId )
{
var playerState = PlayerState . Instance ( ) ;
return playerState ! = null & &
_territoryToAetherCurrentCompFlgSet . TryGetValue ( territoryId , out byte aetherCurrentCompFlgSet ) & &
playerState - > IsAetherCurrentZoneComplete ( aetherCurrentCompFlgSet ) ;
}
2024-05-27 19:54:34 +00:00
public bool IsAetherCurrentUnlocked ( uint aetherCurrentId )
{
var playerState = PlayerState . Instance ( ) ;
return playerState ! = null & &
playerState - > IsAetherCurrentUnlocked ( aetherCurrentId ) ;
}
2024-05-25 21:51:37 +00:00
public void ExecuteCommand ( string command )
{
2024-05-26 19:45:26 +00:00
if ( ! command . StartsWith ( '/' ) )
2024-05-25 21:51:37 +00:00
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 )
{
2024-05-26 19:45:26 +00:00
textPtr = Marshal . AllocHGlobal ( stringBytes . Length + 30 ) ;
Marshal . Copy ( stringBytes , 0 , textPtr , stringBytes . Length ) ;
Marshal . WriteByte ( textPtr + stringBytes . Length , 0 ) ;
2024-05-25 21:51:37 +00:00
2024-05-26 19:45:26 +00:00
textLen = ( ulong ) ( stringBytes . Length + 1 ) ;
2024-05-25 21:51:37 +00:00
2024-05-26 19:45:26 +00:00
unk1 = 64 ;
unk2 = 0 ;
2024-05-25 21:51:37 +00:00
}
public void Dispose ( )
{
2024-05-26 19:45:26 +00:00
Marshal . FreeHGlobal ( textPtr ) ;
2024-05-25 21:51:37 +00:00
}
}
#endregion
2024-05-26 19:45:26 +00:00
2024-05-28 20:24:06 +00:00
public GameObject ? FindObjectByDataId ( uint dataId )
2024-05-26 19:45:26 +00:00
{
foreach ( var gameObject in _objectTable )
{
if ( gameObject . DataId = = dataId )
{
2024-05-27 19:54:34 +00:00
return gameObject ;
2024-05-26 19:45:26 +00:00
}
}
2024-05-27 19:54:34 +00:00
2024-05-31 23:26:46 +00:00
_pluginLog . Warning ( $"Could not find GameObject with dataId {dataId}" ) ;
2024-05-27 19:54:34 +00:00
return null ;
}
public void InteractWith ( uint dataId )
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
2024-05-31 23:26:46 +00:00
_pluginLog . Information ( $"Setting target with {dataId} to {gameObject.ObjectId}" ) ;
2024-05-27 19:54:34 +00:00
_targetManager . Target = gameObject ;
TargetSystem . Instance ( ) - > InteractWithObject (
( FFXIVClientStructs . FFXIV . Client . Game . Object . GameObject * ) gameObject . Address , false ) ;
}
}
2024-05-28 20:24:06 +00:00
public void UseItem ( uint itemId )
{
AgentInventoryContext . Instance ( ) - > UseItem ( itemId ) ;
}
2024-05-27 19:54:34 +00:00
public void UseItem ( uint dataId , uint itemId )
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
_targetManager . Target = gameObject ;
AgentInventoryContext . Instance ( ) - > UseItem ( itemId ) ;
}
}
2024-05-28 22:17:19 +00:00
public void UseItemOnGround ( uint dataId , uint itemId )
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
var position = ( FFXIVClientStructs . FFXIV . Common . Math . Vector3 ) gameObject . Position ;
ActionManager . Instance ( ) - > UseActionLocation ( ActionType . KeyItem , itemId , gameObject . ObjectId , & position ) ;
}
}
2024-05-27 19:54:34 +00:00
public void UseEmote ( uint dataId , EEmote emote )
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
_targetManager . Target = gameObject ;
ExecuteCommand ( $"{_emoteCommands[emote]} motion" ) ;
}
}
2024-05-28 22:17:19 +00:00
public void UseEmote ( EEmote emote )
{
ExecuteCommand ( $"{_emoteCommands[emote]} motion" ) ;
}
2024-05-28 20:24:06 +00:00
public bool IsObjectAtPosition ( uint dataId , Vector3 position )
2024-05-27 19:54:34 +00:00
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
return gameObject ! = null & & ( gameObject . Position - position ) . Length ( ) < 0.05f ;
}
public bool HasStatusPreventingSprintOrMount ( )
{
2024-05-31 23:26:46 +00:00
if ( _condition [ ConditionFlag . Swimming ] & & ! IsFlyingUnlocked ( _clientState . TerritoryType ) )
return true ;
2024-05-27 19:54:34 +00:00
var gameObject = GameObjectManager . GetGameObjectByIndex ( 0 ) ;
if ( gameObject ! = null & & gameObject - > ObjectKind = = 1 )
{
var battleChara = ( BattleChara * ) gameObject ;
StatusManager * statusManager = battleChara - > GetStatusManager ;
2024-05-29 19:22:58 +00:00
return statusManager - > HasStatus ( 565 ) | | statusManager - > HasStatus ( 404 ) | | statusManager - > HasStatus ( 2730 ) ;
2024-05-27 19:54:34 +00:00
}
return false ;
}
public bool Unmount ( )
{
if ( _condition [ ConditionFlag . Mounted ] )
{
if ( ActionManager . Instance ( ) - > GetActionStatus ( ActionType . GeneralAction , 23 ) = = 0 )
ActionManager . Instance ( ) - > UseAction ( ActionType . GeneralAction , 23 ) ;
return true ;
}
return false ;
2024-05-26 19:45:26 +00:00
}
2024-05-25 21:51:37 +00:00
}