diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index 12b27ef4..6b76bb95 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -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(), diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/245_It's Probably Pirates.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/245_It's Probably Pirates.json index 8a352254..0d3aada9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/245_It's Probably Pirates.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/245_It's Probably Pirates.json @@ -112,7 +112,8 @@ { "TerritoryId": 138, "InteractionType": "Duty", - "ContentFinderConditionId": 4 + "ContentFinderConditionId": 4, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json index ff980ffa..cab6933d 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json @@ -71,7 +71,8 @@ { "TerritoryId": 146, "InteractionType": "Duty", - "ContentFinderConditionId": 56 + "ContentFinderConditionId": 56, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json index b6f45f31..e5c21738 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json @@ -62,7 +62,8 @@ { "TerritoryId": 140, "InteractionType": "Duty", - "ContentFinderConditionId": 3 + "ContentFinderConditionId": 3, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/677_Fire in the Gloom.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/677_Fire in the Gloom.json index 7340e66a..726eae29 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/677_Fire in the Gloom.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/677_Fire in the Gloom.json @@ -57,7 +57,8 @@ { "TerritoryId": 148, "InteractionType": "Duty", - "ContentFinderConditionId": 2 + "ContentFinderConditionId": 2, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/514_Into the Beast's Maw.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/514_Into the Beast's Maw.json index cf0acff2..45b28b3f 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/514_Into the Beast's Maw.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/514_Into the Beast's Maw.json @@ -44,7 +44,8 @@ { "TerritoryId": 153, "InteractionType": "Duty", - "ContentFinderConditionId": 1 + "ContentFinderConditionId": 1, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A9-Haukke Manor/801_Skeletons in Her Closet.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A9-Haukke Manor/801_Skeletons in Her Closet.json index c3a02a0c..9269a84b 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A9-Haukke Manor/801_Skeletons in Her Closet.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A9-Haukke Manor/801_Skeletons in Her Closet.json @@ -66,7 +66,8 @@ { "TerritoryId": 148, "InteractionType": "Duty", - "ContentFinderConditionId": 6 + "ContentFinderConditionId": 6, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/B2-Eastern La Noscea, Brayflox, Cheese and Wine/832_The Things We Do for Cheese.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/B2-Eastern La Noscea, Brayflox, Cheese and Wine/832_The Things We Do for Cheese.json index bcb40f05..06402bac 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/B2-Eastern La Noscea, Brayflox, Cheese and Wine/832_The Things We Do for Cheese.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/B2-Eastern La Noscea, Brayflox, Cheese and Wine/832_The Things We Do for Cheese.json @@ -85,7 +85,8 @@ { "TerritoryId": 137, "InteractionType": "Duty", - "ContentFinderConditionId": 8 + "ContentFinderConditionId": 8, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/B4-Titan/857_Lord of Crags.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/B4-Titan/857_Lord of Crags.json index e0898189..4cc4c576 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/B4-Titan/857_Lord of Crags.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/B4-Titan/857_Lord of Crags.json @@ -45,7 +45,8 @@ { "TerritoryId": 139, "InteractionType": "Duty", - "ContentFinderConditionId": 57 + "ContentFinderConditionId": 57, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C1-Coerthas Central Highlands, The Enterprise/952_In Pursuit of the Past.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C1-Coerthas Central Highlands, The Enterprise/952_In Pursuit of the Past.json index ad446eeb..57ef1fa9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C1-Coerthas Central Highlands, The Enterprise/952_In Pursuit of the Past.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C1-Coerthas Central Highlands, The Enterprise/952_In Pursuit of the Past.json @@ -59,7 +59,8 @@ { "TerritoryId": 155, "InteractionType": "Duty", - "ContentFinderConditionId": 11 + "ContentFinderConditionId": 11, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C3-Garuda/519_Lady of the Vortex.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C3-Garuda/519_Lady of the Vortex.json index ff50fed7..d9315eca 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C3-Garuda/519_Lady of the Vortex.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C3-Garuda/519_Lady of the Vortex.json @@ -38,7 +38,8 @@ { "TerritoryId": 331, "InteractionType": "Duty", - "ContentFinderConditionId": 58 + "ContentFinderConditionId": 58, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/3873_Rock the Castrum.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/3873_Rock the Castrum.json index f7d746fe..7eec220a 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/3873_Rock the Castrum.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/3873_Rock the Castrum.json @@ -45,7 +45,8 @@ { "TerritoryId": 147, "InteractionType": "Duty", - "ContentFinderConditionId": 15 + "ContentFinderConditionId": 15, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json index c878f58b..1a788c09 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json @@ -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 } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/75_The Path of the Righteous.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/75_The Path of the Righteous.json index 34e9b723..b6581b9a 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/75_The Path of the Righteous.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/75_The Path of the Righteous.json @@ -88,7 +88,8 @@ { "TerritoryId": 155, "InteractionType": "Duty", - "ContentFinderConditionId": 27 + "ContentFinderConditionId": 27, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E5-2.5/366_The Rising Chorus.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E5-2.5/366_The Rising Chorus.json index fc1d7220..24d72b21 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E5-2.5/366_The Rising Chorus.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E5-2.5/366_The Rising Chorus.json @@ -107,7 +107,8 @@ { "TerritoryId": 156, "InteractionType": "Duty", - "ContentFinderConditionId": 32 + "ContentFinderConditionId": 32, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1131_Gilding the Bilious (Maelstrom).json b/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1131_Gilding the Bilious (Maelstrom).json index e0157efe..165a1839 100644 --- a/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1131_Gilding the Bilious (Maelstrom).json +++ b/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1131_Gilding the Bilious (Maelstrom).json @@ -71,7 +71,8 @@ { "TerritoryId": 155, "InteractionType": "Duty", - "ContentFinderConditionId": 5 + "ContentFinderConditionId": 5, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1132_Gilding the Bilious (Twin Adder).json b/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1132_Gilding the Bilious (Twin Adder).json index 178a0b5a..4defc6a9 100644 --- a/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1132_Gilding the Bilious (Twin Adder).json +++ b/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1132_Gilding the Bilious (Twin Adder).json @@ -71,7 +71,8 @@ { "TerritoryId": 155, "InteractionType": "Duty", - "ContentFinderConditionId": 5 + "ContentFinderConditionId": 5, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1133_Gilding the Bilious (Immortal Flames).json b/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1133_Gilding the Bilious (Immortal Flames).json index 8915d671..cf83d8a4 100644 --- a/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1133_Gilding the Bilious (Immortal Flames).json +++ b/QuestPaths/2.x - A Realm Reborn/Unlocks/Dungeons/1133_Gilding the Bilious (Immortal Flames).json @@ -71,7 +71,8 @@ { "TerritoryId": 155, "InteractionType": "Duty", - "ContentFinderConditionId": 5 + "ContentFinderConditionId": 5, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/Aether Currents/Coerthas Western Highlands/2111_For All the Nights to Come.json b/QuestPaths/3.x - Heavensward/Aether Currents/Coerthas Western Highlands/2111_For All the Nights to Come.json index 5c9fa391..5961d3e1 100644 --- a/QuestPaths/3.x - Heavensward/Aether Currents/Coerthas Western Highlands/2111_For All the Nights to Come.json +++ b/QuestPaths/3.x - Heavensward/Aether Currents/Coerthas Western Highlands/2111_For All the Nights to Come.json @@ -38,7 +38,8 @@ { "TerritoryId": 397, "InteractionType": "Duty", - "ContentFinderConditionId": 36 + "ContentFinderConditionId": 36, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.2-The Dravanian Forelands/1617_Mourn in Passing.json b/QuestPaths/3.x - Heavensward/MSQ/A3.2-The Dravanian Forelands/1617_Mourn in Passing.json index 78abed7d..a321c741 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.2-The Dravanian Forelands/1617_Mourn in Passing.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.2-The Dravanian Forelands/1617_Mourn in Passing.json @@ -78,7 +78,8 @@ { "TerritoryId": 398, "InteractionType": "Duty", - "ContentFinderConditionId": 37 + "ContentFinderConditionId": 37, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1634_Into the Aery.json b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1634_Into the Aery.json index 471a1666..02098cab 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1634_Into the Aery.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1634_Into the Aery.json @@ -42,7 +42,8 @@ { "TerritoryId": 418, "InteractionType": "Duty", - "ContentFinderConditionId": 39 + "ContentFinderConditionId": 39, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1640_A Knight's Calling.json b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1640_A Knight's Calling.json index 492a0d58..9165e2a0 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1640_A Knight's Calling.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1640_A Knight's Calling.json @@ -59,7 +59,8 @@ { "TerritoryId": 419, "InteractionType": "Duty", - "ContentFinderConditionId": 34 + "ContentFinderConditionId": 34, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1660_Forbidden Knowledge.json b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1660_Forbidden Knowledge.json index cf7fe15e..dab4f3da 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1660_Forbidden Knowledge.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1660_Forbidden Knowledge.json @@ -110,7 +110,8 @@ { "TerritoryId": 399, "InteractionType": "Duty", - "ContentFinderConditionId": 31 + "ContentFinderConditionId": 31, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1669_Heavensward.json b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1669_Heavensward.json index 6b6bd71c..7bef51cc 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1669_Heavensward.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1669_Heavensward.json @@ -62,7 +62,8 @@ { "TerritoryId": 402, "InteractionType": "Duty", - "ContentFinderConditionId": 38 + "ContentFinderConditionId": 38, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/C-3.2/2232_The Word of the Mother.json b/QuestPaths/3.x - Heavensward/MSQ/C-3.2/2232_The Word of the Mother.json index 8cb385f4..fe2c3367 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/C-3.2/2232_The Word of the Mother.json +++ b/QuestPaths/3.x - Heavensward/MSQ/C-3.2/2232_The Word of the Mother.json @@ -77,7 +77,8 @@ { "TerritoryId": 463, "InteractionType": "Duty", - "ContentFinderConditionId": 141 + "ContentFinderConditionId": 141, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/E-3.4/2342_Shadows of the First.json b/QuestPaths/3.x - Heavensward/MSQ/E-3.4/2342_Shadows of the First.json index a2bb1f45..c3d9d084 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/E-3.4/2342_Shadows of the First.json +++ b/QuestPaths/3.x - Heavensward/MSQ/E-3.4/2342_Shadows of the First.json @@ -57,7 +57,8 @@ { "TerritoryId": 155, "InteractionType": "Duty", - "ContentFinderConditionId": 182 + "ContentFinderConditionId": 182, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/F-3.5/2354_Griffin, Griffin on the Wall.json b/QuestPaths/3.x - Heavensward/MSQ/F-3.5/2354_Griffin, Griffin on the Wall.json index feba4bf0..42aef1f0 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/F-3.5/2354_Griffin, Griffin on the Wall.json +++ b/QuestPaths/3.x - Heavensward/MSQ/F-3.5/2354_Griffin, Griffin on the Wall.json @@ -109,7 +109,8 @@ { "TerritoryId": 152, "InteractionType": "Duty", - "ContentFinderConditionId": 219 + "ContentFinderConditionId": 219, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/4.x - Stormblood/MSQ/A1.3-Rhalgr's Reach 2/2469_Not without Incident.json b/QuestPaths/4.x - Stormblood/MSQ/A1.3-Rhalgr's Reach 2/2469_Not without Incident.json index 214a764a..6477857e 100644 --- a/QuestPaths/4.x - Stormblood/MSQ/A1.3-Rhalgr's Reach 2/2469_Not without Incident.json +++ b/QuestPaths/4.x - Stormblood/MSQ/A1.3-Rhalgr's Reach 2/2469_Not without Incident.json @@ -87,7 +87,8 @@ { "TerritoryId": 680, "InteractionType": "Duty", - "ContentFinderConditionId": 238 + "ContentFinderConditionId": 238, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/4.x - Stormblood/MSQ/A5-Yanxia 2/2524_The Die Is Cast.json b/QuestPaths/4.x - Stormblood/MSQ/A5-Yanxia 2/2524_The Die Is Cast.json index 2811433e..702737b3 100644 --- a/QuestPaths/4.x - Stormblood/MSQ/A5-Yanxia 2/2524_The Die Is Cast.json +++ b/QuestPaths/4.x - Stormblood/MSQ/A5-Yanxia 2/2524_The Die Is Cast.json @@ -114,7 +114,8 @@ { "TerritoryId": 614, "InteractionType": "Duty", - "ContentFinderConditionId": 241 + "ContentFinderConditionId": 241, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/4.x - Stormblood/MSQ/A6.2-Peaks 2/2544_The Price of Freedom.json b/QuestPaths/4.x - Stormblood/MSQ/A6.2-Peaks 2/2544_The Price of Freedom.json index 3cdd9298..a0b37e35 100644 --- a/QuestPaths/4.x - Stormblood/MSQ/A6.2-Peaks 2/2544_The Price of Freedom.json +++ b/QuestPaths/4.x - Stormblood/MSQ/A6.2-Peaks 2/2544_The Price of Freedom.json @@ -114,7 +114,8 @@ { "TerritoryId": 620, "InteractionType": "Duty", - "ContentFinderConditionId": 242 + "ContentFinderConditionId": 242, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/4.x - Stormblood/MSQ/B-4.1/2964_The Mad King's Trove.json b/QuestPaths/4.x - Stormblood/MSQ/B-4.1/2964_The Mad King's Trove.json index d23fe19c..0856bbe6 100644 --- a/QuestPaths/4.x - Stormblood/MSQ/B-4.1/2964_The Mad King's Trove.json +++ b/QuestPaths/4.x - Stormblood/MSQ/B-4.1/2964_The Mad King's Trove.json @@ -98,7 +98,8 @@ { "TerritoryId": 621, "InteractionType": "Duty", - "ContentFinderConditionId": 279 + "ContentFinderConditionId": 279, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/4.x - Stormblood/MSQ/E-4.4/3144_Feel the Burn.json b/QuestPaths/4.x - Stormblood/MSQ/E-4.4/3144_Feel the Burn.json index 25ec9718..fba0763d 100644 --- a/QuestPaths/4.x - Stormblood/MSQ/E-4.4/3144_Feel the Burn.json +++ b/QuestPaths/4.x - Stormblood/MSQ/E-4.4/3144_Feel the Burn.json @@ -40,7 +40,8 @@ { "TerritoryId": 614, "InteractionType": "Duty", - "ContentFinderConditionId": 585 + "ContentFinderConditionId": 585, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/4.x - Stormblood/MSQ/F-4.5/3183_The Face of War.json b/QuestPaths/4.x - Stormblood/MSQ/F-4.5/3183_The Face of War.json index c32f5c02..fb9960de 100644 --- a/QuestPaths/4.x - Stormblood/MSQ/F-4.5/3183_The Face of War.json +++ b/QuestPaths/4.x - Stormblood/MSQ/F-4.5/3183_The Face of War.json @@ -27,7 +27,8 @@ { "TerritoryId": 829, "InteractionType": "Duty", - "ContentFinderConditionId": 611 + "ContentFinderConditionId": 611, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/A4-Crystarium 2/3300_The Lightwardens.json b/QuestPaths/5.x - Shadowbringers/MSQ/A4-Crystarium 2/3300_The Lightwardens.json index d4eef79d..7f9a5a94 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/A4-Crystarium 2/3300_The Lightwardens.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/A4-Crystarium 2/3300_The Lightwardens.json @@ -120,7 +120,8 @@ { "TerritoryId": 813, "InteractionType": "Duty", - "ContentFinderConditionId": 676 + "ContentFinderConditionId": 676, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/B-Il Mheg/3312_The Key to the Castle.json b/QuestPaths/5.x - Shadowbringers/MSQ/B-Il Mheg/3312_The Key to the Castle.json index 8f5193b0..d1dfe7dc 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/B-Il Mheg/3312_The Key to the Castle.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/B-Il Mheg/3312_The Key to the Castle.json @@ -49,7 +49,8 @@ { "TerritoryId": 816, "InteractionType": "Duty", - "ContentFinderConditionId": 649 + "ContentFinderConditionId": 649, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/C-Rak'tika/3340_The Burden of Knowledge.json b/QuestPaths/5.x - Shadowbringers/MSQ/C-Rak'tika/3340_The Burden of Knowledge.json index 075dff0c..39284d2f 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/C-Rak'tika/3340_The Burden of Knowledge.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/C-Rak'tika/3340_The Burden of Knowledge.json @@ -61,7 +61,8 @@ { "TerritoryId": 817, "InteractionType": "Duty", - "ContentFinderConditionId": 651 + "ContentFinderConditionId": 651, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/E-Kholusia 2/3643_Extinguishing the Last Light.json b/QuestPaths/5.x - Shadowbringers/MSQ/E-Kholusia 2/3643_Extinguishing the Last Light.json index 9ee12fd7..80cbd172 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/E-Kholusia 2/3643_Extinguishing the Last Light.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/E-Kholusia 2/3643_Extinguishing the Last Light.json @@ -62,7 +62,8 @@ { "TerritoryId": 814, "InteractionType": "Duty", - "ContentFinderConditionId": 659 + "ContentFinderConditionId": 659, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json index c947dc96..41dd8315 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3769_Beneath the Surface.json @@ -40,7 +40,8 @@ { "TerritoryId": 814, "InteractionType": "Duty", - "ContentFinderConditionId": 714 + "ContentFinderConditionId": 714, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json b/QuestPaths/6.x - Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json index 22f2d97a..c4336959 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json +++ b/QuestPaths/6.x - Endwalker/MSQ/A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json @@ -55,7 +55,8 @@ { "TerritoryId": 957, "InteractionType": "Duty", - "ContentFinderConditionId": 783 + "ContentFinderConditionId": 783, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/D-Thavnair2/4409_Skies Aflame.json b/QuestPaths/6.x - Endwalker/MSQ/D-Thavnair2/4409_Skies Aflame.json index 37285324..9c0d3a48 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/D-Thavnair2/4409_Skies Aflame.json +++ b/QuestPaths/6.x - Endwalker/MSQ/D-Thavnair2/4409_Skies Aflame.json @@ -71,7 +71,8 @@ { "TerritoryId": 957, "InteractionType": "Duty", - "ContentFinderConditionId": 789 + "ContentFinderConditionId": 789, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/E-Elpis/4437_Caging the Messenger.json b/QuestPaths/6.x - Endwalker/MSQ/E-Elpis/4437_Caging the Messenger.json index 0a7e8e55..9dd680eb 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/E-Elpis/4437_Caging the Messenger.json +++ b/QuestPaths/6.x - Endwalker/MSQ/E-Elpis/4437_Caging the Messenger.json @@ -39,7 +39,8 @@ { "TerritoryId": 961, "InteractionType": "Duty", - "ContentFinderConditionId": 787 + "ContentFinderConditionId": 787, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/F-Labyrinthos2/4449_Her Children One and All.json b/QuestPaths/6.x - Endwalker/MSQ/F-Labyrinthos2/4449_Her Children One and All.json index 5294bab4..f41bf955 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/F-Labyrinthos2/4449_Her Children One and All.json +++ b/QuestPaths/6.x - Endwalker/MSQ/F-Labyrinthos2/4449_Her Children One and All.json @@ -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 } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/H-6.1/4529_Alzadaals Legacy.json b/QuestPaths/6.x - Endwalker/MSQ/H-6.1/4529_Alzadaals Legacy.json index c79468bb..dc9324d1 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/H-6.1/4529_Alzadaals Legacy.json +++ b/QuestPaths/6.x - Endwalker/MSQ/H-6.1/4529_Alzadaals Legacy.json @@ -23,7 +23,8 @@ { "TerritoryId": 957, "InteractionType": "Duty", - "ContentFinderConditionId": 844 + "ContentFinderConditionId": 844, + "AutoDutyEnabled": false } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/I-6.2/4592_In Search of Azdaja.json b/QuestPaths/6.x - Endwalker/MSQ/I-6.2/4592_In Search of Azdaja.json index cc6ea1fa..c91af597 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/I-6.2/4592_In Search of Azdaja.json +++ b/QuestPaths/6.x - Endwalker/MSQ/I-6.2/4592_In Search of Azdaja.json @@ -71,7 +71,8 @@ { "TerritoryId": 1056, "InteractionType": "Duty", - "ContentFinderConditionId": 869 + "ContentFinderConditionId": 869, + "AutoDutyEnabled": false } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/J-6.3/4674_King of the Mountain.json b/QuestPaths/6.x - Endwalker/MSQ/J-6.3/4674_King of the Mountain.json index 130cb389..df1bd69f 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/J-6.3/4674_King of the Mountain.json +++ b/QuestPaths/6.x - Endwalker/MSQ/J-6.3/4674_King of the Mountain.json @@ -57,7 +57,8 @@ "TerritoryId": 958, "InteractionType": "Duty", "Comment": "Lapis Manalis", - "ContentFinderConditionId": 896 + "ContentFinderConditionId": 896, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/K-6.4/4736_Going Haam.json b/QuestPaths/6.x - Endwalker/MSQ/K-6.4/4736_Going Haam.json index 9f738bc8..8af36080 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/K-6.4/4736_Going Haam.json +++ b/QuestPaths/6.x - Endwalker/MSQ/K-6.4/4736_Going Haam.json @@ -160,7 +160,8 @@ { "TerritoryId": 962, "InteractionType": "Duty", - "ContentFinderConditionId": 822 + "ContentFinderConditionId": 822, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/6.x - Endwalker/MSQ/L-6.5/4748_Down in the Dark.json b/QuestPaths/6.x - Endwalker/MSQ/L-6.5/4748_Down in the Dark.json index 3930a8dd..96953ea2 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/L-6.5/4748_Down in the Dark.json +++ b/QuestPaths/6.x - Endwalker/MSQ/L-6.5/4748_Down in the Dark.json @@ -24,7 +24,8 @@ { "TerritoryId": 1162, "InteractionType": "Duty", - "ContentFinderConditionId": 823 + "ContentFinderConditionId": 823, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/A-Kozama'uka1-Urqopacha1/4879_For All Turali.json b/QuestPaths/7.x - Dawntrail/MSQ/A-Kozama'uka1-Urqopacha1/4879_For All Turali.json index 28e35590..28a6850c 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/A-Kozama'uka1-Urqopacha1/4879_For All Turali.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/A-Kozama'uka1-Urqopacha1/4879_For All Turali.json @@ -58,7 +58,8 @@ { "TerritoryId": 1185, "InteractionType": "Duty", - "ContentFinderConditionId": 826 + "ContentFinderConditionId": 826, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/B-Kozama'uka2-Urqopacha2/4891_The High Luminary.json b/QuestPaths/7.x - Dawntrail/MSQ/B-Kozama'uka2-Urqopacha2/4891_The High Luminary.json index 8b1a2021..2ef3a8e6 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/B-Kozama'uka2-Urqopacha2/4891_The High Luminary.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/B-Kozama'uka2-Urqopacha2/4891_The High Luminary.json @@ -23,7 +23,8 @@ { "TerritoryId": 1187, "InteractionType": "Duty", - "ContentFinderConditionId": 824 + "ContentFinderConditionId": 824, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/C-Yak T'el/4909_Road to the Golden City.json b/QuestPaths/7.x - Dawntrail/MSQ/C-Yak T'el/4909_Road to the Golden City.json index 384623ce..528fde3f 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/C-Yak T'el/4909_Road to the Golden City.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/C-Yak T'el/4909_Road to the Golden City.json @@ -115,7 +115,8 @@ { "TerritoryId": 1189, "InteractionType": "Duty", - "ContentFinderConditionId": 829 + "ContentFinderConditionId": 829, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/D-Shaaloani-HeritageFound1/4926_All Aboard.json b/QuestPaths/7.x - Dawntrail/MSQ/D-Shaaloani-HeritageFound1/4926_All Aboard.json index 51a11987..682576cb 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/D-Shaaloani-HeritageFound1/4926_All Aboard.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/D-Shaaloani-HeritageFound1/4926_All Aboard.json @@ -60,7 +60,8 @@ { "TerritoryId": 1219, "InteractionType": "Duty", - "ContentFinderConditionId": 831 + "ContentFinderConditionId": 831, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/E-SolutionNine-HeritageFound2/4945_The Resilient Son.json b/QuestPaths/7.x - Dawntrail/MSQ/E-SolutionNine-HeritageFound2/4945_The Resilient Son.json index 8e967708..15817e8d 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/E-SolutionNine-HeritageFound2/4945_The Resilient Son.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/E-SolutionNine-HeritageFound2/4945_The Resilient Son.json @@ -39,7 +39,8 @@ { "TerritoryId": 1191, "InteractionType": "Duty", - "ContentFinderConditionId": 825 + "ContentFinderConditionId": 825, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/F-Living Memory/4959_Dawntrail.json b/QuestPaths/7.x - Dawntrail/MSQ/F-Living Memory/4959_Dawntrail.json index 778f11db..cafc8975 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/F-Living Memory/4959_Dawntrail.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/F-Living Memory/4959_Dawntrail.json @@ -56,7 +56,8 @@ { "TerritoryId": 1192, "InteractionType": "Duty", - "ContentFinderConditionId": 827 + "ContentFinderConditionId": 827, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/7.x - Dawntrail/MSQ/G-7.1/5246_In Search of the Past.json b/QuestPaths/7.x - Dawntrail/MSQ/G-7.1/5246_In Search of the Past.json index 6d0878d1..40f6d44d 100644 --- a/QuestPaths/7.x - Dawntrail/MSQ/G-7.1/5246_In Search of the Past.json +++ b/QuestPaths/7.x - Dawntrail/MSQ/G-7.1/5246_In Search of the Past.json @@ -122,7 +122,8 @@ { "TerritoryId": 1191, "InteractionType": "Duty", - "ContentFinderConditionId": 1008 + "ContentFinderConditionId": 1008, + "AutoDutyEnabled": true } ] }, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index cb035ef7..eab9789a 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1217,6 +1217,9 @@ "exclusiveMinimum": 0, "exclusiveMaximum": 3000 }, + "AutoDutyEnabled": { + "type": "boolean" + }, "DataId": { "type": "null" }, diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index 0b4a05a2..bd1ce304 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -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?> RequiredQuestVariables { get; set; } = new(); diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index b4eb5a5b..90c42bb5 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -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 WhitelistedDutyCfcIds { get; set; } = []; + public HashSet BlacklistedDutyCfcIds { get; set; } = []; + } + internal sealed class NotificationConfiguration { public bool Enabled { get; set; } = true; diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index c948abe2..d6641073 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -29,7 +29,8 @@ internal sealed class QuestRegistry private readonly LeveData _leveData; private readonly ICallGateProvider _reloadDataIpc; - private readonly Dictionary _quests = new(); + private readonly Dictionary _quests = []; + private readonly Dictionary _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; + } } diff --git a/Questionable/Controller/Steps/Common/SendNotification.cs b/Questionable/Controller/Steps/Common/SendNotification.cs index 7334ca07..e83a1186 100644 --- a/Questionable/Controller/Steps/Common/SendNotification.cs +++ b/Questionable/Controller/Steps/Common/SendNotification.cs @@ -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), diff --git a/Questionable/Controller/Steps/Interactions/Duty.cs b/Questionable/Controller/Steps/Interactions/Duty.cs index 42c94d87..fab5c6ed 100644 --- a/Questionable/Controller/Steps/Interactions/Duty.cs +++ b/Questionable/Controller/Steps/Interactions/Duty.cs @@ -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 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 Task(uint ContentFinderConditionId) : ITask + internal sealed record StartAutoDutyTask(uint ContentFinderConditionId) : ITask + { + public override string ToString() => $"StartAutoDuty({ContentFinderConditionId})"; + } + + internal sealed class StartAutoDutyExecutor( + AutoDutyIpc autoDutyIpc, + TerritoryData territoryData, + IClientState clientState) : TaskExecutor + { + 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 + { + 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 OpenDutyFinderTask(uint ContentFinderConditionId) : ITask { public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})"; } - internal sealed class OpenDutyWindowExecutor( + internal sealed class OpenDutyFinderExecutor( GameFunctions gameFunctions, - ICondition condition) : TaskExecutor + ICondition condition) : TaskExecutor { protected override bool Start() { diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index d64c009b..0b3a02ba 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -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 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()]; diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs index ee91f6b4..970718b2 100644 --- a/Questionable/Data/TerritoryData.cs +++ b/Questionable/Data/TerritoryData.cs @@ -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 _dutyTerritories; private readonly ImmutableDictionary _instanceNames; private readonly ImmutableDictionary _contentFinderConditionNames; + private readonly ImmutableDictionary _contentFinderConditionIds; public TerritoryData(IDataManager dataManager) { @@ -40,11 +44,14 @@ internal sealed class TerritoryData _instanceNames = dataManager.GetExcelSheet() .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() .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() + .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 index 00000000..1089efcc --- /dev/null +++ b/Questionable/External/AutoDutyIpc.cs @@ -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 _logger; + private readonly ICallGateSubscriber _contentHasPath; + private readonly ICallGateSubscriber _run; + private readonly ICallGateSubscriber _isStopped; + + public AutoDutyIpc(IDalamudPluginInterface pluginInterface, Configuration configuration, TerritoryData territoryData, ILogger logger) + { + _configuration = configuration; + _territoryData = territoryData; + _logger = logger; + _contentHasPath = pluginInterface.GetIpcSubscriber("AutoDuty.ContentHasPath"); + _run = pluginInterface.GetIpcSubscriber("AutoDuty.Run"); + _isStopped = pluginInterface.GetIpcSubscriber("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; + } + } +} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 5964eb45..1072a54f 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -129,6 +129,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } private static void AddTaskFactories(ServiceCollection serviceCollection) @@ -178,7 +179,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin .AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactoryAndExecutor(); - serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskFactory(); serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskExecutor(); diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index f4414084..eaf8f622 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -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> _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() .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() + .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()) + { + 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(); + + /// + /// The default implementation for throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text. + /// + 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); }