2024-05-25 23:51:37 +02:00
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Diagnostics.CodeAnalysis ;
using System.Linq ;
2024-05-26 21:45:26 +02:00
using System.Numerics ;
2024-05-25 23:51:37 +02:00
using System.Runtime.InteropServices ;
using System.Text ;
using Dalamud.Game ;
2024-05-27 21:54:34 +02:00
using Dalamud.Game.ClientState.Conditions ;
2024-05-26 21:45:26 +02:00
using Dalamud.Game.ClientState.Objects ;
2024-05-25 23:51:37 +02:00
using Dalamud.Plugin.Services ;
2024-06-06 18:49:49 +02:00
using Dalamud.Utility ;
2024-05-28 22:24:06 +02:00
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions ;
2024-05-25 23:51:37 +02:00
using FFXIVClientStructs.FFXIV.Client.Game ;
2024-05-26 21:45:26 +02:00
using FFXIVClientStructs.FFXIV.Client.Game.Control ;
2024-05-27 21:54:34 +02:00
using FFXIVClientStructs.FFXIV.Client.Game.Object ;
2024-05-25 23:51:37 +02: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 ;
2024-06-09 16:30:53 +02:00
using FFXIVClientStructs.FFXIV.Component.GUI ;
using LLib.GameUI ;
2024-06-03 14:20:02 +02:00
using Lumina.Excel.CustomSheets ;
2024-06-12 18:03:48 +02:00
using Lumina.Excel.GeneratedSheets2 ;
2024-06-08 21:16:57 +02:00
using Microsoft.Extensions.Logging ;
2024-05-28 22:24:06 +02:00
using Questionable.Controller ;
2024-05-26 21:45:26 +02:00
using Questionable.Model.V1 ;
2024-05-27 21:54:34 +02:00
using BattleChara = FFXIVClientStructs . FFXIV . Client . Game . Character . BattleChara ;
2024-06-12 18:03:48 +02:00
using ContentFinderCondition = Lumina . Excel . GeneratedSheets . ContentFinderCondition ;
using ContentTalk = Lumina . Excel . GeneratedSheets . ContentTalk ;
using Emote = Lumina . Excel . GeneratedSheets . Emote ;
2024-05-27 21:54:34 +02:00
using GameObject = Dalamud . Game . ClientState . Objects . Types . GameObject ;
2024-06-15 14:12:32 +02:00
using GrandCompany = FFXIVClientStructs . FFXIV . Client . UI . Agent . GrandCompany ;
2024-06-03 14:20:02 +02:00
using Quest = Questionable . Model . Quest ;
2024-06-12 18:03:48 +02:00
using TerritoryType = Lumina . Excel . GeneratedSheets . TerritoryType ;
2024-05-25 23:51:37 +02: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 21:54:34 +02:00
private readonly ReadOnlyDictionary < EEmote , string > _emoteCommands ;
2024-06-01 14:30:20 +02:00
private readonly ReadOnlyDictionary < uint , ushort > _contentFinderConditionToContentId ;
2024-05-25 23:51:37 +02:00
2024-06-03 14:20:02 +02:00
private readonly IDataManager _dataManager ;
2024-05-26 21:45:26 +02:00
private readonly IObjectTable _objectTable ;
private readonly ITargetManager _targetManager ;
2024-05-27 21:54:34 +02:00
private readonly ICondition _condition ;
2024-06-01 01:26:46 +02:00
private readonly IClientState _clientState ;
2024-06-08 21:16:57 +02:00
private readonly QuestRegistry _questRegistry ;
2024-06-09 16:30:53 +02:00
private readonly IGameGui _gameGui ;
2024-06-12 18:03:48 +02:00
private readonly Configuration _configuration ;
2024-06-08 21:16:57 +02:00
private readonly ILogger < GameFunctions > _logger ;
2024-05-26 21:45:26 +02:00
2024-05-27 21:54:34 +02:00
public GameFunctions ( IDataManager dataManager , IObjectTable objectTable , ISigScanner sigScanner ,
2024-06-08 21:16:57 +02:00
ITargetManager targetManager , ICondition condition , IClientState clientState , QuestRegistry questRegistry ,
2024-06-12 18:03:48 +02:00
IGameGui gameGui , Configuration configuration , ILogger < GameFunctions > logger )
2024-05-25 23:51:37 +02:00
{
2024-06-03 14:20:02 +02:00
_dataManager = dataManager ;
2024-05-26 21:45:26 +02:00
_objectTable = objectTable ;
_targetManager = targetManager ;
2024-05-27 21:54:34 +02:00
_condition = condition ;
2024-06-01 01:26:46 +02:00
_clientState = clientState ;
2024-06-08 21:16:57 +02:00
_questRegistry = questRegistry ;
2024-06-09 16:30:53 +02:00
_gameGui = gameGui ;
2024-06-12 18:03:48 +02:00
_configuration = configuration ;
2024-06-08 21:16:57 +02:00
_logger = logger ;
2024-05-25 23:51:37 +02: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 21:54:34 +02: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-06-01 22:01:50 +02:00
_contentFinderConditionToContentId = dataManager . GetExcelSheet < ContentFinderCondition > ( ) !
2024-06-01 14:30:20 +02:00
. Where ( x = > x . RowId > 0 & & x . Content > 0 )
. ToDictionary ( x = > x . RowId , x = > x . Content )
. AsReadOnly ( ) ;
2024-05-25 23:51:37 +02:00
}
2024-06-14 01:10:05 +02:00
public DateTime ReturnRequestedAt { get ; set ; } = DateTime . MinValue ;
2024-05-26 21:45:26 +02:00
public ( ushort CurrentQuest , byte Sequence ) GetCurrentQuest ( )
2024-06-15 14:12:32 +02:00
{
var ( currentQuest , sequence ) = GetCurrentQuestInternal ( ) ;
QuestManager * questManager = QuestManager . Instance ( ) ;
PlayerState * playerState = PlayerState . Instance ( ) ;
if ( currentQuest = = 0 )
{
if ( _clientState . TerritoryType = = 181 ) // Starting in Limsa
return ( 107 , 0 ) ;
if ( _clientState . TerritoryType = = 183 ) // Starting in Gridania
return ( 39 , 0 ) ;
return default ;
}
else if ( currentQuest = = 681 )
{
// if we have already picked up the GC quest, just return the progress for it
if ( questManager - > IsQuestAccepted ( currentQuest ) | | QuestManager . IsQuestComplete ( currentQuest ) )
return ( currentQuest , sequence ) ;
// The company you keep...
return _configuration . General . GrandCompany switch
{
GrandCompany . TwinAdder = > ( 680 , 0 ) ,
GrandCompany . Maelstrom = > ( 681 , 0 ) ,
_ = > default
} ;
}
else if ( currentQuest = = 3856 & & ! playerState - > IsMountUnlocked ( 1 ) ) // we come in peace
{
ushort chocoboQuest = ( GrandCompany ) playerState - > GrandCompany switch
{
GrandCompany . TwinAdder = > 700 ,
GrandCompany . Maelstrom = > 701 ,
_ = > 0
} ;
if ( chocoboQuest ! = 0 & & ! QuestManager . IsQuestComplete ( chocoboQuest ) )
return ( chocoboQuest , QuestManager . GetQuestSequence ( chocoboQuest ) ) ;
}
return ( currentQuest , sequence ) ;
}
public ( ushort CurrentQuest , byte Sequence ) GetCurrentQuestInternal ( )
2024-05-25 23:51:37 +02:00
{
2024-05-28 22:24:06 +02: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 23:51:37 +02:00
{
2024-05-28 22:24:06 +02:00
foreach ( var tracked in questManager - > TrackedQuestsSpan )
{
switch ( tracked . QuestType )
{
default :
continue ;
case 1 : // normal quest
currentQuest = questManager - > NormalQuestsSpan [ tracked . Index ] . QuestId ;
break ;
}
2024-06-08 21:16:57 +02:00
if ( _questRegistry . IsKnownQuest ( currentQuest ) )
2024-05-28 22:24:06 +02:00
return ( currentQuest , QuestManager . GetQuestSequence ( currentQuest ) ) ;
}
2024-05-25 23:51:37 +02:00
}
2024-05-28 22:24:06 +02:00
var scenarioTree = AgentScenarioTree . Instance ( ) ;
if ( scenarioTree = = null )
return default ;
2024-05-25 23:51:37 +02:00
if ( scenarioTree - > Data = = null )
2024-05-28 22:24:06 +02:00
return default ;
2024-05-25 23:51:37 +02:00
2024-05-28 22:24:06 +02:00
currentQuest = scenarioTree - > Data - > CurrentScenarioQuest ;
2024-05-25 23:51:37 +02:00
if ( currentQuest = = 0 )
2024-05-28 22:24:06 +02: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 21:45:26 +02:00
}
public bool IsAetheryteUnlocked ( uint aetheryteId , out byte subIndex )
{
2024-05-28 22:24:06 +02:00
subIndex = 0 ;
var uiState = UIState . Instance ( ) ;
return uiState ! = null & & uiState - > IsAetheryteUnlocked ( aetheryteId ) ;
2024-05-26 21:45:26 +02:00
}
public bool IsAetheryteUnlocked ( EAetheryteLocation aetheryteLocation )
= > IsAetheryteUnlocked ( ( uint ) aetheryteLocation , out _ ) ;
2024-06-15 14:12:32 +02:00
public bool CanTeleport ( EAetheryteLocation aetheryteLocation )
{
if ( ( ushort ) aetheryteLocation = = PlayerState . Instance ( ) - > HomeAetheryteId & &
ActionManager . Instance ( ) - > GetActionStatus ( ActionType . GeneralAction , 8 ) = = 0 )
return true ;
return ActionManager . Instance ( ) - > GetActionStatus ( ActionType . Action , 5 ) = = 0 ;
}
2024-06-09 16:30:53 +02:00
2024-05-26 21:45:26 +02:00
public bool TeleportAetheryte ( uint aetheryteId )
{
2024-06-15 14:12:32 +02:00
2024-05-26 21:45:26 +02:00
if ( IsAetheryteUnlocked ( aetheryteId , out var subIndex ) )
{
2024-06-14 01:10:05 +02:00
if ( aetheryteId = = PlayerState . Instance ( ) - > HomeAetheryteId & &
ActionManager . Instance ( ) - > GetActionStatus ( ActionType . GeneralAction , 8 ) = = 0 )
{
ReturnRequestedAt = DateTime . Now ;
if ( ActionManager . Instance ( ) - > UseAction ( ActionType . GeneralAction , 8 ) )
return true ;
}
2024-06-15 14:12:32 +02:00
if ( ActionManager . Instance ( ) - > GetActionStatus ( ActionType . Action , 5 ) = = 0 )
// fallback if return isn't available or (more likely) on a different aetheryte
return Telepo . Instance ( ) - > Teleport ( aetheryteId , subIndex ) ;
2024-05-26 21:45:26 +02:00
}
return false ;
2024-05-25 23:51:37 +02:00
}
2024-05-26 21:45:26 +02:00
public bool TeleportAetheryte ( EAetheryteLocation aetheryteLocation )
= > TeleportAetheryte ( ( uint ) aetheryteLocation ) ;
2024-05-25 23:51:37 +02:00
public bool IsFlyingUnlocked ( ushort territoryId )
{
2024-06-14 01:10:05 +02:00
if ( _configuration . Advanced . NeverFly )
return false ;
2024-05-25 23:51:37 +02:00
var playerState = PlayerState . Instance ( ) ;
return playerState ! = null & &
_territoryToAetherCurrentCompFlgSet . TryGetValue ( territoryId , out byte aetherCurrentCompFlgSet ) & &
playerState - > IsAetherCurrentZoneComplete ( aetherCurrentCompFlgSet ) ;
}
2024-06-08 21:16:57 +02:00
public bool IsFlyingUnlockedInCurrentZone ( ) = > IsFlyingUnlocked ( _clientState . TerritoryType ) ;
2024-05-27 21:54:34 +02:00
public bool IsAetherCurrentUnlocked ( uint aetherCurrentId )
{
var playerState = PlayerState . Instance ( ) ;
return playerState ! = null & &
playerState - > IsAetherCurrentUnlocked ( aetherCurrentId ) ;
}
2024-05-25 23:51:37 +02:00
public void ExecuteCommand ( string command )
{
2024-05-26 21:45:26 +02:00
if ( ! command . StartsWith ( '/' ) )
2024-05-25 23:51:37 +02: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 21:45:26 +02:00
textPtr = Marshal . AllocHGlobal ( stringBytes . Length + 30 ) ;
Marshal . Copy ( stringBytes , 0 , textPtr , stringBytes . Length ) ;
Marshal . WriteByte ( textPtr + stringBytes . Length , 0 ) ;
2024-05-25 23:51:37 +02:00
2024-05-26 21:45:26 +02:00
textLen = ( ulong ) ( stringBytes . Length + 1 ) ;
2024-05-25 23:51:37 +02:00
2024-05-26 21:45:26 +02:00
unk1 = 64 ;
unk2 = 0 ;
2024-05-25 23:51:37 +02:00
}
public void Dispose ( )
{
2024-05-26 21:45:26 +02:00
Marshal . FreeHGlobal ( textPtr ) ;
2024-05-25 23:51:37 +02:00
}
}
#endregion
2024-05-26 21:45:26 +02:00
2024-05-28 22:24:06 +02:00
public GameObject ? FindObjectByDataId ( uint dataId )
2024-05-26 21:45:26 +02:00
{
foreach ( var gameObject in _objectTable )
{
if ( gameObject . DataId = = dataId )
{
2024-05-27 21:54:34 +02:00
return gameObject ;
2024-05-26 21:45:26 +02:00
}
}
2024-05-27 21:54:34 +02:00
2024-06-08 21:16:57 +02:00
_logger . LogWarning ( "Could not find GameObject with dataId {DataId}" , dataId ) ;
2024-05-27 21:54:34 +02:00
return null ;
}
2024-06-09 16:30:53 +02:00
public bool InteractWith ( uint dataId )
2024-05-27 21:54:34 +02:00
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
2024-06-08 21:16:57 +02:00
_logger . LogInformation ( "Setting target with {DataId} to {ObjectId}" , dataId , gameObject . ObjectId ) ;
2024-06-14 01:10:05 +02:00
_targetManager . Target = null ;
2024-05-27 21:54:34 +02:00
_targetManager . Target = gameObject ;
2024-06-14 01:10:05 +02:00
long result = ( long ) TargetSystem . Instance ( ) - > InteractWithObject (
2024-05-27 21:54:34 +02:00
( FFXIVClientStructs . FFXIV . Client . Game . Object . GameObject * ) gameObject . Address , false ) ;
2024-06-14 01:10:05 +02:00
_logger . LogInformation ( "Interact result: {Result}" , result ) ;
return result > 0 ;
2024-05-27 21:54:34 +02:00
}
2024-06-09 16:30:53 +02:00
2024-06-14 01:10:05 +02:00
_logger . LogDebug ( "Game object is null" ) ;
2024-06-09 16:30:53 +02:00
return false ;
2024-05-27 21:54:34 +02:00
}
2024-06-12 18:03:48 +02:00
public bool UseItem ( uint itemId )
2024-05-28 22:24:06 +02:00
{
2024-06-14 01:10:05 +02:00
long result = AgentInventoryContext . Instance ( ) - > UseItem ( itemId ) ;
_logger . LogInformation ( "UseItem result: {Result}" , result ) ;
return result = = 0 ;
2024-05-28 22:24:06 +02:00
}
2024-06-12 18:03:48 +02:00
public bool UseItem ( uint dataId , uint itemId )
2024-05-27 21:54:34 +02:00
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
_targetManager . Target = gameObject ;
2024-06-14 01:10:05 +02:00
long result = AgentInventoryContext . Instance ( ) - > UseItem ( itemId ) ;
_logger . LogInformation ( "UseItem result on {DataId}: {Result}" , dataId , result ) ;
return result = = 0 ;
2024-05-27 21:54:34 +02:00
}
2024-06-12 18:03:48 +02:00
return false ;
2024-05-27 21:54:34 +02:00
}
2024-06-12 18:03:48 +02:00
public bool UseItemOnGround ( uint dataId , uint itemId )
2024-05-29 00:17:19 +02:00
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
var position = ( FFXIVClientStructs . FFXIV . Common . Math . Vector3 ) gameObject . Position ;
2024-06-12 18:03:48 +02:00
return ActionManager . Instance ( ) - > UseActionLocation ( ActionType . KeyItem , itemId , location : & position ) ;
2024-05-29 00:17:19 +02:00
}
2024-06-12 18:03:48 +02:00
return false ;
2024-05-29 00:17:19 +02:00
}
2024-05-27 21:54:34 +02:00
public void UseEmote ( uint dataId , EEmote emote )
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
if ( gameObject ! = null )
{
_targetManager . Target = gameObject ;
ExecuteCommand ( $"{_emoteCommands[emote]} motion" ) ;
}
}
2024-05-29 00:17:19 +02:00
public void UseEmote ( EEmote emote )
{
ExecuteCommand ( $"{_emoteCommands[emote]} motion" ) ;
}
2024-05-28 22:24:06 +02:00
public bool IsObjectAtPosition ( uint dataId , Vector3 position )
2024-05-27 21:54:34 +02:00
{
GameObject ? gameObject = FindObjectByDataId ( dataId ) ;
return gameObject ! = null & & ( gameObject . Position - position ) . Length ( ) < 0.05f ;
}
2024-06-14 01:10:05 +02:00
public bool HasStatusPreventingMount ( )
2024-05-27 21:54:34 +02:00
{
2024-06-08 21:16:57 +02:00
if ( _condition [ ConditionFlag . Swimming ] & & ! IsFlyingUnlockedInCurrentZone ( ) )
2024-06-01 01:26:46 +02:00
return true ;
2024-06-03 14:20:02 +02:00
// company chocobo is locked
var playerState = PlayerState . Instance ( ) ;
if ( playerState ! = null & & ! playerState - > IsMountUnlocked ( 1 ) )
return true ;
2024-06-14 01:10:05 +02:00
return HasCharacterStatusPreventingMountOrSprint ( ) ;
}
public bool HasStatusPreventingSprint ( ) = > HasCharacterStatusPreventingMountOrSprint ( ) ;
private bool HasCharacterStatusPreventingMountOrSprint ( )
{
2024-05-27 21:54:34 +02:00
var gameObject = GameObjectManager . GetGameObjectByIndex ( 0 ) ;
if ( gameObject ! = null & & gameObject - > ObjectKind = = 1 )
{
var battleChara = ( BattleChara * ) gameObject ;
StatusManager * statusManager = battleChara - > GetStatusManager ;
2024-05-29 21:22:58 +02:00
return statusManager - > HasStatus ( 565 ) | | statusManager - > HasStatus ( 404 ) | | statusManager - > HasStatus ( 2730 ) ;
2024-05-27 21:54:34 +02:00
}
return false ;
}
2024-06-09 16:30:53 +02:00
public bool Mount ( )
2024-06-03 14:20:02 +02:00
{
2024-06-09 16:30:53 +02:00
if ( _condition [ ConditionFlag . Mounted ] )
return true ;
var playerState = PlayerState . Instance ( ) ;
2024-06-12 18:03:48 +02:00
if ( playerState ! = null & & _configuration . General . MountId ! = 0 & &
playerState - > IsMountUnlocked ( _configuration . General . MountId ) )
2024-06-03 14:20:02 +02:00
{
2024-06-12 18:03:48 +02:00
if ( ActionManager . Instance ( ) - > GetActionStatus ( ActionType . Mount , _configuration . General . MountId ) = = 0 )
2024-06-03 14:20:02 +02:00
{
2024-06-12 18:03:48 +02:00
if ( ActionManager . Instance ( ) - > UseAction ( ActionType . Mount , _configuration . General . MountId ) )
2024-06-09 22:44:35 +02:00
{
2024-06-12 18:03:48 +02:00
_logger . LogInformation ( "Using preferred mount" ) ;
2024-06-09 22:44:35 +02:00
return true ;
}
return false ;
2024-06-03 14:20:02 +02:00
}
2024-06-09 16:30:53 +02:00
}
else
{
if ( ActionManager . Instance ( ) - > GetActionStatus ( ActionType . GeneralAction , 9 ) = = 0 )
2024-06-03 14:20:02 +02:00
{
2024-06-09 22:44:35 +02:00
if ( ActionManager . Instance ( ) - > UseAction ( ActionType . GeneralAction , 9 ) )
{
_logger . LogInformation ( "Using mount roulette" ) ;
return true ;
}
return false ;
2024-06-03 14:20:02 +02:00
}
}
2024-06-09 16:30:53 +02:00
return false ;
2024-06-03 14:20:02 +02:00
}
2024-05-27 21:54:34 +02:00
public bool Unmount ( )
{
2024-06-09 16:30:53 +02:00
if ( ! _condition [ ConditionFlag . Mounted ] )
2024-06-10 19:56:13 +02:00
return true ;
2024-05-27 21:54:34 +02:00
2024-06-09 16:30:53 +02:00
if ( ActionManager . Instance ( ) - > GetActionStatus ( ActionType . GeneralAction , 23 ) = = 0 )
{
_logger . LogInformation ( "Unmounting..." ) ;
2024-06-10 19:56:13 +02:00
return ActionManager . Instance ( ) - > UseAction ( ActionType . GeneralAction , 23 ) ;
2024-05-27 21:54:34 +02:00
}
2024-06-09 16:30:53 +02:00
else
2024-06-10 19:56:13 +02:00
{
2024-06-09 16:30:53 +02:00
_logger . LogWarning ( "Can't unmount right now?" ) ;
2024-06-10 19:56:13 +02:00
return false ;
}
2024-05-26 21:45:26 +02:00
}
2024-06-01 14:30:20 +02:00
public void OpenDutyFinder ( uint contentFinderConditionId )
{
if ( _contentFinderConditionToContentId . TryGetValue ( contentFinderConditionId , out ushort contentId ) )
{
if ( UIState . IsInstanceContentUnlocked ( contentId ) )
AgentContentsFinder . Instance ( ) - > OpenRegularDuty ( contentFinderConditionId ) ;
else
2024-06-08 21:16:57 +02:00
_logger . LogError (
"Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})" ,
contentFinderConditionId , contentId ) ;
2024-06-01 14:30:20 +02:00
}
else
2024-06-09 22:44:35 +02:00
_logger . LogError ( "Could not find content for content finder condition (cf: {ContentFinderId})" ,
contentFinderConditionId ) ;
2024-06-01 14:30:20 +02:00
}
2024-06-03 14:20:02 +02:00
2024-06-06 18:49:49 +02:00
public string? GetDialogueText ( Quest currentQuest , string? excelSheetName , string key )
2024-06-03 14:20:02 +02:00
{
if ( excelSheetName = = null )
{
2024-06-09 22:44:35 +02:00
var questRow =
_dataManager . GetExcelSheet < Lumina . Excel . GeneratedSheets2 . Quest > ( ) ! . GetRow ( ( uint ) currentQuest . QuestId +
0x10000 ) ;
2024-06-03 23:17:35 +02:00
if ( questRow = = null )
2024-06-03 14:20:02 +02:00
{
2024-06-08 21:16:57 +02:00
_logger . LogError ( "Could not find quest row for {QuestId}" , currentQuest . QuestId ) ;
2024-06-03 14:20:02 +02:00
return null ;
}
2024-06-03 23:17:35 +02:00
excelSheetName = $"quest/{(currentQuest.QuestId / 100):000}/{questRow.Id}" ;
2024-06-03 14:20:02 +02:00
}
var excelSheet = _dataManager . Excel . GetSheet < QuestDialogueText > ( excelSheetName ) ;
if ( excelSheet = = null )
{
2024-06-08 21:16:57 +02:00
_logger . LogError ( "Unknown excel sheet '{SheetName}'" , excelSheetName ) ;
2024-06-03 14:20:02 +02:00
return null ;
}
2024-06-06 18:49:49 +02:00
return excelSheet . FirstOrDefault ( x = > x . Key = = key ) ? . Value ? . ToDalamudString ( ) . ToString ( ) ;
}
2024-06-12 18:03:48 +02:00
public string? GetDialogueTextByRowId ( string? excelSheet , uint rowId )
2024-06-06 18:49:49 +02:00
{
2024-06-12 18:03:48 +02:00
if ( excelSheet = = "GimmickYesNo" )
{
var questRow = _dataManager . GetExcelSheet < GimmickYesNo > ( ) ! . GetRow ( rowId ) ;
return questRow ? . Unknown0 ? . ToString ( ) ;
}
2024-06-14 01:10:05 +02:00
else if ( excelSheet = = "Warp" )
{
var questRow = _dataManager . GetExcelSheet < Warp > ( ) ! . GetRow ( rowId ) ;
return questRow ? . Name ? . ToString ( ) ;
}
2024-06-12 18:03:48 +02:00
else if ( excelSheet is "ContentTalk" or null )
{
var questRow = _dataManager . GetExcelSheet < ContentTalk > ( ) ! . GetRow ( rowId ) ;
return questRow ? . Text ? . ToString ( ) ;
}
else
throw new ArgumentOutOfRangeException ( nameof ( excelSheet ) , $"Unsupported excel sheet {excelSheet}" ) ;
2024-06-03 14:20:02 +02:00
}
2024-06-09 16:30:53 +02:00
public bool IsOccupied ( )
{
2024-06-11 00:06:35 +02:00
if ( IsLoadingScreenVisible ( ) )
2024-06-09 16:30:53 +02:00
return true ;
return _condition [ ConditionFlag . Occupied ] | | _condition [ ConditionFlag . Occupied30 ] | |
2024-06-09 22:44:35 +02:00
_condition [ ConditionFlag . Occupied33 ] | | _condition [ ConditionFlag . Occupied38 ] | |
_condition [ ConditionFlag . Occupied39 ] | | _condition [ ConditionFlag . OccupiedInEvent ] | |
_condition [ ConditionFlag . OccupiedInQuestEvent ] | | _condition [ ConditionFlag . OccupiedInCutSceneEvent ] | |
_condition [ ConditionFlag . Casting ] | | _condition [ ConditionFlag . Unknown57 ] | |
_condition [ ConditionFlag . BetweenAreas ] | | _condition [ ConditionFlag . BetweenAreas51 ] ;
2024-06-09 16:30:53 +02:00
}
2024-06-11 00:06:35 +02:00
public bool IsLoadingScreenVisible ( )
{
return _gameGui . TryGetAddonByName ( "FadeMiddle" , out AtkUnitBase * fade ) & &
LAddon . IsAddonReady ( fade ) & &
fade - > IsVisible ;
}
2024-05-25 23:51:37 +02:00
}