From 410d891f7fd7dae22fb53a84acdf009ac2ca13b4 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 12 Jun 2024 18:03:48 +0200 Subject: [PATCH] Quest automation + various fixes --- .../4363_Deeper into the Maze.json | 24 ++++- .../4364_The Medial Circuit.json | 30 +++++- .../B-Garlemald/4383_A Frosty Reception.json | 10 +- .../Endwalker/MSQ/G-UltimaThule/4460_x.json | 2 +- .../MSQ/G-5.1/3673_Shaken Resolve.json | 3 +- .../MSQ/G-5.1/3675_A Welcome Guest.json | 33 +++++- .../MSQ/G-5.1/3676_Good for the Soul.json | 59 +++++++++- .../MSQ/G-5.1/3678_A Notable Absence.json | 28 ++++- .../MSQ/G-5.1/3680_Finding Good Help.json | 2 +- ...3682_Vows of Virtue, Deeds of Cruelty.json | 12 ++- .../MSQ/H-5.2/3762_The Way Home.json | 3 +- .../MSQ/H-5.2/3763_Seeking Council.json | 12 ++- .../MSQ/H-5.2/3764_Facing the Truth.json | 54 ++++++++-- .../MSQ/H-5.2/3765_A Sleep Disturbed.json | 26 ++++- .../MSQ/H-5.2/3767_Deep Designs.json | 13 +++ .../MSQ/H-5.2/3768_A Whale's Tale.json | 33 +++++- .../MSQ/H-5.2/3769_Beneath the Surface.json | 17 ++- .../H-5.2/3770_Echoes of a Fallen Star.json | 16 ++- .../I-5.3/3771_In the Name of the Light.json | 47 +++++--- .../MSQ/I-5.3/3772_Heroic Dreams.json | 29 ++++- .../MSQ/I-5.3/3774_Food for the Soul.json | 3 +- .../MSQ/I-5.3/3775_Faded Memories.json | 9 +- .../MSQ/I-5.3/3778_Hope's Confluence.json | 11 +- .../MSQ/I-5.3/3779_Nothing Unsaid.json | 7 +- .../MSQ/I-5.3/3781_Unto the Morrow.json | 10 +- .../I-5.3/3782_Reflections in Crystal.json | 18 +++- .../MSQ/J-5.4/4008_The Wisdom of Allag.json | 3 +- .../MSQ/J-5.4/4009_Reviving the Legacy.json | 3 +- .../MSQ/J-5.4/4010_Forget Us Not.json | 9 +- .../MSQ/J-5.4/4014_On Rough Seas.json | 9 +- .../J-5.4/4015_The Great Ship Vylbrand.json | 20 ++++ .../MSQ/K-5.5/4060_Righteous Indignation.json | 39 ++++++- .../MSQ/K-5.5/4063_When the Dust Settles.json | 7 ++ QuestPaths/quest-v1.json | 51 ++++----- Questionable/Configuration.cs | 16 ++- Questionable/Controller/GameUiController.cs | 101 +++++++++--------- Questionable/Controller/MovementController.cs | 4 +- Questionable/Controller/QuestController.cs | 35 +++++- Questionable/Controller/QuestRegistry.cs | 1 + .../Steps/BaseFactory/AethernetShortcut.cs | 8 +- .../Controller/Steps/BaseFactory/WaitAtEnd.cs | 34 +++++- .../Controller/Steps/BaseTasks/UnmountTask.cs | 12 ++- .../Steps/InteractionFactory/UseItem.cs | 54 ++++++---- Questionable/DalamudInitializer.cs | 16 ++- Questionable/Data/AetheryteData.cs | 25 +++++ Questionable/GameFunctions.cs | 56 +++++++--- Questionable/Model/Quest.cs | 1 + .../Converter/DialogueChoiceTypeConverter.cs | 2 - .../Model/V1/Converter/ExcelRefConverter.cs | 30 ++++++ Questionable/Model/V1/DialogueChoice.cs | 14 ++- Questionable/Model/V1/EDialogChoiceType.cs | 2 - Questionable/Model/V1/ExcelRef.cs | 48 +++++++++ Questionable/QuestionablePlugin.cs | 1 + Questionable/Windows/ConfigWindow.cs | 88 +++++++++++++++ Questionable/Windows/DebugWindow.cs | 47 +++++--- 55 files changed, 1016 insertions(+), 231 deletions(-) create mode 100644 Questionable/Model/V1/Converter/ExcelRefConverter.cs create mode 100644 Questionable/Model/V1/ExcelRef.cs create mode 100644 Questionable/Windows/ConfigWindow.cs diff --git a/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json b/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json index 54e4f6b63..abd823a3c 100644 --- a/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json +++ b/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json @@ -68,7 +68,15 @@ "Z": -464.7746 }, "TerritoryId": 956, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "DataId": 1038716, @@ -78,7 +86,15 @@ "Z": -458.2132 }, "TerritoryId": 956, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] } ] }, @@ -108,8 +124,8 @@ "Z": -519.18823 }, "TerritoryId": 956, - "InteractionType": "SinglePlayerDuty", - "Comment": "Duty - Shoot Large Green Bird" + "InteractionType": "WaitForManualProgress", + "Comment": "Shoot Large Green Bird" } ] }, diff --git a/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json b/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json index 685f19b2e..b229d25a0 100644 --- a/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json +++ b/QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json @@ -126,7 +126,15 @@ "Z": -108.537415 }, "TerritoryId": 956, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "DataId": 1038707, @@ -136,7 +144,15 @@ "Z": -150.04199 }, "TerritoryId": 956, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 1038708, @@ -146,7 +162,15 @@ "Z": -130.11371 }, "TerritoryId": 956, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] } ] }, diff --git a/QuestPaths/Endwalker/MSQ/B-Garlemald/4383_A Frosty Reception.json b/QuestPaths/Endwalker/MSQ/B-Garlemald/4383_A Frosty Reception.json index 1d54df363..bd8d91c4c 100644 --- a/QuestPaths/Endwalker/MSQ/B-Garlemald/4383_A Frosty Reception.json +++ b/QuestPaths/Endwalker/MSQ/B-Garlemald/4383_A Frosty Reception.json @@ -35,13 +35,13 @@ "Comment": "A Frosty Reception", "DialogueChoices": [ { - "Type": "ContentTalkList", - "Prompt": "264", - "Answer": "267" + "Type": "List", + "Prompt": 264, + "Answer": 267 }, { - "Type": "ContentTalkYesNo", - "Prompt": "268", + "Type": "YesNo", + "Prompt": 268, "Yes": true } ] diff --git a/QuestPaths/Endwalker/MSQ/G-UltimaThule/4460_x.json b/QuestPaths/Endwalker/MSQ/G-UltimaThule/4460_x.json index b320abf47..fa4dedaed 100644 --- a/QuestPaths/Endwalker/MSQ/G-UltimaThule/4460_x.json +++ b/QuestPaths/Endwalker/MSQ/G-UltimaThule/4460_x.json @@ -97,7 +97,7 @@ }, "TerritoryId": 960, "InteractionType": "WaitForManualProgress", - "Comment": "Duty - Find Errant Omicron" + "Comment": "Find Errant Omicron" } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/G-5.1/3673_Shaken Resolve.json b/QuestPaths/Shadowbringers/MSQ/G-5.1/3673_Shaken Resolve.json index 4ff0ea548..110e85022 100644 --- a/QuestPaths/Shadowbringers/MSQ/G-5.1/3673_Shaken Resolve.json +++ b/QuestPaths/Shadowbringers/MSQ/G-5.1/3673_Shaken Resolve.json @@ -113,7 +113,8 @@ "AethernetShortcut": [ "[Crystarium] Aetheryte Plaza", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 }, { "DataId": 1032121, diff --git a/QuestPaths/Shadowbringers/MSQ/G-5.1/3675_A Welcome Guest.json b/QuestPaths/Shadowbringers/MSQ/G-5.1/3675_A Welcome Guest.json index 7a3fa5146..77d97cfdc 100644 --- a/QuestPaths/Shadowbringers/MSQ/G-5.1/3675_A Welcome Guest.json +++ b/QuestPaths/Shadowbringers/MSQ/G-5.1/3675_A Welcome Guest.json @@ -33,7 +33,8 @@ "AethernetShortcut": [ "[Crystarium] Aetheryte Plaza", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 } ] }, @@ -82,7 +83,15 @@ "TerritoryId": 815, "InteractionType": "UseItem", "ItemId": 2002904, - "$.1": "QuestVariables if done first: 1 32 0 0 0 64" + "$.1": "QuestVariables if done first: 1 32 0 0 0 64", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 1027909, @@ -94,7 +103,15 @@ "TerritoryId": 815, "InteractionType": "UseItem", "ItemId": 2002904, - "$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96" + "$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] }, { "DataId": 1027939, @@ -105,7 +122,15 @@ }, "TerritoryId": 815, "InteractionType": "UseItem", - "ItemId": 2002904 + "ItemId": 2002904, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/G-5.1/3676_Good for the Soul.json b/QuestPaths/Shadowbringers/MSQ/G-5.1/3676_Good for the Soul.json index 89b81b1d7..f84d89b9d 100644 --- a/QuestPaths/Shadowbringers/MSQ/G-5.1/3676_Good for the Soul.json +++ b/QuestPaths/Shadowbringers/MSQ/G-5.1/3676_Good for the Soul.json @@ -28,7 +28,14 @@ "Z": 305.19568 }, "TerritoryId": 815, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMG104_03676_Q2_000_100", + "Answer": "TEXT_LUCKMG104_03676_A1_000_100" + } + ] } ] }, @@ -61,7 +68,16 @@ "StopDistance": 1, "TerritoryId": 815, "InteractionType": "Interact", - "$.1": "QuestVariables if done first: 16 16 16 0 0 32" + "$.1": "QuestVariables if done first: 16 16 16 0 0 32", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] }, { "DataId": 2010810, @@ -72,7 +88,15 @@ }, "TerritoryId": 815, "InteractionType": "Interact", - "$.1": "QuestVariables if done after [1]: 33 16 32 0 0 96" + "$.1": "QuestVariables if done after [1]: 33 16 32 0 0 96", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 2010809, @@ -83,13 +107,31 @@ }, "TerritoryId": 815, "InteractionType": "Interact", - "Comment": "Combat not necessary to progress quest" + "Comment": "Combat not necessary to progress quest", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] } ] }, { "Sequence": 4, "Steps": [ + { + "Position": { + "X": 47.593674, + "Y": 42.681213, + "Z": -511.2799 + }, + "TerritoryId": 815, + "InteractionType": "WalkTo", + "Comment": "Should be far enough to reset combat" + }, { "DataId": 1031732, "Position": { @@ -99,7 +141,14 @@ }, "TerritoryId": 815, "InteractionType": "Interact", - "AetheryteShortcut": "Amh Araeng - Inn at Journey's Head" + "AetheryteShortcut": "Amh Araeng - Inn at Journey's Head", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMG104_03676_Q3_000_200", + "Answer": "TEXT_LUCKMG104_03676_A1_000_200" + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/G-5.1/3678_A Notable Absence.json b/QuestPaths/Shadowbringers/MSQ/G-5.1/3678_A Notable Absence.json index 02b98cb58..887371817 100644 --- a/QuestPaths/Shadowbringers/MSQ/G-5.1/3678_A Notable Absence.json +++ b/QuestPaths/Shadowbringers/MSQ/G-5.1/3678_A Notable Absence.json @@ -29,7 +29,15 @@ }, "TerritoryId": 820, "InteractionType": "Interact", - "$.1": "QuestValues if done first: 16 1 16 0 0 128" + "$.1": "QuestValues if done first: 16 1 16 0 0 128", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "DataId": 1027602, @@ -44,7 +52,15 @@ "[Eulmore] Aetheryte Plaza", "[Eulmore] Southeast Derelicts" ], - "$.1": "QuestValues if done after [1]: 32 17 16 16 0 160" + "$.1": "QuestValues if done after [1]: 32 17 16 16 0 160", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] }, { "DataId": 1029990, @@ -58,6 +74,14 @@ "AethernetShortcut": [ "[Eulmore] Southeast Derelicts", "[Eulmore] The Glory Gate" + ], + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 ] } ] diff --git a/QuestPaths/Shadowbringers/MSQ/G-5.1/3680_Finding Good Help.json b/QuestPaths/Shadowbringers/MSQ/G-5.1/3680_Finding Good Help.json index 5897ea956..8155004d6 100644 --- a/QuestPaths/Shadowbringers/MSQ/G-5.1/3680_Finding Good Help.json +++ b/QuestPaths/Shadowbringers/MSQ/G-5.1/3680_Finding Good Help.json @@ -44,7 +44,7 @@ "Z": -161.45575 }, "TerritoryId": 814, - "InteractionType": "SinglePlayerDuty", + "InteractionType": "WaitForManualProgress", "Comment": "Help Master Chai dodge enemies" } ] diff --git a/QuestPaths/Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json b/QuestPaths/Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json index a6a376446..587cb7446 100644 --- a/QuestPaths/Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json +++ b/QuestPaths/Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json @@ -36,7 +36,8 @@ "AethernetShortcut": [ "[Crystarium] Aetheryte Plaza", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 }, { "DataId": 1032121, @@ -74,7 +75,14 @@ }, "TerritoryId": 351, "InteractionType": "SinglePlayerDuty", - "Comment": "Estinien vs. Arch Ultima" + "Comment": "Estinien vs. Arch Ultima", + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_LUCKMG110_03682_Q1_100_125", + "Yes": true + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3762_The Way Home.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3762_The Way Home.json index e60b9f087..f9fdc86ec 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3762_The Way Home.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3762_The Way Home.json @@ -33,7 +33,8 @@ "AethernetShortcut": [ "[Crystarium] Aetheryte Plaza", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 }, { "DataId": 1032121, diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3763_Seeking Council.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3763_Seeking Council.json index cee81551d..fc024ec2e 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3763_Seeking Council.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3763_Seeking Council.json @@ -28,7 +28,8 @@ "Z": 14.22064 }, "TerritoryId": 844, - "InteractionType": "Interact" + "InteractionType": "Interact", + "TargetTerritoryId": 819 }, { "DataId": 1030370, @@ -38,7 +39,14 @@ "Z": 13.321045 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMH103_03763_Q1_000_500", + "Answer": "TEXT_LUCKMH103_03763_A1_000_500" + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3764_Facing the Truth.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3764_Facing the Truth.json index 166a609de..d6ce8b329 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3764_Facing the Truth.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3764_Facing the Truth.json @@ -33,7 +33,8 @@ "AethernetShortcut": [ "[Crystarium] Aetheryte Plaza", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 }, { "DataId": 1031722, @@ -58,7 +59,8 @@ "Z": 14.206055 }, "TerritoryId": 844, - "InteractionType": "Interact" + "InteractionType": "Interact", + "TargetTerritoryId": 819 }, { "DataId": 1027248, @@ -71,7 +73,15 @@ "InteractionType": "Interact", "Comment": "Chessamile", "$.0": "[1]", - "$.1": "QuestVariables if done first: 1 0 0 0 0 64" + "$.1": "QuestVariables if done first: 1 0 0 0 0 64", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 1027224, @@ -84,7 +94,15 @@ "InteractionType": "Interact", "Comment": "Bragi", "$.0": "[2]", - "$.1": "QuestVariables if done after [1]: 2 0 0 0 0 192" + "$.1": "QuestVariables if done after [1]: 2 0 0 0 0 192", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "DataId": 1027322, @@ -98,7 +116,15 @@ "InteractionType": "Interact", "Comment": "Glynard", "$.0": "[3]", - "$.1": "QuestVariables if done after [1, 2]: 3 0 0 0 0 200" + "$.1": "QuestVariables if done after [1, 2]: 3 0 0 0 0 200", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 8 + ] }, { "DataId": 1027232, @@ -115,7 +141,15 @@ "[Crystarium] The Crystalline Mean" ], "$.0": "[4]", - "$.1": "QuestVariables if done after [1, 2, 3]: 4 0 0 0 0 216" + "$.1": "QuestVariables if done after [1, 2, 3]: 4 0 0 0 0 216", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 16 + ] }, { "DataId": 1027226, @@ -130,6 +164,14 @@ "AethernetShortcut": [ "[Crystarium] The Crystalline Mean", "[Crystarium] The Cabinet of Curiosity" + ], + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 ] } ] diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json index a97b039ab..5df9e21f7 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json @@ -47,7 +47,31 @@ "TerritoryId": 817, "InteractionType": "SinglePlayerDuty", "Fly": true, - "Comment": "Duty - A Sleep Disturbed (Opo-Opo, Wolf, Serpent)" + "Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)", + "$": "The dialogue choices and data ids here are recycled", + "DialogueChoices": [ + { + "Type": "YesNo", + "DataId": 2011009, + "ExcelSheet": "GimmickYesNo", + "Prompt": 138, + "Yes": true + }, + { + "Type": "YesNo", + "DataId": 2011006, + "ExcelSheet": "GimmickYesNo", + "Prompt": 139, + "Yes": true + }, + { + "Type": "YesNo", + "DataId": 2011007, + "ExcelSheet": "GimmickYesNo", + "Prompt": 142, + "Yes": true + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3767_Deep Designs.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3767_Deep Designs.json index 0cef105f2..6e2d454b7 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3767_Deep Designs.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3767_Deep Designs.json @@ -36,6 +36,19 @@ { "Sequence": 2, "Steps": [ + { + "Position": { + "X": -475.38354, + "Y": 400.55338, + "Z": -779.4299 + }, + "TerritoryId": 818, + "InteractionType": "WalkTo", + "Fly": true, + "SkipIf": [ + "FlyingLocked" + ] + }, { "Position": { "X": -423.6145, diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3768_A Whale's Tale.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3768_A Whale's Tale.json index e58e654e0..c94d95445 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3768_A Whale's Tale.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3768_A Whale's Tale.json @@ -28,7 +28,7 @@ }, "TerritoryId": 813, "InteractionType": "WalkTo", - "AetheryteShortcut": "Lakeland - Ostall Imperative", + "AetheryteShortcut": "Lakeland - Fort Jobb", "Fly": true }, { @@ -95,7 +95,15 @@ "TerritoryId": 813, "InteractionType": "Interact", "DisableNavmesh": true, - "$.1": "QuestVariables if done first: 1 0 0 0 0 64" + "$.1": "QuestVariables if done first: 1 0 0 0 0 64", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 2010278, @@ -106,7 +114,15 @@ }, "TerritoryId": 813, "InteractionType": "Interact", - "DisableNavmesh": true + "DisableNavmesh": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "DataId": 2010282, @@ -117,7 +133,16 @@ }, "TerritoryId": 813, "InteractionType": "Interact", - "DisableNavmesh": true + "DisableNavmesh": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ], + "Comment": "TODO Check if pathfinding works automatically now" } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json index a2aa8d224..a5bec5dea 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json @@ -1,7 +1,6 @@ { "$schema": "https://carvel.li/questionable/quest-1.0", "Author": "liza", - "Comment": "TODO Missing quest end", "TerritoryBlacklist": [ 898 ], @@ -33,6 +32,7 @@ }, "TerritoryId": 814, "InteractionType": "Interact", + "AetheryteShortcut": "Kholusia - Wright", "Fly": true } ] @@ -46,6 +46,21 @@ "ContentFinderConditionId": 714 } ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1032549, + "Position": { + "X": -1.9074707, + "Y": -200.00002, + "Z": -425.10114 + }, + "TerritoryId": 918, + "InteractionType": "Interact" + } + ] } ] } diff --git a/QuestPaths/Shadowbringers/MSQ/H-5.2/3770_Echoes of a Fallen Star.json b/QuestPaths/Shadowbringers/MSQ/H-5.2/3770_Echoes of a Fallen Star.json index 9b72bf47e..21ac75d59 100644 --- a/QuestPaths/Shadowbringers/MSQ/H-5.2/3770_Echoes of a Fallen Star.json +++ b/QuestPaths/Shadowbringers/MSQ/H-5.2/3770_Echoes of a Fallen Star.json @@ -18,8 +18,7 @@ ] }, { - "Sequence": 1, - "Comment": "TODO verify this is the correct sequence and/or where sequence 2 went", + "Sequence": 2, "Steps": [ { "DataId": 1032529, @@ -67,7 +66,8 @@ "AethernetShortcut": [ "[Crystarium] The Amaro Launch", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 }, { "DataId": 1032121, @@ -92,7 +92,8 @@ "Z": 14.206055 }, "TerritoryId": 844, - "InteractionType": "Interact" + "InteractionType": "Interact", + "TargetTerritoryId": 819 }, { "DataId": 1030610, @@ -107,6 +108,13 @@ "AethernetShortcut": [ "[Crystarium] The Dossal Gate", "[Crystarium] The Pendants" + ], + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_LUCKMH110_03770_Q1_000_600", + "Yes": true + } ] } ] diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3771_In the Name of the Light.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3771_In the Name of the Light.json index f90d3afd2..c45626845 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3771_In the Name of the Light.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3771_In the Name of the Light.json @@ -48,7 +48,14 @@ "Z": -277.7906 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMI101_03771_Q3_000_148", + "Answer": "TEXT_LUCKMI101_03771_A3_000_149" + } + ] } ] }, @@ -98,6 +105,14 @@ }, "TerritoryId": 819, "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ], "$.1": "QuestVariables if done first: 1 16 0 0 0 64" }, { @@ -110,6 +125,14 @@ "StopDistance": 5, "TerritoryId": 819, "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ], "$.1": "QuestVariables if done after [1]: 2 32 0 0 0 192" }, { @@ -120,7 +143,15 @@ "Z": 173.38818 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] } ] }, @@ -142,15 +173,6 @@ { "Sequence": 255, "Steps": [ - { - "Position": { - "X": -140.22343, - "Y": 0.05337119, - "Z": 34.20123 - }, - "TerritoryId": 819, - "InteractionType": "WalkTo" - }, { "DataId": 1027248, "Position": { @@ -159,7 +181,8 @@ "Z": -51.438232 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "AetheryteShortcut": "Crystarium" } ] } diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3772_Heroic Dreams.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3772_Heroic Dreams.json index 1afed5cf6..1ec3cbb8e 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3772_Heroic Dreams.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3772_Heroic Dreams.json @@ -38,6 +38,7 @@ }, { "Sequence": 2, + "Comment": "This isn't solving for the 'best' results, but for the closest waypoints", "Steps": [ { "DataId": 2011078, @@ -56,7 +57,15 @@ ], "Fly": true, "$.0": "[1]", - "$.1": "QuestVariables if done first: 0 0 0 3 0 0 during fight; 16 1 0 2 16 8 after" + "$.1": "QuestVariables if done first: 0 0 0 3 0 0 during fight; 16 1 0 2 16 8 after", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 8 + ] }, { "DataId": 2011076, @@ -69,10 +78,19 @@ "InteractionType": "Combat", "EnemySpawnType": "AfterItemUse", "ItemId": 2003001, - "KillEnemyDataIds": [], - "Comment": "TODO Missing enemy ids", + "KillEnemyDataIds": [ + 12166 + ], "Fly": true, - "$.1": "QuestVariables if done after [1]: 34 1 0 1 48 40" + "$.1": "QuestVariables if done after [1]: 34 1 0 1 48 40", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] }, { "DataId": 2011079, @@ -86,10 +104,11 @@ "EnemySpawnType": "AfterItemUse", "ItemId": 2003001, "KillEnemyDataIds": [ + 12168 ], "Comment": "TODO Missing enemy ids", "Fly": true, - "$.2": "QuestVariables if done after [1, 2]: 0 64 0 0 0 0" + "$.2": "QuestVariables if done after [1, 2]: irrelevant because it automatically progresses to the next step" } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3774_Food for the Soul.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3774_Food for the Soul.json index 17d48f9c6..7fafc7ddb 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3774_Food for the Soul.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3774_Food for the Soul.json @@ -79,7 +79,8 @@ "AethernetShortcut": [ "[Crystarium] Aetheryte Plaza", "[Crystarium] The Dossal Gate" - ] + ], + "TargetTerritoryId": 844 }, { "DataId": 1033819, diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3775_Faded Memories.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3775_Faded Memories.json index b8dec55b4..76c6d30b1 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3775_Faded Memories.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3775_Faded Memories.json @@ -42,7 +42,14 @@ "Z": 604.27246 }, "TerritoryId": 814, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_LUCKMI105_03775_Q2_000_052", + "Yes": true + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3778_Hope's Confluence.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3778_Hope's Confluence.json index 4cc3791bf..3cff2aba4 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3778_Hope's Confluence.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3778_Hope's Confluence.json @@ -16,12 +16,19 @@ "Z": 1.6021729 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMI108_03778_Q1_000_001", + "Answer": "TEXT_LUCKMI108_03778_A1_000_002" + } + ] } ] }, { - "Sequence": 1, + "Sequence": 2, "Steps": [ { "TerritoryId": 931, diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3779_Nothing Unsaid.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3779_Nothing Unsaid.json index fc6d4f274..424df0eae 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3779_Nothing Unsaid.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3779_Nothing Unsaid.json @@ -85,8 +85,13 @@ "Sequence": 5, "Steps": [ { + "Position": { + "X": 0, + "Y": 0, + "Z": 0 + }, "TerritoryId": 820, - "InteractionType": "Interact", + "InteractionType": "WalkTo", "AetheryteShortcut": "Eulmore" } ] diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3781_Unto the Morrow.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3781_Unto the Morrow.json index 3afcc4905..226025408 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3781_Unto the Morrow.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3781_Unto the Morrow.json @@ -75,7 +75,15 @@ "Z": 3.982544 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "AetheryteShortcut": "Crystarium", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMI111_03781_Q1_000_153", + "Answer": "TEXT_LUCKMI111_03781_A1_000_154" + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/I-5.3/3782_Reflections in Crystal.json b/QuestPaths/Shadowbringers/MSQ/I-5.3/3782_Reflections in Crystal.json index 2d9b60556..2d49f9f8a 100644 --- a/QuestPaths/Shadowbringers/MSQ/I-5.3/3782_Reflections in Crystal.json +++ b/QuestPaths/Shadowbringers/MSQ/I-5.3/3782_Reflections in Crystal.json @@ -29,7 +29,14 @@ "Z": 7.156433 }, "TerritoryId": 819, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_LUCKMI112_03782_Q1_000_007", + "Yes": true + } + ] }, { "DataId": 1033888, @@ -39,7 +46,14 @@ "Z": -5.081299 }, "TerritoryId": 844, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_LUCKMI112_03782_Q2_000_044", + "Yes": true + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/J-5.4/4008_The Wisdom of Allag.json b/QuestPaths/Shadowbringers/MSQ/J-5.4/4008_The Wisdom of Allag.json index e483792b9..4ca6ea2ad 100644 --- a/QuestPaths/Shadowbringers/MSQ/J-5.4/4008_The Wisdom of Allag.json +++ b/QuestPaths/Shadowbringers/MSQ/J-5.4/4008_The Wisdom of Allag.json @@ -64,7 +64,8 @@ "EnemySpawnType": "AfterInteraction", "KillEnemyDataIds": [ 12661 - ] + ], + "Fly": true } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/J-5.4/4009_Reviving the Legacy.json b/QuestPaths/Shadowbringers/MSQ/J-5.4/4009_Reviving the Legacy.json index c44483573..29cd35ea8 100644 --- a/QuestPaths/Shadowbringers/MSQ/J-5.4/4009_Reviving the Legacy.json +++ b/QuestPaths/Shadowbringers/MSQ/J-5.4/4009_Reviving the Legacy.json @@ -55,7 +55,8 @@ "Z": -6.9733887 }, "TerritoryId": 351, - "InteractionType": "Interact" + "InteractionType": "Interact", + "TargetTerritoryId": 351 }, { "DataId": 2011332, diff --git a/QuestPaths/Shadowbringers/MSQ/J-5.4/4010_Forget Us Not.json b/QuestPaths/Shadowbringers/MSQ/J-5.4/4010_Forget Us Not.json index 1c639a060..1e0224a03 100644 --- a/QuestPaths/Shadowbringers/MSQ/J-5.4/4010_Forget Us Not.json +++ b/QuestPaths/Shadowbringers/MSQ/J-5.4/4010_Forget Us Not.json @@ -14,7 +14,14 @@ }, "StopDistance": 5, "TerritoryId": 351, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMJ104_04010_Q1_000_000", + "Answer": "TEXT_LUCKMJ104_04010_A1_000_002" + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/J-5.4/4014_On Rough Seas.json b/QuestPaths/Shadowbringers/MSQ/J-5.4/4014_On Rough Seas.json index a80047d3a..09184cea3 100644 --- a/QuestPaths/Shadowbringers/MSQ/J-5.4/4014_On Rough Seas.json +++ b/QuestPaths/Shadowbringers/MSQ/J-5.4/4014_On Rough Seas.json @@ -29,7 +29,14 @@ }, "TerritoryId": 129, "InteractionType": "Interact", - "AetheryteShortcut": "Limsa Lominsa" + "AetheryteShortcut": "Limsa Lominsa", + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_LUCKMJ108_04014_SYSTEM_100_010", + "Yes": true + } + ] }, { "DataId": 1002694, diff --git a/QuestPaths/Shadowbringers/MSQ/J-5.4/4015_The Great Ship Vylbrand.json b/QuestPaths/Shadowbringers/MSQ/J-5.4/4015_The Great Ship Vylbrand.json index f03429017..f2f077b62 100644 --- a/QuestPaths/Shadowbringers/MSQ/J-5.4/4015_The Great Ship Vylbrand.json +++ b/QuestPaths/Shadowbringers/MSQ/J-5.4/4015_The Great Ship Vylbrand.json @@ -39,6 +39,26 @@ { "Sequence": 2, "Steps": [ + { + "Position": { + "X": 46.600548, + "Y": 77.45801, + "Z": -366.82053 + }, + "TerritoryId": 180, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "Position": { + "X": 111.927666, + "Y": 26.050894, + "Z": -612.8873 + }, + "TerritoryId": 180, + "InteractionType": "WalkTo", + "Fly": true + }, { "Position": { "X": 82.19566, diff --git a/QuestPaths/Shadowbringers/MSQ/K-5.5/4060_Righteous Indignation.json b/QuestPaths/Shadowbringers/MSQ/K-5.5/4060_Righteous Indignation.json index 7f7336b7e..dfd5220bf 100644 --- a/QuestPaths/Shadowbringers/MSQ/K-5.5/4060_Righteous Indignation.json +++ b/QuestPaths/Shadowbringers/MSQ/K-5.5/4060_Righteous Indignation.json @@ -79,7 +79,15 @@ "TerritoryId": 402, "InteractionType": "Interact", "Fly": true, - "$.1": "QuestVariables if done first: 1 16 0 0 0 64" + "$.1": "QuestVariables if done first: 1 16 0 0 0 64", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 1036359, @@ -92,7 +100,15 @@ "InteractionType": "Interact", "Mount": true, "Fly": true, - "$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96" + "$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] }, { "DataId": 1036357, @@ -103,7 +119,15 @@ }, "TerritoryId": 402, "InteractionType": "Interact", - "DisableNavmesh": true + "DisableNavmesh": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] } ] }, @@ -119,7 +143,14 @@ }, "TerritoryId": 402, "InteractionType": "Interact", - "Fly": true + "Fly": true, + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMK103_04060_Q1_000_100", + "Answer": "TEXT_LUCKMK103_04060_A2_000_100" + } + ] } ] }, diff --git a/QuestPaths/Shadowbringers/MSQ/K-5.5/4063_When the Dust Settles.json b/QuestPaths/Shadowbringers/MSQ/K-5.5/4063_When the Dust Settles.json index c2b02fcbf..e3b1beedd 100644 --- a/QuestPaths/Shadowbringers/MSQ/K-5.5/4063_When the Dust Settles.json +++ b/QuestPaths/Shadowbringers/MSQ/K-5.5/4063_When the Dust Settles.json @@ -33,6 +33,13 @@ "AethernetShortcut": [ "[Ul'dah] Aetheryte Plaza", "[Ul'dah] Alchemists' Guild" + ], + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_LUCKMK106_04063_Q1_000_100", + "Answer": "TEXT_LUCKMK106_04063_A2_000_100" + } ] } ] diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 58c989077..cc2d097ba 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -672,49 +672,40 @@ "type": "string", "enum": [ "YesNo", - "List", - "ContentTalkYesNo", - "ContentTalkList" + "List" ] }, "ExcelSheet": { "type": "string" - }, - "Prompt": { - "type": [ - "string", - "null" - ] } }, "required": [ - "Type", - "Prompt" + "Type" ], "allOf": [ { "if": { "properties": { "Type": { - "anyOf": [ - { - "const": "YesNo" - }, - { - "const": "ContentTalkYesNo" - } - ] + "const": "YesNo" } } }, "then": { "properties": { + "Prompt": { + "type": [ + "string", + "integer" + ] + }, "Yes": { "type": "boolean", "default": true } }, "required": [ + "Prompt", "Yes" ] } @@ -723,24 +714,28 @@ "if": { "properties": { "Type": { - "anyOf": [ - { - "const": "List" - }, - { - "const": "ContentTalkList" - } - ] + "const": "List" } } }, "then": { "properties": { + "Prompt": { + "type": [ + "string", + "integer", + "null" + ] + }, "Answer": { - "type": "string" + "type": [ + "string", + "integer" + ] } }, "required": [ + "Prompt", "Answer" ] } diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index 6aa01ec3f..d77af7857 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -6,5 +6,19 @@ namespace Questionable; internal sealed class Configuration : IPluginConfiguration { public int Version { get; set; } = 1; - public WindowConfig DebugWindowConfig { get; set; } = new(); + public GeneralConfiguration General { get; } = new(); + public AdvancedConfiguration Advanced { get; } = new(); + public WindowConfig DebugWindowConfig { get; } = new(); + public WindowConfig ConfigWindowConfig { get; } = new(); + + internal sealed class GeneralConfiguration + { + public bool AutoAcceptNextQuest { get; set; } + public uint MountId { get; set; } = 71; + } + + internal sealed class AdvancedConfiguration + { + public bool NeverFly { get; set; } + } } diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index 964235862..e9fd522d4 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -23,16 +24,19 @@ internal sealed class GameUiController : IDisposable private readonly GameFunctions _gameFunctions; private readonly QuestController _questController; private readonly IGameGui _gameGui; + private readonly ITargetManager _targetManager; private readonly ILogger _logger; public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions, - QuestController questController, IGameGui gameGui, ILogger logger) + QuestController questController, IGameGui gameGui, ITargetManager targetManager, + ILogger logger) { _addonLifecycle = addonLifecycle; _dataManager = dataManager; _gameFunctions = gameFunctions; _questController = questController; _gameGui = gameGui; + _targetManager = targetManager; _logger = logger; _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup); @@ -181,40 +185,26 @@ internal sealed class GameUiController : IDisposable foreach (var dialogueChoice in dialogueChoices) { + if (dialogueChoice.Type != EDialogChoiceType.List) + continue; + if (dialogueChoice.Answer == null) { - _logger.LogInformation("Ignoring entry in DialogueChoices, no answer"); + _logger.LogDebug("Ignoring entry in DialogueChoices, no answer"); continue; } - string? excelPrompt = null, excelAnswer; - switch (dialogueChoice.Type) + if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId) { - case EDialogChoiceType.ContentTalkList: - if (dialogueChoice.Prompt != null) - { - excelPrompt = - _gameFunctions.GetContentTalk(uint.Parse(dialogueChoice.Prompt, - CultureInfo.InvariantCulture)); - } - - excelAnswer = - _gameFunctions.GetContentTalk(uint.Parse(dialogueChoice.Answer, CultureInfo.InvariantCulture)); - break; - case EDialogChoiceType.List: - if (dialogueChoice.Prompt != null) - { - excelPrompt = - _gameFunctions.GetDialogueText(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); - } - - excelAnswer = - _gameFunctions.GetDialogueText(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer); - break; - default: - continue; + _logger.LogDebug( + "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}", + dialogueChoice.DataId, _targetManager.Target?.DataId); + continue; } + string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); + string? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer); + if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt)) { _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt); @@ -288,30 +278,25 @@ internal sealed class GameUiController : IDisposable _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count); foreach (var dialogueChoice in dialogueChoices) { - string? excelPrompt; - if (dialogueChoice.Prompt != null) - { - switch (dialogueChoice.Type) - { - case EDialogChoiceType.ContentTalkYesNo: - excelPrompt = - _gameFunctions.GetContentTalk(uint.Parse(dialogueChoice.Prompt, - CultureInfo.InvariantCulture)); - break; - case EDialogChoiceType.YesNo: - excelPrompt = - _gameFunctions.GetDialogueText(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); - break; - default: - continue; - } - } - else - excelPrompt = null; - - if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)) + if (dialogueChoice.Type != EDialogChoiceType.YesNo) continue; + if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId) + { + _logger.LogDebug( + "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}", + dialogueChoice.DataId, _targetManager.Target?.DataId); + continue; + } + + string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt); + if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)) + { + _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", + excelPrompt, actualPrompt); + continue; + } + addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1); if (!checkAllSteps) _questController.IncreaseDialogueChoicesSelected(); @@ -343,7 +328,8 @@ internal sealed class GameUiController : IDisposable increaseStepCount = false; if (step != null) - _logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, step.TargetTerritoryId); + _logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, + step.TargetTerritoryId); } if (step == null || step.TargetTerritoryId == null) @@ -367,7 +353,7 @@ internal sealed class GameUiController : IDisposable _logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt); addonSelectYesno->AtkUnitBase.FireCallbackInt(0); //if (increaseStepCount) - //_questController.IncreaseStepCount(); + //_questController.IncreaseStepCount(); return; } } @@ -403,6 +389,19 @@ internal sealed class GameUiController : IDisposable return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-'); } + private string? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef) + { + if (excelRef == null) + return null; + + if (excelRef.Type == ExcelRef.EType.Key) + return _gameFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey()); + else if (excelRef.Type == ExcelRef.EType.RowId) + return _gameFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId()); + + return null; + } + public void Dispose() { _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup); diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index 200c1a79e..cd5f92563 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -76,7 +76,7 @@ internal sealed class MovementController : IDisposable } } else if (!Destination.IsFlying && !_condition[ConditionFlag.Mounted] && navPoints.Count > 0 && - !_gameFunctions.HasStatusPreventingSprintOrMount() && Destination.CanSprint) + !_gameFunctions.HasStatusPreventingSprintOrMount(true) && Destination.CanSprint) { float actualDistance = 0; foreach (Vector3 end in navPoints) @@ -210,7 +210,7 @@ internal sealed class MovementController : IDisposable _logger.LogInformation("Pathfinding to {Destination}", Destination); _cancellationTokenSource = new(); - _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10)); + _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30)); _pathfindTask = _navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token); } diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 4cb5be171..81b96ce16 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -10,6 +10,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Data; @@ -28,6 +29,7 @@ internal sealed class QuestController private readonly ILogger _logger; private readonly QuestRegistry _questRegistry; private readonly IKeyState _keyState; + private readonly Configuration _configuration; private readonly IReadOnlyList _taskFactories; private readonly Queue _taskQueue = new(); @@ -41,6 +43,7 @@ internal sealed class QuestController ILogger logger, QuestRegistry questRegistry, IKeyState keyState, + Configuration configuration, IEnumerable taskFactories) { _clientState = clientState; @@ -49,6 +52,7 @@ internal sealed class QuestController _logger = logger; _questRegistry = questRegistry; _keyState = keyState; + _configuration = configuration; _taskFactories = taskFactories.ToList().AsReadOnly(); } @@ -79,6 +83,20 @@ internal sealed class QuestController if (CurrentQuest != null && CurrentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType)) return; + // not verified to work + if (_automatic && _currentTask == null && _taskQueue.Count == 0 && CurrentQuest is { Sequence: 0, Step: 255 } + && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15)) + { + _logger.LogWarning("Quest accept apparently didn't work out, resetting progress"); + CurrentQuest = CurrentQuest with + { + Step = 0 + }; + + ExecuteNextStep(true); + return; + } + UpdateCurrentTask(); } @@ -102,7 +120,13 @@ internal sealed class QuestController { _logger.LogInformation("New quest: {QuestName}", quest.Name); CurrentQuest = new QuestProgress(quest, currentSequence, 0); - Stop("Different Quest"); + + bool continueAutomatically = _configuration.General.AutoAcceptNextQuest; + + if (_clientState.LocalPlayer?.Level < quest.Level) + continueAutomatically = false; + + Stop("Different Quest", continueAutomatically); } else if (CurrentQuest != null) { @@ -208,7 +232,7 @@ internal sealed class QuestController CurrentQuest = CurrentQuest with { Step = CurrentQuest.Step + 1, - StepProgress = new() + StepProgress = new(DateTime.Now), }; } else @@ -216,7 +240,7 @@ internal sealed class QuestController CurrentQuest = CurrentQuest with { Step = 255, - StepProgress = new() + StepProgress = new(DateTime.Now), }; } @@ -416,6 +440,8 @@ internal sealed class QuestController public bool HasCurrentTaskMatching() => _currentTask is T; + public bool IsRunning => _currentTask != null || _taskQueue.Count > 0; + public sealed record QuestProgress( Quest Quest, byte Sequence, @@ -423,12 +449,13 @@ internal sealed class QuestController StepProgress StepProgress) { public QuestProgress(Quest quest, byte sequence, int step) - : this(quest, sequence, step, new StepProgress()) + : this(quest, sequence, step, new StepProgress(DateTime.Now)) { } } // TODO is this still required? public sealed record StepProgress( + DateTime StartedAt, int DialogueChoicesSelected = 0); } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 9dccc7d45..e3ddd3bad 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -57,6 +57,7 @@ internal sealed class QuestRegistry continue; quest.Name = questData.Name.ToString(); + quest.Level = questData.ClassJobLevel0; } } diff --git a/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs b/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs index 988d5cd49..34d26abbf 100644 --- a/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs +++ b/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs @@ -108,7 +108,13 @@ internal static class AethernetShortcut return ETaskResult.StillRunning; } - if (aetheryteData.IsCityAetheryte(To)) + if (aetheryteData.IsAirshipLanding(To)) + { + if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, + clientState.TerritoryType, To) > 5) + return ETaskResult.StillRunning; + } + else if (aetheryteData.IsCityAetheryte(To)) { if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, clientState.TerritoryType, To) > 11) diff --git a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs index 22d14d666..c4c97b07e 100644 --- a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; +using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.DependencyInjection; using Questionable.Controller.Steps.BaseTasks; using Questionable.Model; @@ -23,7 +24,7 @@ internal static class WaitAtEnd var task = serviceProvider.GetRequiredService() .With(quest, step); var delay = serviceProvider.GetRequiredService(); - return [task, delay, new NextStep()]; + return [task, delay, Next(quest, sequence, step)]; } switch (step.InteractionType) @@ -41,7 +42,7 @@ internal static class WaitAtEnd case EInteractionType.WalkTo: case EInteractionType.Jump: // no need to wait if we're just moving around - return [new NextStep()]; + return [Next(quest, sequence, step)]; case EInteractionType.WaitForObjectAtPosition: ArgumentNullException.ThrowIfNull(step.DataId); @@ -52,7 +53,7 @@ internal static class WaitAtEnd serviceProvider.GetRequiredService() .With(step.DataId.Value, step.Position.Value), serviceProvider.GetRequiredService(), - new NextStep() + Next(quest, sequence, step) ]; case EInteractionType.Interact when step.TargetTerritoryId != null: @@ -81,17 +82,40 @@ internal static class WaitAtEnd [ waitInteraction, serviceProvider.GetRequiredService(), - new NextStep() + Next(quest, sequence, step) ]; case EInteractionType.Interact: default: - return [serviceProvider.GetRequiredService(), new NextStep()]; + return [serviceProvider.GetRequiredService(), Next(quest, sequence, step)]; } } public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step) => throw new InvalidOperationException(); + + public ITask Next(Quest quest, QuestSequence sequence, QuestStep step) + { + bool lastStep = step == sequence.Steps.LastOrDefault(); + if (sequence.Sequence == 0 && lastStep) + { + return new WaitConditionTask(() => + { + unsafe + { + var questManager = QuestManager.Instance(); + return questManager != null && questManager->IsQuestAccepted(quest.QuestId); + } + }, "Wait(questAccepted)"); + } + else if (sequence.Sequence == 255 && lastStep) + { + return new WaitConditionTask(() => QuestManager.IsQuestComplete(quest.QuestId), + "Wait(questComplete)"); + } + else + return new NextStep(); + } } internal sealed class WaitDelay() : AbstractDelayedTask(TimeSpan.FromSeconds(1)) diff --git a/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs b/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs index ede325b59..9a11cd492 100644 --- a/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs +++ b/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.ClientState.Conditions; +using System; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; @@ -8,6 +9,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger log : ITask { private bool _unmountTriggered; + private DateTime _unmountedAt = DateTime.MinValue; public bool Start() { @@ -16,6 +18,8 @@ internal sealed class UnmountTask(ICondition condition, ILogger log logger.LogInformation("Step explicitly wants no mount, trying to unmount..."); _unmountTriggered = gameFunctions.Unmount(); + if (_unmountTriggered) + _unmountedAt = DateTime.Now; return true; } @@ -24,9 +28,15 @@ internal sealed class UnmountTask(ICondition condition, ILogger log if (!_unmountTriggered) { _unmountTriggered = gameFunctions.Unmount(); + if (_unmountTriggered) + _unmountedAt = DateTime.Now; + return ETaskResult.StillRunning; } + if (DateTime.Now < _unmountedAt.AddSeconds(1)) + return ETaskResult.StillRunning; + return condition[ConditionFlag.Mounted] ? ETaskResult.StillRunning : ETaskResult.TaskComplete; diff --git a/Questionable/Controller/Steps/InteractionFactory/UseItem.cs b/Questionable/Controller/Steps/InteractionFactory/UseItem.cs index b562baf42..4289e6f22 100644 --- a/Questionable/Controller/Steps/InteractionFactory/UseItem.cs +++ b/Questionable/Controller/Steps/InteractionFactory/UseItem.cs @@ -45,8 +45,38 @@ internal static class UseItem => throw new InvalidOperationException(); } + internal abstract class UseItemBase : ITask + { + private bool _usedItem; + private DateTime _continueAt; - internal sealed class UseOnGround(GameFunctions gameFunctions) : AbstractDelayedTask + protected abstract bool UseItem(); + + public bool Start() + { + _usedItem = UseItem(); + _continueAt = DateTime.Now.AddSeconds(2); + return true; + } + + public ETaskResult Update() + { + if (DateTime.Now > _continueAt) + return ETaskResult.StillRunning; + + if (!_usedItem) + { + _usedItem = UseItem(); + _continueAt = DateTime.Now.AddSeconds(2); + return ETaskResult.StillRunning; + } + + return ETaskResult.TaskComplete; + } + } + + + internal sealed class UseOnGround(GameFunctions gameFunctions) : UseItemBase { public uint DataId { get; set; } public uint ItemId { get; set; } @@ -58,16 +88,12 @@ internal static class UseItem return this; } - protected override bool StartInternal() - { - gameFunctions.UseItemOnGround(DataId, ItemId); - return true; - } + protected override bool UseItem() => gameFunctions.UseItemOnGround(DataId, ItemId); public override string ToString() => $"UseItem({ItemId} on ground at {DataId})"; } - internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask + internal sealed class UseOnObject(GameFunctions gameFunctions) : UseItemBase { public uint DataId { get; set; } public uint ItemId { get; set; } @@ -79,16 +105,12 @@ internal static class UseItem return this; } - protected override bool StartInternal() - { - gameFunctions.UseItem(DataId, ItemId); - return true; - } + protected override bool UseItem() => gameFunctions.UseItem(DataId, ItemId); public override string ToString() => $"UseItem({ItemId} on {DataId})"; } - internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask + internal sealed class Use(GameFunctions gameFunctions) : UseItemBase { public uint ItemId { get; set; } @@ -98,11 +120,7 @@ internal static class UseItem return this; } - protected override bool StartInternal() - { - gameFunctions.UseItem(ItemId); - return true; - } + protected override bool UseItem() => gameFunctions.UseItem(ItemId); public override string ToString() => $"UseItem({ItemId})"; } diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index 44607f2d2..192a9ba92 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -18,10 +18,12 @@ internal sealed class DalamudInitializer : IDisposable private readonly NavigationShortcutController _navigationShortcutController; private readonly WindowSystem _windowSystem; private readonly DebugWindow _debugWindow; + private readonly ConfigWindow _configWindow; public DalamudInitializer(DalamudPluginInterface pluginInterface, IFramework framework, ICommandManager commandManager, QuestController questController, MovementController movementController, - GameUiController gameUiController, NavigationShortcutController navigationShortcutController, WindowSystem windowSystem, DebugWindow debugWindow) + GameUiController gameUiController, NavigationShortcutController navigationShortcutController, + WindowSystem windowSystem, DebugWindow debugWindow, ConfigWindow configWindow) { _pluginInterface = pluginInterface; _framework = framework; @@ -31,9 +33,14 @@ internal sealed class DalamudInitializer : IDisposable _navigationShortcutController = navigationShortcutController; _windowSystem = windowSystem; _debugWindow = debugWindow; + _configWindow = configWindow; + + _windowSystem.AddWindow(debugWindow); + _windowSystem.AddWindow(configWindow); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.OpenMainUi += _debugWindow.Toggle; + _pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle; _framework.Update += FrameworkUpdate; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) { @@ -60,7 +67,10 @@ internal sealed class DalamudInitializer : IDisposable private void ProcessCommand(string command, string arguments) { - _debugWindow.Toggle(); + if (arguments is "c" or "config") + _configWindow.Toggle(); + else + _debugWindow.Toggle(); } public void Dispose() @@ -69,5 +79,7 @@ internal sealed class DalamudInitializer : IDisposable _framework.Update -= FrameworkUpdate; _pluginInterface.UiBuilder.OpenMainUi -= _debugWindow.Toggle; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + + _windowSystem.RemoveAllWindows(); } } diff --git a/Questionable/Data/AetheryteData.cs b/Questionable/Data/AetheryteData.cs index 377fb0ce4..cebbd2839 100644 --- a/Questionable/Data/AetheryteData.cs +++ b/Questionable/Data/AetheryteData.cs @@ -217,6 +217,18 @@ internal sealed class AetheryteData } .AsReadOnly(); + /// + /// Airship landings are special as they're one-way only (except for Radz-at-Han, which is a normal aetheryte). + /// + public ReadOnlyDictionary AirshipLandingLocations { get; } = + new Dictionary + { + { EAetheryteLocation.LimsaAirship, new(-19.44352f, 91.99999f, -9.892939f) }, + { EAetheryteLocation.GridaniaAirship, new(24.86354f, -19.000002f, 96f) }, + { EAetheryteLocation.UldahAirship, new(-16.954851f, 82.999985f, -9.421141f) }, + { EAetheryteLocation.KuganeAirship, new(-55.72525f, 79.10602f, 46.23109f) }, + }.AsReadOnly(); + public ReadOnlyDictionary AethernetNames { get; } public ReadOnlyDictionary TerritoryIds { get; } public IReadOnlyList TownTerritoryIds { get; set; } @@ -232,9 +244,22 @@ internal sealed class AetheryteData return (fromPosition - toPosition).Length(); } + public float CalculateAirshipLandingDistance(Vector3 fromPosition, ushort fromTerritoryType, EAetheryteLocation to) + { + if (!TerritoryIds.TryGetValue(to, out ushort toTerritoryType) || fromTerritoryType != toTerritoryType) + return float.MaxValue; + + if (!AirshipLandingLocations.TryGetValue(to, out Vector3 toPosition)) + return float.MaxValue; + + return (fromPosition - toPosition).Length(); + } + public bool IsCityAetheryte(EAetheryteLocation aetheryte) { var territoryId = TerritoryIds[aetheryte]; return TownTerritoryIds.Contains(territoryId); } + + public bool IsAirshipLanding(EAetheryteLocation aetheryte) => AirshipLandingLocations.ContainsKey(aetheryte); } diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index 4542b5a04..67fd79b58 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -23,13 +23,17 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; using Lumina.Excel.CustomSheets; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.GeneratedSheets2; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Model.V1; using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara; +using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition; +using ContentTalk = Lumina.Excel.GeneratedSheets.ContentTalk; +using Emote = Lumina.Excel.GeneratedSheets.Emote; using GameObject = Dalamud.Game.ClientState.Objects.Types.GameObject; using Quest = Questionable.Model.Quest; +using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType; namespace Questionable; @@ -56,11 +60,12 @@ internal sealed unsafe class GameFunctions private readonly IClientState _clientState; private readonly QuestRegistry _questRegistry; private readonly IGameGui _gameGui; + private readonly Configuration _configuration; private readonly ILogger _logger; public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry, - IGameGui gameGui, ILogger logger) + IGameGui gameGui, Configuration configuration, ILogger logger) { _dataManager = dataManager; _objectTable = objectTable; @@ -69,6 +74,7 @@ internal sealed unsafe class GameFunctions _clientState = clientState; _questRegistry = questRegistry; _gameGui = gameGui; + _configuration = configuration; _logger = logger; _processChatBox = Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText(Signatures.SendChat)); @@ -364,29 +370,33 @@ internal sealed unsafe class GameFunctions return false; } - public void UseItem(uint itemId) + public bool UseItem(uint itemId) { - AgentInventoryContext.Instance()->UseItem(itemId); + return AgentInventoryContext.Instance()->UseItem(itemId) == 0; } - public void UseItem(uint dataId, uint itemId) + public bool UseItem(uint dataId, uint itemId) { GameObject? gameObject = FindObjectByDataId(dataId); if (gameObject != null) { _targetManager.Target = gameObject; - AgentInventoryContext.Instance()->UseItem(itemId); + return AgentInventoryContext.Instance()->UseItem(itemId) == 0; } + + return false; } - public void UseItemOnGround(uint dataId, uint itemId) + public bool UseItemOnGround(uint dataId, uint itemId) { GameObject? gameObject = FindObjectByDataId(dataId); if (gameObject != null) { var position = (FFXIVClientStructs.FFXIV.Common.Math.Vector3)gameObject.Position; - ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position); + return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position); } + + return false; } public void UseEmote(uint dataId, EEmote emote) @@ -410,8 +420,11 @@ internal sealed unsafe class GameFunctions return gameObject != null && (gameObject.Position - position).Length() < 0.05f; } - public bool HasStatusPreventingSprintOrMount() + public bool HasStatusPreventingSprintOrMount(bool skipConfigCheck = false) { + if (!skipConfigCheck && _configuration.Advanced.NeverFly) + return true; + if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone()) return true; @@ -437,13 +450,14 @@ internal sealed unsafe class GameFunctions return true; var playerState = PlayerState.Instance(); - if (playerState != null && playerState->IsMountUnlocked(71)) + if (playerState != null && _configuration.General.MountId != 0 && + playerState->IsMountUnlocked(_configuration.General.MountId)) { - if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0) + if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId) == 0) { - if (ActionManager.Instance()->UseAction(ActionType.Mount, 71)) + if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId)) { - _logger.LogInformation("Using SDS Fenrir as mount"); + _logger.LogInformation("Using preferred mount"); return true; } @@ -526,10 +540,20 @@ internal sealed unsafe class GameFunctions return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString(); } - public string? GetContentTalk(uint rowId) + public string? GetDialogueTextByRowId(string? excelSheet, uint rowId) { - var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); - return questRow?.Text?.ToString(); + if (excelSheet == "GimmickYesNo") + { + var questRow = _dataManager.GetExcelSheet()!.GetRow(rowId); + return questRow?.Unknown0?.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}"); } public bool IsOccupied() diff --git a/Questionable/Model/Quest.cs b/Questionable/Model/Quest.cs index 3944301a7..ac6d408a9 100644 --- a/Questionable/Model/Quest.cs +++ b/Questionable/Model/Quest.cs @@ -7,6 +7,7 @@ internal sealed class Quest { public required ushort QuestId { get; init; } public required string Name { get; set; } + public ushort Level { get; set; } public required QuestData Data { get; init; } public QuestSequence? FindSequence(byte currentSequence) diff --git a/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs b/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs index abb41f69a..b40304d72 100644 --- a/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs +++ b/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs @@ -8,7 +8,5 @@ internal sealed class DialogueChoiceTypeConverter() : EnumConverter +{ + public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + return new ExcelRef(reader.GetString()!); + else if (reader.TokenType == JsonTokenType.Number) + return new ExcelRef(reader.GetUInt32()); + else + return null; + } + + public override void Write(Utf8JsonWriter writer, ExcelRef? value, JsonSerializerOptions options) + { + if (value == null) + writer.WriteNullValue(); + else if (value.Type == ExcelRef.EType.Key) + writer.WriteStringValue(value.AsKey()); + else if (value.Type == ExcelRef.EType.RowId) + writer.WriteNumberValue(value.AsRowId()); + else + throw new JsonException(); + } +} diff --git a/Questionable/Model/V1/DialogueChoice.cs b/Questionable/Model/V1/DialogueChoice.cs index d826f53d5..ddfc0ae09 100644 --- a/Questionable/Model/V1/DialogueChoice.cs +++ b/Questionable/Model/V1/DialogueChoice.cs @@ -10,7 +10,17 @@ internal sealed class DialogueChoice [JsonConverter(typeof(DialogueChoiceTypeConverter))] public EDialogChoiceType Type { get; set; } public string? ExcelSheet { get; set; } - public string? Prompt { get; set; } + + [JsonConverter(typeof(ExcelRefConverter))] + public ExcelRef? Prompt { get; set; } + public bool Yes { get; set; } = true; - public string? Answer { get; set; } + + [JsonConverter(typeof(ExcelRefConverter))] + public ExcelRef? Answer { get; set; } + + /// + /// If set, only applies when focusing the given target id. + /// + public uint? DataId { get; set; } } diff --git a/Questionable/Model/V1/EDialogChoiceType.cs b/Questionable/Model/V1/EDialogChoiceType.cs index d17a5b7ad..e8f18dda8 100644 --- a/Questionable/Model/V1/EDialogChoiceType.cs +++ b/Questionable/Model/V1/EDialogChoiceType.cs @@ -5,6 +5,4 @@ internal enum EDialogChoiceType None, YesNo, List, - ContentTalkYesNo, - ContentTalkList, } diff --git a/Questionable/Model/V1/ExcelRef.cs b/Questionable/Model/V1/ExcelRef.cs new file mode 100644 index 000000000..c6451ac2b --- /dev/null +++ b/Questionable/Model/V1/ExcelRef.cs @@ -0,0 +1,48 @@ +using System; + +namespace Questionable.Model.V1; + +public class ExcelRef +{ + private readonly string? _stringValue; + private readonly uint? _rowIdValue; + + public ExcelRef(string value) + { + _stringValue = value; + _rowIdValue = null; + Type = EType.Key; + } + + public ExcelRef(uint value) + { + _stringValue = null; + _rowIdValue = value; + Type = EType.RowId; + } + + public EType Type { get; } + + public string AsKey() + { + if (Type != EType.Key) + throw new InvalidOperationException(); + + return _stringValue!; + } + + public uint AsRowId() + { + if (Type != EType.RowId) + throw new InvalidOperationException(); + + return _rowIdValue!.Value; + } + + public enum EType + { + None, + Key, + RowId, + } +} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index b3575820c..c61828790 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -106,6 +106,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); _serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs new file mode 100644 index 000000000..ebfe6059e --- /dev/null +++ b/Questionable/Windows/ConfigWindow.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dalamud.Interface.Colors; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using ImGuiNET; +using LLib.ImGui; +using Lumina.Excel.GeneratedSheets; + +namespace Questionable.Windows; + +internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig +{ + private readonly DalamudPluginInterface _pluginInterface; + private readonly Configuration _configuration; + + private readonly uint[] _mountIds; + private readonly string[] _mountNames; + + [SuppressMessage("Performance", "CA1861", Justification = "One time initialization")] + public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager) + : base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize) + { + _pluginInterface = pluginInterface; + _configuration = configuration; + + var mounts = dataManager.GetExcelSheet()! + .Where(x => x is { RowId: > 0, Icon: > 0 }) + .Select(x => (MountId: x.RowId, Name: x.Singular.ToString())) + .Where(x => !string.IsNullOrEmpty(x.Name)) + .OrderBy(x => x.Name) + .ToList(); + _mountIds = new uint[] { 0 }.Concat(mounts.Select(x => x.MountId)).ToArray(); + _mountNames = new[] { "Mount Roulette" }.Concat(mounts.Select(x => x.Name)).ToArray(); + } + + public WindowConfig WindowConfig => _configuration.ConfigWindowConfig; + + public override void Draw() + { + if (ImGui.BeginTabBar("QuestionableConfigTabs")) + { + if (ImGui.BeginTabItem("General")) + { + int selectedMount = Array.FindIndex(_mountIds, x => x == _configuration.General.MountId); + if (selectedMount == -1) + { + selectedMount = 0; + _configuration.General.MountId = _mountIds[selectedMount]; + Save(); + } + + if (ImGui.Combo("Preferred Mount", ref selectedMount, _mountNames, _mountNames.Length)) + { + _configuration.General.MountId = _mountIds[selectedMount]; + Save(); + } + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Advanced")) + { + ImGui.TextColored(ImGuiColors.DalamudRed, + "Enabling any option here may cause unexpected behavior. Use at your own risk."); + + ImGui.Separator(); + + bool neverFly = _configuration.Advanced.NeverFly; + if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly)) + { + _configuration.Advanced.NeverFly = neverFly; + Save(); + } + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + } + + private void Save() => _pluginInterface.SavePluginConfig(_configuration); + + public void SaveWindowConfig() => Save(); +} diff --git a/Questionable/Windows/DebugWindow.cs b/Questionable/Windows/DebugWindow.cs index 7e2d353e3..03fdf7907 100644 --- a/Questionable/Windows/DebugWindow.cs +++ b/Questionable/Windows/DebugWindow.cs @@ -23,10 +23,9 @@ using Questionable.Model.V1; namespace Questionable.Windows; -internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposable +internal sealed class DebugWindow : LWindow, IPersistableWindowConfig { private readonly DalamudPluginInterface _pluginInterface; - private readonly WindowSystem _windowSystem; private readonly MovementController _movementController; private readonly QuestController _questController; private readonly GameFunctions _gameFunctions; @@ -37,14 +36,19 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab private readonly Configuration _configuration; private readonly ILogger _logger; - public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem, - MovementController movementController, QuestController questController, GameFunctions gameFunctions, - IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController, - Configuration configuration, ILogger logger) + public DebugWindow(DalamudPluginInterface pluginInterface, + MovementController movementController, + QuestController questController, + GameFunctions gameFunctions, + IClientState clientState, + IFramework framework, + ITargetManager targetManager, + GameUiController gameUiController, + Configuration configuration, + ILogger logger) : base("Questionable", ImGuiWindowFlags.AlwaysAutoResize) { _pluginInterface = pluginInterface; - _windowSystem = windowSystem; _movementController = movementController; _questController = questController; _gameFunctions = gameFunctions; @@ -61,8 +65,6 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab MinimumSize = new Vector2(200, 30), MaximumSize = default }; - - _windowSystem.AddWindow(this); } public WindowConfig WindowConfig => _configuration.DebugWindowConfig; @@ -127,6 +129,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab ImGui.Text(_questController.ToStatString()); //ImGui.EndDisabled(); + ImGui.BeginDisabled(_questController.IsRunning); if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) { _questController.ExecuteNextStep(true); @@ -139,6 +142,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab _questController.ExecuteNextStep(false); } + ImGui.EndDisabled(); ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop)) @@ -151,7 +155,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab .FindSequence(currentQuest.Sequence) ?.FindStep(currentQuest.Step); bool colored = currentStep != null && currentStep.InteractionType == EInteractionType.Instruction - && _questController.HasCurrentTaskMatching(); + && _questController + .HasCurrentTaskMatching(); if (colored) ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen); @@ -161,8 +166,16 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab _questController.Stop("Manual"); _questController.IncreaseStepCount(); } + if (colored) ImGui.PopStyleColor(); + + bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest; + if (ImGui.Checkbox("Automatically accept next quest", ref autoAcceptNextQuest)) + { + _configuration.General.AutoAcceptNextQuest = autoAcceptNextQuest; + _pluginInterface.SavePluginConfig(_configuration); + } } else ImGui.Text("No active quest"); @@ -261,7 +274,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab } else { - if (ImGui.Button($"Copy")) + ImGui.Button($"Copy"); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { ImGui.SetClipboardText($$""" "Position": { @@ -273,6 +287,12 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab "InteractionType": "" """); } + else if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + Vector3 position = _clientState.LocalPlayer!.Position; + ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture, + $"new({position.X}f, {position.Y}f, {position.Z}f)")); + } } } @@ -317,9 +337,4 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab ImGui.EndDisabled(); } } - - public void Dispose() - { - _windowSystem.RemoveWindow(this); - } }