},
"TerritoryId": 957,
"InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 128
+ ],
"$": "QuestVariables after: 16 1 0 0 0 128"
},
{
"Z": -159.90234
},
"TerritoryId": 957,
- "InteractionType": "WalkTo"
+ "InteractionType": "WalkTo",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 32
+ ]
},
{
"DataId": 2011914,
"TerritoryId": 957,
"InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 32
+ ],
"$": "QuestVariables after: 32 17 0 0 0 160"
},
{
"Z": -157.09167
},
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 64
+ ]
}
]
},
"Z": 799.2217
},
"TerritoryId": 957,
- "InteractionType": "CutsceneSelectString",
+ "InteractionType": "Interact",
"DialogueChoices": [
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q2_000_086",
"Answer": "TEXT_AKTKMA114_04370_A2_000_088"
},
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q3_000_096",
"Answer": "TEXT_AKTKMA114_04370_A3_000_098"
},
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q5_000_106",
"Answer": "TEXT_AKTKMA114_04370_A5_000_107"
}
]
"Z": 681.7273
},
"TerritoryId": 957,
- "InteractionType": "CutsceneSelectString",
+ "InteractionType": "Interact",
"DialogueChoices": [
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q6_000_147",
"Answer": "TEXT_AKTKMA114_04370_A6_000_149"
},
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q7_000_157",
"Answer": "TEXT_AKTKMA114_04370_A7_000_158"
},
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q8_000_162",
"Answer": "TEXT_AKTKMA114_04370_A8_000_164"
}
]
"Z": 517.72327
},
"TerritoryId": 957,
- "InteractionType": "CutsceneSelectString",
+ "InteractionType": "Interact",
"DialogueChoices": [
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q9_000_198",
"Answer": "TEXT_AKTKMA114_04370_A9_000_200"
},
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q10_000_207",
"Answer": "TEXT_AKTKMA114_04370_A10_000_209"
},
{
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMA114_04370_Q11_000_216",
"Answer": "TEXT_AKTKMA114_04370_A11_000_218"
}
]
{
"$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza",
+ "TerritoryBlacklist": [
+ 1097
+ ],
"QuestSequence": [
{
"Sequence": 0,
{
"$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza",
+ "TerritoryBlacklist": [
+ 1095
+ ],
"QuestSequence": [
{
"Sequence": 0,
},
"StopDistance": 5,
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "DialogueChoices": [
+ {
+ "Type": "YesNo",
+ "Prompt": "TEXT_AKTKMK102_04736_Q2_000_094",
+ "Yes": true
+ }
+ ]
}
]
},
},
"TerritoryId": 958,
"InteractionType": "Interact",
- "Comment": "TODO Check flags",
"CompletionQuestVariablesFlags": [
null,
null,
{
"Sequence": 255,
"Steps": [
+ {
+ "Position": {
+ "X": 534.8861,
+ "Y": -36.65,
+ "Z": -245.12135
+ },
+ "TerritoryId": 958,
+ "InteractionType": "WalkTo",
+ "SkipIf": [
+ "FlyingLocked"
+ ]
+ },
{
"DataId": 1045430,
"Position": {
},
"TerritoryId": 958,
"InteractionType": "Interact",
- "Fly": true
+ "Fly": true,
+ "TargetTerritoryId": 1160
}
]
},
},
"TerritoryId": 1160,
"InteractionType": "Interact",
- "Comment": "TODO Check Flags",
"CompletionQuestVariablesFlags": [
null,
null,
"Y": 10.8,
"Z": -231.61676
},
+ "StopDistance": 5,
"TerritoryId": 958,
"InteractionType": "Interact"
}
"TerritoryId": 959,
"InteractionType": "Interact",
"AetheryteShortcut": "Mare Lamentorum - Bestways Burrow",
- "SkipIf": ["FlyingUnlocked"],
- "Comment": "Check if the flying unlocked check is good enough"
+ "SkipIf": [
+ "FlyingUnlocked"
+ ]
+ },
+ {
+ "Position": {
+ "X": -19.779482,
+ "Y": -56.63768,
+ "Z": -464.9354
+ },
+ "StopDistance": 1,
+ "TerritoryId": 959,
+ "InteractionType": "WalkTo",
+ "SkipIf": [
+ "FlyingLocked"
+ ],
+ "Fly": true
},
{
"DataId": 1039686,
"Z": -620.3861
},
"TerritoryId": 959,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "Fly": true
}
]
},
null,
null,
32
- ],
- "Comment": "TODO Check Flags"
+ ]
},
{
"DataId": 1045473,
16
]
}
- ]
+ ],
+ "Comment": "TODO Check Flags (32)"
},
{
"Sequence": 255,
null,
null,
128
- ],
- "Comment": "TODO Check Flags"
+ ]
},
{
"DataId": 2013355,
{
"$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza",
+ "TerritoryBlacklist": [
+ 1140
+ ],
"QuestSequence": [
{
"Sequence": 0,
"TerritoryId": 959,
"InteractionType": "Interact",
"AetheryteShortcut": "Mare Lamentorum - Bestways Burrow",
- "SkipIf": ["FlyingUnlocked"]
+ "SkipIf": [
+ "FlyingUnlocked"
+ ]
+ },
+ {
+ "Position": {
+ "X": -19.779482,
+ "Y": -56.63768,
+ "Z": -464.9354
+ },
+ "StopDistance": 1,
+ "TerritoryId": 959,
+ "InteractionType": "WalkTo",
+ "SkipIf": [
+ "FlyingLocked"
+ ],
+ "Fly": true
},
{
"DataId": 1045466,
"AethernetShortcut": [
"[Radz-at-Han] Aetheryte Plaza",
"[Radz-at-Han] Meghaduta"
+ ],
+ "DialogueChoices": [
+ {
+ "Type": "YesNo",
+ "Prompt": "TEXT_AKTKMK109_04743_Q1_000_000",
+ "Yes": true
+ }
]
}
]
"Y": 55,
"Z": -68.61987
},
- "StopDistance": 5,
+ "StopDistance": 7,
"TerritoryId": 963,
"InteractionType": "Interact"
}
{
"$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza",
+ "TerritoryBlacklist": [
+ 1164,
+ 1168
+ ],
"QuestSequence": [
{
"Sequence": 0,
"Z": -440.63483
},
"TerritoryId": 1184,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "DialogueChoices": [
+ {
+ "Type": "YesNo",
+ "Prompt": "TEXT_AKTKML105_04748_SYSTEM_000_406",
+ "Yes": true
+ }
+ ]
}
]
},
"Y": 56.66061,
"Z": 467.39905
},
+ "StopDistance": 15,
"TerritoryId": 1162,
"InteractionType": "Interact"
}
"Z": -191.51605
},
"TerritoryId": 958,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Garlemald - Tertium"
}
]
},
},
"TerritoryId": 962,
"InteractionType": "Interact",
+ "AetheryteShortcut": "Old Sharlayan",
"AethernetShortcut": [
"[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Rostra"
"Y": 41.530136,
"Z": -165.27051
},
+ "StopDistance": 7,
"TerritoryId": 962,
"InteractionType": "Interact"
}
},
"TerritoryId": 819,
"InteractionType": "Interact",
+ "AetheryteShortcut": "Crystarium",
"AethernetShortcut": [
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
+ ],
+ "DialogueChoices": [
+ {
+ "Type": "YesNo",
+ "Prompt": "TEXT_AKTKML107_04750_SYSTEM_000_101",
+ "Yes": true
+ }
]
}
]
}
]
},
+ {
+ "Sequence": 3,
+ "Steps": [
+ {
+ "DataId": 1045684,
+ "Position": {
+ "X": -0.96136475,
+ "Y": 0,
+ "Z": -3.3417358
+ },
+ "StopDistance": 5,
+ "TerritoryId": 844,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
{
"Sequence": 4,
"Steps": [
},
"TerritoryId": 963,
"InteractionType": "Interact",
+ "AetheryteShortcut": "Radz-at-Han",
"AethernetShortcut": [
"[Radz-at-Han] Aetheryte Plaza",
"[Radz-at-Han] Meghaduta"
"Sequence": 255,
"Steps": [
{
- "DataId": 196,
+ "DataId": 1039645,
"Position": {
- "X": -42.61847,
- "Y": -0.015319824,
- "Z": -197.61963
+ "X": -338.33832,
+ "Y": 55,
+ "Z": -68.40625
},
"TerritoryId": 963,
"InteractionType": "Interact",
"AethernetShortcut": [
"[Radz-at-Han] Mehryde's Meyhane",
- "[Radz-at-Han] Aetheryte Plaza"
+ "[Radz-at-Han] Meghaduta"
]
}
]
"Z": -68.40625
},
"TerritoryId": 963,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "DialogueChoices": [
+ {
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMM103_04753_Q1_000_000",
+ "Answer": "TEXT_AKTKMM103_04753_A1_000_001"
+ }
+ ]
}
]
},
"Y": 4.357494,
"Z": 0.7476196
},
+ "StopDistance": 7,
"TerritoryId": 962,
"InteractionType": "Interact",
+ "AetheryteShortcut": "Old Sharlayan",
"AethernetShortcut": [
"[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Baldesion Annex"
{
"$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza",
+ "TerritoryBlacklist": [
+ 1177
+ ],
"QuestSequence": [
{
"Sequence": 0,
"Y": -14.169313,
"Z": 105.30249
},
+ "StopDistance": 7,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": -15.127002,
"Z": 139.42163
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": -15.127001,
"Z": 139.45215
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"AethernetShortcut": [
"[Old Sharlayan] Scholar's Harbor",
"[Old Sharlayan] The Studium"
+ ],
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 16
]
},
{
"Z": 103.28821
},
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 32
+ ]
},
{
"DataId": 2013417,
"Z": 59.00659
},
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 64
+ ]
},
{
"DataId": 2013416,
"Z": 20.523315
},
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "CompletionQuestVariablesFlags": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ 128
+ ]
}
]
},
"Z": 0.7476196
},
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "DialogueChoices": [
+ {
+ "Type": "YesNo",
+ "Prompt": "TEXT_AKTKMM103_04753_SYSTEM_000_302",
+ "Yes": true
+ }
+ ]
}
]
},
"Y": 4.357494,
"Z": 0.7476196
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza",
"Comment": "TODO Missing Quest Start",
+ "TerritoryBlacklist": [
+ 838,
+ 847
+ ],
"QuestSequence": [
{
"Sequence": 7,
"Z": -9.10968
},
"TerritoryId": 351,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "DialogueChoices": [
+ {
+ "Type": "List",
+ "Prompt": "TEXT_LUCKMG101_03673_Q1_000_500",
+ "Answer": "TEXT_LUCKMG101_03673_A1_000_500"
+ }
+ ]
}
]
},
"Z": -656.1909
},
"TerritoryId": 156,
- "InteractionType": "WalkTo"
+ "InteractionType": "WalkTo",
+ "Mount": true
},
{
"DataId": 1018433,
"Y": -0.67464465,
"Z": 653.1527
},
+ "StopDistance": 0.5,
"TerritoryId": 813,
"InteractionType": "WalkTo",
"AetheryteShortcut": "Lakeland - Fort Jobb",
--- /dev/null
+Currying Flavor:
+ "DialogueChoices": [
+ {
+ "Type": "List",
+ "Prompt": "TEXT_AKTKMK101_04735_Q1_000_000",
+ "Answer": "TEXT_AKTKMK101_04735_A1_000_003"
+ },
+ {
+ "Type": "YesNo",
+ "Prompt": "TEXT_AKTKMK101_04735_Q2_000_182",
+ "Yes": true
+ }
+ ]
"Duty",
"SinglePlayerDuty",
"Jump",
- "CutsceneSelectString",
"ShouldBeAJump",
"Instruction"
]
"if": {
"properties": {
"InteractionType": {
- "const": "CutsceneSelectString"
+ "const": "Interact"
}
}
},
"items": {
"type": "object",
"properties": {
+ "Type": {
+ "type": "string",
+ "enum": [
+ "YesNo",
+ "List"
+ ]
+ },
"ExcelSheet": {
"type": "string"
},
- "Answer": {
+ "Prompt": {
"type": "string"
}
},
"required": [
- "Answer"
+ "Type",
+ "Prompt"
+ ],
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "Type": {
+ "const": "YesNo"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "Yes": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ "required": [
+ "Yes"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "Type": {
+ "const": "List"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "Answer": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "Answer"
+ ]
+ }
+ }
]
}
}
- },
- "required": [
- "DialogueChoices"
- ]
+ }
}
},
{
--- /dev/null
+using System;
+using System.Linq;
+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 Lumina.Excel.GeneratedSheets;
+using Questionable.Model.V1;
+using Quest = Questionable.Model.Quest;
+
+namespace Questionable.Controller;
+
+internal sealed class GameUiController : IDisposable
+{
+ private readonly IClientState _clientState;
+ private readonly IAddonLifecycle _addonLifecycle;
+ private readonly IDataManager _dataManager;
+ private readonly GameFunctions _gameFunctions;
+ private readonly QuestController _questController;
+ private readonly IPluginLog _pluginLog;
+
+ public GameUiController(IClientState clientState, IAddonLifecycle addonLifecycle, IDataManager dataManager,
+ GameFunctions gameFunctions, QuestController questController, IPluginLog pluginLog)
+ {
+ _clientState = clientState;
+ _addonLifecycle = addonLifecycle;
+ _dataManager = dataManager;
+ _gameFunctions = gameFunctions;
+ _questController = questController;
+ _pluginLog = pluginLog;
+
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
+ _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
+ }
+
+ private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
+ string? actualPrompt = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
+ if (actualPrompt == null)
+ return;
+
+ var currentQuest = _questController.CurrentQuest;
+ if (currentQuest == null)
+ return;
+
+ var quest = currentQuest.Quest;
+ var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+ if (step == null)
+ return;
+
+ foreach (var dialogueChoice in step.DialogueChoices)
+ {
+ if (dialogueChoice.Answer == null)
+ continue;
+
+ string? excelPrompt =
+ _gameFunctions.GetExcelString(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
+ string? excelAnswer =
+ _gameFunctions.GetExcelString(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer);
+ if (excelPrompt == null || actualPrompt != excelPrompt)
+ continue;
+
+ for (ushort i = 7; i <= addonSelectString->AtkUnitBase.AtkValuesCount; ++i)
+ {
+ string? actualAnswer = addonSelectString->AtkUnitBase.AtkValues[i].ReadAtkString();
+ if (actualAnswer == null || actualAnswer != excelAnswer)
+ continue;
+
+ _questController.IncreaseDialogueChoicesSelected();
+ addonSelectString->AtkUnitBase.FireCallbackInt(i - 7);
+ return;
+ }
+ }
+ }
+
+ private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonCutSceneSelectString* addonCutSceneSelectString = (AddonCutSceneSelectString*)args.Addon;
+ string? actualPrompt = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
+ if (actualPrompt == null)
+ return;
+
+ var currentQuest = _questController.CurrentQuest;
+ if (currentQuest == null)
+ return;
+
+ var quest = currentQuest.Quest;
+ var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+ if (step == null)
+ return;
+
+ foreach (DialogueChoice dialogueChoice in step.DialogueChoices)
+ {
+ if (dialogueChoice.Answer == null)
+ continue;
+
+ string? excelPrompt =
+ _gameFunctions.GetExcelString(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
+ string? excelAnswer =
+ _gameFunctions.GetExcelString(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer);
+ if (excelPrompt == null || actualPrompt != excelPrompt)
+ continue;
+
+ for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; ++i)
+ {
+ string? actualAnswer = addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString();
+ if (actualAnswer == null || actualAnswer != excelAnswer)
+ continue;
+
+ _questController.IncreaseDialogueChoicesSelected();
+ addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(i - 5);
+ return;
+ }
+ }
+ }
+
+ private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
+ {
+ AddonSelectYesno* addonSelectYesno = (AddonSelectYesno*)args.Addon;
+ string? actualPrompt = addonSelectYesno->AtkUnitBase.AtkValues[0].ReadAtkString();
+ if (actualPrompt == null)
+ return;
+
+ _pluginLog.Verbose($"Prompt: '{actualPrompt}'");
+
+ var currentQuest = _questController.CurrentQuest;
+ if (currentQuest == null)
+ return;
+
+ var quest = currentQuest.Quest;
+ var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+ if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step, actualPrompt))
+ return;
+
+ HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt);
+ }
+
+ private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest, QuestStep step,
+ string actualPrompt)
+ {
+ _pluginLog.Verbose($"DefaultYesNo: Choice count: {step.DialogueChoices.Count}");
+ foreach (var dialogueChoice in step.DialogueChoices)
+ {
+ string? excelPrompt =
+ _gameFunctions.GetExcelString(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
+ if (excelPrompt == null || actualPrompt != excelPrompt)
+ continue;
+
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
+ _questController.IncreaseDialogueChoicesSelected();
+ return true;
+ }
+
+ return false;
+ }
+
+ private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
+ QuestController.QuestProgress currentQuest, string actualPrompt)
+ {
+ // 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 false;
+
+ bool increaseStepCount = true;
+ QuestStep? step = sequence.FindStep(currentQuest.Step);
+ if (step != null)
+ _pluginLog.Verbose($"Current step: {step.TerritoryId}, {step.TargetTerritoryId}");
+
+ if (step == null || step.TargetTerritoryId == null || step.TerritoryId != _clientState.TerritoryType)
+ {
+ _pluginLog.Verbose("TravelYesNo: Checking previous step...");
+ step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
+ increaseStepCount = false;
+
+ if (step != null)
+ _pluginLog.Verbose($"Previous step: {step.TerritoryId}, {step.TargetTerritoryId}");
+ }
+
+ if (step == null || step.TargetTerritoryId == null || step.TerritoryId != _clientState.TerritoryType)
+ {
+ _pluginLog.Verbose("TravelYesNo: Not found");
+ return false;
+ }
+
+ var warps = _dataManager.GetExcelSheet<Warp>()!
+ .Where(x => x.RowId > 0 && x.TerritoryType.Row == step.TargetTerritoryId)
+ .Where(x => x.ConfirmEvent.Row == 0); // unsure if this is needed
+ foreach (var entry in warps)
+ {
+ string? excelPrompt = entry.Question?.ToString();
+ if (excelPrompt == null || excelPrompt != actualPrompt)
+ {
+ _pluginLog.Information($"Ignoring prompt '{excelPrompt}'");
+ continue;
+ }
+
+ _pluginLog.Information($"Using warp {entry.RowId}, {excelPrompt}");
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+ if (increaseStepCount)
+ _questController.IncreaseStepCount();
+ return true;
+ }
+
+ return false;
+ }
+
+ private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
+ {
+ _pluginLog.Information("Closing Credits sequence");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(-2);
+ }
+
+ private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
+ {
+ if (_questController.CurrentQuest?.Quest.QuestId == 4526)
+ {
+ _pluginLog.Information("Closing Unending Codex");
+ AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
+ addon->FireCallbackInt(-2);
+ }
+ }
+
+ public void Dispose()
+ {
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
+ _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
+ }
+}
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
-using FFXIVClientStructs.FFXIV.Client.UI;
-using LLib.GameUI;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.V1;
using Questionable.Model.V1.Converter;
-using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Questionable.Controller;
}
}
+ public void IncreaseDialogueChoicesSelected()
+ {
+ (QuestSequence? seq, QuestStep? step) = GetNextStep();
+ if (CurrentQuest == null || seq == null || step == null)
+ {
+ _pluginLog.Warning("Unable to retrieve next quest step, not increasing dialogue choice count");
+ return;
+ }
+
+ CurrentQuest = CurrentQuest with
+ {
+ StepProgress = CurrentQuest.StepProgress with
+ {
+ DialogueChoicesSelected = CurrentQuest.StepProgress.DialogueChoicesSelected + 1
+ }
+ };
+
+ if (CurrentQuest.StepProgress.DialogueChoicesSelected >= step.DialogueChoices.Count)
+ IncreaseStepCount();
+ }
+
public unsafe void ExecuteNextStep()
{
(QuestSequence? seq, QuestStep? step) = GetNextStep();
}
}
else
- _pluginLog.Warning($"Aethernet shortcut not unlocked (from: {step.AethernetShortcut.From}, to: {step.AethernetShortcut.To}), walking manually");
+ _pluginLog.Warning(
+ $"Aethernet shortcut not unlocked (from: {step.AethernetShortcut.From}, to: {step.AethernetShortcut.To}), walking manually");
}
if (step.TargetTerritoryId == _clientState.TerritoryType && !step.SkipIf.Contains(ESkipCondition.Never))
{
_pluginLog.Information("We're at the jump destination, skipping movement");
}
- else if (step.InteractionType == EInteractionType.CutsceneSelectString &&
- _condition[ConditionFlag.OccupiedInCutSceneEvent])
- {
- _pluginLog.Information("In cutscene selection, skipping movement");
- }
else if (step.Position != null)
{
float distance;
}
_gameFunctions.InteractWith(step.DataId.Value);
- IncreaseStepCount();
+
+ // if we have any dialogue, that is handled in GameUiController
+ if (step.DialogueChoices.Count == 0)
+ IncreaseStepCount();
}
else
_pluginLog.Warning("Not interacting on current step, DataId is null");
// Need to manually forward
break;
- case EInteractionType.CutsceneSelectString:
- // to do this automatically, should likely be in Addon's post setup
- if (_gameGui.TryGetAddonByName<AddonCutSceneSelectString>("CutSceneSelectString", out var addon) &&
- LAddon.IsAddonReady(&addon->AtkUnitBase))
- {
- foreach (DialogueChoice dialogueChoice in step.DialogueChoices)
- {
- string? excelString = _gameFunctions.GetExcelString(CurrentQuest.Quest,
- dialogueChoice.ExcelSheet, dialogueChoice.Answer);
- if (excelString == null)
- return;
-
- _pluginLog.Verbose($"Looking for option '{excelString}'");
- for (int i = 5; i < addon->AtkUnitBase.AtkValuesCount; ++i)
- {
- var atkValue = addon->AtkUnitBase.AtkValues[i];
- if (atkValue.Type != ValueType.String)
- continue;
-
- string? atkString = atkValue.ReadAtkString();
- _pluginLog.Verbose($"Option {i}: {atkString}");
- if (excelString == atkString)
- {
- _pluginLog.Information($"Selecting option {i - 5}: {atkString}");
- addon->AtkUnitBase.FireCallbackInt(i - 5);
- return;
- }
- }
- }
- }
- else if (step.DataId != null && !_condition[ConditionFlag.OccupiedInCutSceneEvent])
- _gameFunctions.InteractWith(step.DataId.Value);
-
- break;
-
default:
_pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
break;
public sealed record StepProgress(
bool AetheryteShortcutUsed = false,
- bool AethernetShortcutUsed = false);
+ bool AethernetShortcutUsed = false,
+ int DialogueChoicesSelected = 0);
}
_pluginLog.Error($"Could not find content for content finder condition (cf: {contentFinderConditionId})");
}
- public string? GetExcelString(Quest currentQuestQuest, string? excelSheetName, string key)
+ public string? GetExcelString(Quest currentQuest, string? excelSheetName, string key)
{
if (excelSheetName == null)
{
- string questPrefix = $"quest/{(currentQuestQuest.QuestId / 100):000}/";
- string questSuffix = $"_{currentQuestQuest.QuestId:00000}";
- excelSheetName = _dataManager.Excel
- .GetSheetNames()
- .SingleOrDefault(x =>
- x.StartsWith(questPrefix, StringComparison.Ordinal) &&
- x.EndsWith(questSuffix, StringComparison.Ordinal));
- if (excelSheetName == null)
+ var questRow = _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestId + 0x10000);
+ if (questRow == null)
{
- _pluginLog.Error($"Could not find sheet matching '{questPrefix}*{questSuffix}");
+ _pluginLog.Error($"Could not find quest row for {currentQuest.QuestId}");
return null;
}
+
+ excelSheetName = $"quest/{(currentQuest.QuestId / 100):000}/{questRow.Id}";
}
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
--- /dev/null
+using System.Collections.Generic;
+
+namespace Questionable.Model.V1.Converter;
+
+public sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
+{
+ private static readonly Dictionary<EDialogChoiceType, string> Values = new()
+ {
+ { EDialogChoiceType.YesNo, "YesNo" },
+ { EDialogChoiceType.List, "List" },
+ };
+}
{ EInteractionType.Duty, "Duty" },
{ EInteractionType.SinglePlayerDuty, "SinglePlayerDuty" },
{ EInteractionType.Jump, "Jump" },
- { EInteractionType.CutsceneSelectString, "CutsceneSelectString" },
{ EInteractionType.ShouldBeAJump, "ShouldBeAJump" },
{ EInteractionType.Instruction, "Instruction" },
};
-namespace Questionable.Model.V1;
+using System.Text.Json.Serialization;
+using Questionable.Model.V1.Converter;
-public sealed class DialogueChoice
+namespace Questionable.Model.V1;
+
+public class DialogueChoice
{
+ [JsonConverter(typeof(DialogueChoiceTypeConverter))]
+ public EDialogChoiceType Type { get; set; }
public string? ExcelSheet { get; set; }
- public string Answer { get; set; } = null!;
+ public string Prompt { get; set; } = null!;
+ public bool Yes { get; set; } = true;
+ public string? Answer { get; set; }
}
--- /dev/null
+namespace Questionable.Model.V1;
+
+public enum EDialogChoiceType
+{
+ None,
+ YesNo,
+ List
+}
Duty,
SinglePlayerDuty,
Jump,
- CutsceneSelectString,
/// <summary>
/// Needs to be adjusted for coords etc. in the quest data.
public required int Sequence { get; set; }
public string? Comment { get; set; }
public List<QuestStep> Steps { get; set; } = new();
+
+ public QuestStep? FindStep(int step)
+ {
+ if (step < 0 || step >= Steps.Count)
+ return null;
+
+ return Steps[step];
+ }
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
- <Version>0.2</Version>
+ <Version>0.3</Version>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
private readonly ICommandManager _commandManager;
private readonly GameFunctions _gameFunctions;
private readonly QuestController _questController;
-
private readonly MovementController _movementController;
+ private readonly GameUiController _gameUiController;
public QuestionablePlugin(DalamudPluginInterface pluginInterface, IClientState clientState,
ITargetManager targetManager, IFramework framework, IGameGui gameGui, IDataManager dataManager,
ISigScanner sigScanner, IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui,
- ICommandManager commandManager)
+ ICommandManager commandManager, IAddonLifecycle addonLifecycle)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(sigScanner);
new MovementController(navmeshIpc, clientState, _gameFunctions, condition, pluginLog);
_questController = new QuestController(pluginInterface, dataManager, _clientState, _gameFunctions,
_movementController, pluginLog, condition, chatGui, framework, gameGui, aetheryteData, lifestreamIpc);
+ _gameUiController =
+ new GameUiController(clientState, addonLifecycle, dataManager, _gameFunctions, _questController, pluginLog);
+
_windowSystem.AddWindow(new DebugWindow(_movementController, _questController, _gameFunctions, clientState,
targetManager));
_framework.Update -= FrameworkUpdate;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
+ _gameUiController.Dispose();
_movementController.Dispose();
}
}
using System.Globalization;
-using System.Linq;
using System.Numerics;
-using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
-using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
};
}
+ public override bool DrawConditions()
+ {
+ if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
+ return false;
+
+ var currentQuest = _questController.CurrentQuest;
+ return currentQuest == null || !currentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType);
+ }
+
public override unsafe void Draw()
{
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)