--- /dev/null
+{
+ "$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
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
"Z": -8.316223
},
"TerritoryId": 1185,
- "InteractionType": "CompleteQuest"
+ "InteractionType": "CompleteQuest",
+ "NextQuestId": 4990
}
]
}
{
"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": {
"Z": -5.6916504
},
"TerritoryId": 1185,
- "InteractionType": "CompleteQuest"
+ "InteractionType": "CompleteQuest",
+ "NextQuestId": 4991
}
]
}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+using System;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.Logging;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class CraftworksSupplyController : IDisposable
+{
+ private readonly QuestController _questController;
+ private readonly IAddonLifecycle _addonLifecycle;
+ private readonly IGameGui _gameGui;
+ private readonly IFramework _framework;
+ private readonly ILogger<CraftworksSupplyController> _logger;
+
+ public CraftworksSupplyController(QuestController questController, IAddonLifecycle addonLifecycle,
+ IGameGui gameGui, IFramework framework, ILogger<CraftworksSupplyController> logger)
+ {
+ _questController = questController;
+ _addonLifecycle = addonLifecycle;
+ _gameGui = gameGui;
+ _framework = framework;
+ _logger = logger;
+
+ _addonLifecycle.RegisterListener(AddonEvent.PostReceiveEvent, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "BankaCraftworksSupply",
+ BankaCraftworksSupplyPostUpdate);
+ }
+
+ private bool ShouldHandleUiInteractions => _questController.IsRunning;
+
+ private unsafe void BankaCraftworksSupplyPostUpdate(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ InteractWithBankaCraftworksSupply(addon);
+ }
+
+ private unsafe void InteractWithBankaCraftworksSupply()
+ {
+ if (_gameGui.TryGetAddonByName("BankaCraftworksSupply", out AtkUnitBase* addon))
+ InteractWithBankaCraftworksSupply(addon);
+ }
+
+ private unsafe void InteractWithBankaCraftworksSupply(AtkUnitBase* addon)
+ {
+ AtkValue* atkValues = addon->AtkValues;
+
+ uint completedCount = atkValues[7].UInt;
+ uint missingCount = 6 - completedCount;
+ for (int slot = 0; slot < missingCount; ++slot)
+ {
+ if (atkValues[31 + slot].UInt != 0)
+ continue;
+
+ _logger.LogInformation("Selecting an item for slot {Slot}", slot);
+ var selectSlot = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 2 },
+ new() { Type = ValueType.Int, Int = slot /* slot */ },
+ };
+ addon->FireCallback(2, selectSlot);
+ return;
+ }
+
+ // do turn-in if any item is provided
+ if (atkValues[31].UInt != 0)
+ {
+ _logger.LogInformation("Confirming turn-in");
+ addon->FireCallbackInt(0);
+ }
+ }
+
+ // FIXME: This seems to not work if the mouse isn't over the FFXIV window?
+ private unsafe void ContextIconMenuPostReceiveEvent(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ AddonContextIconMenu* addonContextIconMenu = (AddonContextIconMenu*)args.Addon;
+ if (!addonContextIconMenu->IsVisible)
+ return;
+
+ ushort parentId = addonContextIconMenu->ContextMenuParentId;
+ if (parentId == 0)
+ return;
+
+ AtkUnitBase* parentAddon = AtkStage.Instance()->RaptureAtkUnitManager->GetAddonById(parentId);
+ if (parentAddon->NameString is "BankaCraftworksSupply")
+ {
+ _logger.LogInformation("Picking item for {AddonName}", parentAddon->NameString);
+ var selectSlot = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 0 },
+ new() { Type = ValueType.Int, Int = 0 /* slot */ },
+ new() { Type = ValueType.UInt, UInt = 20802 /* probably the item's icon */ },
+ new() { Type = ValueType.UInt, UInt = 0 },
+ new() { Type = 0, Int = 0 },
+ };
+ addonContextIconMenu->FireCallback(5, selectSlot);
+ addonContextIconMenu->Close(true);
+
+ if (parentAddon->NameString == "BankaCraftworksSupply")
+ _framework.RunOnTick(InteractWithBankaCraftworksSupply, TimeSpan.FromMilliseconds(50));
+ }
+ else
+ _logger.LogTrace("Ignoring contextmenu event for {AddonName}", parentAddon->NameString);
+ }
+
+ public void Dispose()
+ {
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "BankaCraftworksSupply",
+ BankaCraftworksSupplyPostUpdate);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostReceiveEvent, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
+ }
+}
--- /dev/null
+using System;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using Microsoft.Extensions.Logging;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class CreditsController : IDisposable
+{
+ private readonly IAddonLifecycle _addonLifecycle;
+ private readonly ILogger<CreditsController> _logger;
+
+ public CreditsController(IAddonLifecycle addonLifecycle, ILogger<CreditsController> logger)
+ {
+ _addonLifecycle = addonLifecycle;
+ _logger = logger;
+
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
+ }
+
+
+ /// <summary>
+ /// ARR Credits.
+ /// </summary>
+ private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args)
+ {
+ _logger.LogInformation("Closing Credits sequence");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(-2);
+ }
+
+ /// <summary>
+ /// Credits for (possibly all?) expansions, not used for ARR.
+ /// </summary>
+ private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
+ {
+ _logger.LogInformation("Closing Credits sequence");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(-2);
+ }
+
+ private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args)
+ {
+ _logger.LogInformation("Closing CreditPlayer");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->Close(true);
+ }
+
+ public void Dispose()
+ {
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
+ }
+}
--- /dev/null
+using System;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using Microsoft.Extensions.Logging;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class HelpUiController : IDisposable
+{
+ private readonly QuestController _questController;
+ private readonly IAddonLifecycle _addonLifecycle;
+ private readonly ILogger<HelpUiController> _logger;
+
+ public HelpUiController(QuestController questController, IAddonLifecycle addonLifecycle, ILogger<HelpUiController> logger)
+ {
+ _questController = questController;
+ _addonLifecycle = addonLifecycle;
+ _logger = logger;
+
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
+ }
+
+ private bool ShouldHandleUiInteractions => _questController.IsRunning;
+
+ private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ if (_questController.StartedQuest?.Quest.Id.Value == 4526)
+ {
+ _logger.LogInformation("Closing Unending Codex");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(-2);
+ }
+ }
+
+ private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ if (_questController.StartedQuest?.Quest.Id.Value == 245)
+ {
+ _logger.LogInformation("Closing ContentsTutorial");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(13);
+ }
+ }
+
+ /// <summary>
+ /// Opened e.g. the first time you open the duty finder window during Sastasha.
+ /// </summary>
+ private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ if (_questController.StartedQuest?.Quest.Id.Value == 245)
+ {
+ _logger.LogInformation("Closing MultipleHelpWindow");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(-2);
+ addon->FireCallbackInt(-1);
+ }
+ }
+
+ public void Dispose()
+ {
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.Event;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib;
+using LLib.GameData;
+using LLib.GameUI;
+using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.Interactions;
+using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model;
+using Questionable.Model.Common;
+using Questionable.Model.Gathering;
+using Questionable.Model.Questing;
+using AethernetShortcut = Questionable.Controller.Steps.Shared.AethernetShortcut;
+using EAetheryteLocationExtensions = Questionable.Model.Common.EAetheryteLocationExtensions;
+using Quest = Questionable.Model.Quest;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class InteractionUiController : IDisposable
+{
+ private readonly IAddonLifecycle _addonLifecycle;
+ private readonly IDataManager _dataManager;
+ private readonly QuestFunctions _questFunctions;
+ private readonly AetheryteFunctions _aetheryteFunctions;
+ private readonly ExcelFunctions _excelFunctions;
+ private readonly QuestController _questController;
+ private readonly GatheringData _gatheringData;
+ private readonly GatheringPointRegistry _gatheringPointRegistry;
+ private readonly QuestRegistry _questRegistry;
+ private readonly QuestData _questData;
+ private readonly IGameGui _gameGui;
+ private readonly ITargetManager _targetManager;
+ private readonly IClientState _clientState;
+ private readonly ILogger<InteractionUiController> _logger;
+ private readonly Regex _returnRegex;
+
+ private bool _isInitialCheck;
+
+ public InteractionUiController(
+ IAddonLifecycle addonLifecycle,
+ IDataManager dataManager,
+ QuestFunctions questFunctions,
+ AetheryteFunctions aetheryteFunctions,
+ ExcelFunctions excelFunctions,
+ QuestController questController,
+ GatheringData gatheringData,
+ GatheringPointRegistry gatheringPointRegistry,
+ QuestRegistry questRegistry,
+ QuestData questData,
+ IGameGui gameGui,
+ ITargetManager targetManager,
+ IFramework framework,
+ IPluginLog pluginLog,
+ IClientState clientState,
+ ILogger<InteractionUiController> logger)
+ {
+ _addonLifecycle = addonLifecycle;
+ _dataManager = dataManager;
+ _questFunctions = questFunctions;
+ _aetheryteFunctions = aetheryteFunctions;
+ _excelFunctions = excelFunctions;
+ _questController = questController;
+ _gatheringData = gatheringData;
+ _gatheringPointRegistry = gatheringPointRegistry;
+ _questRegistry = questRegistry;
+ _questData = questData;
+ _gameGui = gameGui;
+ _targetManager = targetManager;
+ _clientState = clientState;
+ _logger = logger;
+
+ _returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
+
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
+ }
+
+ private bool ShouldHandleUiInteractions => _isInitialCheck || _questController.IsRunning;
+
+ internal unsafe void HandleCurrentDialogueChoices()
+ {
+ try
+ {
+ _isInitialCheck = true;
+ if (_gameGui.TryGetAddonByName("SelectString", out AddonSelectString* addonSelectString))
+ {
+ _logger.LogInformation("SelectString window is open");
+ SelectStringPostSetup(addonSelectString, true);
+ }
+
+ if (_gameGui.TryGetAddonByName("CutSceneSelectString",
+ out AddonCutSceneSelectString* addonCutSceneSelectString))
+ {
+ _logger.LogInformation("CutSceneSelectString window is open");
+ CutsceneSelectStringPostSetup(addonCutSceneSelectString, true);
+ }
+
+ if (_gameGui.TryGetAddonByName("SelectIconString", out AddonSelectIconString* addonSelectIconString))
+ {
+ _logger.LogInformation("SelectIconString window is open");
+ SelectIconStringPostSetup(addonSelectIconString, true);
+ }
+
+ if (_gameGui.TryGetAddonByName("SelectYesno", out AddonSelectYesno* addonSelectYesno))
+ {
+ _logger.LogInformation("SelectYesno window is open");
+ SelectYesnoPostSetup(addonSelectYesno, true);
+ }
+
+ if (_gameGui.TryGetAddonByName("PointMenu", out AtkUnitBase* addonPointMenu))
+ {
+ _logger.LogInformation("PointMenu is open");
+ PointMenuPostSetup(addonPointMenu);
+ }
+ }
+ finally
+ {
+ _isInitialCheck = false;
+ }
+ }
+
+ private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
+ SelectStringPostSetup(addonSelectString, false);
+ }
+
+ private unsafe void SelectStringPostSetup(AddonSelectString* addonSelectString, bool checkAllSteps)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ string? actualPrompt = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
+ if (actualPrompt == null)
+ return;
+
+ List<string?> answers = new();
+ for (ushort i = 7; i < addonSelectString->AtkUnitBase.AtkValuesCount; ++i)
+ {
+ if (addonSelectString->AtkUnitBase.AtkValues[i].Type == ValueType.String)
+ answers.Add(addonSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
+ }
+
+ int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
+ if (answer != null)
+ addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
+ }
+
+ private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonCutSceneSelectString* addonCutSceneSelectString = (AddonCutSceneSelectString*)args.Addon;
+ CutsceneSelectStringPostSetup(addonCutSceneSelectString, false);
+ }
+
+ private unsafe void CutsceneSelectStringPostSetup(AddonCutSceneSelectString* addonCutSceneSelectString,
+ bool checkAllSteps)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ string? actualPrompt = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
+ if (actualPrompt == null)
+ return;
+
+ List<string?> answers = new();
+ for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; ++i)
+ answers.Add(addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
+
+ int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
+ if (answer != null)
+ addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
+ }
+
+ private unsafe void SelectIconStringPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonSelectIconString* addonSelectIconString = (AddonSelectIconString*)args.Addon;
+ SelectIconStringPostSetup(addonSelectIconString, false);
+ }
+
+ [SuppressMessage("ReSharper", "RedundantJumpStatement")]
+ private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ string? actualPrompt = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString();
+ if (string.IsNullOrEmpty(actualPrompt))
+ actualPrompt = null;
+
+ var answers = GetChoices(addonSelectIconString);
+ int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
+ if (answer != null)
+ {
+ addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
+ return;
+ }
+
+ // this is 'Daily Quests' for tribal quests, but not set for normal selections
+ string? title = addonSelectIconString->AtkValues[0].ReadAtkString();
+
+ var currentQuest = _questController.StartedQuest;
+ if (currentQuest != null && (actualPrompt == null || title != null))
+ {
+ _logger.LogInformation("Checking if current quest {Name} is on the list", currentQuest.Quest.Info.Name);
+ if (CheckQuestSelection(addonSelectIconString, currentQuest.Quest, answers))
+ return;
+ }
+
+ var nextQuest = _questController.NextQuest;
+ if (nextQuest != null && (actualPrompt == null || title != null))
+ {
+ _logger.LogInformation("Checking if next quest {Name} is on the list", nextQuest.Quest.Info.Name);
+ if (CheckQuestSelection(addonSelectIconString, nextQuest.Quest, answers))
+ return;
+ }
+ }
+
+ private unsafe bool CheckQuestSelection(AddonSelectIconString* addonSelectIconString, Quest quest,
+ List<string?> answers)
+ {
+ // it is possible for this to be a quest selection
+ string questName = quest.Info.Name;
+ int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
+ if (questSelection >= 0)
+ {
+ addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
+ return true;
+ }
+
+ return false;
+ }
+
+ public static unsafe List<string?> GetChoices(AddonSelectIconString* addonSelectIconString)
+ {
+ List<string?> answers = new();
+ for (ushort i = 0; i < addonSelectIconString->AtkUnitBase.AtkValues[5].Int; i++)
+ answers.Add(addonSelectIconString->AtkValues[i * 3 + 7].ReadAtkString());
+
+ return answers;
+ }
+
+ private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
+ {
+ List<DialogueChoiceInfo> dialogueChoices = [];
+
+ // levequest choices have some vague sort of priority
+ if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
+ interact.Quest != null &&
+ interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
+ {
+ if (interact.InteractionType == EInteractionType.AcceptLeve)
+ {
+ dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
+ new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = "leve/GuildleveAssignment",
+ Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+ Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_01"),
+ }));
+ interact.InteractionType = EInteractionType.None;
+ }
+ else if (interact.InteractionType == EInteractionType.CompleteLeve)
+ {
+ dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
+ new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = "leve/GuildleveAssignment",
+ Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+ Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_REWARD"),
+ }));
+ interact.InteractionType = EInteractionType.None;
+ }
+ }
+
+ var currentQuest = _questController.SimulatedQuest ??
+ _questController.GatheringQuest ??
+ _questController.StartedQuest;
+ if (currentQuest != null)
+ {
+ var quest = currentQuest.Quest;
+ if (checkAllSteps)
+ {
+ var sequence = quest.FindSequence(currentQuest.Sequence);
+ var choices = sequence?.Steps.SelectMany(x => x.DialogueChoices);
+ if (choices != null)
+ dialogueChoices.AddRange(choices.Select(x => new DialogueChoiceInfo(quest, x)));
+ }
+ else
+ {
+ var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+ if (step == null)
+ _logger.LogDebug("Ignoring current quest dialogue choices, no active step");
+ else
+ dialogueChoices.AddRange(step.DialogueChoices.Select(x => new DialogueChoiceInfo(quest, x)));
+ }
+
+ // add all travel dialogue choices
+ var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
+ if (targetTerritoryId != null)
+ {
+ foreach (string? answer in answers)
+ {
+ if (answer == null)
+ continue;
+
+ if (TryFindWarp(targetTerritoryId.Value, answer, out uint? warpId, out string? warpText))
+ {
+ _logger.LogInformation("Adding warp {Id}, {Prompt}", warpId, warpText);
+ dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = null,
+ Prompt = null,
+ Answer = ExcelRef.FromSheetValue(warpText),
+ }));
+ }
+ }
+ }
+ }
+ else
+ _logger.LogDebug("Ignoring current quest dialogue choices, no active quest");
+
+ // add all quests that start with the targeted npc
+ var target = _targetManager.Target;
+ if (target != null)
+ {
+ foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId).Where(x => x.QuestId is QuestId))
+ {
+ if (_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) &&
+ _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest))
+ {
+ var questChoices = knownQuest.FindSequence(0)?.Steps
+ .SelectMany(x => x.DialogueChoices)
+ .ToList();
+ if (questChoices != null && questChoices.Count > 0)
+ {
+ _logger.LogInformation("Adding {Count} dialogue choices from not accepted quest {QuestName}",
+ questChoices.Count, questInfo.Name);
+ dialogueChoices.AddRange(questChoices.Select(x => new DialogueChoiceInfo(knownQuest, x)));
+ }
+ }
+ }
+
+ if ((_questController.IsRunning || _questController.WasLastTaskUpdateWithin(TimeSpan.FromSeconds(5)))
+ && _questController.NextQuest == null)
+ {
+ // make sure to always close the leve dialogue
+ if (_questData.GetAllByIssuerDataId(target.DataId).Any(x => x.QuestId is LeveId))
+ {
+ _logger.LogInformation("Adding close leve dialogue as option");
+ dialogueChoices.Add(new DialogueChoiceInfo(null,
+ new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = "leve/GuildleveAssignment",
+ Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
+ Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_07"),
+ }));
+ }
+ }
+ }
+
+ if (dialogueChoices.Count == 0)
+ {
+ _logger.LogDebug("No dialogue choices to check");
+ return null;
+ }
+
+ foreach (var (quest, dialogueChoice) in dialogueChoices)
+ {
+ if (dialogueChoice.Type != EDialogChoiceType.List)
+ continue;
+
+ if (dialogueChoice.Answer == null)
+ {
+ _logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
+ continue;
+ }
+
+ if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
+ {
+ _logger.LogDebug(
+ "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
+ dialogueChoice.DataId, _targetManager.Target?.DataId);
+ continue;
+ }
+
+ string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
+ ?.GetString();
+ StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer,
+ dialogueChoice.AnswerIsRegularExpression);
+
+ if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
+ {
+ _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
+ continue;
+ }
+
+ if (actualPrompt != null &&
+ (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt)))
+ {
+ _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
+ excelPrompt, actualPrompt);
+ continue;
+ }
+
+ for (int i = 0; i < answers.Count; ++i)
+ {
+ _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
+ answers[i], excelAnswer);
+ if (IsMatch(answers[i], excelAnswer))
+ {
+ _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
+ i, answers[i], actualPrompt);
+
+ // ensure we only open the dialog once
+ if (quest?.Id is SatisfactionSupplyNpcId)
+ {
+ if (_questController.GatheringQuest == null ||
+ _questController.GatheringQuest.Sequence == 255)
+ return null;
+
+ _questController.GatheringQuest.SetSequence(1);
+ _questController.StartSingleQuest("SatisfactionSupply turn in");
+ }
+
+ return i;
+ }
+ }
+ }
+
+ _logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
+ return null;
+ }
+
+ private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
+ {
+ if (actualAnswer == null && expectedAnswer == null)
+ return true;
+
+ if (actualAnswer == null || expectedAnswer == null)
+ return false;
+
+ return expectedAnswer.IsMatch(actualAnswer);
+ }
+
+ private int? HandleInstanceListChoice(string? actualPrompt)
+ {
+ string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString();
+ if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt))
+ {
+ _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
+ return 0; // any instance
+ }
+
+ return null;
+ }
+
+ private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonSelectYesno* addonSelectYesno = (AddonSelectYesno*)args.Addon;
+ SelectYesnoPostSetup(addonSelectYesno, false);
+ }
+
+ [SuppressMessage("ReSharper", "RedundantJumpStatement")]
+ private unsafe void SelectYesnoPostSetup(AddonSelectYesno* addonSelectYesno, bool checkAllSteps)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ string? actualPrompt = addonSelectYesno->AtkUnitBase.AtkValues[0].ReadAtkString();
+ if (actualPrompt == null)
+ return;
+
+ _logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
+ var director = UIState.Instance()->DirectorTodo.Director;
+ if (director != null && director->EventHandlerInfo != null &&
+ director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
+ director->Sequence == 254)
+ {
+ // just close the dialogue for 'do you want to return to next settlement', should prolly be different for
+ // ARR territories
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(1);
+ return;
+ }
+
+ var currentQuest = _questController.StartedQuest;
+ if (currentQuest != null && CheckQuestYesNo(addonSelectYesno, currentQuest, actualPrompt, checkAllSteps))
+ return;
+
+ var simulatedQuest = _questController.SimulatedQuest;
+ if (simulatedQuest != null && HandleTravelYesNo(addonSelectYesno, simulatedQuest, actualPrompt))
+ return;
+
+ var nextQuest = _questController.NextQuest;
+ if (nextQuest != null && CheckQuestYesNo(addonSelectYesno, nextQuest, actualPrompt, checkAllSteps))
+ return;
+
+ return;
+ }
+
+ private unsafe bool CheckQuestYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest,
+ string actualPrompt, bool checkAllSteps)
+ {
+ var quest = currentQuest.Quest;
+ if (checkAllSteps)
+ {
+ var sequence = quest.FindSequence(currentQuest.Sequence);
+ if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest,
+ sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt))
+ return true;
+ }
+ else
+ {
+ var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+ if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt))
+ return true;
+ }
+
+ if (currentQuest.Quest.Id is LeveId)
+ {
+ var dialogueChoice = new DialogueChoice
+ {
+ Type = EDialogChoiceType.YesNo,
+ ExcelSheet = "Addon",
+ Prompt = new ExcelRef(608),
+ Yes = true
+ };
+
+ if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
+ return true;
+ }
+
+ if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
+ return true;
+
+ return false;
+ }
+
+ private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
+ IList<DialogueChoice> dialogueChoices, string actualPrompt)
+ {
+ _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
+ foreach (var dialogueChoice in dialogueChoices)
+ {
+ if (dialogueChoice.Type != EDialogChoiceType.YesNo)
+ continue;
+
+ if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
+ {
+ _logger.LogDebug(
+ "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
+ dialogueChoice.DataId, _targetManager.Target?.DataId);
+ continue;
+ }
+
+ string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
+ ?.GetString();
+ if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))
+ {
+ _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
+ excelPrompt, actualPrompt);
+ continue;
+ }
+
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
+ return true;
+ }
+
+ return false;
+ }
+
+ private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
+ QuestController.QuestProgress currentQuest, string actualPrompt)
+ {
+ _logger.LogInformation("TravelYesNo");
+ if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
+ {
+ _logger.LogInformation("Automatically confirming return...");
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+ return true;
+ }
+
+ if (_questController.IsRunning && _gameGui.TryGetAddonByName("HousingSelectBlock", out AtkUnitBase* _))
+ {
+ _logger.LogInformation("Automatically confirming ward selection");
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+ return true;
+ }
+
+ var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
+ if (targetTerritoryId != null &&
+ TryFindWarp(targetTerritoryId.Value, actualPrompt, out uint? warpId, out string? warpText))
+ {
+ _logger.LogInformation("Using warp {Id}, {Prompt}", warpId, warpText);
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+ return true;
+ }
+
+ return false;
+ }
+
+ private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
+ {
+ // this can be triggered either manually (in which case we should increase the step counter), or automatically
+ // (in which case it is ~1 frame later, and the step counter has already been increased)
+ var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
+ if (sequence == null)
+ return null;
+
+ QuestStep? step = sequence.FindStep(currentQuest.Step);
+ if (step != null)
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}",
+ step.TerritoryId,
+ step.TargetTerritoryId);
+
+ if (step != null && (step.TerritoryId != _clientState.TerritoryType || step.TargetTerritoryId == null) &&
+ step.RequiredGatheredItems.Count > 0)
+ {
+ if (_gatheringData.TryGetGatheringPointId(step.RequiredGatheredItems[0].ItemId,
+ (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer,
+ out GatheringPointId? gatheringPointId) &&
+ _gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot? root))
+ {
+ foreach (var gatheringStep in root.Steps)
+ {
+ if (gatheringStep.TerritoryId == _clientState.TerritoryType &&
+ gatheringStep.TargetTerritoryId != null)
+ {
+ _logger.LogTrace(
+ "FindTargetTerritoryFromQuestStep (gathering): {CurrentTerritory}, {TargetTerritory}",
+ gatheringStep.TerritoryId,
+ gatheringStep.TargetTerritoryId);
+ return gatheringStep.TargetTerritoryId;
+ }
+ }
+ }
+ }
+
+ if (step == null || step.TargetTerritoryId == null)
+ {
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
+ step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
+
+ if (step != null)
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}",
+ step.TerritoryId,
+ step.TargetTerritoryId);
+ }
+
+ if (step == null || step.TargetTerritoryId == null)
+ {
+ _logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
+ return null;
+ }
+
+ _logger.LogDebug("Target territory for quest step: {TargetTerritory}", step.TargetTerritoryId);
+ return step.TargetTerritoryId;
+ }
+
+ private bool TryFindWarp(ushort targetTerritoryId, string actualPrompt, [NotNullWhen(true)] out uint? warpId,
+ [NotNullWhen(true)] out string? warpText)
+ {
+ var warps = _dataManager.GetExcelSheet<Warp>()!
+ .Where(x => x.RowId > 0 && x.TerritoryType.Row == targetTerritoryId);
+ foreach (var entry in warps)
+ {
+ string? excelName = entry.Name?.ToString();
+ string? excelQuestion = entry.Question?.ToString();
+
+ if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt))
+ {
+ warpId = entry.RowId;
+ warpText = excelQuestion;
+ return true;
+ }
+ else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt))
+ {
+ warpId = entry.RowId;
+ warpText = excelName;
+ return true;
+ }
+ else
+ {
+ _logger.LogDebug("Ignoring prompt '{Prompt}'", excelQuestion);
+ }
+ }
+
+ warpId = null;
+ warpText = null;
+ return false;
+ }
+
+ private unsafe void PointMenuPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AtkUnitBase* addonPointMenu = (AtkUnitBase*)args.Addon;
+ PointMenuPostSetup(addonPointMenu);
+ }
+
+ private unsafe void PointMenuPostSetup(AtkUnitBase* addonPointMenu)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ var currentQuest = _questController.StartedQuest;
+ if (currentQuest == null)
+ {
+ _logger.LogInformation("Ignoring point menu, no active quest");
+ return;
+ }
+
+ var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
+ if (sequence == null)
+ return;
+
+ QuestStep? step = sequence.FindStep(currentQuest.Step);
+ if (step == null)
+ return;
+
+ if (step.PointMenuChoices.Count == 0)
+ {
+ _logger.LogWarning("No point menu choices");
+ return;
+ }
+
+ int counter = currentQuest.StepProgress.PointMenuCounter;
+ if (counter >= step.PointMenuChoices.Count)
+ {
+ _logger.LogWarning("No remaining point menu choices");
+ return;
+ }
+
+ uint choice = step.PointMenuChoices[counter];
+
+ _logger.LogInformation("Handling point menu, picking choice {Choice} (index = {Index})", choice, counter);
+ var selectChoice = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 13 },
+ new() { Type = ValueType.UInt, UInt = choice }
+ };
+ addonPointMenu->FireCallback(2, selectChoice);
+
+ currentQuest.IncreasePointMenuCounter();
+ }
+
+ private unsafe void HousingSelectBlockPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ _logger.LogInformation("Confirming selected housing ward");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(0);
+ }
+
+ private void TeleportTownPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (ShouldHandleUiInteractions &&
+ _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) &&
+ EAetheryteLocationExtensions.IsFirmamentAetheryte(aethernetShortcut.From))
+ {
+ // this might be better via atkvalues; but this works for now
+ uint toIndex = aethernetShortcut.To switch
+ {
+ EAetheryteLocation.FirmamentMendicantsCourt => 0,
+ EAetheryteLocation.FirmamentMattock => 1,
+ EAetheryteLocation.FirmamentNewNest => 2,
+ EAetheryteLocation.FirmanentSaintRoellesDais => 3,
+ EAetheryteLocation.FirmamentFeatherfall => 4,
+ EAetheryteLocation.FirmamentHoarfrostHall => 5,
+ EAetheryteLocation.FirmamentWesternRisensongQuarter => 6,
+ EAetheryteLocation.FIrmamentEasternRisensongQuarter => 7,
+ _ => uint.MaxValue,
+ };
+
+ if (toIndex == uint.MaxValue)
+ return;
+
+ _logger.LogInformation("Teleporting to {ToName} with menu index {ToIndex}", aethernetShortcut.From,
+ toIndex);
+ unsafe
+ {
+ var teleportToDestination = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 11 },
+ new() { Type = ValueType.UInt, UInt = toIndex }
+ };
+
+ var addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallback(2, teleportToDestination);
+ addon->FireCallback(2, teleportToDestination, true);
+ }
+ }
+ }
+
+ private StringOrRegex? ResolveReference(Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
+ {
+ if (excelRef == null)
+ return null;
+
+ if (excelRef.Type == ExcelRef.EType.Key)
+ return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
+ else if (excelRef.Type == ExcelRef.EType.RowId)
+ return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
+ else if (excelRef.Type == ExcelRef.EType.RawString)
+ return new StringOrRegex(excelRef.AsRawString());
+
+ return null;
+ }
+
+ public void Dispose()
+ {
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
+ }
+
+ private sealed record DialogueChoiceInfo(Quest? Quest, DialogueChoice DialogueChoice);
+}
--- /dev/null
+using System;
+using System.Linq;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using Microsoft.Extensions.Logging;
+using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model.Questing;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class LeveUiController : IDisposable
+{
+ private readonly QuestController _questController;
+ private readonly QuestData _questData;
+ private readonly QuestFunctions _questFunctions;
+ private readonly IAddonLifecycle _addonLifecycle;
+ private readonly IGameGui _gameGui;
+ private readonly ITargetManager _targetManager;
+ private readonly IFramework _framework;
+ private readonly ILogger<LeveUiController> _logger;
+
+ public LeveUiController(QuestController questController, QuestData questData, QuestFunctions questFunctions,
+ IAddonLifecycle addonLifecycle, IGameGui gameGui, ITargetManager targetManager, IFramework framework,
+ ILogger<LeveUiController> logger)
+ {
+ _questController = questController;
+ _questData = questData;
+ _questFunctions = questFunctions;
+ _addonLifecycle = addonLifecycle;
+ _gameGui = gameGui;
+ _targetManager = targetManager;
+ _framework = framework;
+ _logger = logger;
+
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
+ }
+
+ private bool ShouldHandleUiInteractions => _questController.IsRunning;
+
+ private unsafe void JournalResultPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (!ShouldHandleUiInteractions)
+ return;
+
+ _logger.LogInformation("Checking for quest name of journal result");
+ AddonJournalResult* addon = (AddonJournalResult*)args.Addon;
+
+ string questName = addon->AtkTextNode250->NodeText.ToString();
+ if (_questController.CurrentQuest is { Quest.Id: LeveId } &&
+ GameFunctions.GameStringEquals(_questController.CurrentQuest.Quest.Info.Name, questName))
+ {
+ _logger.LogInformation("JournalResult has the current leve, auto-accepting it");
+ addon->FireCallbackInt(0);
+ }
+ else if (_targetManager.Target is { } target)
+ {
+ var issuedLeves = _questData.GetAllByIssuerDataId(target.DataId)
+ .Where(x => x.QuestId is LeveId)
+ .ToList();
+
+ if (issuedLeves.Any(x => GameFunctions.GameStringEquals(x.Name, questName)))
+ {
+ _logger.LogInformation(
+ "JournalResult has a leve but not the one we're currently on, auto-declining it");
+ addon->FireCallbackInt(1);
+ }
+ }
+ }
+
+ private unsafe void GuildLevePostSetup(AddonEvent type, AddonArgs args)
+ {
+ var target = _targetManager.Target;
+ if (target == null)
+ return;
+
+ if (_questController is { IsRunning: true, NextQuest: { Quest.Id: LeveId } nextQuest } &&
+ _questFunctions.IsReadyToAcceptQuest(nextQuest.Quest.Id))
+ {
+ var addon = (AddonGuildLeve*)args.Addon;
+ /*
+ var atkValues = addon->AtkValues;
+
+ var availableLeves = _questData.GetAllByIssuerDataId(target.DataId);
+ List<(int, IQuestInfo)> offeredLeves = [];
+ for (int i = 0; i <= 20; ++i) // 3 leves per group, 1 label for group
+ {
+ string? leveName = atkValues[626 + i * 2].ReadAtkString();
+ if (leveName == null)
+ continue;
+
+ var questInfo = availableLeves.FirstOrDefault(x => GameFunctions.GameStringEquals(x.Name, leveName));
+ if (questInfo == null)
+ continue;
+
+ offeredLeves.Add((i, questInfo));
+
+ }
+
+ foreach (var (i, questInfo) in offeredLeves)
+ _logger.LogInformation("Leve {Index} = {Id}, {Name}", i, questInfo.QuestId, questInfo.Name);
+ */
+
+ _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest), TimeSpan.FromMilliseconds(100));
+ }
+ }
+
+ private unsafe void AcceptLeveOrWait(QuestController.QuestProgress nextQuest, int counter = 0)
+ {
+ var agent = UIModule.Instance()->GetAgentModule()->GetAgentByInternalId(AgentId.LeveQuest);
+ if (agent->IsAgentActive() &&
+ _gameGui.TryGetAddonByName("GuildLeve", out AddonGuildLeve* addonGuildLeve) &&
+ LAddon.IsAddonReady(&addonGuildLeve->AtkUnitBase) &&
+ _gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail) &&
+ LAddon.IsAddonReady(addonJournalDetail))
+ {
+ AcceptLeve(agent, addonGuildLeve, nextQuest);
+ }
+ else if (counter >= 10)
+ _logger.LogWarning("Unable to accept leve?");
+ else
+ _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest, counter + 1), TimeSpan.FromMilliseconds(100));
+ }
+
+ private unsafe void AcceptLeve(AgentInterface* agent, AddonGuildLeve* addon,
+ QuestController.QuestProgress nextQuest)
+ {
+ _questController.SetPendingQuest(nextQuest);
+ _questController.SetNextQuest(null);
+
+ var returnValue = stackalloc AtkValue[1];
+ var selectQuest = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 3 },
+ new() { Type = ValueType.UInt, UInt = nextQuest.Quest.Id.Value }
+ };
+ agent->ReceiveEvent(returnValue, selectQuest, 2, 0);
+ addon->Close(true);
+ }
+
+ public void Dispose()
+ {
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
+ }
+}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text.RegularExpressions;
-using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-using Dalamud.Game.ClientState.Objects;
-using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.Game.Event;
-using FFXIVClientStructs.FFXIV.Client.Game.UI;
-using FFXIVClientStructs.FFXIV.Client.UI;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using LLib;
-using LLib.GameData;
-using LLib.GameUI;
-using Lumina.Excel.GeneratedSheets;
-using Microsoft.Extensions.Logging;
-using Questionable.Controller.Steps.Interactions;
-using Questionable.Data;
-using Questionable.Functions;
-using Questionable.Model;
-using Questionable.Model.Common;
-using Questionable.Model.Gathering;
-using Questionable.Model.Questing;
-using AethernetShortcut = Questionable.Controller.Steps.Shared.AethernetShortcut;
-using Quest = Questionable.Model.Quest;
-using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
-
-namespace Questionable.Controller;
-
-internal sealed class GameUiController : IDisposable
-{
- private readonly IAddonLifecycle _addonLifecycle;
- private readonly IDataManager _dataManager;
- private readonly QuestFunctions _questFunctions;
- private readonly AetheryteFunctions _aetheryteFunctions;
- private readonly ExcelFunctions _excelFunctions;
- private readonly QuestController _questController;
- private readonly GatheringData _gatheringData;
- private readonly GatheringPointRegistry _gatheringPointRegistry;
- private readonly QuestRegistry _questRegistry;
- private readonly QuestData _questData;
- private readonly IGameGui _gameGui;
- private readonly ITargetManager _targetManager;
- private readonly IFramework _framework;
- private readonly IClientState _clientState;
- private readonly ILogger<GameUiController> _logger;
- private readonly Regex _returnRegex;
-
- private bool _isInitialCheck;
-
- public GameUiController(
- IAddonLifecycle addonLifecycle,
- IDataManager dataManager,
- QuestFunctions questFunctions,
- AetheryteFunctions aetheryteFunctions,
- ExcelFunctions excelFunctions,
- QuestController questController,
- GatheringData gatheringData,
- GatheringPointRegistry gatheringPointRegistry,
- QuestRegistry questRegistry,
- QuestData questData,
- IGameGui gameGui,
- ITargetManager targetManager,
- IFramework framework,
- IPluginLog pluginLog,
- IClientState clientState,
- ILogger<GameUiController> logger)
- {
- _addonLifecycle = addonLifecycle;
- _dataManager = dataManager;
- _questFunctions = questFunctions;
- _aetheryteFunctions = aetheryteFunctions;
- _excelFunctions = excelFunctions;
- _questController = questController;
- _gatheringData = gatheringData;
- _gatheringPointRegistry = gatheringPointRegistry;
- _questRegistry = questRegistry;
- _questData = questData;
- _gameGui = gameGui;
- _targetManager = targetManager;
- _framework = framework;
- _clientState = clientState;
- _logger = logger;
-
- _returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
-
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
- _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
- }
-
- private bool ShouldHandleUiInteractions => _isInitialCheck || _questController.IsRunning;
-
- internal unsafe void HandleCurrentDialogueChoices()
- {
- try
- {
- _isInitialCheck = true;
- if (_gameGui.TryGetAddonByName("SelectString", out AddonSelectString* addonSelectString))
- {
- _logger.LogInformation("SelectString window is open");
- SelectStringPostSetup(addonSelectString, true);
- }
-
- if (_gameGui.TryGetAddonByName("CutSceneSelectString",
- out AddonCutSceneSelectString* addonCutSceneSelectString))
- {
- _logger.LogInformation("CutSceneSelectString window is open");
- CutsceneSelectStringPostSetup(addonCutSceneSelectString, true);
- }
-
- if (_gameGui.TryGetAddonByName("SelectIconString", out AddonSelectIconString* addonSelectIconString))
- {
- _logger.LogInformation("SelectIconString window is open");
- SelectIconStringPostSetup(addonSelectIconString, true);
- }
-
- if (_gameGui.TryGetAddonByName("SelectYesno", out AddonSelectYesno* addonSelectYesno))
- {
- _logger.LogInformation("SelectYesno window is open");
- SelectYesnoPostSetup(addonSelectYesno, true);
- }
-
- if (_gameGui.TryGetAddonByName("PointMenu", out AtkUnitBase* addonPointMenu))
- {
- _logger.LogInformation("PointMenu is open");
- PointMenuPostSetup(addonPointMenu);
- }
- }
- finally
- {
- _isInitialCheck = false;
- }
- }
-
- private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
- {
- AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
- SelectStringPostSetup(addonSelectString, false);
- }
-
- private unsafe void SelectStringPostSetup(AddonSelectString* addonSelectString, bool checkAllSteps)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- string? actualPrompt = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
- if (actualPrompt == null)
- return;
-
- List<string?> answers = new();
- for (ushort i = 7; i < addonSelectString->AtkUnitBase.AtkValuesCount; ++i)
- {
- if (addonSelectString->AtkUnitBase.AtkValues[i].Type == ValueType.String)
- answers.Add(addonSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
- }
-
- int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
- if (answer != null)
- addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
- }
-
- private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
- {
- AddonCutSceneSelectString* addonCutSceneSelectString = (AddonCutSceneSelectString*)args.Addon;
- CutsceneSelectStringPostSetup(addonCutSceneSelectString, false);
- }
-
- private unsafe void CutsceneSelectStringPostSetup(AddonCutSceneSelectString* addonCutSceneSelectString,
- bool checkAllSteps)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- string? actualPrompt = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
- if (actualPrompt == null)
- return;
-
- List<string?> answers = new();
- for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; ++i)
- answers.Add(addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
-
- int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
- if (answer != null)
- addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
- }
-
- private unsafe void SelectIconStringPostSetup(AddonEvent type, AddonArgs args)
- {
- AddonSelectIconString* addonSelectIconString = (AddonSelectIconString*)args.Addon;
- SelectIconStringPostSetup(addonSelectIconString, false);
- }
-
- [SuppressMessage("ReSharper", "RedundantJumpStatement")]
- private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- string? actualPrompt = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString();
- if (string.IsNullOrEmpty(actualPrompt))
- actualPrompt = null;
-
- var answers = GetChoices(addonSelectIconString);
- int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
- if (answer != null)
- {
- addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
- return;
- }
-
- // this is 'Daily Quests' for tribal quests, but not set for normal selections
- string? title = addonSelectIconString->AtkValues[0].ReadAtkString();
-
- var currentQuest = _questController.StartedQuest;
- if (currentQuest != null && (actualPrompt == null || title != null))
- {
- _logger.LogInformation("Checking if current quest {Name} is on the list", currentQuest.Quest.Info.Name);
- if (CheckQuestSelection(addonSelectIconString, currentQuest.Quest, answers))
- return;
- }
-
- var nextQuest = _questController.NextQuest;
- if (nextQuest != null && (actualPrompt == null || title != null))
- {
- _logger.LogInformation("Checking if next quest {Name} is on the list", nextQuest.Quest.Info.Name);
- if (CheckQuestSelection(addonSelectIconString, nextQuest.Quest, answers))
- return;
- }
- }
-
- private unsafe bool CheckQuestSelection(AddonSelectIconString* addonSelectIconString, Quest quest,
- List<string?> answers)
- {
- // it is possible for this to be a quest selection
- string questName = quest.Info.Name;
- int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
- if (questSelection >= 0)
- {
- addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
- return true;
- }
-
- return false;
- }
-
- public static unsafe List<string?> GetChoices(AddonSelectIconString* addonSelectIconString)
- {
- List<string?> answers = new();
- for (ushort i = 0; i < addonSelectIconString->AtkUnitBase.AtkValues[5].Int; i++)
- answers.Add(addonSelectIconString->AtkValues[i * 3 + 7].ReadAtkString());
-
- return answers;
- }
-
- private int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
- {
- List<DialogueChoiceInfo> dialogueChoices = [];
-
- // levequest choices have some vague sort of priority
- if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
- interact.Quest != null &&
- interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
- {
- if (interact.InteractionType == EInteractionType.AcceptLeve)
- {
- dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
- new DialogueChoice
- {
- Type = EDialogChoiceType.List,
- ExcelSheet = "leve/GuildleveAssignment",
- Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
- Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_01"),
- }));
- interact.InteractionType = EInteractionType.None;
- }
- else if (interact.InteractionType == EInteractionType.CompleteLeve)
- {
- dialogueChoices.Add(new DialogueChoiceInfo(interact.Quest,
- new DialogueChoice
- {
- Type = EDialogChoiceType.List,
- ExcelSheet = "leve/GuildleveAssignment",
- Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
- Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_REWARD"),
- }));
- interact.InteractionType = EInteractionType.None;
- }
- }
-
- var currentQuest = _questController.SimulatedQuest ??
- _questController.GatheringQuest ??
- _questController.StartedQuest;
- if (currentQuest != null)
- {
- var quest = currentQuest.Quest;
- if (checkAllSteps)
- {
- var sequence = quest.FindSequence(currentQuest.Sequence);
- var choices = sequence?.Steps.SelectMany(x => x.DialogueChoices);
- if (choices != null)
- dialogueChoices.AddRange(choices.Select(x => new DialogueChoiceInfo(quest, x)));
- }
- else
- {
- var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
- if (step == null)
- _logger.LogDebug("Ignoring current quest dialogue choices, no active step");
- else
- dialogueChoices.AddRange(step.DialogueChoices.Select(x => new DialogueChoiceInfo(quest, x)));
- }
-
- // add all travel dialogue choices
- var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
- if (targetTerritoryId != null)
- {
- foreach (string? answer in answers)
- {
- if (answer == null)
- continue;
-
- if (TryFindWarp(targetTerritoryId.Value, answer, out uint? warpId, out string? warpText))
- {
- _logger.LogInformation("Adding warp {Id}, {Prompt}", warpId, warpText);
- dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice
- {
- Type = EDialogChoiceType.List,
- ExcelSheet = null,
- Prompt = null,
- Answer = ExcelRef.FromSheetValue(warpText),
- }));
- }
- }
- }
- }
- else
- _logger.LogDebug("Ignoring current quest dialogue choices, no active quest");
-
- // add all quests that start with the targeted npc
- var target = _targetManager.Target;
- if (target != null)
- {
- foreach (var questInfo in _questData.GetAllByIssuerDataId(target.DataId).Where(x => x.QuestId is QuestId))
- {
- if (_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) &&
- _questRegistry.TryGetQuest(questInfo.QuestId, out Quest? knownQuest))
- {
- var questChoices = knownQuest.FindSequence(0)?.Steps
- .SelectMany(x => x.DialogueChoices)
- .ToList();
- if (questChoices != null && questChoices.Count > 0)
- {
- _logger.LogInformation("Adding {Count} dialogue choices from not accepted quest {QuestName}",
- questChoices.Count, questInfo.Name);
- dialogueChoices.AddRange(questChoices.Select(x => new DialogueChoiceInfo(knownQuest, x)));
- }
- }
- }
-
- if ((_questController.IsRunning || _questController.WasLastTaskUpdateWithin(TimeSpan.FromSeconds(5)))
- && _questController.NextQuest == null)
- {
- // make sure to always close the leve dialogue
- if (_questData.GetAllByIssuerDataId(target.DataId).Any(x => x.QuestId is LeveId))
- {
- _logger.LogInformation("Adding close leve dialogue as option");
- dialogueChoices.Add(new DialogueChoiceInfo(null,
- new DialogueChoice
- {
- Type = EDialogChoiceType.List,
- ExcelSheet = "leve/GuildleveAssignment",
- Prompt = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_TITLE"),
- Answer = new ExcelRef("TEXT_GUILDLEVEASSIGNMENT_SELECT_MENU_07"),
- }));
- }
- }
- }
-
- if (dialogueChoices.Count == 0)
- {
- _logger.LogDebug("No dialogue choices to check");
- return null;
- }
-
- foreach (var (quest, dialogueChoice) in dialogueChoices)
- {
- if (dialogueChoice.Type != EDialogChoiceType.List)
- continue;
-
- if (dialogueChoice.Answer == null)
- {
- _logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
- continue;
- }
-
- if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
- {
- _logger.LogDebug(
- "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
- dialogueChoice.DataId, _targetManager.Target?.DataId);
- continue;
- }
-
- string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
- ?.GetString();
- StringOrRegex? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer,
- dialogueChoice.AnswerIsRegularExpression);
-
- if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
- {
- _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
- continue;
- }
-
- if (actualPrompt != null &&
- (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt)))
- {
- _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
- excelPrompt, actualPrompt);
- continue;
- }
-
- for (int i = 0; i < answers.Count; ++i)
- {
- _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
- answers[i], excelAnswer);
- if (IsMatch(answers[i], excelAnswer))
- {
- _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
- i, answers[i], actualPrompt);
-
- // ensure we only open the dialog once
- if (quest?.Id is SatisfactionSupplyNpcId)
- {
- if (_questController.GatheringQuest == null ||
- _questController.GatheringQuest.Sequence == 255)
- return null;
-
- _questController.GatheringQuest.SetSequence(1);
- _questController.StartSingleQuest("SatisfactionSupply turn in");
- }
-
- return i;
- }
- }
- }
-
- _logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
- return null;
- }
-
- private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
- {
- if (actualAnswer == null && expectedAnswer == null)
- return true;
-
- if (actualAnswer == null || expectedAnswer == null)
- return false;
-
- return expectedAnswer.IsMatch(actualAnswer);
- }
-
- private int? HandleInstanceListChoice(string? actualPrompt)
- {
- string? expectedPrompt = _excelFunctions.GetDialogueTextByRowId("Addon", 2090, false).GetString();
- if (GameFunctions.GameStringEquals(actualPrompt, expectedPrompt))
- {
- _logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
- return 0; // any instance
- }
-
- return null;
- }
-
- private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
- {
- AddonSelectYesno* addonSelectYesno = (AddonSelectYesno*)args.Addon;
- SelectYesnoPostSetup(addonSelectYesno, false);
- }
-
- [SuppressMessage("ReSharper", "RedundantJumpStatement")]
- private unsafe void SelectYesnoPostSetup(AddonSelectYesno* addonSelectYesno, bool checkAllSteps)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- string? actualPrompt = addonSelectYesno->AtkUnitBase.AtkValues[0].ReadAtkString();
- if (actualPrompt == null)
- return;
-
- _logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
- var director = UIState.Instance()->DirectorTodo.Director;
- if (director != null && director->EventHandlerInfo != null &&
- director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
- director->Sequence == 254)
- {
- // just close the dialogue for 'do you want to return to next settlement', should prolly be different for
- // ARR territories
- addonSelectYesno->AtkUnitBase.FireCallbackInt(1);
- return;
- }
-
- var currentQuest = _questController.StartedQuest;
- if (currentQuest != null && CheckQuestYesNo(addonSelectYesno, currentQuest, actualPrompt, checkAllSteps))
- return;
-
- var simulatedQuest = _questController.SimulatedQuest;
- if (simulatedQuest != null && HandleTravelYesNo(addonSelectYesno, simulatedQuest, actualPrompt))
- return;
-
- var nextQuest = _questController.NextQuest;
- if (nextQuest != null && CheckQuestYesNo(addonSelectYesno, nextQuest, actualPrompt, checkAllSteps))
- return;
-
- return;
- }
-
- private unsafe bool CheckQuestYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest,
- string actualPrompt, bool checkAllSteps)
- {
- var quest = currentQuest.Quest;
- if (checkAllSteps)
- {
- var sequence = quest.FindSequence(currentQuest.Sequence);
- if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest,
- sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt))
- return true;
- }
- else
- {
- var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
- if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt))
- return true;
- }
-
- if (currentQuest.Quest.Id is LeveId)
- {
- var dialogueChoice = new DialogueChoice
- {
- Type = EDialogChoiceType.YesNo,
- ExcelSheet = "Addon",
- Prompt = new ExcelRef(608),
- Yes = true
- };
-
- if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
- return true;
- }
-
- if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
- return true;
-
- return false;
- }
-
- private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
- IList<DialogueChoice> dialogueChoices, string actualPrompt)
- {
- _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
- foreach (var dialogueChoice in dialogueChoices)
- {
- if (dialogueChoice.Type != EDialogChoiceType.YesNo)
- continue;
-
- if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
- {
- _logger.LogDebug(
- "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
- dialogueChoice.DataId, _targetManager.Target?.DataId);
- continue;
- }
-
- string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, false)
- ?.GetString();
- if (excelPrompt == null || !GameFunctions.GameStringEquals(actualPrompt, excelPrompt))
- {
- _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
- excelPrompt, actualPrompt);
- continue;
- }
-
- addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
- return true;
- }
-
- return false;
- }
-
- private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
- QuestController.QuestProgress currentQuest, string actualPrompt)
- {
- _logger.LogInformation("TravelYesNo");
- if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
- {
- _logger.LogInformation("Automatically confirming return...");
- addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
- return true;
- }
-
- if (_questController.IsRunning && _gameGui.TryGetAddonByName("HousingSelectBlock", out AtkUnitBase* _))
- {
- _logger.LogInformation("Automatically confirming ward selection");
- addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
- return true;
- }
-
- var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
- if (targetTerritoryId != null &&
- TryFindWarp(targetTerritoryId.Value, actualPrompt, out uint? warpId, out string? warpText))
- {
- _logger.LogInformation("Using warp {Id}, {Prompt}", warpId, warpText);
- addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
- return true;
- }
-
- return false;
- }
-
- private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
- {
- // this can be triggered either manually (in which case we should increase the step counter), or automatically
- // (in which case it is ~1 frame later, and the step counter has already been increased)
- var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
- if (sequence == null)
- return null;
-
- QuestStep? step = sequence.FindStep(currentQuest.Step);
- if (step != null)
- _logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}",
- step.TerritoryId,
- step.TargetTerritoryId);
-
- if (step != null && (step.TerritoryId != _clientState.TerritoryType || step.TargetTerritoryId == null) &&
- step.RequiredGatheredItems.Count > 0)
- {
- if (_gatheringData.TryGetGatheringPointId(step.RequiredGatheredItems[0].ItemId,
- (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer,
- out GatheringPointId? gatheringPointId) &&
- _gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot? root))
- {
- foreach (var gatheringStep in root.Steps)
- {
- if (gatheringStep.TerritoryId == _clientState.TerritoryType &&
- gatheringStep.TargetTerritoryId != null)
- {
- _logger.LogTrace(
- "FindTargetTerritoryFromQuestStep (gathering): {CurrentTerritory}, {TargetTerritory}",
- gatheringStep.TerritoryId,
- gatheringStep.TargetTerritoryId);
- return gatheringStep.TargetTerritoryId;
- }
- }
- }
- }
-
- if (step == null || step.TargetTerritoryId == null)
- {
- _logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
- step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
-
- if (step != null)
- _logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}",
- step.TerritoryId,
- step.TargetTerritoryId);
- }
-
- if (step == null || step.TargetTerritoryId == null)
- {
- _logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
- return null;
- }
-
- _logger.LogDebug("Target territory for quest step: {TargetTerritory}", step.TargetTerritoryId);
- return step.TargetTerritoryId;
- }
-
- private bool TryFindWarp(ushort targetTerritoryId, string actualPrompt, [NotNullWhen(true)] out uint? warpId,
- [NotNullWhen(true)] out string? warpText)
- {
- var warps = _dataManager.GetExcelSheet<Warp>()!
- .Where(x => x.RowId > 0 && x.TerritoryType.Row == targetTerritoryId);
- foreach (var entry in warps)
- {
- string? excelName = entry.Name?.ToString();
- string? excelQuestion = entry.Question?.ToString();
-
- if (excelQuestion != null && GameFunctions.GameStringEquals(excelQuestion, actualPrompt))
- {
- warpId = entry.RowId;
- warpText = excelQuestion;
- return true;
- }
- else if (excelName != null && GameFunctions.GameStringEquals(excelName, actualPrompt))
- {
- warpId = entry.RowId;
- warpText = excelName;
- return true;
- }
- else
- {
- _logger.LogDebug("Ignoring prompt '{Prompt}'", excelQuestion);
- }
- }
-
- warpId = null;
- warpText = null;
- return false;
- }
-
- private unsafe void PointMenuPostSetup(AddonEvent type, AddonArgs args)
- {
- AtkUnitBase* addonPointMenu = (AtkUnitBase*)args.Addon;
- PointMenuPostSetup(addonPointMenu);
- }
-
- private unsafe void PointMenuPostSetup(AtkUnitBase* addonPointMenu)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- var currentQuest = _questController.StartedQuest;
- if (currentQuest == null)
- {
- _logger.LogInformation("Ignoring point menu, no active quest");
- return;
- }
-
- var sequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
- if (sequence == null)
- return;
-
- QuestStep? step = sequence.FindStep(currentQuest.Step);
- if (step == null)
- return;
-
- if (step.PointMenuChoices.Count == 0)
- {
- _logger.LogWarning("No point menu choices");
- return;
- }
-
- int counter = currentQuest.StepProgress.PointMenuCounter;
- if (counter >= step.PointMenuChoices.Count)
- {
- _logger.LogWarning("No remaining point menu choices");
- return;
- }
-
- uint choice = step.PointMenuChoices[counter];
-
- _logger.LogInformation("Handling point menu, picking choice {Choice} (index = {Index})", choice, counter);
- var selectChoice = stackalloc AtkValue[]
- {
- new() { Type = ValueType.Int, Int = 13 },
- new() { Type = ValueType.UInt, UInt = choice }
- };
- addonPointMenu->FireCallback(2, selectChoice);
-
- currentQuest.IncreasePointMenuCounter();
- }
-
- /// <summary>
- /// ARR Credits.
- /// </summary>
- private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args)
- {
- _logger.LogInformation("Closing Credits sequence");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->FireCallbackInt(-2);
- }
-
- /// <summary>
- /// Credits for (possibly all?) expansions, not used for ARR.
- /// </summary>
- private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
- {
- _logger.LogInformation("Closing Credits sequence");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->FireCallbackInt(-2);
- }
-
- private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args)
- {
- _logger.LogInformation("Closing CreditPlayer");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->Close(true);
- }
-
- private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- if (_questController.StartedQuest?.Quest.Id.Value == 4526)
- {
- _logger.LogInformation("Closing Unending Codex");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->FireCallbackInt(-2);
- }
- }
-
- private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- if (_questController.StartedQuest?.Quest.Id.Value == 245)
- {
- _logger.LogInformation("Closing ContentsTutorial");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->FireCallbackInt(13);
- }
- }
-
- /// <summary>
- /// Opened e.g. the first time you open the duty finder window during Sastasha.
- /// </summary>
- private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- if (_questController.StartedQuest?.Quest.Id.Value == 245)
- {
- _logger.LogInformation("Closing MultipleHelpWindow");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->FireCallbackInt(-2);
- addon->FireCallbackInt(-1);
- }
- }
-
- private unsafe void HousingSelectBlockPostSetup(AddonEvent type, AddonArgs args)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- _logger.LogInformation("Confirming selected housing ward");
- AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
- addon->FireCallbackInt(0);
- }
-
- private unsafe void JournalResultPostSetup(AddonEvent type, AddonArgs args)
- {
- if (!ShouldHandleUiInteractions)
- return;
-
- _logger.LogInformation("Checking for quest name of journal result");
- AddonJournalResult* addon = (AddonJournalResult*)args.Addon;
-
- string questName = addon->AtkTextNode250->NodeText.ToString();
- if (_questController.CurrentQuest is { Quest.Id: LeveId } &&
- GameFunctions.GameStringEquals(_questController.CurrentQuest.Quest.Info.Name, questName))
- {
- _logger.LogInformation("JournalResult has the current leve, auto-accepting it");
- addon->FireCallbackInt(0);
- }
- else if (_targetManager.Target is { } target)
- {
- var issuedLeves = _questData.GetAllByIssuerDataId(target.DataId)
- .Where(x => x.QuestId is LeveId)
- .ToList();
-
- if (issuedLeves.Any(x => GameFunctions.GameStringEquals(x.Name, questName)))
- {
- _logger.LogInformation(
- "JournalResult has a leve but not the one we're currently on, auto-declining it");
- addon->FireCallbackInt(1);
- }
- }
- }
-
- private unsafe void GuildLevePostSetup(AddonEvent type, AddonArgs args)
- {
- var target = _targetManager.Target;
- if (target == null)
- return;
-
- if (_questController is { IsRunning: true, NextQuest: { Quest.Id: LeveId } nextQuest } &&
- _questFunctions.IsReadyToAcceptQuest(nextQuest.Quest.Id))
- {
- var addon = (AddonGuildLeve*)args.Addon;
- /*
- var atkValues = addon->AtkValues;
-
- var availableLeves = _questData.GetAllByIssuerDataId(target.DataId);
- List<(int, IQuestInfo)> offeredLeves = [];
- for (int i = 0; i <= 20; ++i) // 3 leves per group, 1 label for group
- {
- string? leveName = atkValues[626 + i * 2].ReadAtkString();
- if (leveName == null)
- continue;
-
- var questInfo = availableLeves.FirstOrDefault(x => GameFunctions.GameStringEquals(x.Name, leveName));
- if (questInfo == null)
- continue;
-
- offeredLeves.Add((i, questInfo));
-
- }
-
- foreach (var (i, questInfo) in offeredLeves)
- _logger.LogInformation("Leve {Index} = {Id}, {Name}", i, questInfo.QuestId, questInfo.Name);
- */
-
- _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest), TimeSpan.FromMilliseconds(100));
- }
- }
-
- private unsafe void AcceptLeveOrWait(QuestController.QuestProgress nextQuest, int counter = 0)
- {
- var agent = UIModule.Instance()->GetAgentModule()->GetAgentByInternalId(AgentId.LeveQuest);
- if (agent->IsAgentActive() &&
- _gameGui.TryGetAddonByName("GuildLeve", out AddonGuildLeve* addonGuildLeve) &&
- LAddon.IsAddonReady(&addonGuildLeve->AtkUnitBase) &&
- _gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail) &&
- LAddon.IsAddonReady(addonJournalDetail))
- {
- AcceptLeve(agent, addonGuildLeve, nextQuest);
- }
- else if (counter >= 10)
- _logger.LogWarning("Unable to accept leve?");
- else
- _framework.RunOnTick(() => AcceptLeveOrWait(nextQuest, counter + 1), TimeSpan.FromMilliseconds(100));
- }
-
- private unsafe void AcceptLeve(AgentInterface* agent, AddonGuildLeve* addon,
- QuestController.QuestProgress nextQuest)
- {
- _questController.SetPendingQuest(nextQuest);
- _questController.SetNextQuest(null);
-
- var returnValue = stackalloc AtkValue[1];
- var selectQuest = stackalloc AtkValue[]
- {
- new() { Type = ValueType.Int, Int = 3 },
- new() { Type = ValueType.UInt, UInt = nextQuest.Quest.Id.Value }
- };
- agent->ReceiveEvent(returnValue, selectQuest, 2, 0);
- addon->Close(true);
- }
-
- private void TeleportTownPostSetup(AddonEvent type, AddonArgs args)
- {
- if (ShouldHandleUiInteractions &&
- _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) &&
- aethernetShortcut.From.IsFirmamentAetheryte())
- {
- // this might be better via atkvalues; but this works for now
- uint toIndex = aethernetShortcut.To switch
- {
- EAetheryteLocation.FirmamentMendicantsCourt => 0,
- EAetheryteLocation.FirmamentMattock => 1,
- EAetheryteLocation.FirmamentNewNest => 2,
- EAetheryteLocation.FirmanentSaintRoellesDais => 3,
- EAetheryteLocation.FirmamentFeatherfall => 4,
- EAetheryteLocation.FirmamentHoarfrostHall => 5,
- EAetheryteLocation.FirmamentWesternRisensongQuarter => 6,
- EAetheryteLocation.FIrmamentEasternRisensongQuarter => 7,
- _ => uint.MaxValue,
- };
-
- if (toIndex == uint.MaxValue)
- return;
-
- _logger.LogInformation("Teleporting to {ToName} with menu index {ToIndex}", aethernetShortcut.From,
- toIndex);
- unsafe
- {
- var teleportToDestination = stackalloc AtkValue[]
- {
- new() { Type = ValueType.Int, Int = 11 },
- new() { Type = ValueType.UInt, UInt = toIndex }
- };
-
- var addon = (AtkUnitBase*)args.Addon;
- addon->FireCallback(2, teleportToDestination);
- addon->FireCallback(2, teleportToDestination, true);
- }
- }
- }
-
- private StringOrRegex? ResolveReference(Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
- {
- if (excelRef == null)
- return null;
-
- if (excelRef.Type == ExcelRef.EType.Key)
- return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
- else if (excelRef.Type == ExcelRef.EType.RowId)
- return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
- else if (excelRef.Type == ExcelRef.EType.RawString)
- return new StringOrRegex(excelRef.AsRawString());
-
- return null;
- }
-
- public void Dispose()
- {
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GuildLeve", GuildLevePostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "JournalResult", JournalResultPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
- _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
- }
-
- private sealed record DialogueChoiceInfo(Quest? Quest, DialogueChoice DialogueChoice);
-}
while (_taskQueue.TryDequeue(out ITask? nextTask))
{
- if (nextTask is ILastTask)
+ if (nextTask is ILastTask or GatheringRequiredItems.SkipMarker)
{
_currentTask = nextTask;
return;
{
foreach (var task in serviceProvider.GetRequiredService<TaskCreator>()
.CreateTasks(quest, gatheringSequence, gatheringStep))
- if (task is not WaitAtEnd.NextStep)
+ if (task is WaitAtEnd.NextStep)
+ yield return serviceProvider.GetRequiredService<SkipMarker>();
+ else
yield return task;
}
}
$"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {_gatheredItem.Collectability})";
}
}
+
+ /// <summary>
+ /// A task that does nothing, but if we're skipping a step, this will be the task next in queue to be executed (instead of progressing to the next step) if gathering.
+ /// </summary>
+ internal sealed class SkipMarker : ITask
+ {
+ public bool Start() => true;
+ public ETaskResult Update() => ETaskResult.TaskComplete;
+ }
}
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
+using Questionable.Controller.GameUi;
using Questionable.Windows;
namespace Questionable;
IFramework framework,
QuestController questController,
MovementController movementController,
- GameUiController gameUiController,
+ InteractionUiController interactionUiController,
WindowSystem windowSystem,
QuestWindow questWindow,
DebugOverlay debugOverlay,
_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;
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;
IAddonLifecycle addonLifecycle,
IKeyState keyState,
IContextMenu contextMenu,
- IToastGui toastGui)
+ IToastGui toastGui,
+ IGameInteropProvider gameInteropProvider)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(chatGui);
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());
// task factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
serviceCollection.AddSingleton<ITaskFactory, EquipRecommended.BeforeDutyOrInstance>();
- serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering>();
+ serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering, GatheringRequiredItems.SkipMarker>();
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>();
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
serviceCollection.AddSingleton<GatheringPointRegistry>();
serviceCollection.AddSingleton<QuestRegistry>();
serviceCollection.AddSingleton<QuestController>();
- serviceCollection.AddSingleton<GameUiController>();
serviceCollection.AddSingleton<CombatController>();
serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ContextMenuController>();
+ serviceCollection.AddSingleton<CraftworksSupplyController>();
+ serviceCollection.AddSingleton<CreditsController>();
+ serviceCollection.AddSingleton<HelpUiController>();
+ serviceCollection.AddSingleton<InteractionUiController>();
+ serviceCollection.AddSingleton<LeveUiController>();
+
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
}
serviceProvider.GetRequiredService<GatheringPointRegistry>().Reload();
serviceProvider.GetRequiredService<CommandHandler>();
serviceProvider.GetRequiredService<ContextMenuController>();
+ serviceProvider.GetRequiredService<CraftworksSupplyController>();
+ serviceProvider.GetRequiredService<CreditsController>();
+ serviceProvider.GetRequiredService<HelpUiController>();
+ serviceProvider.GetRequiredService<LeveUiController>();
serviceProvider.GetRequiredService<DalamudInitializer>();
}
{
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);
using LLib.GameUI;
using LLib.ImGui;
using Questionable.Controller;
+using Questionable.Controller.GameUi;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
_quests = _questData.GetAllByIssuerDataId(targetId);
if (_gameGui.TryGetAddonByName<AddonSelectIconString>("SelectIconString", out var addonSelectIconString))
{
- var answers = GameUiController.GetChoices(addonSelectIconString);
+ var answers = InteractionUiController.GetChoices(addonSelectIconString);
_offeredQuests = _quests
.Where(x => answers.Any(y => GameFunctions.GameStringEquals(x.Name, y)))
.ToList();
using ImGuiNET;
using LLib.ImGui;
using Questionable.Controller;
+using Questionable.Controller.GameUi;
using Questionable.Data;
using Questionable.Windows.QuestComponents;
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,
QuickAccessButtonsComponent quickAccessButtonsComponent,
RemainingTasksComponent remainingTasksComponent,
IFramework framework,
- GameUiController gameUiController)
+ InteractionUiController interactionUiController)
: base($"Questionable v{PluginVersion.ToString(2)}###Questionable",
ImGuiWindowFlags.AlwaysAutoResize)
{
_quickAccessButtonsComponent = quickAccessButtonsComponent;
_remainingTasksComponent = remainingTasksComponent;
_framework = framework;
- _gameUiController = gameUiController;
+ _interactionUiController = interactionUiController;
#if DEBUG
IsOpen = true;
internal void Reload()
{
_questController.Reload();
- _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
+ _framework.RunOnTick(() => _interactionUiController.HandleCurrentDialogueChoices(),
TimeSpan.FromMilliseconds(200));
}
}