using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Lumina.Excel; using Lumina.Excel.Sheets; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows; namespace Questionable.Controller; internal sealed class CommandHandler : IDisposable { public const string MessageTag = "Questionable"; public const ushort TagColor = 576; private readonly ICommandManager _commandManager; private readonly IChatGui _chatGui; private readonly QuestController _questController; private readonly MovementController _movementController; private readonly QuestRegistry _questRegistry; private readonly ConfigWindow _configWindow; private readonly DebugOverlay _debugOverlay; private readonly OneTimeSetupWindow _oneTimeSetupWindow; private readonly QuestWindow _questWindow; private readonly QuestSelectionWindow _questSelectionWindow; private readonly JournalProgressWindow _journalProgressWindow; private readonly PriorityWindow _priorityWindow; private readonly ITargetManager _targetManager; private readonly QuestFunctions _questFunctions; private readonly GameFunctions _gameFunctions; private readonly IDataManager _dataManager; private readonly IClientState _clientState; private readonly IObjectTable _objectTable; private readonly Configuration _configuration; private IReadOnlyList _previouslyUnlockedUnlockLinks = Array.Empty(); public CommandHandler(ICommandManager commandManager, IChatGui chatGui, QuestController questController, MovementController movementController, QuestRegistry questRegistry, ConfigWindow configWindow, DebugOverlay debugOverlay, OneTimeSetupWindow oneTimeSetupWindow, QuestWindow questWindow, QuestSelectionWindow questSelectionWindow, JournalProgressWindow journalProgressWindow, PriorityWindow priorityWindow, ITargetManager targetManager, QuestFunctions questFunctions, GameFunctions gameFunctions, IDataManager dataManager, IClientState clientState, IObjectTable objectTable, Configuration configuration) { _commandManager = commandManager; _chatGui = chatGui; _questController = questController; _movementController = movementController; _questRegistry = questRegistry; _configWindow = configWindow; _debugOverlay = debugOverlay; _oneTimeSetupWindow = oneTimeSetupWindow; _questWindow = questWindow; _questSelectionWindow = questSelectionWindow; _journalProgressWindow = journalProgressWindow; _priorityWindow = priorityWindow; _targetManager = targetManager; _questFunctions = questFunctions; _gameFunctions = gameFunctions; _dataManager = dataManager; _clientState = clientState; _objectTable = objectTable; _configuration = configuration; _clientState.Logout += OnLogout; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) { HelpMessage = string.Join(Environment.NewLine + "\t", "Opens the Questing window", "/qst help - displays simplified commands", "/qst help-all - displays all available commands", "/qst config - opens the configuration window", "/qst start - starts doing quests", "/qst stop - stops doing quests") }); } private void ProcessCommand(string command, string arguments) { if (!OpenSetupIfNeeded(arguments)) { string[] array = arguments.Split(' '); switch (array[0]) { case "h": case "help": _chatGui.Print("Available commands:", "Questionable", 576); _chatGui.Print("/qst - toggles the Questing window", "Questionable", 576); _chatGui.Print("/qst help - displays simplified commands", "Questionable", 576); _chatGui.Print("/qst help-all - displays all available commands", "Questionable", 576); _chatGui.Print("/qst config - opens the configuration window", "Questionable", 576); _chatGui.Print("/qst start - starts doing quests", "Questionable", 576); _chatGui.Print("/qst stop - stops doing quests", "Questionable", 576); _chatGui.Print("/qst reload - reload all quest data", "Questionable", 576); break; case "help-all": case "ha": _chatGui.Print("Available commands:", "Questionable", 576); _chatGui.Print("/qst - toggles the Questing window", "Questionable", 576); _chatGui.Print("/qst help - displays available commands", "Questionable", 576); _chatGui.Print("/qst help-all - displays all available commands", "Questionable", 576); _chatGui.Print("/qst config - opens the configuration window", "Questionable", 576); _chatGui.Print("/qst start - starts doing quests", "Questionable", 576); _chatGui.Print("/qst stop - stops doing quests", "Questionable", 576); _chatGui.Print("/qst reload - reload all quest data", "Questionable", 576); _chatGui.Print("/qst which - shows all quests starting with your selected target", "Questionable", 576); _chatGui.Print("/qst zone - shows all quests starting in the current zone (only includes quests with a known quest path, and currently visible unaccepted quests)", "Questionable", 576); _chatGui.Print("/qst journal - toggles the Journal Progress window", "Questionable", 576); _chatGui.Print("/qst priority - toggles the Priority window", "Questionable", 576); _chatGui.Print("/qst handle-interrupt - makes Questionable handle queued interrupts immediately (useful if you manually start combat)", "Questionable", 576); break; case "c": case "config": _configWindow.ToggleOrUncollapse(); break; case "start": _questWindow.IsOpenAndUncollapsed = true; _questController.Start("Start command"); break; case "stop": _movementController.Stop(); _questController.Stop("Stop command"); break; case "reload": _questWindow.Reload(); break; case "which": _questSelectionWindow.OpenForTarget(_targetManager.Target); break; case "z": case "zone": _questSelectionWindow.OpenForCurrentZone(); break; case "j": case "journal": _journalProgressWindow.ToggleOrUncollapse(); break; case "p": case "priority": _priorityWindow.ToggleOrUncollapse(); break; case "handle-interrupt": _questController.InterruptQueueWithCombat(); break; case "": _questWindow.ToggleOrUncollapse(); break; default: _chatGui.PrintError("Unknown subcommand " + array[0], "Questionable", 576); break; } } } private unsafe void ProcessDebugCommand(string command, string arguments) { if (OpenSetupIfNeeded(arguments)) { return; } string[] array = arguments.Split(' '); string text = array[0]; if (text == null) { return; } switch (text.Length) { case 4: switch (text[0]) { case 'n': if (text == "next") { SetNextQuest(array.Skip(1).ToArray()); } break; case 't': { if (!(text == "taxi")) { break; } List list = new List(); ExcelSheet excelSheet = _dataManager.GetExcelSheet(); UIState* ptr = UIState.Instance(); if (ptr == null) { _chatGui.PrintError("UIState is null", "Questionable", 576); break; } for (int num8 = 0; num8 < 192; num8++) { uint num9 = (uint)(num8 + 1179648); try { if (excelSheet.HasRow(num9) && ptr->IsChocoboTaxiStandUnlocked(num9)) { string value10 = excelSheet.GetRow(num9).PlaceName.ToString(); if (string.IsNullOrEmpty(value10)) { value10 = "Unknown"; } list.Add($"{value10} (ID: {num8}, Row: 0x{num9:X})"); } } catch { } } _chatGui.Print($"Unlocked taxi stands ({list.Count}):", "Questionable", 576); if (list.Count == 0) { _chatGui.Print(" (No unlocked taxi stands found)", "Questionable", 576); break; } { foreach (string item3 in list) { _chatGui.Print(" - " + item3, "Questionable", 576); } break; } } } break; case 12: switch (text[0]) { case 'a': if (text == "abandon-duty") { _gameFunctions.AbandonDuty(); } break; case 'u': { if (!(text == "unlock-links")) { break; } IReadOnlyList unlockLinks = _gameFunctions.GetUnlockLinks(); if (unlockLinks.Count >= 0) { _chatGui.Print($"Saved {unlockLinks.Count} unlock links to log.", "Questionable", 576); List list3 = unlockLinks.Except(_previouslyUnlockedUnlockLinks).ToList(); if (_previouslyUnlockedUnlockLinks.Count > 0 && list3.Count > 0) { _chatGui.Print("New unlock links: " + string.Join(", ", list3), "Questionable", 576); } } else { _chatGui.PrintError("Could not query unlock links.", "Questionable", 576); } _previouslyUnlockedUnlockLinks = unlockLinks; break; } } break; case 2: if (text == "do") { ConfigureDebugOverlay(array.Skip(1).ToArray()); } break; case 3: if (text == "sim") { SetSimulatedQuest(array.Skip(1).ToArray()); } break; case 7: if (text == "mountid") { PrintMountId(); } break; case 9: { if (!(text == "festivals")) { break; } List list2 = new List(); for (byte b8 = 0; b8 < 4; b8++) { GameMain.Festival festival = GameMain.Instance()->ActiveFestivals[b8]; if (festival.Id == 0) { list2.Add($"Slot {b8}: None"); } else { list2.Add($"Slot {b8}: {festival.Id}({festival.Phase})"); } } _chatGui.Print("Festival slots:", "Questionable", 576); { foreach (string item4 in list2) { _chatGui.Print(" " + item4, "Questionable", 576); } break; } } case 11: { if (!(text == "quest-kills")) { break; } (QuestController.QuestProgress, QuestController.ECurrentQuestType)? currentQuestDetails = _questController.CurrentQuestDetails; if (!currentQuestDetails.HasValue) { _chatGui.PrintError("No active quest.", "Questionable", 576); break; } QuestController.QuestProgress item = currentQuestDetails.Value.Item1; Questionable.Model.Quest quest = item.Quest; QuestProgressInfo questProgressInfo = null; if (quest.Id is QuestId elementId) { questProgressInfo = _questFunctions.GetQuestProgressInfo(elementId); } if (questProgressInfo == null) { _chatGui.PrintError("Unable to retrieve quest progress information.", "Questionable", 576); break; } QuestSequence questSequence = quest.FindSequence(item.Sequence); if (questSequence == null) { _chatGui.PrintError($"Sequence {item.Sequence} not found for quest {quest.Id}.", "Questionable", 576); break; } QuestStep questStep = ((item.Step < questSequence.Steps.Count) ? questSequence.Steps[item.Step] : null); if (questStep == null) { _chatGui.PrintError($"Step {item.Step} not found in sequence {item.Sequence}.", "Questionable", 576); break; } _chatGui.Print($"Quest: {quest.Info.Name} ({quest.Id})", "Questionable", 576); _chatGui.Print($"Sequence: {item.Sequence}, Step: {item.Step}", "Questionable", 576); _chatGui.Print("", "Questionable", 576); _chatGui.Print("Quest Variables: " + string.Join(", ", questProgressInfo.Variables.Select((byte v, int i) => $"[{i}]={v}")), "Questionable", 576); _chatGui.Print("", "Questionable", 576); ExcelSheet bnpcNameSheet = _dataManager.GetExcelSheet(); HashSet hashSet = new HashSet(questStep.KillEnemyDataIds); foreach (ComplexCombatData complexCombatDatum in questStep.ComplexCombatData) { hashSet.Add(complexCombatDatum.DataId); } if (hashSet.Count > 0) { _chatGui.Print($"All Enemy DataIds Found: {hashSet.Count}", "Questionable", 576); foreach (uint item5 in hashSet.OrderBy((uint x) => x)) { (string Name, bool Found) tuple = GetEnemyName(item5); var (value, _) = tuple; if (tuple.Found) { _chatGui.Print($" - {value} (DataId: {item5})", "Questionable", 576); } else { _chatGui.Print($" - DataId: {item5}", "Questionable", 576); } } _chatGui.Print("", "Questionable", 576); } if (questStep.ComplexCombatData.Count > 0) { _chatGui.Print($"Complex Combat Data Entries: {questStep.ComplexCombatData.Count}", "Questionable", 576); _chatGui.Print("Kill Progress:", "Questionable", 576); if (questStep.ComplexCombatData.Count == 1 && hashSet.Count > 1) { ComplexCombatData complexCombatData = questStep.ComplexCombatData[0]; int num = -1; byte? b = null; for (int num2 = 0; num2 < complexCombatData.CompletionQuestVariablesFlags.Count; num2++) { QuestWorkValue questWorkValue = complexCombatData.CompletionQuestVariablesFlags[num2]; if (questWorkValue != null && questWorkValue.Low.HasValue) { num = num2; b = questWorkValue.Low; break; } } byte b2 = (byte)(((num >= 0 && num < questProgressInfo.Variables.Count) ? questProgressInfo.Variables[num] : 0) & 0xF); string value2 = (b.HasValue ? $" {b2}/{b}" : ""); string value3 = ((b.HasValue && b2 >= b) ? "✓" : "○"); foreach (uint item6 in hashSet.OrderBy((uint x) => x)) { (string Name, bool Found) tuple3 = GetEnemyName(item6); var (value4, _) = tuple3; if (tuple3.Found) { _chatGui.Print($" {value3} Slay {value4}.{value2} (DataId: {item6})", "Questionable", 576); } else { _chatGui.Print($" {value3} Slay enemy.{value2} (DataId: {item6})", "Questionable", 576); } } } else { for (int num3 = 0; num3 < questStep.ComplexCombatData.Count; num3++) { ComplexCombatData complexCombatData2 = questStep.ComplexCombatData[num3]; int num4 = -1; byte? b3 = null; bool flag = false; for (int num5 = 0; num5 < complexCombatData2.CompletionQuestVariablesFlags.Count; num5++) { QuestWorkValue questWorkValue2 = complexCombatData2.CompletionQuestVariablesFlags[num5]; if (questWorkValue2 != null) { if (questWorkValue2.Low.HasValue) { num4 = num5; b3 = questWorkValue2.Low; flag = false; break; } if (questWorkValue2.High.HasValue) { num4 = num5; b3 = questWorkValue2.High; flag = true; break; } } } byte b4 = (byte)((num4 >= 0 && num4 < questProgressInfo.Variables.Count) ? questProgressInfo.Variables[num4] : 0); byte b5 = (flag ? ((byte)(b4 >> 4)) : ((byte)(b4 & 0xF))); string value5; if (complexCombatData2.NameId.HasValue) { BNpcName? bNpcName = bnpcNameSheet?.GetRowOrDefault(complexCombatData2.NameId.Value); value5 = ((!bNpcName.HasValue || string.IsNullOrEmpty(bNpcName.Value.Singular.ToString())) ? "enemy" : bNpcName.Value.Singular.ToString()); } else { (string Name, bool Found) tuple5 = GetEnemyName(complexCombatData2.DataId); string item2 = tuple5.Name; value5 = (tuple5.Found ? item2 : "enemy"); } string value6 = (b3.HasValue ? $" {b5}/{b3}" : ""); string value7 = ((b3.HasValue && b5 >= b3) ? "✓" : "○"); string value8 = (complexCombatData2.NameId.HasValue ? $" (DataId: {complexCombatData2.DataId}, NameId: {complexCombatData2.NameId})" : $" (DataId: {complexCombatData2.DataId})"); _chatGui.Print($" {value7} Slay {value5}.{value6}{value8}", "Questionable", 576); } } _chatGui.Print("", "Questionable", 576); } else if (questStep.KillEnemyDataIds.Count == 0) { _chatGui.Print("No kill enemy data for this step.", "Questionable", 576); _chatGui.Print("", "Questionable", 576); } if (questStep.CompletionQuestVariablesFlags.Count <= 0 || !questStep.CompletionQuestVariablesFlags.Any((QuestWorkValue x) => x != null)) { break; } _chatGui.Print("Completion Flags (Debug):", "Questionable", 576); for (int num6 = 0; num6 < questStep.CompletionQuestVariablesFlags.Count; num6++) { QuestWorkValue questWorkValue3 = questStep.CompletionQuestVariablesFlags[num6]; if (questWorkValue3 != null) { int num7 = ((num6 < questProgressInfo.Variables.Count) ? questProgressInfo.Variables[num6] : 0); byte b6 = (byte)(num7 >> 4); byte b7 = (byte)(num7 & 0xF); string value9 = (((!questWorkValue3.High.HasValue || questWorkValue3.High == b6) && (!questWorkValue3.Low.HasValue || questWorkValue3.Low == b7)) ? " ✓" : " ✗"); _chatGui.Print($" [{num6}] Expected: H={questWorkValue3.High?.ToString(CultureInfo.InvariantCulture) ?? "any"} L={questWorkValue3.Low?.ToString(CultureInfo.InvariantCulture) ?? "any"} | Actual: H={b6.ToString(CultureInfo.InvariantCulture)} L={b7.ToString(CultureInfo.InvariantCulture)}{value9}", "Questionable", 576); } } break; } case 5: case 6: case 8: case 10: break; } } private bool OpenSetupIfNeeded(string arguments) { if (!_configuration.IsPluginSetupComplete()) { if (string.IsNullOrEmpty(arguments)) { _oneTimeSetupWindow.IsOpenAndUncollapsed = true; } else { _chatGui.PrintError("Please complete the one-time setup first.", "Questionable", 576); } return true; } return false; } private void ConfigureDebugOverlay(string[] arguments) { ElementId elementId; if (!_debugOverlay.DrawConditions()) { _chatGui.PrintError("You don't have the debug overlay enabled.", "Questionable", 576); } else if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out elementId) && elementId != null) { if (_questRegistry.TryGetQuest(elementId, out Questionable.Model.Quest quest)) { _debugOverlay.HighlightedQuest = quest.Id; _chatGui.Print($"Set highlighted quest to {elementId} ({quest.Info.Name}).", "Questionable", 576); } else { _chatGui.PrintError($"Unknown quest {elementId}.", "Questionable", 576); } } else { _debugOverlay.HighlightedQuest = null; _chatGui.Print("Cleared highlighted quest.", "Questionable", 576); } } private void SetNextQuest(string[] arguments) { if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId elementId) && elementId != null) { Questionable.Model.Quest quest; if (_questFunctions.IsQuestLocked(elementId)) { _chatGui.PrintError($"Quest {elementId} is locked.", "Questionable", 576); } else if (_questRegistry.TryGetQuest(elementId, out quest)) { _questController.SetNextQuest(quest); _chatGui.Print($"Set next quest to {elementId} ({quest.Info.Name}).", "Questionable", 576); } else { _chatGui.PrintError($"Unknown quest {elementId}.", "Questionable", 576); } } else { _questController.SetNextQuest(null); _chatGui.Print("Cleared next quest.", "Questionable", 576); } } private void SetSimulatedQuest(string[] arguments) { if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId elementId) && elementId != null) { if (_questRegistry.TryGetQuest(elementId, out Questionable.Model.Quest quest)) { byte sequence = 0; int step = 0; if (arguments.Length >= 2 && byte.TryParse(arguments[1], out var result)) { QuestSequence questSequence = quest.FindSequence(result); if (questSequence != null) { sequence = questSequence.Sequence; if (arguments.Length >= 3 && int.TryParse(arguments[2], out var result2) && questSequence.FindStep(result2) != null) { step = result2; } } } _questController.SimulateQuest(quest, sequence, step); _chatGui.Print($"Simulating quest {elementId} ({quest.Info.Name}).", "Questionable", 576); } else { _chatGui.PrintError($"Unknown quest {elementId}.", "Questionable", 576); } } else { _questController.SimulateQuest(null, 0, 0); _chatGui.Print("Cleared simulated quest.", "Questionable", 576); } } private void PrintMountId() { ushort? mountId = _gameFunctions.GetMountId(); if (mountId.HasValue) { Mount? rowOrDefault = _dataManager.GetExcelSheet().GetRowOrDefault(mountId.Value); _chatGui.Print($"Mount ID: {mountId}, Name: {rowOrDefault?.Singular}, Obtainable: {((rowOrDefault?.Order == -1) ? "No" : "Yes")}", "Questionable", 576); } else { _chatGui.Print("You are not mounted.", "Questionable", 576); } } private void OnLogout(int type, int code) { _previouslyUnlockedUnlockLinks = Array.Empty(); } public void Dispose() { _commandManager.RemoveHandler("/qst"); _clientState.Logout -= OnLogout; } }