Add Wachumeqimeqi deliveries
authorLiza Carvelli <liza@carvel.li>
Sat, 17 Aug 2024 23:55:38 +0000 (01:55 +0200)
committerLiza Carvelli <liza@carvel.li>
Sat, 17 Aug 2024 23:55:38 +0000 (01:55 +0200)
22 files changed:
GatheringPaths/7.x - Dawntrail/Yak T'el/1001_Iq Rrax Tsoly_BTN.json [new file with mode: 0644]
GatheringPaths/7.x - Dawntrail/Yak T'el/980_Iq Rrax Tsoly_MIN.json [new file with mode: 0644]
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4989_Hands for Hire.json
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4990_Test of Talents.json
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4991_A Discerning Eye.json [new file with mode: 0644]
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4992_As Nature Intends.json [new file with mode: 0644]
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4993_The Cycle of Life.json [new file with mode: 0644]
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4994_Digging Up the Truth.json [new file with mode: 0644]
QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4995_Wellspring of Tears.json [new file with mode: 0644]
Questionable/Controller/GameUi/CraftworksSupplyController.cs [new file with mode: 0644]
Questionable/Controller/GameUi/CreditsController.cs [new file with mode: 0644]
Questionable/Controller/GameUi/HelpUiController.cs [new file with mode: 0644]
Questionable/Controller/GameUi/InteractionUiController.cs [new file with mode: 0644]
Questionable/Controller/GameUi/LeveUiController.cs [new file with mode: 0644]
Questionable/Controller/GameUiController.cs [deleted file]
Questionable/Controller/MiniTaskController.cs
Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs
Questionable/DalamudInitializer.cs
Questionable/QuestionablePlugin.cs
Questionable/Validation/QuestValidator.cs
Questionable/Windows/QuestSelectionWindow.cs
Questionable/Windows/QuestWindow.cs

diff --git a/GatheringPaths/7.x - Dawntrail/Yak T'el/1001_Iq Rrax Tsoly_BTN.json b/GatheringPaths/7.x - Dawntrail/Yak T'el/1001_Iq Rrax Tsoly_BTN.json
new file mode 100644 (file)
index 0000000..c9d98f7
--- /dev/null
@@ -0,0 +1,178 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
+  "Author": "liza",
+  "Steps": [
+    {
+      "Position": {
+        "X": 417.1447,
+        "Y": -0.6,
+        "Z": -647.60004
+      },
+      "TerritoryId": 1189,
+      "InteractionType": "Dive",
+      "AetheryteShortcut": "Yak T'el - Iq Br'aax",
+      "SkipConditions": {
+        "StepIf": {
+          "Flying": "Unlocked"
+        },
+        "AetheryteShortcutIf": {
+          "InSameTerritory": true
+        }
+      }
+    },
+    {
+      "Position": {
+        "X": 417.1447,
+        "Y": 3,
+        "Z": -647.60004
+      },
+      "TerritoryId": 1189,
+      "InteractionType": "WalkTo",
+      "Fly": true,
+      "SkipConditions": {
+        "StepIf": {
+          "Flying": "Locked"
+        }
+      }
+    },
+    {
+      "Position": {
+        "X": 419.8578,
+        "Y": -32.6974,
+        "Z": -653.75275
+      },
+      "TerritoryId": 1189,
+      "InteractionType": "WalkTo",
+      "DisableNavmesh": true,
+      "Fly": true
+    }
+  ],
+  "Groups": [
+    {
+      "Nodes": [
+        {
+          "DataId": 34912,
+          "Locations": [
+            {
+              "Position": {
+                "X": 458.8916,
+                "Y": -51.02777,
+                "Z": -689.8627
+              }
+            },
+            {
+              "Position": {
+                "X": 430.228,
+                "Y": -56.21914,
+                "Z": -693.9346
+              }
+            },
+            {
+              "Position": {
+                "X": 462.8787,
+                "Y": -58.29268,
+                "Z": -704.244
+              }
+            }
+          ]
+        },
+        {
+          "DataId": 34911,
+          "Locations": [
+            {
+              "Position": {
+                "X": 448.169,
+                "Y": -53.1458,
+                "Z": -696.1208
+              }
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Nodes": [
+        {
+          "DataId": 34914,
+          "Locations": [
+            {
+              "Position": {
+                "X": 453.7438,
+                "Y": -59.20442,
+                "Z": -884.0787
+              }
+            },
+            {
+              "Position": {
+                "X": 399.0516,
+                "Y": -48.41589,
+                "Z": -900.1575
+              }
+            },
+            {
+              "Position": {
+                "X": 470.4918,
+                "Y": -54.81378,
+                "Z": -912.1257
+              }
+            }
+          ]
+        },
+        {
+          "DataId": 34913,
+          "Locations": [
+            {
+              "Position": {
+                "X": 433.2036,
+                "Y": -56.63199,
+                "Z": -898.0532
+              }
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Nodes": [
+        {
+          "DataId": 34915,
+          "Locations": [
+            {
+              "Position": {
+                "X": 263.8979,
+                "Y": -44.71192,
+                "Z": -873.9875
+              }
+            }
+          ]
+        },
+        {
+          "DataId": 34916,
+          "Locations": [
+            {
+              "Position": {
+                "X": 287.7073,
+                "Y": -43.04572,
+                "Z": -886.5245
+              }
+            },
+            {
+              "Position": {
+                "X": 266.3744,
+                "Y": -47.55014,
+                "Z": -846.1501
+              }
+            },
+            {
+              "Position": {
+                "X": 259.2106,
+                "Y": -44.82758,
+                "Z": -817.9664
+              }
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/GatheringPaths/7.x - Dawntrail/Yak T'el/980_Iq Rrax Tsoly_MIN.json b/GatheringPaths/7.x - Dawntrail/Yak T'el/980_Iq Rrax Tsoly_MIN.json
new file mode 100644 (file)
index 0000000..0b135d8
--- /dev/null
@@ -0,0 +1,200 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
+  "Author": "liza",
+  "Steps": [
+    {
+      "Position": {
+        "X": 417.1447,
+        "Y": -0.6,
+        "Z": -647.60004
+      },
+      "TerritoryId": 1189,
+      "InteractionType": "Dive",
+      "AetheryteShortcut": "Yak T'el - Iq Br'aax",
+      "SkipConditions": {
+        "StepIf": {
+          "Flying": "Unlocked"
+        },
+        "AetheryteShortcutIf": {
+          "InSameTerritory": true
+        }
+      }
+    },
+    {
+      "Position": {
+        "X": 417.1447,
+        "Y": 3,
+        "Z": -647.60004
+      },
+      "TerritoryId": 1189,
+      "InteractionType": "WalkTo",
+      "Fly": true,
+      "SkipConditions": {
+        "StepIf": {
+          "Flying": "Locked"
+        }
+      }
+    },
+    {
+      "Position": {
+        "X": 419.8578,
+        "Y": -32.6974,
+        "Z": -653.75275
+      },
+      "TerritoryId": 1189,
+      "InteractionType": "WalkTo",
+      "DisableNavmesh": true,
+      "Fly": true
+    }
+  ],
+  "Groups": [
+    {
+      "Nodes": [
+        {
+          "DataId": 34787,
+          "Locations": [
+            {
+              "Position": {
+                "X": 482.7197,
+                "Y": -38.14573,
+                "Z": -612.8046
+              },
+              "MinimumAngle": 100,
+              "MaximumAngle": 275
+            }
+          ]
+        },
+        {
+          "DataId": 34788,
+          "Locations": [
+            {
+              "Position": {
+                "X": 503.5652,
+                "Y": -41.40348,
+                "Z": -600.9512
+              },
+              "MinimumAngle": 185,
+              "MaximumAngle": 275
+            },
+            {
+              "Position": {
+                "X": 441.1733,
+                "Y": -36.58192,
+                "Z": -610.3331
+              },
+              "MinimumAngle": 120,
+              "MaximumAngle": 265
+            },
+            {
+              "Position": {
+                "X": 457.5484,
+                "Y": -40.0437,
+                "Z": -608.3312
+              },
+              "MinimumAngle": 115,
+              "MaximumAngle": 240
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Nodes": [
+        {
+          "DataId": 34790,
+          "Locations": [
+            {
+              "Position": {
+                "X": 584.035,
+                "Y": -49.84215,
+                "Z": -759.925
+              },
+              "MinimumAngle": 115,
+              "MaximumAngle": 240
+            },
+            {
+              "Position": {
+                "X": 624.3585,
+                "Y": -61.07853,
+                "Z": -748.2542
+              }
+            },
+            {
+              "Position": {
+                "X": 605.4849,
+                "Y": -59.0002,
+                "Z": -772.6049
+              },
+              "MinimumAngle": 175,
+              "MaximumAngle": 275
+            }
+          ]
+        },
+        {
+          "DataId": 34789,
+          "Locations": [
+            {
+              "Position": {
+                "X": 601.6854,
+                "Y": -53.68699,
+                "Z": -741.3439
+              },
+              "MinimumAngle": 185,
+              "MaximumAngle": 355
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "Nodes": [
+        {
+          "DataId": 34785,
+          "Locations": [
+            {
+              "Position": {
+                "X": 754.1298,
+                "Y": -57.09224,
+                "Z": -571.5818
+              },
+              "MinimumAngle": 100,
+              "MaximumAngle": 250
+            }
+          ]
+        },
+        {
+          "DataId": 34786,
+          "Locations": [
+            {
+              "Position": {
+                "X": 734.2795,
+                "Y": -55.15427,
+                "Z": -573.6763
+              },
+              "MinimumAngle": 90,
+              "MaximumAngle": 260
+            },
+            {
+              "Position": {
+                "X": 714.931,
+                "Y": -53.3118,
+                "Z": -569.4072
+              },
+              "MinimumAngle": 115,
+              "MaximumAngle": 250
+            },
+            {
+              "Position": {
+                "X": 773.049,
+                "Y": -55.97124,
+                "Z": -569.7167
+              },
+              "MinimumAngle": 105,
+              "MaximumAngle": 240
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
index 36eb6d7ba4f71df887b544600ad091d68fb9a7e9..ad7a2c8c9f1ea3a7fbb8bc4a4180b33206ef1d50 100644 (file)
@@ -28,7 +28,8 @@
             "Z": -8.316223
           },
           "TerritoryId": 1185,
-          "InteractionType": "CompleteQuest"
+          "InteractionType": "CompleteQuest",
+          "NextQuestId": 4990
         }
       ]
     }
index 5693f6bf78dbf5d548b228c200b95b8dd2aff90a..6a72ac3753faa96924c3f655b0daa915854161ff 100644 (file)
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "TerritoryId": 1185,
+          "InteractionType": "None",
+          "RequiredGatheredItems": [
+            {
+              "ItemId": 43899,
+              "ItemCount": 6,
+              "Collectability": 600
+            }
+          ],
+          "AetheryteShortcut": "Tuliyollal",
+          "AethernetShortcut": [
+            "[Tuliyollal] Aetheryte Plaza",
+            "[Tuliyollal] Wachumeqimeqi"
+          ]
+        },
         {
           "DataId": 1047132,
           "Position": {
@@ -28,7 +44,8 @@
             "Z": -5.6916504
           },
           "TerritoryId": 1185,
-          "InteractionType": "CompleteQuest"
+          "InteractionType": "CompleteQuest",
+          "NextQuestId": 4991
         }
       ]
     }
diff --git a/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4991_A Discerning Eye.json b/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4991_A Discerning Eye.json
new file mode 100644 (file)
index 0000000..7150a44
--- /dev/null
@@ -0,0 +1,53 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "AcceptQuest"
+        }
+      ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "TerritoryId": 1185,
+          "InteractionType": "None",
+          "RequiredGatheredItems": [
+            {
+              "ItemId": 43900,
+              "ItemCount": 6,
+              "Collectability": 600
+            }
+          ],
+          "AetheryteShortcut": "Tuliyollal",
+          "AethernetShortcut": [
+            "[Tuliyollal] Aetheryte Plaza",
+            "[Tuliyollal] Wachumeqimeqi"
+          ]
+        },
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "CompleteQuest",
+          "NextQuestId": 4992
+        }
+      ]
+    }
+  ]
+}
diff --git a/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4992_As Nature Intends.json b/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4992_As Nature Intends.json
new file mode 100644 (file)
index 0000000..dd99b09
--- /dev/null
@@ -0,0 +1,53 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "AcceptQuest"
+        }
+      ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "TerritoryId": 1185,
+          "InteractionType": "None",
+          "RequiredGatheredItems": [
+            {
+              "ItemId": 43901,
+              "ItemCount": 6,
+              "Collectability": 600
+            }
+          ],
+          "AetheryteShortcut": "Tuliyollal",
+          "AethernetShortcut": [
+            "[Tuliyollal] Aetheryte Plaza",
+            "[Tuliyollal] Wachumeqimeqi"
+          ]
+        },
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "CompleteQuest",
+          "NextQuestId": 4993
+        }
+      ]
+    }
+  ]
+}
diff --git a/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4993_The Cycle of Life.json b/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4993_The Cycle of Life.json
new file mode 100644 (file)
index 0000000..9b9155f
--- /dev/null
@@ -0,0 +1,71 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "AcceptQuest"
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "DataId": 1047153,
+          "Position": {
+            "X": -270.4662,
+            "Y": 40.0732,
+            "Z": -12.253052
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "Interact",
+          "AethernetShortcut": [
+            "[Tuliyollal] Wachumeqimeqi",
+            "[Tuliyollal] The Resplendent Quarter"
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "TerritoryId": 1185,
+          "InteractionType": "None",
+          "RequiredGatheredItems": [
+            {
+              "ItemId": 43913,
+              "ItemCount": 1
+            }
+          ],
+          "AetheryteShortcut": "Tuliyollal",
+          "AethernetShortcut": [
+            "[Tuliyollal] Aetheryte Plaza",
+            "[Tuliyollal] Wachumeqimeqi"
+          ]
+        },
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "CompleteQuest",
+          "NextQuestId": 4994
+        }
+      ]
+    }
+  ]
+}
diff --git a/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4994_Digging Up the Truth.json b/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4994_Digging Up the Truth.json
new file mode 100644 (file)
index 0000000..1202151
--- /dev/null
@@ -0,0 +1,53 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "AcceptQuest"
+        }
+      ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "TerritoryId": 1185,
+          "InteractionType": "None",
+          "RequiredGatheredItems": [
+            {
+              "ItemId": 43902,
+              "ItemCount": 6,
+              "Collectability": 600
+            }
+          ],
+          "AetheryteShortcut": "Tuliyollal",
+          "AethernetShortcut": [
+            "[Tuliyollal] Aetheryte Plaza",
+            "[Tuliyollal] Wachumeqimeqi"
+          ]
+        },
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "CompleteQuest",
+          "NextQuestId": 4995
+        }
+      ]
+    }
+  ]
+}
diff --git a/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4995_Wellspring of Tears.json b/QuestPaths/7.x - Dawntrail/Custom Deliveries/Wachumeqimeqi/MIN, BTN/4995_Wellspring of Tears.json
new file mode 100644 (file)
index 0000000..03be2b8
--- /dev/null
@@ -0,0 +1,284 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "AcceptQuest"
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "DataId": 1047155,
+          "Position": {
+            "X": 746.24243,
+            "Y": -133.18861,
+            "Z": 507.5608
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "AetheryteShortcut": "Yak T'el - Mamook"
+        }
+      ]
+    },
+    {
+      "Sequence": 2,
+      "Steps": [
+        {
+          "DataId": 1048981,
+          "Position": {
+            "X": 686.51855,
+            "Y": -137.174,
+            "Z": 534.8745
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "Fly": true,
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            16
+          ]
+        },
+        {
+          "DataId": 1048973,
+          "Position": {
+            "X": 661.86,
+            "Y": -135.17876,
+            "Z": 582.11633
+          },
+          "StopDistance": 4,
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ]
+        },
+        {
+          "DataId": 1047156,
+          "Position": {
+            "X": 632.5017,
+            "Y": -137.17401,
+            "Z": 590.8445
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            32
+          ]
+        },
+        {
+          "DataId": 1048974,
+          "Position": {
+            "X": 621.51514,
+            "Y": -135.12726,
+            "Z": 531.1207
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 3,
+      "Steps": [
+        {
+          "DataId": 1047158,
+          "Position": {
+            "X": 539.69617,
+            "Y": -142.49185,
+            "Z": 481.65088
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            64
+          ],
+          "Fly": true
+        },
+        {
+          "DataId": 1047157,
+          "Position": {
+            "X": 586.26685,
+            "Y": -142.4984,
+            "Z": 462.97388
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "CompletionQuestVariablesFlags": [
+            null,
+            null,
+            null,
+            null,
+            null,
+            128
+          ],
+          "Fly": true
+        }
+      ]
+    },
+    {
+      "Sequence": 4,
+      "Steps": [
+        {
+          "DataId": 1047159,
+          "Position": {
+            "X": 191.45496,
+            "Y": -160.64616,
+            "Z": 414.0536
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "Fly": true
+        }
+      ]
+    },
+    {
+      "Sequence": 5,
+      "Steps": [
+        {
+          "DataId": 1047160,
+          "Position": {
+            "X": 664.6067,
+            "Y": 1.554378,
+            "Z": -477.22595
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "AetheryteShortcut": "Yak T'el - Mamook",
+          "Fly": true
+        }
+      ]
+    },
+    {
+      "Sequence": 6,
+      "Steps": [
+        {
+          "Position": {
+            "X": 436.87848,
+            "Y": 4.0999737,
+            "Z": -551.09174
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "WalkTo",
+          "SkipConditions": {
+            "StepIf": {
+              "Flying": "Unlocked"
+            }
+          }
+        },
+        {
+          "Position": {
+            "X": 674.17834,
+            "Y": -33.187485,
+            "Z": -598.0982
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "WalkTo",
+          "Fly": true,
+          "RequiredGatheredItems": [
+            {
+              "ItemId": 43914,
+              "ItemCount": 1
+            }
+          ]
+        },
+        {
+          "Position": {
+            "X": 674.17834,
+            "Y": -0.6,
+            "Z": -598.0982
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "WalkTo",
+          "Fly": true,
+          "DisableNavmesh": true
+        },
+        {
+          "DataId": 1047160,
+          "Position": {
+            "X": 664.6067,
+            "Y": 1.554378,
+            "Z": -477.22595
+          },
+          "TerritoryId": 1189,
+          "InteractionType": "Interact",
+          "Fly": true
+        }
+      ]
+    },
+    {
+      "Sequence": 7,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "Interact",
+          "AetheryteShortcut": "Tuliyollal",
+          "AethernetShortcut": [
+            "[Tuliyollal] Aetheryte Plaza",
+            "[Tuliyollal] Wachumeqimeqi"
+          ]
+        }
+      ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "DataId": 1047132,
+          "Position": {
+            "X": 217.36475,
+            "Y": -14.000001,
+            "Z": -5.6916504
+          },
+          "TerritoryId": 1185,
+          "InteractionType": "CompleteQuest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Questionable/Controller/GameUi/CraftworksSupplyController.cs b/Questionable/Controller/GameUi/CraftworksSupplyController.cs
new file mode 100644 (file)
index 0000000..97c5003
--- /dev/null
@@ -0,0 +1,123 @@
+using System;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.Logging;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class CraftworksSupplyController : IDisposable
+{
+    private readonly QuestController _questController;
+    private readonly IAddonLifecycle _addonLifecycle;
+    private readonly IGameGui _gameGui;
+    private readonly IFramework _framework;
+    private readonly ILogger<CraftworksSupplyController> _logger;
+
+    public CraftworksSupplyController(QuestController questController, IAddonLifecycle addonLifecycle,
+        IGameGui gameGui, IFramework framework, ILogger<CraftworksSupplyController> logger)
+    {
+        _questController = questController;
+        _addonLifecycle = addonLifecycle;
+        _gameGui = gameGui;
+        _framework = framework;
+        _logger = logger;
+
+        _addonLifecycle.RegisterListener(AddonEvent.PostReceiveEvent, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "BankaCraftworksSupply",
+            BankaCraftworksSupplyPostUpdate);
+    }
+
+    private bool ShouldHandleUiInteractions => _questController.IsRunning;
+
+    private unsafe void BankaCraftworksSupplyPostUpdate(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+        InteractWithBankaCraftworksSupply(addon);
+    }
+
+    private unsafe void InteractWithBankaCraftworksSupply()
+    {
+        if (_gameGui.TryGetAddonByName("BankaCraftworksSupply", out AtkUnitBase* addon))
+            InteractWithBankaCraftworksSupply(addon);
+    }
+
+    private unsafe void InteractWithBankaCraftworksSupply(AtkUnitBase* addon)
+    {
+        AtkValue* atkValues = addon->AtkValues;
+
+        uint completedCount = atkValues[7].UInt;
+        uint missingCount = 6 - completedCount;
+        for (int slot = 0; slot < missingCount; ++slot)
+        {
+            if (atkValues[31 + slot].UInt != 0)
+                continue;
+
+            _logger.LogInformation("Selecting an item for slot {Slot}", slot);
+            var selectSlot = stackalloc AtkValue[]
+            {
+                new() { Type = ValueType.Int, Int = 2 },
+                new() { Type = ValueType.Int, Int = slot /* slot */ },
+            };
+            addon->FireCallback(2, selectSlot);
+            return;
+        }
+
+        // do turn-in if any item is provided
+        if (atkValues[31].UInt != 0)
+        {
+            _logger.LogInformation("Confirming turn-in");
+            addon->FireCallbackInt(0);
+        }
+    }
+
+    // FIXME: This seems to not work if the mouse isn't over the FFXIV window?
+    private unsafe void ContextIconMenuPostReceiveEvent(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        AddonContextIconMenu* addonContextIconMenu = (AddonContextIconMenu*)args.Addon;
+        if (!addonContextIconMenu->IsVisible)
+            return;
+
+        ushort parentId = addonContextIconMenu->ContextMenuParentId;
+        if (parentId == 0)
+            return;
+
+        AtkUnitBase* parentAddon = AtkStage.Instance()->RaptureAtkUnitManager->GetAddonById(parentId);
+        if (parentAddon->NameString is "BankaCraftworksSupply")
+        {
+            _logger.LogInformation("Picking item for {AddonName}", parentAddon->NameString);
+            var selectSlot = stackalloc AtkValue[]
+            {
+                new() { Type = ValueType.Int, Int = 0 },
+                new() { Type = ValueType.Int, Int = 0 /* slot */ },
+                new() { Type = ValueType.UInt, UInt = 20802 /* probably the item's icon */ },
+                new() { Type = ValueType.UInt, UInt = 0 },
+                new() { Type = 0, Int = 0 },
+            };
+            addonContextIconMenu->FireCallback(5, selectSlot);
+            addonContextIconMenu->Close(true);
+
+            if (parentAddon->NameString == "BankaCraftworksSupply")
+                _framework.RunOnTick(InteractWithBankaCraftworksSupply, TimeSpan.FromMilliseconds(50));
+        }
+        else
+            _logger.LogTrace("Ignoring contextmenu event for {AddonName}", parentAddon->NameString);
+    }
+
+    public void Dispose()
+    {
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "BankaCraftworksSupply",
+            BankaCraftworksSupplyPostUpdate);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostReceiveEvent, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
+    }
+}
diff --git a/Questionable/Controller/GameUi/CreditsController.cs b/Questionable/Controller/GameUi/CreditsController.cs
new file mode 100644 (file)
index 0000000..121f5bd
--- /dev/null
@@ -0,0 +1,59 @@
+using System;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using Microsoft.Extensions.Logging;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class CreditsController : IDisposable
+{
+    private readonly IAddonLifecycle _addonLifecycle;
+    private readonly ILogger<CreditsController> _logger;
+
+    public CreditsController(IAddonLifecycle addonLifecycle, ILogger<CreditsController> logger)
+    {
+        _addonLifecycle = addonLifecycle;
+        _logger = logger;
+
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
+    }
+
+
+    /// <summary>
+    /// ARR Credits.
+    /// </summary>
+    private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args)
+    {
+        _logger.LogInformation("Closing Credits sequence");
+        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+        addon->FireCallbackInt(-2);
+    }
+
+    /// <summary>
+    /// Credits for (possibly all?) expansions, not used for ARR.
+    /// </summary>
+    private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
+    {
+        _logger.LogInformation("Closing Credits sequence");
+        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+        addon->FireCallbackInt(-2);
+    }
+
+    private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args)
+    {
+        _logger.LogInformation("Closing CreditPlayer");
+        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+        addon->Close(true);
+    }
+
+    public void Dispose()
+    {
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
+    }
+}
diff --git a/Questionable/Controller/GameUi/HelpUiController.cs b/Questionable/Controller/GameUi/HelpUiController.cs
new file mode 100644 (file)
index 0000000..a7b2339
--- /dev/null
@@ -0,0 +1,78 @@
+using System;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using Microsoft.Extensions.Logging;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class HelpUiController : IDisposable
+{
+    private readonly QuestController _questController;
+    private readonly IAddonLifecycle _addonLifecycle;
+    private readonly ILogger<HelpUiController> _logger;
+
+    public HelpUiController(QuestController questController, IAddonLifecycle addonLifecycle, ILogger<HelpUiController> logger)
+    {
+        _questController = questController;
+        _addonLifecycle = addonLifecycle;
+        _logger = logger;
+
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
+    }
+
+    private bool ShouldHandleUiInteractions => _questController.IsRunning;
+
+    private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        if (_questController.StartedQuest?.Quest.Id.Value == 4526)
+        {
+            _logger.LogInformation("Closing Unending Codex");
+            AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+            addon->FireCallbackInt(-2);
+        }
+    }
+
+    private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        if (_questController.StartedQuest?.Quest.Id.Value == 245)
+        {
+            _logger.LogInformation("Closing ContentsTutorial");
+            AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+            addon->FireCallbackInt(13);
+        }
+    }
+
+    /// <summary>
+    /// Opened e.g. the first time you open the duty finder window during Sastasha.
+    /// </summary>
+    private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        if (_questController.StartedQuest?.Quest.Id.Value == 245)
+        {
+            _logger.LogInformation("Closing MultipleHelpWindow");
+            AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+            addon->FireCallbackInt(-2);
+            addon->FireCallbackInt(-1);
+        }
+    }
+
+    public void Dispose()
+    {
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
+    }
+}
diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs
new file mode 100644 (file)
index 0000000..4c8367e
--- /dev/null
@@ -0,0 +1,844 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.Event;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib;
+using LLib.GameData;
+using LLib.GameUI;
+using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.Interactions;
+using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model;
+using Questionable.Model.Common;
+using Questionable.Model.Gathering;
+using Questionable.Model.Questing;
+using AethernetShortcut = Questionable.Controller.Steps.Shared.AethernetShortcut;
+using EAetheryteLocationExtensions = Questionable.Model.Common.EAetheryteLocationExtensions;
+using Quest = Questionable.Model.Quest;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class InteractionUiController : IDisposable
+{
+    private readonly IAddonLifecycle _addonLifecycle;
+    private readonly IDataManager _dataManager;
+    private readonly QuestFunctions _questFunctions;
+    private readonly AetheryteFunctions _aetheryteFunctions;
+    private readonly ExcelFunctions _excelFunctions;
+    private readonly QuestController _questController;
+    private readonly GatheringData _gatheringData;
+    private readonly GatheringPointRegistry _gatheringPointRegistry;
+    private readonly QuestRegistry _questRegistry;
+    private readonly QuestData _questData;
+    private readonly IGameGui _gameGui;
+    private readonly ITargetManager _targetManager;
+    private readonly IClientState _clientState;
+    private readonly ILogger<InteractionUiController> _logger;
+    private readonly Regex _returnRegex;
+
+    private bool _isInitialCheck;
+
+    public InteractionUiController(
+        IAddonLifecycle addonLifecycle,
+        IDataManager dataManager,
+        QuestFunctions questFunctions,
+        AetheryteFunctions aetheryteFunctions,
+        ExcelFunctions excelFunctions,
+        QuestController questController,
+        GatheringData gatheringData,
+        GatheringPointRegistry gatheringPointRegistry,
+        QuestRegistry questRegistry,
+        QuestData questData,
+        IGameGui gameGui,
+        ITargetManager targetManager,
+        IFramework framework,
+        IPluginLog pluginLog,
+        IClientState clientState,
+        ILogger<InteractionUiController> logger)
+    {
+        _addonLifecycle = addonLifecycle;
+        _dataManager = dataManager;
+        _questFunctions = questFunctions;
+        _aetheryteFunctions = aetheryteFunctions;
+        _excelFunctions = excelFunctions;
+        _questController = questController;
+        _gatheringData = gatheringData;
+        _gatheringPointRegistry = gatheringPointRegistry;
+        _questRegistry = questRegistry;
+        _questData = questData;
+        _gameGui = gameGui;
+        _targetManager = targetManager;
+        _clientState = clientState;
+        _logger = logger;
+
+        _returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
+
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
+    }
+
+    private bool ShouldHandleUiInteractions => _isInitialCheck || _questController.IsRunning;
+
+    internal unsafe void HandleCurrentDialogueChoices()
+    {
+        try
+        {
+            _isInitialCheck = true;
+            if (_gameGui.TryGetAddonByName("SelectString", out AddonSelectString* addonSelectString))
+            {
+                _logger.LogInformation("SelectString window is open");
+                SelectStringPostSetup(addonSelectString, true);
+            }
+
+            if (_gameGui.TryGetAddonByName("CutSceneSelectString",
+                    out AddonCutSceneSelectString* addonCutSceneSelectString))
+            {
+                _logger.LogInformation("CutSceneSelectString window is open");
+                CutsceneSelectStringPostSetup(addonCutSceneSelectString, true);
+            }
+
+            if (_gameGui.TryGetAddonByName("SelectIconString", out AddonSelectIconString* addonSelectIconString))
+            {
+                _logger.LogInformation("SelectIconString window is open");
+                SelectIconStringPostSetup(addonSelectIconString, true);
+            }
+
+            if (_gameGui.TryGetAddonByName("SelectYesno", out AddonSelectYesno* addonSelectYesno))
+            {
+                _logger.LogInformation("SelectYesno window is open");
+                SelectYesnoPostSetup(addonSelectYesno, true);
+            }
+
+            if (_gameGui.TryGetAddonByName("PointMenu", out AtkUnitBase* addonPointMenu))
+            {
+                _logger.LogInformation("PointMenu is open");
+                PointMenuPostSetup(addonPointMenu);
+            }
+        }
+        finally
+        {
+            _isInitialCheck = false;
+        }
+    }
+
+    private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
+    {
+        AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
+        SelectStringPostSetup(addonSelectString, false);
+    }
+
+    private unsafe void SelectStringPostSetup(AddonSelectString* addonSelectString, bool checkAllSteps)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        string? actualPrompt = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
+        if (actualPrompt == null)
+            return;
+
+        List<string?> answers = new();
+        for (ushort i = 7; i < addonSelectString->AtkUnitBase.AtkValuesCount; ++i)
+        {
+            if (addonSelectString->AtkUnitBase.AtkValues[i].Type == ValueType.String)
+                answers.Add(addonSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
+        }
+
+        int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
+        if (answer != null)
+            addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
+    }
+
+    private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
+    {
+        AddonCutSceneSelectString* addonCutSceneSelectString = (AddonCutSceneSelectString*)args.Addon;
+        CutsceneSelectStringPostSetup(addonCutSceneSelectString, false);
+    }
+
+    private unsafe void CutsceneSelectStringPostSetup(AddonCutSceneSelectString* addonCutSceneSelectString,
+        bool checkAllSteps)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        string? actualPrompt = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
+        if (actualPrompt == null)
+            return;
+
+        List<string?> answers = new();
+        for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; ++i)
+            answers.Add(addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
+
+        int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
+        if (answer != null)
+            addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
+    }
+
+    private unsafe void SelectIconStringPostSetup(AddonEvent type, AddonArgs args)
+    {
+        AddonSelectIconString* addonSelectIconString = (AddonSelectIconString*)args.Addon;
+        SelectIconStringPostSetup(addonSelectIconString, false);
+    }
+
+    [SuppressMessage("ReSharper", "RedundantJumpStatement")]
+    private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        string? actualPrompt = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString();
+        if (string.IsNullOrEmpty(actualPrompt))
+            actualPrompt = null;
+
+        var answers = GetChoices(addonSelectIconString);
+        int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
+        if (answer != null)
+        {
+            addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
+            return;
+        }
+
+        // this is 'Daily Quests' for tribal quests, but not set for normal selections
+        string? title = addonSelectIconString->AtkValues[0].ReadAtkString();
+
+        var currentQuest = _questController.StartedQuest;
+        if (currentQuest != null && (actualPrompt == null || title != null))
+        {
+            _logger.LogInformation("Checking if current quest {Name} is on the list", currentQuest.Quest.Info.Name);
+            if (CheckQuestSelection(addonSelectIconString, currentQuest.Quest, answers))
+                return;
+        }
+
+        var nextQuest = _questController.NextQuest;
+        if (nextQuest != null && (actualPrompt == null || title != null))
+        {
+            _logger.LogInformation("Checking if next quest {Name} is on the list", nextQuest.Quest.Info.Name);
+            if (CheckQuestSelection(addonSelectIconString, nextQuest.Quest, answers))
+                return;
+        }
+    }
+
+    private unsafe bool CheckQuestSelection(AddonSelectIconString* addonSelectIconString, Quest quest,
+        List<string?> answers)
+    {
+        // it is possible for this to be a quest selection
+        string questName = quest.Info.Name;
+        int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
+        if (questSelection >= 0)
+        {
+            addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
+            return true;
+        }
+
+        return false;
+    }
+
+    public static unsafe List<string?> GetChoices(AddonSelectIconString* addonSelectIconString)
+    {
+        List<string?> answers = new();
+        for (ushort i = 0; i < addonSelectIconString->AtkUnitBase.AtkValues[5].Int; i++)
+            answers.Add(addonSelectIconString->AtkValues[i * 3 + 7].ReadAtkString());
+
+        return answers;
+    }
+
+    private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
+    {
+        List<DialogueChoiceInfo> dialogueChoices = [];
+
+        // levequest choices have some vague sort of priority
+        if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
+            interact.Quest != null &&
+            interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
+        {
+            if (interact.InteractionType == EInteractionType.AcceptLeve)
+            {
+                dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
+                    new DialogueChoice
+                    {
+                        Type = EDialogChoiceType.List,
+                        ExcelSheet = "leve/GuildleveAssignment",
+                        Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+                        Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_01"),
+                    }));
+                interact.InteractionType = EInteractionType.None;
+            }
+            else if (interact.InteractionType == EInteractionType.CompleteLeve)
+            {
+                dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
+                    new DialogueChoice
+                    {
+                        Type = EDialogChoiceType.List,
+                        ExcelSheet = "leve/GuildleveAssignment",
+                        Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+                        Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_REWARD"),
+                    }));
+                interact.InteractionType = EInteractionType.None;
+            }
+        }
+
+        var currentQuest = _questController.SimulatedQuest ??
+                           _questController.GatheringQuest ??
+                           _questController.StartedQuest;
+        if (currentQuest != null)
+        {
+            var quest = currentQuest.Quest;
+            if (checkAllSteps)
+            {
+                var sequence = quest.FindSequence(currentQuest.Sequence);
+                var choices = sequence?.Steps.SelectMany(x => x.DialogueChoices);
+                if (choices != null)
+                    dialogueChoices.AddRange(choices.Select(x => new DialogueChoiceInfo(quest, x)));
+            }
+            else
+            {
+                var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+                if (step == null)
+                    _logger.LogDebug("Ignoring current quest dialogue choices, no active step");
+                else
+                    dialogueChoices.AddRange(step.DialogueChoices.Select(x => new DialogueChoiceInfo(quest, x)));
+            }
+
+            // add all travel dialogue choices
+            var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
+            if (targetTerritoryId != null)
+            {
+                foreach (string? answer in answers)
+                {
+                    if (answer == null)
+                        continue;
+
+                    if (TryFindWarp(targetTerritoryId.Value, answer, out uint? warpId, out string? warpText))
+                    {
+                        _logger.LogInformation("Adding warp {Id}, {Prompt}", warpId, warpText);
+                        dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice
+                        {
+                            Type = EDialogChoiceType.List,
+                            ExcelSheet = null,
+                            Prompt = null,
+                            Answer = ExcelRef.FromSheetValue(warpText),
+                        }));
+                    }
+                }
+            }
+        }
+        else
+            _logger.LogDebug("Ignoring current quest dialogue choices, no active quest");
+
+        // add all quests that start with the targeted npc
+        var target = _targetManager.Target;
+        if (target != null)
+        {
+            foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId).Where(x => x.QuestId is QuestId))
+            {
+                if (_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) &&
+                    _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest))
+                {
+                    var questChoices = knownQuest.FindSequence(0)?.Steps
+                        .SelectMany(x => x.DialogueChoices)
+                        .ToList();
+                    if (questChoices != null && questChoices.Count > 0)
+                    {
+                        _logger.LogInformation("Adding {Count} dialogue choices from not accepted quest {QuestName}",
+                            questChoices.Count, questInfo.Name);
+                        dialogueChoices.AddRange(questChoices.Select(x => new DialogueChoiceInfo(knownQuest, x)));
+                    }
+                }
+            }
+
+            if ((_questController.IsRunning || _questController.WasLastTaskUpdateWithin(TimeSpan.FromSeconds(5)))
+                && _questController.NextQuest == null)
+            {
+                // make sure to always close the leve dialogue
+                if (_questData.GetAllByIssuerDataId(target.DataId).Any(x => x.QuestId is LeveId))
+                {
+                    _logger.LogInformation("Adding close leve dialogue as option");
+                    dialogueChoices.Add(new DialogueChoiceInfo(null,
+                        new DialogueChoice
+                        {
+                            Type = EDialogChoiceType.List,
+                            ExcelSheet = "leve/GuildleveAssignment",
+                            Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+                            Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_07"),
+                        }));
+                }
+            }
+        }
+
+        if (dialogueChoices.Count == 0)
+        {
+            _logger.LogDebug("No dialogue choices to check");
+            return null;
+        }
+
+        foreach (var (quest, dialogueChoice) in dialogueChoices)
+        {
+            if (dialogueChoice.Type != EDialogChoiceType.List)
+                continue;
+
+            if (dialogueChoice.Answer == null)
+            {
+                _logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
+                continue;
+            }
+
+            if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
+            {
+                _logger.LogDebug(
+                    "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
+                    dialogueChoice.DataId, _targetManager.Target?.DataId);
+                continue;
+            }
+
+            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
+                ?.GetString();
+            StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer,
+                dialogueChoice.AnswerIsRegularExpression);
+
+            if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
+            {
+                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
+                continue;
+            }
+
+            if (actualPrompt != null &&
+                (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt)))
+            {
+                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
+                    excelPrompt, actualPrompt);
+                continue;
+            }
+
+            for (int i = 0; i < answers.Count; ++i)
+            {
+                _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
+                    answers[i], excelAnswer);
+                if (IsMatch(answers[i], excelAnswer))
+                {
+                    _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
+                        i, answers[i], actualPrompt);
+
+                    // ensure we only open the dialog once
+                    if (quest?.Id is SatisfactionSupplyNpcId)
+                    {
+                        if (_questController.GatheringQuest == null ||
+                            _questController.GatheringQuest.Sequence == 255)
+                            return null;
+
+                        _questController.GatheringQuest.SetSequence(1);
+                        _questController.StartSingleQuest("SatisfactionSupply turn in");
+                    }
+
+                    return i;
+                }
+            }
+        }
+
+        _logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
+        return null;
+    }
+
+    private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
+    {
+        if (actualAnswer == null && expectedAnswer == null)
+            return true;
+
+        if (actualAnswer == null || expectedAnswer == null)
+            return false;
+
+        return expectedAnswer.IsMatch(actualAnswer);
+    }
+
+    private int? HandleInstanceListChoice(string? actualPrompt)
+    {
+        string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString();
+        if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt))
+        {
+            _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
+            return 0; // any instance
+        }
+
+        return null;
+    }
+
+    private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
+    {
+        AddonSelectYesno* addonSelectYesno = (AddonSelectYesno*)args.Addon;
+        SelectYesnoPostSetup(addonSelectYesno, false);
+    }
+
+    [SuppressMessage("ReSharper", "RedundantJumpStatement")]
+    private unsafe void SelectYesnoPostSetup(AddonSelectYesno* addonSelectYesno, bool checkAllSteps)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        string? actualPrompt = addonSelectYesno->AtkUnitBase.AtkValues[0].ReadAtkString();
+        if (actualPrompt == null)
+            return;
+
+        _logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
+        var director = UIState.Instance()->DirectorTodo.Director;
+        if (director != null && director->EventHandlerInfo != null &&
+            director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
+            director->Sequence == 254)
+        {
+            // just close the dialogue for 'do you want to return to next settlement', should prolly be different for
+            // ARR territories
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(1);
+            return;
+        }
+
+        var currentQuest = _questController.StartedQuest;
+        if (currentQuest != null && CheckQuestYesNo(addonSelectYesno, currentQuest, actualPrompt, checkAllSteps))
+            return;
+
+        var simulatedQuest = _questController.SimulatedQuest;
+        if (simulatedQuest != null && HandleTravelYesNo(addonSelectYesno, simulatedQuest, actualPrompt))
+            return;
+
+        var nextQuest = _questController.NextQuest;
+        if (nextQuest != null && CheckQuestYesNo(addonSelectYesno, nextQuest, actualPrompt, checkAllSteps))
+            return;
+
+        return;
+    }
+
+    private unsafe bool CheckQuestYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest,
+        string actualPrompt, bool checkAllSteps)
+    {
+        var quest = currentQuest.Quest;
+        if (checkAllSteps)
+        {
+            var sequence = quest.FindSequence(currentQuest.Sequence);
+            if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest,
+                    sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt))
+                return true;
+        }
+        else
+        {
+            var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+            if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt))
+                return true;
+        }
+
+        if (currentQuest.Quest.Id is LeveId)
+        {
+            var dialogueChoice = new DialogueChoice
+            {
+                Type = EDialogChoiceType.YesNo,
+                ExcelSheet = "Addon",
+                Prompt = new ExcelRef(608),
+                Yes = true
+            };
+
+            if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
+                return true;
+        }
+
+        if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
+            return true;
+
+        return false;
+    }
+
+    private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
+        IList<DialogueChoice> dialogueChoices, string actualPrompt)
+    {
+        _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
+        foreach (var dialogueChoice in dialogueChoices)
+        {
+            if (dialogueChoice.Type != EDialogChoiceType.YesNo)
+                continue;
+
+            if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
+            {
+                _logger.LogDebug(
+                    "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
+                    dialogueChoice.DataId, _targetManager.Target?.DataId);
+                continue;
+            }
+
+            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
+                ?.GetString();
+            if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))
+            {
+                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
+                    excelPrompt, actualPrompt);
+                continue;
+            }
+
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
+            return true;
+        }
+
+        return false;
+    }
+
+    private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
+        QuestController.QuestProgress currentQuest, string actualPrompt)
+    {
+        _logger.LogInformation("TravelYesNo");
+        if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
+        {
+            _logger.LogInformation("Automatically confirming return...");
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+            return true;
+        }
+
+        if (_questController.IsRunning && _gameGui.TryGetAddonByName("HousingSelectBlock", out AtkUnitBase* _))
+        {
+            _logger.LogInformation("Automatically confirming ward selection");
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+            return true;
+        }
+
+        var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
+        if (targetTerritoryId != null &&
+            TryFindWarp(targetTerritoryId.Value, actualPrompt, out uint? warpId, out string? warpText))
+        {
+            _logger.LogInformation("Using warp {Id}, {Prompt}", warpId, warpText);
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+            return true;
+        }
+
+        return false;
+    }
+
+    private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
+    {
+        // this can be triggered either manually (in which case we should increase the step counter), or automatically
+        // (in which case it is ~1 frame later, and the step counter has already been increased)
+        var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
+        if (sequence == null)
+            return null;
+
+        QuestStep? step = sequence.FindStep(currentQuest.Step);
+        if (step != null)
+            _logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}",
+                step.TerritoryId,
+                step.TargetTerritoryId);
+
+        if (step != null && (step.TerritoryId != _clientState.TerritoryType || step.TargetTerritoryId == null) &&
+            step.RequiredGatheredItems.Count > 0)
+        {
+            if (_gatheringData.TryGetGatheringPointId(step.RequiredGatheredItems[0].ItemId,
+                    (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer,
+                    out GatheringPointId? gatheringPointId) &&
+                _gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot? root))
+            {
+                foreach (var gatheringStep in root.Steps)
+                {
+                    if (gatheringStep.TerritoryId == _clientState.TerritoryType &&
+                        gatheringStep.TargetTerritoryId != null)
+                    {
+                        _logger.LogTrace(
+                            "FindTargetTerritoryFromQuestStep (gathering): {CurrentTerritory}, {TargetTerritory}",
+                            gatheringStep.TerritoryId,
+                            gatheringStep.TargetTerritoryId);
+                        return gatheringStep.TargetTerritoryId;
+                    }
+                }
+            }
+        }
+
+        if (step == null || step.TargetTerritoryId == null)
+        {
+            _logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
+            step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
+
+            if (step != null)
+                _logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}",
+                    step.TerritoryId,
+                    step.TargetTerritoryId);
+        }
+
+        if (step == null || step.TargetTerritoryId == null)
+        {
+            _logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
+            return null;
+        }
+
+        _logger.LogDebug("Target territory for quest step: {TargetTerritory}", step.TargetTerritoryId);
+        return step.TargetTerritoryId;
+    }
+
+    private bool TryFindWarp(ushort targetTerritoryId, string actualPrompt, [NotNullWhen(true)] out uint? warpId,
+        [NotNullWhen(true)] out string? warpText)
+    {
+        var warps = _dataManager.GetExcelSheet<Warp>()!
+            .Where(x => x.RowId > 0 && x.TerritoryType.Row == targetTerritoryId);
+        foreach (var entry in warps)
+        {
+            string? excelName = entry.Name?.ToString();
+            string? excelQuestion = entry.Question?.ToString();
+
+            if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt))
+            {
+                warpId = entry.RowId;
+                warpText = excelQuestion;
+                return true;
+            }
+            else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt))
+            {
+                warpId = entry.RowId;
+                warpText = excelName;
+                return true;
+            }
+            else
+            {
+                _logger.LogDebug("Ignoring prompt '{Prompt}'", excelQuestion);
+            }
+        }
+
+        warpId = null;
+        warpText = null;
+        return false;
+    }
+
+    private unsafe void PointMenuPostSetup(AddonEvent type, AddonArgs args)
+    {
+        AtkUnitBase* addonPointMenu = (AtkUnitBase*)args.Addon;
+        PointMenuPostSetup(addonPointMenu);
+    }
+
+    private unsafe void PointMenuPostSetup(AtkUnitBase* addonPointMenu)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        var currentQuest = _questController.StartedQuest;
+        if (currentQuest == null)
+        {
+            _logger.LogInformation("Ignoring point menu, no active quest");
+            return;
+        }
+
+        var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
+        if (sequence == null)
+            return;
+
+        QuestStep? step = sequence.FindStep(currentQuest.Step);
+        if (step == null)
+            return;
+
+        if (step.PointMenuChoices.Count == 0)
+        {
+            _logger.LogWarning("No point menu choices");
+            return;
+        }
+
+        int counter = currentQuest.StepProgress.PointMenuCounter;
+        if (counter >= step.PointMenuChoices.Count)
+        {
+            _logger.LogWarning("No remaining point menu choices");
+            return;
+        }
+
+        uint choice = step.PointMenuChoices[counter];
+
+        _logger.LogInformation("Handling point menu, picking choice {Choice} (index = {Index})", choice, counter);
+        var selectChoice = stackalloc AtkValue[]
+        {
+            new() { Type = ValueType.Int, Int = 13 },
+            new() { Type = ValueType.UInt, UInt = choice }
+        };
+        addonPointMenu->FireCallback(2, selectChoice);
+
+        currentQuest.IncreasePointMenuCounter();
+    }
+
+    private unsafe void HousingSelectBlockPostSetup(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        _logger.LogInformation("Confirming selected housing ward");
+        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+        addon->FireCallbackInt(0);
+    }
+
+    private void TeleportTownPostSetup(AddonEvent type, AddonArgs args)
+    {
+        if (ShouldHandleUiInteractions &&
+            _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) &&
+            EAetheryteLocationExtensions.IsFirmamentAetheryte(aethernetShortcut.From))
+        {
+            // this might be better via atkvalues; but this works for now
+            uint toIndex = aethernetShortcut.To switch
+            {
+                EAetheryteLocation.FirmamentMendicantsCourt => 0,
+                EAetheryteLocation.FirmamentMattock => 1,
+                EAetheryteLocation.FirmamentNewNest => 2,
+                EAetheryteLocation.FirmanentSaintRoellesDais => 3,
+                EAetheryteLocation.FirmamentFeatherfall => 4,
+                EAetheryteLocation.FirmamentHoarfrostHall => 5,
+                EAetheryteLocation.FirmamentWesternRisensongQuarter => 6,
+                EAetheryteLocation.FIrmamentEasternRisensongQuarter => 7,
+                _ => uint.MaxValue,
+            };
+
+            if (toIndex == uint.MaxValue)
+                return;
+
+            _logger.LogInformation("Teleporting to {ToName} with menu index {ToIndex}", aethernetShortcut.From,
+                toIndex);
+            unsafe
+            {
+                var teleportToDestination = stackalloc AtkValue[]
+                {
+                    new() { Type = ValueType.Int, Int = 11 },
+                    new() { Type = ValueType.UInt, UInt = toIndex }
+                };
+
+                var addon = (AtkUnitBase*)args.Addon;
+                addon->FireCallback(2, teleportToDestination);
+                addon->FireCallback(2, teleportToDestination, true);
+            }
+        }
+    }
+
+    private StringOrRegex? ResolveReference(Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
+    {
+        if (excelRef == null)
+            return null;
+
+        if (excelRef.Type == ExcelRef.EType.Key)
+            return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
+        else if (excelRef.Type == ExcelRef.EType.RowId)
+            return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
+        else if (excelRef.Type == ExcelRef.EType.RawString)
+            return new StringOrRegex(excelRef.AsRawString());
+
+        return null;
+    }
+
+    public void Dispose()
+    {
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
+    }
+
+    private sealed record DialogueChoiceInfo(Quest? Quest, DialogueChoice DialogueChoice);
+}
diff --git a/Questionable/Controller/GameUi/LeveUiController.cs b/Questionable/Controller/GameUi/LeveUiController.cs
new file mode 100644 (file)
index 0000000..533d5d3
--- /dev/null
@@ -0,0 +1,154 @@
+using System;
+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.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.Logging;
+using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model.Questing;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class LeveUiController : IDisposable
+{
+    private readonly QuestController _questController;
+    private readonly QuestData _questData;
+    private readonly QuestFunctions _questFunctions;
+    private readonly IAddonLifecycle _addonLifecycle;
+    private readonly IGameGui _gameGui;
+    private readonly ITargetManager _targetManager;
+    private readonly IFramework _framework;
+    private readonly ILogger<LeveUiController> _logger;
+
+    public LeveUiController(QuestController questController, QuestData questData, QuestFunctions questFunctions,
+        IAddonLifecycle addonLifecycle, IGameGui gameGui, ITargetManager targetManager, IFramework framework,
+        ILogger<LeveUiController> logger)
+    {
+        _questController = questController;
+        _questData = questData;
+        _questFunctions = questFunctions;
+        _addonLifecycle = addonLifecycle;
+        _gameGui = gameGui;
+        _targetManager = targetManager;
+        _framework = framework;
+        _logger = logger;
+
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
+    }
+
+    private bool ShouldHandleUiInteractions => _questController.IsRunning;
+
+    private unsafe void JournalResultPostSetup(AddonEvent type, AddonArgs args)
+    {
+        if (!ShouldHandleUiInteractions)
+            return;
+
+        _logger.LogInformation("Checking for quest name of journal result");
+        AddonJournalResult* addon = (AddonJournalResult*)args.Addon;
+
+        string questName = addon->AtkTextNode250->NodeText.ToString();
+        if (_questController.CurrentQuest is { Quest.Id: LeveId } &&
+            GameFunctions.GameStringEquals(_questController.CurrentQuest.Quest.Info.Name, questName))
+        {
+            _logger.LogInformation("JournalResult has the current leve, auto-accepting it");
+            addon->FireCallbackInt(0);
+        }
+        else if (_targetManager.Target is { } target)
+        {
+            var issuedLeves = _questData.GetAllByIssuerDataId(target.DataId)
+                .Where(x => x.QuestId is LeveId)
+                .ToList();
+
+            if (issuedLeves.Any(x => GameFunctions.GameStringEquals(x.Name, questName)))
+            {
+                _logger.LogInformation(
+                    "JournalResult has a leve but not the one we're currently on, auto-declining it");
+                addon->FireCallbackInt(1);
+            }
+        }
+    }
+
+    private unsafe void GuildLevePostSetup(AddonEvent type, AddonArgs args)
+    {
+        var target = _targetManager.Target;
+        if (target == null)
+            return;
+
+        if (_questController is { IsRunning: true, NextQuest: { Quest.Id: LeveId } nextQuest } &&
+            _questFunctions.IsReadyToAcceptQuest(nextQuest.Quest.Id))
+        {
+            var addon = (AddonGuildLeve*)args.Addon;
+            /*
+            var atkValues = addon->AtkValues;
+
+            var availableLeves = _questData.GetAllByIssuerDataId(target.DataId);
+            List<(int, IQuestInfo)> offeredLeves = [];
+            for (int i = 0; i <= 20; ++i) // 3 leves per group, 1 label for group
+            {
+                string? leveName = atkValues[626 + i * 2].ReadAtkString();
+                if (leveName == null)
+                    continue;
+
+                var questInfo = availableLeves.FirstOrDefault(x => GameFunctions.GameStringEquals(x.Name, leveName));
+                if (questInfo == null)
+                    continue;
+
+                offeredLeves.Add((i, questInfo));
+
+            }
+
+            foreach (var (i, questInfo) in offeredLeves)
+                _logger.LogInformation("Leve {Index} = {Id}, {Name}", i, questInfo.QuestId, questInfo.Name);
+            */
+
+            _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest), TimeSpan.FromMilliseconds(100));
+        }
+    }
+
+    private unsafe void AcceptLeveOrWait(QuestController.QuestProgress nextQuest, int counter = 0)
+    {
+        var agent = UIModule.Instance()->GetAgentModule()->GetAgentByInternalId(AgentId.LeveQuest);
+        if (agent->IsAgentActive() &&
+            _gameGui.TryGetAddonByName("GuildLeve", out AddonGuildLeve* addonGuildLeve) &&
+            LAddon.IsAddonReady(&addonGuildLeve->AtkUnitBase) &&
+            _gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail) &&
+            LAddon.IsAddonReady(addonJournalDetail))
+        {
+            AcceptLeve(agent, addonGuildLeve, nextQuest);
+        }
+        else if (counter >= 10)
+            _logger.LogWarning("Unable to accept leve?");
+        else
+            _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest, counter + 1), TimeSpan.FromMilliseconds(100));
+    }
+
+    private unsafe void AcceptLeve(AgentInterface* agent, AddonGuildLeve* addon,
+        QuestController.QuestProgress nextQuest)
+    {
+        _questController.SetPendingQuest(nextQuest);
+        _questController.SetNextQuest(null);
+
+        var returnValue = stackalloc AtkValue[1];
+        var selectQuest = stackalloc AtkValue[]
+        {
+            new() { Type = ValueType.Int, Int = 3 },
+            new() { Type = ValueType.UInt, UInt = nextQuest.Quest.Id.Value }
+        };
+        agent->ReceiveEvent(returnValue, selectQuest, 2, 0);
+        addon->Close(true);
+    }
+
+    public void Dispose()
+    {
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
+    }
+}
diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs
deleted file mode 100644 (file)
index cbc312a..0000000
+++ /dev/null
@@ -1,1032 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text.RegularExpressions;
-using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-using Dalamud.Game.ClientState.Objects;
-using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.Game.Event;
-using FFXIVClientStructs.FFXIV.Client.Game.UI;
-using FFXIVClientStructs.FFXIV.Client.UI;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using LLib;
-using LLib.GameData;
-using LLib.GameUI;
-using Lumina.Excel.GeneratedSheets;
-using Microsoft.Extensions.Logging;
-using Questionable.Controller.Steps.Interactions;
-using Questionable.Data;
-using Questionable.Functions;
-using Questionable.Model;
-using Questionable.Model.Common;
-using Questionable.Model.Gathering;
-using Questionable.Model.Questing;
-using AethernetShortcut = Questionable.Controller.Steps.Shared.AethernetShortcut;
-using Quest = Questionable.Model.Quest;
-using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
-
-namespace Questionable.Controller;
-
-internal sealed class GameUiController : IDisposable
-{
-    private readonly IAddonLifecycle _addonLifecycle;
-    private readonly IDataManager _dataManager;
-    private readonly QuestFunctions _questFunctions;
-    private readonly AetheryteFunctions _aetheryteFunctions;
-    private readonly ExcelFunctions _excelFunctions;
-    private readonly QuestController _questController;
-    private readonly GatheringData _gatheringData;
-    private readonly GatheringPointRegistry _gatheringPointRegistry;
-    private readonly QuestRegistry _questRegistry;
-    private readonly QuestData _questData;
-    private readonly IGameGui _gameGui;
-    private readonly ITargetManager _targetManager;
-    private readonly IFramework _framework;
-    private readonly IClientState _clientState;
-    private readonly ILogger<GameUiController> _logger;
-    private readonly Regex _returnRegex;
-
-    private bool _isInitialCheck;
-
-    public GameUiController(
-        IAddonLifecycle addonLifecycle,
-        IDataManager dataManager,
-        QuestFunctions questFunctions,
-        AetheryteFunctions aetheryteFunctions,
-        ExcelFunctions excelFunctions,
-        QuestController questController,
-        GatheringData gatheringData,
-        GatheringPointRegistry gatheringPointRegistry,
-        QuestRegistry questRegistry,
-        QuestData questData,
-        IGameGui gameGui,
-        ITargetManager targetManager,
-        IFramework framework,
-        IPluginLog pluginLog,
-        IClientState clientState,
-        ILogger<GameUiController> logger)
-    {
-        _addonLifecycle = addonLifecycle;
-        _dataManager = dataManager;
-        _questFunctions = questFunctions;
-        _aetheryteFunctions = aetheryteFunctions;
-        _excelFunctions = excelFunctions;
-        _questController = questController;
-        _gatheringData = gatheringData;
-        _gatheringPointRegistry = gatheringPointRegistry;
-        _questRegistry = questRegistry;
-        _questData = questData;
-        _gameGui = gameGui;
-        _targetManager = targetManager;
-        _framework = framework;
-        _clientState = clientState;
-        _logger = logger;
-
-        _returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
-
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
-        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
-    }
-
-    private bool ShouldHandleUiInteractions => _isInitialCheck || _questController.IsRunning;
-
-    internal unsafe void HandleCurrentDialogueChoices()
-    {
-        try
-        {
-            _isInitialCheck = true;
-            if (_gameGui.TryGetAddonByName("SelectString", out AddonSelectString* addonSelectString))
-            {
-                _logger.LogInformation("SelectString window is open");
-                SelectStringPostSetup(addonSelectString, true);
-            }
-
-            if (_gameGui.TryGetAddonByName("CutSceneSelectString",
-                    out AddonCutSceneSelectString* addonCutSceneSelectString))
-            {
-                _logger.LogInformation("CutSceneSelectString window is open");
-                CutsceneSelectStringPostSetup(addonCutSceneSelectString, true);
-            }
-
-            if (_gameGui.TryGetAddonByName("SelectIconString", out AddonSelectIconString* addonSelectIconString))
-            {
-                _logger.LogInformation("SelectIconString window is open");
-                SelectIconStringPostSetup(addonSelectIconString, true);
-            }
-
-            if (_gameGui.TryGetAddonByName("SelectYesno", out AddonSelectYesno* addonSelectYesno))
-            {
-                _logger.LogInformation("SelectYesno window is open");
-                SelectYesnoPostSetup(addonSelectYesno, true);
-            }
-
-            if (_gameGui.TryGetAddonByName("PointMenu", out AtkUnitBase* addonPointMenu))
-            {
-                _logger.LogInformation("PointMenu is open");
-                PointMenuPostSetup(addonPointMenu);
-            }
-        }
-        finally
-        {
-            _isInitialCheck = false;
-        }
-    }
-
-    private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
-    {
-        AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
-        SelectStringPostSetup(addonSelectString, false);
-    }
-
-    private unsafe void SelectStringPostSetup(AddonSelectString* addonSelectString, bool checkAllSteps)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        string? actualPrompt = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
-        if (actualPrompt == null)
-            return;
-
-        List<string?> answers = new();
-        for (ushort i = 7; i < addonSelectString->AtkUnitBase.AtkValuesCount; ++i)
-        {
-            if (addonSelectString->AtkUnitBase.AtkValues[i].Type == ValueType.String)
-                answers.Add(addonSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
-        }
-
-        int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
-        if (answer != null)
-            addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
-    }
-
-    private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
-    {
-        AddonCutSceneSelectString* addonCutSceneSelectString = (AddonCutSceneSelectString*)args.Addon;
-        CutsceneSelectStringPostSetup(addonCutSceneSelectString, false);
-    }
-
-    private unsafe void CutsceneSelectStringPostSetup(AddonCutSceneSelectString* addonCutSceneSelectString,
-        bool checkAllSteps)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        string? actualPrompt = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
-        if (actualPrompt == null)
-            return;
-
-        List<string?> answers = new();
-        for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; ++i)
-            answers.Add(addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
-
-        int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
-        if (answer != null)
-            addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
-    }
-
-    private unsafe void SelectIconStringPostSetup(AddonEvent type, AddonArgs args)
-    {
-        AddonSelectIconString* addonSelectIconString = (AddonSelectIconString*)args.Addon;
-        SelectIconStringPostSetup(addonSelectIconString, false);
-    }
-
-    [SuppressMessage("ReSharper", "RedundantJumpStatement")]
-    private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        string? actualPrompt = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString();
-        if (string.IsNullOrEmpty(actualPrompt))
-            actualPrompt = null;
-
-        var answers = GetChoices(addonSelectIconString);
-        int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
-        if (answer != null)
-        {
-            addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
-            return;
-        }
-
-        // this is 'Daily Quests' for tribal quests, but not set for normal selections
-        string? title = addonSelectIconString->AtkValues[0].ReadAtkString();
-
-        var currentQuest = _questController.StartedQuest;
-        if (currentQuest != null && (actualPrompt == null || title != null))
-        {
-            _logger.LogInformation("Checking if current quest {Name} is on the list", currentQuest.Quest.Info.Name);
-            if (CheckQuestSelection(addonSelectIconString, currentQuest.Quest, answers))
-                return;
-        }
-
-        var nextQuest = _questController.NextQuest;
-        if (nextQuest != null && (actualPrompt == null || title != null))
-        {
-            _logger.LogInformation("Checking if next quest {Name} is on the list", nextQuest.Quest.Info.Name);
-            if (CheckQuestSelection(addonSelectIconString, nextQuest.Quest, answers))
-                return;
-        }
-    }
-
-    private unsafe bool CheckQuestSelection(AddonSelectIconString* addonSelectIconString, Quest quest,
-        List<string?> answers)
-    {
-        // it is possible for this to be a quest selection
-        string questName = quest.Info.Name;
-        int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
-        if (questSelection >= 0)
-        {
-            addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
-            return true;
-        }
-
-        return false;
-    }
-
-    public static unsafe List<string?> GetChoices(AddonSelectIconString* addonSelectIconString)
-    {
-        List<string?> answers = new();
-        for (ushort i = 0; i < addonSelectIconString->AtkUnitBase.AtkValues[5].Int; i++)
-            answers.Add(addonSelectIconString->AtkValues[i * 3 + 7].ReadAtkString());
-
-        return answers;
-    }
-
-    private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
-    {
-        List<DialogueChoiceInfo> dialogueChoices = [];
-
-        // levequest choices have some vague sort of priority
-        if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
-            interact.Quest != null &&
-            interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
-        {
-            if (interact.InteractionType == EInteractionType.AcceptLeve)
-            {
-                dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
-                    new DialogueChoice
-                    {
-                        Type = EDialogChoiceType.List,
-                        ExcelSheet = "leve/GuildleveAssignment",
-                        Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
-                        Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_01"),
-                    }));
-                interact.InteractionType = EInteractionType.None;
-            }
-            else if (interact.InteractionType == EInteractionType.CompleteLeve)
-            {
-                dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
-                    new DialogueChoice
-                    {
-                        Type = EDialogChoiceType.List,
-                        ExcelSheet = "leve/GuildleveAssignment",
-                        Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
-                        Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_REWARD"),
-                    }));
-                interact.InteractionType = EInteractionType.None;
-            }
-        }
-
-        var currentQuest = _questController.SimulatedQuest ??
-                           _questController.GatheringQuest ??
-                           _questController.StartedQuest;
-        if (currentQuest != null)
-        {
-            var quest = currentQuest.Quest;
-            if (checkAllSteps)
-            {
-                var sequence = quest.FindSequence(currentQuest.Sequence);
-                var choices = sequence?.Steps.SelectMany(x => x.DialogueChoices);
-                if (choices != null)
-                    dialogueChoices.AddRange(choices.Select(x => new DialogueChoiceInfo(quest, x)));
-            }
-            else
-            {
-                var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
-                if (step == null)
-                    _logger.LogDebug("Ignoring current quest dialogue choices, no active step");
-                else
-                    dialogueChoices.AddRange(step.DialogueChoices.Select(x => new DialogueChoiceInfo(quest, x)));
-            }
-
-            // add all travel dialogue choices
-            var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
-            if (targetTerritoryId != null)
-            {
-                foreach (string? answer in answers)
-                {
-                    if (answer == null)
-                        continue;
-
-                    if (TryFindWarp(targetTerritoryId.Value, answer, out uint? warpId, out string? warpText))
-                    {
-                        _logger.LogInformation("Adding warp {Id}, {Prompt}", warpId, warpText);
-                        dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice
-                        {
-                            Type = EDialogChoiceType.List,
-                            ExcelSheet = null,
-                            Prompt = null,
-                            Answer = ExcelRef.FromSheetValue(warpText),
-                        }));
-                    }
-                }
-            }
-        }
-        else
-            _logger.LogDebug("Ignoring current quest dialogue choices, no active quest");
-
-        // add all quests that start with the targeted npc
-        var target = _targetManager.Target;
-        if (target != null)
-        {
-            foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId).Where(x => x.QuestId is QuestId))
-            {
-                if (_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) &&
-                    _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest))
-                {
-                    var questChoices = knownQuest.FindSequence(0)?.Steps
-                        .SelectMany(x => x.DialogueChoices)
-                        .ToList();
-                    if (questChoices != null && questChoices.Count > 0)
-                    {
-                        _logger.LogInformation("Adding {Count} dialogue choices from not accepted quest {QuestName}",
-                            questChoices.Count, questInfo.Name);
-                        dialogueChoices.AddRange(questChoices.Select(x => new DialogueChoiceInfo(knownQuest, x)));
-                    }
-                }
-            }
-
-            if ((_questController.IsRunning || _questController.WasLastTaskUpdateWithin(TimeSpan.FromSeconds(5)))
-                && _questController.NextQuest == null)
-            {
-                // make sure to always close the leve dialogue
-                if (_questData.GetAllByIssuerDataId(target.DataId).Any(x => x.QuestId is LeveId))
-                {
-                    _logger.LogInformation("Adding close leve dialogue as option");
-                    dialogueChoices.Add(new DialogueChoiceInfo(null,
-                        new DialogueChoice
-                        {
-                            Type = EDialogChoiceType.List,
-                            ExcelSheet = "leve/GuildleveAssignment",
-                            Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
-                            Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_07"),
-                        }));
-                }
-            }
-        }
-
-        if (dialogueChoices.Count == 0)
-        {
-            _logger.LogDebug("No dialogue choices to check");
-            return null;
-        }
-
-        foreach (var (quest, dialogueChoice) in dialogueChoices)
-        {
-            if (dialogueChoice.Type != EDialogChoiceType.List)
-                continue;
-
-            if (dialogueChoice.Answer == null)
-            {
-                _logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
-                continue;
-            }
-
-            if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
-            {
-                _logger.LogDebug(
-                    "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
-                    dialogueChoice.DataId, _targetManager.Target?.DataId);
-                continue;
-            }
-
-            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
-                ?.GetString();
-            StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer,
-                dialogueChoice.AnswerIsRegularExpression);
-
-            if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
-            {
-                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
-                continue;
-            }
-
-            if (actualPrompt != null &&
-                (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt)))
-            {
-                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
-                    excelPrompt, actualPrompt);
-                continue;
-            }
-
-            for (int i = 0; i < answers.Count; ++i)
-            {
-                _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
-                    answers[i], excelAnswer);
-                if (IsMatch(answers[i], excelAnswer))
-                {
-                    _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
-                        i, answers[i], actualPrompt);
-
-                    // ensure we only open the dialog once
-                    if (quest?.Id is SatisfactionSupplyNpcId)
-                    {
-                        if (_questController.GatheringQuest == null ||
-                            _questController.GatheringQuest.Sequence == 255)
-                            return null;
-
-                        _questController.GatheringQuest.SetSequence(1);
-                        _questController.StartSingleQuest("SatisfactionSupply turn in");
-                    }
-
-                    return i;
-                }
-            }
-        }
-
-        _logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
-        return null;
-    }
-
-    private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
-    {
-        if (actualAnswer == null && expectedAnswer == null)
-            return true;
-
-        if (actualAnswer == null || expectedAnswer == null)
-            return false;
-
-        return expectedAnswer.IsMatch(actualAnswer);
-    }
-
-    private int? HandleInstanceListChoice(string? actualPrompt)
-    {
-        string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString();
-        if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt))
-        {
-            _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
-            return 0; // any instance
-        }
-
-        return null;
-    }
-
-    private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
-    {
-        AddonSelectYesno* addonSelectYesno = (AddonSelectYesno*)args.Addon;
-        SelectYesnoPostSetup(addonSelectYesno, false);
-    }
-
-    [SuppressMessage("ReSharper", "RedundantJumpStatement")]
-    private unsafe void SelectYesnoPostSetup(AddonSelectYesno* addonSelectYesno, bool checkAllSteps)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        string? actualPrompt = addonSelectYesno->AtkUnitBase.AtkValues[0].ReadAtkString();
-        if (actualPrompt == null)
-            return;
-
-        _logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
-        var director = UIState.Instance()->DirectorTodo.Director;
-        if (director != null && director->EventHandlerInfo != null &&
-            director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
-            director->Sequence == 254)
-        {
-            // just close the dialogue for 'do you want to return to next settlement', should prolly be different for
-            // ARR territories
-            addonSelectYesno->AtkUnitBase.FireCallbackInt(1);
-            return;
-        }
-
-        var currentQuest = _questController.StartedQuest;
-        if (currentQuest != null && CheckQuestYesNo(addonSelectYesno, currentQuest, actualPrompt, checkAllSteps))
-            return;
-
-        var simulatedQuest = _questController.SimulatedQuest;
-        if (simulatedQuest != null && HandleTravelYesNo(addonSelectYesno, simulatedQuest, actualPrompt))
-            return;
-
-        var nextQuest = _questController.NextQuest;
-        if (nextQuest != null && CheckQuestYesNo(addonSelectYesno, nextQuest, actualPrompt, checkAllSteps))
-            return;
-
-        return;
-    }
-
-    private unsafe bool CheckQuestYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest,
-        string actualPrompt, bool checkAllSteps)
-    {
-        var quest = currentQuest.Quest;
-        if (checkAllSteps)
-        {
-            var sequence = quest.FindSequence(currentQuest.Sequence);
-            if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest,
-                    sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt))
-                return true;
-        }
-        else
-        {
-            var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
-            if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt))
-                return true;
-        }
-
-        if (currentQuest.Quest.Id is LeveId)
-        {
-            var dialogueChoice = new DialogueChoice
-            {
-                Type = EDialogChoiceType.YesNo,
-                ExcelSheet = "Addon",
-                Prompt = new ExcelRef(608),
-                Yes = true
-            };
-
-            if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
-                return true;
-        }
-
-        if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
-            return true;
-
-        return false;
-    }
-
-    private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
-        IList<DialogueChoice> dialogueChoices, string actualPrompt)
-    {
-        _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
-        foreach (var dialogueChoice in dialogueChoices)
-        {
-            if (dialogueChoice.Type != EDialogChoiceType.YesNo)
-                continue;
-
-            if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
-            {
-                _logger.LogDebug(
-                    "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
-                    dialogueChoice.DataId, _targetManager.Target?.DataId);
-                continue;
-            }
-
-            string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
-                ?.GetString();
-            if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))
-            {
-                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
-                    excelPrompt, actualPrompt);
-                continue;
-            }
-
-            addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
-            return true;
-        }
-
-        return false;
-    }
-
-    private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
-        QuestController.QuestProgress currentQuest, string actualPrompt)
-    {
-        _logger.LogInformation("TravelYesNo");
-        if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
-        {
-            _logger.LogInformation("Automatically confirming return...");
-            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
-            return true;
-        }
-
-        if (_questController.IsRunning && _gameGui.TryGetAddonByName("HousingSelectBlock", out AtkUnitBase* _))
-        {
-            _logger.LogInformation("Automatically confirming ward selection");
-            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
-            return true;
-        }
-
-        var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
-        if (targetTerritoryId != null &&
-            TryFindWarp(targetTerritoryId.Value, actualPrompt, out uint? warpId, out string? warpText))
-        {
-            _logger.LogInformation("Using warp {Id}, {Prompt}", warpId, warpText);
-            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
-            return true;
-        }
-
-        return false;
-    }
-
-    private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
-    {
-        // this can be triggered either manually (in which case we should increase the step counter), or automatically
-        // (in which case it is ~1 frame later, and the step counter has already been increased)
-        var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
-        if (sequence == null)
-            return null;
-
-        QuestStep? step = sequence.FindStep(currentQuest.Step);
-        if (step != null)
-            _logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}",
-                step.TerritoryId,
-                step.TargetTerritoryId);
-
-        if (step != null && (step.TerritoryId != _clientState.TerritoryType || step.TargetTerritoryId == null) &&
-            step.RequiredGatheredItems.Count > 0)
-        {
-            if (_gatheringData.TryGetGatheringPointId(step.RequiredGatheredItems[0].ItemId,
-                    (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer,
-                    out GatheringPointId? gatheringPointId) &&
-                _gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot? root))
-            {
-                foreach (var gatheringStep in root.Steps)
-                {
-                    if (gatheringStep.TerritoryId == _clientState.TerritoryType &&
-                        gatheringStep.TargetTerritoryId != null)
-                    {
-                        _logger.LogTrace(
-                            "FindTargetTerritoryFromQuestStep (gathering): {CurrentTerritory}, {TargetTerritory}",
-                            gatheringStep.TerritoryId,
-                            gatheringStep.TargetTerritoryId);
-                        return gatheringStep.TargetTerritoryId;
-                    }
-                }
-            }
-        }
-
-        if (step == null || step.TargetTerritoryId == null)
-        {
-            _logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
-            step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
-
-            if (step != null)
-                _logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}",
-                    step.TerritoryId,
-                    step.TargetTerritoryId);
-        }
-
-        if (step == null || step.TargetTerritoryId == null)
-        {
-            _logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
-            return null;
-        }
-
-        _logger.LogDebug("Target territory for quest step: {TargetTerritory}", step.TargetTerritoryId);
-        return step.TargetTerritoryId;
-    }
-
-    private bool TryFindWarp(ushort targetTerritoryId, string actualPrompt, [NotNullWhen(true)] out uint? warpId,
-        [NotNullWhen(true)] out string? warpText)
-    {
-        var warps = _dataManager.GetExcelSheet<Warp>()!
-            .Where(x => x.RowId > 0 && x.TerritoryType.Row == targetTerritoryId);
-        foreach (var entry in warps)
-        {
-            string? excelName = entry.Name?.ToString();
-            string? excelQuestion = entry.Question?.ToString();
-
-            if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt))
-            {
-                warpId = entry.RowId;
-                warpText = excelQuestion;
-                return true;
-            }
-            else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt))
-            {
-                warpId = entry.RowId;
-                warpText = excelName;
-                return true;
-            }
-            else
-            {
-                _logger.LogDebug("Ignoring prompt '{Prompt}'", excelQuestion);
-            }
-        }
-
-        warpId = null;
-        warpText = null;
-        return false;
-    }
-
-    private unsafe void PointMenuPostSetup(AddonEvent type, AddonArgs args)
-    {
-        AtkUnitBase* addonPointMenu = (AtkUnitBase*)args.Addon;
-        PointMenuPostSetup(addonPointMenu);
-    }
-
-    private unsafe void PointMenuPostSetup(AtkUnitBase* addonPointMenu)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        var currentQuest = _questController.StartedQuest;
-        if (currentQuest == null)
-        {
-            _logger.LogInformation("Ignoring point menu, no active quest");
-            return;
-        }
-
-        var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
-        if (sequence == null)
-            return;
-
-        QuestStep? step = sequence.FindStep(currentQuest.Step);
-        if (step == null)
-            return;
-
-        if (step.PointMenuChoices.Count == 0)
-        {
-            _logger.LogWarning("No point menu choices");
-            return;
-        }
-
-        int counter = currentQuest.StepProgress.PointMenuCounter;
-        if (counter >= step.PointMenuChoices.Count)
-        {
-            _logger.LogWarning("No remaining point menu choices");
-            return;
-        }
-
-        uint choice = step.PointMenuChoices[counter];
-
-        _logger.LogInformation("Handling point menu, picking choice {Choice} (index = {Index})", choice, counter);
-        var selectChoice = stackalloc AtkValue[]
-        {
-            new() { Type = ValueType.Int, Int = 13 },
-            new() { Type = ValueType.UInt, UInt = choice }
-        };
-        addonPointMenu->FireCallback(2, selectChoice);
-
-        currentQuest.IncreasePointMenuCounter();
-    }
-
-    /// <summary>
-    /// ARR Credits.
-    /// </summary>
-    private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args)
-    {
-        _logger.LogInformation("Closing Credits sequence");
-        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-        addon->FireCallbackInt(-2);
-    }
-
-    /// <summary>
-    /// Credits for (possibly all?) expansions, not used for ARR.
-    /// </summary>
-    private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
-    {
-        _logger.LogInformation("Closing Credits sequence");
-        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-        addon->FireCallbackInt(-2);
-    }
-
-    private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args)
-    {
-        _logger.LogInformation("Closing CreditPlayer");
-        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-        addon->Close(true);
-    }
-
-    private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        if (_questController.StartedQuest?.Quest.Id.Value == 4526)
-        {
-            _logger.LogInformation("Closing Unending Codex");
-            AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-            addon->FireCallbackInt(-2);
-        }
-    }
-
-    private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        if (_questController.StartedQuest?.Quest.Id.Value == 245)
-        {
-            _logger.LogInformation("Closing ContentsTutorial");
-            AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-            addon->FireCallbackInt(13);
-        }
-    }
-
-    /// <summary>
-    /// Opened e.g. the first time you open the duty finder window during Sastasha.
-    /// </summary>
-    private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        if (_questController.StartedQuest?.Quest.Id.Value == 245)
-        {
-            _logger.LogInformation("Closing MultipleHelpWindow");
-            AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-            addon->FireCallbackInt(-2);
-            addon->FireCallbackInt(-1);
-        }
-    }
-
-    private unsafe void HousingSelectBlockPostSetup(AddonEvent type, AddonArgs args)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        _logger.LogInformation("Confirming selected housing ward");
-        AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
-        addon->FireCallbackInt(0);
-    }
-
-    private unsafe void JournalResultPostSetup(AddonEvent type, AddonArgs args)
-    {
-        if (!ShouldHandleUiInteractions)
-            return;
-
-        _logger.LogInformation("Checking for quest name of journal result");
-        AddonJournalResult* addon = (AddonJournalResult*)args.Addon;
-
-        string questName = addon->AtkTextNode250->NodeText.ToString();
-        if (_questController.CurrentQuest is { Quest.Id: LeveId } &&
-            GameFunctions.GameStringEquals(_questController.CurrentQuest.Quest.Info.Name, questName))
-        {
-            _logger.LogInformation("JournalResult has the current leve, auto-accepting it");
-            addon->FireCallbackInt(0);
-        }
-        else if (_targetManager.Target is { } target)
-        {
-            var issuedLeves = _questData.GetAllByIssuerDataId(target.DataId)
-                .Where(x => x.QuestId is LeveId)
-                .ToList();
-
-            if (issuedLeves.Any(x => GameFunctions.GameStringEquals(x.Name, questName)))
-            {
-                _logger.LogInformation(
-                    "JournalResult has a leve but not the one we're currently on, auto-declining it");
-                addon->FireCallbackInt(1);
-            }
-        }
-    }
-
-    private unsafe void GuildLevePostSetup(AddonEvent type, AddonArgs args)
-    {
-        var target = _targetManager.Target;
-        if (target == null)
-            return;
-
-        if (_questController is { IsRunning: true, NextQuest: { Quest.Id: LeveId } nextQuest } &&
-            _questFunctions.IsReadyToAcceptQuest(nextQuest.Quest.Id))
-        {
-            var addon = (AddonGuildLeve*)args.Addon;
-            /*
-            var atkValues = addon->AtkValues;
-
-            var availableLeves = _questData.GetAllByIssuerDataId(target.DataId);
-            List<(int, IQuestInfo)> offeredLeves = [];
-            for (int i = 0; i <= 20; ++i) // 3 leves per group, 1 label for group
-            {
-                string? leveName = atkValues[626 + i * 2].ReadAtkString();
-                if (leveName == null)
-                    continue;
-
-                var questInfo = availableLeves.FirstOrDefault(x => GameFunctions.GameStringEquals(x.Name, leveName));
-                if (questInfo == null)
-                    continue;
-
-                offeredLeves.Add((i, questInfo));
-
-            }
-
-            foreach (var (i, questInfo) in offeredLeves)
-                _logger.LogInformation("Leve {Index} = {Id}, {Name}", i, questInfo.QuestId, questInfo.Name);
-            */
-
-            _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest), TimeSpan.FromMilliseconds(100));
-        }
-    }
-
-    private unsafe void AcceptLeveOrWait(QuestController.QuestProgress nextQuest, int counter = 0)
-    {
-        var agent = UIModule.Instance()->GetAgentModule()->GetAgentByInternalId(AgentId.LeveQuest);
-        if (agent->IsAgentActive() &&
-            _gameGui.TryGetAddonByName("GuildLeve", out AddonGuildLeve* addonGuildLeve) &&
-            LAddon.IsAddonReady(&addonGuildLeve->AtkUnitBase) &&
-            _gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail) &&
-            LAddon.IsAddonReady(addonJournalDetail))
-        {
-            AcceptLeve(agent, addonGuildLeve, nextQuest);
-        }
-        else if (counter >= 10)
-            _logger.LogWarning("Unable to accept leve?");
-        else
-            _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest, counter + 1), TimeSpan.FromMilliseconds(100));
-    }
-
-    private unsafe void AcceptLeve(AgentInterface* agent, AddonGuildLeve* addon,
-        QuestController.QuestProgress nextQuest)
-    {
-        _questController.SetPendingQuest(nextQuest);
-        _questController.SetNextQuest(null);
-
-        var returnValue = stackalloc AtkValue[1];
-        var selectQuest = stackalloc AtkValue[]
-        {
-            new() { Type = ValueType.Int, Int = 3 },
-            new() { Type = ValueType.UInt, UInt = nextQuest.Quest.Id.Value }
-        };
-        agent->ReceiveEvent(returnValue, selectQuest, 2, 0);
-        addon->Close(true);
-    }
-
-    private void TeleportTownPostSetup(AddonEvent type, AddonArgs args)
-    {
-        if (ShouldHandleUiInteractions &&
-            _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) &&
-            aethernetShortcut.From.IsFirmamentAetheryte())
-        {
-            // this might be better via atkvalues; but this works for now
-            uint toIndex = aethernetShortcut.To switch
-            {
-                EAetheryteLocation.FirmamentMendicantsCourt => 0,
-                EAetheryteLocation.FirmamentMattock => 1,
-                EAetheryteLocation.FirmamentNewNest => 2,
-                EAetheryteLocation.FirmanentSaintRoellesDais => 3,
-                EAetheryteLocation.FirmamentFeatherfall => 4,
-                EAetheryteLocation.FirmamentHoarfrostHall => 5,
-                EAetheryteLocation.FirmamentWesternRisensongQuarter => 6,
-                EAetheryteLocation.FIrmamentEasternRisensongQuarter => 7,
-                _ => uint.MaxValue,
-            };
-
-            if (toIndex == uint.MaxValue)
-                return;
-
-            _logger.LogInformation("Teleporting to {ToName} with menu index {ToIndex}", aethernetShortcut.From,
-                toIndex);
-            unsafe
-            {
-                var teleportToDestination = stackalloc AtkValue[]
-                {
-                    new() { Type = ValueType.Int, Int = 11 },
-                    new() { Type = ValueType.UInt, UInt = toIndex }
-                };
-
-                var addon = (AtkUnitBase*)args.Addon;
-                addon->FireCallback(2, teleportToDestination);
-                addon->FireCallback(2, teleportToDestination, true);
-            }
-        }
-    }
-
-    private StringOrRegex? ResolveReference(Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
-    {
-        if (excelRef == null)
-            return null;
-
-        if (excelRef.Type == ExcelRef.EType.Key)
-            return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
-        else if (excelRef.Type == ExcelRef.EType.RowId)
-            return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
-        else if (excelRef.Type == ExcelRef.EType.RawString)
-            return new StringOrRegex(excelRef.AsRawString());
-
-        return null;
-    }
-
-    public void Dispose()
-    {
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
-        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
-    }
-
-    private sealed record DialogueChoiceInfo(Quest? Quest, DialogueChoice DialogueChoice);
-}
index 4d19d73b81fbae303f0f6c6896a032a2e021a04b..63dfba1060b68a30ebb7f013ed0415e0cd631411 100644 (file)
@@ -81,7 +81,7 @@ internal abstract class MiniTaskController<T>
 
                 while (_taskQueue.TryDequeue(out ITask? nextTask))
                 {
-                    if (nextTask is ILastTask)
+                    if (nextTask is ILastTask or GatheringRequiredItems.SkipMarker)
                     {
                         _currentTask = nextTask;
                         return;
index 9fdf9033878fc371b835118a67e823a43fdad19e..9318ce13c8b9bcd7c51199129bc7f65086f12db7 100644 (file)
@@ -68,7 +68,9 @@ internal static class GatheringRequiredItems
                     {
                         foreach (var task in serviceProvider.GetRequiredService<TaskCreator>()
                                      .CreateTasks(quest, gatheringSequence, gatheringStep))
-                            if (task is not WaitAtEnd.NextStep)
+                            if (task is WaitAtEnd.NextStep)
+                                yield return serviceProvider.GetRequiredService<SkipMarker>();
+                            else
                                 yield return task;
                     }
                 }
@@ -143,4 +145,13 @@ internal static class GatheringRequiredItems
                     $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {_gatheredItem.Collectability})";
         }
     }
+
+    /// <summary>
+    /// A task that does nothing, but if we're skipping a step, this will be the task next in queue to be executed (instead of progressing to the next step) if gathering.
+    /// </summary>
+    internal sealed class SkipMarker : ITask
+    {
+        public bool Start() => true;
+        public ETaskResult Update() => ETaskResult.TaskComplete;
+    }
 }
index 7cd37391a51ad534e2de0f287925e813b3fdcad5..f510fb71796ceb7e1a562553bcb3c9539ec7394b 100644 (file)
@@ -6,6 +6,7 @@ using Dalamud.Plugin;
 using Dalamud.Plugin.Services;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller;
+using Questionable.Controller.GameUi;
 using Questionable.Windows;
 
 namespace Questionable;
@@ -27,7 +28,7 @@ internal sealed class DalamudInitializer : IDisposable
         IFramework framework,
         QuestController questController,
         MovementController movementController,
-        GameUiController gameUiController,
+        InteractionUiController interactionUiController,
         WindowSystem windowSystem,
         QuestWindow questWindow,
         DebugOverlay debugOverlay,
@@ -59,7 +60,7 @@ internal sealed class DalamudInitializer : IDisposable
         _pluginInterface.UiBuilder.OpenMainUi += _questWindow.Toggle;
         _pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
         _framework.Update += FrameworkUpdate;
-        _framework.RunOnTick(gameUiController.HandleCurrentDialogueChoices, TimeSpan.FromMilliseconds(200));
+        _framework.RunOnTick(interactionUiController.HandleCurrentDialogueChoices, TimeSpan.FromMilliseconds(200));
         _toastGui.Toast += OnToast;
         _toastGui.ErrorToast += OnErrorToast;
         _toastGui.QuestToast += OnQuestToast;
index 8c3a1816aef9fa24bb490647d7cc189949a1fd55..77440ad6e2dc836ec76ffceedb2279e1be2026f0 100644 (file)
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Controller.CombatModules;
+using Questionable.Controller.GameUi;
 using Questionable.Controller.NavigationOverrides;
 using Questionable.Controller.Steps;
 using Questionable.Controller.Steps.Shared;
@@ -48,7 +49,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         IAddonLifecycle addonLifecycle,
         IKeyState keyState,
         IContextMenu contextMenu,
-        IToastGui toastGui)
+        IToastGui toastGui,
+        IGameInteropProvider gameInteropProvider)
     {
         ArgumentNullException.ThrowIfNull(pluginInterface);
         ArgumentNullException.ThrowIfNull(chatGui);
@@ -75,6 +77,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
             serviceCollection.AddSingleton(keyState);
             serviceCollection.AddSingleton(contextMenu);
             serviceCollection.AddSingleton(toastGui);
+            serviceCollection.AddSingleton(gameInteropProvider);
             serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
             serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
 
@@ -131,7 +134,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         // task factories
         serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
         serviceCollection.AddSingleton<ITaskFactory, EquipRecommended.BeforeDutyOrInstance>();
-        serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering>();
+        serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering, GatheringRequiredItems.SkipMarker>();
         serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
         serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>();
         serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
@@ -184,11 +187,16 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<GatheringPointRegistry>();
         serviceCollection.AddSingleton<QuestRegistry>();
         serviceCollection.AddSingleton<QuestController>();
-        serviceCollection.AddSingleton<GameUiController>();
         serviceCollection.AddSingleton<CombatController>();
         serviceCollection.AddSingleton<GatheringController>();
         serviceCollection.AddSingleton<ContextMenuController>();
 
+        serviceCollection.AddSingleton<CraftworksSupplyController>();
+        serviceCollection.AddSingleton<CreditsController>();
+        serviceCollection.AddSingleton<HelpUiController>();
+        serviceCollection.AddSingleton<InteractionUiController>();
+        serviceCollection.AddSingleton<LeveUiController>();
+
         serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
     }
 
@@ -231,6 +239,10 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceProvider.GetRequiredService<GatheringPointRegistry>().Reload();
         serviceProvider.GetRequiredService<CommandHandler>();
         serviceProvider.GetRequiredService<ContextMenuController>();
+        serviceProvider.GetRequiredService<CraftworksSupplyController>();
+        serviceProvider.GetRequiredService<CreditsController>();
+        serviceProvider.GetRequiredService<HelpUiController>();
+        serviceProvider.GetRequiredService<LeveUiController>();
         serviceProvider.GetRequiredService<DalamudInitializer>();
     }
 
index c91f0df2fe4219325a0cc6a4c6116b8a2a2ca1fb..80050d4072063b2546fb5807690d909291d274cf 100644 (file)
@@ -51,12 +51,14 @@ internal sealed class QuestValidator
                     {
                         foreach (var issue in validator.Validate(quest))
                         {
+                            /*
                             var level = issue.Severity == EIssueSeverity.Error
                                 ? LogLevel.Warning
                                 : LogLevel.Debug;
                             _logger.Log(level,
                                 "Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}",
                                 issue.ElementId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description);
+                            */
                             if (issue.Type == EIssueType.QuestDisabled && quest.Info.AlliedSociety != EAlliedSociety.None)
                             {
                                 disabledTribeQuests.TryAdd(quest.Info.AlliedSociety, 0);
index 48e0b333210fec8f0dad4d0325120fb722ff9fdd..ba9b5ee1ac35e052e039a64a97f848cdb5656e31 100644 (file)
@@ -16,6 +16,7 @@ using ImGuiNET;
 using LLib.GameUI;
 using LLib.ImGui;
 using Questionable.Controller;
+using Questionable.Controller.GameUi;
 using Questionable.Data;
 using Questionable.Functions;
 using Questionable.Model;
@@ -88,7 +89,7 @@ internal sealed class QuestSelectionWindow : LWindow
             _quests = _questData.GetAllByIssuerDataId(targetId);
             if (_gameGui.TryGetAddonByName<AddonSelectIconString>("SelectIconString", out var addonSelectIconString))
             {
-                var answers = GameUiController.GetChoices(addonSelectIconString);
+                var answers = InteractionUiController.GetChoices(addonSelectIconString);
                 _offeredQuests = _quests
                     .Where(x => answers.Any(y => GameFunctions.GameStringEquals(x.Name, y)))
                     .ToList();
index b5c30076e4e86160e0b6fbc5f2ed083f40b68d3b..296f0ac5475eb05eb434d53739c04dc725b62bb6 100644 (file)
@@ -7,6 +7,7 @@ using Dalamud.Plugin.Services;
 using ImGuiNET;
 using LLib.ImGui;
 using Questionable.Controller;
+using Questionable.Controller.GameUi;
 using Questionable.Data;
 using Questionable.Windows.QuestComponents;
 
@@ -27,7 +28,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
     private readonly QuickAccessButtonsComponent _quickAccessButtonsComponent;
     private readonly RemainingTasksComponent _remainingTasksComponent;
     private readonly IFramework _framework;
-    private readonly GameUiController _gameUiController;
+    private readonly InteractionUiController _interactionUiController;
     private readonly TitleBarButton _minimizeButton;
 
     public QuestWindow(IDalamudPluginInterface pluginInterface,
@@ -41,7 +42,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
         QuickAccessButtonsComponent quickAccessButtonsComponent,
         RemainingTasksComponent remainingTasksComponent,
         IFramework framework,
-        GameUiController gameUiController)
+        InteractionUiController interactionUiController)
         : base($"Questionable v{PluginVersion.ToString(2)}###Questionable",
             ImGuiWindowFlags.AlwaysAutoResize)
     {
@@ -56,7 +57,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
         _quickAccessButtonsComponent = quickAccessButtonsComponent;
         _remainingTasksComponent = remainingTasksComponent;
         _framework = framework;
-        _gameUiController = gameUiController;
+        _interactionUiController = interactionUiController;
 
 #if DEBUG
         IsOpen = true;
@@ -152,7 +153,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
     internal void Reload()
     {
         _questController.Reload();
-        _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
+        _framework.RunOnTick(() => _interactionUiController.HandleCurrentDialogueChoices(),
             TimeSpan.FromMilliseconds(200));
     }
 }