punish v6.8.18.0
This commit is contained in:
commit
e786325cda
322 changed files with 554232 additions and 0 deletions
153
Questionable/Questionable.Functions/AetheryteFunctions.cs
Normal file
153
Questionable/Questionable.Functions/AetheryteFunctions.cs
Normal file
|
@ -0,0 +1,153 @@
|
|||
using System;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model.Common;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed class AetheryteFunctions
|
||||
{
|
||||
private const uint TeleportAction = 5u;
|
||||
|
||||
private const uint ReturnAction = 6u;
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private readonly ILogger<AetheryteFunctions> _logger;
|
||||
|
||||
private readonly IDataManager _dataManager;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
|
||||
|
||||
public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> logger, IDataManager dataManager, IClientState clientState)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
_dataManager = dataManager;
|
||||
_clientState = clientState;
|
||||
}
|
||||
|
||||
public unsafe bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
|
||||
{
|
||||
subIndex = 0;
|
||||
UIState* ptr = UIState.Instance();
|
||||
if (ptr != null)
|
||||
{
|
||||
return ptr->IsAetheryteUnlocked(aetheryteId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if (aetheryteLocation.IsFirmamentAetheryte())
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<QuestFunctions>().IsQuestComplete(new QuestId(3672));
|
||||
}
|
||||
byte subIndex;
|
||||
return IsAetheryteUnlocked((uint)aetheryteLocation, out subIndex);
|
||||
}
|
||||
|
||||
public unsafe bool CanTeleport(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId && ActionManager.Instance()->GetActionStatus(ActionType.Action, 6u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0;
|
||||
}
|
||||
|
||||
public unsafe bool IsTeleportUnlocked()
|
||||
{
|
||||
uint rowId = _dataManager.GetExcelSheet<Lumina.Excel.Sheets.Action>().GetRow(5u).UnlockLink.RowId;
|
||||
return UIState.Instance()->IsUnlockLinkUnlocked(rowId);
|
||||
}
|
||||
|
||||
public unsafe bool TeleportAetheryte(uint aetheryteId)
|
||||
{
|
||||
_logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
|
||||
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
|
||||
{
|
||||
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId && ActionManager.Instance()->GetActionStatus(ActionType.Action, 6u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
ReturnRequestedAt = DateTime.Now;
|
||||
if (ActionManager.Instance()->UseAction(ActionType.Action, 6u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null))
|
||||
{
|
||||
_logger.LogInformation("Using 'return' for home aetheryte");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
_logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
|
||||
return Telepo.Instance()->Teleport(aetheryteId, subIndex);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
return TeleportAetheryte((uint)aetheryteLocation);
|
||||
}
|
||||
|
||||
public unsafe bool IsFreeAetheryte(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr != null)
|
||||
{
|
||||
if ((EAetheryteLocation)ptr->FreeAetheryteId != aetheryteLocation)
|
||||
{
|
||||
return (EAetheryteLocation)ptr->FreeAetherytePlayStationPlus == aetheryteLocation;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe AetheryteRegistrationResult CanRegisterFreeOrFavoriteAetheryte(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
if (_clientState.LocalPlayer == null)
|
||||
{
|
||||
return AetheryteRegistrationResult.NotPossible;
|
||||
}
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
return AetheryteRegistrationResult.NotPossible;
|
||||
}
|
||||
if (IsFreeAetheryte(aetheryteLocation))
|
||||
{
|
||||
return AetheryteRegistrationResult.NotPossible;
|
||||
}
|
||||
bool flag = false;
|
||||
for (int i = 0; i < ptr->FavouriteAetheryteCount; i++)
|
||||
{
|
||||
if (ptr->FavouriteAetherytes[i] == (ushort)aetheryteLocation)
|
||||
{
|
||||
return AetheryteRegistrationResult.NotPossible;
|
||||
}
|
||||
if (ptr->FavouriteAetherytes[i] == 0)
|
||||
{
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ptr->IsPlayerStateFlagSet(PlayerStateFlag.IsLoginSecurityToken) && ptr->FreeAetheryteId == 0)
|
||||
{
|
||||
return AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable;
|
||||
}
|
||||
if (!flag)
|
||||
{
|
||||
return AetheryteRegistrationResult.NotPossible;
|
||||
}
|
||||
return AetheryteRegistrationResult.FavoredDestinationAvailable;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace Questionable.Functions;
|
||||
|
||||
public enum AetheryteRegistrationResult
|
||||
{
|
||||
NotPossible,
|
||||
SecurityTokenFreeDestinationAvailable,
|
||||
FavoredDestinationAvailable
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed class AlliedSocietyQuestFunctions
|
||||
{
|
||||
private sealed class NpcData
|
||||
{
|
||||
public required uint IssuerDataId { get; init; }
|
||||
|
||||
public required List<QuestInfo> AllQuests { get; init; } = new List<QuestInfo>();
|
||||
}
|
||||
|
||||
private record struct Rng(uint S0, uint S1 = 0u, uint S2 = 0u, uint S3 = 0u)
|
||||
{
|
||||
public int Next(int range)
|
||||
{
|
||||
uint s = S3;
|
||||
uint s2 = Transform(S0, S1);
|
||||
uint s3 = S1;
|
||||
uint s4 = S2;
|
||||
S0 = s;
|
||||
S1 = s2;
|
||||
S2 = s3;
|
||||
S3 = s4;
|
||||
return (int)(S1 % range);
|
||||
}
|
||||
|
||||
private static uint Transform(uint s0, uint s1)
|
||||
{
|
||||
uint num = s0 ^ (s0 << 11);
|
||||
return s1 ^ num ^ ((num ^ (s1 >> 11)) >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ILogger<AlliedSocietyQuestFunctions> _logger;
|
||||
|
||||
private readonly Dictionary<EAlliedSociety, List<NpcData>> _questsByAlliedSociety = new Dictionary<EAlliedSociety, List<NpcData>>();
|
||||
|
||||
private readonly Dictionary<(uint NpcDataId, byte Seed, bool OutranksAll, bool RankedUp), List<QuestId>> _dailyQuests = new Dictionary<(uint, byte, bool, bool), List<QuestId>>();
|
||||
|
||||
public AlliedSocietyQuestFunctions(QuestData questData, ILogger<AlliedSocietyQuestFunctions> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
foreach (EAlliedSociety item in from x in Enum.GetValues<EAlliedSociety>()
|
||||
where x != EAlliedSociety.None
|
||||
select x)
|
||||
{
|
||||
foreach (KeyValuePair<uint, List<QuestInfo>> item2 in (from x in questData.GetAllByAlliedSociety(item)
|
||||
where x.IsRepeatable
|
||||
group x by x.IssuerDataId).ToDictionary((IGrouping<uint, QuestInfo> x) => x.Key, (IGrouping<uint, QuestInfo> x) => (from y in x
|
||||
orderby y.AlliedSocietyQuestGroup == 3, y.QuestId
|
||||
select y).ToList()))
|
||||
{
|
||||
item2.Deconstruct(out var key, out var value);
|
||||
uint issuerDataId = key;
|
||||
List<QuestInfo> allQuests = value;
|
||||
NpcData npcData = new NpcData
|
||||
{
|
||||
IssuerDataId = issuerDataId,
|
||||
AllQuests = allQuests
|
||||
};
|
||||
if (_questsByAlliedSociety.TryGetValue(item, out List<NpcData> value2))
|
||||
{
|
||||
value2.Add(npcData);
|
||||
continue;
|
||||
}
|
||||
Dictionary<EAlliedSociety, List<NpcData>> questsByAlliedSociety = _questsByAlliedSociety;
|
||||
int num = 1;
|
||||
List<NpcData> list = new List<NpcData>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<NpcData> span = CollectionsMarshal.AsSpan(list);
|
||||
int index = 0;
|
||||
span[index] = npcData;
|
||||
questsByAlliedSociety[item] = list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe List<QuestId> GetAvailableAlliedSocietyQuests(EAlliedSociety alliedSociety)
|
||||
{
|
||||
byte rank = QuestManager.Instance()->BeastReputation[(int)(alliedSociety - 1)].Rank;
|
||||
byte currentRank = (byte)(rank & 0x7F);
|
||||
if (currentRank == 0)
|
||||
{
|
||||
return new List<QuestId>();
|
||||
}
|
||||
bool flag = (rank & 0x80) != 0;
|
||||
byte dailyQuestSeed = QuestManager.Instance()->DailyQuestSeed;
|
||||
List<QuestId> list = new List<QuestId>();
|
||||
foreach (NpcData item in _questsByAlliedSociety[alliedSociety])
|
||||
{
|
||||
bool flag2 = item.AllQuests.All((QuestInfo x) => currentRank > x.AlliedSocietyRank);
|
||||
(uint, byte, bool, bool) key = (item.IssuerDataId, dailyQuestSeed, flag2, flag);
|
||||
if (_dailyQuests.TryGetValue(key, out List<QuestId> value))
|
||||
{
|
||||
list.AddRange(value);
|
||||
continue;
|
||||
}
|
||||
List<QuestId> list2 = CalculateAvailableQuests(item.AllQuests, dailyQuestSeed, flag2, currentRank, flag);
|
||||
_logger.LogInformation("Available for {Tribe} (Seed: {Seed}, Issuer: {IssuerId}): {Quests}", alliedSociety, dailyQuestSeed, item.IssuerDataId, string.Join(", ", list2));
|
||||
_dailyQuests[key] = list2;
|
||||
list.AddRange(list2);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<QuestId> CalculateAvailableQuests(List<QuestInfo> allQuests, byte seed, bool outranksAll, byte currentRank, bool rankedUp)
|
||||
{
|
||||
List<QuestInfo> list = allQuests.Where((QuestInfo q) => IsEligible(q, currentRank, rankedUp)).ToList();
|
||||
List<QuestInfo> list2 = new List<QuestInfo>();
|
||||
if (list.Count == 0)
|
||||
{
|
||||
return new List<QuestId>();
|
||||
}
|
||||
Rng rng = new Rng(seed);
|
||||
if (outranksAll)
|
||||
{
|
||||
int num = 0;
|
||||
for (int num2 = Math.Min(list.Count, 3); num < num2; num++)
|
||||
{
|
||||
int num3 = rng.Next(list.Count);
|
||||
while (list2.Contains(list[num3]))
|
||||
{
|
||||
num3 = (num3 + 1) % list.Count;
|
||||
}
|
||||
list2.Add(list[num3]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int num4 = list.FindIndex((QuestInfo q) => q.AlliedSocietyQuestGroup == 3);
|
||||
if (num4 >= 0)
|
||||
{
|
||||
list2.Add(list[num4 + rng.Next(list.Count - num4)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
num4 = list.Count;
|
||||
}
|
||||
int num5 = list2.Count;
|
||||
for (int num6 = Math.Min(num4, 3); num5 < num6; num5++)
|
||||
{
|
||||
int num7 = rng.Next(num4);
|
||||
while (list2.Contains(list[num7]))
|
||||
{
|
||||
num7 = (num7 + 1) % num4;
|
||||
}
|
||||
list2.Add(list[num7]);
|
||||
}
|
||||
}
|
||||
return list2.Select((QuestInfo x) => (QuestId)x.QuestId).ToList();
|
||||
}
|
||||
|
||||
private static bool IsEligible(QuestInfo questInfo, byte currentRank, bool rankedUp)
|
||||
{
|
||||
if (!rankedUp)
|
||||
{
|
||||
return questInfo.AlliedSocietyRank <= currentRank;
|
||||
}
|
||||
return questInfo.AlliedSocietyRank == currentRank;
|
||||
}
|
||||
}
|
145
Questionable/Questionable.Functions/ChatFunctions.cs
Normal file
145
Questionable/Questionable.Functions/ChatFunctions.cs
Normal file
|
@ -0,0 +1,145 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
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.Sheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed class ChatFunctions
|
||||
{
|
||||
private delegate void ProcessChatBoxDelegate(nint uiModule, nint message, nint unused, byte a4);
|
||||
|
||||
private static class Signatures
|
||||
{
|
||||
internal const string SendChat = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F2 48 8B F9 45 84 C9";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private readonly struct ChatPayload : IDisposable
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
private readonly nint 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 = 64uL;
|
||||
unk2 = 0uL;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Marshal.FreeHGlobal(textPtr);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ReadOnlyDictionary<EEmote, string> _emoteCommands;
|
||||
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
|
||||
private readonly ITargetManager _targetManager;
|
||||
|
||||
private readonly ILogger<ChatFunctions> _logger;
|
||||
|
||||
private readonly ProcessChatBoxDelegate _processChatBox;
|
||||
|
||||
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("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F2 48 8B F9 45 84 C9"));
|
||||
_emoteCommands = (from x in dataManager.GetExcelSheet<Emote>()
|
||||
where x.RowId != 0
|
||||
where x.TextCommand.IsValid
|
||||
select (RowId: x.RowId, Command: x.TextCommand.Value.Command.ToString()) into x
|
||||
where !string.IsNullOrEmpty(x.Command) && x.Command.StartsWith('/')
|
||||
select x).ToDictionary<(uint, string), EEmote, string>(((uint RowId, string Command) x) => (EEmote)x.RowId, ((uint RowId, string Command) x) => x.Command).AsReadOnly();
|
||||
}
|
||||
|
||||
private unsafe void SendMessageUnsafe(byte[] message)
|
||||
{
|
||||
nint uIModule = (nint)Framework.Instance()->GetUIModule();
|
||||
using ChatPayload structure = new ChatPayload(message);
|
||||
nint num = Marshal.AllocHGlobal(400);
|
||||
Marshal.StructureToPtr(structure, num, fDeleteOld: false);
|
||||
_processChatBox(uIModule, num, IntPtr.Zero, 0);
|
||||
Marshal.FreeHGlobal(num);
|
||||
}
|
||||
|
||||
private void SendMessage(string message)
|
||||
{
|
||||
_logger.LogDebug("Attempting to send chat message '{Message}'", message);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(message);
|
||||
if (bytes.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("message is empty", "message");
|
||||
}
|
||||
if (bytes.Length > 500)
|
||||
{
|
||||
throw new ArgumentException("message is longer than 500 bytes", "message");
|
||||
}
|
||||
if (message.Length != SanitiseText(message).Length)
|
||||
{
|
||||
throw new ArgumentException("message contained invalid characters", "message");
|
||||
}
|
||||
SendMessageUnsafe(bytes);
|
||||
}
|
||||
|
||||
private unsafe string SanitiseText(string text)
|
||||
{
|
||||
Utf8String* intPtr = Utf8String.FromString(text);
|
||||
intPtr->SanitizeString(AllowedEntities.UppercaseLetters | AllowedEntities.LowercaseLetters | AllowedEntities.Numbers | AllowedEntities.SpecialCharacters | AllowedEntities.CharacterList | AllowedEntities.OtherCharacters | AllowedEntities.Payloads | AllowedEntities.Unknown9, null);
|
||||
string result = intPtr->ToString();
|
||||
intPtr->Dtor();
|
||||
IMemorySpace.Free(intPtr);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ExecuteCommand(string command)
|
||||
{
|
||||
if (command.StartsWith('/'))
|
||||
{
|
||||
SendMessage(command);
|
||||
}
|
||||
}
|
||||
|
||||
public void UseEmote(uint dataId, EEmote emote)
|
||||
{
|
||||
IGameObject gameObject = _gameFunctions.FindObjectByDataId(dataId);
|
||||
if (gameObject != null)
|
||||
{
|
||||
_targetManager.Target = gameObject;
|
||||
ExecuteCommand(_emoteCommands[emote] + " motion");
|
||||
}
|
||||
}
|
||||
|
||||
public void UseEmote(EEmote emote)
|
||||
{
|
||||
ExecuteCommand(_emoteCommands[emote] + " motion");
|
||||
}
|
||||
}
|
103
Questionable/Questionable.Functions/ExcelFunctions.cs
Normal file
103
Questionable/Questionable.Functions/ExcelFunctions.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using LLib;
|
||||
using Lumina.Excel.Exceptions;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed class ExcelFunctions
|
||||
{
|
||||
private readonly IDataManager _dataManager;
|
||||
|
||||
private readonly ILogger<ExcelFunctions> _logger;
|
||||
|
||||
public ExcelFunctions(IDataManager dataManager, ILogger<ExcelFunctions> logger)
|
||||
{
|
||||
_dataManager = dataManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public StringOrRegex GetDialogueText(Questionable.Model.Quest? currentQuest, string? excelSheetName, string key, bool isRegex)
|
||||
{
|
||||
ReadOnlySeString? rawDialogueText = GetRawDialogueText(currentQuest, excelSheetName, key);
|
||||
if (isRegex)
|
||||
{
|
||||
return new StringOrRegex(rawDialogueText.ToRegex());
|
||||
}
|
||||
return new StringOrRegex(rawDialogueText?.WithCertainMacroCodeReplacements());
|
||||
}
|
||||
|
||||
public ReadOnlySeString? GetRawDialogueText(Questionable.Model.Quest? currentQuest, string? excelSheetName, string key)
|
||||
{
|
||||
if (currentQuest != null && excelSheetName == null)
|
||||
{
|
||||
Lumina.Excel.Sheets.Quest? rowOrDefault = _dataManager.GetExcelSheet<Lumina.Excel.Sheets.Quest>().GetRowOrDefault((uint)(currentQuest.Id.Value + 65536));
|
||||
if (!rowOrDefault.HasValue)
|
||||
{
|
||||
_logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id);
|
||||
return null;
|
||||
}
|
||||
excelSheetName = $"quest/{currentQuest.Id.Value / 100:000}/{rowOrDefault.Value.Id}";
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(excelSheetName, "excelSheetName");
|
||||
try
|
||||
{
|
||||
IDataManager dataManager = _dataManager;
|
||||
string name = excelSheetName;
|
||||
return dataManager.GetExcelSheet<QuestDialogueText>(null, name).Cast<QuestDialogueText?>().FirstOrDefault((QuestDialogueText? x) => x.Value.Key == key)?.Value;
|
||||
}
|
||||
catch (SheetNotFoundException innerException)
|
||||
{
|
||||
throw new SheetNotFoundException("Sheet '" + excelSheetName + "' not found", innerException);
|
||||
}
|
||||
}
|
||||
|
||||
public StringOrRegex GetDialogueTextByRowId(string? excelSheet, uint rowId, bool isRegex)
|
||||
{
|
||||
ReadOnlySeString? rawDialogueTextByRowId = GetRawDialogueTextByRowId(excelSheet, rowId);
|
||||
if (isRegex)
|
||||
{
|
||||
return new StringOrRegex(rawDialogueTextByRowId.ToRegex());
|
||||
}
|
||||
return new StringOrRegex(rawDialogueTextByRowId?.ToDalamudString().ToString());
|
||||
}
|
||||
|
||||
public ReadOnlySeString? GetRawDialogueTextByRowId(string? excelSheet, uint rowId)
|
||||
{
|
||||
bool flag;
|
||||
switch (excelSheet)
|
||||
{
|
||||
case "GimmickYesNo":
|
||||
return _dataManager.GetExcelSheet<GimmickYesNo>().GetRowOrDefault(rowId)?.Unknown0;
|
||||
case "Warp":
|
||||
return _dataManager.GetExcelSheet<Warp>().GetRowOrDefault(rowId)?.Name;
|
||||
case "Addon":
|
||||
return _dataManager.GetExcelSheet<Addon>().GetRowOrDefault(rowId)?.Text;
|
||||
case "EventPathMove":
|
||||
return _dataManager.GetExcelSheet<EventPathMove>().GetRowOrDefault(rowId)?.Unknown0;
|
||||
case "GilShop":
|
||||
return _dataManager.GetExcelSheet<GilShop>().GetRowOrDefault(rowId)?.Name;
|
||||
default:
|
||||
if (excelSheet != null)
|
||||
{
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
goto case "ContentTalk";
|
||||
case "ContentTalk":
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
if (flag)
|
||||
{
|
||||
return _dataManager.GetExcelSheet<ContentTalk>().GetRowOrDefault(rowId)?.Text;
|
||||
}
|
||||
throw new ArgumentOutOfRangeException("excelSheet", "Unsupported excel sheet " + excelSheet);
|
||||
}
|
||||
}
|
543
Questionable/Questionable.Functions/GameFunctions.cs
Normal file
543
Questionable/Questionable.Functions/GameFunctions.cs
Normal file
|
@ -0,0 +1,543 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LLib.GameUI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed class GameFunctions
|
||||
{
|
||||
private delegate void AbandonDutyDelegate(bool a1);
|
||||
|
||||
private static class Signatures
|
||||
{
|
||||
internal const string AbandonDuty = "E8 ?? ?? ?? ?? 41 B2 01 EB 39";
|
||||
}
|
||||
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
|
||||
private readonly IDataManager _dataManager;
|
||||
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
private readonly ITargetManager _targetManager;
|
||||
|
||||
private readonly ICondition _condition;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly IGameGui _gameGui;
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
|
||||
private readonly ILogger<GameFunctions> _logger;
|
||||
|
||||
private readonly AbandonDutyDelegate _abandonDuty;
|
||||
|
||||
private readonly ReadOnlyDictionary<ushort, uint> _territoryToAetherCurrentCompFlgSet;
|
||||
|
||||
private readonly ReadOnlyDictionary<uint, uint> _contentFinderConditionToContentId;
|
||||
|
||||
public GameFunctions(QuestFunctions questFunctions, IDataManager dataManager, IObjectTable objectTable, ITargetManager targetManager, ICondition condition, IClientState clientState, IGameGui gameGui, Configuration configuration, ISigScanner sigScanner, ILogger<GameFunctions> logger)
|
||||
{
|
||||
_questFunctions = questFunctions;
|
||||
_dataManager = dataManager;
|
||||
_objectTable = objectTable;
|
||||
_targetManager = targetManager;
|
||||
_condition = condition;
|
||||
_clientState = clientState;
|
||||
_gameGui = gameGui;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_abandonDuty = Marshal.GetDelegateForFunctionPointer<AbandonDutyDelegate>(sigScanner.ScanText("E8 ?? ?? ?? ?? 41 B2 01 EB 39"));
|
||||
_territoryToAetherCurrentCompFlgSet = (from x in dataManager.GetExcelSheet<TerritoryType>()
|
||||
where x.RowId != 0
|
||||
where x.AetherCurrentCompFlgSet.RowId != 0
|
||||
select x).ToDictionary((TerritoryType x) => (ushort)x.RowId, (TerritoryType x) => x.AetherCurrentCompFlgSet.RowId).AsReadOnly();
|
||||
_contentFinderConditionToContentId = (from x in dataManager.GetExcelSheet<ContentFinderCondition>()
|
||||
where x.RowId != 0 && x.Content.RowId != 0
|
||||
select x).ToDictionary((ContentFinderCondition x) => x.RowId, (ContentFinderCondition x) => x.Content.RowId).AsReadOnly();
|
||||
}
|
||||
|
||||
public unsafe bool IsFlyingUnlocked(ushort territoryId)
|
||||
{
|
||||
if (_configuration.Advanced.NeverFly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_questFunctions.IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted] && GetMountId() == 198)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr != null && _territoryToAetherCurrentCompFlgSet.TryGetValue(territoryId, out var value))
|
||||
{
|
||||
return ptr->IsAetherCurrentZoneComplete(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe ushort? GetMountId()
|
||||
{
|
||||
BattleChara* ptr = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
|
||||
if (ptr != null && ptr->Mount.MountId != 0)
|
||||
{
|
||||
return ptr->Mount.MountId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool IsFlyingUnlockedInCurrentZone()
|
||||
{
|
||||
return IsFlyingUnlocked(_clientState.TerritoryType);
|
||||
}
|
||||
|
||||
public unsafe bool IsAetherCurrentUnlocked(uint aetherCurrentId)
|
||||
{
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr != null)
|
||||
{
|
||||
return ptr->IsAetherCurrentUnlocked(aetherCurrentId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public IGameObject? FindObjectByDataId(uint dataId, Dalamud.Game.ClientState.Objects.Enums.ObjectKind? kind = null)
|
||||
{
|
||||
foreach (IGameObject item in _objectTable)
|
||||
{
|
||||
Dalamud.Game.ClientState.Objects.Enums.ObjectKind objectKind = item.ObjectKind;
|
||||
bool flag = ((objectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player || objectKind - 8 <= Dalamud.Game.ClientState.Objects.Enums.ObjectKind.BattleNpc || objectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Housing) ? true : false);
|
||||
if (!flag && (item == null || item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint || item.IsTargetable) && item.DataId == dataId && (!kind.HasValue || kind.Value == item.ObjectKind))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
_logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool InteractWith(uint dataId, Dalamud.Game.ClientState.Objects.Enums.ObjectKind? kind = null)
|
||||
{
|
||||
IGameObject gameObject = FindObjectByDataId(dataId, kind);
|
||||
if (gameObject != null)
|
||||
{
|
||||
return InteractWith(gameObject);
|
||||
}
|
||||
_logger.LogDebug("Game object is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool InteractWith(IGameObject gameObject)
|
||||
{
|
||||
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId);
|
||||
_targetManager.Target = null;
|
||||
_targetManager.Target = gameObject;
|
||||
if (gameObject.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint)
|
||||
{
|
||||
TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address);
|
||||
_logger.LogInformation("Interact result: (none) for GatheringPoint");
|
||||
return true;
|
||||
}
|
||||
long num = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, checkLineOfSight: false);
|
||||
_logger.LogInformation("Interact result: {Result}", num);
|
||||
if (num != 7)
|
||||
{
|
||||
return num > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool UseItem(uint itemId)
|
||||
{
|
||||
long num = AgentInventoryContext.Instance()->UseItem(itemId, InventoryType.Invalid, 0u, 0);
|
||||
_logger.LogInformation("UseItem result: {Result}", num);
|
||||
return num == 0;
|
||||
}
|
||||
|
||||
public unsafe bool UseItem(uint dataId, uint itemId)
|
||||
{
|
||||
IGameObject gameObject = FindObjectByDataId(dataId);
|
||||
if (gameObject != null)
|
||||
{
|
||||
_targetManager.Target = gameObject;
|
||||
long num = AgentInventoryContext.Instance()->UseItem(itemId, InventoryType.Invalid, 0u, 0);
|
||||
_logger.LogInformation("UseItem result on {DataId}: {Result}", dataId, num);
|
||||
if ((ulong)num <= 1uL)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool UseItemOnGround(uint dataId, uint itemId)
|
||||
{
|
||||
IGameObject gameObject = FindObjectByDataId(dataId);
|
||||
if (gameObject != null)
|
||||
{
|
||||
Vector3 position = gameObject.Position;
|
||||
return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, 3758096384uL, &position, 0u, 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool UseItemOnPosition(Vector3 position, uint itemId)
|
||||
{
|
||||
return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, 3758096384uL, &position, 0u, 0);
|
||||
}
|
||||
|
||||
public unsafe bool UseAction(EAction action)
|
||||
{
|
||||
uint num = (uint)(action & (EAction)65535);
|
||||
ActionType actionType = (((action & (EAction)65536) != (EAction)65536) ? ActionType.Action : ActionType.GeneralAction);
|
||||
if (actionType == ActionType.Action)
|
||||
{
|
||||
num = ActionManager.Instance()->GetAdjustedActionId(num);
|
||||
}
|
||||
if (ActionManager.Instance()->GetActionStatus(actionType, num, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
bool flag = ActionManager.Instance()->UseAction(actionType, num, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
||||
_logger.LogInformation("UseAction {Action} (adjusted: {AdjustedActionId}) result: {Result}", action, num, flag);
|
||||
return flag;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool UseAction(IGameObject gameObject, EAction action, bool checkCanUse = true)
|
||||
{
|
||||
uint actionId = (uint)(action & (EAction)65535);
|
||||
ActionType actionType = (((action & (EAction)65536) != (EAction)65536) ? ActionType.Action : ActionType.GeneralAction);
|
||||
if (actionType == ActionType.GeneralAction)
|
||||
{
|
||||
_logger.LogWarning("Can not use general action {Action} on target {Target}", action, gameObject);
|
||||
return false;
|
||||
}
|
||||
actionId = ActionManager.Instance()->GetAdjustedActionId(actionId);
|
||||
if (checkCanUse && !ActionManager.CanUseActionOnTarget(actionId, (GameObject*)gameObject.Address))
|
||||
{
|
||||
_logger.LogWarning("Can not use action {Action} (adjusted: {AdjustedActionId}) on target {Target}", action, actionId, gameObject);
|
||||
return false;
|
||||
}
|
||||
Lumina.Excel.Sheets.Action row = _dataManager.GetExcelSheet<Lumina.Excel.Sheets.Action>().GetRow(actionId);
|
||||
_targetManager.Target = gameObject;
|
||||
if (ActionManager.Instance()->GetActionStatus(actionType, actionId, gameObject.GameObjectId, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
bool flag;
|
||||
if (row.TargetArea)
|
||||
{
|
||||
Vector3 position = gameObject.Position;
|
||||
flag = ActionManager.Instance()->UseActionLocation(actionType, actionId, 3758096384uL, &position, 0u, 0);
|
||||
_logger.LogInformation("UseAction {Action} (adjusted: {AdjustedActionId}) on target area {Target} result: {Result}", action, actionId, gameObject, flag);
|
||||
}
|
||||
else
|
||||
{
|
||||
flag = ActionManager.Instance()->UseAction(actionType, actionId, gameObject.GameObjectId, 0u, ActionManager.UseActionMode.None, 0u, null);
|
||||
_logger.LogInformation("UseAction {Action} (adjusted: {AdjustedActionId}) on target {Target} result: {Result}", action, actionId, gameObject, flag);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsObjectAtPosition(uint dataId, Vector3 position, float distance)
|
||||
{
|
||||
IGameObject gameObject = FindObjectByDataId(dataId);
|
||||
if (gameObject != null)
|
||||
{
|
||||
return (gameObject.Position - position).Length() < distance;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool HasStatusPreventingMount()
|
||||
{
|
||||
if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr != null && !ptr->IsMountUnlocked(1u))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BattleChara* address = (BattleChara*)localPlayer.Address;
|
||||
StatusManager* statusManager = address->GetStatusManager();
|
||||
if (statusManager->HasStatus(1151u) || statusManager->HasStatus(1945u))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return HasCharacterStatusPreventingMountOrSprint();
|
||||
}
|
||||
|
||||
public bool HasStatusPreventingSprint()
|
||||
{
|
||||
return HasCharacterStatusPreventingMountOrSprint();
|
||||
}
|
||||
|
||||
private unsafe bool HasCharacterStatusPreventingMountOrSprint()
|
||||
{
|
||||
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BattleChara* address = (BattleChara*)localPlayer.Address;
|
||||
StatusManager* statusManager = address->GetStatusManager();
|
||||
if (!statusManager->HasStatus(565u) && !statusManager->HasStatus(404u) && !statusManager->HasStatus(416u) && !statusManager->HasStatus(2729u))
|
||||
{
|
||||
return statusManager->HasStatus(2730u);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool HasStatus(EStatus statusId)
|
||||
{
|
||||
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BattleChara* address = (BattleChara*)localPlayer.Address;
|
||||
return address->GetStatusManager()->HasStatus((uint)statusId);
|
||||
}
|
||||
|
||||
public static bool RemoveStatus(EStatus statusId)
|
||||
{
|
||||
return StatusManager.ExecuteStatusOff((uint)statusId);
|
||||
}
|
||||
|
||||
public unsafe bool Mount()
|
||||
{
|
||||
if (_condition[ConditionFlag.Mounted])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr != null && _configuration.General.MountId != 0 && ptr->IsMountUnlocked(_configuration.General.MountId))
|
||||
{
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
_logger.LogDebug("Attempting to use preferred mount...");
|
||||
if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null))
|
||||
{
|
||||
_logger.LogInformation("Using preferred mount");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
_logger.LogDebug("Attempting to use mount roulette...");
|
||||
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null))
|
||||
{
|
||||
_logger.LogInformation("Using mount roulette");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool Unmount()
|
||||
{
|
||||
if (!_condition[ConditionFlag.Mounted])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
_logger.LogDebug("Attempting to unmount...");
|
||||
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null))
|
||||
{
|
||||
_logger.LogInformation("Unmounted");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_logger.LogWarning("Can't unmount right now?");
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe void OpenDutyFinder(uint contentFinderConditionId)
|
||||
{
|
||||
if (_contentFinderConditionToContentId.TryGetValue(contentFinderConditionId, out var value))
|
||||
{
|
||||
if (UIState.IsInstanceContentUnlocked(value))
|
||||
{
|
||||
AgentContentsFinder.Instance()->OpenRegularDuty(contentFinderConditionId);
|
||||
return;
|
||||
}
|
||||
_logger.LogError("Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})", contentFinderConditionId, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Could not find content for content finder condition (cf: {ContentFinderId})", contentFinderConditionId);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool GameStringEquals(string? a, string? b)
|
||||
{
|
||||
if (a == null)
|
||||
{
|
||||
return b == null;
|
||||
}
|
||||
if (b == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return a.ReplaceLineEndings().Replace('–', '-') == b.ReplaceLineEndings().Replace('–', '-');
|
||||
}
|
||||
|
||||
public unsafe bool IsOccupied()
|
||||
{
|
||||
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (IsLoadingScreenVisible())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (_condition[ConditionFlag.Crafting])
|
||||
{
|
||||
if (!AgentRecipeNote.Instance()->IsAgentActive())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!_condition[ConditionFlag.PreparingToCraft])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (_condition[ConditionFlag.Unconscious] && _condition[ConditionFlag.SufferingStatusAffliction63] && _clientState.TerritoryType == 1052)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!_condition[ConditionFlag.Occupied] && !_condition[ConditionFlag.Occupied30] && !_condition[ConditionFlag.Occupied33] && !_condition[ConditionFlag.Occupied38] && !_condition[ConditionFlag.Occupied39] && !_condition[ConditionFlag.OccupiedInEvent] && !_condition[ConditionFlag.OccupiedInQuestEvent] && !_condition[ConditionFlag.OccupiedInCutSceneEvent] && !_condition[ConditionFlag.Casting] && !_condition[ConditionFlag.MountOrOrnamentTransition] && !_condition[ConditionFlag.BetweenAreas] && !_condition[ConditionFlag.BetweenAreas51] && !_condition[ConditionFlag.Jumping61] && !_condition[ConditionFlag.ExecutingGatheringAction])
|
||||
{
|
||||
return _condition[ConditionFlag.Jumping];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool IsOccupiedWithCustomDeliveryNpc(Questionable.Model.Quest? currentQuest)
|
||||
{
|
||||
if (currentQuest == null || !(currentQuest.Info is SatisfactionSupplyInfo))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_targetManager.Target == null || _targetManager.Target.DataId != currentQuest.Info.IssuerDataId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!AgentSatisfactionSupply.Instance()->IsAgentActive())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
HashSet<ConditionFlag> hashSet = _condition.AsReadOnlySet().ToHashSet();
|
||||
hashSet.Remove(ConditionFlag.InDutyQueue);
|
||||
if (hashSet.Count == 2 && hashSet.Contains(ConditionFlag.NormalConditions))
|
||||
{
|
||||
return hashSet.Contains(ConditionFlag.OccupiedInQuestEvent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool IsLoadingScreenVisible()
|
||||
{
|
||||
if (_gameGui.TryGetAddonByName<AtkUnitBase>("FadeMiddle", out var addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (_gameGui.TryGetAddonByName<AtkUnitBase>("FadeBack", out addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (_gameGui.TryGetAddonByName<AtkUnitBase>("NowLoading", out addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe int GetFreeInventorySlots()
|
||||
{
|
||||
InventoryManager* ptr = InventoryManager.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int num = 0;
|
||||
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
|
||||
{
|
||||
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
|
||||
if (inventoryContainer == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < inventoryContainer->Size; i++)
|
||||
{
|
||||
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
|
||||
if (inventorySlot == null || inventorySlot->ItemId == 0)
|
||||
{
|
||||
num++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
public void AbandonDuty()
|
||||
{
|
||||
_abandonDuty(a1: false);
|
||||
}
|
||||
|
||||
public unsafe IReadOnlyList<uint> GetUnlockLinks()
|
||||
{
|
||||
UIState* ptr = UIState.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
_logger.LogError("Could not query unlock links");
|
||||
return Array.Empty<uint>();
|
||||
}
|
||||
List<uint> list = new List<uint>();
|
||||
for (uint num = 0u; num < ptr->UnlockLinkBitmask.Length * 8; num++)
|
||||
{
|
||||
if (ptr->IsUnlockLinkUnlocked(num))
|
||||
{
|
||||
list.Add(num);
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("Unlocked unlock links: {UnlockedUnlockLinks}", string.Join(", ", list));
|
||||
return list;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace Questionable.Functions;
|
||||
|
||||
public enum MainScenarioQuestState
|
||||
{
|
||||
Unavailable,
|
||||
Available,
|
||||
Complete,
|
||||
LoadingScreen
|
||||
}
|
8
Questionable/Questionable.Functions/PriorityQuestInfo.cs
Normal file
8
Questionable/Questionable.Functions/PriorityQuestInfo.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed record PriorityQuestInfo(ElementId QuestId, string? UnavailableReason = null)
|
||||
{
|
||||
public bool IsAvailable => UnavailableReason == null;
|
||||
}
|
959
Questionable/Questionable.Functions/QuestFunctions.cs
Normal file
959
Questionable/Questionable.Functions/QuestFunctions.cs
Normal file
|
@ -0,0 +1,959 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Game.ClientState.Aetherytes;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LLib.GameData;
|
||||
using LLib.GameUI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Questionable.Controller;
|
||||
using Questionable.Data;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Common;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
internal sealed class QuestFunctions
|
||||
{
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
|
||||
private readonly QuestData _questData;
|
||||
|
||||
private readonly AetheryteFunctions _aetheryteFunctions;
|
||||
|
||||
private readonly AlliedSocietyQuestFunctions _alliedSocietyQuestFunctions;
|
||||
|
||||
private readonly AlliedSocietyData _alliedSocietyData;
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
|
||||
private readonly IDataManager _dataManager;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly IGameGui _gameGui;
|
||||
|
||||
private readonly IAetheryteList _aetheryteList;
|
||||
|
||||
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui, IAetheryteList aetheryteList)
|
||||
{
|
||||
_questRegistry = questRegistry;
|
||||
_questData = questData;
|
||||
_aetheryteFunctions = aetheryteFunctions;
|
||||
_alliedSocietyQuestFunctions = alliedSocietyQuestFunctions;
|
||||
_alliedSocietyData = alliedSocietyData;
|
||||
_configuration = configuration;
|
||||
_dataManager = dataManager;
|
||||
_clientState = clientState;
|
||||
_gameGui = gameGui;
|
||||
_aetheryteList = aetheryteList;
|
||||
}
|
||||
|
||||
public unsafe QuestReference GetCurrentQuest(bool allowNewMsq = true)
|
||||
{
|
||||
QuestReference currentQuestInternal = GetCurrentQuestInternal(allowNewMsq);
|
||||
currentQuestInternal.Deconstruct(out ElementId CurrentQuest, out byte Sequence, out MainScenarioQuestState State);
|
||||
ElementId elementId = CurrentQuest;
|
||||
byte sequence = Sequence;
|
||||
MainScenarioQuestState state = State;
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (elementId == null || elementId.Value == 0)
|
||||
{
|
||||
if (_clientState.TerritoryType == 181)
|
||||
{
|
||||
return new QuestReference(new QuestId(107), 0, MainScenarioQuestState.Available);
|
||||
}
|
||||
if (_clientState.TerritoryType == 182)
|
||||
{
|
||||
return new QuestReference(new QuestId(594), 0, MainScenarioQuestState.Available);
|
||||
}
|
||||
if (_clientState.TerritoryType == 183)
|
||||
{
|
||||
return new QuestReference(new QuestId(39), 0, MainScenarioQuestState.Available);
|
||||
}
|
||||
return QuestReference.NoQuest(state);
|
||||
}
|
||||
if (elementId.Value == 681)
|
||||
{
|
||||
if (IsQuestAccepted(elementId) || IsQuestComplete(elementId))
|
||||
{
|
||||
return new QuestReference(elementId, sequence, state);
|
||||
}
|
||||
return _configuration.General.GrandCompany switch
|
||||
{
|
||||
FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.TwinAdder => new QuestReference(new QuestId(680), 0, state),
|
||||
FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.Maelstrom => new QuestReference(new QuestId(681), 0, state),
|
||||
FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.ImmortalFlames => new QuestReference(new QuestId(682), 0, state),
|
||||
_ => QuestReference.NoQuest(MainScenarioQuestState.Unavailable),
|
||||
};
|
||||
}
|
||||
if (elementId.Value == 3856 && !ptr->IsMountUnlocked(1u))
|
||||
{
|
||||
ushort num = (FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany)ptr->GrandCompany switch
|
||||
{
|
||||
FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.TwinAdder => 700,
|
||||
FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.Maelstrom => 701,
|
||||
FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.ImmortalFlames => 702,
|
||||
_ => 0,
|
||||
};
|
||||
if (num != 0 && !QuestManager.IsQuestComplete(num))
|
||||
{
|
||||
return new QuestReference(new QuestId(num), QuestManager.GetQuestSequence(num), state);
|
||||
}
|
||||
}
|
||||
else if (elementId.Value == 801)
|
||||
{
|
||||
QuestId questId = new QuestId(802);
|
||||
if (IsQuestAccepted(questId))
|
||||
{
|
||||
return new QuestReference(questId, QuestManager.GetQuestSequence(questId.Value), state);
|
||||
}
|
||||
}
|
||||
return currentQuestInternal;
|
||||
}
|
||||
|
||||
public unsafe QuestReference GetCurrentQuestInternal(bool allowNewMsq)
|
||||
{
|
||||
QuestManager* ptr = QuestManager.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
return QuestReference.NoQuest(MainScenarioQuestState.Unavailable);
|
||||
}
|
||||
QuestReference questReference = GetMainScenarioQuest().Item1;
|
||||
if (questReference.CurrentQuest != null && !_questRegistry.IsKnownQuest(questReference.CurrentQuest))
|
||||
{
|
||||
questReference = QuestReference.NoQuest(questReference.State);
|
||||
}
|
||||
if (questReference.CurrentQuest != null && !IsQuestAccepted(questReference.CurrentQuest))
|
||||
{
|
||||
if (allowNewMsq)
|
||||
{
|
||||
return questReference;
|
||||
}
|
||||
questReference = QuestReference.NoQuest(questReference.State);
|
||||
}
|
||||
List<(ElementId, byte)> list = new List<(ElementId, byte)>();
|
||||
for (int num = ptr->TrackedQuests.Length - 1; num >= 0; num--)
|
||||
{
|
||||
TrackingWork trackingWork = ptr->TrackedQuests[num];
|
||||
switch (trackingWork.QuestType)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
ElementId elementId = new QuestId(ptr->NormalQuests[trackingWork.Index].QuestId);
|
||||
if (_questRegistry.IsKnownQuest(elementId))
|
||||
{
|
||||
list.Add((elementId, QuestManager.GetQuestSequence(elementId.Value)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_configuration.General.SkipLowPriorityDuties && list.Count > 0)
|
||||
{
|
||||
IReadOnlyList<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> lowPriorityQuests = _questRegistry.LowPriorityContentFinderConditionQuests;
|
||||
list.RemoveAll(((ElementId Quest, byte Sequence) x) => lowPriorityQuests.Any<(uint, ElementId, int)>(((uint ContentFinderConditionId, ElementId QuestId, int Sequence) y) => x.Quest == y.QuestId && x.Sequence == y.Sequence));
|
||||
}
|
||||
if (list.Count > 0)
|
||||
{
|
||||
(ElementId, byte) tuple = list.First();
|
||||
ElementId item = tuple.Item1;
|
||||
byte item2 = tuple.Item2;
|
||||
EAlliedSociety firstTrackedAlliedSociety = _alliedSocietyData.GetCommonAlliedSocietyTurnIn(item);
|
||||
if (firstTrackedAlliedSociety != EAlliedSociety.None)
|
||||
{
|
||||
List<(ElementId, byte)> list2 = (from quest in list.Skip(1)
|
||||
where _alliedSocietyData.GetCommonAlliedSocietyTurnIn(quest.Quest) == firstTrackedAlliedSociety
|
||||
select quest).ToList();
|
||||
if (list2.Count > 0)
|
||||
{
|
||||
if (item2 == byte.MaxValue)
|
||||
{
|
||||
foreach (var (currentQuest, b) in list2)
|
||||
{
|
||||
if (b != byte.MaxValue)
|
||||
{
|
||||
return new QuestReference(currentQuest, b, questReference.State);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!IsOnAlliedSocietyMount())
|
||||
{
|
||||
list2.Insert(0, (item, item2));
|
||||
_alliedSocietyData.GetCommonAlliedSocietyNpcs(firstTrackedAlliedSociety, out uint[] normalNpcs, out uint[] _);
|
||||
if (normalNpcs.Length != 0)
|
||||
{
|
||||
(ElementId, byte)? tuple3 = (from x in list2
|
||||
where x.Sequence < byte.MaxValue
|
||||
where IsInteractSequence(x.Quest, x.Sequence, normalNpcs)
|
||||
select x).Cast<(ElementId, byte)?>().FirstOrDefault();
|
||||
if (tuple3.HasValue)
|
||||
{
|
||||
return new QuestReference(tuple3.Value.Item1, tuple3.Value.Item2, questReference.State);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new QuestReference(item, item2, questReference.State);
|
||||
}
|
||||
ElementId elementId2 = (from x in GetNextPriorityQuestsThatCanBeAccepted()
|
||||
where x.IsAvailable
|
||||
select x.QuestId).FirstOrDefault();
|
||||
if (elementId2 != null)
|
||||
{
|
||||
return new QuestReference(elementId2, QuestManager.GetQuestSequence(elementId2.Value), questReference.State);
|
||||
}
|
||||
if (questReference.CurrentQuest != null)
|
||||
{
|
||||
return questReference;
|
||||
}
|
||||
return QuestReference.NoQuest(questReference.State);
|
||||
}
|
||||
|
||||
public unsafe (QuestReference, string?) GetMainScenarioQuest()
|
||||
{
|
||||
if (QuestManager.IsQuestComplete(3759))
|
||||
{
|
||||
AgentInterface* agentByInternalId = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud);
|
||||
if (agentByInternalId != null && agentByInternalId->IsAgentActive())
|
||||
{
|
||||
AtkUnitBase* addonPtr;
|
||||
bool flag = _gameGui.TryGetAddonByName<AtkUnitBase>("QuestRedoHud", out addonPtr) && addonPtr->AtkValuesCount == 4;
|
||||
if (flag)
|
||||
{
|
||||
uint uInt = addonPtr->AtkValues->UInt;
|
||||
bool flag2 = ((uInt == 0 || uInt - 2 <= 2) ? true : false);
|
||||
flag = flag2;
|
||||
}
|
||||
if (flag)
|
||||
{
|
||||
ushort num = MemoryHelper.Read<ushort>((nint)((byte*)agentByInternalId + 46));
|
||||
return (new QuestReference(new QuestId(num), QuestManager.GetQuestSequence(num), MainScenarioQuestState.Available), "NG+");
|
||||
}
|
||||
}
|
||||
}
|
||||
AgentScenarioTree* ptr = AgentScenarioTree.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Unavailable), "No Scenario Tree");
|
||||
}
|
||||
if (ptr->Data == null)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.LoadingScreen), "Scenario Tree Data is null");
|
||||
}
|
||||
QuestId questId = new QuestId(ptr->Data->CurrentScenarioQuest);
|
||||
string item = $"sq: {questId}";
|
||||
if (questId.Value == 0)
|
||||
{
|
||||
if (IsMainScenarioQuestComplete())
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Complete), "Main Scenario is complete");
|
||||
}
|
||||
PlayerState* playerState = PlayerState.Instance();
|
||||
List<QuestInfo> list = (from q in _questData.MainScenarioQuests
|
||||
where q.StartingCity == 0 || q.StartingCity == playerState->StartTown
|
||||
where IsReadyToAcceptQuest(q.QuestId, ignoreLevel: true)
|
||||
select q).ToList();
|
||||
if (list.Count == 0)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Unavailable), "No potential quests found");
|
||||
}
|
||||
if (list.Count > 1)
|
||||
{
|
||||
if (list.All(delegate(QuestInfo x)
|
||||
{
|
||||
ushort value = x.QuestId.Value;
|
||||
return (uint)(value - 680) <= 2u;
|
||||
}))
|
||||
{
|
||||
questId = new QuestId(681);
|
||||
}
|
||||
else if (list.Any((QuestInfo x) => x.QuestId.Value == 1583))
|
||||
{
|
||||
questId = new QuestId(1583);
|
||||
}
|
||||
else if (list.Any((QuestInfo x) => x.QuestId.Value == 2451))
|
||||
{
|
||||
questId = new QuestId(2451);
|
||||
}
|
||||
else if (list.Any((QuestInfo x) => x.QuestId.Value == 3282))
|
||||
{
|
||||
questId = new QuestId(3282);
|
||||
}
|
||||
else if (list.Any((QuestInfo x) => x.QuestId.Value == 4359))
|
||||
{
|
||||
questId = new QuestId(4359);
|
||||
}
|
||||
else if (list.Any((QuestInfo x) => x.QuestId.Value == 4865))
|
||||
{
|
||||
questId = new QuestId(4865);
|
||||
}
|
||||
if (list.Count != 1)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Unavailable), "Multiple potential quests found: " + string.Join(", ", list.Select((QuestInfo x) => x.QuestId.Value)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
questId = (QuestId)list.Single().QuestId;
|
||||
}
|
||||
}
|
||||
QuestManager* ptr2 = QuestManager.Instance();
|
||||
if (IsQuestAccepted(questId) && ptr2->GetQuestById(questId.Value)->IsHidden)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Available), "Quest accepted but hidden");
|
||||
}
|
||||
if (IsQuestComplete(questId))
|
||||
{
|
||||
return (new QuestReference(questId, byte.MaxValue, MainScenarioQuestState.Available), $"Quest {questId.Value} complete");
|
||||
}
|
||||
if (!IsReadyToAcceptQuest(questId))
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Unavailable), $"Not readdy to accept quest {questId.Value}");
|
||||
}
|
||||
byte? b = _clientState.LocalPlayer?.Level;
|
||||
if (!b.HasValue)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.LoadingScreen), "In loading screen");
|
||||
}
|
||||
if (_questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest) && quest.Info.Level > b)
|
||||
{
|
||||
return (QuestReference.NoQuest(MainScenarioQuestState.Unavailable), "Low level");
|
||||
}
|
||||
return (new QuestReference(questId, QuestManager.GetQuestSequence(questId.Value), MainScenarioQuestState.Available), item);
|
||||
}
|
||||
|
||||
private unsafe bool IsOnAlliedSocietyMount()
|
||||
{
|
||||
BattleChara* ptr = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
|
||||
if (ptr != null && ptr->Mount.MountId != 0)
|
||||
{
|
||||
return _alliedSocietyData.Mounts.ContainsKey(ptr->Mount.MountId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsInteractSequence(ElementId questId, byte sequenceNo, uint[] dataIds)
|
||||
{
|
||||
if (_questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest))
|
||||
{
|
||||
return quest.FindSequence(sequenceNo)?.Steps.All(delegate(QuestStep x)
|
||||
{
|
||||
if (x == null || x.InteractionType != EInteractionType.WalkTo)
|
||||
{
|
||||
if (x != null && x.InteractionType == EInteractionType.Interact)
|
||||
{
|
||||
uint? dataId = x.DataId;
|
||||
if (dataId.HasValue)
|
||||
{
|
||||
uint valueOrDefault = dataId.GetValueOrDefault();
|
||||
return dataIds.Contains(valueOrDefault);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) ?? false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe QuestProgressInfo? GetQuestProgressInfo(ElementId elementId)
|
||||
{
|
||||
if (elementId is QuestId questId)
|
||||
{
|
||||
QuestWork* questById = QuestManager.Instance()->GetQuestById(questId.Value);
|
||||
if (questById == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new QuestProgressInfo(*questById);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public unsafe List<PriorityQuestInfo> GetNextPriorityQuestsThatCanBeAccepted()
|
||||
{
|
||||
InventoryManager* ptr = InventoryManager.Instance();
|
||||
int gil = ptr->GetItemCountInContainer(1u, InventoryType.Currency, isHq: false, 0);
|
||||
return (from x in GetPriorityQuests()
|
||||
where IsReadyToAcceptQuest(x)
|
||||
select x).Select(delegate(ElementId x)
|
||||
{
|
||||
if (!_questRegistry.TryGetQuest(x, out Questionable.Model.Quest quest))
|
||||
{
|
||||
return new PriorityQuestInfo(x, "Unknown quest");
|
||||
}
|
||||
QuestStep questStep = quest.FindSequence(0)?.FindStep(0);
|
||||
if (questStep == null)
|
||||
{
|
||||
return new PriorityQuestInfo(x, "No sequence 0 with steps");
|
||||
}
|
||||
if (!_aetheryteFunctions.IsTeleportUnlocked())
|
||||
{
|
||||
return new PriorityQuestInfo(x, "Teleport not unlocked");
|
||||
}
|
||||
if (!questStep.IsTeleportableForPriorityQuests())
|
||||
{
|
||||
return new PriorityQuestInfo(x, "Can't teleport to start");
|
||||
}
|
||||
DefaultInterpolatedStringHandler handler;
|
||||
if (gil < EstimateTeleportCosts(quest))
|
||||
{
|
||||
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
|
||||
handler = new DefaultInterpolatedStringHandler(32, 2, invariantCulture);
|
||||
handler.AppendLiteral("Not enough gil, estimated cost: ");
|
||||
handler.AppendFormatted(EstimateTeleportCosts(quest), "N0");
|
||||
handler.AppendFormatted(SeIconChar.Gil.ToIconString());
|
||||
return new PriorityQuestInfo(x, string.Create(invariantCulture, ref handler));
|
||||
}
|
||||
EAetheryteLocation? value = quest.AllSteps().Select<(QuestSequence, int, QuestStep), EAetheryteLocation?>(delegate((QuestSequence Sequence, int StepId, QuestStep Step) y)
|
||||
{
|
||||
EAetheryteLocation? aetheryteShortcut = y.Step.AetheryteShortcut;
|
||||
if (aetheryteShortcut.HasValue)
|
||||
{
|
||||
EAetheryteLocation valueOrDefault = aetheryteShortcut.GetValueOrDefault();
|
||||
if (!_aetheryteFunctions.IsAetheryteUnlocked(valueOrDefault))
|
||||
{
|
||||
SkipConditions? skipConditions = y.Step.SkipConditions;
|
||||
if (skipConditions == null || skipConditions.AetheryteShortcutIf?.AetheryteLocked != valueOrDefault)
|
||||
{
|
||||
return valueOrDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
AethernetShortcut aethernetShortcut = y.Step.AethernetShortcut;
|
||||
if (aethernetShortcut != null)
|
||||
{
|
||||
if (!_aetheryteFunctions.IsAetheryteUnlocked(aethernetShortcut.From))
|
||||
{
|
||||
return aethernetShortcut.From;
|
||||
}
|
||||
if (!_aetheryteFunctions.IsAetheryteUnlocked(aethernetShortcut.To))
|
||||
{
|
||||
return aethernetShortcut.To;
|
||||
}
|
||||
}
|
||||
return (EAetheryteLocation?)null;
|
||||
}).FirstOrDefault((EAetheryteLocation? y) => y.HasValue);
|
||||
if (value.HasValue)
|
||||
{
|
||||
handler = new DefaultInterpolatedStringHandler(18, 1);
|
||||
handler.AppendLiteral("Aetheryte locked: ");
|
||||
handler.AppendFormatted(value);
|
||||
return new PriorityQuestInfo(x, handler.ToStringAndClear());
|
||||
}
|
||||
return new PriorityQuestInfo(x);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private int EstimateTeleportCosts(Questionable.Model.Quest quest)
|
||||
{
|
||||
Dictionary<EAetheryteLocation, float> cheapTeleports = _aetheryteList.Where((IAetheryteEntry x) => x.IsFavourite || _aetheryteFunctions.IsFreeAetheryte((EAetheryteLocation)x.AetheryteId)).ToDictionary((IAetheryteEntry x) => (EAetheryteLocation)x.AetheryteId, (IAetheryteEntry x) => _aetheryteFunctions.IsFreeAetheryte((EAetheryteLocation)x.AetheryteId) ? 0f : 0.5f);
|
||||
int baseCost = ((quest.Info.Expansion == EExpansionVersion.ARealmReborn) ? 300 : 1000);
|
||||
return (from x in quest.AllSteps()
|
||||
where x.Step.AetheryteShortcut.HasValue
|
||||
select x).Sum<(QuestSequence, int, QuestStep)>(((QuestSequence Sequence, int StepId, QuestStep Step) x) => (int)((float)baseCost * cheapTeleports.GetValueOrDefault(x.Step.AetheryteShortcut.Value, 1f)));
|
||||
}
|
||||
|
||||
public List<ElementId> GetPriorityQuests(bool onlyClassAndRoleQuests = false)
|
||||
{
|
||||
List<ElementId> list = new List<ElementId>();
|
||||
if (!onlyClassAndRoleQuests)
|
||||
{
|
||||
if (!_configuration.Advanced.SkipARealmRebornHardModePrimals)
|
||||
{
|
||||
list.AddRange(QuestData.HardModePrimals.Skip(1));
|
||||
}
|
||||
if (!_configuration.Advanced.SkipCrystalTowerRaids)
|
||||
{
|
||||
list.AddRange(QuestData.CrystalTowerQuests);
|
||||
}
|
||||
}
|
||||
if (!_configuration.Advanced.SkipClassJobQuests)
|
||||
{
|
||||
EClassJob valueOrDefault = ((EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId).GetValueOrDefault();
|
||||
uint[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select((IReadOnlyList<uint> x) => x[0]).ToArray();
|
||||
if (valueOrDefault != EClassJob.Adventurer)
|
||||
{
|
||||
list.AddRange(from x in _questRegistry.GetKnownClassJobQuests(valueOrDefault).Where(delegate(QuestInfo x)
|
||||
{
|
||||
if (_questRegistry.TryGetQuest(x.QuestId, out Questionable.Model.Quest quest))
|
||||
{
|
||||
IQuestInfo info = quest.Info;
|
||||
QuestInfo questInfo = info as QuestInfo;
|
||||
if (questInfo != null)
|
||||
{
|
||||
if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter))
|
||||
{
|
||||
return !QuestData.FinalShadowbringersRoleQuests.Any(IsQuestComplete);
|
||||
}
|
||||
if (QuestData.AllRoleQuestChapters.Any((IReadOnlyList<uint> y) => y.Contains(questInfo.NewGamePlusChapter)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
select x.QuestId);
|
||||
}
|
||||
}
|
||||
return list.Where(_questRegistry.IsKnownQuest).ToList();
|
||||
}
|
||||
|
||||
public unsafe bool IsReadyToAcceptQuest(ElementId questId, bool ignoreLevel = false)
|
||||
{
|
||||
_questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest);
|
||||
if (quest != null)
|
||||
{
|
||||
IQuestInfo info = quest.Info;
|
||||
if (info != null && info.IsRepeatable)
|
||||
{
|
||||
if (IsQuestAccepted(questId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (questId is QuestId questId2 && IsDailyAlliedSocietyQuest(questId2))
|
||||
{
|
||||
if (QuestManager.Instance()->IsDailyQuestCompleted(questId.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsDailyAlliedSocietyQuestAndAvailableToday(questId2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (IsQuestComplete(questId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
goto IL_0077;
|
||||
}
|
||||
}
|
||||
if (IsQuestAcceptedOrComplete(questId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
goto IL_0077;
|
||||
IL_0077:
|
||||
if (IsQuestLocked(questId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!ignoreLevel)
|
||||
{
|
||||
byte b = _clientState.LocalPlayer?.Level ?? 0;
|
||||
if (b != 0 && quest != null && quest.Info.Level > b)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsQuestAcceptedOrComplete(ElementId elementId)
|
||||
{
|
||||
if (!IsQuestComplete(elementId))
|
||||
{
|
||||
return IsQuestAccepted(elementId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsQuestAccepted(ElementId elementId)
|
||||
{
|
||||
if (elementId is QuestId questId)
|
||||
{
|
||||
return IsQuestAccepted(questId);
|
||||
}
|
||||
if (elementId is SatisfactionSupplyNpcId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (elementId is AlliedSocietyDailyId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (elementId is UnlockLinkId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
throw new ArgumentOutOfRangeException("elementId");
|
||||
}
|
||||
|
||||
public unsafe bool IsQuestAccepted(QuestId questId)
|
||||
{
|
||||
return QuestManager.Instance()->IsQuestAccepted(questId.Value);
|
||||
}
|
||||
|
||||
public bool IsQuestComplete(ElementId elementId)
|
||||
{
|
||||
if (elementId is QuestId questId)
|
||||
{
|
||||
return IsQuestComplete(questId);
|
||||
}
|
||||
if (elementId is SatisfactionSupplyNpcId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (elementId is AlliedSocietyDailyId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (elementId is UnlockLinkId unlockLinkId)
|
||||
{
|
||||
return IsQuestComplete(unlockLinkId);
|
||||
}
|
||||
throw new ArgumentOutOfRangeException("elementId");
|
||||
}
|
||||
|
||||
public bool IsQuestComplete(QuestId questId)
|
||||
{
|
||||
return QuestManager.IsQuestComplete(questId.Value);
|
||||
}
|
||||
|
||||
public unsafe bool IsQuestComplete(UnlockLinkId unlockLinkId)
|
||||
{
|
||||
return UIState.Instance()->IsUnlockLinkUnlocked(unlockLinkId.Value);
|
||||
}
|
||||
|
||||
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
|
||||
{
|
||||
if (elementId is QuestId questId)
|
||||
{
|
||||
return IsQuestLocked(questId, extraCompletedQuest);
|
||||
}
|
||||
if (elementId is SatisfactionSupplyNpcId satisfactionSupplyNpcId)
|
||||
{
|
||||
return IsQuestLocked(satisfactionSupplyNpcId);
|
||||
}
|
||||
if (elementId is AlliedSocietyDailyId alliedSocietyDailyId)
|
||||
{
|
||||
return IsQuestLocked(alliedSocietyDailyId);
|
||||
}
|
||||
if (elementId is UnlockLinkId unlockLinkId)
|
||||
{
|
||||
return IsQuestLocked(unlockLinkId);
|
||||
}
|
||||
throw new ArgumentOutOfRangeException("elementId");
|
||||
}
|
||||
|
||||
private unsafe bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
|
||||
{
|
||||
if (IsQuestUnobtainable(questId, extraCompletedQuest))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
|
||||
if (questInfo.GrandCompany != FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questInfo.AlliedSociety != EAlliedSociety.None && questInfo.IsRepeatable)
|
||||
{
|
||||
return !IsDailyAlliedSocietyQuestAndAvailableToday(questId);
|
||||
}
|
||||
if (questInfo.IsMoogleDeliveryQuest)
|
||||
{
|
||||
byte b = PlayerState.Instance()->DeliveryLevel;
|
||||
if (extraCompletedQuest != null && _questData.TryGetQuestInfo(extraCompletedQuest, out IQuestInfo questInfo2) && questInfo2 is QuestInfo { IsMoogleDeliveryQuest: not false })
|
||||
{
|
||||
b++;
|
||||
}
|
||||
if (questInfo.MoogleDeliveryLevel > b)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (HasCompletedPreviousQuests(questInfo, extraCompletedQuest))
|
||||
{
|
||||
return !HasCompletedPreviousInstances(questInfo);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsQuestLocked(SatisfactionSupplyNpcId satisfactionSupplyNpcId)
|
||||
{
|
||||
SatisfactionSupplyInfo questInfo = (SatisfactionSupplyInfo)_questData.GetQuestInfo(satisfactionSupplyNpcId);
|
||||
return !HasCompletedPreviousQuests(questInfo, null);
|
||||
}
|
||||
|
||||
private unsafe bool IsQuestLocked(AlliedSocietyDailyId alliedSocietyDailyId)
|
||||
{
|
||||
byte beastTribeRank = PlayerState.Instance()->GetBeastTribeRank(alliedSocietyDailyId.AlliedSociety);
|
||||
if (beastTribeRank != 0)
|
||||
{
|
||||
return beastTribeRank < alliedSocietyDailyId.Rank;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsQuestLocked(UnlockLinkId unlockLinkId)
|
||||
{
|
||||
return IsQuestUnobtainable(unlockLinkId);
|
||||
}
|
||||
|
||||
public bool IsDailyAlliedSocietyQuest(QuestId questId)
|
||||
{
|
||||
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
|
||||
if (questInfo.AlliedSociety != EAlliedSociety.None)
|
||||
{
|
||||
return questInfo.IsRepeatable;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsDailyAlliedSocietyQuestAndAvailableToday(QuestId questId)
|
||||
{
|
||||
if (!IsDailyAlliedSocietyQuest(questId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
|
||||
return _alliedSocietyQuestFunctions.GetAvailableAlliedSocietyQuests(questInfo.AlliedSociety).Contains(questId);
|
||||
}
|
||||
|
||||
public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null)
|
||||
{
|
||||
if (elementId is QuestId questId)
|
||||
{
|
||||
return IsQuestUnobtainable(questId, extraCompletedQuest);
|
||||
}
|
||||
if (elementId is UnlockLinkId unlockLinkId)
|
||||
{
|
||||
return IsQuestUnobtainable(unlockLinkId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool IsQuestUnobtainable(QuestId questId, ElementId? extraCompletedQuest = null)
|
||||
{
|
||||
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
|
||||
if ((int)questInfo.Expansion > (int)PlayerState.Instance()->MaxExpansion)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questInfo.QuestLocks.Count > 0)
|
||||
{
|
||||
int num = questInfo.QuestLocks.Count((QuestId x) => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
|
||||
if (questInfo.QuestLockJoin == EQuestJoin.All && questInfo.QuestLocks.Count == num)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questInfo.QuestLockJoin == EQuestJoin.AtLeastOne && num > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (_questData.GetLockedClassQuests().Contains(questId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
byte startTown = PlayerState.Instance()->StartTown;
|
||||
if (questInfo.StartingCity > 0 && questInfo.StartingCity != startTown)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questId.Value == 674 && startTown == 3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questId.Value == 673 && startTown != 3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Dictionary<ushort, EClassJob> dictionary = new Dictionary<ushort, EClassJob>
|
||||
{
|
||||
{
|
||||
108,
|
||||
EClassJob.Marauder
|
||||
},
|
||||
{
|
||||
109,
|
||||
EClassJob.Arcanist
|
||||
},
|
||||
{
|
||||
85,
|
||||
EClassJob.Lancer
|
||||
},
|
||||
{
|
||||
123,
|
||||
EClassJob.Archer
|
||||
},
|
||||
{
|
||||
124,
|
||||
EClassJob.Conjurer
|
||||
},
|
||||
{
|
||||
568,
|
||||
EClassJob.Gladiator
|
||||
},
|
||||
{
|
||||
569,
|
||||
EClassJob.Pugilist
|
||||
},
|
||||
{
|
||||
570,
|
||||
EClassJob.Thaumaturge
|
||||
}
|
||||
};
|
||||
if (dictionary.TryGetValue(questId.Value, out var value) && dictionary.Any((KeyValuePair<ushort, EClassJob> x) => IsQuestAcceptedOrComplete(new QuestId(x.Key))) && (EClassJob)PlayerState.Instance()->FirstClass != value)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (IsQuestRemoved(questId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsQuestUnobtainable(UnlockLinkId unlockLinkId)
|
||||
{
|
||||
if (unlockLinkId.Value == 506)
|
||||
{
|
||||
return !IsFestivalActive(160, (ushort)2);
|
||||
}
|
||||
if (unlockLinkId.Value == 568)
|
||||
{
|
||||
return !IsFestivalActive(160, (ushort)3);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe static bool IsFestivalActive(ushort id, ushort? phase = null)
|
||||
{
|
||||
for (int i = 0; i < GameMain.Instance()->ActiveFestivals.Length; i++)
|
||||
{
|
||||
GameMain.Festival festival = GameMain.Instance()->ActiveFestivals[i];
|
||||
if (festival.Id == id)
|
||||
{
|
||||
if (phase.HasValue)
|
||||
{
|
||||
return festival.Phase == phase;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsQuestRemoved(ElementId elementId)
|
||||
{
|
||||
if (elementId is QuestId questId)
|
||||
{
|
||||
return IsQuestRemoved(questId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsQuestRemoved(QuestId questId)
|
||||
{
|
||||
ushort value = questId.Value;
|
||||
if (value == 487 || (uint)(value - 1428) <= 1u)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool HasCompletedPreviousQuests(IQuestInfo questInfo, ElementId? extraCompletedQuest)
|
||||
{
|
||||
if (questInfo.PreviousQuests.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int num = questInfo.PreviousQuests.Count((PreviousQuestInfo x) => HasEnoughProgressOnPreviousQuest(x) || x.QuestId.Equals(extraCompletedQuest));
|
||||
if (questInfo.PreviousQuestJoin == EQuestJoin.All && questInfo.PreviousQuests.Count == num)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questInfo.PreviousQuestJoin == EQuestJoin.AtLeastOne && num > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool HasEnoughProgressOnPreviousQuest(PreviousQuestInfo previousQuestInfo)
|
||||
{
|
||||
if (IsQuestComplete(previousQuestInfo.QuestId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (previousQuestInfo.Sequence != 0 && IsQuestAccepted(previousQuestInfo.QuestId))
|
||||
{
|
||||
QuestProgressInfo questProgressInfo = GetQuestProgressInfo(previousQuestInfo.QuestId);
|
||||
if (questProgressInfo != null)
|
||||
{
|
||||
return questProgressInfo.Sequence >= previousQuestInfo.Sequence;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasCompletedPreviousInstances(QuestInfo questInfo)
|
||||
{
|
||||
if (questInfo.PreviousInstanceContent.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int num = questInfo.PreviousInstanceContent.Count((ushort x) => UIState.IsInstanceContentCompleted(x));
|
||||
if (questInfo.PreviousInstanceContentJoin == EQuestJoin.All && questInfo.PreviousInstanceContent.Count == num)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (questInfo.PreviousInstanceContentJoin == EQuestJoin.AtLeastOne && num > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public unsafe bool IsClassJobUnlocked(EClassJob classJob)
|
||||
{
|
||||
ClassJob row = _dataManager.GetExcelSheet<ClassJob>().GetRow((uint)classJob);
|
||||
ushort num = (ushort)row.UnlockQuest.RowId;
|
||||
if (num != 0)
|
||||
{
|
||||
return IsQuestComplete(new QuestId(num));
|
||||
}
|
||||
PlayerState* ptr = PlayerState.Instance();
|
||||
if (ptr != null)
|
||||
{
|
||||
return ptr->ClassJobLevels[row.ExpArrayIndex] > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsJobUnlocked(EClassJob classJob)
|
||||
{
|
||||
return IsClassJobUnlocked((EClassJob)_dataManager.GetExcelSheet<ClassJob>().GetRow((uint)classJob).ClassJobParent.RowId);
|
||||
}
|
||||
|
||||
public unsafe FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany GetGrandCompany()
|
||||
{
|
||||
return (FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany)PlayerState.Instance()->GrandCompany;
|
||||
}
|
||||
|
||||
public bool IsMainScenarioQuestComplete()
|
||||
{
|
||||
return IsQuestComplete(_questData.LastMainScenarioQuestId);
|
||||
}
|
||||
}
|
11
Questionable/Questionable.Functions/QuestReference.cs
Normal file
11
Questionable/Questionable.Functions/QuestReference.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Functions;
|
||||
|
||||
public sealed record QuestReference(ElementId? CurrentQuest, byte Sequence, MainScenarioQuestState State)
|
||||
{
|
||||
public static QuestReference NoQuest(MainScenarioQuestState state)
|
||||
{
|
||||
return new QuestReference(null, 0, state);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue