Configure a few early MSQ battles
authorLiza Carvelli <liza@carvel.li>
Fri, 21 Feb 2025 23:03:06 +0000 (00:03 +0100)
committerLiza Carvelli <liza@carvel.li>
Fri, 21 Feb 2025 23:03:06 +0000 (00:03 +0100)
58 files changed:
QuestPathGenerator/RoslynElements/QuestStepExtensions.cs
QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs [new file with mode: 0644]
QuestPathGenerator/RoslynShortcuts.cs
QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json
QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json
QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json
QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json
QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json
QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json
QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json
QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json
QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/466_Double Dealing.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/303_Step Nine.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/334_Storms on the Horizon.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3853_Heir Today, Gone Tomorrow.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/A0-Gridania to East Shroud/3856_We Come in Peace.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json
QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json
QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json
QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json
QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json
QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json
QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json
QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json
QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json
QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json
QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json
QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json
QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/QuestStep.cs
Questionable.Model/Questing/SinglePlayerDutyOptions.cs [new file with mode: 0644]
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/Controller/Steps/Common/SendNotification.cs
Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/External/BossModIpc.cs
Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs

index 7e57e1ae1366fee391e377e121f346120350486a..1ff4fbc1b0df1207218e412d90bc003d95c18d9b 100644 (file)
@@ -108,7 +108,7 @@ internal static class QuestStepExtensions
                             AssignmentList(nameof(QuestStep.ComplexCombatData), step.ComplexCombatData)
                                 .AsSyntaxNodeOrToken(),
                             Assignment(nameof(QuestStep.CombatItemUse), step.CombatItemUse,
-                                emptyStep.CombatItemUse)
+                                    emptyStep.CombatItemUse)
                                 .AsSyntaxNodeOrToken(),
                             Assignment(nameof(QuestStep.CombatDelaySecondsAtStart),
                                     step.CombatDelaySecondsAtStart,
@@ -123,14 +123,8 @@ internal static class QuestStepExtensions
                             Assignment(nameof(QuestStep.AutoDutyEnabled),
                                     step.AutoDutyEnabled, emptyStep.AutoDutyEnabled)
                                 .AsSyntaxNodeOrToken(),
-                            Assignment(nameof(QuestStep.BossModEnabled),
-                                    step.BossModEnabled, emptyStep.BossModEnabled)
-                                .AsSyntaxNodeOrToken(),
-                            Assignment(nameof(QuestStep.BossModNotes),
-                                    step.BossModNotes, emptyStep.BossModNotes)
-                                .AsSyntaxNodeOrToken(),
-                            Assignment(nameof(QuestStep.SinglePlayerDutyIndex),
-                                    step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex)
+                            Assignment(nameof(QuestStep.SinglePlayerDutyOptions), step.SinglePlayerDutyOptions,
+                                emptyStep.SinglePlayerDutyOptions)
                                 .AsSyntaxNodeOrToken(),
                             Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
                                     emptyStep.SkipConditions)
diff --git a/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs b/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs
new file mode 100644 (file)
index 0000000..7a54531
--- /dev/null
@@ -0,0 +1,31 @@
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Questionable.Model.Questing;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Questionable.QuestPathGenerator.RoslynShortcuts;
+
+namespace Questionable.QuestPathGenerator.RoslynElements;
+
+internal static class SinglePlayerDutyOptionsExtensions
+{
+    public static ExpressionSyntax ToExpressionSyntax(this SinglePlayerDutyOptions dutyOptions)
+    {
+        var emptyOptions = new SinglePlayerDutyOptions();
+        return ObjectCreationExpression(
+                IdentifierName(nameof(SinglePlayerDutyOptions)))
+            .WithInitializer(
+                InitializerExpression(
+                    SyntaxKind.ObjectInitializerExpression,
+                    SeparatedList<ExpressionSyntax>(
+                        SyntaxNodeList(
+                            Assignment(nameof(SinglePlayerDutyOptions.Enabled),
+                                    dutyOptions.Enabled, emptyOptions.Enabled)
+                                .AsSyntaxNodeOrToken(),
+                            Assignment(nameof(SinglePlayerDutyOptions.Notes),
+                                    dutyOptions.Notes, emptyOptions.Notes)
+                                .AsSyntaxNodeOrToken(),
+                            Assignment(nameof(SinglePlayerDutyOptions.Index),
+                                    dutyOptions.Index, emptyOptions.Index)
+                                .AsSyntaxNodeOrToken()))));
+    }
+}
index 2c1df09fbc2db5a6a3660df524a394fd33d4be39..70cae35dbca0f23ce4c2872bfb1df5ef70d9bb58 100644 (file)
@@ -62,6 +62,7 @@ public static class RoslynShortcuts
                 ComplexCombatData complexCombatData => complexCombatData.ToExpressionSyntax(),
                 QuestWorkValue questWorkValue => questWorkValue.ToExpressionSyntax(),
                 List<QuestWorkValue> list => list.ToExpressionSyntax(), // TODO fix in AssignmentList
+                SinglePlayerDutyOptions dutyOptions => dutyOptions.ToExpressionSyntax(),
                 SkipConditions skipConditions => skipConditions.ToExpressionSyntax(),
                 SkipStepConditions skipStepConditions => skipStepConditions.ToExpressionSyntax(),
                 SkipItemConditions skipItemCondition => skipItemCondition.ToExpressionSyntax(),
index f374ebd71b3ad17ea7d395a31b41ee0385defd06..3e3fc39bff9b5028535fe7d95ff8765aa87bebb3 100644 (file)
@@ -57,7 +57,9 @@
           },
           "TerritoryId": 153,
           "InteractionType": "SinglePlayerDuty",
-          "SinglePlayerDutyIndex": 1,
+          "SinglePlayerDutyOptions": {
+            "Index": 1
+          },
           "Fly": true
         }
       ]
index c9af0007bc59e50552eb4d35806c1a78dd4f1a65..bbbf6dedc981636d50a3cbda7b6f02329c32bb05 100644 (file)
@@ -62,7 +62,9 @@
           },\r
           "TerritoryId": 154,\r
           "InteractionType": "SinglePlayerDuty",\r
-          "SinglePlayerDutyIndex": 1,\r
+          "SinglePlayerDutyOptions": {\r
+            "Index": 1\r
+          },\r
           "AetheryteShortcut": "North Shroud - Fallgourd Float",\r
           "Fly": true\r
         }\r
index b588274ccd322dc2b58f544b63ffe06fb916fb9c..0b0d455964fcf88471f4a30ffb847e4e739e3c99 100644 (file)
           },\r
           "TerritoryId": 152,\r
           "InteractionType": "SinglePlayerDuty",\r
-          "SinglePlayerDutyIndex": 1\r
+          "SinglePlayerDutyOptions": {\r
+            "Index": 1\r
+          }\r
         }\r
       ]\r
     },\r
index 8924e6fdb37ef335bf4a83eb4e7d5558ad3f7ab5..f45e4e63b08eb68c07ac303aea3e191e2e0fdc4a 100644 (file)
           },
           "TerritoryId": 141,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
           "Fly": true
         }
       ]
index b8d8505f5a828f89659083942834d27ce0320dc1..507069443968da841081dfae6fb7c368e7b89fe8 100644 (file)
@@ -92,7 +92,9 @@
           },
           "TerritoryId": 130,
           "InteractionType": "SinglePlayerDuty",
-          "SinglePlayerDutyIndex": 1,
+          "SinglePlayerDutyOptions": {
+            "Index": 1
+          },
           "AetheryteShortcut": "Ul'dah"
         }
       ]
index 08758166bcf4999393435e08762fa53a93177283..6abfe26897f0db073ea930c3d3b3bb26b35b7637 100644 (file)
@@ -35,6 +35,9 @@
           },
           "TerritoryId": 137,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
           "AetheryteShortcut": "Eastern La Noscea - Wineport",
           "Fly": true
         }
index 0dfda14549e6fa7bcc608234ea15ce75fc0158e9..b54256d6df872bed81b3afe394a1631561d39f6f 100644 (file)
           },
           "TerritoryId": 152,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
           "Fly": true
         }
       ]
index a176c3b22fe824b9561e7792f3196d4c06110956..c110a643e58d92cc236a5c7a1061693955134723 100644 (file)
@@ -65,7 +65,8 @@
           "AetheryteShortcut": "East Shroud - Hawthorne Hut",
           "SkipConditions": {
             "AetheryteShortcutIf": {
-              "InSameTerritory": true
+              "InSameTerritory": true,
+              "AetheryteLocked": "East Shroud - Hawthorne Hut"
             }
           }
         }
             "Z": 35.568726
           },
           "TerritoryId": 152,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index cc3a749159ff201132b536be3348b93d07636843..aebc91387897d6c968a583bb524a886f96041ab8 100644 (file)
     {\r
       "Sequence": 1,\r
       "Steps": [\r
+        {\r
+          "DataId": 1001263,\r
+          "Position": {\r
+            "X": 181.41443,\r
+            "Y": -2.3519497,\r
+            "Z": -240.40594\r
+          },\r
+          "TerritoryId": 133,\r
+          "InteractionType": "Interact",\r
+          "TargetTerritoryId": 152,\r
+          "AethernetShortcut": [\r
+            "[Gridania] Conjurers' Guild",\r
+            "[Gridania] Lancers' Guild"\r
+          ],\r
+          "SkipConditions": {\r
+            "StepIf": {\r
+              "AetheryteUnlocked": "East Shroud - Hawthorne Hut",\r
+              "InTerritory": [\r
+                152\r
+              ]\r
+            }\r
+          }\r
+        },\r
+        {\r
+          "TerritoryId": 152,\r
+          "InteractionType": "AttuneAetheryte",\r
+          "Aetheryte": "East Shroud - Hawthorne Hut",\r
+          "SkipConditions": {\r
+            "StepIf": {\r
+              "AetheryteUnlocked": "East Shroud - Hawthorne Hut"\r
+            }\r
+          }\r
+        },\r
         {\r
           "Position": {\r
             "X": -276.804,\r
           "TerritoryId": 152,\r
           "InteractionType": "WalkTo",\r
           "AetheryteShortcut": "East Shroud - Hawthorne Hut",\r
-          "Fly": true\r
+          "Fly": true,\r
+          "SkipConditions": {\r
+            "AetheryteShortcutIf": {\r
+              "AetheryteLocked": "East Shroud - Hawthorne Hut"\r
+            }\r
+          }\r
         },\r
         {\r
           "DataId": 2000889,\r
           },\r
           "TerritoryId": 152,\r
           "InteractionType": "SinglePlayerDuty",\r
+          "SinglePlayerDutyOptions": {\r
+            "Enabled": true\r
+          },\r
           "Fly": true\r
         }\r
       ]\r
index cdb594ab56b88ae3f74ced4c18a235f1083b7f8e..3bc3081b6a55eba839649d94c5a3a37488eb7c46 100644 (file)
             "Z": 192.2179
           },
           "TerritoryId": 148,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 9bd7daf81e4da0d7997c562ea94126aec4b84a4e..981c73c5edd31d6260632057038ec4b9ac51739b 100644 (file)
             "Z": 295.52136
           },
           "TerritoryId": 148,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true,
+            "Notes": [
+              "Healer NPC is only killed after the boss dies; all NPCs need to be killed for the duty to complete"
+            ]
+          }
         }
       ]
     },
index bb4aa6554a4be23ff214b28c854ccf702dad01ec..769e2f5f7d5dbb30d590a260276a3cb0fd281dea 100644 (file)
           },
           "TerritoryId": 148,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": false,
-          "BossModNotes": [
-            "AI doesn't automatically target newly spawning adds and dies until after the boss died (tested on CNJ)"
-          ]
+          "SinglePlayerDutyOptions": {
+            "Enabled": false,
+            "Notes": [
+              "AI doesn't automatically target newly spawning adds until after the boss died, and dies (tested on CNJ)"
+            ]
+          }
         }
       ]
     },
index aa50f006f6b0597cc5bfe6858dbd9613d496a3c3..d8b6848a2ee0d56822480dea209396243f6708dd 100644 (file)
           },
           "TerritoryId": 148,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true,
+            "Notes": [
+              "(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete"
+            ]
+          },
           "AetheryteShortcut": "Central Shroud - Bentbranch Meadows"
         }
       ]
index 05886053a4791c7b924aaccb22a031dbb64d7d9f..236b0f94bd509729c77fbae8b22e847507a1697d 100644 (file)
           },
           "TerritoryId": 135,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": false,
+            "Notes": [
+              "(Phase 1, second enemy group) Stuck with enemy being out of sight -- but still able to attack you (tested on ACN)"
+            ]
+          },
           "AetheryteShortcut": "Lower La Noscea - Moraby Drydocks"
         }
       ]
index 72ba9d7a0461e38a37ed1c4748fc914274bc0237..ee2f686b97d00242b10f3212a521097c4c4b80e0 100644 (file)
           "TerritoryId": 134,
           "InteractionType": "Combat",
           "EnemySpawnType": "AutoOnEnterArea",
-          "KillEnemyDataIds": [
-            52
+          "ComplexCombatData": [
+            {
+              "DataId": 52,
+              "IgnoreQuestMarker": true
+            }
           ]
         },
         {
index 62476d4e70ebc75d76405ef96be62edaaf5b39ea..54199db01230b79b7a880a279f8bbaa62c252285 100644 (file)
             "Z": -432.15082
           },
           "TerritoryId": 134,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 8e6aad4066bafb03cc1a7c907fbda2f51365dbbf..901d407825d0a6b45df1b5884212ba893385396b 100644 (file)
             "Z": -141.7716
           },
           "TerritoryId": 134,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": false,
+            "Notes": [
+              "AI doesn't automatically target newly spawning adds until after the boss died (requires healing luck on ACN)"
+            ]
+          }
         }
       ]
     },
index e812ae0f18998b80227a37fb9baec5347f1c8ce2..6ffa3d5884e0fdb3ef4d5fb68be1d832387afb00 100644 (file)
           },
           "TerritoryId": 138,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true,
+            "Notes": [
+              "(Phase 1) Kills PGL NPCs and then the boss - allied NPCs will kill most other NPCs eventually; all NPCs need to be killed for the duty to complete"
+            ]
+          },
           "AetheryteShortcut": "Western La Noscea - Swiftperch"
         }
       ]
index cab6933d01e60f5c06c371fc9aa9767d54a23daa..c236b60bb280e493df9d5e8d30e732af3c3efa01 100644 (file)
             "Z": -242.51166
           },
           "TerritoryId": 145,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index e5c21738f348e47869fd4cafcb19909018b719d7..4141d32004b77d3e8c9925a289c802fe2939945d 100644 (file)
@@ -79,6 +79,9 @@
           },
           "TerritoryId": 130,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
           "AetheryteShortcut": "Ul'dah",
           "AethernetShortcut": [
             "[Ul'dah] Aetheryte Plaza",
@@ -87,6 +90,9 @@
         }
       ]
     },
+    {
+      "Sequence": 5
+    },
     {
       "Sequence": 255,
       "Steps": [
index 4874da87d9e74521769b4e142cf8cfecbf606505..310b20cc87afa29cfb5e615faaad54c4ca3ecc46 100644 (file)
           "AethernetShortcut": [
             "[Gridania] Aetheryte Plaza",
             "[Gridania] Lancers' Guild"
-          ]
+          ],
+          "SkipConditions": {
+            "StepIf": {
+              "AetheryteUnlocked": "East Shroud - Hawthorne Hut"
+            }
+          }
         },
         {
           "TerritoryId": 152,
           "InteractionType": "AttuneAetheryte",
-          "Aetheryte": "East Shroud - Hawthorne Hut"
+          "Aetheryte": "East Shroud - Hawthorne Hut",
+          "SkipConditions": {
+            "StepIf": {
+              "AetheryteUnlocked": "East Shroud - Hawthorne Hut"
+            }
+          }
         },
         {
           "DataId": 1004886,
             "Z": 475.30322
           },
           "TerritoryId": 152,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
+          "AetheryteShortcut": "East Shroud - Hawthorne Hut",
+          "SkipConditions": {
+            "AetheryteShortcutIf": {
+              "InSameTerritory": true
+            }
+          }
         }
       ]
     },
index 4d2c5e406d58c479a50143058d452006ecec73d5..dae94b36b989d517837c43264ac3b95bb3e7fd40 100644 (file)
@@ -64,6 +64,9 @@
           },
           "TerritoryId": 135,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
           "AethernetShortcut": [
             "[Limsa Lominsa] The Aftcastle",
             "[Limsa Lominsa] Tempest Gate (Lower La Noscea)"
index ba6b4a76767bf8c506cf2cf73c611d82b8a44424..9bcf3680e2f17d449d7e53521a111bb180af6a15 100644 (file)
@@ -59,6 +59,9 @@
           },
           "TerritoryId": 140,
           "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          },
           "AetheryteShortcut": "Western Thanalan - Horizon"
         }
       ]
index 0e262818a4ae400c04ce2a986430f8f3e74ffd6b..4c8290b14b57f009d6f88aa8b35b48204aed71ff 100644 (file)
@@ -46,7 +46,8 @@
           },
           "StopDistance": 7,
           "TerritoryId": 141,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "DelaySecondsAtStart": 2
         }
       ]
     },
index 42a2b633ad2ca8621bea6533d1ff7b914d698ab8..a61bf9782d2d5a7050c0891bcb4396a06939c1c4 100644 (file)
             "Z": 117.29602
           },
           "TerritoryId": 141,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 7c6a109b0c28b070306c0a39f4e4a22ee442b1da..0074bc2bc7c997941ca841597b81d6713aed79b8 100644 (file)
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "Position": {
+            "X": -174.73444,
+            "Y": 15.450659,
+            "Z": -266.76144
+          },
+          "TerritoryId": 140,
+          "InteractionType": "WalkTo"
+        },
         {
           "Position": {
             "X": -289.1099,
index b35b8ea3292c21915f2b5bbd0ec272c6956f5d77..0b89ddcc76db1f7fe38c188fd5f2d2ec71d57e52 100644 (file)
             "Z": -293.1411
           },
           "TerritoryId": 140,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 1430561727adce48a3f1ecebd9878fed4dae4560..6cc4dc5b0ccecdf6e8f728c7475480b114db1847 100644 (file)
@@ -29,7 +29,7 @@
           },
           "TerritoryId": 141,
           "InteractionType": "Combat",
-          "EnemySpawnType": "OverworldEnemies",
+          "EnemySpawnType": "FinishCombatIfAny",
           "KillEnemyDataIds": [
             352,
             353
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "Position": {
+            "X": 131.78122,
+            "Y": 20.119337,
+            "Z": -115.27284
+          },
+          "TerritoryId": 141,
+          "InteractionType": "WalkTo"
+        },
+        {
+          "Position": {
+            "X": 127.7017,
+            "Y": -0.15994573,
+            "Z": -161.89238
+          },
+          "TerritoryId": 141,
+          "InteractionType": "WalkTo",
+          "DisableNavmesh": true
+        },
         {
           "DataId": 1001605,
           "Position": {
index c12bda58a08a7628eb93d8291ec32c11462bcada..c211fef2b427bf0478ee04e680a9c9b5b8ae9484 100644 (file)
             "Z": 536.88855
           },
           "TerritoryId": 141,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index c16c22b97d97df2cc77f8e6a87dfc47e64d2c4d2..841ded1882aa23436a8b28dd3424fbbd1599facc 100644 (file)
             "Z": -131.48706
           },
           "TerritoryId": 141,
-          "InteractionType": "Interact",
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true,
+            "Notes": [
+              "(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete"
+            ]
+          },
           "AetheryteShortcut": "Central Thanalan - Black Brush Station"
         }
       ]
index a7fddd2e902ef3b9a23f12fb10aa8c2782644cc1..b0e58b23b13eb60e5b34c52f4bdeffb168f0ecf8 100644 (file)
           },
           "TerritoryId": 133,
           "InteractionType": "Interact",
-          "TargetTerritoryId": 152
+          "TargetTerritoryId": 152,
+          "SkipConditions": {
+            "StepIf": {
+              "AetheryteUnlocked": "East Shroud - Hawthorne Hut"
+            }
+          }
         },
         {
           "TerritoryId": 152,
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "East Shroud - Hawthorne Hut",
-          "StopDistance": 5
+          "StopDistance": 5,
+          "SkipConditions": {
+            "StepIf": {
+              "AetheryteUnlocked": "East Shroud - Hawthorne Hut"
+            }
+          }
         },
         {
           "DataId": 1006188,
             "Z": 283.4973
           },
           "TerritoryId": 152,
-          "InteractionType": "CompleteQuest"
+          "InteractionType": "CompleteQuest",
+          "AetheryteShortcut": "East Shroud - Hawthorne Hut",
+          "SkipConditions": {
+            "AetheryteShortcutIf": {
+              "InSameTerritory": true
+            }
+          }
         }
       ]
     }
index 2b4fbd24c87b7a1067e9a9864a9812d4cb0d93b1..0a54ca732543fa6e50f85c91611e3f41e311ad81 100644 (file)
             "Z": -39.383606
           },
           "TerritoryId": 152,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index dfda0e548fac6e8e75cecb7caba995eeef6b4295..3b4137858d1fec85135ecfaacb6434c68c70bf58 100644 (file)
             "Z": -12.985474
           },
           "TerritoryId": 153,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "SinglePlayerDutyOptions": {
+            "Enabled": true,
+            "Notes": [
+              "AI will kill initial adds before the boss, but not switch target whenever new enemies spawn; all NPCs need to be killed for the duty to complete"
+            ]
+          }
         }
       ]
     },
index cd396c519cbab433d7ade17ffdefbc0a36a7ea19..8d6f72e7c8aef63a9a0d69585b59af6fcf688b7f 100644 (file)
           },
           "TerritoryId": 1053,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": false,
-          "BossModNotes": [
-            "Doesn't handle death properly"
-          ]
+          "SinglePlayerDutyOptions": {
+            "Enabled": false,
+            "Notes": [
+              "Doesn't handle death properly"
+            ]
+          }
         }
       ]
     },
index f0598d7b43820bbc85eb608f88e3e119ad6c6e94..b4f80942ed2fcef0623de3eb6436aa1d8ea8c48b 100644 (file)
@@ -96,7 +96,9 @@
           "TerritoryId": 138,
           "InteractionType": "SinglePlayerDuty",
           "Fly": true,
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 10c0755f4e2e11f842b6c34ad4f1c83fe7f802c5..a72b233402d9bdb6d6b3e50d8b75d5eb4c8e7588 100644 (file)
@@ -59,7 +59,9 @@
           },
           "TerritoryId": 401,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index e3a1d105dac4f17537948c9eb224775de764ddb4..d1e8cd475b6bdc392a2c6c767140c1dbcc1b92cc 100644 (file)
@@ -79,7 +79,9 @@
             "[Ishgard] The Forgotten Knight",
             "[Ishgard] The Tribunal"
           ],
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 6e052e8b4264a71449bd94726eafd61912e2b7f4..92b63e22e8408295e9df999198e721589b3b5ded 100644 (file)
@@ -29,7 +29,9 @@
           },
           "TerritoryId": 145,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 3503cfee9fd70a82e50d1df08a55493a3af67bd8..c7fd5ede39696f29b8d905fae131972f526ab6d4 100644 (file)
@@ -79,7 +79,9 @@
           "TerritoryId": 397,
           "InteractionType": "SinglePlayerDuty",
           "DisableNavmesh": true,
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index eb9876a68589f150dd45436bc8be8fd7c51fddbe..15f79ef812162a0053658b0f879a5048f3a86b0b 100644 (file)
@@ -75,7 +75,9 @@
           },
           "TerritoryId": 418,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index b156029fd6d29dd8e5e46b0c924f330b276a701f..30809e01aa86a22277bcd53a9d61a719ed5cd465 100644 (file)
@@ -57,7 +57,9 @@
           "InteractionType": "SinglePlayerDuty",
           "Emote": "lookout",
           "StopDistance": 0.25,
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 31218140f582c0b002b1ac59c200295a31b194e6..0b5e5d49ae3424f0bed9355a1bb0ea6138e6a1c7 100644 (file)
@@ -48,7 +48,9 @@
             "[Idyllshire] Aetheryte Plaza",
             "[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
           ],
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index a498f657c684b157e0fc84af3ef9167e54eb6e80..18809a5e509f7499d960032d57da8cc8e48d83a9 100644 (file)
@@ -69,7 +69,9 @@
           },
           "TerritoryId": 402,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": true
+          "SinglePlayerDutyOptions": {
+            "Enabled": true
+          }
         }
       ]
     },
index 51863a11f26344406887e477d51081edeb60bff9..2465f30c9ed0f888f70bb5a93731eaa6cf713650 100644 (file)
           "TerritoryId": 351,
           "InteractionType": "SinglePlayerDuty",
           "Comment": "Estinien vs. Arch Ultima",
-          "BossModEnabled": false,
-          "BossModNotes": [
-            "AI doesn't move automatically for the first boss",
-            "AI doesn't move automatically for the dialogue with gaius on the bridge",
-            "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)",
-            "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking"
-          ],
-          "$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one"
+          "SinglePlayerDutyOptions": {
+            "Enabled": false,
+            "Notes": [
+              "AI doesn't move automatically for the first boss",
+              "AI doesn't move automatically for the dialogue with gaius on the bridge",
+              "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)",
+              "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking"
+            ]
+          },
+          "$": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one"
         }
       ]
     },
index 49a098916eecc5e4ca18d09a45d2ea3eeba2432d..4fab7756d497ed44ded25af77cf229f8e1a4a3c0 100644 (file)
           },
           "TerritoryId": 817,
           "InteractionType": "SinglePlayerDuty",
-          "BossModEnabled": false,
-          "BossModNotes": [
-            "Doesn't walk to the teleporter to finish the duty"
-          ],
+          "SinglePlayerDutyOptions": {
+            "Enabled": false,
+            "Notes": [
+              "Doesn't walk to the teleporter to finish the duty"
+            ]
+          },
           "Fly": true,
           "Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)",
           "$": "The dialogue choices and data ids here are recycled",
index 160a2970dbc43bc24f50f410302320bc2f6b6207..206d7457bf05220746c6dd9af071166aac342dcd 100644 (file)
           "StopDistance": 5,
           "TerritoryId": 829,
           "InteractionType": "SinglePlayerDuty",
-          "SinglePlayerDutyIndex": 1,
+          "SinglePlayerDutyOptions": {
+            "Index": 1
+          },
           "DialogueChoices": [
             {
               "Type": "List",
index f5df18c8a15ba6ff5bd72471798c8a52711d1719..40204b393bd7bda0644bcf9c78c9d1f8dce077a0 100644 (file)
           },
           "then": {
             "properties": {
-              "BossModEnabled": {
-                "type": "boolean"
-              },
-              "BossModNotes": {
-                "type": "array",
-                "items": {
-                  "type": "string"
-                }
-              },
-              "SinglePlayerDutyIndex": {
-                "type": "integer",
-                "minimum": 0,
-                "maximum": 1,
-                "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is"
+              "SinglePlayerDutyOptions": {
+                "type": "object",
+                "properties": {
+                  "Enabled": {
+                    "type": "boolean"
+                  },
+                  "Notes": {
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    }
+                  },
+                  "Index": {
+                    "type": "integer",
+                    "minimum": 0,
+                    "maximum": 1,
+                    "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is"
+                  },
+                  "$": {
+                    "type": "string"
+                  }
+                },
+                "TODO_required": [
+                  "Enabled"
+                ],
+                "additionalProperties": false
               }
             }
           }
index 8d9d31ecb5e82df4172053806b0802f63049efae..b96a30a20f9b43179483c7764f8ca9a2a20ea788 100644 (file)
@@ -75,9 +75,8 @@ public sealed class QuestStep
     public JumpDestination? JumpDestination { get; set; }
     public uint? ContentFinderConditionId { get; set; }
     public bool AutoDutyEnabled { get; set; }
-    public bool BossModEnabled { get; set; }
-    public List<string> BossModNotes { get; set; } = [];
-    public byte SinglePlayerDutyIndex { get; set; }
+    public SinglePlayerDutyOptions? SinglePlayerDutyOptions { get; set; }
+    public byte SinglePlayerDutyIndex => SinglePlayerDutyOptions?.Index ?? 0;
     public SkipConditions? SkipConditions { get; set; }
 
     public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
diff --git a/Questionable.Model/Questing/SinglePlayerDutyOptions.cs b/Questionable.Model/Questing/SinglePlayerDutyOptions.cs
new file mode 100644 (file)
index 0000000..874fc5c
--- /dev/null
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Questionable.Model.Questing;
+
+public sealed class SinglePlayerDutyOptions
+{
+    public bool Enabled { get; set; }
+    public List<string> Notes { get; set; } = [];
+    public byte Index { get; set; }
+}
index 1825b4f793f69d79a6803efa44096e5d4dff2d4c..c4c70b982fd97bc55fb05b4c92257a82f8c48b71 100644 (file)
@@ -691,7 +691,7 @@ internal sealed class InteractionUiController : IDisposable
     private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step)
     {
         if (step is { InteractionType: EInteractionType.SinglePlayerDuty } &&
-            _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyIndex, step.BossModEnabled))
+            _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyOptions))
         {
             // Most of these are yes/no dialogs "Duty calls, ...".
             //
index b2d146f0018122d169e0de9c4b0c9c0b7f1ab01b..97089936ce9ae73e7f65644e824b122982758284 100644 (file)
@@ -27,7 +27,7 @@ internal static class SendNotification
                     new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
                         ? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name
                         : step.Comment),
-                EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled) =>
+                EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions) =>
                     new Task(step.InteractionType, quest.Info.Name),
                 _ => null,
             };
index b8bf39fe621ecf25625d0e9dc34efa969569324f..188729be4d1a9805cf6ce5138f02895661b37290 100644 (file)
@@ -21,7 +21,7 @@ internal static class SinglePlayerDuty
             if (step.InteractionType != EInteractionType.SinglePlayerDuty)
                 yield break;
 
-            if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled))
+            if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
             {
                 if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out var cfcData))
                     throw new TaskException("Failed to get content finder condition for solo instance");
index 1476ed3c4a66141ca8acef22a10d58d7f5606966..86e5d47d4c4146dcb4dc881ed3cd6ea7604d8a64 100644 (file)
@@ -54,7 +54,7 @@ internal static class WaitAtEnd
                     return [new WaitNextStepOrSequence()];
 
                 case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
-                case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled):
+                case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions):
                     return [new EndAutomation()];
 
                 case EInteractionType.WalkTo:
index e73e1863dc21540b238700d75018aa8591020fe4..d157622eb081152688d9ec44e9a2f78724e5c6cc 100644 (file)
@@ -82,7 +82,7 @@ internal sealed class BossModIpc
         ClearPreset();
     }
 
-    public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
+    public bool IsConfiguredToRunSoloInstance(ElementId questId, SinglePlayerDutyOptions? dutyOptions)
     {
         if (!IsSupported())
             return false;
@@ -90,7 +90,8 @@ internal sealed class BossModIpc
         if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
             return false;
 
-        if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData))
+        dutyOptions ??= new();
+        if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyOptions.Index, out var cfcData))
             return false;
 
         if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
@@ -99,6 +100,6 @@ internal sealed class BossModIpc
         if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
             return true;
 
-        return enabledByDefault;
+        return dutyOptions.Enabled;
     }
 }
index 07149debaba85cd1362d27de1ae7d39220267089..c49a2c814fb1f8ded4335c03825bc5aec67e541e 100644 (file)
@@ -122,70 +122,24 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         foreach (var (questId, index, cfcData) in _territoryData.GetAllQuestsWithQuestBattles())
         {
             IQuestInfo questInfo = _questData.GetQuestInfo(questId);
-            QuestStep questStep = new QuestStep
-            {
-                SinglePlayerDutyIndex = 0,
-                BossModEnabled = false,
-            };
-            bool enabled;
-            if (_questRegistry.TryGetQuest(questId, out var quest))
-            {
-                if (quest.Root.Disabled)
-                {
-                    _logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId);
-                    enabled = false;
-                }
-                else
-                {
-                    var foundStep = quest.AllSteps().FirstOrDefault(x =>
-                        x.Step.InteractionType == EInteractionType.SinglePlayerDuty &&
-                        x.Step.SinglePlayerDutyIndex == index);
-                    if (foundStep == default)
-                    {
-                        _logger.LogWarning(
-                            "Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId,
-                            index);
-                        enabled = false;
-                    }
-                    else
-                    {
-                        questStep = foundStep.Step;
-                        enabled = true;
-                    }
-                }
-            }
-            else
-            {
-                _logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId);
-                enabled = false;
-            }
+            (bool enabled, SinglePlayerDutyOptions options) = FindDutyOptions(questId, index);
 
             string name = $"{FormatLevel(questInfo.Level)} {questInfo.Name}";
             if (!string.IsNullOrEmpty(cfcData.Name) && !questInfo.Name.EndsWith(cfcData.Name, StringComparison.Ordinal))
                 name += $" ({cfcData.Name})";
 
             if (questsWithMultipleBattles.Contains(questId))
-                name += $" (Part {questStep.SinglePlayerDutyIndex + 1})";
+                name += $" (Part {options.Index + 1})";
             else if (cfcData.ContentFinderConditionId is 674 or 691)
                 name += " (Melee/Phys. Ranged)";
 
-            var dutyInfo = new SinglePlayerDutyInfo(
-                cfcData.ContentFinderConditionId,
-                cfcData.TerritoryId,
-                name,
-                questInfo.Expansion,
-                questInfo.JournalGenre ?? uint.MaxValue,
-                questInfo.SortKey,
-                questStep.SinglePlayerDutyIndex,
-                enabled,
-                questStep.BossModEnabled,
-                questStep.BossModNotes);
-
-            if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334)
+            var dutyInfo = new SinglePlayerDutyInfo(name, questInfo, cfcData, options, enabled);
+
+            if (dutyInfo.IsLimsaStart)
                 startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo);
-            else if (cfcData.ContentFinderConditionId is 296 or 297 or 299 or 298)
+            else if (dutyInfo.IsGridaniaStart)
                 startingCityBattles[EAetheryteLocation.Gridania].Add(dutyInfo);
-            else if (cfcData.ContentFinderConditionId is 335 or 312 or 337 or 336)
+            else if (dutyInfo.IsUldahStart)
                 startingCityBattles[EAetheryteLocation.Uldah].Add(dutyInfo);
             else if (questInfo.IsMainScenarioQuest)
                 mainScenarioBattles.Add(dutyInfo);
@@ -196,7 +150,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
                 foreach (var roleClassJob in classJobs)
                     roleQuestBattles[roleClassJob].Add(dutyInfo);
             }
-            else if (dutyInfo.CfcId is 845 or 1016)
+            else if (dutyInfo.IsOtherRoleQuest)
                 otherRoleQuestBattles.Add(dutyInfo);
             else
                 otherBattles.Add(dutyInfo);
@@ -220,7 +174,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
                 x =>
                     x.Value
                         // level 10 quests use the same quest battle for [you started as this class] and [you picked this class up later]
-                        .DistinctBy(y => y.CfcId)
+                        .DistinctBy(y => y.ContentFinderConditionId)
                         .OrderBy(y => y.JournalGenreId)
                         .ThenBy(y => y.SortKey)
                         .ThenBy(y => y.Index)
@@ -242,6 +196,47 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
             .ToImmutableList();
     }
 
+    private (bool Enabled, SinglePlayerDutyOptions Options) FindDutyOptions(ElementId questId, byte index)
+    {
+        SinglePlayerDutyOptions options = new()
+        {
+            Index = 0,
+            Enabled = false,
+        };
+        if (_questRegistry.TryGetQuest(questId, out var quest))
+        {
+            if (quest.Root.Disabled)
+            {
+                _logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId);
+                return (false, options);
+            }
+            else
+            {
+                var foundStep = quest.AllSteps()
+                    .Select(x => x.Step)
+                    .FirstOrDefault(x =>
+                        x.InteractionType == EInteractionType.SinglePlayerDuty &&
+                        x.SinglePlayerDutyIndex == index);
+                if (foundStep == null)
+                {
+                    _logger.LogWarning(
+                        "Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId,
+                        index);
+                    return (false, options);
+                }
+                else
+                {
+                    return (true, foundStep.SinglePlayerDutyOptions ?? options);
+                }
+            }
+        }
+        else
+        {
+            _logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId);
+            return (false, options);
+        }
+    }
+
     private string BuildJournalGenreLabel(uint journalGenreId)
     {
         var journalGenre = _dataManager.GetExcelSheet<JournalGenre>().GetRow(journalGenreId);
@@ -250,7 +245,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         string genreName = journalGenre.Name.ExtractText();
         string categoryName = journalCategory.Name.ExtractText();
 
-        return $"{categoryName} {SeIconChar.ArrowRight.ToIconString()} {genreName}";
+        return $"{categoryName} \u203B {genreName}";
     }
 
     public override void DrawTab()
@@ -423,13 +418,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
             {
                 ImGui.TableNextRow();
 
-                string[] labels = dutyInfo.BossModEnabledByDefault
+                string[] labels = dutyInfo.EnabledByDefault
                     ? SupportedCfcOptions
                     : UnsupportedCfcOptions;
                 int value = 0;
-                if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
+                if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.ContentFinderConditionId))
                     value = 1;
-                if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
+                if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.ContentFinderConditionId))
                     value = 2;
 
                 if (ImGui.TableNextColumn())
@@ -445,7 +440,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
                             ImGui.TextUnformatted(dutyInfo.Name);
                             ImGui.Separator();
                             ImGui.BulletText($"TerritoryId: {dutyInfo.TerritoryId}");
-                            ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.CfcId}");
+                            ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.ContentFinderConditionId}");
                         }
                     }
 
@@ -457,11 +452,18 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
                     else if (dutyInfo.Notes.Count > 0)
                     {
                         using var color = new ImRaii.Color();
-                        color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow);
+                        if (!dutyInfo.EnabledByDefault)
+                            color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow);
+                        else
+                            color.Push(ImGuiCol.TextDisabled, ImGuiColors.ParsedBlue);
+
                         ImGui.SameLine();
                         using (ImRaii.PushFont(UiBuilder.IconFont))
                         {
-                            ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString());
+                            if (!dutyInfo.EnabledByDefault)
+                                ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString());
+                            else
+                                ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString());
                         }
 
                         if (ImGui.IsItemHovered())
@@ -478,19 +480,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
 
                 if (ImGui.TableNextColumn())
                 {
-                    using var _ = ImRaii.PushId($"##Duty{dutyInfo.CfcId}");
+                    using var _ = ImRaii.PushId($"##Duty{dutyInfo.ContentFinderConditionId}");
                     using (ImRaii.Disabled(!dutyInfo.Enabled))
                     {
                         ImGui.SetNextItemWidth(200);
                         if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
                         {
-                            Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
-                            Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
+                            Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.ContentFinderConditionId);
+                            Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.ContentFinderConditionId);
 
                             if (value == 1)
-                                Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
+                                Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.ContentFinderConditionId);
                             else if (value == 2)
-                                Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
+                                Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.ContentFinderConditionId);
 
                             Save();
                         }
@@ -519,14 +521,28 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
     }
 
     private sealed record SinglePlayerDutyInfo(
-        uint CfcId,
-        uint TerritoryId,
         string Name,
-        EExpansionVersion Expansion,
-        uint JournalGenreId,
-        ushort SortKey,
-        byte Index,
-        bool Enabled,
-        bool BossModEnabledByDefault,
-        List<string> Notes);
+        IQuestInfo QuestInfo,
+        TerritoryData.ContentFinderConditionData ContentFinderConditionData,
+        SinglePlayerDutyOptions Options,
+        bool Enabled)
+    {
+        public EExpansionVersion Expansion => QuestInfo.Expansion;
+        public uint JournalGenreId => QuestInfo.JournalGenre ?? uint.MaxValue;
+        public ushort SortKey => QuestInfo.SortKey;
+        public uint ContentFinderConditionId => ContentFinderConditionData.ContentFinderConditionId;
+        public uint TerritoryId => ContentFinderConditionData.TerritoryId;
+        public byte Index => Options.Index;
+        public bool EnabledByDefault => Options.Enabled;
+        public IReadOnlyList<string> Notes => Options.Notes;
+
+        public bool IsLimsaStart => ContentFinderConditionId is 332 or 333 or 313 or 334;
+        public bool IsGridaniaStart => ContentFinderConditionId is 296 or 297 or 299 or 298;
+        public bool IsUldahStart => ContentFinderConditionId is 335 or 312 or 337 or 336;
+
+        /// <summary>
+        /// 'Other' role quest is the post-EW/DT role quests.
+        /// </summary>
+        public bool IsOtherRoleQuest => ContentFinderConditionId is 845 or 1016;
+    }
 }