From 35a2f8bc367b3575b88ca185dd10277526bdf9e4 Mon Sep 17 00:00:00 2001 From: alydev Date: Sun, 19 Oct 2025 17:39:44 +1000 Subject: [PATCH] muffin v6.28 --- .../Combat.cs | 11 + .../CombatController.cs | 45 +++ .../Questionable.Controller/CommandHandler.cs | 379 ++++++++++++++---- 3 files changed, 357 insertions(+), 78 deletions(-) diff --git a/Questionable/Questionable.Controller.Steps.Interactions/Combat.cs b/Questionable/Questionable.Controller.Steps.Interactions/Combat.cs index 0a15d77..d338175 100644 --- a/Questionable/Questionable.Controller.Steps.Interactions/Combat.cs +++ b/Questionable/Questionable.Controller.Steps.Interactions/Combat.cs @@ -164,6 +164,17 @@ internal static class Combat { return ETaskResult.StillRunning; } + if (base.Task.CombatData.ComplexCombatDatas.Any((ComplexCombatData x) => x.MinimumKillCount.HasValue)) + { + if ((from x in base.Task.CombatData.ComplexCombatDatas.Select((ComplexCombatData ccd, int index) => (ccd: ccd, index: index)) + where x.ccd.MinimumKillCount.HasValue + select x).All(((ComplexCombatData ccd, int index) x) => base.Task.CombatData.CompletedComplexDatas.Contains(x.index))) + { + combatController.Stop("Kill count requirements met"); + return ETaskResult.TaskComplete; + } + return ETaskResult.StillRunning; + } if (QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariableFlags) && base.Task.CombatData.ElementId is QuestId elementId) { QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(elementId); diff --git a/Questionable/Questionable.Controller/CombatController.cs b/Questionable/Questionable.Controller/CombatController.cs index 27a61b3..3b79beb 100644 --- a/Questionable/Questionable.Controller/CombatController.cs +++ b/Questionable/Questionable.Controller/CombatController.cs @@ -35,6 +35,12 @@ internal sealed class CombatController : IDisposable public required DateTime LastDistanceCheck { get; set; } public bool HasAttemptedFateSync { get; set; } + + public Dictionary KillCountsByComplexDataIndex { get; } = new Dictionary(); + + public int TotalKillCount { get; set; } + + public HashSet CountedEnemies { get; } = new HashSet(); } public sealed class CombatData @@ -158,6 +164,7 @@ internal sealed class CombatController : IDisposable { if (gameObject.IsDead) { + TrackKillIfNeeded(gameObject); ElementId elementId = _currentFight.Data.ElementId; QuestProgressInfo questProgressInfo = ((elementId != null) ? _questFunctions.GetQuestProgressInfo(elementId) : null); if (questProgressInfo != null && questProgressInfo.Sequence == _currentFight.Data.Sequence && QuestWorkUtils.HasCompletionFlags(_currentFight.Data.CompletionQuestVariablesFlags) && QuestWorkUtils.MatchesQuestWork(_currentFight.Data.CompletionQuestVariablesFlags, questProgressInfo)) @@ -230,6 +237,34 @@ internal sealed class CombatController : IDisposable return EStatus.InCombat; } + private void TrackKillIfNeeded(IGameObject deadEnemy) + { + if (_currentFight == null || !(deadEnemy is IBattleNpc battleNpc) || !_currentFight.CountedEnemies.Add(deadEnemy.GameObjectId)) + { + return; + } + List complexCombatDatas = _currentFight.Data.ComplexCombatDatas; + for (int i = 0; i < complexCombatDatas.Count; i++) + { + ComplexCombatData complexCombatData = complexCombatDatas[i]; + if (complexCombatData.DataId == battleNpc.BaseId && (!complexCombatData.NameId.HasValue || complexCombatData.NameId == battleNpc.NameId) && complexCombatData.MinimumKillCount.HasValue) + { + if (!_currentFight.KillCountsByComplexDataIndex.ContainsKey(i)) + { + _currentFight.KillCountsByComplexDataIndex[i] = 0; + } + _currentFight.KillCountsByComplexDataIndex[i]++; + _logger.LogInformation("Tracked kill for ComplexCombatData[{Index}] ({DataId}): {Count}/{Required}", i, complexCombatData.DataId, _currentFight.KillCountsByComplexDataIndex[i], complexCombatData.MinimumKillCount.Value); + return; + } + } + if (complexCombatDatas.Count == 0 && _currentFight.Data.KillEnemyDataIds.Contains(battleNpc.BaseId)) + { + _currentFight.TotalKillCount++; + _logger.LogInformation("Tracked kill for KillEnemyDataIds ({DataId}): Total count = {Count}", battleNpc.BaseId, _currentFight.TotalKillCount); + } + } + private unsafe IGameObject? FindNextTarget() { if (_currentFight == null) @@ -246,6 +281,16 @@ internal sealed class CombatController : IDisposable continue; } ComplexCombatData complexCombatData = complexCombatDatas[i]; + if (complexCombatData.MinimumKillCount.HasValue) + { + int valueOrDefault = _currentFight.KillCountsByComplexDataIndex.GetValueOrDefault(i, 0); + if (valueOrDefault >= complexCombatData.MinimumKillCount.Value) + { + _logger.LogInformation("Complex combat condition fulfilled: killCount({DataId}) = {Count}/{Required}", complexCombatData.DataId, valueOrDefault, complexCombatData.MinimumKillCount.Value); + _currentFight.Data.CompletedComplexDatas.Add(i); + continue; + } + } if (complexCombatData.RewardItemId.HasValue && complexCombatData.RewardItemCount.HasValue && InventoryManager.Instance()->GetInventoryItemCount(complexCombatData.RewardItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0) >= complexCombatData.RewardItemCount.Value) { _logger.LogInformation("Complex combat condition fulfilled: itemCount({ItemId}) >= {ItemCount}", complexCombatData.RewardItemId, complexCombatData.RewardItemCount); diff --git a/Questionable/Questionable.Controller/CommandHandler.cs b/Questionable/Questionable.Controller/CommandHandler.cs index 0101485..6c3a0fc 100644 --- a/Questionable/Questionable.Controller/CommandHandler.cs +++ b/Questionable/Questionable.Controller/CommandHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; @@ -55,11 +56,13 @@ internal sealed class CommandHandler : IDisposable 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, Configuration configuration) + 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; @@ -78,6 +81,7 @@ internal sealed class CommandHandler : IDisposable _gameFunctions = gameFunctions; _dataManager = dataManager; _clientState = clientState; + _objectTable = objectTable; _configuration = configuration; _clientState.Logout += OnLogout; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) @@ -104,8 +108,8 @@ internal sealed class CommandHandler : IDisposable _chatGui.Print("/qst stop - stops doing quests", "Questionable", 576); _chatGui.Print("/qst reload - reload all quest data", "Questionable", 576); break; - case "ha": 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); @@ -114,15 +118,10 @@ internal sealed class CommandHandler : IDisposable _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 do - highlights the specified quest in the debug overlay (requires debug overlay to be enabled)", "Questionable", 576); - _chatGui.Print("/qst do - clears the highlighted quest in the debug overlay (requires debug overlay to be enabled)", "Questionable", 576); - _chatGui.Print("/qst next - sets the next quest to do (or clears it if no questId is specified)", "Questionable", 576); - _chatGui.Print("/qst sim [sequence] [step] - simulates the specified quest (or clears it if no questId is specified)", "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 mountid - prints information about your current mount", "Questionable", 576); _chatGui.Print("/qst handle-interrupt - makes Questionable handle queued interrupts immediately (useful if you manually start combat)", "Questionable", 576); break; case "c": @@ -140,15 +139,6 @@ internal sealed class CommandHandler : IDisposable case "reload": _questWindow.Reload(); break; - case "do": - ConfigureDebugOverlay(array.Skip(1).ToArray()); - break; - case "next": - SetNextQuest(array.Skip(1).ToArray()); - break; - case "sim": - SetSimulatedQuest(array.Skip(1).ToArray()); - break; case "which": _questSelectionWindow.OpenForTarget(_targetManager.Target); break; @@ -164,9 +154,6 @@ internal sealed class CommandHandler : IDisposable case "priority": _priorityWindow.ToggleOrUncollapse(); break; - case "mountid": - PrintMountId(); - break; case "handle-interrupt": _questController.InterruptQueueWithCombat(); break; @@ -186,97 +173,333 @@ internal sealed class CommandHandler : IDisposable { return; } - switch (arguments.Split(' ')[0]) + string[] array = arguments.Split(' '); + string text = array[0]; + if (text == null) { - case "abandon-duty": - _gameFunctions.AbandonDuty(); - break; - case "unlock-links": - { - IReadOnlyList unlockLinks = _gameFunctions.GetUnlockLinks(); - if (unlockLinks.Count >= 0) - { - _chatGui.Print($"Saved {unlockLinks.Count} unlock links to log.", "Questionable", 576); - List list2 = unlockLinks.Except(_previouslyUnlockedUnlockLinks).ToList(); - if (_previouslyUnlockedUnlockLinks.Count > 0 && list2.Count > 0) - { - _chatGui.Print("New unlock links: " + string.Join(", ", list2), "Questionable", 576); - } - } - else - { - _chatGui.PrintError("Could not query unlock links.", "Questionable", 576); - } - _previouslyUnlockedUnlockLinks = unlockLinks; - break; + return; } - case "taxi": + switch (text.Length) { - List list3 = new List(); - ExcelSheet excelSheet = _dataManager.GetExcelSheet(); - UIState* ptr = UIState.Instance(); - if (ptr == null) + case 4: + switch (text[0]) { - _chatGui.PrintError("UIState is null", "Questionable", 576); - break; - } - for (int i = 0; i < 192; i++) - { - uint num = (uint)(i + 1179648); - try + case 'n': + if (text == "next") { - if (excelSheet.HasRow(num) && ptr->IsChocoboTaxiStandUnlocked(num)) + 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 { - string value = excelSheet.GetRow(num).PlaceName.ToString(); - if (string.IsNullOrEmpty(value)) + if (excelSheet.HasRow(num9) && ptr->IsChocoboTaxiStandUnlocked(num9)) { - value = "Unknown"; + string value10 = excelSheet.GetRow(num9).PlaceName.ToString(); + if (string.IsNullOrEmpty(value10)) + { + value10 = "Unknown"; + } + list.Add($"{value10} (ID: {num8}, Row: 0x{num9:X})"); } - list3.Add($"{value} (ID: {i}, Row: 0x{num:X})"); + } + catch + { } } - 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; } } - _chatGui.Print($"Unlocked taxi stands ({list3.Count}):", "Questionable", 576); - if (list3.Count == 0) - { - _chatGui.Print(" (No unlocked taxi stands found)", "Questionable", 576); - break; } + break; + case 12: + switch (text[0]) { - foreach (string item in list3) + case 'a': + if (text == "abandon-duty") { - _chatGui.Print(" - " + item, "Questionable", 576); + _gameFunctions.AbandonDuty(); } break; - } - } - case "festivals": - { - List list = new List(); - for (byte b = 0; b < 4; b++) + case 'u': { - GameMain.Festival festival = GameMain.Instance()->ActiveFestivals[b]; - if (festival.Id == 0) + if (!(text == "unlock-links")) { - list.Add($"Slot {b}: None"); + 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 { - list.Add($"Slot {b}: {festival.Id}({festival.Phase})"); + _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 item2 in list) + foreach (string item4 in list2) { - _chatGui.Print(" " + item2, "Questionable", 576); + _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; } }