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