From 09f11d1914ee9cae8a5878f1cd3c18c390df9764 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 4 Aug 2024 16:03:23 +0200 Subject: [PATCH] Add basic support for gathering custom delivery items automatically --- GatheringPathRenderer/packages.lock.json | 6 - .../Yanxia/731_Yuzuka Manor_BTN.json | 115 ++++++++++++++++++ .../Zhloe}/1551_Arms Wide Open.json | 0 .../Zhloe/S1_Zhloe Aliapoh.json | 33 +++++ .../Adkiragh/S4_Adkiragh.json | 23 ++++ .../Custom Deliveries/Kurenai/S3_Kurenai.json | 23 ++++ .../Custom Deliveries/M'naago/S2_M'naago.json | 23 ++++ .../Charlemend/S7_Charlemend.json | 26 ++++ .../Ehll Tou/S6_Ehll Tou.json | 26 ++++ .../Kai-Shirr/S5_Kai-Shirr.json | 23 ++++ .../Ameliance/S8_Ameliance.json | 27 ++++ .../Custom Deliveries/Anden/S9_Anden.json | 24 ++++ .../Margrat/S10_Margrat.json | 33 +++++ .../Studium}/4473_The Faculty.json | 0 .../MIN, BTN/4153_Cultured Pursuits.json | 0 .../MIN, BTN/4154_Cooking Up a Culture.json | 0 .../4155_The Culture of Ceruleum.json | 0 .../MIN, BTN/4156_The Culture of Carrots.json | 0 .../MIN, BTN/4157_Hinageshi in Hingashi.json | 0 .../4158_The Culture of the Past.json | 0 .../MIN, BTN/4159_The Culture of Love.json | 0 QuestPaths/QuestPaths.csproj | 4 - QuestPaths/quest-v1.json | 9 +- .../Common/EAetheryteLocation.cs | 1 + .../Converter/AethernetShardConverter.cs | 1 + .../Questing/Converter/ElementIdConverter.cs | 8 +- Questionable.Model/Questing/ElementId.cs | 46 ++++--- Questionable.Model/common-schema.json | 1 + Questionable/Controller/CommandHandler.cs | 18 +-- .../Controller/ContextMenuController.cs | 111 +++++++++++++++++ Questionable/Controller/GameUiController.cs | 6 +- Questionable/Controller/QuestController.cs | 95 +++++++++------ Questionable/Controller/QuestRegistry.cs | 33 ++--- .../Controller/Steps/Common/NextQuest.cs | 4 +- .../Controller/Steps/Interactions/Combat.cs | 4 +- .../Controller/Steps/Interactions/Interact.cs | 3 +- .../Controller/Steps/Interactions/UseItem.cs | 10 +- .../Steps/Shared/GatheringRequiredItems.cs | 2 +- .../Controller/Steps/Shared/SkipCondition.cs | 2 +- .../Controller/Steps/Shared/WaitAtEnd.cs | 8 +- Questionable/Data/AetheryteData.cs | 8 ++ Questionable/Data/GatheringData.cs | 40 +++++- Questionable/Data/JournalData.cs | 6 +- Questionable/Data/QuestData.cs | 37 +++--- Questionable/External/LifestreamIpc.cs | 6 + Questionable/GameFunctions.cs | 34 ++++-- Questionable/Model/IQuestInfo.cs | 20 +++ Questionable/Model/Quest.cs | 4 +- Questionable/Model/QuestInfo.cs | 8 +- Questionable/Model/SatisfactionSupplyInfo.cs | 23 ++++ Questionable/QuestionablePlugin.cs | 6 +- .../Validators/AethernetShortcutValidator.cs | 2 +- .../Validators/BasicSequenceValidator.cs | 12 +- .../Validators/CompletionFlagsValidator.cs | 2 +- .../Validators/JsonSchemaValidator.cs | 4 +- .../Validators/NextQuestValidator.cs | 4 +- .../Validators/QuestDisabledValidator.cs | 2 +- .../Validators/UniqueStartStopValidator.cs | 11 +- Questionable/Windows/DebugOverlay.cs | 2 +- Questionable/Windows/JournalProgressWindow.cs | 2 +- .../QuestComponents/ActiveQuestComponent.cs | 18 +-- .../QuestComponents/QuestTooltipComponent.cs | 13 +- Questionable/Windows/QuestSelectionWindow.cs | 14 +-- Questionable/Windows/UiUtils.cs | 6 +- 64 files changed, 826 insertions(+), 206 deletions(-) create mode 100644 GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json rename QuestPaths/3.x - Heavensward/{Unlocks/Custom Deliveries => Custom Deliveries/Zhloe}/1551_Arms Wide Open.json (100%) create mode 100644 QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json create mode 100644 QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json create mode 100644 QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json create mode 100644 QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json create mode 100644 QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json create mode 100644 QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json create mode 100644 QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json create mode 100644 QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json create mode 100644 QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json create mode 100644 QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/4473_The Faculty.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4153_Cultured Pursuits.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4154_Cooking Up a Culture.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4155_The Culture of Ceruleum.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4156_The Culture of Carrots.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4157_Hinageshi in Hingashi.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4158_The Culture of the Past.json (100%) rename QuestPaths/6.x - Endwalker/{Studium Deliveries => Custom Deliveries/Studium}/MIN, BTN/4159_The Culture of Love.json (100%) create mode 100644 Questionable/Controller/ContextMenuController.cs create mode 100644 Questionable/Model/IQuestInfo.cs create mode 100644 Questionable/Model/SatisfactionSupplyInfo.cs diff --git a/GatheringPathRenderer/packages.lock.json b/GatheringPathRenderer/packages.lock.json index c7a267af8..269c43d76 100644 --- a/GatheringPathRenderer/packages.lock.json +++ b/GatheringPathRenderer/packages.lock.json @@ -92,12 +92,6 @@ "ecommons": { "type": "Project" }, - "gatheringpaths": { - "type": "Project", - "dependencies": { - "Questionable.Model": "[1.0.0, )" - } - }, "questionable.model": { "type": "Project", "dependencies": { diff --git a/GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json b/GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json new file mode 100644 index 000000000..23fae294a --- /dev/null +++ b/GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json", + "Author": "liza", + "TerritoryId": 614, + "AetheryteShortcut": "Yanxia - Namai", + "Groups": [ + { + "Nodes": [ + { + "DataId": 33334, + "Locations": [ + { + "Position": { + "X": -222.386, + "Y": 23.28162, + "Z": 425.76 + } + }, + { + "Position": { + "X": -209.1725, + "Y": 22.35068, + "Z": 425.5524 + } + } + ] + }, + { + "DataId": 33333, + "Locations": [ + { + "Position": { + "X": -219.9592, + "Y": 22.78741, + "Z": 431.5036 + } + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 33335, + "Locations": [ + { + "Position": { + "X": -349.8553, + "Y": 33.90925, + "Z": 452.5893 + }, + "MinimumAngle": -90, + "MaximumAngle": 90 + } + ] + }, + { + "DataId": 33336, + "Locations": [ + { + "Position": { + "X": -361.5062, + "Y": 33.49068, + "Z": 453.4639 + } + }, + { + "Position": { + "X": -359.826, + "Y": 35.47207, + "Z": 442.164 + } + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 33331, + "Locations": [ + { + "Position": { + "X": -231.3864, + "Y": 17.74118, + "Z": 511.0694 + } + } + ] + }, + { + "DataId": 33332, + "Locations": [ + { + "Position": { + "X": -219.0789, + "Y": 18.05494, + "Z": 525.418 + } + }, + { + "Position": { + "X": -220.9139, + "Y": 17.97838, + "Z": 514.0063 + } + } + ] + } + ] + } + ] +} diff --git a/QuestPaths/3.x - Heavensward/Unlocks/Custom Deliveries/1551_Arms Wide Open.json b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/1551_Arms Wide Open.json similarity index 100% rename from QuestPaths/3.x - Heavensward/Unlocks/Custom Deliveries/1551_Arms Wide Open.json rename to QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/1551_Arms Wide Open.json 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 new file mode 100644 index 000000000..e909b81d2 --- /dev/null +++ b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "Position": { + "X": -71.31451, + "Y": 206.56206, + "Z": 29.3684 + }, + "TerritoryId": 478, + "InteractionType": "WalkTo", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Idyllshire" + }, + { + "DataId": 1019615, + "Position": { + "X": -71.763245, + "Y": 206.50021, + "Z": 32.638916 + }, + "StopDistance": 5, + "TerritoryId": 478, + "InteractionType": "Interact" + } + ] + } + ] +} diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json new file mode 100644 index 000000000..07c9f8284 --- /dev/null +++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1018393, + "Position": { + "X": -60.380005, + "Y": 206.50021, + "Z": 26.16919 + }, + "TerritoryId": 478, + "InteractionType": "Interact", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Idyllshire" + } + ] + } + ] +} diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json new file mode 100644 index 000000000..edcd6dd8f --- /dev/null +++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1025878, + "Position": { + "X": 343.984, + "Y": -120.32947, + "Z": -306.0197 + }, + "TerritoryId": 613, + "InteractionType": "Interact", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Ruby Sea - Tamamizu" + } + ] + } + ] +} 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 new file mode 100644 index 000000000..435169ec8 --- /dev/null +++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1020337, + "Position": { + "X": 171.31299, + "Y": 13.02367, + "Z": -89.951965 + }, + "TerritoryId": 635, + "InteractionType": "Interact", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Rhalgr's Reach" + } + ] + } + ] +} diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json new file mode 100644 index 000000000..0e73e7d0e --- /dev/null +++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1035211, + "Position": { + "X": -116.96039, + "Y": 0, + "Z": -133.95898 + }, + "TerritoryId": 886, + "InteractionType": "Interact", + "AetheryteShortcut": "Ishgard", + "AethernetShortcut": [ + "[Ishgard] Aetheryte Plaza", + "[Ishgard] Firmament" + ] + } + ] + } + ] +} 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 new file mode 100644 index 000000000..2e9c047b2 --- /dev/null +++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1033543, + "Position": { + "X": 113.38977, + "Y": -20, + "Z": -0.96136475 + }, + "TerritoryId": 886, + "InteractionType": "Interact", + "AetheryteShortcut": "Ishgard", + "AethernetShortcut": [ + "[Ishgard] Aetheryte Plaza", + "[Ishgard] Firmament" + ] + } + ] + } + ] +} 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 new file mode 100644 index 000000000..9e279e15b --- /dev/null +++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1031801, + "Position": { + "X": 52.8114, + "Y": 83.001076, + "Z": -65.38495 + }, + "TerritoryId": 820, + "InteractionType": "Interact", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Eulmore" + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json new file mode 100644 index 000000000..cc256d9f1 --- /dev/null +++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1042241, + "Position": { + "X": 222.85791, + "Y": 24.942732, + "Z": -197.77222 + }, + "TerritoryId": 962, + "InteractionType": "Interact", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Old Sharlayan", + "AethernetShortcut": [ + "[Old Sharlayan] Aetheryte Plaza", + "[Old Sharlayan] The Leveilleur Estate" + ] + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json new file mode 100644 index 000000000..7cdc39047 --- /dev/null +++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1044547, + "Position": { + "X": -241.68768, + "Y": 51.058994, + "Z": 620.8744 + }, + "TerritoryId": 816, + "InteractionType": "Interact", + "RequiredGatheredItems": [], + "AetheryteShortcut": "Il Mheg - Lydha Lran", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json new file mode 100644 index 000000000..3d9a80bee --- /dev/null +++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "Position": { + "X": -44.066154, + "Y": -29.530005, + "Z": -55.85129 + }, + "TerritoryId": 956, + "InteractionType": "WalkTo", + "AetheryteShortcut": "Labyrinthos - Sharlayan Hamlet", + "RequiredGatheredItems": [], + "Fly": true + }, + { + "DataId": 1046073, + "Position": { + "X": -53.635498, + "Y": -29.497286, + "Z": -65.14081 + }, + "TerritoryId": 956, + "InteractionType": "Interact" + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/4473_The Faculty.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/4473_The Faculty.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/4473_The Faculty.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/4473_The Faculty.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4153_Cultured Pursuits.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4153_Cultured Pursuits.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4154_Cooking Up a Culture.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4154_Cooking Up a Culture.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4155_The Culture of Ceruleum.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4155_The Culture of Ceruleum.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4156_The Culture of Carrots.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4156_The Culture of Carrots.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4157_Hinageshi in Hingashi.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4157_Hinageshi in Hingashi.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4158_The Culture of the Past.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4158_The Culture of the Past.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4158_The Culture of the Past.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4158_The Culture of the Past.json diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4159_The Culture of Love.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4159_The Culture of Love.json similarity index 100% rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4159_The Culture of Love.json rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4159_The Culture of Love.json diff --git a/QuestPaths/QuestPaths.csproj b/QuestPaths/QuestPaths.csproj index 5d91a016f..1e0389ef9 100644 --- a/QuestPaths/QuestPaths.csproj +++ b/QuestPaths/QuestPaths.csproj @@ -40,8 +40,4 @@ - - - - diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 9a031631e..e7d7ee2b4 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1041,7 +1041,8 @@ "PickUpQuestId": { "type": [ "null", - "number" + "number", + "string" ], "description": "Determines the quest which should be accepted. If empty/null, accepts the quest corresponding to the file name." } @@ -1061,14 +1062,16 @@ "TurnInQuestId": { "type": [ "null", - "number" + "number", + "string" ], "description": "Determines the quest which should be turned in. If empty/null, turns in the quest corresponding to the file name." }, "NextQuestId": { "type": [ "null", - "number" + "number", + "string" ], "description": "For quest chains (e.g. DT healer role quests) Which quest to do next, given that you meet the required level." } diff --git a/Questionable.Model/Common/EAetheryteLocation.cs b/Questionable.Model/Common/EAetheryteLocation.cs index a00f7b51c..3313d4df3 100644 --- a/Questionable.Model/Common/EAetheryteLocation.cs +++ b/Questionable.Model/Common/EAetheryteLocation.cs @@ -85,6 +85,7 @@ public enum EAetheryteLocation IshgardTribunal = 86, IshgardLastVigil = 87, IshgardGatesOfJudgement = 88, + IshgardFirmament = 100001, Idyllshire = 75, IdyllshireWest = 90, diff --git a/Questionable.Model/Questing/Converter/AethernetShardConverter.cs b/Questionable.Model/Questing/Converter/AethernetShardConverter.cs index bd07bc393..b1560c7c5 100644 --- a/Questionable.Model/Questing/Converter/AethernetShardConverter.cs +++ b/Questionable.Model/Questing/Converter/AethernetShardConverter.cs @@ -56,6 +56,7 @@ public sealed class AethernetShardConverter() : EnumConverter +public sealed class ElementIdConverter : JsonConverter { public override ElementId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - uint value = reader.GetUInt32(); - return ElementId.From(value); + if (reader.TokenType == JsonTokenType.Number) + return new QuestId(reader.GetUInt16()); + else + return ElementId.FromString(reader.GetString() ?? throw new JsonException()); } public override void Write(Utf8JsonWriter writer, ElementId value, JsonSerializerOptions options) diff --git a/Questionable.Model/Questing/ElementId.cs b/Questionable.Model/Questing/ElementId.cs index e4b3c71ce..396c3524b 100644 --- a/Questionable.Model/Questing/ElementId.cs +++ b/Questionable.Model/Questing/ElementId.cs @@ -50,37 +50,51 @@ public abstract class ElementId : IComparable, IEquatable return !Equals(left, right); } - public static ElementId From(uint value) + public static ElementId FromString(string value) { - if (value >= 100_000 && value < 200_000) - return new LeveId((ushort)(value - 100_000)); + if (value.StartsWith("L")) + return new LeveId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture)); + else if (value.StartsWith("S")) + return new SatisfactionSupplyNpcId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture)); else - return new QuestId((ushort)value); + return new QuestId(ushort.Parse(value, CultureInfo.InvariantCulture)); + } + + public static bool TryFromString(string value, out ElementId? elementId) + { + try + { + elementId = FromString(value); + return true; + } + catch (Exception) + { + elementId = null; + return false; + } } } -public sealed class QuestId : ElementId +public sealed class QuestId(ushort value) : ElementId(value) { - public QuestId(ushort value) - : base(value) - { - } - public override string ToString() { return Value.ToString(CultureInfo.InvariantCulture); } } -public sealed class LeveId : ElementId +public sealed class LeveId(ushort value) : ElementId(value) { - public LeveId(ushort value) - : base(value) - { - } - public override string ToString() { return "L" + Value.ToString(CultureInfo.InvariantCulture); } } + +public sealed class SatisfactionSupplyNpcId(ushort value) : ElementId(value) +{ + public override string ToString() + { + return "S" + Value.ToString(CultureInfo.InvariantCulture); + } +} diff --git a/Questionable.Model/common-schema.json b/Questionable.Model/common-schema.json index 6017ecb7f..01d1ef6c3 100644 --- a/Questionable.Model/common-schema.json +++ b/Questionable.Model/common-schema.json @@ -161,6 +161,7 @@ "[Ishgard] The Tribunal", "[Ishgard] The Last Vigil", "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)", + "[Ishgard] Firmament", "[Idyllshire] Aetheryte Plaza", "[Idyllshire] West Idyllshire", "[Idyllshire] Prologue Gate (Western Hinterlands)", diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs index 5e88cf85a..142d3263a 100644 --- a/Questionable/Controller/CommandHandler.cs +++ b/Questionable/Controller/CommandHandler.cs @@ -77,7 +77,7 @@ internal sealed class CommandHandler : IDisposable case "start": _questWindow.IsOpen = true; - _questController.ExecuteNextStep(true); + _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic); break; case "stop": @@ -128,11 +128,11 @@ internal sealed class CommandHandler : IDisposable return; } - if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId)) + if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null) { - if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest)) + if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { - _debugOverlay.HighlightedQuest = quest.QuestElementId; + _debugOverlay.HighlightedQuest = quest.Id; _chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name})."); } else @@ -147,11 +147,11 @@ internal sealed class CommandHandler : IDisposable private void SetNextQuest(string[] arguments) { - if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId)) + if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null) { - if (_gameFunctions.IsQuestLocked(ElementId.From(questId))) + if (_gameFunctions.IsQuestLocked(questId)) _chatGui.PrintError($"[Questionable] Quest {questId} is locked."); - else if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest)) + else if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { _questController.SetNextQuest(quest); _chatGui.Print($"[Questionable] Set next quest to {questId} ({quest.Info.Name})."); @@ -170,9 +170,9 @@ internal sealed class CommandHandler : IDisposable private void SetSimulatedQuest(string[] arguments) { - if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId)) + if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null) { - if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest)) + if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { _questController.SimulateQuest(quest); _chatGui.Print($"[Questionable] Simulating quest {questId} ({quest.Info.Name})."); diff --git a/Questionable/Controller/ContextMenuController.cs b/Questionable/Controller/ContextMenuController.cs new file mode 100644 index 000000000..13c5e03f4 --- /dev/null +++ b/Questionable/Controller/ContextMenuController.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Text; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using Microsoft.Extensions.Logging; +using Questionable.Data; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Controller; + +internal sealed class ContextMenuController : IDisposable +{ + private readonly IContextMenu _contextMenu; + private readonly QuestController _questController; + private readonly GatheringData _gatheringData; + private readonly QuestRegistry _questRegistry; + private readonly QuestData _questData; + private readonly IGameGui _gameGui; + private readonly IChatGui _chatGui; + private readonly IClientState _clientState; + private readonly ILogger _logger; + + public ContextMenuController( + IContextMenu contextMenu, + QuestController questController, + GatheringData gatheringData, + QuestRegistry questRegistry, + QuestData questData, + IGameGui gameGui, + IChatGui chatGui, + IClientState clientState, + ILogger logger) + { + _contextMenu = contextMenu; + _questController = questController; + _gatheringData = gatheringData; + _questRegistry = questRegistry; + _questData = questData; + _gameGui = gameGui; + _chatGui = chatGui; + _clientState = clientState; + _logger = logger; + + _contextMenu.OnMenuOpened += MenuOpened; + } + + private void MenuOpened(IMenuOpenedArgs args) + { + uint itemId = (uint) _gameGui.HoveredItem; + if (itemId == 0) + return; + + if (itemId > 1_000_000) + itemId -= 1_000_000; + + if (itemId >= 500_000) + itemId -= 500_000; + + if (!_gatheringData.TryGetGatheringPointId(itemId, _clientState.LocalPlayer!.ClassJob.Id, 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; + + args.AddMenuItem(new MenuItem + { + Prefix = SeIconChar.Hyadelyn, + PrefixColor = 52, + Name = "Gather with Questionable", + OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability), + }); + } + } + + private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability) + { + 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)!; + step.RequiredGatheredItems = + [ + new GatheredItem + { + ItemId = itemId, + ItemCount = quantity, + Collectability = collectability + } + ]; + _questController.SetNextQuest(quest); + _questController.ExecuteNextStep(QuestController.EAutomationType.CurrentQuestOnly); + } + else + _chatGui.PrintError($"No associated quest ({info.QuestId}).", "Questionable"); + } + + public void Dispose() + { + _contextMenu.OnMenuOpened -= MenuOpened; + } +} diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index e7df22139..7f8a6223d 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -600,7 +600,7 @@ internal sealed class GameUiController : IDisposable private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args) { - if (_questController.StartedQuest?.Quest.QuestElementId.Value == 4526) + if (_questController.StartedQuest?.Quest.Id.Value == 4526) { _logger.LogInformation("Closing Unending Codex"); AtkUnitBase* addon = (AtkUnitBase*)args.Addon; @@ -610,7 +610,7 @@ internal sealed class GameUiController : IDisposable private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args) { - if (_questController.StartedQuest?.Quest.QuestElementId.Value == 245) + if (_questController.StartedQuest?.Quest.Id.Value == 245) { _logger.LogInformation("Closing ContentsTutorial"); AtkUnitBase* addon = (AtkUnitBase*)args.Addon; @@ -623,7 +623,7 @@ internal sealed class GameUiController : IDisposable /// private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args) { - if (_questController.StartedQuest?.Quest.QuestElementId.Value == 245) + if (_questController.StartedQuest?.Quest.Id.Value == 245) { _logger.LogInformation("Closing MultipleHelpWindow"); AtkUnitBase* addon = (AtkUnitBase*)args.Addon; diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index dcd3b8b02..c953b851e 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -33,7 +33,7 @@ internal sealed class QuestController : MiniTaskController private QuestProgress? _startedQuest; private QuestProgress? _nextQuest; private QuestProgress? _simulatedQuest; - private bool _automatic; + private EAutomationType _automationType; /// /// Some combat encounters finish relatively early (i.e. they're done as part of progressing the quest, but not @@ -71,16 +71,16 @@ internal sealed class QuestController : MiniTaskController _taskFactories = taskFactories.ToList().AsReadOnly(); } - public (QuestProgress Progress, CurrentQuestType Type)? CurrentQuestDetails + public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails { get { if (_simulatedQuest != null) - return (_simulatedQuest, CurrentQuestType.Simulated); - else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.QuestElementId)) - return (_nextQuest, CurrentQuestType.Next); + return (_simulatedQuest, ECurrentQuestType.Simulated); + else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) + return (_nextQuest, ECurrentQuestType.Next); else if (_startedQuest != null) - return (_startedQuest, CurrentQuestType.Normal); + return (_startedQuest, ECurrentQuestType.Normal); else return null; } @@ -153,10 +153,11 @@ internal sealed class QuestController : MiniTaskController if (CurrentQuest != null && CurrentQuest.Quest.Root.TerritoryBlacklist.Contains(_clientState.TerritoryType)) return; - if (_automatic && ((_currentTask == null && _taskQueue.Count == 0) || - _currentTask is WaitAtEnd.WaitQuestAccepted) - && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 } - && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15)) + if (_automationType == EAutomationType.Automatic && + ((_currentTask == null && _taskQueue.Count == 0) || + _currentTask is WaitAtEnd.WaitQuestAccepted) + && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 } + && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15)) { lock (_progressLock) { @@ -164,7 +165,7 @@ internal sealed class QuestController : MiniTaskController CurrentQuest.SetStep(0); } - ExecuteNextStep(true); + ExecuteNextStep(_automationType); return; } @@ -182,13 +183,14 @@ 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.QuestElementId); + canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.Id); else - canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.QuestElementId); + canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id); if (!canUseNextQuest) { - _logger.LogInformation("Next quest {QuestId} accepted or completed", _nextQuest.Quest.QuestElementId); + _logger.LogInformation("Next quest {QuestId} accepted or completed", + _nextQuest.Quest.Id); _nextQuest = null; } } @@ -200,12 +202,15 @@ internal sealed class QuestController : MiniTaskController currentSequence = _simulatedQuest.Sequence; questToRun = _simulatedQuest; } - else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.QuestElementId)) + else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) { questToRun = _nextQuest; currentSequence = _nextQuest.Sequence; // by definition, this should always be 0 - if (_nextQuest.Step == 0 && _currentTask == null && _taskQueue.Count == 0 && _automatic) - ExecuteNextStep(true); + if (_nextQuest.Step == 0 && + _currentTask == null && + _taskQueue.Count == 0 && + _automationType == EAutomationType.Automatic) + ExecuteNextStep(_automationType); } else { @@ -221,7 +226,7 @@ internal sealed class QuestController : MiniTaskController questToRun = null; } - else if (_startedQuest == null || _startedQuest.Quest.QuestElementId != currentQuestId) + else if (_startedQuest == null || _startedQuest.Quest.Id != currentQuestId) { if (_questRegistry.TryGetQuest(currentQuestId, out var quest)) { @@ -341,11 +346,11 @@ internal sealed class QuestController : MiniTaskController return; } - if (questId != null && CurrentQuest.Quest.QuestElementId != questId) + if (questId != null && CurrentQuest.Quest.Id != questId) { _logger.LogWarning( "Ignoring 'increase step count' for different quest (expected {ExpectedQuestId}, but we are at {CurrentQuestId}", - questId, CurrentQuest.Quest.QuestElementId); + questId, CurrentQuest.Quest.Id); return; } @@ -363,8 +368,8 @@ internal sealed class QuestController : MiniTaskController CurrentQuest.SetStep(255); } - if (shouldContinue && _automatic) - ExecuteNextStep(true); + if (shouldContinue && _automationType != EAutomationType.Manual) + ExecuteNextStep(_automationType); } private void ClearTasksInternal() @@ -387,17 +392,17 @@ internal sealed class QuestController : MiniTaskController ClearTasksInternal(); // reset task queue - if (continueIfAutomatic && _automatic) + if (continueIfAutomatic && _automationType == EAutomationType.Automatic) { if (CurrentQuest?.Step is >= 0 and < 255) - ExecuteNextStep(true); + ExecuteNextStep(_automationType); else _logger.LogInformation("Couldn't execute next step during Stop() call"); } - else if (_automatic) + else if (_automationType != EAutomationType.Manual) { _logger.LogInformation("Stopping automatic questing"); - _automatic = false; + _automationType = EAutomationType.Manual; _nextQuest = null; } } @@ -406,7 +411,7 @@ internal sealed class QuestController : MiniTaskController public void SimulateQuest(Quest? quest) { - _logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestElementId); + _logger.LogInformation("SimulateQuest: {QuestId}", quest?.Id); if (quest != null) _simulatedQuest = new QuestProgress(quest); else @@ -415,7 +420,7 @@ internal sealed class QuestController : MiniTaskController public void SetNextQuest(Quest? quest) { - _logger.LogInformation("NextQuest: {QuestId}", quest?.QuestElementId); + _logger.LogInformation("NextQuest: {QuestId}", quest?.Id); if (quest != null) _nextQuest = new QuestProgress(quest); else @@ -441,10 +446,10 @@ internal sealed class QuestController : MiniTaskController IncreaseStepCount(task.QuestElementId, task.Sequence, true); } - public void ExecuteNextStep(bool automatic) + public void ExecuteNextStep(EAutomationType automatic) { ClearTasksInternal(); - _automatic = automatic; + _automationType = automatic; if (TryPickPriorityQuest()) _logger.LogInformation("Using priority quest over current quest"); @@ -452,8 +457,21 @@ internal sealed class QuestController : MiniTaskController (QuestSequence? seq, QuestStep? step) = GetNextStep(); if (CurrentQuest == null || seq == null || step == null) { - _logger.LogWarning("Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]", - CurrentQuest?.Quest.QuestElementId, CurrentQuest?.Sequence, CurrentQuest?.Step); + if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId && + CurrentQuestDetails?.Progress.Sequence == 0 && + CurrentQuestDetails?.Progress.Step == 255 && + CurrentQuestDetails?.Type == ECurrentQuestType.Next) + { + _logger.LogInformation("Completed delivery quest"); + SetNextQuest(null); + } + else + { + _logger.LogWarning( + "Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]", + CurrentQuest?.Quest.Id, CurrentQuest?.Sequence, CurrentQuest?.Step); + } + return; } @@ -488,7 +506,7 @@ internal sealed class QuestController : MiniTaskController } _logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}", - CurrentQuest.Quest.QuestElementId, seq.Sequence, seq.Steps.IndexOf(step), + CurrentQuest.Quest.Id, seq.Sequence, seq.Steps.IndexOf(step), string.Join(", ", newTasks.Select(x => x.ToString()))); foreach (var task in newTasks) _taskQueue.Enqueue(task); @@ -587,7 +605,7 @@ internal sealed class QuestController : MiniTaskController return false; var (currentQuest, type) = details.Value; - if (type != CurrentQuestType.Normal) + if (type != ECurrentQuestType.Normal) return false; QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence); @@ -628,10 +646,17 @@ internal sealed class QuestController : MiniTaskController DateTime StartedAt, int PointMenuCounter = 0); - public enum CurrentQuestType + public enum ECurrentQuestType { Normal, Next, Simulated, } + + public enum EAutomationType + { + Manual, + Automatic, + CurrentQuestOnly, + } } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 67cd658e2..58b93f3ff 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -91,12 +91,12 @@ internal sealed class QuestRegistry { Quest quest = new() { - QuestElementId = new QuestId(questId), + Id = new QuestId(questId), Root = questRoot, Info = _questData.GetQuestInfo(new QuestId(questId)), ReadOnly = true, }; - _quests[quest.QuestElementId] = quest; + _quests[quest.Id] = quest; } _logger.LogInformation("Loaded {Count} quests from assembly", _quests.Count); @@ -145,12 +145,12 @@ internal sealed class QuestRegistry Quest quest = new Quest { - QuestElementId = questId, + Id = questId, Root = questNode.Deserialize()!, Info = _questData.GetQuestInfo(questId), ReadOnly = false, }; - _quests[quest.QuestElementId] = quest; + _quests[quest.Id] = quest; } private void LoadFromDirectory(DirectoryInfo directory, LogLevel logLevel = LogLevel.Information) @@ -188,30 +188,11 @@ internal sealed class QuestRegistry return null; string[] parts = name.Split('_', 2); - return ElementId.From(uint.Parse(parts[0], CultureInfo.InvariantCulture)); + return ElementId.FromString(parts[0]); } - public bool IsKnownQuest(ElementId elementId) - { - if (elementId is QuestId questId) - return IsKnownQuest(questId); - else - return false; - } + public bool IsKnownQuest(ElementId questId) => _quests.ContainsKey(questId); - public bool IsKnownQuest(QuestId questId) => _quests.ContainsKey(questId); - - public bool TryGetQuest(ElementId elementId, [NotNullWhen(true)] out Quest? quest) - { - if (elementId is QuestId questId) - return TryGetQuest(questId, out quest); - else - { - quest = null; - return false; - } - } - - public bool TryGetQuest(QuestId questId, [NotNullWhen(true)] out Quest? quest) + public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest) => _quests.TryGetValue(questId, out quest); } diff --git a/Questionable/Controller/Steps/Common/NextQuest.cs b/Questionable/Controller/Steps/Common/NextQuest.cs index ecc521b3a..2afc6ecb1 100644 --- a/Questionable/Controller/Steps/Common/NextQuest.cs +++ b/Questionable/Controller/Steps/Common/NextQuest.cs @@ -18,11 +18,11 @@ internal static class NextQuest if (step.NextQuestId == null) return null; - if (step.NextQuestId == quest.QuestElementId) + if (step.NextQuestId == quest.Id) return null; return serviceProvider.GetRequiredService() - .With(step.NextQuestId, quest.QuestElementId); + .With(step.NextQuestId, quest.Id); } } diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index d6a3f52a8..d23608f0b 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -47,7 +47,7 @@ internal static class Combat ArgumentNullException.ThrowIfNull(step.ItemId); yield return serviceProvider.GetRequiredService() - .With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, + .With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, true); yield return CreateTask(quest, sequence, step); break; @@ -73,7 +73,7 @@ internal static class Combat bool isLastStep = sequence.Steps.Last() == step; return serviceProvider.GetRequiredService() - .With(quest.QuestElementId, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds, + .With(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds, step.CompletionQuestVariablesFlags, step.ComplexCombatData); } } diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index df27d3c21..869a1fcfd 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -33,7 +33,8 @@ internal static class Interact yield return serviceProvider.GetRequiredService(); yield return serviceProvider.GetRequiredService() - .With(step.DataId.Value, step.TargetTerritoryId != null); + .With(step.DataId.Value, + step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId); } public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step) diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index ea46939ce..cf8664144 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -48,7 +48,7 @@ internal static class UseItem } var task = serviceProvider.GetRequiredService() - .With(quest.QuestElementId, step.ItemId.Value, step.CompletionQuestVariablesFlags); + .With(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags); return [ unmount, task, @@ -65,12 +65,12 @@ internal static class UseItem ITask task; if (step.DataId != null) task = serviceProvider.GetRequiredService() - .With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags); + .With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags); else { ArgumentNullException.ThrowIfNull(step.Position); task = serviceProvider.GetRequiredService() - .With(quest.QuestElementId, step.Position.Value, step.ItemId.Value, + .With(quest.Id, step.Position.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags); } @@ -79,13 +79,13 @@ internal static class UseItem else if (step.DataId != null) { var task = serviceProvider.GetRequiredService() - .With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags); + .With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags); return [unmount, task]; } else { var task = serviceProvider.GetRequiredService() - .With(quest.QuestElementId, step.ItemId.Value, step.CompletionQuestVariablesFlags); + .With(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags); return [unmount, task]; } } diff --git a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs index aebb66dab..a2428a8d1 100644 --- a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs +++ b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs @@ -33,7 +33,7 @@ internal static class GatheringRequiredItems if (!AssemblyGatheringLocationLoader.GetLocations() .TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot)) - throw new TaskException("No path found for gathering point"); + throw new TaskException($"No path found for gathering point {gatheringPointId}"); if (HasRequiredItems(requiredGatheredItems)) continue; diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index a122d57cb..c95894eb8 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -34,7 +34,7 @@ internal static class SkipCondition return null; return serviceProvider.GetRequiredService() - .With(step, skipConditions ?? new(), quest.QuestElementId); + .With(step, skipConditions ?? new(), quest.Id); } } diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index 1b84c2bc0..fcaacdf64 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -30,7 +30,7 @@ internal static class WaitAtEnd if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags)) { var task = serviceProvider.GetRequiredService() - .With((QuestId)quest.QuestElementId, step); + .With((QuestId)quest.Id, step); var delay = serviceProvider.GetRequiredService(); return [task, delay, Next(quest, sequence)]; } @@ -110,7 +110,7 @@ internal static class WaitAtEnd case EInteractionType.AcceptQuest: { var accept = serviceProvider.GetRequiredService() - .With(step.PickUpQuestId ?? quest.QuestElementId); + .With(step.PickUpQuestId ?? quest.Id); var delay = serviceProvider.GetRequiredService(); if (step.PickUpQuestId != null) return [accept, delay, Next(quest, sequence)]; @@ -121,7 +121,7 @@ internal static class WaitAtEnd case EInteractionType.CompleteQuest: { var complete = serviceProvider.GetRequiredService() - .With(step.TurnInQuestId ?? quest.QuestElementId); + .With(step.TurnInQuestId ?? quest.Id); var delay = serviceProvider.GetRequiredService(); if (step.TurnInQuestId != null) return [complete, delay, Next(quest, sequence)]; @@ -140,7 +140,7 @@ internal static class WaitAtEnd private static NextStep Next(Quest quest, QuestSequence sequence) { - return new NextStep(quest.QuestElementId, sequence.Sequence); + return new NextStep(quest.Id, sequence.Sequence); } } diff --git a/Questionable/Data/AetheryteData.cs b/Questionable/Data/AetheryteData.cs index 28d8e459d..74d183130 100644 --- a/Questionable/Data/AetheryteData.cs +++ b/Questionable/Data/AetheryteData.cs @@ -28,6 +28,10 @@ internal sealed class AetheryteData aethernetGroups[(EAetheryteLocation)aetheryte.RowId] = aetheryte.AethernetGroup; } + aethernetNames[EAetheryteLocation.IshgardFirmament] = "Firmament"; + territoryIds[EAetheryteLocation.IshgardFirmament] = 886; + aethernetGroups[EAetheryteLocation.IshgardFirmament] = aethernetGroups[EAetheryteLocation.Ishgard]; + AethernetNames = aethernetNames.AsReadOnly(); TerritoryIds = territoryIds.AsReadOnly(); AethernetGroups = aethernetGroups.AsReadOnly(); @@ -267,6 +271,7 @@ internal sealed class AetheryteData { 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) }, + { EAetheryteLocation.IshgardFirmament, new(9.92315f, -15.2f, 173.5059f) }, }.AsReadOnly(); public ReadOnlyDictionary AethernetNames { get; } @@ -298,6 +303,9 @@ internal sealed class AetheryteData public bool IsCityAetheryte(EAetheryteLocation aetheryte) { + if (aetheryte == EAetheryteLocation.IshgardFirmament) + return true; + var territoryId = TerritoryIds[aetheryte]; return TownTerritoryIds.Contains(territoryId); } diff --git a/Questionable/Data/GatheringData.cs b/Questionable/Data/GatheringData.cs index ed44fdc2c..5877a1a3b 100644 --- a/Questionable/Data/GatheringData.cs +++ b/Questionable/Data/GatheringData.cs @@ -1,20 +1,21 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; +using Microsoft.Extensions.Logging; namespace Questionable.Data; internal sealed class GatheringData { - private readonly Dictionary _gatheringItemToItem; private readonly Dictionary _minerGatheringPoints = []; private readonly Dictionary _botanistGatheringPoints = []; + private readonly Dictionary _itemIdToCollectability; + private readonly Dictionary _npcForCustomDeliveries; public GatheringData(IDataManager dataManager) { - _gatheringItemToItem = dataManager.GetExcelSheet()! + Dictionary gatheringItemToItem = dataManager.GetExcelSheet()! .Where(x => x.RowId != 0 && x.Item != 0) .ToDictionary(x => x.RowId, x => (uint)x.Item); @@ -22,7 +23,7 @@ internal sealed class GatheringData { foreach (var gatheringItemId in gatheringPointBase.Item.Where(x => x != 0)) { - if (_gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId)) + if (gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId)) { if (gatheringPointBase.GatheringType.Row is 0 or 1) _minerGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId; @@ -31,8 +32,31 @@ internal sealed class GatheringData } } } - } + _itemIdToCollectability = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .Where(x => x.Slot is 2) + .Select(x => new + { + ItemId = x.Item.Row, + Collectability = x.CollectabilityHigh, + }) + .Distinct() + .ToDictionary(x => x.ItemId, x => x.Collectability); + + _npcForCustomDeliveries = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .SelectMany(x => dataManager.GetExcelSheet()! + .Where(y => y.RowId == x.SupplyIndex.Last()) + .Select(y => new + { + ItemId = y.Item.Row, + NpcId = x.Npc.Row + })) + .Where(x => x.ItemId > 0) + .Distinct() + .ToDictionary(x => x.ItemId, x => x.NpcId); + } public bool TryGetGatheringPointId(uint itemId, uint classJobId, out ushort gatheringPointId) { @@ -46,4 +70,10 @@ internal sealed class GatheringData return false; } } + + public ushort GetRecommendedCollectability(uint itemId) + => _itemIdToCollectability.GetValueOrDefault(itemId); + + public bool TryGetCustomDeliveryNpc(uint itemId, out uint npcId) + => _npcForCustomDeliveries.TryGetValue(itemId, out npcId); } diff --git a/Questionable/Data/JournalData.cs b/Questionable/Data/JournalData.cs index 171dfe827..ae6c6ac2a 100644 --- a/Questionable/Data/JournalData.cs +++ b/Questionable/Data/JournalData.cs @@ -22,17 +22,17 @@ internal sealed class JournalData var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1, new uint[] { 108, 109 }.Concat(limsaStart.Quest.Select(x => x.Row)) .Where(x => x != 0) - .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) + .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) .ToList()); var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1, new uint[] { 85, 123, 124 }.Concat(gridaniaStart.Quest.Select(x => x.Row)) .Where(x => x != 0) - .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) + .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) .ToList()); var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1, new uint[] { 568, 569, 570 }.Concat(uldahStart.Quest.Select(x => x.Row)) .Where(x => x != 0) - .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) + .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) .ToList()); genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]); genres.Single(x => x.Id == 1) diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index aef7b726d..1cf0733e4 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Dalamud.Plugin.Services; +using Lumina.Excel.GeneratedSheets; using Questionable.Model; using Questionable.Model.Questing; using Quest = Lumina.Excel.GeneratedSheets.Quest; @@ -11,32 +12,30 @@ namespace Questionable.Data; internal sealed class QuestData { - private readonly ImmutableDictionary _quests; + private readonly Dictionary _quests; public QuestData(IDataManager dataManager) { - _quests = dataManager.GetExcelSheet()! - .Where(x => x.RowId > 0) - .Where(x => x.IssuerLocation.Row > 0) - .Where(x => x.Festival.Row == 0) - .Select(x => new QuestInfo(x)) - .ToImmutableDictionary(x => x.QuestId, x => x); + List quests = + [ + ..dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .Where(x => x.IssuerLocation.Row > 0) + .Where(x => x.Festival.Row == 0) + .Select(x => new QuestInfo(x)), + ..dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .Select(x => new SatisfactionSupplyInfo(x)) + ]; + _quests = quests.ToDictionary(x => x.QuestId, x => x); } - public QuestInfo GetQuestInfo(ElementId elementId) + public IQuestInfo GetQuestInfo(ElementId elementId) { - if (elementId is QuestId questId) - return GetQuestInfo(questId); - - throw new ArgumentException("Invalid id", nameof(elementId)); + return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId)); } - public QuestInfo GetQuestInfo(QuestId questId) - { - return _quests[questId] ?? throw new ArgumentOutOfRangeException(nameof(questId)); - } - - public List GetAllByIssuerDataId(uint targetId) + public List GetAllByIssuerDataId(uint targetId) { return _quests.Values .Where(x => x.IssuerDataId == targetId) @@ -48,6 +47,8 @@ internal sealed class QuestData public List GetAllByJournalGenre(uint journalGenre) { return _quests.Values + .Where(x => x is QuestInfo) + .Cast() .Where(x => x.JournalGenre == journalGenre) .OrderBy(x => x.SortKey) .ThenBy(x => x.QuestId) diff --git a/Questionable/External/LifestreamIpc.cs b/Questionable/External/LifestreamIpc.cs index f32677090..f5bd45448 100644 --- a/Questionable/External/LifestreamIpc.cs +++ b/Questionable/External/LifestreamIpc.cs @@ -18,6 +18,12 @@ internal sealed class LifestreamIpc public bool Teleport(EAetheryteLocation aetheryteLocation) { + if (aetheryteLocation == EAetheryteLocation.IshgardFirmament) + { + // TODO does this even work on non-EN clients? + return _aethernetTeleport.InvokeFunc("Firmament"); + } + if (!_aetheryteData.AethernetNames.TryGetValue(aetheryteLocation, out string? name)) return false; diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index 83f3b27db..b247cd800 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -246,7 +246,10 @@ internal sealed unsafe class GameFunctions { if (elementId is QuestId questId) return IsReadyToAcceptQuest(questId); - return false; + else if (elementId is SatisfactionSupplyNpcId) + return true; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); } public bool IsReadyToAcceptQuest(QuestId questId) @@ -283,7 +286,10 @@ internal sealed unsafe class GameFunctions { if (elementId is QuestId questId) return IsQuestAccepted(questId); - return false; + else if (elementId is SatisfactionSupplyNpcId) + return false; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); } public bool IsQuestAccepted(QuestId questId) @@ -296,7 +302,10 @@ internal sealed unsafe class GameFunctions { if (elementId is QuestId questId) return IsQuestComplete(questId); - return false; + else if (elementId is SatisfactionSupplyNpcId) + return false; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); } [SuppressMessage("Performance", "CA1822")] @@ -309,12 +318,15 @@ internal sealed unsafe class GameFunctions { if (elementId is QuestId questId) return IsQuestLocked(questId, extraCompletedQuest); - return false; + else if (elementId is SatisfactionSupplyNpcId) + return false; + else + throw new ArgumentOutOfRangeException(nameof(elementId)); } public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null) { - var questInfo = _questData.GetQuestInfo(questId); + var questInfo = (QuestInfo) _questData.GetQuestInfo(questId); if (questInfo.QuestLocks.Count > 0) { var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); @@ -369,7 +381,11 @@ internal sealed unsafe class GameFunctions } public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation) - => IsAetheryteUnlocked((uint)aetheryteLocation, out _); + { + if (aetheryteLocation == EAetheryteLocation.IshgardFirmament) + return IsQuestComplete(new QuestId(3672)); + return IsAetheryteUnlocked((uint)aetheryteLocation, out _); + } public bool CanTeleport(EAetheryteLocation aetheryteLocation) { @@ -707,15 +723,15 @@ internal sealed unsafe class GameFunctions if (excelSheetName == null) { var questRow = - _dataManager.GetExcelSheet()!.GetRow((uint)currentQuest.QuestElementId.Value + + _dataManager.GetExcelSheet()!.GetRow((uint)currentQuest.Id.Value + 0x10000); if (questRow == null) { - _logger.LogError("Could not find quest row for {QuestId}", currentQuest.QuestElementId); + _logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id); return null; } - excelSheetName = $"quest/{(currentQuest.QuestElementId.Value / 100):000}/{questRow.Id}"; + excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}"; } var excelSheet = _dataManager.Excel.GetSheet(excelSheetName); diff --git a/Questionable/Model/IQuestInfo.cs b/Questionable/Model/IQuestInfo.cs new file mode 100644 index 000000000..6822cce9b --- /dev/null +++ b/Questionable/Model/IQuestInfo.cs @@ -0,0 +1,20 @@ +using System; +using Dalamud.Game.Text; +using Questionable.Model.Questing; + +namespace Questionable.Model; + +public interface IQuestInfo +{ + public ElementId QuestId { get; } + public string Name { get; } + public uint IssuerDataId { get; } + public bool IsRepeatable { get; } + public ushort Level { get; } + public EBeastTribe BeastTribe { get; } + public bool IsMainScenarioQuest { get; } + + public string SimplifiedName => Name + .Replace(".", "", StringComparison.Ordinal) + .TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' '); +} diff --git a/Questionable/Model/Quest.cs b/Questionable/Model/Quest.cs index 391e0602b..3befbc698 100644 --- a/Questionable/Model/Quest.cs +++ b/Questionable/Model/Quest.cs @@ -6,9 +6,9 @@ namespace Questionable.Model; internal sealed class Quest { - public required ElementId QuestElementId { get; init; } + public required ElementId Id { get; init; } public required QuestRoot Root { get; init; } - public required QuestInfo Info { get; init; } + public required IQuestInfo Info { get; init; } public required bool ReadOnly { get; init; } public QuestSequence? FindSequence(byte currentSequence) diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 59a3adad6..526b973a2 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -10,7 +10,7 @@ using ExcelQuest = Lumina.Excel.GeneratedSheets.Quest; namespace Questionable.Model; -internal sealed class QuestInfo +internal sealed class QuestInfo : IQuestInfo { public QuestInfo(ExcelQuest quest) { @@ -56,7 +56,7 @@ internal sealed class QuestInfo } - public QuestId QuestId { get; } + public ElementId QuestId { get; } public string Name { get; } public ushort Level { get; } public uint IssuerDataId { get; } @@ -74,10 +74,6 @@ internal sealed class QuestInfo public GrandCompany GrandCompany { get; } public EBeastTribe BeastTribe { get; } - public string SimplifiedName => Name - .Replace(".", "", StringComparison.Ordinal) - .TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' '); - [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] public enum QuestJoin : byte { diff --git a/Questionable/Model/SatisfactionSupplyInfo.cs b/Questionable/Model/SatisfactionSupplyInfo.cs new file mode 100644 index 000000000..b5d11bfed --- /dev/null +++ b/Questionable/Model/SatisfactionSupplyInfo.cs @@ -0,0 +1,23 @@ +using Lumina.Excel.GeneratedSheets; +using Questionable.Model.Questing; + +namespace Questionable.Model; + +internal sealed class SatisfactionSupplyInfo : IQuestInfo +{ + public SatisfactionSupplyInfo(SatisfactionNpc npc) + { + QuestId = new SatisfactionSupplyNpcId((ushort)npc.RowId); + Name = npc.Npc.Value!.Singular; + IssuerDataId = npc.Npc.Row; + Level = npc.LevelUnlock; + } + + public ElementId QuestId { get; } + public string Name { get; } + public uint IssuerDataId { get; } + public bool IsRepeatable => true; + public ushort Level { get; } + public EBeastTribe BeastTribe => EBeastTribe.None; + public bool IsMainScenarioQuest => false; +} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 944e6bebb..477a37232 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -43,7 +43,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin IChatGui chatGui, ICommandManager commandManager, IAddonLifecycle addonLifecycle, - IKeyState keyState) + IKeyState keyState, + IContextMenu contextMenu) { ArgumentNullException.ThrowIfNull(pluginInterface); @@ -66,6 +67,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin 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()); @@ -81,6 +83,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin _serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider.GetRequiredService().Reload(); _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); } @@ -156,6 +159,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } diff --git a/Questionable/Validation/Validators/AethernetShortcutValidator.cs b/Questionable/Validation/Validators/AethernetShortcutValidator.cs index f5820ac24..af380f8a5 100644 --- a/Questionable/Validation/Validators/AethernetShortcutValidator.cs +++ b/Questionable/Validation/Validators/AethernetShortcutValidator.cs @@ -18,7 +18,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator public IEnumerable Validate(Quest quest) { return quest.AllSteps() - .Select(x => Validate(quest.QuestElementId, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut)) + .Select(x => Validate(quest.Id, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut)) .Where(x => x != null) .Cast(); } diff --git a/Questionable/Validation/Validators/BasicSequenceValidator.cs b/Questionable/Validation/Validators/BasicSequenceValidator.cs index a7330d8ff..7fa73dfb9 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.QuestElementId, + QuestId = quest.Id, Sequence = 0, Step = null, Type = EIssueType.MissingSequence0, @@ -28,7 +28,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator yield break; } - if (quest.Info.CompletesInstantly) + if (quest.Info is QuestInfo { CompletesInstantly: true }) { foreach (var sequence in sequences) { @@ -37,7 +37,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = (byte)sequence.Sequence, Step = null, Type = EIssueType.InstantQuestWithMultipleSteps, @@ -46,7 +46,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator }; } } - else + else if (quest.Info is QuestInfo) { int maxSequence = sequences.Select(x => x.Sequence) .Where(x => x != 255) @@ -73,7 +73,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator { return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = (byte)sequenceNo, Step = null, Type = EIssueType.MissingSequence, @@ -85,7 +85,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator { return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = 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 76b539e20..9aa67760e 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.QuestElementId, + QuestId = quest.Id, Sequence = (byte)sequence.Sequence, Step = i, Type = EIssueType.DuplicateCompletionFlags, diff --git a/Questionable/Validation/Validators/JsonSchemaValidator.cs b/Questionable/Validation/Validators/JsonSchemaValidator.cs index ca3625608..b68fa9679 100644 --- a/Questionable/Validation/Validators/JsonSchemaValidator.cs +++ b/Questionable/Validation/Validators/JsonSchemaValidator.cs @@ -25,7 +25,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator { _questSchema ??= JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result; - if (_questNodes.TryGetValue(quest.QuestElementId, out JsonNode? questNode)) + if (_questNodes.TryGetValue(quest.Id, out JsonNode? questNode)) { var evaluationResult = _questSchema.Evaluate(questNode, new EvaluationOptions { @@ -36,7 +36,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = null, Step = null, Type = EIssueType.InvalidJsonSchema, diff --git a/Questionable/Validation/Validators/NextQuestValidator.cs b/Questionable/Validation/Validators/NextQuestValidator.cs index 9c0afbc0b..c2e899a4f 100644 --- a/Questionable/Validation/Validators/NextQuestValidator.cs +++ b/Questionable/Validation/Validators/NextQuestValidator.cs @@ -8,11 +8,11 @@ internal sealed class NextQuestValidator : IQuestValidator { public IEnumerable Validate(Quest quest) { - foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.QuestElementId)) + foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.Id)) { yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = 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 18764ecb0..60539d5bd 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.QuestElementId, + QuestId = quest.Id, Sequence = null, Step = null, Type = EIssueType.QuestDisabled, diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs index 12adeecb6..5ffe6e0f0 100644 --- a/Questionable/Validation/Validators/UniqueStartStopValidator.cs +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -9,6 +9,9 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { public IEnumerable Validate(Quest quest) { + if (quest.Id is SatisfactionSupplyNpcId) + yield break; + var questAccepts = FindQuestStepsWithInteractionType(quest, EInteractionType.AcceptQuest) .Where(x => x.Step.PickUpQuestId == null) .ToList(); @@ -18,7 +21,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = (byte)accept.Sequence.Sequence, Step = accept.StepId, Type = EIssueType.UnexpectedAcceptQuestStep, @@ -32,7 +35,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = 0, Step = null, Type = EIssueType.MissingQuestAccept, @@ -50,7 +53,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = (byte)complete.Sequence.Sequence, Step = complete.StepId, Type = EIssueType.UnexpectedCompleteQuestStep, @@ -64,7 +67,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { yield return new ValidationIssue { - QuestId = quest.QuestElementId, + QuestId = quest.Id, Sequence = 255, Step = null, Type = EIssueType.MissingQuestComplete, diff --git a/Questionable/Windows/DebugOverlay.cs b/Questionable/Windows/DebugOverlay.cs index 5c7286de6..4fbb9074f 100644 --- a/Questionable/Windows/DebugOverlay.cs +++ b/Questionable/Windows/DebugOverlay.cs @@ -103,7 +103,7 @@ internal sealed class DebugOverlay : Window QuestStep? step = sequence.FindStep(i); if (step != null && TryGetPosition(step, out Vector3? position)) { - DrawStep($"{quest.QuestElementId} / {sequence.Sequence} / {i}", step, position.Value, 0xFFFFFFFF); + DrawStep($"{quest.Id} / {sequence.Sequence} / {i}", step, position.Value, 0xFFFFFFFF); } } } diff --git a/Questionable/Windows/JournalProgressWindow.cs b/Questionable/Windows/JournalProgressWindow.cs index c9dcd9fc1..bdad03da2 100644 --- a/Questionable/Windows/JournalProgressWindow.cs +++ b/Questionable/Windows/JournalProgressWindow.cs @@ -201,7 +201,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable if (ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo)) { - _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString(), commandInfo); + _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo); } if (ImGui.IsItemHovered()) diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs index 4f83cca88..eb48f4c43 100644 --- a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -58,7 +58,7 @@ internal sealed class ActiveQuestComponent { var currentQuestDetails = _questController.CurrentQuestDetails; QuestController.QuestProgress? currentQuest = currentQuestDetails?.Progress; - QuestController.CurrentQuestType? currentQuestType = currentQuestDetails?.Type; + QuestController.ECurrentQuestType? currentQuestType = currentQuestDetails?.Type; if (currentQuest != null) { DrawQuestNames(currentQuest, currentQuestType); @@ -108,9 +108,9 @@ internal sealed class ActiveQuestComponent } private void DrawQuestNames(QuestController.QuestProgress currentQuest, - QuestController.CurrentQuestType? currentQuestType) + QuestController.ECurrentQuestType? currentQuestType) { - if (currentQuestType == QuestController.CurrentQuestType.Simulated) + if (currentQuestType == QuestController.ECurrentQuestType.Simulated) { using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGui.TextUnformatted( @@ -151,7 +151,7 @@ internal sealed class ActiveQuestComponent private QuestWork? DrawQuestWork(QuestController.QuestProgress currentQuest) { - if (currentQuest.Quest.QuestElementId is not QuestId questId) + if (currentQuest.Quest.Id is not QuestId questId) return null; var questWork = _gameFunctions.GetQuestEx(questId); @@ -210,7 +210,7 @@ internal sealed class ActiveQuestComponent { using var disabled = ImRaii.Disabled(); - if (currentQuest.Quest.QuestElementId == _questController.NextQuest?.Quest.QuestElementId) + if (currentQuest.Quest.Id == _questController.NextQuest?.Quest.Id) ImGui.TextUnformatted("(Next quest in story line not accepted)"); else ImGui.TextUnformatted("(Not accepted)"); @@ -229,14 +229,14 @@ internal sealed class ActiveQuestComponent if (questWork == null) _questController.SetNextQuest(currentQuest.Quest); - _questController.ExecuteNextStep(true); + _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic); } ImGui.SameLine(); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step")) { - _questController.ExecuteNextStep(false); + _questController.ExecuteNextStep(QuestController.EAutomationType.Manual); } ImGui.EndDisabled(); @@ -262,7 +262,7 @@ internal sealed class ActiveQuestComponent if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip")) { _movementController.Stop(); - _questController.Skip(currentQuest.Quest.QuestElementId, currentQuest.Sequence); + _questController.Skip(currentQuest.Quest.Id, currentQuest.Sequence); } if (colored) @@ -274,7 +274,7 @@ internal sealed class ActiveQuestComponent ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Atlas)) _commandManager.DispatchCommand("/questinfo", - currentQuest.Quest.QuestElementId.ToString() ?? string.Empty, commandInfo); + currentQuest.Quest.Id.ToString() ?? string.Empty, commandInfo); } bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest; diff --git a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs index 41b042141..2a19a858b 100644 --- a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs +++ b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs @@ -6,6 +6,7 @@ using ImGuiNET; using Questionable.Controller; using Questionable.Data; using Questionable.Model; +using Questionable.Model.Questing; namespace Questionable.Windows.QuestComponents; @@ -31,6 +32,12 @@ internal sealed class QuestTooltipComponent _uiUtils = uiUtils; } + public void Draw(IQuestInfo quest) + { + if (quest is QuestInfo questInfo) + Draw(questInfo); + } + public void Draw(QuestInfo quest) { using var tooltip = ImRaii.Tooltip(); @@ -93,8 +100,8 @@ internal sealed class QuestTooltipComponent _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); - if (counter <= 2 || icon != FontAwesomeIcon.Check) - DrawQuestUnlocks(qInfo, counter + 1); + if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check)) + DrawQuestUnlocks(qstInfo, counter + 1); } } @@ -162,7 +169,7 @@ internal sealed class QuestTooltipComponent ImGui.Unindent(); } - private static string FormatQuestUnlockName(QuestInfo questInfo) + private static string FormatQuestUnlockName(IQuestInfo questInfo) { if (questInfo.IsMainScenarioQuest) return $"{questInfo.Name} ({questInfo.QuestId}, MSQ)"; diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 123753375..96d0cfd72 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -39,8 +39,8 @@ internal sealed class QuestSelectionWindow : LWindow private readonly UiUtils _uiUtils; private readonly QuestTooltipComponent _questTooltipComponent; - private List _quests = []; - private List _offeredQuests = []; + private List _quests = []; + private List _offeredQuests = []; private bool _onlyAvailableQuests = true; public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions, @@ -105,7 +105,7 @@ internal sealed class QuestSelectionWindow : LWindow _quests = _questRegistry.AllQuests .Where(x => x.FindSequence(0)?.FindStep(0)?.TerritoryId == territoryId) - .Select(x => _questData.GetQuestInfo(x.QuestElementId)) + .Select(x => _questData.GetQuestInfo(x.Id)) .ToList(); foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers) @@ -157,11 +157,11 @@ internal sealed class QuestSelectionWindow : LWindow ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, actionIconSize); ImGui.TableHeadersRow(); - foreach (QuestInfo quest in (_offeredQuests.Count != 0 && _onlyAvailableQuests) ? _offeredQuests : _quests) + foreach (IQuestInfo quest in (_offeredQuests.Count != 0 && _onlyAvailableQuests) ? _offeredQuests : _quests) { ImGui.TableNextRow(); - string questId = quest.QuestId.ToString(); + string questId = quest.QuestId.ToString() ?? string.Empty; bool isKnownQuest = _questRegistry.TryGetQuest(quest.QuestId, out var knownQuest); if (ImGui.TableNextColumn()) @@ -228,7 +228,7 @@ internal sealed class QuestSelectionWindow : LWindow if (startNextQuest) { _questController.SetNextQuest(knownQuest); - _questController.ExecuteNextStep(true); + _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic); } ImGui.SameLine(); @@ -245,7 +245,7 @@ internal sealed class QuestSelectionWindow : LWindow } } - private void CopyToClipboard(QuestInfo quest, bool suffix) + private void CopyToClipboard(IQuestInfo quest, bool suffix) { string fileName = $"{quest.QuestId}_{quest.SimplifiedName}{(suffix ? ".json" : "")}"; ImGui.SetClipboardText(fileName); diff --git a/Questionable/Windows/UiUtils.cs b/Questionable/Windows/UiUtils.cs index 6e750e9d3..d6bdba36b 100644 --- a/Questionable/Windows/UiUtils.cs +++ b/Questionable/Windows/UiUtils.cs @@ -22,13 +22,13 @@ internal sealed class UiUtils public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId questElementId) { if (_gameFunctions.IsQuestAccepted(questElementId)) - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active"); + return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active"); else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId)) return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); else if (_gameFunctions.IsQuestLocked(questElementId)) return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked"); else - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Available"); + return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available"); } public static (Vector4 color, FontAwesomeIcon icon) GetInstanceStyle(ushort instanceId) @@ -36,7 +36,7 @@ internal sealed class UiUtils if (UIState.IsInstanceContentCompleted(instanceId)) return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check); else if (UIState.IsInstanceContentUnlocked(instanceId)) - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight); + return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running); else return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times); }