Quest automation + various fixes
authorLiza Carvelli <liza@carvel.li>
Wed, 12 Jun 2024 16:03:48 +0000 (18:03 +0200)
committerLiza Carvelli <liza@carvel.li>
Wed, 12 Jun 2024 16:03:48 +0000 (18:03 +0200)
55 files changed:
QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json
QuestPaths/Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json
QuestPaths/Endwalker/MSQ/B-Garlemald/4383_A Frosty Reception.json
QuestPaths/Endwalker/MSQ/G-UltimaThule/4460_x.json
QuestPaths/Shadowbringers/MSQ/G-5.1/3673_Shaken Resolve.json
QuestPaths/Shadowbringers/MSQ/G-5.1/3675_A Welcome Guest.json
QuestPaths/Shadowbringers/MSQ/G-5.1/3676_Good for the Soul.json
QuestPaths/Shadowbringers/MSQ/G-5.1/3678_A Notable Absence.json
QuestPaths/Shadowbringers/MSQ/G-5.1/3680_Finding Good Help.json
QuestPaths/Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3762_The Way Home.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3763_Seeking Council.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3764_Facing the Truth.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3767_Deep Designs.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3768_A Whale's Tale.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json
QuestPaths/Shadowbringers/MSQ/H-5.2/3770_Echoes of a Fallen Star.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3771_In the Name of the Light.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3772_Heroic Dreams.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3774_Food for the Soul.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3775_Faded Memories.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3778_Hope's Confluence.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3779_Nothing Unsaid.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3781_Unto the Morrow.json
QuestPaths/Shadowbringers/MSQ/I-5.3/3782_Reflections in Crystal.json
QuestPaths/Shadowbringers/MSQ/J-5.4/4008_The Wisdom of Allag.json
QuestPaths/Shadowbringers/MSQ/J-5.4/4009_Reviving the Legacy.json
QuestPaths/Shadowbringers/MSQ/J-5.4/4010_Forget Us Not.json
QuestPaths/Shadowbringers/MSQ/J-5.4/4014_On Rough Seas.json
QuestPaths/Shadowbringers/MSQ/J-5.4/4015_The Great Ship Vylbrand.json
QuestPaths/Shadowbringers/MSQ/K-5.5/4060_Righteous Indignation.json
QuestPaths/Shadowbringers/MSQ/K-5.5/4063_When the Dust Settles.json
QuestPaths/quest-v1.json
Questionable/Configuration.cs
Questionable/Controller/GameUiController.cs
Questionable/Controller/MovementController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs
Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs
Questionable/Controller/Steps/BaseTasks/UnmountTask.cs
Questionable/Controller/Steps/InteractionFactory/UseItem.cs
Questionable/DalamudInitializer.cs
Questionable/Data/AetheryteData.cs
Questionable/GameFunctions.cs
Questionable/Model/Quest.cs
Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs
Questionable/Model/V1/Converter/ExcelRefConverter.cs [new file with mode: 0644]
Questionable/Model/V1/DialogueChoice.cs
Questionable/Model/V1/EDialogChoiceType.cs
Questionable/Model/V1/ExcelRef.cs [new file with mode: 0644]
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigWindow.cs [new file with mode: 0644]
Questionable/Windows/DebugWindow.cs

index 54e4f6b63ff0cc916c6b15c526aa43935c10c0e6..abd823a3ce336cb280b48d7ce198effc841a0591 100644 (file)
             "Z": -464.7746
           },
           "TerritoryId": 956,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
         },
         {
           "DataId": 1038716,
             "Z": -458.2132
           },
           "TerritoryId": 956,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
+          ]
         }
       ]
     },
             "Z": -519.18823
           },
           "TerritoryId": 956,
-          "InteractionType": "SinglePlayerDuty",
-          "Comment": "Duty - Shoot Large Green Bird"
+          "InteractionType": "WaitForManualProgress",
+          "Comment": "Shoot Large Green Bird"
         }
       ]
     },
index 685f19b2ef4e570f7a692c19f7403fcabde84250..b229d25a018407bf134551fb330380b421dc7e10 100644 (file)
             "Z": -108.537415
           },
           "TerritoryId": 956,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
         },
         {
           "DataId": 1038707,
             "Z": -150.04199
           },
           "TerritoryId": 956,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
+          ]
         },
         {
           "DataId": 1038708,
             "Z": -130.11371
           },
           "TerritoryId": 956,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            32
+          ]
         }
       ]
     },
index 1d54df3637b9fff08b870dea7d8deac87f9f6b80..bd8d91c4cbdd97d57398eecae259b00373846f12 100644 (file)
           "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
             }
           ]
index b320abf47d6bd184796e80ea50807082091ecaab..fa4dedaed905fd30ef9ff9d9928e2129ceb2f7ff 100644 (file)
@@ -97,7 +97,7 @@
           },
           "TerritoryId": 960,
           "InteractionType": "WaitForManualProgress",
-          "Comment": "Duty - Find Errant Omicron"
+          "Comment": "Find Errant Omicron"
         }
       ]
     },
index 4ff0ea5488c8cdd4b6811b2a0a754747c57e25d7..110e85022e6030c43af6110c638671a56c6e61cd 100644 (file)
           "AethernetShortcut": [
             "[Crystarium] Aetheryte Plaza",
             "[Crystarium] The Dossal Gate"
-          ]
+          ],
+          "TargetTerritoryId": 844
         },
         {
           "DataId": 1032121,
index 7a3fa514662276bd378ec277ac9733c4532e5362..77d97cfdce31c7c733f0c9b215bf1e714c75ffc3 100644 (file)
@@ -33,7 +33,8 @@
           "AethernetShortcut": [
             "[Crystarium] Aetheryte Plaza",
             "[Crystarium] The Dossal Gate"
-          ]
+          ],
+          "TargetTerritoryId": 844
         }
       ]
     },
           "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,
           "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,
           },
           "TerritoryId": 815,
           "InteractionType": "UseItem",
-          "ItemId": 2002904
+          "ItemId": 2002904,
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
         }
       ]
     },
index 89b81b1d76fbd909bbaaaa8c55846ae5e04dd4e4..f84d89b9d9f9dae421cb65fb0bb2f0c80071eb51 100644 (file)
             "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"
+            }
+          ]
         }
       ]
     },
           "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,
           },
           "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,
           },
           "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": {
           },
           "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"
+            }
+          ]
         }
       ]
     },
index 02b98cb580d2a0364940d3babfcacf879294fc0b..8873718177d50c1bc88371e0437492125d06e5d3 100644 (file)
           },
           "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,
             "[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,
           "AethernetShortcut": [
             "[Eulmore] Southeast Derelicts",
             "[Eulmore] The Glory Gate"
+          ],
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
           ]
         }
       ]
index 5897ea956c4b52ebc056cf187403d5dcab89b640..8155004d671c02bbbc929cfaca07b0870ced2437 100644 (file)
@@ -44,7 +44,7 @@
             "Z": -161.45575
           },
           "TerritoryId": 814,
-          "InteractionType": "SinglePlayerDuty",
+          "InteractionType": "WaitForManualProgress",
           "Comment": "Help Master Chai dodge enemies"
         }
       ]
index a6a376446a5de0ab96527538d9bf8555049674ee..587cb74460e05fb33dd8188b69b5876387ac1dbc 100644 (file)
@@ -36,7 +36,8 @@
           "AethernetShortcut": [
             "[Crystarium] Aetheryte Plaza",
             "[Crystarium] The Dossal Gate"
-          ]
+          ],
+          "TargetTerritoryId": 844
         },
         {
           "DataId": 1032121,
           },
           "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
+            }
+          ]
         }
       ]
     },
index e60b9f0877601d5b4b7130d9062a899ff0ba21b5..f9fdc86ece78705a0955c11b18bdc976b37ffb54 100644 (file)
@@ -33,7 +33,8 @@
           "AethernetShortcut": [
             "[Crystarium] Aetheryte Plaza",
             "[Crystarium] The Dossal Gate"
-          ]
+          ],
+          "TargetTerritoryId": 844
         },
         {
           "DataId": 1032121,
index cee81551d451ce130761208170fa72b4e68f67fa..fc024ec2e25293b218420e7652df13f68b64769c 100644 (file)
@@ -28,7 +28,8 @@
             "Z": 14.22064
           },
           "TerritoryId": 844,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "TargetTerritoryId": 819
         },
         {
           "DataId": 1030370,
             "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"
+            }
+          ]
         }
       ]
     },
index 166a609de6bb0efce07f6e6e4be6ef0091d74960..d6ce8b32990eba7d89c37783299e2db4ab3946c1 100644 (file)
@@ -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,
           "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,
           "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,
           "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,
             "[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,
           "AethernetShortcut": [
             "[Crystarium] The Crystalline Mean",
             "[Crystarium] The Cabinet of Curiosity"
+          ],
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            32
           ]
         }
       ]
index a97b039ab59d300f399ced373760d461888c5cf2..5df9e21f761f5b8dce21ea3afeb779a9cc0f79f7 100644 (file)
           "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
+            }
+          ]
         }
       ]
     },
index 0cef105f27a545b83d8abf14f66cb7bde6208cc3..6e2d454b75057c8ac3ff64f41fc67fba724cc477 100644 (file)
     {
       "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,
index e58e654e0a67cb73df9e466cacea34494c652f10..c94d954453bb52fe1a1b502c2196edbef7afc27e 100644 (file)
@@ -28,7 +28,7 @@
           },
           "TerritoryId": 813,
           "InteractionType": "WalkTo",
-          "AetheryteShortcut": "Lakeland - Ostall Imperative",
+          "AetheryteShortcut": "Lakeland - Fort Jobb",
           "Fly": true
         },
         {
           "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,
           },
           "TerritoryId": 813,
           "InteractionType": "Interact",
-          "DisableNavmesh": true
+          "DisableNavmesh": true,
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
         },
         {
           "DataId": 2010282,
           },
           "TerritoryId": 813,
           "InteractionType": "Interact",
-          "DisableNavmesh": true
+          "DisableNavmesh": true,
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            32
+          ],
+          "Comment": "TODO Check if pathfinding works automatically now"
         }
       ]
     },
index a2aa8d224f2bb3a86d233346770c55e8881bb49e..a5bec5deaa16803781ea4e546120e946298658a0 100644 (file)
@@ -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
         }
       ]
           "ContentFinderConditionId": 714
         }
       ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "DataId": 1032549,
+          "Position": {
+            "X": -1.9074707,
+            "Y": -200.00002,
+            "Z": -425.10114
+          },
+          "TerritoryId": 918,
+          "InteractionType": "Interact"
+        }
+      ]
     }
   ]
 }
index 9b72bf47ec608fd281b0e269820bd9f498add394..21ac75d5968fba58c584893910b1abd17208eb5f 100644 (file)
@@ -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,
           "AethernetShortcut": [
             "[Crystarium] The Dossal Gate",
             "[Crystarium] The Pendants"
+          ],
+          "DialogueChoices": [
+            {
+              "Type": "YesNo",
+              "Prompt": "TEXT_LUCKMH110_03770_Q1_000_600",
+              "Yes": true
+            }
           ]
         }
       ]
index f90d3afd221850600d0287de04eb8f799540c938..c45626845220084a9419400a108591ab48424e26 100644 (file)
             "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"
+            }
+          ]
         }
       ]
     },
           },
           "TerritoryId": 819,
           "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
+          ],
           "$.1": "QuestVariables if done first: 1 16 0 0 0 64"
         },
         {
           "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"
         },
         {
             "Z": 173.38818
           },
           "TerritoryId": 819,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            32
+          ]
         }
       ]
     },
     {
       "Sequence": 255,
       "Steps": [
-        {
-          "Position": {
-            "X": -140.22343,
-            "Y": 0.05337119,
-            "Z": 34.20123
-          },
-          "TerritoryId": 819,
-          "InteractionType": "WalkTo"
-        },
         {
           "DataId": 1027248,
           "Position": {
             "Z": -51.438232
           },
           "TerritoryId": 819,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "AetheryteShortcut": "Crystarium"
         }
       ]
     }
index 1afed5cf6d299cd96628cbb304a51b01a5517174..1ec3cbb8e682d6d2df06988b3022e5be6bef1064 100644 (file)
@@ -38,6 +38,7 @@
     },
     {
       "Sequence": 2,
+      "Comment": "This isn't solving for the 'best' results, but for the closest waypoints",
       "Steps": [
         {
           "DataId": 2011078,
           ],
           "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,
           "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,
           "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"
         }
       ]
     },
index 17d48f9c6b606fdf57164d9ebca2f71975357413..7fafc7ddb01a875f2d5d9560038ec027bc769adc 100644 (file)
@@ -79,7 +79,8 @@
           "AethernetShortcut": [
             "[Crystarium] Aetheryte Plaza",
             "[Crystarium] The Dossal Gate"
-          ]
+          ],
+          "TargetTerritoryId": 844
         },
         {
           "DataId": 1033819,
index b8dec55b4076a65c2b905f9fd7f9494c5369b9b1..76c6d30b16e5e880463ce15b61346c456508754c 100644 (file)
             "Z": 604.27246
           },
           "TerritoryId": 814,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "DialogueChoices": [
+            {
+              "Type": "YesNo",
+              "Prompt": "TEXT_LUCKMI105_03775_Q2_000_052",
+              "Yes": true
+            }
+          ]
         }
       ]
     },
index 4cc3791bf1708b932c8d966e6e0f9d954a51122f..3cff2aba44966f4641c199929d87ba2258f73b0f 100644 (file)
             "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,
index fc6d4f2749f33a50d20b3065121af1c0a760dec4..424df0eae61e0b71aa9ad0e589ec5e75e4efc6a6 100644 (file)
       "Sequence": 5,
       "Steps": [
         {
+          "Position": {
+            "X": 0,
+            "Y": 0,
+            "Z": 0
+          },
           "TerritoryId": 820,
-          "InteractionType": "Interact",
+          "InteractionType": "WalkTo",
           "AetheryteShortcut": "Eulmore"
         }
       ]
index 3afcc49057271526f7d1fb21b52ad16c10705242..226025408a0788e4f1085a58f04e8131da3aa1a4 100644 (file)
             "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"
+            }
+          ]
         }
       ]
     },
index 2d9b605566e0935a3c2c9028c7394feb1b89c1b1..2d49f9f8ab0844bdb38102147612cbe9711b3f94 100644 (file)
             "Z": 7.156433
           },
           "TerritoryId": 819,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "DialogueChoices": [
+            {
+              "Type": "YesNo",
+              "Prompt": "TEXT_LUCKMI112_03782_Q1_000_007",
+              "Yes": true
+            }
+          ]
         },
         {
           "DataId": 1033888,
             "Z": -5.081299
           },
           "TerritoryId": 844,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "DialogueChoices": [
+            {
+              "Type": "YesNo",
+              "Prompt": "TEXT_LUCKMI112_03782_Q2_000_044",
+              "Yes": true
+            }
+          ]
         }
       ]
     },
index e483792b9c4da169868d4ca52ecf8a4891b4077f..4ca6ea2ade3c7feadd310739c4fa6b6329ba9c22 100644 (file)
@@ -64,7 +64,8 @@
           "EnemySpawnType": "AfterInteraction",
           "KillEnemyDataIds": [
             12661
-          ]
+          ],
+          "Fly": true
         }
       ]
     },
index c4448357392e46b3bc981c168b8070690ceb4f81..29cd35ea8aacc1c47a6d5a68ebd678296df54200 100644 (file)
@@ -55,7 +55,8 @@
             "Z": -6.9733887
           },
           "TerritoryId": 351,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "TargetTerritoryId": 351
         },
         {
           "DataId": 2011332,
index 1c639a06068a059305fecc8f860d09bc4bf4c00c..1e0224a03961088ffa7d8066bd00cce4b50e7191 100644 (file)
           },
           "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"
+            }
+          ]
         }
       ]
     },
index a80047d3af830e8131ef44a3796c86f8a0a2d42f..09184cea3805af4e5049d843a7f64e8e00ed6b3a 100644 (file)
           },
           "TerritoryId": 129,
           "InteractionType": "Interact",
-          "AetheryteShortcut": "Limsa Lominsa"
+          "AetheryteShortcut": "Limsa Lominsa",
+          "DialogueChoices": [
+            {
+              "Type": "YesNo",
+              "Prompt": "TEXT_LUCKMJ108_04014_SYSTEM_100_010",
+              "Yes": true
+            }
+          ]
         },
         {
           "DataId": 1002694,
index f03429017d61d74cb30e4fc094ee5e34ebac3cd8..f2f077b6264b9cd21d69e4683a63e850949fa5e3 100644 (file)
     {
       "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,
index 7f7336b7ebe28d35e1be2fc9446afd0643e298b4..dfd5220bfa567db6bb25a613335a9ccf3628bdd2 100644 (file)
           "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,
           "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,
           },
           "TerritoryId": 402,
           "InteractionType": "Interact",
-          "DisableNavmesh": true
+          "DisableNavmesh": true,
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
         }
       ]
     },
           },
           "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"
+            }
+          ]
         }
       ]
     },
index c2b02fcbf77a59a9c4e39ec142e40fcdc1edef8b..e3b1beedde8812eebd6c6b9fa0ef578db886c2ce 100644 (file)
           "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"
+            }
           ]
         }
       ]
index 58c9890775350625f7310b6a7ef641bed4e47e85..cc2d097ba0da04512de9ae2a8cc0403cbc32e0f7 100644 (file)
                               "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"
                                 ]
                               }
                               "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"
                                 ]
                               }
index 6aa01ec3fa948ec4db7d3f9ddea252ea9ef3363b..d77af785724e9d6e86672cbd2f35e96d6da83ced 100644 (file)
@@ -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; }
+    }
 }
index 964235862b50a32ab4f63e4878a12a5256921e2a..e9fd522d4fc5ef132e9aeb7a24a64434ad951f50 100644 (file)
@@ -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<GameUiController> _logger;
 
     public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions,
-        QuestController questController, IGameGui gameGui, ILogger<GameUiController> logger)
+        QuestController questController, IGameGui gameGui, ITargetManager targetManager,
+        ILogger<GameUiController> 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,29 +278,24 @@ internal sealed class GameUiController : IDisposable
         _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
         foreach (var dialogueChoice in dialogueChoices)
         {
-            string? excelPrompt;
-            if (dialogueChoice.Prompt != null)
+            if (dialogueChoice.Type != EDialogChoiceType.YesNo)
+                continue;
+
+            if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
             {
-                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;
-                }
+                _logger.LogDebug(
+                    "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
+                    dialogueChoice.DataId, _targetManager.Target?.DataId);
+                continue;
             }
-            else
-                excelPrompt = null;
 
+            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)
@@ -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);
index 200c1a79e8b40f4f0eef25acac11f27c33eac3ae..cd5f92563e61598c04d98815a226c2b58c9cb679 100644 (file)
@@ -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);
     }
index 4cb5be17177c5cb8de747806477d152ab405ec23..81b96ce162fbf71e972d79f206bf231cc335c231 100644 (file)
@@ -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<QuestController> _logger;
     private readonly QuestRegistry _questRegistry;
     private readonly IKeyState _keyState;
+    private readonly Configuration _configuration;
     private readonly IReadOnlyList<ITaskFactory> _taskFactories;
 
     private readonly Queue<ITask> _taskQueue = new();
@@ -41,6 +43,7 @@ internal sealed class QuestController
         ILogger<QuestController> logger,
         QuestRegistry questRegistry,
         IKeyState keyState,
+        Configuration configuration,
         IEnumerable<ITaskFactory> 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<T>() =>
         _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);
 }
index 9dccc7d4541a7a3d4a74f6a2aca7969a4ca409eb..e3ddd3bada1de35e8f61198dcf1b4b2fe5a4053b 100644 (file)
@@ -57,6 +57,7 @@ internal sealed class QuestRegistry
                 continue;
 
             quest.Name = questData.Name.ToString();
+            quest.Level = questData.ClassJobLevel0;
         }
     }
 
index 988d5cd4921456fad625b12e678006bb9e461387..34d26abbf027ae605c1de5494046ce2701fab1f7 100644 (file)
@@ -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)
index 22d14d666f1423df07582e3061758265519506a4..c4c97b07ec99fae6e8abdf0749e78f60d79cfdd2 100644 (file)
@@ -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<WaitForCompletionFlags>()
                     .With(quest, step);
                 var delay = serviceProvider.GetRequiredService<WaitDelay>();
-                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<WaitObjectAtPosition>()
                             .With(step.DataId.Value, step.Position.Value),
                         serviceProvider.GetRequiredService<WaitDelay>(),
-                        new NextStep()
+                        Next(quest, sequence, step)
                     ];
 
                 case EInteractionType.Interact when step.TargetTerritoryId != null:
@@ -81,17 +82,40 @@ internal static class WaitAtEnd
                     [
                         waitInteraction,
                         serviceProvider.GetRequiredService<WaitDelay>(),
-                        new NextStep()
+                        Next(quest, sequence, step)
                     ];
 
                 case EInteractionType.Interact:
                 default:
-                    return [serviceProvider.GetRequiredService<WaitDelay>(), new NextStep()];
+                    return [serviceProvider.GetRequiredService<WaitDelay>(), 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))
index ede325b59c81e130415b4909ab6df5f9cb93deb0..9a11cd49232d09e9d2dbf5b97183fa696aff0042 100644 (file)
@@ -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<UnmountTask> log
     : ITask
 {
     private bool _unmountTriggered;
+    private DateTime _unmountedAt = DateTime.MinValue;
 
     public bool Start()
     {
@@ -16,6 +18,8 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> 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<UnmountTask> 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;
index b562baf42acb99c15b685b8e92ce540576433838..4289e6f226646f24ef0f34115d18df0f9a967168 100644 (file)
@@ -45,8 +45,38 @@ internal static class UseItem
             => throw new InvalidOperationException();
     }
 
+    internal abstract class UseItemBase : ITask
+    {
+        private bool _usedItem;
+        private DateTime _continueAt;
+
+        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) : AbstractDelayedTask
+    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})";
     }
index 44607f2d228b81df7347532143d3ae7feee40732..192a9ba925293774619d732097c3e0c92e590caa 100644 (file)
@@ -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();
     }
 }
index 377fb0ce42b91ad3fe5ecbc680bed9e2afcecb60..cebbd2839d54921684d0b47e5868da592c2709e9 100644 (file)
@@ -217,6 +217,18 @@ internal sealed class AetheryteData
             }
             .AsReadOnly();
 
+    /// <summary>
+    /// Airship landings are special as they're one-way only (except for Radz-at-Han, which is a normal aetheryte).
+    /// </summary>
+    public ReadOnlyDictionary<EAetheryteLocation, Vector3> AirshipLandingLocations { get; } =
+        new Dictionary<EAetheryteLocation, Vector3>
+        {
+            { 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<EAetheryteLocation, string> AethernetNames { get; }
     public ReadOnlyDictionary<EAetheryteLocation, ushort> TerritoryIds { get; }
     public IReadOnlyList<ushort> 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);
 }
index 4542b5a045fde198f32ed0560a7afd9030c055c6..67fd79b589c0f3e076ffecf3e375a9a975112868 100644 (file)
@@ -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<GameFunctions> _logger;
 
     public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
         ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
-        IGameGui gameGui, ILogger<GameFunctions> logger)
+        IGameGui gameGui, Configuration configuration, ILogger<GameFunctions> 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<ProcessChatBoxDelegate>(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<ContentTalk>()!.GetRow(rowId);
-        return questRow?.Text?.ToString();
+        if (excelSheet == "GimmickYesNo")
+        {
+            var questRow = _dataManager.GetExcelSheet<GimmickYesNo>()!.GetRow(rowId);
+            return questRow?.Unknown0?.ToString();
+        }
+        else if (excelSheet is "ContentTalk" or null)
+        {
+            var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
+            return questRow?.Text?.ToString();
+        }
+        else
+            throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}");
     }
 
     public bool IsOccupied()
index 3944301a71db1332172f6923dc7085dec8cc119e..ac6d408a996ee730ed3eac51bebce4644c499ba3 100644 (file)
@@ -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)
index abb41f69a365669ba671021a90fd967602fe5f60..b40304d72acf856abf0524f12b47ed03c28df621 100644 (file)
@@ -8,7 +8,5 @@ internal sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoic
     {
         { EDialogChoiceType.YesNo, "YesNo" },
         { EDialogChoiceType.List, "List" },
-        { EDialogChoiceType.ContentTalkYesNo, "ContentTalkYesNo" },
-        { EDialogChoiceType.ContentTalkList, "ContentTalkList" },
     };
 }
diff --git a/Questionable/Model/V1/Converter/ExcelRefConverter.cs b/Questionable/Model/V1/Converter/ExcelRefConverter.cs
new file mode 100644 (file)
index 0000000..0c446e4
--- /dev/null
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.V1.Converter;
+
+internal sealed class ExcelRefConverter : JsonConverter<ExcelRef>
+{
+    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();
+    }
+}
index d826f53d52745824434c3ea2bc4a51ecb2fed29e..ddfc0ae0913c660ff6f9e16433f22b7c3eabeb50 100644 (file)
@@ -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; }
+
+    /// <summary>
+    /// If set, only applies when focusing the given target id.
+    /// </summary>
+    public uint? DataId { get; set; }
 }
index d17a5b7adfc31e138907250fc1f4a202625b9647..e8f18dda846c901693191fa883252f70d3a4f329 100644 (file)
@@ -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 (file)
index 0000000..c6451ac
--- /dev/null
@@ -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,
+    }
+}
index b3575820c28a4666e29a688a75a52f9542f66846..c61828790998f531ce175ead5305225f7d1ba5bc 100644 (file)
@@ -106,6 +106,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<NavigationShortcutController>();
 
         serviceCollection.AddSingleton<DebugWindow>();
+        serviceCollection.AddSingleton<ConfigWindow>();
         serviceCollection.AddSingleton<DalamudInitializer>();
 
         _serviceProvider = serviceCollection.BuildServiceProvider();
diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs
new file mode 100644 (file)
index 0000000..ebfe605
--- /dev/null
@@ -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<Mount>()!
+            .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();
+}
index 7e2d353e3ad226cd16f37843cf7e2a1136d60c3a..03fdf7907044e0020b2b01d16c2c7bc361e0da20 100644 (file)
@@ -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<DebugWindow> _logger;
 
-    public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
-        MovementController movementController, QuestController questController, GameFunctions gameFunctions,
-        IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
-        Configuration configuration, ILogger<DebugWindow> logger)
+    public DebugWindow(DalamudPluginInterface pluginInterface,
+        MovementController movementController,
+        QuestController questController,
+        GameFunctions gameFunctions,
+        IClientState clientState,
+        IFramework framework,
+        ITargetManager targetManager,
+        GameUiController gameUiController,
+        Configuration configuration,
+        ILogger<DebugWindow> 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<WaitAtEnd.WaitNextStepOrSequence>();
+                                               && _questController
+                                                   .HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
 
             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);
-    }
 }