From 139250c4a4f07200dbca4ba73efbf59c38489578 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 5 Aug 2024 17:09:49 +0200 Subject: [PATCH] Automatic weekly custom delivery turn in + some gathering cleanup --- .../The Peaks/531_Sleeping Stones_MIN.json | 4 +- LLib | 2 +- QuestPathGenerator/RoslynShortcuts.cs | 7 + .../Zhloe/S1_Zhloe Aliapoh.json | 28 +- .../Adkiragh/S4_Adkiragh.json | 28 +- .../Custom Deliveries/Kurenai/S3_Kurenai.json | 28 +- .../Custom Deliveries/M'naago/S2_M'naago.json | 28 +- .../Charlemend/S7_Charlemend.json | 26 ++ .../Ehll Tou/S6_Ehll Tou.json | 26 ++ .../Kai-Shirr/S5_Kai-Shirr.json | 28 +- .../Ameliance/S8_Ameliance.json | 26 ++ .../Custom Deliveries/Anden/S9_Anden.json | 28 +- .../Margrat/S10_Margrat.json | 28 +- .../MSQ/M-6.55/4751_When One Door Closes.json | 9 +- QuestPaths/quest-v1.json | 1 + .../Converter/InteractionTypeConverter.cs | 1 + Questionable.Model/Questing/DialogueChoice.cs | 1 + .../Questing/EInteractionType.cs | 1 + Questionable.Model/Questing/GatheredItem.cs | 5 + Questionable/Controller/CombatController.cs | 13 +- Questionable/Controller/CommandHandler.cs | 9 +- .../Controller/ContextMenuController.cs | 85 ++-- Questionable/Controller/GameUiController.cs | 98 +++-- .../Controller/GatheringController.cs | 1 + Questionable/Controller/MovementController.cs | 1 + .../NavigationShortcutController.cs | 1 + Questionable/Controller/QuestController.cs | 36 +- .../Controller/Steps/Common/MountTask.cs | 1 + .../Controller/Steps/Common/NextQuest.cs | 25 +- .../Controller/Steps/Common/UnmountTask.cs | 1 + .../Steps/Gathering/DoGatherCollectable.cs | 1 + .../Steps/Gathering/MoveToLandingLocation.cs | 1 + .../Steps/Gathering/TurnInDelivery.cs | 83 ++++ Questionable/Controller/Steps/ILastTask.cs | 2 +- .../Controller/Steps/Interactions/Action.cs | 1 + .../Steps/Interactions/AetherCurrent.cs | 1 + .../Steps/Interactions/AethernetShard.cs | 1 + .../Steps/Interactions/Aetheryte.cs | 1 + .../Controller/Steps/Interactions/Combat.cs | 11 +- .../Controller/Steps/Interactions/Duty.cs | 1 + .../Controller/Steps/Interactions/Emote.cs | 1 + .../Controller/Steps/Interactions/Interact.cs | 1 + .../Controller/Steps/Interactions/Say.cs | 5 +- .../Controller/Steps/Interactions/UseItem.cs | 38 +- .../Steps/Shared/AethernetShortcut.cs | 1 + .../Steps/Shared/AetheryteShortcut.cs | 1 + .../Steps/Shared/GatheringRequiredItems.cs | 18 +- Questionable/Controller/Steps/Shared/Move.cs | 1 + .../Controller/Steps/Shared/SkipCondition.cs | 20 +- .../Controller/Steps/Shared/SwitchClassJob.cs | 44 ++ .../Controller/Steps/Shared/WaitAtEnd.cs | 35 +- Questionable/{ => Functions}/ChatFunctions.cs | 2 +- Questionable/Functions/ExcelFunctions.cs | 101 +++++ Questionable/{ => Functions}/GameFunctions.cs | 396 ++---------------- Questionable/Functions/QuestFunctions.cs | 346 +++++++++++++++ Questionable/Model/StringOrRegex.cs | 43 ++ Questionable/Questionable.csproj | 2 +- Questionable/QuestionablePlugin.cs | 90 ++-- Questionable/Validation/EIssueType.cs | 1 + Questionable/Validation/QuestValidator.cs | 10 +- Questionable/Validation/ValidationIssue.cs | 2 +- .../Validators/AethernetShortcutValidator.cs | 4 +- .../Validators/BasicSequenceValidator.cs | 8 +- .../Validators/CompletionFlagsValidator.cs | 2 +- .../Validators/DialogueChoiceValidator.cs | 83 ++++ .../Validators/JsonSchemaValidator.cs | 4 +- .../Validators/NextQuestValidator.cs | 2 +- .../Validators/QuestDisabledValidator.cs | 2 +- .../Validators/UniqueStartStopValidator.cs | 8 +- Questionable/Windows/JournalProgressWindow.cs | 9 +- .../QuestComponents/ARealmRebornComponent.cs | 15 +- .../QuestComponents/ActiveQuestComponent.cs | 12 +- .../QuestComponents/CreationUtilsComponent.cs | 19 +- .../QuestComponents/QuestTooltipComponent.cs | 9 +- .../QuickAccessButtonsComponent.cs | 1 + Questionable/Windows/QuestSelectionWindow.cs | 28 +- Questionable/Windows/QuestValidationWindow.cs | 6 +- Questionable/Windows/QuestWindow.cs | 5 + Questionable/Windows/UiUtils.cs | 15 +- 79 files changed, 1433 insertions(+), 636 deletions(-) create mode 100644 Questionable/Controller/Steps/Gathering/TurnInDelivery.cs create mode 100644 Questionable/Controller/Steps/Shared/SwitchClassJob.cs rename Questionable/{ => Functions}/ChatFunctions.cs (99%) create mode 100644 Questionable/Functions/ExcelFunctions.cs rename Questionable/{ => Functions}/GameFunctions.cs (53%) create mode 100644 Questionable/Functions/QuestFunctions.cs create mode 100644 Questionable/Model/StringOrRegex.cs create mode 100644 Questionable/Validation/Validators/DialogueChoiceValidator.cs diff --git a/GatheringPaths/4.x - Stormblood/The Peaks/531_Sleeping Stones_MIN.json b/GatheringPaths/4.x - Stormblood/The Peaks/531_Sleeping Stones_MIN.json index 10ec9162..d9e04e5e 100644 --- a/GatheringPaths/4.x - Stormblood/The Peaks/531_Sleeping Stones_MIN.json +++ b/GatheringPaths/4.x - Stormblood/The Peaks/531_Sleeping Stones_MIN.json @@ -79,7 +79,7 @@ "Y": 257.4255, "Z": -669.3115 }, - "MinimumAngle": -65, + "MinimumAngle": -30, "MaximumAngle": 5 } ] @@ -128,4 +128,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/LLib b/LLib index 9db9f95b..43c3dba1 160000 --- a/LLib +++ b/LLib @@ -1 +1 @@ -Subproject commit 9db9f95b8cd3f36262b5b4b14f12b7331d3c7279 +Subproject commit 43c3dba112c202e2d0ff1a6909020c2b83e20dc3 diff --git a/QuestPathGenerator/RoslynShortcuts.cs b/QuestPathGenerator/RoslynShortcuts.cs index 027e597a..e01b9201 100644 --- a/QuestPathGenerator/RoslynShortcuts.cs +++ b/QuestPathGenerator/RoslynShortcuts.cs @@ -155,6 +155,10 @@ public static class RoslynShortcuts .AsSyntaxNodeOrToken(), Assignment(nameof(DialogueChoice.Answer), dialogueChoice.Answer, emptyChoice.Answer) .AsSyntaxNodeOrToken(), + Assignment(nameof(DialogueChoice.AnswerIsRegularExpression), + dialogueChoice.AnswerIsRegularExpression, + emptyChoice.AnswerIsRegularExpression) + .AsSyntaxNodeOrToken(), Assignment(nameof(DialogueChoice.DataId), dialogueChoice.DataId, emptyChoice.DataId) .AsSyntaxNodeOrToken())))); } @@ -359,6 +363,9 @@ public static class RoslynShortcuts .AsSyntaxNodeOrToken(), Assignment(nameof(GatheredItem.Collectability), gatheredItem.Collectability, emptyItem.Collectability) + .AsSyntaxNodeOrToken(), + Assignment(nameof(GatheredItem.ClassJob), gatheredItem.ClassJob, + emptyItem.ClassJob) .AsSyntaxNodeOrToken())))); } else if (value is GatheringNodeGroup nodeGroup) diff --git a/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json index e909b81d..c2154923 100644 --- a/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json +++ b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json @@ -25,7 +25,33 @@ }, "StopDistance": 5, "TerritoryId": 478, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/003/CtsSfsCharacter1_00386", + "Prompt": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/003/CtsSfsCharacter1_00386", + "Prompt": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER1_00386_TOPMENU_000_003" + } + ] } ] } diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json index 07c9f828..3912cfc6 100644 --- a/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json +++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json @@ -15,7 +15,33 @@ "TerritoryId": 478, "InteractionType": "Interact", "RequiredGatheredItems": [], - "AetheryteShortcut": "Idyllshire" + "AetheryteShortcut": "Idyllshire", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/005/CtsSfsCharacter4_00541", + "Prompt": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/005/CtsSfsCharacter4_00541", + "Prompt": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER4_00541_TOPMENU_000_004" + } + ] } ] } diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json index edcd6dd8..3fef30be 100644 --- a/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json +++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json @@ -15,7 +15,33 @@ "TerritoryId": 613, "InteractionType": "Interact", "RequiredGatheredItems": [], - "AetheryteShortcut": "Ruby Sea - Tamamizu" + "AetheryteShortcut": "Ruby Sea - Tamamizu", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/004/CtsSfsCharacter3_00481", + "Prompt": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 613, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/004/CtsSfsCharacter3_00481", + "Prompt": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER3_00481_TOPMENU_000_004" + } + ] } ] } diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json index 435169ec..4acf85fc 100644 --- a/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json +++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json @@ -15,7 +15,33 @@ "TerritoryId": 635, "InteractionType": "Interact", "RequiredGatheredItems": [], - "AetheryteShortcut": "Rhalgr's Reach" + "AetheryteShortcut": "Rhalgr's Reach", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/004/CtsSfsCharacter2_00434", + "Prompt": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/004/CtsSfsCharacter2_00434", + "Prompt": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER2_00434_TOPMENU_000_003" + } + ] } ] } diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json index 0e73e7d0..ea885fd4 100644 --- a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json +++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json @@ -18,6 +18,32 @@ "AethernetShortcut": [ "[Ishgard] Aetheryte Plaza", "[Ishgard] Firmament" + ], + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/007/CtsSfsCharacter7_00710", + "Prompt": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/007/CtsSfsCharacter7_00710", + "Prompt": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER7_00710_TOPMENU_000_004" + } ] } ] diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json index 2e9c047b..6e4e68f3 100644 --- a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json +++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json @@ -18,6 +18,32 @@ "AethernetShortcut": [ "[Ishgard] Aetheryte Plaza", "[Ishgard] Firmament" + ], + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/006/CtsSfsCharacter6_00674", + "Prompt": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/006/CtsSfsCharacter6_00674", + "Prompt": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER6_00674_TOPMENU_000_003" + } ] } ] diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json index 9e279e15..325c08cd 100644 --- a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json +++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json @@ -15,7 +15,33 @@ "TerritoryId": 820, "InteractionType": "Interact", "RequiredGatheredItems": [], - "AetheryteShortcut": "Eulmore" + "AetheryteShortcut": "Eulmore", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/006/CtsSfsCharacter5_00640", + "Prompt": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/006/CtsSfsCharacter5_00640", + "Prompt": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER5_00640_TOPMENU_000_004" + } + ] } ] } diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json index cc256d9f..36229edd 100644 --- a/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json +++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json @@ -19,6 +19,32 @@ "AethernetShortcut": [ "[Old Sharlayan] Aetheryte Plaza", "[Old Sharlayan] The Leveilleur Estate" + ], + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/007/CtsSfsCharacter8_00773", + "Prompt": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/007/CtsSfsCharacter8_00773", + "Prompt": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER8_00773_TOPMENU_000_004" + } ] } ] diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json index 7cdc3904..1ac3ad73 100644 --- a/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json +++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json @@ -16,7 +16,33 @@ "InteractionType": "Interact", "RequiredGatheredItems": [], "AetheryteShortcut": "Il Mheg - Lydha Lran", - "Fly": true + "Fly": true, + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/008/CtsSfsCharacter9_00815", + "Prompt": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/008/CtsSfsCharacter9_00815", + "Prompt": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER9_00815_TOPMENU_000_004" + } + ] } ] } diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json index 3d9a80be..1d5507af 100644 --- a/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json +++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json @@ -25,7 +25,33 @@ "Z": -65.14081 }, "TerritoryId": 956, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/008/CtsSfsCharacter10_00842", + "Prompt": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_001", + "AnswerIsRegularExpression": true + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 635, + "InteractionType": "None", + "DialogueChoices": [ + { + "Type": "List", + "ExcelSheet": "custom/008/CtsSfsCharacter10_00842", + "Prompt": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_000", + "Answer": "TEXT_CTSSFSCHARACTER10_00842_TOPMENU_000_004" + } + ] } ] } diff --git a/QuestPaths/6.x - Endwalker/MSQ/M-6.55/4751_When One Door Closes.json b/QuestPaths/6.x - Endwalker/MSQ/M-6.55/4751_When One Door Closes.json index 6053a1d5..67a64455 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/M-6.55/4751_When One Door Closes.json +++ b/QuestPaths/6.x - Endwalker/MSQ/M-6.55/4751_When One Door Closes.json @@ -13,14 +13,7 @@ "Z": -68.40625 }, "TerritoryId": 963, - "InteractionType": "AcceptQuest", - "DialogueChoices": [ - { - "Type": "List", - "Prompt": "TEXT_AKTKMM103_04753_Q1_000_000", - "Answer": "TEXT_AKTKMM103_04753_A1_000_001" - } - ] + "InteractionType": "AcceptQuest" } ] }, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index e7d7ee2b..f0029f01 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -101,6 +101,7 @@ "type": "string", "description": "What to do at the position", "enum": [ + "None", "Interact", "WalkTo", "AttuneAethernetShard", diff --git a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs index 23f0366e..4e375415 100644 --- a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs +++ b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs @@ -7,6 +7,7 @@ public sealed class InteractionTypeConverter() : EnumConverter { private static readonly Dictionary Values = new() { + { EInteractionType.None, "None" }, { EInteractionType.Interact, "Interact" }, { EInteractionType.WalkTo, "WalkTo" }, { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" }, diff --git a/Questionable.Model/Questing/DialogueChoice.cs b/Questionable.Model/Questing/DialogueChoice.cs index 91370f5d..41cb2519 100644 --- a/Questionable.Model/Questing/DialogueChoice.cs +++ b/Questionable.Model/Questing/DialogueChoice.cs @@ -16,6 +16,7 @@ public sealed class DialogueChoice [JsonConverter(typeof(ExcelRefConverter))] public ExcelRef? Answer { get; set; } + public bool AnswerIsRegularExpression { get; set; } /// /// If set, only applies when focusing the given target id. diff --git a/Questionable.Model/Questing/EInteractionType.cs b/Questionable.Model/Questing/EInteractionType.cs index 5080714a..e080f07c 100644 --- a/Questionable.Model/Questing/EInteractionType.cs +++ b/Questionable.Model/Questing/EInteractionType.cs @@ -6,6 +6,7 @@ namespace Questionable.Model.Questing; [JsonConverter(typeof(InteractionTypeConverter))] public enum EInteractionType { + None, Interact, WalkTo, AttuneAethernetShard, diff --git a/Questionable.Model/Questing/GatheredItem.cs b/Questionable.Model/Questing/GatheredItem.cs index bfc6fd1a..8b915954 100644 --- a/Questionable.Model/Questing/GatheredItem.cs +++ b/Questionable.Model/Questing/GatheredItem.cs @@ -5,4 +5,9 @@ public sealed class GatheredItem public uint ItemId { get; set; } public int ItemCount { get; set; } public ushort Collectability { get; set; } + + /// + /// Either miner or botanist; null if it is irrelevant (prefers current class/job, then any unlocked ones). + /// + public uint? ClassJob { get; set; } } diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index 8c999825..057604d5 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Common.Math; using Microsoft.Extensions.Logging; using Questionable.Controller.CombatModules; using Questionable.Controller.Utils; +using Questionable.Functions; using Questionable.Model.Questing; namespace Questionable.Controller; @@ -26,7 +27,7 @@ internal sealed class CombatController : IDisposable private readonly IObjectTable _objectTable; private readonly ICondition _condition; private readonly IClientState _clientState; - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly ILogger _logger; private CurrentFight? _currentFight; @@ -39,7 +40,7 @@ internal sealed class CombatController : IDisposable IObjectTable objectTable, ICondition condition, IClientState clientState, - GameFunctions gameFunctions, + QuestFunctions questFunctions, ILogger logger) { _combatModules = combatModules.ToList(); @@ -48,7 +49,7 @@ internal sealed class CombatController : IDisposable _objectTable = objectTable; _condition = condition; _clientState = clientState; - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _logger = logger; _clientState.TerritoryChanged += TerritoryChanged; @@ -168,9 +169,9 @@ internal sealed class CombatController : IDisposable } } - if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) && _currentFight.Data.QuestElementId is QuestId questId) + if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) && _currentFight.Data.ElementId is QuestId questId) { - var questWork = _gameFunctions.GetQuestEx(questId); + var questWork = _questFunctions.GetQuestEx(questId); if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags, questWork.Value)) { @@ -303,7 +304,7 @@ internal sealed class CombatController : IDisposable public sealed class CombatData { - public required ElementId QuestElementId { get; init; } + public required ElementId ElementId { get; init; } public required EEnemySpawnType SpawnType { get; init; } public required List KillEnemyDataIds { get; init; } public required List ComplexCombatDatas { get; init; } diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs index 142d3263..322e6ccd 100644 --- a/Questionable/Controller/CommandHandler.cs +++ b/Questionable/Controller/CommandHandler.cs @@ -3,6 +3,7 @@ using System.Linq; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Plugin.Services; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows; @@ -23,7 +24,7 @@ internal sealed class CommandHandler : IDisposable private readonly QuestWindow _questWindow; private readonly QuestSelectionWindow _questSelectionWindow; private readonly ITargetManager _targetManager; - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; public CommandHandler( ICommandManager commandManager, @@ -37,7 +38,7 @@ internal sealed class CommandHandler : IDisposable QuestWindow questWindow, QuestSelectionWindow questSelectionWindow, ITargetManager targetManager, - GameFunctions gameFunctions) + QuestFunctions questFunctions) { _commandManager = commandManager; _chatGui = chatGui; @@ -50,7 +51,7 @@ internal sealed class CommandHandler : IDisposable _questWindow = questWindow; _questSelectionWindow = questSelectionWindow; _targetManager = targetManager; - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) { @@ -149,7 +150,7 @@ internal sealed class CommandHandler : IDisposable { if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null) { - if (_gameFunctions.IsQuestLocked(questId)) + if (_questFunctions.IsQuestLocked(questId)) _chatGui.PrintError($"[Questionable] Quest {questId} is locked."); else if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { diff --git a/Questionable/Controller/ContextMenuController.cs b/Questionable/Controller/ContextMenuController.cs index e4f7e329..6b3f7f9f 100644 --- a/Questionable/Controller/ContextMenuController.cs +++ b/Questionable/Controller/ContextMenuController.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LLib.GameData; using Microsoft.Extensions.Logging; using Questionable.Data; +using Questionable.Functions; using Questionable.GameStructs; using Questionable.Model; using Questionable.Model.Questing; @@ -21,6 +22,8 @@ internal sealed class ContextMenuController : IDisposable private readonly GatheringData _gatheringData; private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; + private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly IGameGui _gameGui; private readonly IChatGui _chatGui; private readonly IClientState _clientState; @@ -32,6 +35,8 @@ internal sealed class ContextMenuController : IDisposable GatheringData gatheringData, QuestRegistry questRegistry, QuestData questData, + GameFunctions gameFunctions, + QuestFunctions questFunctions, IGameGui gameGui, IChatGui chatGui, IClientState clientState, @@ -42,6 +47,8 @@ internal sealed class ContextMenuController : IDisposable _gatheringData = gatheringData; _questRegistry = questRegistry; _questData = questData; + _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _gameGui = gameGui; _chatGui = chatGui; _clientState = clientState; @@ -52,7 +59,7 @@ internal sealed class ContextMenuController : IDisposable private void MenuOpened(IMenuOpenedArgs args) { - uint itemId = (uint) _gameGui.HoveredItem; + uint itemId = (uint)_gameGui.HoveredItem; if (itemId == 0) return; @@ -62,43 +69,66 @@ internal sealed class ContextMenuController : IDisposable if (itemId >= 500_000) itemId -= 500_000; - if (!_gatheringData.TryGetGatheringPointId(itemId, (EClassJob)_clientState.LocalPlayer!.ClassJob.Id, out _)) + if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId)) + { + AddContextMenuEntry(args, itemId, npcId, EClassJob.Miner, "Mine"); + AddContextMenuEntry(args, itemId, npcId, EClassJob.Botanist, "Harvest"); + } + } + + private void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EClassJob classJob, string verb) + { + EClassJob currentClassJob = (EClassJob)_clientState.LocalPlayer!.ClassJob.Id; + if (classJob != currentClassJob && currentClassJob is EClassJob.Miner or EClassJob.Botanist) + return; + + if (!_gatheringData.TryGetGatheringPointId(itemId, classJob, out _)) { _logger.LogInformation("No gathering point found for current job."); return; } - if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId)) + ushort collectability = _gatheringData.GetRecommendedCollectability(itemId); + int quantityToGather = collectability > 0 ? 6 : int.MaxValue; + if (collectability == 0) + return; + + unsafe { - ushort collectability = _gatheringData.GetRecommendedCollectability(itemId); - int quantityToGather = collectability > 0 ? 6 : int.MaxValue; - if (collectability == 0) - return; - - unsafe + var agentSatisfactionSupply = AgentSatisfactionSupply.Instance(); + if (agentSatisfactionSupply->IsAgentActive()) { - var agentSatisfactionSupply = AgentSatisfactionSupply.Instance(); - if (agentSatisfactionSupply->IsAgentActive()) - { - quantityToGather = Math.Min(agentSatisfactionSupply->RemainingAllowances, - ((AgentSatisfactionSupply2*)agentSatisfactionSupply)->TurnInsToNextRank); - } + quantityToGather = Math.Min(agentSatisfactionSupply->RemainingAllowances, + ((AgentSatisfactionSupply2*)agentSatisfactionSupply)->TurnInsToNextRank); } - - args.AddMenuItem(new MenuItem - { - Prefix = SeIconChar.Hyadelyn, - PrefixColor = 52, - Name = "Gather with Questionable", - OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability), - IsEnabled = quantityToGather > 0, - }); } + + string lockedReasonn = string.Empty; + if (!_questFunctions.IsClassJobUnlocked(classJob)) + lockedReasonn = $"{classJob} not unlocked"; + else if (quantityToGather == 0) + lockedReasonn = "No allowances"; + else if (_gameFunctions.IsOccupied()) + lockedReasonn = "Can't be used while interacting"; + + string name = $"{verb} with Questionable"; + if (!string.IsNullOrEmpty(lockedReasonn)) + name += $" ({lockedReasonn})"; + + args.AddMenuItem(new MenuItem + { + Prefix = SeIconChar.Hyadelyn, + PrefixColor = 52, + Name = name, + OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability, classJob), + IsEnabled = string.IsNullOrEmpty(lockedReasonn), + }); } - private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability) + private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability, EClassJob classJob) { - var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single(x => x is SatisfactionSupplyInfo); + var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId) + .Single(x => x is SatisfactionSupplyInfo); if (_questRegistry.TryGetQuest(info.QuestId, out Quest? quest)) { var step = quest.FindSequence(0)!.FindStep(0)!; @@ -108,7 +138,8 @@ internal sealed class ContextMenuController : IDisposable { ItemId = itemId, ItemCount = quantity, - Collectability = collectability + Collectability = collectability, + ClassJob = (uint)classJob, } ]; _questController.SetGatheringQuest(quest); diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index 7f8a6223..8cd3f046 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -14,6 +14,8 @@ using LLib.GameUI; using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.Logging; using Questionable.Data; +using Questionable.Functions; +using Questionable.Model; using Questionable.Model.Questing; using Quest = Questionable.Model.Quest; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; @@ -25,6 +27,8 @@ internal sealed class GameUiController : IDisposable private readonly IAddonLifecycle _addonLifecycle; private readonly IDataManager _dataManager; private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; + private readonly ExcelFunctions _excelFunctions; private readonly QuestController _questController; private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; @@ -33,13 +37,24 @@ internal sealed class GameUiController : IDisposable private readonly ILogger _logger; private readonly Regex _returnRegex; - public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions, - QuestController questController, QuestRegistry questRegistry, QuestData questData, IGameGui gameGui, - ITargetManager targetManager, IPluginLog pluginLog, ILogger logger) + public GameUiController( + IAddonLifecycle addonLifecycle, + IDataManager dataManager, + GameFunctions gameFunctions, + QuestFunctions questFunctions, + ExcelFunctions excelFunctions, + QuestController questController, + QuestRegistry questRegistry, + QuestData questData, + IGameGui gameGui, + ITargetManager targetManager, + IPluginLog pluginLog, ILogger logger) { _addonLifecycle = addonLifecycle; _dataManager = dataManager; _gameFunctions = gameFunctions; + _questFunctions = questFunctions; + _excelFunctions = excelFunctions; _questController = questController; _questRegistry = questRegistry; _questData = questData; @@ -188,7 +203,7 @@ internal sealed class GameUiController : IDisposable { // it is possible for this to be a quest selection string questName = quest.Info.Name; - int questSelection = answers.FindIndex(x => GameStringEquals(questName, x)); + int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x)); if (questSelection >= 0) { addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection); @@ -210,7 +225,7 @@ internal sealed class GameUiController : IDisposable private int? HandleListChoice(string? actualPrompt, List answers, bool checkAllSteps) { List dialogueChoices = []; - var currentQuest = _questController.SimulatedQuest ?? _questController.StartedQuest; + var currentQuest = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest; if (currentQuest != null) { var quest = currentQuest.Quest; @@ -260,9 +275,9 @@ internal sealed class GameUiController : IDisposable var target = _targetManager.Target; if (target != null) { - foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId)) + foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId).Where(x => x.QuestId is QuestId)) { - if (_gameFunctions.IsReadyToAcceptQuest(questInfo.QuestId) && + if (_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) && _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest)) { var questChoices = knownQuest.FindSequence(0)?.Steps @@ -300,8 +315,10 @@ internal sealed class GameUiController : IDisposable continue; } - string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); - string? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer); + string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false) + ?.GetString(); + StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer, + dialogueChoice.AnswerIsRegularExpression); if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt)) { @@ -309,7 +326,8 @@ internal sealed class GameUiController : IDisposable continue; } - if (actualPrompt != null && (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt))) + if (actualPrompt != null && + (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))) { _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", excelPrompt, actualPrompt); @@ -320,10 +338,22 @@ internal sealed class GameUiController : IDisposable { _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}", answers[i], excelAnswer); - if (GameStringEquals(answers[i], excelAnswer)) + if (IsMatch(answers[i], excelAnswer)) { _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'", i, answers[i], actualPrompt); + + // ensure we only open the dialog once + if (quest.Id is SatisfactionSupplyNpcId) + { + if (_questController.GatheringQuest == null || + _questController.GatheringQuest.Sequence == 255) + return null; + + _questController.GatheringQuest.SetSequence(1); + _questController.ExecuteNextStep(QuestController.EAutomationType.CurrentQuestOnly); + } + return i; } } @@ -333,13 +363,24 @@ internal sealed class GameUiController : IDisposable return null; } + private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer) + { + if (actualAnswer == null && expectedAnswer == null) + return true; + + if (actualAnswer == null || expectedAnswer == null) + return false; + + return expectedAnswer.IsMatch(actualAnswer); + } + private int? HandleInstanceListChoice(string? actualPrompt) { if (!_questController.IsRunning) return null; - string? expectedPrompt = _gameFunctions.GetDialogueTextByRowId("Addon", 2090); - if (GameStringEquals(actualPrompt, expectedPrompt)) + string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString(); + if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt)) { _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt); return 0; // any instance @@ -419,8 +460,9 @@ internal sealed class GameUiController : IDisposable continue; } - string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); - if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)) + string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false) + ?.GetString(); + if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt)) { _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", excelPrompt, actualPrompt); @@ -506,13 +548,13 @@ internal sealed class GameUiController : IDisposable string? excelName = entry.Name?.ToString(); string? excelQuestion = entry.Question?.ToString(); - if (excelQuestion != null && GameStringEquals(excelQuestion, actualPrompt)) + if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt)) { warpId = entry.RowId; warpText = excelQuestion; return true; } - else if (excelName != null && GameStringEquals(excelName, actualPrompt)) + else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt)) { warpId = entry.RowId; warpText = excelName; @@ -642,31 +684,17 @@ internal sealed class GameUiController : IDisposable } } - /// - /// Ensures characters like '-' are handled equally in both strings. - /// - public static bool GameStringEquals(string? a, string? b) - { - if (a == null) - return b == null; - - if (b == null) - return false; - - return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-'); - } - - private string? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef) + private StringOrRegex? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp) { if (excelRef == null) return null; if (excelRef.Type == ExcelRef.EType.Key) - return _gameFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey()); + return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp); else if (excelRef.Type == ExcelRef.EType.RowId) - return _gameFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId()); + return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp); else if (excelRef.Type == ExcelRef.EType.RawString) - return excelRef.AsRawString(); + return new StringOrRegex(excelRef.AsRawString()); return null; } diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index 053b552f..87a1eee1 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -14,6 +14,7 @@ using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Shared; using Questionable.External; +using Questionable.Functions; using Questionable.GatheringPaths; using Questionable.Model.Gathering; diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index c4f1b05b..2f40a3be 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -17,6 +17,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Control; using Microsoft.Extensions.Logging; using Questionable.Controller.NavigationOverrides; using Questionable.External; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Common.Converter; diff --git a/Questionable/Controller/NavigationShortcutController.cs b/Questionable/Controller/NavigationShortcutController.cs index fcdd8fa4..f3294f2a 100644 --- a/Questionable/Controller/NavigationShortcutController.cs +++ b/Questionable/Controller/NavigationShortcutController.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; +using Questionable.Functions; using Questionable.Model; namespace Questionable.Controller; diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index cde68ae2..1b62bc65 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -5,10 +5,12 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Shared; using Questionable.External; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -18,6 +20,7 @@ internal sealed class QuestController : MiniTaskController { private readonly IClientState _clientState; private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly MovementController _movementController; private readonly CombatController _combatController; private readonly GatheringController _gatheringController; @@ -46,6 +49,7 @@ internal sealed class QuestController : MiniTaskController public QuestController( IClientState clientState, GameFunctions gameFunctions, + QuestFunctions questFunctions, MovementController movementController, CombatController combatController, GatheringController gatheringController, @@ -61,6 +65,7 @@ internal sealed class QuestController : MiniTaskController { _clientState = clientState; _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _movementController = movementController; _combatController = combatController; _gatheringController = gatheringController; @@ -78,7 +83,7 @@ internal sealed class QuestController : MiniTaskController { if (_simulatedQuest != null) return (_simulatedQuest, ECurrentQuestType.Simulated); - else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) + else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) return (_nextQuest, ECurrentQuestType.Next); else if (_gatheringQuest != null) return (_gatheringQuest, ECurrentQuestType.Gathering); @@ -177,7 +182,7 @@ internal sealed class QuestController : MiniTaskController UpdateCurrentTask(); } - private void UpdateCurrentQuest() + private unsafe void UpdateCurrentQuest() { lock (_progressLock) { @@ -188,9 +193,9 @@ internal sealed class QuestController : MiniTaskController // if the quest is accepted, we no longer track it bool canUseNextQuest; if (_nextQuest.Quest.Info.IsRepeatable) - canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.Id); + canUseNextQuest = !_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id); else - canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id); + canUseNextQuest = !_questFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id); if (!canUseNextQuest) { @@ -207,7 +212,7 @@ internal sealed class QuestController : MiniTaskController currentSequence = _simulatedQuest.Sequence; questToRun = _simulatedQuest; } - else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) + else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) { questToRun = _nextQuest; currentSequence = _nextQuest.Sequence; // by definition, this should always be 0 @@ -226,11 +231,10 @@ internal sealed class QuestController : MiniTaskController _taskQueue.Count == 0 && _automationType == EAutomationType.Automatic) ExecuteNextStep(_automationType); - } else { - (ElementId? currentQuestId, currentSequence) = _gameFunctions.GetCurrentQuest(); + (ElementId? currentQuestId, currentSequence) = _questFunctions.GetCurrentQuest(); if (currentQuestId == null || currentQuestId.Value == 0) { if (_startedQuest != null) @@ -276,7 +280,7 @@ internal sealed class QuestController : MiniTaskController return; } - if (_gameFunctions.IsOccupied()) + if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questToRun.Quest)) { DebugState = "Occupied"; return; @@ -303,7 +307,7 @@ internal sealed class QuestController : MiniTaskController if (questToRun.Sequence != currentSequence) { questToRun.SetSequence(currentSequence); - Stop($"New sequence {questToRun == _startedQuest}/{_gameFunctions.GetCurrentQuestInternal()}", + Stop($"New sequence {questToRun == _startedQuest}/{_questFunctions.GetCurrentQuestInternal()}", continueIfAutomatic: true); } @@ -455,7 +459,7 @@ internal sealed class QuestController : MiniTaskController protected override void UpdateCurrentTask() { - if (_gameFunctions.IsOccupied()) + if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(CurrentQuest?.Quest)) return; base.UpdateCurrentTask(); @@ -469,7 +473,7 @@ internal sealed class QuestController : MiniTaskController protected override void OnNextStep(ILastTask task) { - IncreaseStepCount(task.QuestElementId, task.Sequence, true); + IncreaseStepCount(task.ElementId, task.Sequence, true); } public void ExecuteNextStep(EAutomationType automatic) @@ -484,7 +488,7 @@ internal sealed class QuestController : MiniTaskController if (CurrentQuest == null || seq == null || step == null) { if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId && - CurrentQuestDetails?.Progress.Sequence == 0 && + CurrentQuestDetails?.Progress.Sequence == 1 && CurrentQuestDetails?.Progress.Step == 255 && CurrentQuestDetails?.Type == ECurrentQuestType.Gathering) { @@ -590,7 +594,7 @@ internal sealed class QuestController : MiniTaskController } } - public void Skip(ElementId questQuestElementId, byte currentQuestSequence) + public void Skip(ElementId elementId, byte currentQuestSequence) { lock (_progressLock) { @@ -609,13 +613,13 @@ internal sealed class QuestController : MiniTaskController if (_taskQueue.Count == 0) { Stop("Skip"); - IncreaseStepCount(questQuestElementId, currentQuestSequence); + IncreaseStepCount(elementId, currentQuestSequence); } } else { Stop("SkipNx"); - IncreaseStepCount(questQuestElementId, currentQuestSequence); + IncreaseStepCount(elementId, currentQuestSequence); } } } @@ -657,7 +661,7 @@ internal sealed class QuestController : MiniTaskController foreach (var id in priorityQuests) { var questId = new QuestId(id); - if (_gameFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest)) + if (_questFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest)) { SetNextQuest(quest); _chatGui.Print( diff --git a/Questionable/Controller/Steps/Common/MountTask.cs b/Questionable/Controller/Steps/Common/MountTask.cs index bcf489d1..6dfba0b0 100644 --- a/Questionable/Controller/Steps/Common/MountTask.cs +++ b/Questionable/Controller/Steps/Common/MountTask.cs @@ -5,6 +5,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.Logging; using Questionable.Data; +using Questionable.Functions; namespace Questionable.Controller.Steps.Common; diff --git a/Questionable/Controller/Steps/Common/NextQuest.cs b/Questionable/Controller/Steps/Common/NextQuest.cs index 2afc6ecb..4efe08ae 100644 --- a/Questionable/Controller/Steps/Common/NextQuest.cs +++ b/Questionable/Controller/Steps/Common/NextQuest.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -26,32 +27,32 @@ internal static class NextQuest } } - internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, GameFunctions gameFunctions, ILogger logger) : ITask + internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger logger) : ITask { - public ElementId NextQuestElementId { get; set; } = null!; - public ElementId CurrentQuestElementId { get; set; } = null!; + public ElementId NextQuestId { get; set; } = null!; + public ElementId CurrentQuestId { get; set; } = null!; - public ITask With(ElementId nextQuestElementId, ElementId currentQuestElementId) + public ITask With(ElementId nextQuestId, ElementId currentQuestId) { - NextQuestElementId = nextQuestElementId; - CurrentQuestElementId = currentQuestElementId; + NextQuestId = nextQuestId; + CurrentQuestId = currentQuestId; return this; } public bool Start() { - if (gameFunctions.IsQuestLocked(NextQuestElementId, CurrentQuestElementId)) + if (questFunctions.IsQuestLocked(NextQuestId, CurrentQuestId)) { - logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestElementId); + logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestId); } - else if (questRegistry.TryGetQuest(NextQuestElementId, out Quest? quest)) + else if (questRegistry.TryGetQuest(NextQuestId, out Quest? quest)) { - logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestElementId, quest.Info.Name); + logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestId, quest.Info.Name); questController.SetNextQuest(quest); } else { - logger.LogInformation("Next quest with id {QuestId} not found", NextQuestElementId); + logger.LogInformation("Next quest with id {QuestId} not found", NextQuestId); questController.SetNextQuest(null); } @@ -60,6 +61,6 @@ internal static class NextQuest public ETaskResult Update() => ETaskResult.TaskComplete; - public override string ToString() => $"SetNextQuest({NextQuestElementId})"; + public override string ToString() => $"SetNextQuest({NextQuestId})"; } } diff --git a/Questionable/Controller/Steps/Common/UnmountTask.cs b/Questionable/Controller/Steps/Common/UnmountTask.cs index 5ee67844..a379dc9d 100644 --- a/Questionable/Controller/Steps/Common/UnmountTask.cs +++ b/Questionable/Controller/Steps/Common/UnmountTask.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; +using Questionable.Functions; namespace Questionable.Controller.Steps.Common; diff --git a/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs b/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs index 2590f7cf..45f4113d 100644 --- a/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs +++ b/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameData; using LLib.GameUI; using Microsoft.Extensions.Logging; +using Questionable.Functions; using Questionable.Model.Gathering; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs index 46cc268c..b565cfc1 100644 --- a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs +++ b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs @@ -8,6 +8,7 @@ using GatheringPathRenderer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Shared; +using Questionable.Functions; using Questionable.Model.Gathering; namespace Questionable.Controller.Steps.Gathering; diff --git a/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs b/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs new file mode 100644 index 00000000..f3a4ed51 --- /dev/null +++ b/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs @@ -0,0 +1,83 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LLib.GameUI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Questionable.Model; +using Questionable.Model.Questing; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Questionable.Controller.Steps.Gathering; + +internal static class TurnInDelivery +{ + internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory + { + public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) + { + if (quest.Id is not SatisfactionSupplyNpcId || sequence.Sequence != 1) + return null; + + return serviceProvider.GetRequiredService(); + } + } + + internal sealed class SatisfactionSupplyTurnIn(ILogger logger) : ITask + { + private ushort? _remainingAllowances; + + public bool Start() => true; + + public unsafe ETaskResult Update() + { + AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance(); + if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive()) + return _remainingAllowances == null ? ETaskResult.StillRunning : ETaskResult.TaskComplete; + + var addonId = agentSatisfactionSupply->GetAddonId(); + if (addonId == 0) + return _remainingAllowances == null ? ETaskResult.StillRunning : ETaskResult.TaskComplete; + + AtkUnitBase* addon = LAddon.GetAddonById(addonId); + if (addon == null || !LAddon.IsAddonReady(addon)) + return ETaskResult.StillRunning; + + ushort remainingAllowances = agentSatisfactionSupply->RemainingAllowances; + if (remainingAllowances == 0) + { + logger.LogInformation("No remaining weekly allowances"); + addon->FireCallbackInt(0); + return ETaskResult.TaskComplete; + } + + if (InventoryManager.Instance()->GetInventoryItemCount(agentSatisfactionSupply->Items[1].Id, + minCollectability: (short)agentSatisfactionSupply->Items[1].Collectability1) == 0) + { + logger.LogInformation("Inventory has no {ItemId}", agentSatisfactionSupply->Items[1].Id); + addon->FireCallbackInt(0); + return ETaskResult.TaskComplete; + } + + // we should at least wait until we have less allowances + if (_remainingAllowances == remainingAllowances) + return ETaskResult.StillRunning; + + // try turning it in... + logger.LogInformation("Attempting turn-in (remaining allowances: {RemainingAllowances})", + remainingAllowances); + _remainingAllowances = remainingAllowances; + + var pickGatheringItem = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 1 }, + new() { Type = ValueType.Int, Int = 1 } + }; + addon->FireCallback(2, pickGatheringItem); + return ETaskResult.StillRunning; + } + + public override string ToString() => "WeeklyDeliveryTurnIn"; + } +} diff --git a/Questionable/Controller/Steps/ILastTask.cs b/Questionable/Controller/Steps/ILastTask.cs index 71e8b0a6..0e0b275a 100644 --- a/Questionable/Controller/Steps/ILastTask.cs +++ b/Questionable/Controller/Steps/ILastTask.cs @@ -4,6 +4,6 @@ namespace Questionable.Controller.Steps; internal interface ILastTask : ITask { - public ElementId QuestElementId { get; } + public ElementId ElementId { get; } public int Sequence { get; } } diff --git a/Questionable/Controller/Steps/Interactions/Action.cs b/Questionable/Controller/Steps/Interactions/Action.cs index 000811ca..cf60b6c7 100644 --- a/Questionable/Controller/Steps/Interactions/Action.cs +++ b/Questionable/Controller/Steps/Interactions/Action.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/AetherCurrent.cs b/Questionable/Controller/Steps/Interactions/AetherCurrent.cs index 3ae2c4b3..7df86e78 100644 --- a/Questionable/Controller/Steps/Interactions/AetherCurrent.cs +++ b/Questionable/Controller/Steps/Interactions/AetherCurrent.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/AethernetShard.cs b/Questionable/Controller/Steps/Interactions/AethernetShard.cs index b3219a6e..741d92ab 100644 --- a/Questionable/Controller/Steps/Interactions/AethernetShard.cs +++ b/Questionable/Controller/Steps/Interactions/AethernetShard.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/Aetheryte.cs b/Questionable/Controller/Steps/Interactions/Aetheryte.cs index c2cab7df..c38d20ce 100644 --- a/Questionable/Controller/Steps/Interactions/Aetheryte.cs +++ b/Questionable/Controller/Steps/Interactions/Aetheryte.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index d23608f0..51120464 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -78,19 +79,19 @@ internal static class Combat } } - internal sealed class HandleCombat(CombatController combatController, GameFunctions gameFunctions) : ITask + internal sealed class HandleCombat(CombatController combatController, QuestFunctions questFunctions) : ITask { private bool _isLastStep; private CombatController.CombatData _combatData = null!; private IList _completionQuestVariableFlags = null!; - public ITask With(ElementId questElementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList killEnemyDataIds, + public ITask With(ElementId elementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList killEnemyDataIds, IList completionQuestVariablesFlags, IList complexCombatData) { _isLastStep = isLastStep; _combatData = new CombatController.CombatData { - QuestElementId = questElementId, + ElementId = elementId, SpawnType = enemySpawnType, KillEnemyDataIds = killEnemyDataIds.ToList(), ComplexCombatDatas = complexCombatData.ToList(), @@ -107,9 +108,9 @@ internal static class Combat return ETaskResult.StillRunning; // if our quest step has any completion flags, we need to check if they are set - if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.QuestElementId is QuestId questId) + if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.ElementId is QuestId questId) { - var questWork = gameFunctions.GetQuestEx(questId); + var questWork = questFunctions.GetQuestEx(questId); if (questWork == null) return ETaskResult.StillRunning; diff --git a/Questionable/Controller/Steps/Interactions/Duty.cs b/Questionable/Controller/Steps/Interactions/Duty.cs index ab2afdcd..35a73dd9 100644 --- a/Questionable/Controller/Steps/Interactions/Duty.cs +++ b/Questionable/Controller/Steps/Interactions/Duty.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/Emote.cs b/Questionable/Controller/Steps/Interactions/Emote.cs index 0a5e9064..fafa0fd8 100644 --- a/Questionable/Controller/Steps/Interactions/Emote.cs +++ b/Questionable/Controller/Steps/Interactions/Emote.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Questionable.Controller.Steps.Common; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index 869a1fcf..06674e92 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Shared; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Interactions/Say.cs b/Questionable/Controller/Steps/Interactions/Say.cs index 20fc7f67..ee900af3 100644 --- a/Questionable/Controller/Steps/Interactions/Say.cs +++ b/Questionable/Controller/Steps/Interactions/Say.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Questionable.Controller.Steps.Common; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -9,7 +10,7 @@ namespace Questionable.Controller.Steps.Interactions; internal static class Say { - internal sealed class Factory(IServiceProvider serviceProvider, GameFunctions gameFunctions) : ITaskFactory + internal sealed class Factory(IServiceProvider serviceProvider, ExcelFunctions excelFunctions) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -20,7 +21,7 @@ internal static class Say ArgumentNullException.ThrowIfNull(step.ChatMessage); string? excelString = - gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key); + excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, false).GetString(); ArgumentNullException.ThrowIfNull(excelString); var unmount = serviceProvider.GetRequiredService(); diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index cf866414..dca8260f 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -12,6 +12,7 @@ using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; @@ -103,7 +104,7 @@ internal static class UseItem yield return serviceProvider.GetRequiredService() .With(null, EAetheryteLocation.Limsa, territoryId); yield return serviceProvider.GetRequiredService() - .With(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist, null); + .With(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); yield return serviceProvider.GetRequiredService(); yield return serviceProvider.GetRequiredService() .With(territoryId, destination, dataId: npcId, sprint: false); @@ -112,7 +113,7 @@ internal static class UseItem } } - internal abstract class UseItemBase(GameFunctions gameFunctions, ICondition condition, ILogger logger) : ITask + internal abstract class UseItemBase(QuestFunctions questFunctions, ICondition condition, ILogger logger) : ITask { private bool _usedItem; private DateTime _continueAt; @@ -144,7 +145,7 @@ internal static class UseItem { if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags)) { - QuestWork? questWork = gameFunctions.GetQuestEx(questId); + QuestWork? questWork = questFunctions.GetQuestEx(questId); if (questWork != null && QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork.Value)) return ETaskResult.TaskComplete; @@ -196,11 +197,9 @@ internal static class UseItem } - internal sealed class UseOnGround(GameFunctions gameFunctions, ICondition condition, ILogger logger) - : UseItemBase(gameFunctions, condition, logger) + internal sealed class UseOnGround(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger logger) + : UseItemBase(questFunctions, condition, logger) { - private readonly GameFunctions _gameFunctions = gameFunctions; - public uint DataId { get; set; } public ITask With(ElementId? questId, uint dataId, uint itemId, IList completionQuestVariablesFlags) @@ -212,19 +211,18 @@ internal static class UseItem return this; } - protected override bool UseItem() => _gameFunctions.UseItemOnGround(DataId, ItemId); + protected override bool UseItem() => gameFunctions.UseItemOnGround(DataId, ItemId); public override string ToString() => $"UseItem({ItemId} on ground at {DataId})"; } internal sealed class UseOnPosition( GameFunctions gameFunctions, + QuestFunctions questFunctions, ICondition condition, ILogger logger) - : UseItemBase(gameFunctions, condition, logger) + : UseItemBase(questFunctions, condition, logger) { - private readonly GameFunctions _gameFunctions = gameFunctions; - public Vector3 Position { get; set; } public ITask With(ElementId? questId, Vector3 position, uint itemId, IList completionQuestVariablesFlags) @@ -236,17 +234,15 @@ internal static class UseItem return this; } - protected override bool UseItem() => _gameFunctions.UseItemOnPosition(Position, ItemId); + protected override bool UseItem() => gameFunctions.UseItemOnPosition(Position, ItemId); public override string ToString() => $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})"; } - internal sealed class UseOnObject(GameFunctions gameFunctions, ICondition condition, ILogger logger) - : UseItemBase(gameFunctions, condition, logger) + internal sealed class UseOnObject(QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger logger) + : UseItemBase(questFunctions, condition, logger) { - private readonly GameFunctions _gameFunctions = gameFunctions; - public uint DataId { get; set; } public ITask With(ElementId? questId, uint dataId, uint itemId, IList completionQuestVariablesFlags, @@ -260,16 +256,14 @@ internal static class UseItem return this; } - protected override bool UseItem() => _gameFunctions.UseItem(DataId, ItemId); + protected override bool UseItem() => gameFunctions.UseItem(DataId, ItemId); public override string ToString() => $"UseItem({ItemId} on {DataId})"; } - internal sealed class Use(GameFunctions gameFunctions, ICondition condition, ILogger logger) - : UseItemBase(gameFunctions, condition, logger) + internal sealed class Use(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger logger) + : UseItemBase(questFunctions, condition, logger) { - private readonly GameFunctions _gameFunctions = gameFunctions; - public ITask With(ElementId? questId, uint itemId, IList completionQuestVariablesFlags) { QuestId = questId; @@ -278,7 +272,7 @@ internal static class UseItem return this; } - protected override bool UseItem() => _gameFunctions.UseItem(ItemId); + protected override bool UseItem() => gameFunctions.UseItem(ItemId); public override string ToString() => $"UseItem({ItemId})"; } diff --git a/Questionable/Controller/Steps/Shared/AethernetShortcut.cs b/Questionable/Controller/Steps/Shared/AethernetShortcut.cs index d8d33cfb..82643de1 100644 --- a/Questionable/Controller/Steps/Shared/AethernetShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AethernetShortcut.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.External; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Common.Converter; diff --git a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs index ba03874a..b73f9585 100644 --- a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs index df213a33..1a083c17 100644 --- a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs +++ b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs @@ -28,14 +28,25 @@ internal static class GatheringRequiredItems { foreach (var requiredGatheredItems in step.RequiredGatheredItems) { - if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId, - (EClassJob)clientState.LocalPlayer!.ClassJob.Id, out var gatheringPointId)) + EClassJob currentClassJob = (EClassJob)clientState.LocalPlayer!.ClassJob.Id; + EClassJob classJob = currentClassJob; + if (requiredGatheredItems.ClassJob != null) + classJob = (EClassJob)requiredGatheredItems.ClassJob.Value; + + if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId, classJob, + out var gatheringPointId)) throw new TaskException($"No gathering point found for item {requiredGatheredItems.ItemId}"); if (!AssemblyGatheringLocationLoader.GetLocations() .TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot)) throw new TaskException($"No path found for gathering point {gatheringPointId}"); + if (classJob != currentClassJob) + { + yield return serviceProvider.GetRequiredService() + .With(classJob); + } + if (HasRequiredItems(requiredGatheredItems)) continue; @@ -71,7 +82,8 @@ internal static class GatheringRequiredItems InventoryManager* inventoryManager = InventoryManager.Instance(); return inventoryManager != null && inventoryManager->GetInventoryItemCount(requiredGatheredItems.ItemId, - minCollectability: (short)requiredGatheredItems.Collectability) >= requiredGatheredItems.ItemCount; + minCollectability: (short)requiredGatheredItems.Collectability) >= + requiredGatheredItems.ItemCount; } public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step) diff --git a/Questionable/Controller/Steps/Shared/Move.cs b/Questionable/Controller/Steps/Shared/Move.cs index 24d3e7f8..8ba93503 100644 --- a/Questionable/Controller/Steps/Shared/Move.cs +++ b/Questionable/Controller/Steps/Shared/Move.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Questionable.Controller.NavigationOverrides; using Questionable.Controller.Steps.Common; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index c95894eb..731d5d38 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -10,6 +10,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Utils; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; @@ -41,17 +42,18 @@ internal static class SkipCondition internal sealed class CheckSkip( ILogger logger, GameFunctions gameFunctions, + QuestFunctions questFunctions, IClientState clientState) : ITask { public QuestStep Step { get; set; } = null!; public SkipStepConditions SkipConditions { get; set; } = null!; - public ElementId QuestElementId { get; set; } = null!; + public ElementId ElementId { get; set; } = null!; - public ITask With(QuestStep step, SkipStepConditions skipConditions, ElementId questElementId) + public ITask With(QuestStep step, SkipStepConditions skipConditions, ElementId elementId) { Step = step; SkipConditions = skipConditions; - QuestElementId = questElementId; + ElementId = elementId; return this; } @@ -95,14 +97,14 @@ internal static class SkipCondition } if (SkipConditions.QuestsCompleted.Count > 0 && - SkipConditions.QuestsCompleted.All(gameFunctions.IsQuestComplete)) + SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete)) { logger.LogInformation("Skipping step, all prequisite quests are complete"); return true; } if (SkipConditions.QuestsAccepted.Count > 0 && - SkipConditions.QuestsAccepted.All(gameFunctions.IsQuestAccepted)) + SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted)) { logger.LogInformation("Skipping step, all prequisite quests are accepted"); return true; @@ -156,9 +158,9 @@ internal static class SkipCondition return true; } - if (QuestElementId is QuestId questId) + if (ElementId is QuestId questId) { - QuestWork? questWork = gameFunctions.GetQuestEx(questId); + QuestWork? questWork = questFunctions.GetQuestEx(questId); if (QuestWorkUtils.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && questWork != null) { if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)) @@ -198,13 +200,13 @@ internal static class SkipCondition } } - if (Step.PickUpQuestId != null && gameFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId)) + if (Step.PickUpQuestId != null && questFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId)) { logger.LogInformation("Skipping step, as we have already picked up the relevant quest"); return true; } - if (Step.TurnInQuestId != null && gameFunctions.IsQuestComplete(Step.TurnInQuestId)) + if (Step.TurnInQuestId != null && questFunctions.IsQuestComplete(Step.TurnInQuestId)) { logger.LogInformation("Skipping step, as we have already completed the relevant quest"); return true; diff --git a/Questionable/Controller/Steps/Shared/SwitchClassJob.cs b/Questionable/Controller/Steps/Shared/SwitchClassJob.cs new file mode 100644 index 00000000..c8159b93 --- /dev/null +++ b/Questionable/Controller/Steps/Shared/SwitchClassJob.cs @@ -0,0 +1,44 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using LLib.GameData; +using Questionable.Controller.Steps.Common; + +namespace Questionable.Controller.Steps.Shared; + +internal sealed class SwitchClassJob(IClientState clientState) : AbstractDelayedTask +{ + private EClassJob _classJob; + + public ITask With(EClassJob classJob) + { + _classJob = classJob; + return this; + } + + protected override unsafe bool StartInternal() + { + if (clientState.LocalPlayer!.ClassJob.Id == (uint)_classJob) + return false; + + var gearsetModule = RaptureGearsetModule.Instance(); + if (gearsetModule != null) + { + for (int i = 0; i < 100; ++i) + { + var gearset = gearsetModule->GetGearset(i); + if (gearset->ClassJob == (byte)_classJob) + { + gearsetModule->EquipGearset(gearset->Id, gearset->BannerIndex); + return true; + } + } + } + + throw new TaskException($"No gearset found for {_classJob}"); + } + + protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete; + + public override string ToString() => $"SwitchJob({_classJob})"; +} diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index fcaacdf6..badab7dc 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Questionable.Controller.Steps.Common; using Questionable.Controller.Utils; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -160,7 +161,7 @@ internal static class WaitAtEnd public override string ToString() => "Wait(next step or sequence)"; } - internal sealed class WaitForCompletionFlags(GameFunctions gameFunctions) : ITask + internal sealed class WaitForCompletionFlags(QuestFunctions questFunctions) : ITask { public QuestId Quest { get; set; } = null!; public QuestStep Step { get; set; } = null!; @@ -178,7 +179,7 @@ internal static class WaitAtEnd public ETaskResult Update() { - QuestWork? questWork = gameFunctions.GetQuestEx(Quest); + QuestWork? questWork = questFunctions.GetQuestEx(Quest); return questWork != null && QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value) ? ETaskResult.TaskComplete @@ -214,13 +215,13 @@ internal static class WaitAtEnd $"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})"; } - internal sealed class WaitQuestAccepted(GameFunctions gameFunctions) : ITask + internal sealed class WaitQuestAccepted(QuestFunctions questFunctions) : ITask { - public ElementId QuestElementId { get; set; } = null!; + public ElementId ElementId { get; set; } = null!; - public ITask With(ElementId questElementId) + public ITask With(ElementId elementId) { - QuestElementId = questElementId; + ElementId = elementId; return this; } @@ -228,21 +229,21 @@ internal static class WaitAtEnd public ETaskResult Update() { - return gameFunctions.IsQuestAccepted(QuestElementId) + return questFunctions.IsQuestAccepted(ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; } - public override string ToString() => $"WaitQuestAccepted({QuestElementId})"; + public override string ToString() => $"WaitQuestAccepted({ElementId})"; } - internal sealed class WaitQuestCompleted(GameFunctions gameFunctions) : ITask + internal sealed class WaitQuestCompleted(QuestFunctions questFunctions) : ITask { - public ElementId QuestElementId { get; set; } = null!; + public ElementId ElementId { get; set; } = null!; - public ITask With(ElementId questElementId) + public ITask With(ElementId elementId) { - QuestElementId = questElementId; + ElementId = elementId; return this; } @@ -250,15 +251,15 @@ internal static class WaitAtEnd public ETaskResult Update() { - return gameFunctions.IsQuestComplete(QuestElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; + return questFunctions.IsQuestComplete(ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; } - public override string ToString() => $"WaitQuestComplete({QuestElementId})"; + public override string ToString() => $"WaitQuestComplete({ElementId})"; } - internal sealed class NextStep(ElementId questElementId, int sequence) : ILastTask + internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask { - public ElementId QuestElementId { get; } = questElementId; + public ElementId ElementId { get; } = elementId; public int Sequence { get; } = sequence; public bool Start() => true; @@ -270,7 +271,7 @@ internal static class WaitAtEnd internal sealed class EndAutomation : ILastTask { - public ElementId QuestElementId => throw new InvalidOperationException(); + public ElementId ElementId => throw new InvalidOperationException(); public int Sequence => throw new InvalidOperationException(); public bool Start() => true; diff --git a/Questionable/ChatFunctions.cs b/Questionable/Functions/ChatFunctions.cs similarity index 99% rename from Questionable/ChatFunctions.cs rename to Questionable/Functions/ChatFunctions.cs index d28946a8..5c31e4d8 100644 --- a/Questionable/ChatFunctions.cs +++ b/Questionable/Functions/ChatFunctions.cs @@ -16,7 +16,7 @@ using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.Logging; using Questionable.Model.Questing; -namespace Questionable; +namespace Questionable.Functions; internal sealed unsafe class ChatFunctions { diff --git a/Questionable/Functions/ExcelFunctions.cs b/Questionable/Functions/ExcelFunctions.cs new file mode 100644 index 00000000..cdf50650 --- /dev/null +++ b/Questionable/Functions/ExcelFunctions.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using LLib; +using Lumina.Excel.CustomSheets; +using Lumina.Excel.GeneratedSheets; +using Lumina.Text; +using Microsoft.Extensions.Logging; +using Questionable.Model; +using Quest = Questionable.Model.Quest; +using GimmickYesNo = Lumina.Excel.GeneratedSheets2.GimmickYesNo; + +namespace Questionable.Functions; + +internal sealed class ExcelFunctions +{ + private readonly IDataManager _dataManager; + private readonly ILogger _logger; + + public ExcelFunctions(IDataManager dataManager, ILogger logger) + { + _dataManager = dataManager; + _logger = logger; + } + + public StringOrRegex GetDialogueText(Quest currentQuest, string? excelSheetName, string key, bool isRegex) + { + var seString = GetRawDialogueText(currentQuest, excelSheetName, key); + if (isRegex) + return new StringOrRegex(seString.ToRegex()); + else + return new StringOrRegex(seString?.ToDalamudString().ToString()); + } + + public SeString? GetRawDialogueText(Quest currentQuest, string? excelSheetName, string key) + { + if (excelSheetName == null) + { + var questRow = + _dataManager.GetExcelSheet()!.GetRow((uint)currentQuest.Id.Value + + 0x10000); + if (questRow == null) + { + _logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id); + return null; + } + + excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}"; + } + + var excelSheet = _dataManager.Excel.GetSheet(excelSheetName); + if (excelSheet == null) + { + _logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName); + return null; + } + + return excelSheet.FirstOrDefault(x => x.Key == key)?.Value; + } + + public StringOrRegex GetDialogueTextByRowId(string? excelSheet, uint rowId, bool isRegex) + { + var seString = GetRawDialogueTextByRowId(excelSheet, rowId); + if (isRegex) + return new StringOrRegex(seString.ToRegex()); + else + return new StringOrRegex(seString?.ToDalamudString().ToString()); + } + + public SeString? GetRawDialogueTextByRowId(string? excelSheet, uint rowId) + { + if (excelSheet == "GimmickYesNo") + { + var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); + return questRow?.Unknown0; + } + else if (excelSheet == "Warp") + { + var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); + return questRow?.Name; + } + else if (excelSheet is "Addon") + { + var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); + return questRow?.Text; + } + else if (excelSheet is "EventPathMove") + { + var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); + return questRow?.Unknown10; + } + else if (excelSheet is "ContentTalk" or null) + { + var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); + return questRow?.Text; + } + else + throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}"); + } +} diff --git a/Questionable/GameFunctions.cs b/Questionable/Functions/GameFunctions.cs similarity index 53% rename from Questionable/GameFunctions.cs rename to Questionable/Functions/GameFunctions.cs index b247cd80..689c340e 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/Functions/GameFunctions.cs @@ -1,16 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Memory; using Dalamud.Plugin.Services; -using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -18,60 +14,51 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; -using Lumina.Excel.CustomSheets; -using Lumina.Excel.GeneratedSheets2; using Microsoft.Extensions.Logging; -using Questionable.Controller; -using Questionable.Data; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; using Action = Lumina.Excel.GeneratedSheets2.Action; using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara; using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition; -using ContentTalk = Lumina.Excel.GeneratedSheets.ContentTalk; -using EventPathMove = Lumina.Excel.GeneratedSheets.EventPathMove; -using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using Quest = Questionable.Model.Quest; using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType; -namespace Questionable; +namespace Questionable.Functions; internal sealed unsafe class GameFunctions { private readonly ReadOnlyDictionary _territoryToAetherCurrentCompFlgSet; private readonly ReadOnlyDictionary _contentFinderConditionToContentId; + 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 QuestRegistry _questRegistry; - private readonly QuestData _questData; private readonly IGameGui _gameGui; private readonly Configuration _configuration; private readonly ILogger _logger; - public GameFunctions(IDataManager dataManager, + public GameFunctions( + QuestFunctions questFunctions, + IDataManager dataManager, IObjectTable objectTable, ITargetManager targetManager, ICondition condition, IClientState clientState, - QuestRegistry questRegistry, - QuestData questData, IGameGui gameGui, Configuration configuration, ILogger logger) { + _questFunctions = questFunctions; _dataManager = dataManager; _objectTable = objectTable; _targetManager = targetManager; _condition = condition; _clientState = clientState; - _questRegistry = questRegistry; - _questData = questData; _gameGui = gameGui; _configuration = configuration; _logger = logger; @@ -89,289 +76,6 @@ internal sealed unsafe class GameFunctions public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue; - public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuest() - { - var (currentQuest, sequence) = GetCurrentQuestInternal(); - PlayerState* playerState = PlayerState.Instance(); - - if (currentQuest == null || currentQuest.Value == 0) - { - if (_clientState.TerritoryType == 181) // Starting in Limsa - return (new QuestId(107), 0); - if (_clientState.TerritoryType == 182) // Starting in Ul'dah - return (new QuestId(594), 0); - if (_clientState.TerritoryType == 183) // Starting in Gridania - return (new QuestId(39), 0); - return default; - } - else if (currentQuest.Value == 681) - { - // if we have already picked up the GC quest, just return the progress for it - if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest)) - return (currentQuest, sequence); - - // The company you keep... - return _configuration.General.GrandCompany switch - { - GrandCompany.TwinAdder => (new QuestId(680), 0), - GrandCompany.Maelstrom => (new QuestId(681), 0), - _ => default - }; - } - else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace - { - ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch - { - GrandCompany.TwinAdder => 700, - GrandCompany.Maelstrom => 701, - _ => 0 - }; - - if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest)) - return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest)); - } - else if (currentQuest.Value == 801) - { - // skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate - QuestId broadeningHorizons = new QuestId(802); - if (IsQuestAccepted(broadeningHorizons)) - return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value)); - } - - return (currentQuest, sequence); - } - - public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuestInternal() - { - var questManager = QuestManager.Instance(); - if (questManager != null) - { - // always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do - // side quests until the end of time. - var msqQuest = GetMainScenarioQuest(questManager); - if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest)) - return msqQuest; - - // Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item, - // do the MSQ; if a side quest is the first item do that side quest. - // - // If no quests are marked as 'priority', accepting a new quest adds it to the top of the list. - for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i) - { - ElementId currentQuest; - var trackedQuest = questManager->TrackedQuests[i]; - switch (trackedQuest.QuestType) - { - default: - continue; - - case 1: // normal quest - currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId); - break; - } - - if (_questRegistry.IsKnownQuest(currentQuest)) - return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value)); - } - - // if we know no quest of those currently in the to-do list, just do MSQ - return msqQuest; - } - - return default; - } - - private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager) - { - if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled - { - AgentInterface* questRedoHud = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud); - if (questRedoHud != null && questRedoHud->IsAgentActive()) - { - // there's surely better ways to check this, but the one in the OOB Plugin was even less reliable - if (_gameGui.TryGetAddonByName("QuestRedoHud", out var addon) && - addon->AtkValuesCount == 4 && - // 0 seems to be active, - // 1 seems to be paused, - // 2 is unknown, but it happens e.g. before the quest 'Alzadaal's Legacy' - // 3 seems to be having /ng+ open while active, - // 4 seems to be when (a) suspending the chapter, or (b) having turned in a quest - addon->AtkValues[0].UInt is 0 or 2 or 3 or 4) - { - // redoHud+44 is chapter - // redoHud+46 is quest - ushort questId = MemoryHelper.Read((nint)questRedoHud + 46); - return (new QuestId(questId), QuestManager.GetQuestSequence(questId)); - } - } - } - - var scenarioTree = AgentScenarioTree.Instance(); - if (scenarioTree == null) - return default; - - if (scenarioTree->Data == null) - return default; - - QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest); - if (currentQuest.Value == 0) - return default; - - // if the MSQ is hidden, we generally ignore it - if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden) - return default; - - // it can sometimes happen (although this isn't reliably reproducible) that the quest returned here - // is one you've just completed. - if (!IsReadyToAcceptQuest(currentQuest)) - return default; - - // if we're not at a high enough level to continue, we also ignore it - var currentLevel = _clientState.LocalPlayer?.Level ?? 0; - if (currentLevel != 0 && - _questRegistry.TryGetQuest(currentQuest, out Quest? quest) - && quest.Info.Level > currentLevel) - return default; - - return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value)); - } - - public QuestWork? GetQuestEx(QuestId questId) - { - QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value); - return questWork != null ? *questWork : null; - } - - public bool IsReadyToAcceptQuest(ElementId elementId) - { - if (elementId is QuestId questId) - return IsReadyToAcceptQuest(questId); - else if (elementId is SatisfactionSupplyNpcId) - return true; - else - throw new ArgumentOutOfRangeException(nameof(elementId)); - } - - public bool IsReadyToAcceptQuest(QuestId questId) - { - _questRegistry.TryGetQuest(questId, out var quest); - if (quest is { Info.IsRepeatable: true }) - { - if (IsQuestAccepted(questId)) - return false; - } - else - { - if (IsQuestAcceptedOrComplete(questId)) - return false; - } - - if (IsQuestLocked(questId)) - return false; - - // if we're not at a high enough level to continue, we also ignore it - var currentLevel = _clientState.LocalPlayer?.Level ?? 0; - if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel) - return false; - - return true; - } - - public bool IsQuestAcceptedOrComplete(ElementId questElementId) - { - return IsQuestComplete(questElementId) || IsQuestAccepted(questElementId); - } - - public bool IsQuestAccepted(ElementId elementId) - { - if (elementId is QuestId questId) - return IsQuestAccepted(questId); - else if (elementId is SatisfactionSupplyNpcId) - return false; - else - throw new ArgumentOutOfRangeException(nameof(elementId)); - } - - public bool IsQuestAccepted(QuestId questId) - { - QuestManager* questManager = QuestManager.Instance(); - return questManager->IsQuestAccepted(questId.Value); - } - - public bool IsQuestComplete(ElementId elementId) - { - if (elementId is QuestId questId) - return IsQuestComplete(questId); - else if (elementId is SatisfactionSupplyNpcId) - return false; - else - throw new ArgumentOutOfRangeException(nameof(elementId)); - } - - [SuppressMessage("Performance", "CA1822")] - public bool IsQuestComplete(QuestId questId) - { - return QuestManager.IsQuestComplete(questId.Value); - } - - public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null) - { - if (elementId is QuestId questId) - return IsQuestLocked(questId, extraCompletedQuest); - else if (elementId is SatisfactionSupplyNpcId) - return false; - else - throw new ArgumentOutOfRangeException(nameof(elementId)); - } - - public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null) - { - var questInfo = (QuestInfo) _questData.GetQuestInfo(questId); - if (questInfo.QuestLocks.Count > 0) - { - var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); - if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests) - return true; - else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) - return true; - } - - if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany()) - return true; - - return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); - } - - private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest) - { - if (questInfo.PreviousQuests.Count == 0) - return true; - - var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); - if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All && - questInfo.PreviousQuests.Count == completedQuests) - return true; - else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) - return true; - else - return false; - } - - private static bool HasCompletedPreviousInstances(QuestInfo questInfo) - { - if (questInfo.PreviousInstanceContent.Count == 0) - return true; - - var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x)); - if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All && - questInfo.PreviousInstanceContent.Count == completedInstances) - return true; - else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0) - return true; - else - return false; - } - public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex) { subIndex = 0; @@ -383,7 +87,7 @@ internal sealed unsafe class GameFunctions public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation) { if (aetheryteLocation == EAetheryteLocation.IshgardFirmament) - return IsQuestComplete(new QuestId(3672)); + return _questFunctions.IsQuestComplete(new QuestId(3672)); return IsAetheryteUnlocked((uint)aetheryteLocation, out _); } @@ -431,7 +135,7 @@ internal sealed unsafe class GameFunctions if (_configuration.Advanced.NeverFly) return false; - if (IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted]) + if (_questFunctions.IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted]) { BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0); if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one @@ -718,61 +422,18 @@ internal sealed unsafe class GameFunctions contentFinderConditionId); } - public string? GetDialogueText(Quest currentQuest, string? excelSheetName, string key) + /// + /// Ensures characters like '-' are handled equally in both strings. + /// + public static bool GameStringEquals(string? a, string? b) { - if (excelSheetName == null) - { - var questRow = - _dataManager.GetExcelSheet()!.GetRow((uint)currentQuest.Id.Value + - 0x10000); - if (questRow == null) - { - _logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id); - return null; - } + if (a == null) + return b == null; - excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}"; - } + if (b == null) + return false; - var excelSheet = _dataManager.Excel.GetSheet(excelSheetName); - if (excelSheet == null) - { - _logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName); - return null; - } - - return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString(); - } - - public string? GetDialogueTextByRowId(string? excelSheet, uint rowId) - { - if (excelSheet == "GimmickYesNo") - { - var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); - return questRow?.Unknown0?.ToString(); - } - else if (excelSheet == "Warp") - { - var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); - return questRow?.Name?.ToString(); - } - else if (excelSheet is "Addon") - { - var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); - return questRow?.Text?.ToString(); - } - else if (excelSheet is "EventPathMove") - { - var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); - return questRow?.Unknown10?.ToString(); - } - else if (excelSheet is "ContentTalk" or null) - { - var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); - return questRow?.Text?.ToString(); - } - else - throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}"); + return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-'); } public bool IsOccupied() @@ -792,15 +453,28 @@ internal sealed unsafe class GameFunctions _condition[ConditionFlag.Jumping61] || _condition[ConditionFlag.Gathering42]; } + public bool IsOccupiedWithCustomDeliveryNpc(Quest? currentQuest) + { + // not a supply quest? + if (currentQuest is not { Info: SatisfactionSupplyInfo }) + return false; + + if (_targetManager.Target == null || _targetManager.Target.DataId != currentQuest.Info.IssuerDataId) + return false; + + if (!AgentSatisfactionSupply.Instance()->IsAgentActive()) + return false; + + var flags = _condition.AsReadOnlySet(); + return flags.Count == 2 && + flags.Contains(ConditionFlag.NormalConditions) && + flags.Contains(ConditionFlag.OccupiedInQuestEvent); + } + public bool IsLoadingScreenVisible() { return _gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) && LAddon.IsAddonReady(fade) && fade->IsVisible; } - - public GrandCompany GetGrandCompany() - { - return (GrandCompany)PlayerState.Instance()->GrandCompany; - } } diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs new file mode 100644 index 00000000..505546da --- /dev/null +++ b/Questionable/Functions/QuestFunctions.cs @@ -0,0 +1,346 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; +using FFXIVClientStructs.FFXIV.Client.Game; +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.GeneratedSheets; +using Questionable.Controller; +using Questionable.Data; +using Questionable.Model; +using Questionable.Model.Questing; +using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; +using Quest = Questionable.Model.Quest; + +namespace Questionable.Functions; + +internal sealed unsafe class QuestFunctions +{ + private readonly QuestRegistry _questRegistry; + private readonly QuestData _questData; + private readonly Configuration _configuration; + private readonly IDataManager _dataManager; + private readonly IClientState _clientState; + private readonly IGameGui _gameGui; + + public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui) + { + _questRegistry = questRegistry; + _questData = questData; + _configuration = configuration; + _dataManager = dataManager; + _clientState = clientState; + _gameGui = gameGui; + } + + public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuest() + { + var (currentQuest, sequence) = GetCurrentQuestInternal(); + PlayerState* playerState = PlayerState.Instance(); + + if (currentQuest == null || currentQuest.Value == 0) + { + if (_clientState.TerritoryType == 181) // Starting in Limsa + return (new QuestId(107), 0); + if (_clientState.TerritoryType == 182) // Starting in Ul'dah + return (new QuestId(594), 0); + if (_clientState.TerritoryType == 183) // Starting in Gridania + return (new QuestId(39), 0); + return default; + } + else if (currentQuest.Value == 681) + { + // if we have already picked up the GC quest, just return the progress for it + if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest)) + return (currentQuest, sequence); + + // The company you keep... + return _configuration.General.GrandCompany switch + { + GrandCompany.TwinAdder => (new QuestId(680), 0), + GrandCompany.Maelstrom => (new QuestId(681), 0), + _ => default + }; + } + else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace + { + ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch + { + GrandCompany.TwinAdder => 700, + GrandCompany.Maelstrom => 701, + _ => 0 + }; + + if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest)) + return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest)); + } + else if (currentQuest.Value == 801) + { + // skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate + QuestId broadeningHorizons = new QuestId(802); + if (IsQuestAccepted(broadeningHorizons)) + return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value)); + } + + return (currentQuest, sequence); + } + + public (ElementId? CurrentQuest, byte Sequence) GetCurrentQuestInternal() + { + var questManager = QuestManager.Instance(); + if (questManager != null) + { + // always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do + // side quests until the end of time. + var msqQuest = GetMainScenarioQuest(questManager); + if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest)) + return msqQuest; + + // Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item, + // do the MSQ; if a side quest is the first item do that side quest. + // + // If no quests are marked as 'priority', accepting a new quest adds it to the top of the list. + for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i) + { + ElementId currentQuest; + var trackedQuest = questManager->TrackedQuests[i]; + switch (trackedQuest.QuestType) + { + default: + continue; + + case 1: // normal quest + currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId); + break; + } + + if (_questRegistry.IsKnownQuest(currentQuest)) + return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value)); + } + + // if we know no quest of those currently in the to-do list, just do MSQ + return msqQuest; + } + + return default; + } + + private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager) + { + if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled + { + AgentInterface* questRedoHud = AgentModule.Instance()->GetAgentByInternalId(AgentId.QuestRedoHud); + if (questRedoHud != null && questRedoHud->IsAgentActive()) + { + // there's surely better ways to check this, but the one in the OOB Plugin was even less reliable + if (_gameGui.TryGetAddonByName("QuestRedoHud", out var addon) && + addon->AtkValuesCount == 4 && + // 0 seems to be active, + // 1 seems to be paused, + // 2 is unknown, but it happens e.g. before the quest 'Alzadaal's Legacy' + // 3 seems to be having /ng+ open while active, + // 4 seems to be when (a) suspending the chapter, or (b) having turned in a quest + addon->AtkValues[0].UInt is 0 or 2 or 3 or 4) + { + // redoHud+44 is chapter + // redoHud+46 is quest + ushort questId = MemoryHelper.Read((nint)questRedoHud + 46); + return (new QuestId(questId), QuestManager.GetQuestSequence(questId)); + } + } + } + + var scenarioTree = AgentScenarioTree.Instance(); + if (scenarioTree == null) + return default; + + if (scenarioTree->Data == null) + return default; + + QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest); + if (currentQuest.Value == 0) + return default; + + // if the MSQ is hidden, we generally ignore it + if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden) + return default; + + // it can sometimes happen (although this isn't reliably reproducible) that the quest returned here + // is one you've just completed. + if (!IsReadyToAcceptQuest(currentQuest)) + return default; + + // if we're not at a high enough level to continue, we also ignore it + var currentLevel = _clientState.LocalPlayer?.Level ?? 0; + if (currentLevel != 0 && + _questRegistry.TryGetQuest(currentQuest, out Quest? quest) + && quest.Info.Level > currentLevel) + return default; + + return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value)); + } + + public QuestWork? GetQuestEx(QuestId questId) + { + QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value); + return questWork != null ? *questWork : null; + } + + public bool IsReadyToAcceptQuest(ElementId elementId) + { + if (elementId is QuestId questId) + return IsReadyToAcceptQuest(questId); + else if (elementId is SatisfactionSupplyNpcId) + return true; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); + } + + public bool IsReadyToAcceptQuest(QuestId questId) + { + _questRegistry.TryGetQuest(questId, out var quest); + if (quest is { Info.IsRepeatable: true }) + { + if (IsQuestAccepted(questId)) + return false; + } + else + { + if (IsQuestAcceptedOrComplete(questId)) + return false; + } + + if (IsQuestLocked(questId)) + return false; + + // if we're not at a high enough level to continue, we also ignore it + var currentLevel = _clientState.LocalPlayer?.Level ?? 0; + if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel) + return false; + + return true; + } + + public bool IsQuestAcceptedOrComplete(ElementId elementId) + { + return IsQuestComplete(elementId) || IsQuestAccepted(elementId); + } + + public bool IsQuestAccepted(ElementId elementId) + { + if (elementId is QuestId questId) + return IsQuestAccepted(questId); + else if (elementId is SatisfactionSupplyNpcId) + return false; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); + } + + public bool IsQuestAccepted(QuestId questId) + { + QuestManager* questManager = QuestManager.Instance(); + return questManager->IsQuestAccepted(questId.Value); + } + + public bool IsQuestComplete(ElementId elementId) + { + if (elementId is QuestId questId) + return IsQuestComplete(questId); + else if (elementId is SatisfactionSupplyNpcId) + return false; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); + } + + [SuppressMessage("Performance", "CA1822")] + public bool IsQuestComplete(QuestId questId) + { + return QuestManager.IsQuestComplete(questId.Value); + } + + public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null) + { + if (elementId is QuestId questId) + return IsQuestLocked(questId, extraCompletedQuest); + else if (elementId is SatisfactionSupplyNpcId) + return false; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); + } + + public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null) + { + var questInfo = (QuestInfo)_questData.GetQuestInfo(questId); + if (questInfo.QuestLocks.Count > 0) + { + var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); + if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests) + return true; + else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) + return true; + } + + if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany()) + return true; + + return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); + } + + private bool HasCompletedPreviousQuests(QuestInfo questInfo, ElementId? extraCompletedQuest) + { + if (questInfo.PreviousQuests.Count == 0) + return true; + + var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); + if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All && + questInfo.PreviousQuests.Count == completedQuests) + return true; + else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) + return true; + else + return false; + } + + private static bool HasCompletedPreviousInstances(QuestInfo questInfo) + { + if (questInfo.PreviousInstanceContent.Count == 0) + return true; + + var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x)); + if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All && + questInfo.PreviousInstanceContent.Count == completedInstances) + return true; + else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0) + return true; + else + return false; + } + + public bool IsClassJobUnlocked(EClassJob classJob) + { + var classJobRow = _dataManager.GetExcelSheet()!.GetRow((uint)classJob)!; + var questId = (ushort)classJobRow.UnlockQuest.Row; + if (questId != 0) + return IsQuestComplete(new QuestId(questId)); + + PlayerState* playerState = PlayerState.Instance(); + return playerState != null && playerState->ClassJobLevels[classJobRow.ExpArrayIndex] > 0; + } + + public bool IsJobUnlocked(EClassJob classJob) + { + var classJobRow = _dataManager.GetExcelSheet()!.GetRow((uint)classJob)!; + return IsClassJobUnlocked((EClassJob)classJobRow.ClassJobParent.Row); + } + + public GrandCompany GetGrandCompany() + { + return (GrandCompany)PlayerState.Instance()->GrandCompany; + } +} diff --git a/Questionable/Model/StringOrRegex.cs b/Questionable/Model/StringOrRegex.cs new file mode 100644 index 00000000..638c6792 --- /dev/null +++ b/Questionable/Model/StringOrRegex.cs @@ -0,0 +1,43 @@ +using System; +using System.Text.RegularExpressions; +using Questionable.Functions; + +namespace Questionable.Model; + +internal sealed class StringOrRegex +{ + private readonly Regex? _regex; + private readonly string? _stringValue; + + public StringOrRegex(Regex? regex) + { + ArgumentNullException.ThrowIfNull(regex); + _regex = regex; + _stringValue = null; + } + + public StringOrRegex(string? str) + { + ArgumentNullException.ThrowIfNull(str); + _regex = null; + _stringValue = str; + } + + public bool IsMatch(string other) + { + if (_regex != null) + return _regex.IsMatch(other); + else + return GameFunctions.GameStringEquals(_stringValue, other); + } + + public string? GetString() + { + if (_stringValue == null) + throw new InvalidOperationException(); + + return _stringValue; + } + + public override string? ToString() => _regex?.ToString() ?? _stringValue; +} diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index bc466645..afd9f891 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -1,6 +1,6 @@  - 2.1 + 2.2 dist $(SolutionDir)=X:\ x64 diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 477a3723..766bb45d 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -17,6 +17,7 @@ using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Interactions; using Questionable.Data; using Questionable.External; +using Questionable.Functions; using Questionable.Validation; using Questionable.Validation.Validators; using Questionable.Windows; @@ -47,50 +48,58 @@ public sealed class QuestionablePlugin : IDalamudPlugin IContextMenu contextMenu) { ArgumentNullException.ThrowIfNull(pluginInterface); + ArgumentNullException.ThrowIfNull(chatGui); + try + { + ServiceCollection serviceCollection = new(); + serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace) + .ClearProviders() + .AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..])); + serviceCollection.AddSingleton(this); + serviceCollection.AddSingleton(pluginInterface); + serviceCollection.AddSingleton(clientState); + serviceCollection.AddSingleton(targetManager); + serviceCollection.AddSingleton(framework); + serviceCollection.AddSingleton(gameGui); + serviceCollection.AddSingleton(dataManager); + serviceCollection.AddSingleton(sigScanner); + serviceCollection.AddSingleton(objectTable); + serviceCollection.AddSingleton(pluginLog); + serviceCollection.AddSingleton(condition); + serviceCollection.AddSingleton(chatGui); + serviceCollection.AddSingleton(commandManager); + serviceCollection.AddSingleton(addonLifecycle); + serviceCollection.AddSingleton(keyState); + serviceCollection.AddSingleton(contextMenu); + serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable))); + serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration()); - ServiceCollection serviceCollection = new(); - serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace) - .ClearProviders() - .AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..])); - serviceCollection.AddSingleton(this); - serviceCollection.AddSingleton(pluginInterface); - serviceCollection.AddSingleton(clientState); - serviceCollection.AddSingleton(targetManager); - serviceCollection.AddSingleton(framework); - serviceCollection.AddSingleton(gameGui); - serviceCollection.AddSingleton(dataManager); - serviceCollection.AddSingleton(sigScanner); - serviceCollection.AddSingleton(objectTable); - serviceCollection.AddSingleton(pluginLog); - serviceCollection.AddSingleton(condition); - serviceCollection.AddSingleton(chatGui); - serviceCollection.AddSingleton(commandManager); - serviceCollection.AddSingleton(addonLifecycle); - serviceCollection.AddSingleton(keyState); - serviceCollection.AddSingleton(contextMenu); - serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable))); - serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration()); + AddBasicFunctionsAndData(serviceCollection); + AddTaskFactories(serviceCollection); + AddControllers(serviceCollection); + AddWindows(serviceCollection); + AddQuestValidators(serviceCollection); - AddBasicFunctionsAndData(serviceCollection); - AddTaskFactories(serviceCollection); - AddControllers(serviceCollection); - AddWindows(serviceCollection); - AddQuestValidators(serviceCollection); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - - _serviceProvider = serviceCollection.BuildServiceProvider(); - _serviceProvider.GetRequiredService().Reload(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + _serviceProvider = serviceCollection.BuildServiceProvider(); + Initialize(_serviceProvider); + } + catch (Exception) + { + chatGui.PrintError("Unable to load plugin, check /xllog for details", "Questionable"); + throw; + } } private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection) { + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -110,6 +119,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); // task factories serviceCollection.AddTaskWithFactory(); @@ -135,6 +145,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); serviceCollection .AddTaskWithFactory(); @@ -192,10 +203,19 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); } + private static void Initialize(IServiceProvider serviceProvider) + { + serviceProvider.GetRequiredService().Reload(); + serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); + } + public void Dispose() { _serviceProvider?.Dispose(); diff --git a/Questionable/Validation/EIssueType.cs b/Questionable/Validation/EIssueType.cs index a94f1f23..0f51ce6b 100644 --- a/Questionable/Validation/EIssueType.cs +++ b/Questionable/Validation/EIssueType.cs @@ -16,4 +16,5 @@ public enum EIssueType UnexpectedAcceptQuestStep, UnexpectedCompleteQuestStep, InvalidAethernetShortcut, + InvalidExcelRef, } diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs index ce521598..137e62b4 100644 --- a/Questionable/Validation/QuestValidator.cs +++ b/Questionable/Validation/QuestValidator.cs @@ -56,7 +56,7 @@ internal sealed class QuestValidator : LogLevel.Information; _logger.Log(level, "Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}", - issue.QuestId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description); + issue.ElementId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description); if (issue.Type == EIssueType.QuestDisabled && quest.Info.BeastTribe != EBeastTribe.None) { disabledTribeQuests.TryAdd(quest.Info.BeastTribe, 0); @@ -70,12 +70,12 @@ internal sealed class QuestValidator var disabledQuests = issues .Where(x => x.Type == EIssueType.QuestDisabled) - .Select(x => x.QuestId) + .Select(x => x.ElementId) .ToList(); _validationIssues = issues - .Where(x => !disabledQuests.Contains(x.QuestId) || x.Type == EIssueType.QuestDisabled) - .OrderBy(x => x.QuestId) + .Where(x => !disabledQuests.Contains(x.ElementId) || x.Type == EIssueType.QuestDisabled) + .OrderBy(x => x.ElementId) .ThenBy(x => x.Sequence) .ThenBy(x => x.Step) .ThenBy(x => x.Description) @@ -95,7 +95,7 @@ internal sealed class QuestValidator .OrderBy(x => x.Key) .Select(x => new ValidationIssue { - QuestId = null, + ElementId = null, Sequence = null, Step = null, BeastTribe = x.Key, diff --git a/Questionable/Validation/ValidationIssue.cs b/Questionable/Validation/ValidationIssue.cs index 31402620..654ac514 100644 --- a/Questionable/Validation/ValidationIssue.cs +++ b/Questionable/Validation/ValidationIssue.cs @@ -5,7 +5,7 @@ namespace Questionable.Validation; internal sealed record ValidationIssue { - public required ElementId? QuestId { get; init; } + public required ElementId? ElementId { get; init; } public required byte? Sequence { get; init; } public required int? Step { get; init; } public EBeastTribe BeastTribe { get; init; } = EBeastTribe.None; diff --git a/Questionable/Validation/Validators/AethernetShortcutValidator.cs b/Questionable/Validation/Validators/AethernetShortcutValidator.cs index 037ba3f4..13a00aa8 100644 --- a/Questionable/Validation/Validators/AethernetShortcutValidator.cs +++ b/Questionable/Validation/Validators/AethernetShortcutValidator.cs @@ -24,7 +24,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator .Cast(); } - private ValidationIssue? Validate(ElementId questElementId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut) + private ValidationIssue? Validate(ElementId elementId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut) { if (aethernetShortcut == null) return null; @@ -35,7 +35,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator { return new ValidationIssue { - QuestId = questElementId, + ElementId = elementId, Sequence = (byte)sequenceNo, Step = stepId, Type = EIssueType.InvalidAethernetShortcut, diff --git a/Questionable/Validation/Validators/BasicSequenceValidator.cs b/Questionable/Validation/Validators/BasicSequenceValidator.cs index 7fa73dfb..0c2bee27 100644 --- a/Questionable/Validation/Validators/BasicSequenceValidator.cs +++ b/Questionable/Validation/Validators/BasicSequenceValidator.cs @@ -18,7 +18,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = 0, Step = null, Type = EIssueType.MissingSequence0, @@ -37,7 +37,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)sequence.Sequence, Step = null, Type = EIssueType.InstantQuestWithMultipleSteps, @@ -73,7 +73,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator { return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)sequenceNo, Step = null, Type = EIssueType.MissingSequence, @@ -85,7 +85,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator { return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)sequenceNo, Step = null, Type = EIssueType.DuplicateSequence, diff --git a/Questionable/Validation/Validators/CompletionFlagsValidator.cs b/Questionable/Validation/Validators/CompletionFlagsValidator.cs index 9aa67760..e5c9fdc1 100644 --- a/Questionable/Validation/Validators/CompletionFlagsValidator.cs +++ b/Questionable/Validation/Validators/CompletionFlagsValidator.cs @@ -45,7 +45,7 @@ internal sealed class CompletionFlagsValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)sequence.Sequence, Step = i, Type = EIssueType.DuplicateCompletionFlags, diff --git a/Questionable/Validation/Validators/DialogueChoiceValidator.cs b/Questionable/Validation/Validators/DialogueChoiceValidator.cs new file mode 100644 index 00000000..ee6d6f5b --- /dev/null +++ b/Questionable/Validation/Validators/DialogueChoiceValidator.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Validation.Validators; + +internal sealed class DialogueChoiceValidator : IQuestValidator +{ + private readonly ExcelFunctions _excelFunctions; + + public DialogueChoiceValidator(ExcelFunctions excelFunctions) + { + _excelFunctions = excelFunctions; + } + + public IEnumerable Validate(Quest quest) + { + foreach (var x in quest.AllSteps()) + { + if (x.Step.DialogueChoices.Count == 0) + continue; + + foreach (var dialogueChoice in x.Step.DialogueChoices) + { + ExcelRef? prompt = dialogueChoice.Prompt; + if (prompt != null) + { + ValidationIssue? promptIssue = Validate(quest, x.Sequence, x.StepId, dialogueChoice.ExcelSheet, + prompt, "Prompt"); + if (promptIssue != null) + yield return promptIssue; + } + + ExcelRef? answer = dialogueChoice.Answer; + if (answer != null) + { + ValidationIssue? answerIssue = Validate(quest, x.Sequence, x.StepId, dialogueChoice.ExcelSheet, + answer, "Answer"); + if (answerIssue != null) + yield return answerIssue; + } + } + } + } + + private ValidationIssue? Validate(Quest quest, QuestSequence sequence, int stepId, string? excelSheet, + ExcelRef excelRef, string label) + { + if (excelRef.Type == ExcelRef.EType.Key) + { + if (_excelFunctions.GetRawDialogueText(quest, excelSheet, excelRef.AsKey()) == null) + { + return new ValidationIssue + { + ElementId = quest.Id, + Sequence = (byte)sequence.Sequence, + Step = stepId, + Type = EIssueType.InvalidExcelRef, + Severity = EIssueSeverity.Error, + Description = $"{label} invalid: {excelSheet} → {excelRef.AsKey()}", + }; + } + } + else if (excelRef.Type == ExcelRef.EType.RowId) + { + if (_excelFunctions.GetRawDialogueTextByRowId(excelSheet, excelRef.AsRowId()) == null) + { + return new ValidationIssue + { + ElementId = quest.Id, + Sequence = (byte)sequence.Sequence, + Step = stepId, + Type = EIssueType.InvalidExcelRef, + Severity = EIssueSeverity.Error, + Description = $"{label} invalid: {excelSheet} → {excelRef.AsRowId()}", + }; + } + } + + return null; + } +} diff --git a/Questionable/Validation/Validators/JsonSchemaValidator.cs b/Questionable/Validation/Validators/JsonSchemaValidator.cs index b68fa967..d6bb483d 100644 --- a/Questionable/Validation/Validators/JsonSchemaValidator.cs +++ b/Questionable/Validation/Validators/JsonSchemaValidator.cs @@ -36,7 +36,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = null, Step = null, Type = EIssueType.InvalidJsonSchema, @@ -47,7 +47,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator } } - public void Enqueue(ElementId questElementId, JsonNode questNode) => _questNodes[questElementId] = questNode; + public void Enqueue(ElementId elementId, JsonNode questNode) => _questNodes[elementId] = questNode; public void Reset() => _questNodes.Clear(); } diff --git a/Questionable/Validation/Validators/NextQuestValidator.cs b/Questionable/Validation/Validators/NextQuestValidator.cs index c2e899a4..d08bea61 100644 --- a/Questionable/Validation/Validators/NextQuestValidator.cs +++ b/Questionable/Validation/Validators/NextQuestValidator.cs @@ -12,7 +12,7 @@ internal sealed class NextQuestValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)invalidNextQuest.Sequence.Sequence, Step = invalidNextQuest.StepId, Type = EIssueType.InvalidNextQuestId, diff --git a/Questionable/Validation/Validators/QuestDisabledValidator.cs b/Questionable/Validation/Validators/QuestDisabledValidator.cs index 60539d5b..8ff522eb 100644 --- a/Questionable/Validation/Validators/QuestDisabledValidator.cs +++ b/Questionable/Validation/Validators/QuestDisabledValidator.cs @@ -11,7 +11,7 @@ internal sealed class QuestDisabledValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = null, Step = null, Type = EIssueType.QuestDisabled, diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs index 5ffe6e0f..60f67b12 100644 --- a/Questionable/Validation/Validators/UniqueStartStopValidator.cs +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -21,7 +21,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)accept.Sequence.Sequence, Step = accept.StepId, Type = EIssueType.UnexpectedAcceptQuestStep, @@ -35,7 +35,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = 0, Step = null, Type = EIssueType.MissingQuestAccept, @@ -53,7 +53,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = (byte)complete.Sequence.Sequence, Step = complete.StepId, Type = EIssueType.UnexpectedCompleteQuestStep, @@ -67,7 +67,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.Id, + ElementId = quest.Id, Sequence = 255, Step = null, Type = EIssueType.MissingQuestComplete, diff --git a/Questionable/Windows/JournalProgressWindow.cs b/Questionable/Windows/JournalProgressWindow.cs index bdad03da..2d19c1be 100644 --- a/Questionable/Windows/JournalProgressWindow.cs +++ b/Questionable/Windows/JournalProgressWindow.cs @@ -12,6 +12,7 @@ using ImGuiNET; using LLib.ImGui; using Questionable.Controller; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Windows.QuestComponents; @@ -21,7 +22,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable { private readonly JournalData _journalData; private readonly QuestRegistry _questRegistry; - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly UiUtils _uiUtils; private readonly QuestTooltipComponent _questTooltipComponent; private readonly IDalamudPluginInterface _pluginInterface; @@ -37,7 +38,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable public JournalProgressWindow(JournalData journalData, QuestRegistry questRegistry, - GameFunctions gameFunctions, + QuestFunctions questFunctions, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface, @@ -47,7 +48,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable { _journalData = journalData; _questRegistry = questRegistry; - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _uiUtils = uiUtils; _questTooltipComponent = questTooltipComponent; _pluginInterface = pluginInterface; @@ -327,7 +328,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable { int available = genre.Quests.Count(x => _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled); - int completed = genre.Quests.Count(x => _gameFunctions.IsQuestComplete(x.QuestId)); + int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId)); _genreCounts[genre] = (available, completed); } diff --git a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs index 39e9ec97..a94f253b 100644 --- a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs +++ b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Common.Math; using Questionable.Data; +using Questionable.Functions; using Questionable.Model.Questing; namespace Questionable.Windows.QuestComponents; @@ -18,26 +19,26 @@ internal sealed class ARealmRebornComponent private static readonly QuestId[] RequiredAllianceRaidQuests = [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)]; - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly QuestData _questData; private readonly TerritoryData _territoryData; private readonly UiUtils _uiUtils; - public ARealmRebornComponent(GameFunctions gameFunctions, QuestData questData, TerritoryData territoryData, + public ARealmRebornComponent(QuestFunctions questFunctions, QuestData questData, TerritoryData territoryData, UiUtils uiUtils) { - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _questData = questData; _territoryData = territoryData; _uiUtils = uiUtils; } - public bool ShouldDraw => !_gameFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) && - _gameFunctions.IsQuestComplete(TheUltimateWeapon); + public bool ShouldDraw => !_questFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) && + _questFunctions.IsQuestComplete(TheUltimateWeapon); public void Draw() { - if (!_gameFunctions.IsQuestAcceptedOrComplete(GoodIntentions)) + if (!_questFunctions.IsQuestAcceptedOrComplete(GoodIntentions)) DrawPrimals(); DrawAllianceRaids(); @@ -63,7 +64,7 @@ internal sealed class ARealmRebornComponent private void DrawAllianceRaids() { - bool complete = _gameFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last()); + bool complete = _questFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last()); bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete); if (complete || !hover) return; diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs index eb48f4c4..3c1a92ff 100644 --- a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -13,6 +13,7 @@ using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using ImGuiNET; using Questionable.Controller; using Questionable.Controller.Steps.Shared; +using Questionable.Functions; using Questionable.Model.Questing; namespace Questionable.Windows.QuestComponents; @@ -24,6 +25,7 @@ internal sealed class ActiveQuestComponent private readonly CombatController _combatController; private readonly GatheringController _gatheringController; private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly ICommandManager _commandManager; private readonly IDalamudPluginInterface _pluginInterface; private readonly Configuration _configuration; @@ -36,6 +38,7 @@ internal sealed class ActiveQuestComponent CombatController combatController, GatheringController gatheringController, GameFunctions gameFunctions, + QuestFunctions questFunctions, ICommandManager commandManager, IDalamudPluginInterface pluginInterface, Configuration configuration, @@ -47,6 +50,7 @@ internal sealed class ActiveQuestComponent _combatController = combatController; _gatheringController = gatheringController; _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _commandManager = commandManager; _pluginInterface = pluginInterface; _configuration = configuration; @@ -116,6 +120,12 @@ internal sealed class ActiveQuestComponent ImGui.TextUnformatted( $"Simulated Quest: {Shorten(currentQuest.Quest.Info.Name)} / {currentQuest.Sequence} / {currentQuest.Step}"); } + else if (currentQuestType == QuestController.ECurrentQuestType.Gathering) + { + using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGold); + ImGui.TextUnformatted( + $"Gathering: {Shorten(currentQuest.Quest.Info.Name)} / {currentQuest.Sequence} / {currentQuest.Step}"); + } else { var startedQuest = _questController.StartedQuest; @@ -154,7 +164,7 @@ internal sealed class ActiveQuestComponent if (currentQuest.Quest.Id is not QuestId questId) return null; - var questWork = _gameFunctions.GetQuestEx(questId); + var questWork = _questFunctions.GetQuestEx(questId); if (questWork != null) { Vector4 color; diff --git a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs index b824f1af..dfcae518 100644 --- a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs +++ b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs @@ -15,6 +15,7 @@ using ImGuiNET; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Questing; @@ -26,6 +27,7 @@ internal sealed class CreationUtilsComponent { private readonly MovementController _movementController; private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly TerritoryData _territoryData; private readonly QuestData _questData; private readonly QuestSelectionWindow _questSelectionWindow; @@ -35,13 +37,22 @@ internal sealed class CreationUtilsComponent private readonly IGameGui _gameGui; private readonly ILogger _logger; - public CreationUtilsComponent(MovementController movementController, GameFunctions gameFunctions, - TerritoryData territoryData, QuestData questData, QuestSelectionWindow questSelectionWindow, - IClientState clientState, ITargetManager targetManager, ICondition condition, IGameGui gameGui, + public CreationUtilsComponent( + MovementController movementController, + GameFunctions gameFunctions, + QuestFunctions questFunctions, + TerritoryData territoryData, + QuestData questData, + QuestSelectionWindow questSelectionWindow, + IClientState clientState, + ITargetManager targetManager, + ICondition condition, + IGameGui gameGui, ILogger logger) { _movementController = movementController; _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _territoryData = territoryData; _questData = questData; _questSelectionWindow = questSelectionWindow; @@ -65,7 +76,7 @@ internal sealed class CreationUtilsComponent ImGui.Text(SeIconChar.BotanistSprout.ToIconString()); } - var q = _gameFunctions.GetCurrentQuest(); + var q = _questFunctions.GetCurrentQuest(); ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}"); #if false diff --git a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs index 2a19a858..df2a243b 100644 --- a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs +++ b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; using Questionable.Controller; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -15,20 +16,20 @@ internal sealed class QuestTooltipComponent private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; private readonly TerritoryData _territoryData; - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly UiUtils _uiUtils; public QuestTooltipComponent( QuestRegistry questRegistry, QuestData questData, TerritoryData territoryData, - GameFunctions gameFunctions, + QuestFunctions questFunctions, UiUtils uiUtils) { _questRegistry = questRegistry; _questData = questData; _territoryData = territoryData; - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _uiUtils = uiUtils; } @@ -161,7 +162,7 @@ internal sealed class QuestTooltipComponent _ => "None", }; - GrandCompany currentGrandCompany = _gameFunctions.GetGrandCompany(); + GrandCompany currentGrandCompany = ~_questFunctions.GetGrandCompany(); _uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany); } diff --git a/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs b/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs index 7cdfb388..c1896861 100644 --- a/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs +++ b/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs @@ -12,6 +12,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; using Questionable.Controller; using Questionable.External; +using Questionable.Functions; namespace Questionable.Windows.QuestComponents; diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 96d0cfd7..ce6cb0fd 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -12,12 +12,12 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; using LLib.GameUI; using LLib.ImGui; using Questionable.Controller; using Questionable.Data; +using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.QuestComponents; @@ -30,7 +30,7 @@ internal sealed class QuestSelectionWindow : LWindow private readonly QuestData _questData; private readonly IGameGui _gameGui; private readonly IChatGui _chatGui; - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly QuestController _questController; private readonly QuestRegistry _questRegistry; private readonly IDalamudPluginInterface _pluginInterface; @@ -43,16 +43,24 @@ internal sealed class QuestSelectionWindow : LWindow private List _offeredQuests = []; private bool _onlyAvailableQuests = true; - public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions, - QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface, - TerritoryData territoryData, IClientState clientState, UiUtils uiUtils, + public QuestSelectionWindow( + QuestData questData, + IGameGui gameGui, + IChatGui chatGui, + QuestFunctions questFunctions, + QuestController questController, + QuestRegistry questRegistry, + IDalamudPluginInterface pluginInterface, + TerritoryData territoryData, + IClientState clientState, + UiUtils uiUtils, QuestTooltipComponent questTooltipComponent) : base($"Quest Selection{WindowId}") { _questData = questData; _gameGui = gameGui; _chatGui = chatGui; - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _questController = questController; _questRegistry = questRegistry; _pluginInterface = pluginInterface; @@ -82,7 +90,7 @@ internal sealed class QuestSelectionWindow : LWindow { var answers = GameUiController.GetChoices(addonSelectIconString); _offeredQuests = _quests - .Where(x => answers.Any(y => GameUiController.GameStringEquals(x.Name, y))) + .Where(x => answers.Any(y => GameFunctions.GameStringEquals(x.Name, y))) .ToList(); } else @@ -216,9 +224,9 @@ internal sealed class QuestSelectionWindow : LWindow if (knownQuest != null && knownQuest.FindSequence(0)?.LastStep()?.InteractionType == EInteractionType.AcceptQuest && - !_gameFunctions.IsQuestAccepted(quest.QuestId) && - !_gameFunctions.IsQuestLocked(quest.QuestId) && - (quest.IsRepeatable || !_gameFunctions.IsQuestAcceptedOrComplete(quest.QuestId))) + !_questFunctions.IsQuestAccepted(quest.QuestId) && + !_questFunctions.IsQuestLocked(quest.QuestId) && + (quest.IsRepeatable || !_questFunctions.IsQuestAcceptedOrComplete(quest.QuestId))) { ImGui.BeginDisabled(_questController.NextQuest != null || _questController.SimulatedQuest != null); diff --git a/Questionable/Windows/QuestValidationWindow.cs b/Questionable/Windows/QuestValidationWindow.cs index 15eff981..8f3eb3ac 100644 --- a/Questionable/Windows/QuestValidationWindow.cs +++ b/Questionable/Windows/QuestValidationWindow.cs @@ -56,11 +56,11 @@ internal sealed class QuestValidationWindow : LWindow ImGui.TableNextRow(); if (ImGui.TableNextColumn()) - ImGui.TextUnformatted(validationIssue.QuestId?.ToString() ?? string.Empty); + ImGui.TextUnformatted(validationIssue.ElementId?.ToString() ?? string.Empty); if (ImGui.TableNextColumn()) - ImGui.TextUnformatted(validationIssue.QuestId != null - ? _questData.GetQuestInfo(validationIssue.QuestId).Name + ImGui.TextUnformatted(validationIssue.ElementId != null + ? _questData.GetQuestInfo(validationIssue.ElementId).Name : validationIssue.BeastTribe.ToString()); if (ImGui.TableNextColumn()) diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 30546bc4..dab69b8b 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -63,6 +63,11 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration); + public override void PreOpenCheck() + { + IsOpen |= _questController.IsRunning; + } + public override bool DrawConditions() { if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null || _clientState.IsPvPExcludingDen) diff --git a/Questionable/Windows/UiUtils.cs b/Questionable/Windows/UiUtils.cs index d6bdba36..d04d5dac 100644 --- a/Questionable/Windows/UiUtils.cs +++ b/Questionable/Windows/UiUtils.cs @@ -4,28 +4,29 @@ using Dalamud.Interface.Colors; using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; +using Questionable.Functions; using Questionable.Model.Questing; namespace Questionable.Windows; internal sealed class UiUtils { - private readonly GameFunctions _gameFunctions; + private readonly QuestFunctions _questFunctions; private readonly IDalamudPluginInterface _pluginInterface; - public UiUtils(GameFunctions gameFunctions, IDalamudPluginInterface pluginInterface) + public UiUtils(QuestFunctions questFunctions, IDalamudPluginInterface pluginInterface) { - _gameFunctions = gameFunctions; + _questFunctions = questFunctions; _pluginInterface = pluginInterface; } - public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId questElementId) + public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId elementId) { - if (_gameFunctions.IsQuestAccepted(questElementId)) + if (_questFunctions.IsQuestAccepted(elementId)) return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active"); - else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId)) + else if (_questFunctions.IsQuestAcceptedOrComplete(elementId)) return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); - else if (_gameFunctions.IsQuestLocked(questElementId)) + else if (_questFunctions.IsQuestLocked(elementId)) return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked"); else return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");