Add AutoDuty integration
authorLiza Carvelli <liza@carvel.li>
Mon, 30 Dec 2024 01:50:18 +0000 (02:50 +0100)
committerLiza Carvelli <liza@carvel.li>
Mon, 30 Dec 2024 01:50:18 +0000 (02:50 +0100)
65 files changed:
QuestPathGenerator/RoslynElements/QuestStepExtensions.cs
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/245_It's Probably Pirates.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/677_Fire in the Gloom.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/514_Into the Beast's Maw.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/A9-Haukke Manor/801_Skeletons in Her Closet.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/B2-Eastern La Noscea, Brayflox, Cheese and Wine/832_The Things We Do for Cheese.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/B4-Titan/857_Lord of Crags.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/C1-Coerthas Central Highlands, The Enterprise/952_In Pursuit of the Past.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/C3-Garuda/519_Lady of the Vortex.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/3873_Rock the Castrum.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/75_The Path of the Righteous.json
QuestPaths/2.x - A Realm Reborn/MSQ-2/E5-2.5/366_The Rising Chorus.json
QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1131_Gilding the Bilious (Maelstrom).json
QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1132_Gilding the Bilious (Twin Adder).json
QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1133_Gilding the Bilious (Immortal Flames).json
QuestPaths/3.x - Heavensward/Aether Currents/Coerthas Western Highlands/2111_For All the Nights to Come.json
QuestPaths/3.x - Heavensward/MSQ/A3.2-The Dravanian Forelands/1617_Mourn in Passing.json
QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1634_Into the Aery.json
QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1640_A Knight's Calling.json
QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1660_Forbidden Knowledge.json
QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1669_Heavensward.json
QuestPaths/3.x - Heavensward/MSQ/C-3.2/2232_The Word of the Mother.json
QuestPaths/3.x - Heavensward/MSQ/E-3.4/2342_Shadows of the First.json
QuestPaths/3.x - Heavensward/MSQ/F-3.5/2354_Griffin, Griffin on the Wall.json
QuestPaths/4.x - Stormblood/MSQ/A1.3-Rhalgr's Reach 2/2469_Not without Incident.json
QuestPaths/4.x - Stormblood/MSQ/A5-Yanxia 2/2524_The Die Is Cast.json
QuestPaths/4.x - Stormblood/MSQ/A6.2-Peaks 2/2544_The Price of Freedom.json
QuestPaths/4.x - Stormblood/MSQ/B-4.1/2964_The Mad King's Trove.json
QuestPaths/4.x - Stormblood/MSQ/E-4.4/3144_Feel the Burn.json
QuestPaths/4.x - Stormblood/MSQ/F-4.5/3183_The Face of War.json
QuestPaths/5.x - Shadowbringers/MSQ/A4-Crystarium 2/3300_The Lightwardens.json
QuestPaths/5.x - Shadowbringers/MSQ/B-Il Mheg/3312_The Key to the Castle.json
QuestPaths/5.x - Shadowbringers/MSQ/C-Rak'tika/3340_The Burden of Knowledge.json
QuestPaths/5.x - Shadowbringers/MSQ/E-Kholusia 2/3643_Extinguishing the Last Light.json
QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json
QuestPaths/6.x - Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json
QuestPaths/6.x - Endwalker/MSQ/D-Thavnair2/4409_Skies Aflame.json
QuestPaths/6.x - Endwalker/MSQ/E-Elpis/4437_Caging the Messenger.json
QuestPaths/6.x - Endwalker/MSQ/F-Labyrinthos2/4449_Her Children One and All.json
QuestPaths/6.x - Endwalker/MSQ/H-6.1/4529_Alzadaals Legacy.json
QuestPaths/6.x - Endwalker/MSQ/I-6.2/4592_In Search of Azdaja.json
QuestPaths/6.x - Endwalker/MSQ/J-6.3/4674_King of the Mountain.json
QuestPaths/6.x - Endwalker/MSQ/K-6.4/4736_Going Haam.json
QuestPaths/6.x - Endwalker/MSQ/L-6.5/4748_Down in the Dark.json
QuestPaths/7.x - Dawntrail/MSQ/A-Kozama'uka1-Urqopacha1/4879_For All Turali.json
QuestPaths/7.x - Dawntrail/MSQ/B-Kozama'uka2-Urqopacha2/4891_The High Luminary.json
QuestPaths/7.x - Dawntrail/MSQ/C-Yak T'el/4909_Road to the Golden City.json
QuestPaths/7.x - Dawntrail/MSQ/D-Shaaloani-HeritageFound1/4926_All Aboard.json
QuestPaths/7.x - Dawntrail/MSQ/E-SolutionNine-HeritageFound2/4945_The Resilient Son.json
QuestPaths/7.x - Dawntrail/MSQ/F-Living Memory/4959_Dawntrail.json
QuestPaths/7.x - Dawntrail/MSQ/G-7.1/5246_In Search of the Past.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/QuestStep.cs
Questionable/Configuration.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/Common/SendNotification.cs
Questionable/Controller/Steps/Interactions/Duty.cs
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/Data/TerritoryData.cs
Questionable/External/AutoDutyIpc.cs [new file with mode: 0644]
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigWindow.cs

index 12b27ef4eac5c5b82ca11208a6a944d9dafc5594..6b76bb95ee36e7e4d4e50eee6fcf317908fb4909 100644 (file)
@@ -117,6 +117,9 @@ internal static class QuestStepExtensions
                             Assignment(nameof(QuestStep.ContentFinderConditionId),
                                     step.ContentFinderConditionId, emptyStep.ContentFinderConditionId)
                                 .AsSyntaxNodeOrToken(),
+                            Assignment(nameof(QuestStep.AutoDutyEnabled),
+                                    step.AutoDutyEnabled, emptyStep.AutoDutyEnabled)
+                                .AsSyntaxNodeOrToken(),
                             Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
                                     emptyStep.SkipConditions)
                                 .AsSyntaxNodeOrToken(),
index 8a35225400220561dfb52b75d3c127c574e55207..0d3aada9033e69ee2973d5eef1f544926911046a 100644 (file)
         {
           "TerritoryId": 138,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 4
+          "ContentFinderConditionId": 4,
+          "AutoDutyEnabled": true
         }
       ]
     },
index ff980ffa8937f095af338e5c769c2ba4e5581544..cab6933d01e60f5c06c371fc9aa9767d54a23daa 100644 (file)
@@ -71,7 +71,8 @@
         {
           "TerritoryId": 146,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 56
+          "ContentFinderConditionId": 56,
+          "AutoDutyEnabled": true
         }
       ]
     },
index b6f45f31c0926f9db6a8a583ceceebc654f7ab55..e5c21738f348e47869fd4cafcb19909018b719d7 100644 (file)
@@ -62,7 +62,8 @@
         {
           "TerritoryId": 140,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 3
+          "ContentFinderConditionId": 3,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 7340e66a7ccf6a10776e38a7d99281521772766b..726eae29145256e79ec39a0089a6848feb34a506 100644 (file)
@@ -57,7 +57,8 @@
         {
           "TerritoryId": 148,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 2
+          "ContentFinderConditionId": 2,
+          "AutoDutyEnabled": true
         }
       ]
     },
index cf0acff267c4db162c2c9e8ea75d2253ba810988..45b28b3fdc642c47c0fc7bccc1bd4fbb84e1a38e 100644 (file)
@@ -44,7 +44,8 @@
         {
           "TerritoryId": 153,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 1
+          "ContentFinderConditionId": 1,
+          "AutoDutyEnabled": true
         }
       ]
     },
index c3a02a0ca1bd6a1f4ef810b8a9708819ac0b9953..9269a84bd6aa542a243609e29e53d9c9ef1e862b 100644 (file)
@@ -66,7 +66,8 @@
         {
           "TerritoryId": 148,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 6
+          "ContentFinderConditionId": 6,
+          "AutoDutyEnabled": true
         }
       ]
     },
index bcb40f054282be8543382676967608910e942dbf..06402bacb4699825be0cee62f83fd3aa082a239f 100644 (file)
@@ -85,7 +85,8 @@
         {
           "TerritoryId": 137,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 8
+          "ContentFinderConditionId": 8,
+          "AutoDutyEnabled": true
         }
       ]
     },
index e08981891058fb2eb2e2ecbc708a376b05ee9137..4cc4c5768b02d7c83374371a901ab7f58de6722e 100644 (file)
@@ -45,7 +45,8 @@
         {
           "TerritoryId": 139,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 57
+          "ContentFinderConditionId": 57,
+          "AutoDutyEnabled": true
         }
       ]
     },
index ad446eeb16eefbbb14beab9b28916db2a483728a..57ef1fa996b6ea163ecb71dc71e1500b22be5b92 100644 (file)
@@ -59,7 +59,8 @@
         {
           "TerritoryId": 155,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 11
+          "ContentFinderConditionId": 11,
+          "AutoDutyEnabled": true
         }
       ]
     },
index ff50fed79e43d45cc748a45eaa32322e5ca14939..d9315eca218d283fd20745a059d02595815d5923 100644 (file)
@@ -38,7 +38,8 @@
         {
           "TerritoryId": 331,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 58
+          "ContentFinderConditionId": 58,
+          "AutoDutyEnabled": true
         }
       ]
     },
index f7d746fe6fd44c765f209974a0ff62a87593c0b5..7eec220ae3341b5dc438503853fedff29c20df50 100644 (file)
@@ -45,7 +45,8 @@
         {
           "TerritoryId": 147,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 15
+          "ContentFinderConditionId": 15,
+          "AutoDutyEnabled": true
         }
       ]
     },
index c878f58b6ad6df36d5af70e6276c7e92f5794129..1a788c09f37e775c5d527dbe6ddb8976ccec89db 100644 (file)
@@ -46,7 +46,8 @@
         {
           "TerritoryId": 147,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 16
+          "ContentFinderConditionId": 16,
+          "AutoDutyEnabled": true
         }
       ]
     },
@@ -71,7 +72,8 @@
         {
           "TerritoryId": 1053,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 830
+          "ContentFinderConditionId": 830,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 34e9b723c3551f5b1308980037ea50e9ec025bff..b6581b9a845373dc67ab4f00fba88293419d9f93 100644 (file)
@@ -88,7 +88,8 @@
         {
           "TerritoryId": 155,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 27
+          "ContentFinderConditionId": 27,
+          "AutoDutyEnabled": true
         }
       ]
     },
index fc1d72204071fd37e710bad668c672b6cddac760..24d72b21450e123b90b1dc9ea4791494ed16f5fd 100644 (file)
         {
           "TerritoryId": 156,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 32
+          "ContentFinderConditionId": 32,
+          "AutoDutyEnabled": true
         }
       ]
     },
index e0157efee276e0c0047a7c15ca6fa05ab3a55542..165a1839cd8328cf963e3a1a236bedc787794a68 100644 (file)
@@ -71,7 +71,8 @@
         {
           "TerritoryId": 155,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 5
+          "ContentFinderConditionId": 5,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 178a0b5a4ebf103e87290584551816397f640487..4defc6a918fea520b6e3160b69b8ca3e4ab3ba37 100644 (file)
@@ -71,7 +71,8 @@
         {
           "TerritoryId": 155,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 5
+          "ContentFinderConditionId": 5,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 8915d67142fced8efdc5c5d75a97ad74d309e933..cf83d8a4ca6ce59cadda18c15a748b2784cf2bca 100644 (file)
@@ -71,7 +71,8 @@
         {
           "TerritoryId": 155,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 5
+          "ContentFinderConditionId": 5,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 5c9fa391d2d9129e5ea7ce18d377e79862d4fc83..5961d3e18aa6f119d51a02f33386c4b8e8e29214 100644 (file)
@@ -38,7 +38,8 @@
         {
           "TerritoryId": 397,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 36
+          "ContentFinderConditionId": 36,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 78abed7d62a82896e0541878723a2781b29a56d3..a321c741548f0f4cff34c28f38dae7781778e856 100644 (file)
@@ -78,7 +78,8 @@
         {
           "TerritoryId": 398,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 37
+          "ContentFinderConditionId": 37,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 471a166622571a50c32c438a03cc8193ed2cc62e..02098cab905e6fd7fa8a504e2f55fc3cc3ff39c7 100644 (file)
@@ -42,7 +42,8 @@
         {
           "TerritoryId": 418,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 39
+          "ContentFinderConditionId": 39,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 492a0d582e9b96cbd53c510ac4bf08cb27a25345..9165e2a02cb358db79e12c50cb17265b1be776de 100644 (file)
@@ -59,7 +59,8 @@
         {
           "TerritoryId": 419,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 34
+          "ContentFinderConditionId": 34,
+          "AutoDutyEnabled": true
         }
       ]
     },
index cf7fe15e45ef35022ea0997b640aa4a276686a54..dab4f3da34b3eb2ab4cc8ffa5e322fcfc5675364 100644 (file)
         {
           "TerritoryId": 399,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 31
+          "ContentFinderConditionId": 31,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 6b6bd71cdcee22180f71289837127fc4310994e4..7bef51cc73884a2d985c283e59a7a5f999b8b88d 100644 (file)
@@ -62,7 +62,8 @@
         {
           "TerritoryId": 402,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 38
+          "ContentFinderConditionId": 38,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 8cb385f4827639acbb876af2c85bedb5996b06e9..fe2c33676b24ca535d28b998ba2da7d9811ce97f 100644 (file)
@@ -77,7 +77,8 @@
         {
           "TerritoryId": 463,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 141
+          "ContentFinderConditionId": 141,
+          "AutoDutyEnabled": true
         }
       ]
     },
index a2bb1f45e5461360fc7d7f76c536aa12da715b40..c3d9d0847a882af98157b8269e20849cb71dc975 100644 (file)
@@ -57,7 +57,8 @@
         {
           "TerritoryId": 155,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 182
+          "ContentFinderConditionId": 182,
+          "AutoDutyEnabled": true
         }
       ]
     },
index feba4bf066c6cc9dfbeb8bc74d1b346fd4a0b240..42aef1f00d79e91c30d3f0c45d6d23e64b82a516 100644 (file)
         {
           "TerritoryId": 152,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 219
+          "ContentFinderConditionId": 219,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 214a764af7a48545d36462d573536b1d46231d1b..6477857e4c764af851a44068ce355822c028ad1b 100644 (file)
@@ -87,7 +87,8 @@
         {
           "TerritoryId": 680,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 238
+          "ContentFinderConditionId": 238,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 2811433e505e4efb6fcdce8ac0e05641ea2e9579..702737b31c46b0d4bc3dde9d1f680e43c3593380 100644 (file)
         {
           "TerritoryId": 614,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 241
+          "ContentFinderConditionId": 241,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 3cdd92981ff7e157e618fbb2e07f089f4082aa86..a0b37e35011249a74ca9f19d8c300871abb40715 100644 (file)
         {
           "TerritoryId": 620,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 242
+          "ContentFinderConditionId": 242,
+          "AutoDutyEnabled": true
         }
       ]
     },
index d23fe19c2827189e530e513df9bee80dde49f55e..0856bbe6f830b0969e230791c883e415b28b925c 100644 (file)
@@ -98,7 +98,8 @@
         {\r
           "TerritoryId": 621,\r
           "InteractionType": "Duty",\r
-          "ContentFinderConditionId": 279\r
+          "ContentFinderConditionId": 279,\r
+          "AutoDutyEnabled": true\r
         }\r
       ]\r
     },\r
index 25ec971834f0a932f2a79bda8be300338b1e92c3..fba0763d8d133d7d2fc50df886ffdd232cb1c2ed 100644 (file)
@@ -40,7 +40,8 @@
         {
           "TerritoryId": 614,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 585
+          "ContentFinderConditionId": 585,
+          "AutoDutyEnabled": true
         }
       ]
     },
index c32f5c02cc87c9872b61d47135ea5aea90ea3cc1..fb9960de05dd45cbdebe87bde6923687c3c54dcc 100644 (file)
@@ -27,7 +27,8 @@
         {
           "TerritoryId": 829,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 611
+          "ContentFinderConditionId": 611,
+          "AutoDutyEnabled": true
         }
       ]
     },
index d4eef79d6d4a641067065171df441dab84146141..7f9a5a9484b293186b80cb1bab914ef037ee413f 100644 (file)
         {
           "TerritoryId": 813,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 676
+          "ContentFinderConditionId": 676,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 8f5193b0ba215198d0fb5f4ef6e512b23c7608e5..d1dfe7dcffb54586b63c376fd75f2b6dd9c37b63 100644 (file)
@@ -49,7 +49,8 @@
         {
           "TerritoryId": 816,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 649
+          "ContentFinderConditionId": 649,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 075dff0cc1888cb862194a4d8de532e832dea750..39284d2f79f8983c93e2f6a5f7d1f73ceb2d3321 100644 (file)
@@ -61,7 +61,8 @@
         {
           "TerritoryId": 817,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 651
+          "ContentFinderConditionId": 651,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 9ee12fd73d229a5f787178053746ab921eab6444..80cbd172fd0e382bd9d0b7160d3c6fcfd976a9e0 100644 (file)
@@ -62,7 +62,8 @@
         {
           "TerritoryId": 814,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 659
+          "ContentFinderConditionId": 659,
+          "AutoDutyEnabled": true
         }
       ]
     },
index c947dc964c1323f4635a228f8e4ef3a8008fa7cb..41dd831524035e697f0877dd19b138b5b50fbfe3 100644 (file)
@@ -40,7 +40,8 @@
         {
           "TerritoryId": 814,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 714
+          "ContentFinderConditionId": 714,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 22f2d97abbfd457f6cbad61f7008fe44c3c180b6..c4336959cf18b27641ca57b340de03f6ea5c205e 100644 (file)
@@ -55,7 +55,8 @@
         {
           "TerritoryId": 957,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 783
+          "ContentFinderConditionId": 783,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 37285324eb61817ff81c41befa6876d5d287cd4e..9c0d3a4875b132022e2cece8b23546bf660d8f87 100644 (file)
@@ -71,7 +71,8 @@
         {
           "TerritoryId": 957,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 789
+          "ContentFinderConditionId": 789,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 0a7e8e55546af1708d5258903daa2baac88e3309..9dd680ebdc5b0081a8ba1a7954c84b5cb4a81626 100644 (file)
@@ -39,7 +39,8 @@
         {
           "TerritoryId": 961,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 787
+          "ContentFinderConditionId": 787,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 5294bab43943c3bec527374e0f0ee1552e6aea87..f41bf95524db5432d07a1371512d24aa20e999e6 100644 (file)
@@ -38,7 +38,8 @@
         {
           "TerritoryId": 956,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 786
+          "ContentFinderConditionId": 786,
+          "AutoDutyEnabled": true
         }
       ]
     },
@@ -63,7 +64,8 @@
         {
           "TerritoryId": 1030,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 790
+          "ContentFinderConditionId": 790,
+          "AutoDutyEnabled": true
         }
       ]
     },
index c79468bb387ce358d6e60a28f62a3613540e7d13..dc9324d1d674ca316a62b35af28307e11cf31c98 100644 (file)
@@ -23,7 +23,8 @@
         {
           "TerritoryId": 957,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 844
+          "ContentFinderConditionId": 844,
+          "AutoDutyEnabled": false
         }
       ]
     },
index cc6ea1fad84afbe700253437992f8d5f28310370..c91af5976cd2aca77ec40c33148977403520a2ff 100644 (file)
@@ -71,7 +71,8 @@
         {
           "TerritoryId": 1056,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 869
+          "ContentFinderConditionId": 869,
+          "AutoDutyEnabled": false
         }
       ]
     },
index 130cb389ea18c07fa48a8b917542cac1f7e1998f..df1bd69f0f7df04d37a2fe77dadf324fea96f70c 100644 (file)
@@ -57,7 +57,8 @@
           "TerritoryId": 958,
           "InteractionType": "Duty",
           "Comment": "Lapis Manalis",
-          "ContentFinderConditionId": 896
+          "ContentFinderConditionId": 896,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 9f738bc8fd74d48761150a2734b326bcea4ef7b0..8af360800cd71bd26c519db9453a12c7763175ed 100644 (file)
         {
           "TerritoryId": 962,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 822
+          "ContentFinderConditionId": 822,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 3930a8dde21d96b33b5da03c1cfcab4f40fc1e15..96953ea2776b3e82ee6b81adef482bde03421553 100644 (file)
@@ -24,7 +24,8 @@
         {
           "TerritoryId": 1162,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 823
+          "ContentFinderConditionId": 823,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 28e35590e94d3903db372b888477de1a6eea2706..28a6850cd69fd2e701126216a76b770135a75e19 100644 (file)
@@ -58,7 +58,8 @@
         {
           "TerritoryId": 1185,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 826
+          "ContentFinderConditionId": 826,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 8b1a2021beef34b1428f51311c3938aa2167aeb2..2ef3a8e69877dc92c9f4ac3edea0dd66d483e692 100644 (file)
@@ -23,7 +23,8 @@
         {
           "TerritoryId": 1187,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 824
+          "ContentFinderConditionId": 824,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 384623cef3c0450c278beac6aa5292b94a963861..528fde3f8830528fc51e476bc32dbc929c109ed4 100644 (file)
         {
           "TerritoryId": 1189,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 829
+          "ContentFinderConditionId": 829,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 51a11987797553d13d8e2d053922cc26da6f305d..682576cb38d8f2eb2b6055df4568ba3764aca129 100644 (file)
@@ -60,7 +60,8 @@
         {
           "TerritoryId": 1219,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 831
+          "ContentFinderConditionId": 831,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 8e9677088fc95897ec4781775e2f90a7f2a85108..15817e8d17eb736aed5068279877c3ceccb5709f 100644 (file)
@@ -39,7 +39,8 @@
         {
           "TerritoryId": 1191,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 825
+          "ContentFinderConditionId": 825,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 778f11db546e73058a09c6d735cef011ed82f43d..cafc8975c2f2937c0beeb174b3390df31f63d93b 100644 (file)
@@ -56,7 +56,8 @@
         {
           "TerritoryId": 1192,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 827
+          "ContentFinderConditionId": 827,
+          "AutoDutyEnabled": true
         }
       ]
     },
index 6d0878d1499e674d29c9fd36ae04276e2f18b043..40f6d44d7eefd739d13bb137b7634f8df01f3dd4 100644 (file)
         {
           "TerritoryId": 1191,
           "InteractionType": "Duty",
-          "ContentFinderConditionId": 1008
+          "ContentFinderConditionId": 1008,
+          "AutoDutyEnabled": true
         }
       ]
     },
index cb035ef71296e1372bfc4e318b10126010aedbdd..eab9789a6f7ba17dcd70e0148d4a8790348c9513 100644 (file)
                 "exclusiveMinimum": 0,
                 "exclusiveMaximum": 3000
               },
+              "AutoDutyEnabled": {
+                "type": "boolean"
+              },
               "DataId": {
                 "type": "null"
               },
index 0b4a05a21dbfd4ee9fc8b39b8f643a11b479d590..bd1ce3040d4e02d1091ee8c4e2df3fe6aee86203 100644 (file)
@@ -74,6 +74,7 @@ public sealed class QuestStep
 
     public JumpDestination? JumpDestination { get; set; }
     public uint? ContentFinderConditionId { get; set; }
+    public bool AutoDutyEnabled { get; set; }
     public SkipConditions? SkipConditions { get; set; }
 
     public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
index b4eb5a5b850dee5f10c6833ad99209c67c2a4d3f..90c42bb5031ac0d3fa0874b77613383833871b90 100644 (file)
@@ -1,4 +1,5 @@
-using Dalamud.Configuration;
+using System.Collections.Generic;
+using Dalamud.Configuration;
 using Dalamud.Game.Text;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using LLib.ImGui;
@@ -12,6 +13,7 @@ internal sealed class Configuration : IPluginConfiguration
     public int Version { get; set; } = 1;
     public int PluginSetupCompleteVersion { get; set; }
     public GeneralConfiguration General { get; } = new();
+    public DutyConfiguration Duties { get; } = new();
     public NotificationConfiguration Notifications { get; } = new();
     public AdvancedConfiguration Advanced { get; } = new();
     public WindowConfig DebugWindowConfig { get; } = new();
@@ -32,6 +34,13 @@ internal sealed class Configuration : IPluginConfiguration
         public bool ConfigureTextAdvance { get; set; } = true;
     }
 
+    internal sealed class DutyConfiguration
+    {
+        public bool RunInstancedContentWithAutoDuty { get; set; }
+        public HashSet<uint> WhitelistedDutyCfcIds { get; set; } = [];
+        public HashSet<uint> BlacklistedDutyCfcIds { get; set; } = [];
+    }
+
     internal sealed class NotificationConfiguration
     {
         public bool Enabled { get; set; } = true;
index c948abe23179e67a2aaa7c4b9fb772cd7a88690e..d6641073ca97e50c995faa7cca98ab44155a5e68 100644 (file)
@@ -29,7 +29,8 @@ internal sealed class QuestRegistry
     private readonly LeveData _leveData;
 
     private readonly ICallGateProvider<object> _reloadDataIpc;
-    private readonly Dictionary<ElementId, Quest> _quests = new();
+    private readonly Dictionary<ElementId, Quest> _quests = [];
+    private readonly Dictionary<uint, (ElementId QuestId, QuestStep Step)> _contentFinderConditionIds = [];
 
     public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
         QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator,
@@ -55,6 +56,7 @@ internal sealed class QuestRegistry
     {
         _questValidator.Reset();
         _quests.Clear();
+        _contentFinderConditionIds.Clear();
 
         LoadQuestsFromAssembly();
         LoadQuestsFromProjectDirectory();
@@ -70,6 +72,7 @@ internal sealed class QuestRegistry
                 "Failed to load all quests from user directory (some may have been successfully loaded)");
         }
 
+        LoadCfcIds();
         ValidateQuests();
         Reloaded?.Invoke(this, EventArgs.Empty);
         try
@@ -142,6 +145,18 @@ internal sealed class QuestRegistry
         }
     }
 
+    private void LoadCfcIds()
+    {
+        foreach (var quest in _quests.Values)
+        {
+            foreach (var dutyStep in quest.AllSteps().Where(x =>
+                         x.Step.InteractionType == EInteractionType.Duty && x.Step.ContentFinderConditionId != null))
+            {
+                _contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step);
+            }
+        }
+    }
+
     private void ValidateQuests()
     {
         _questValidator.Validate(_quests.Values.Where(x => x.Source != Quest.ESource.Assembly).ToList());
@@ -223,4 +238,16 @@ internal sealed class QuestRegistry
             .Where(x => IsKnownQuest(x.QuestId))
             .ToList();
     }
+
+    public bool TryGetDutyByContentFinderConditionId(uint cfcId, out bool autoDutyEnabledByDefault)
+    {
+        if (_contentFinderConditionIds.TryGetValue(cfcId, out var value))
+        {
+            autoDutyEnabledByDefault = value.Step.AutoDutyEnabled;
+            return true;
+        }
+
+        autoDutyEnabledByDefault = false;
+        return false;
+    }
 }
index 7334ca075ac059f7b66851f32deda8c89b1c7c58..e83a1186c29313d366ba5093dba7de1caaf0461a 100644 (file)
@@ -13,6 +13,7 @@ internal static class SendNotification
 {
     internal sealed class Factory(
         AutomatonIpc automatonIpc,
+        AutoDutyIpc autoDutyIpc,
         TerritoryData territoryData) : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
@@ -21,7 +22,7 @@ internal static class SendNotification
             {
                 EInteractionType.Snipe when !automatonIpc.IsAutoSnipeEnabled =>
                     new Task(step.InteractionType, step.Comment),
-                EInteractionType.Duty =>
+                EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled) =>
                     new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
                         ? territoryData.GetContentFinderConditionName(step.ContentFinderConditionId.Value)
                         : step.Comment),
index 42c94d87bf4db614dbddb11d28f64ac503373fe6..fab5c6ed30a9a59b29b5661bb3effd86672303b9 100644 (file)
@@ -1,6 +1,10 @@
 using System;
+using System.Collections.Generic;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
+using Questionable.Controller.Steps.Shared;
+using Questionable.Data;
+using Questionable.External;
 using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
@@ -9,26 +13,86 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Duty
 {
-    internal sealed class Factory : SimpleTaskFactory
+    internal sealed class Factory(AutoDutyIpc autoDutyIpc) : ITaskFactory
     {
-        public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+        public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
             if (step.InteractionType != EInteractionType.Duty)
-                return null;
+                yield break;
 
             ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
-            return new Task(step.ContentFinderConditionId.Value);
+
+            if (autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled))
+            {
+                yield return new StartAutoDutyTask(step.ContentFinderConditionId.Value);
+                yield return new WaitAutoDutyTask(step.ContentFinderConditionId.Value);
+                yield return new WaitAtEnd.WaitNextStepOrSequence();
+            }
+            else
+            {
+                yield return new OpenDutyFinderTask(step.ContentFinderConditionId.Value);
+            }
+        }
+    }
+
+    internal sealed record StartAutoDutyTask(uint ContentFinderConditionId) : ITask
+    {
+        public override string ToString() => $"StartAutoDuty({ContentFinderConditionId})";
+    }
+
+    internal sealed class StartAutoDutyExecutor(
+        AutoDutyIpc autoDutyIpc,
+        TerritoryData territoryData,
+        IClientState clientState) : TaskExecutor<StartAutoDutyTask>
+    {
+        protected override bool Start()
+        {
+            autoDutyIpc.StartInstance(Task.ContentFinderConditionId);
+            return true;
+        }
+
+        public override ETaskResult Update()
+        {
+            if (!territoryData.TryGetTerritoryIdForContentFinderCondition(Task.ContentFinderConditionId,
+                    out uint territoryId))
+                throw new TaskException("Failed to get territory ID for content finder condition");
+
+            return clientState.TerritoryType == territoryId ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+        }
+    }
+
+    internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask
+    {
+        public override string ToString() => $"Wait(AutoDuty, left instance {ContentFinderConditionId})";
+    }
+
+    internal sealed class WaitAutoDutyExecutor(
+        AutoDutyIpc autoDutyIpc,
+        TerritoryData territoryData,
+        IClientState clientState) : TaskExecutor<WaitAutoDutyTask>
+    {
+        protected override bool Start() => true;
+
+        public override ETaskResult Update()
+        {
+            if (!territoryData.TryGetTerritoryIdForContentFinderCondition(Task.ContentFinderConditionId,
+                    out uint territoryId))
+                throw new TaskException("Failed to get territory ID for content finder condition");
+
+            return clientState.TerritoryType != territoryId && autoDutyIpc.IsStopped()
+                ? ETaskResult.TaskComplete
+                : ETaskResult.StillRunning;
         }
     }
 
-    internal sealed record Task(uint ContentFinderConditionId) : ITask
+    internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask
     {
         public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
     }
 
-    internal sealed class OpenDutyWindowExecutor(
+    internal sealed class OpenDutyFinderExecutor(
         GameFunctions gameFunctions,
-        ICondition condition) : TaskExecutor<Task>
+        ICondition condition) : TaskExecutor<OpenDutyFinderTask>
     {
         protected override bool Start()
         {
index d64c009bf503ddf3eb3a7d1fd3b528049ffa82c8..0b3a02ba97e551e7c431205abcacafeaeba82541 100644 (file)
@@ -8,6 +8,7 @@ using Dalamud.Plugin.Services;
 using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Utils;
 using Questionable.Data;
+using Questionable.External;
 using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
@@ -19,7 +20,8 @@ internal static class WaitAtEnd
     internal sealed class Factory(
         IClientState clientState,
         ICondition condition,
-        TerritoryData territoryData)
+        TerritoryData territoryData,
+        AutoDutyIpc autoDutyIpc)
         : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@@ -50,7 +52,7 @@ internal static class WaitAtEnd
                 case EInteractionType.Snipe:
                     return [new WaitNextStepOrSequence()];
 
-                case EInteractionType.Duty:
+                case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
                 case EInteractionType.SinglePlayerDuty:
                     return [new EndAutomation()];
 
index ee91f6b44fe08fb5267fb54125f65d7d4b7e5e70..970718b2fcfab02722641e25137b268b74ad5267 100644 (file)
@@ -1,8 +1,11 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.Globalization;
 using System.Linq;
+using Dalamud.Game;
 using Dalamud.Plugin.Services;
+using Dalamud.Utility;
 using FFXIVClientStructs.FFXIV.Client.Game.Character;
 using Lumina.Excel.Sheets;
 
@@ -15,6 +18,7 @@ internal sealed class TerritoryData
     private readonly ImmutableDictionary<ushort, uint> _dutyTerritories;
     private readonly ImmutableDictionary<uint, string> _instanceNames;
     private readonly ImmutableDictionary<uint, string> _contentFinderConditionNames;
+    private readonly ImmutableDictionary<uint, uint> _contentFinderConditionIds;
 
     public TerritoryData(IDataManager dataManager)
     {
@@ -40,11 +44,14 @@ internal sealed class TerritoryData
 
         _instanceNames = dataManager.GetExcelSheet<ContentFinderCondition>()
             .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
-            .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToString());
+            .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToDalamudString().ToString());
 
         _contentFinderConditionNames = dataManager.GetExcelSheet<ContentFinderCondition>()
             .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
-            .ToImmutableDictionary(x => x.RowId, x => x.Name.ToString());
+            .ToImmutableDictionary(x => x.RowId, x => FixName(x.Name.ToDalamudString().ToString(), dataManager.Language));
+        _contentFinderConditionIds = dataManager.GetExcelSheet<ContentFinderCondition>()
+            .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
+            .ToImmutableDictionary(x => x.RowId, x => x.TerritoryType.RowId);
     }
 
     public string? GetName(ushort territoryId) => _territoryNames.GetValueOrDefault(territoryId);
@@ -68,4 +75,15 @@ internal sealed class TerritoryData
     public string? GetInstanceName(ushort instanceId) => _instanceNames.GetValueOrDefault(instanceId);
 
     public string? GetContentFinderConditionName(uint cfcId) => _contentFinderConditionNames.GetValueOrDefault(cfcId);
+
+    public bool TryGetTerritoryIdForContentFinderCondition(uint cfcId, out uint territoryId) =>
+        _contentFinderConditionIds.TryGetValue(cfcId, out territoryId);
+
+    private static string FixName(string name, ClientLanguage language)
+    {
+        if (string.IsNullOrEmpty(name) || language != ClientLanguage.English)
+            return name;
+
+        return string.Concat(name[0].ToString().ToUpper(CultureInfo.InvariantCulture), name.AsSpan(1));
+    }
 }
diff --git a/Questionable/External/AutoDutyIpc.cs b/Questionable/External/AutoDutyIpc.cs
new file mode 100644 (file)
index 0000000..1089efc
--- /dev/null
@@ -0,0 +1,89 @@
+using Dalamud.Plugin;
+using Dalamud.Plugin.Ipc;
+using Dalamud.Plugin.Ipc.Exceptions;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps;
+using Questionable.Data;
+
+namespace Questionable.External;
+
+internal sealed class AutoDutyIpc
+{
+    private readonly Configuration _configuration;
+    private readonly TerritoryData _territoryData;
+    private readonly ILogger<AutoDutyIpc> _logger;
+    private readonly ICallGateSubscriber<uint,bool> _contentHasPath;
+    private readonly ICallGateSubscriber<uint,int,bool,object> _run;
+    private readonly ICallGateSubscriber<bool> _isStopped;
+
+    public AutoDutyIpc(IDalamudPluginInterface pluginInterface, Configuration configuration, TerritoryData territoryData, ILogger<AutoDutyIpc> logger)
+    {
+        _configuration = configuration;
+        _territoryData = territoryData;
+        _logger = logger;
+        _contentHasPath = pluginInterface.GetIpcSubscriber<uint, bool>("AutoDuty.ContentHasPath");
+        _run = pluginInterface.GetIpcSubscriber<uint, int, bool, object>("AutoDuty.Run");
+        _isStopped = pluginInterface.GetIpcSubscriber<bool>("AutoDuty.IsStopped");
+    }
+
+    public bool IsConfiguredToRunContent(uint? cfcId, bool autoDutyEnabled)
+    {
+        if (cfcId == null)
+            return false;
+
+        if (!_configuration.Duties.RunInstancedContentWithAutoDuty)
+            return false;
+
+        if (_configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId.Value))
+            return false;
+
+        if (_configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId.Value) &&
+            _territoryData.TryGetTerritoryIdForContentFinderCondition(cfcId.Value, out _))
+            return true;
+
+        return autoDutyEnabled && HasPath(cfcId.Value);
+    }
+
+    public bool HasPath(uint cfcId)
+    {
+        if (!_territoryData.TryGetTerritoryIdForContentFinderCondition(cfcId, out uint territoryType))
+            return false;
+
+        try
+        {
+            return _contentHasPath.InvokeFunc(territoryType);
+        }
+        catch (IpcError e)
+        {
+            _logger.LogWarning("Unable to query AutoDuty for path in territory {TerritoryType}: {Message}", territoryType, e.Message);
+            return false;
+        }
+    }
+
+    public void StartInstance(uint cfcId)
+    {
+        if (!_territoryData.TryGetTerritoryIdForContentFinderCondition(cfcId, out uint territoryType))
+            throw new TaskException($"Unknown ContentFinderConditionId {cfcId}");
+
+        try
+        {
+            _run.InvokeAction(territoryType, 0, true);
+        }
+        catch (IpcError e)
+        {
+            throw new TaskException($"Unable to run content with AutoDuty: {e.Message}", e);
+        }
+    }
+
+    public bool IsStopped()
+    {
+        try
+        {
+            return _isStopped.InvokeFunc();
+        }
+        catch (IpcError)
+        {
+            return true;
+        }
+    }
+}
index 5964eb45ad375a99c975a431642e3cb97f7ff6e5..1072a54f29c0c6737ab7a71ea3f81e9cd7f69243 100644 (file)
@@ -129,6 +129,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<TextAdvanceIpc>();
         serviceCollection.AddSingleton<NotificationMasterIpc>();
         serviceCollection.AddSingleton<AutomatonIpc>();
+        serviceCollection.AddSingleton<AutoDutyIpc>();
     }
 
     private static void AddTaskFactories(ServiceCollection serviceCollection)
@@ -178,7 +179,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
             .AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
         serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>();
         serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
-        serviceCollection.AddTaskFactoryAndExecutor<Duty.Task, Duty.Factory, Duty.OpenDutyWindowExecutor>();
+        serviceCollection.AddTaskFactoryAndExecutor<Duty.OpenDutyFinderTask, Duty.Factory, Duty.OpenDutyFinderExecutor>();
+        serviceCollection.AddTaskExecutor<Duty.StartAutoDutyTask, Duty.StartAutoDutyExecutor>();
+        serviceCollection.AddTaskExecutor<Duty.WaitAutoDutyTask, Duty.WaitAutoDutyExecutor>();
         serviceCollection.AddTaskFactory<Emote.Factory>();
         serviceCollection.AddTaskExecutor<Emote.UseOnObject, Emote.UseOnObjectExecutor>();
         serviceCollection.AddTaskExecutor<Emote.UseOnSelf, Emote.UseOnSelfExecutor>();
index f4414084699aa36bd121c367a820f030b766ac89..eaf8f62266e48ae1f5bc918f4da6c33b4bc7e5ce 100644 (file)
@@ -1,7 +1,11 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
+using System.Numerics;
+using System.Text;
 using Dalamud.Game.Text;
+using Dalamud.Interface;
 using Dalamud.Interface.Colors;
 using Dalamud.Interface.Components;
 using Dalamud.Interface.Utility.Raii;
@@ -12,19 +16,28 @@ using ImGuiNET;
 using LLib.ImGui;
 using Lumina.Excel.Sheets;
 using Questionable.Controller;
+using Questionable.Data;
 using Questionable.External;
+using Questionable.Model;
 using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
 
 namespace Questionable.Windows;
 
 internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
 {
+    private const string DutyClipboardPrefix = "qst:duty:";
+    private const string DutyClipboardSeparator = ";";
+    private const string DutyWhitelistPrefix = "+";
+    private const string DutyBlacklistPrefix = "-";
+
     private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")];
 
     private readonly IDalamudPluginInterface _pluginInterface;
     private readonly NotificationMasterIpc _notificationMasterIpc;
     private readonly Configuration _configuration;
     private readonly CombatController _combatController;
+    private readonly QuestRegistry _questRegistry;
+    private readonly AutoDutyIpc _autoDutyIpc;
 
     private readonly uint[] _mountIds;
     private readonly string[] _mountNames;
@@ -34,17 +47,38 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
     private readonly string[] _grandCompanyNames =
         ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
 
+    private readonly string[] _supportedCfcOptions =
+    [
+        $"{SeIconChar.Circle.ToIconChar()} Enabled (Default)",
+        $"{SeIconChar.Circle.ToIconChar()} Enabled",
+        $"{SeIconChar.Cross.ToIconChar()} Disabled"
+    ];
+
+    private readonly string[] _unsupportedCfcOptions =
+    [
+        $"{SeIconChar.Cross.ToIconChar()} Disabled (Default)",
+        $"{SeIconChar.Circle.ToIconChar()} Enabled",
+        $"{SeIconChar.Cross.ToIconChar()} Disabled"
+    ];
+
+    private readonly Dictionary<EExpansionVersion, List<DutyInfo>> _contentFinderConditionNames;
+
     public ConfigWindow(IDalamudPluginInterface pluginInterface,
         NotificationMasterIpc notificationMasterIpc,
         Configuration configuration,
         IDataManager dataManager,
-        CombatController combatController)
+        CombatController combatController,
+        TerritoryData territoryData,
+        QuestRegistry questRegistry,
+        AutoDutyIpc autoDutyIpc)
         : base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize)
     {
         _pluginInterface = pluginInterface;
         _notificationMasterIpc = notificationMasterIpc;
         _configuration = configuration;
         _combatController = combatController;
+        _questRegistry = questRegistry;
+        _autoDutyIpc = autoDutyIpc;
 
         var mounts = dataManager.GetExcelSheet<Mount>()
             .Where(x => x is { RowId: > 0, Icon: > 0 })
@@ -54,10 +88,41 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
             .ToList();
         _mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray();
         _mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray();
+
+        _contentFinderConditionNames = dataManager.GetExcelSheet<DawnContent>()
+            .Where(x => x.RowId > 0)
+            .Select(x => x.Content.ValueNullable)
+            .Where(x => x != null)
+            .Select(x => x!.Value)
+            .Select(x => new
+            {
+                Expansion = (EExpansionVersion)x.TerritoryType.Value.ExVersion.RowId,
+                CfcId = x.RowId,
+                Name = territoryData.GetContentFinderConditionName(x.RowId) ?? "?",
+                TerritoryId = x.TerritoryType.RowId,
+                ContentType = x.ContentType.RowId,
+                Level = x.ClassJobLevelRequired,
+                x.SortKey
+            })
+            .GroupBy(x => x.Expansion)
+            .ToDictionary(x => x.Key,
+                x => x.OrderBy(y => y.Level)
+                    .ThenBy(y => y.ContentType)
+                    .ThenBy(y => y.SortKey)
+                    .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}"))
+                    .ToList());
     }
 
     public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
 
+    private static string FormatLevel(int level)
+    {
+        if (level == 0)
+            return string.Empty;
+
+        return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}";
+    }
+
     public override void Draw()
     {
         using var tabBar = ImRaii.TabBar("QuestionableConfigTabs");
@@ -65,6 +130,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
             return;
 
         DrawGeneralTab();
+        DrawDutiesTab();
         DrawNotificationsTab();
         DrawAdvancedTab();
     }
@@ -138,6 +204,175 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
         }
     }
 
+    private void DrawDutiesTab()
+    {
+        using var tab = ImRaii.TabItem("Duties");
+        if (!tab)
+            return;
+
+        bool runInstancedContentWithAutoDuty = _configuration.Duties.RunInstancedContentWithAutoDuty;
+        if (ImGui.Checkbox("Run instanced content with AutoDuty and BossMod", ref runInstancedContentWithAutoDuty))
+        {
+            _configuration.Duties.RunInstancedContentWithAutoDuty = runInstancedContentWithAutoDuty;
+            Save();
+        }
+
+        ImGui.SameLine();
+        ImGuiComponents.HelpMarker(
+            "The combat module used for this is configured by AutoDuty, ignoring whichever selection you've made in Questionable's \"General\" configuration.");
+
+        ImGui.Separator();
+
+        using (ImRaii.Disabled(!runInstancedContentWithAutoDuty))
+        {
+            ImGui.Text(
+                "Questionable includes a default list of duties that work if AutoDuty and BossMod are installed.");
+
+            ImGui.Text("The included list of duties can change with each update, and is based on the following spreadsheet:");
+            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.GlobeEurope, "Open AutoDuty spreadsheet"))
+                Util.OpenLink(
+                    "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0");
+
+            ImGui.Separator();
+            ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:");
+
+            using (var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true))
+            {
+                if (child)
+                {
+                    foreach (EExpansionVersion expansion in Enum.GetValues<EExpansionVersion>())
+                    {
+                        if (ImGui.CollapsingHeader(expansion.ToString()))
+                        {
+                            using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit);
+                            if (table)
+                            {
+                                ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch);
+                                ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f);
+
+                                if (_contentFinderConditionNames.TryGetValue(expansion, out var cfcNames))
+                                {
+                                    foreach (var (cfcId, territoryId, name) in cfcNames)
+                                    {
+                                        if (_questRegistry.TryGetDutyByContentFinderConditionId(cfcId,
+                                                out bool autoDutyEnabledByDefault))
+                                        {
+                                            ImGui.TableNextRow();
+
+                                            string[] labels = autoDutyEnabledByDefault
+                                                ? _supportedCfcOptions
+                                                : _unsupportedCfcOptions;
+                                            int value = 0;
+                                            if (_configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId))
+                                                value = 1;
+                                            if (_configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId))
+                                                value = 2;
+
+                                            if (ImGui.TableNextColumn())
+                                            {
+                                                ImGui.AlignTextToFramePadding();
+                                                ImGui.TextUnformatted(name);
+                                                if (ImGui.IsItemHovered() && _configuration.Advanced.AdditionalStatusInformation)
+                                                {
+                                                    using var tooltip = ImRaii.Tooltip();
+                                                    if (tooltip)
+                                                    {
+                                                        ImGui.TextUnformatted(name);
+                                                        ImGui.Separator();
+                                                        ImGui.BulletText($"TerritoryId: {territoryId}");
+                                                        ImGui.BulletText($"ContentFinderConditionId: {cfcId}");
+                                                    }
+                                                }
+
+                                                if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(cfcId))
+                                                    ImGuiComponents.HelpMarker("This duty is not supported by AutoDuty", FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
+                                            }
+
+                                            if (ImGui.TableNextColumn())
+                                            {
+                                                using var _ = ImRaii.PushId($"##Dungeon{cfcId}");
+                                                ImGui.SetNextItemWidth(200);
+                                                if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
+                                                {
+                                                    _configuration.Duties.WhitelistedDutyCfcIds.Remove(cfcId);
+                                                    _configuration.Duties.BlacklistedDutyCfcIds.Remove(cfcId);
+
+                                                    if (value == 1)
+                                                        _configuration.Duties.WhitelistedDutyCfcIds.Add(cfcId);
+                                                    else if (value == 2)
+                                                        _configuration.Duties.BlacklistedDutyCfcIds.Add(cfcId);
+
+                                                    Save();
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            using (ImRaii.Disabled(_configuration.Duties.WhitelistedDutyCfcIds.Count +
+                       _configuration.Duties.BlacklistedDutyCfcIds.Count == 0))
+            {
+                if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, "Export to clipboard"))
+                {
+                    var whitelisted =
+                        _configuration.Duties.WhitelistedDutyCfcIds.Select(x => $"{DutyWhitelistPrefix}{x}");
+                    var blacklisted =
+                        _configuration.Duties.BlacklistedDutyCfcIds.Select(x => $"{DutyBlacklistPrefix}{x}");
+                    string text = DutyClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(
+                        string.Join(DutyClipboardSeparator, whitelisted.Concat(blacklisted))));
+                    ImGui.SetClipboardText(text);
+                }
+            }
+
+            ImGui.SameLine();
+
+            string? clipboardText = GetClipboardText();
+            using (ImRaii.Disabled(clipboardText == null || !clipboardText.StartsWith(DutyClipboardPrefix, StringComparison.InvariantCulture)))
+            {
+                if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Paste, "Import from Clipboard"))
+                {
+                    clipboardText = clipboardText!.Substring(DutyClipboardPrefix.Length);
+                    string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText));
+
+                    _configuration.Duties.WhitelistedDutyCfcIds.Clear();
+                    _configuration.Duties.BlacklistedDutyCfcIds.Clear();
+                    foreach (string part in text.Split(DutyClipboardSeparator))
+                    {
+                        if (part.StartsWith(DutyWhitelistPrefix, StringComparison.InvariantCulture) &&
+                            uint.TryParse(part.AsSpan(DutyWhitelistPrefix.Length), CultureInfo.InvariantCulture,
+                                out uint whitelistedCfcId))
+                            _configuration.Duties.WhitelistedDutyCfcIds.Add(whitelistedCfcId);
+
+                        if (part.StartsWith(DutyBlacklistPrefix, StringComparison.InvariantCulture) &&
+                            uint.TryParse(part.AsSpan(DutyBlacklistPrefix.Length), CultureInfo.InvariantCulture,
+                                out uint blacklistedCfcId))
+                            _configuration.Duties.WhitelistedDutyCfcIds.Add(blacklistedCfcId);
+                    }
+                }
+            }
+
+            ImGui.SameLine();
+
+            using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
+            {
+                if (ImGui.Button("Reset to default"))
+                {
+                    _configuration.Duties.WhitelistedDutyCfcIds.Clear();
+                    _configuration.Duties.BlacklistedDutyCfcIds.Clear();
+                    Save();
+                }
+            }
+
+            if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
+                ImGui.SetTooltip("Hold CTRL to enable this button.");
+        }
+    }
+
     private void DrawNotificationsTab()
     {
         using var tab = ImRaii.TabItem("Notifications");
@@ -231,4 +466,21 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
     private void Save() => _pluginInterface.SavePluginConfig(_configuration);
 
     public void SaveWindowConfig() => Save();
+
+    /// <summary>
+    /// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
+    /// </summary>
+    private unsafe string? GetClipboardText()
+    {
+        byte* ptr = ImGuiNative.igGetClipboardText();
+        if (ptr == null)
+            return null;
+
+        int byteCount = 0;
+        while (ptr[byteCount] != 0)
+            ++byteCount;
+        return Encoding.UTF8.GetString(ptr, byteCount);
+    }
+
+    private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name);
 }