1
0
forked from liza/Questionable

Quest automation + various fixes

This commit is contained in:
Liza 2024-06-12 18:03:48 +02:00
parent 1356818abe
commit 410d891f7f
Signed by: liza
GPG Key ID: 7199F8D727D55F67
55 changed files with 1016 additions and 231 deletions

View File

@ -68,7 +68,15 @@
"Z": -464.7746 "Z": -464.7746
}, },
"TerritoryId": 956, "TerritoryId": 956,
"InteractionType": "Interact" "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}, },
{ {
"DataId": 1038716, "DataId": 1038716,
@ -78,7 +86,15 @@
"Z": -458.2132 "Z": -458.2132
}, },
"TerritoryId": 956, "TerritoryId": 956,
"InteractionType": "Interact" "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
} }
] ]
}, },
@ -108,8 +124,8 @@
"Z": -519.18823 "Z": -519.18823
}, },
"TerritoryId": 956, "TerritoryId": 956,
"InteractionType": "SinglePlayerDuty", "InteractionType": "WaitForManualProgress",
"Comment": "Duty - Shoot Large Green Bird" "Comment": "Shoot Large Green Bird"
} }
] ]
}, },

View File

@ -126,7 +126,15 @@
"Z": -108.537415 "Z": -108.537415
}, },
"TerritoryId": 956, "TerritoryId": 956,
"InteractionType": "Interact" "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}, },
{ {
"DataId": 1038707, "DataId": 1038707,
@ -136,7 +144,15 @@
"Z": -150.04199 "Z": -150.04199
}, },
"TerritoryId": 956, "TerritoryId": 956,
"InteractionType": "Interact" "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 1038708, "DataId": 1038708,
@ -146,7 +162,15 @@
"Z": -130.11371 "Z": -130.11371
}, },
"TerritoryId": 956, "TerritoryId": 956,
"InteractionType": "Interact" "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
} }
] ]
}, },

View File

@ -35,13 +35,13 @@
"Comment": "A Frosty Reception", "Comment": "A Frosty Reception",
"DialogueChoices": [ "DialogueChoices": [
{ {
"Type": "ContentTalkList", "Type": "List",
"Prompt": "264", "Prompt": 264,
"Answer": "267" "Answer": 267
}, },
{ {
"Type": "ContentTalkYesNo", "Type": "YesNo",
"Prompt": "268", "Prompt": 268,
"Yes": true "Yes": true
} }
] ]

View File

@ -97,7 +97,7 @@
}, },
"TerritoryId": 960, "TerritoryId": 960,
"InteractionType": "WaitForManualProgress", "InteractionType": "WaitForManualProgress",
"Comment": "Duty - Find Errant Omicron" "Comment": "Find Errant Omicron"
} }
] ]
}, },

View File

@ -113,7 +113,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] Aetheryte Plaza", "[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
}, },
{ {
"DataId": 1032121, "DataId": 1032121,

View File

@ -33,7 +33,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] Aetheryte Plaza", "[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
} }
] ]
}, },
@ -82,7 +83,15 @@
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "UseItem", "InteractionType": "UseItem",
"ItemId": 2002904, "ItemId": 2002904,
"$.1": "QuestVariables if done first: 1 32 0 0 0 64" "$.1": "QuestVariables if done first: 1 32 0 0 0 64",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 1027909, "DataId": 1027909,
@ -94,7 +103,15 @@
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "UseItem", "InteractionType": "UseItem",
"ItemId": 2002904, "ItemId": 2002904,
"$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96" "$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"DataId": 1027939, "DataId": 1027939,
@ -105,7 +122,15 @@
}, },
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "UseItem", "InteractionType": "UseItem",
"ItemId": 2002904 "ItemId": 2002904,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
} }
] ]
}, },

View File

@ -28,7 +28,14 @@
"Z": 305.19568 "Z": 305.19568
}, },
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMG104_03676_Q2_000_100",
"Answer": "TEXT_LUCKMG104_03676_A1_000_100"
}
]
} }
] ]
}, },
@ -61,7 +68,16 @@
"StopDistance": 1, "StopDistance": 1,
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "Interact", "InteractionType": "Interact",
"$.1": "QuestVariables if done first: 16 16 16 0 0 32" "$.1": "QuestVariables if done first: 16 16 16 0 0 32",
"Fly": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"DataId": 2010810, "DataId": 2010810,
@ -72,7 +88,15 @@
}, },
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "Interact", "InteractionType": "Interact",
"$.1": "QuestVariables if done after [1]: 33 16 32 0 0 96" "$.1": "QuestVariables if done after [1]: 33 16 32 0 0 96",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 2010809, "DataId": 2010809,
@ -83,13 +107,31 @@
}, },
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "Interact", "InteractionType": "Interact",
"Comment": "Combat not necessary to progress quest" "Comment": "Combat not necessary to progress quest",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
} }
] ]
}, },
{ {
"Sequence": 4, "Sequence": 4,
"Steps": [ "Steps": [
{
"Position": {
"X": 47.593674,
"Y": 42.681213,
"Z": -511.2799
},
"TerritoryId": 815,
"InteractionType": "WalkTo",
"Comment": "Should be far enough to reset combat"
},
{ {
"DataId": 1031732, "DataId": 1031732,
"Position": { "Position": {
@ -99,7 +141,14 @@
}, },
"TerritoryId": 815, "TerritoryId": 815,
"InteractionType": "Interact", "InteractionType": "Interact",
"AetheryteShortcut": "Amh Araeng - Inn at Journey's Head" "AetheryteShortcut": "Amh Araeng - Inn at Journey's Head",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMG104_03676_Q3_000_200",
"Answer": "TEXT_LUCKMG104_03676_A1_000_200"
}
]
} }
] ]
}, },

View File

@ -29,7 +29,15 @@
}, },
"TerritoryId": 820, "TerritoryId": 820,
"InteractionType": "Interact", "InteractionType": "Interact",
"$.1": "QuestValues if done first: 16 1 16 0 0 128" "$.1": "QuestValues if done first: 16 1 16 0 0 128",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}, },
{ {
"DataId": 1027602, "DataId": 1027602,
@ -44,7 +52,15 @@
"[Eulmore] Aetheryte Plaza", "[Eulmore] Aetheryte Plaza",
"[Eulmore] Southeast Derelicts" "[Eulmore] Southeast Derelicts"
], ],
"$.1": "QuestValues if done after [1]: 32 17 16 16 0 160" "$.1": "QuestValues if done after [1]: 32 17 16 16 0 160",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"DataId": 1029990, "DataId": 1029990,
@ -58,6 +74,14 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Eulmore] Southeast Derelicts", "[Eulmore] Southeast Derelicts",
"[Eulmore] The Glory Gate" "[Eulmore] The Glory Gate"
],
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
] ]
} }
] ]

View File

@ -44,7 +44,7 @@
"Z": -161.45575 "Z": -161.45575
}, },
"TerritoryId": 814, "TerritoryId": 814,
"InteractionType": "SinglePlayerDuty", "InteractionType": "WaitForManualProgress",
"Comment": "Help Master Chai dodge enemies" "Comment": "Help Master Chai dodge enemies"
} }
] ]

View File

@ -36,7 +36,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] Aetheryte Plaza", "[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
}, },
{ {
"DataId": 1032121, "DataId": 1032121,
@ -74,7 +75,14 @@
}, },
"TerritoryId": 351, "TerritoryId": 351,
"InteractionType": "SinglePlayerDuty", "InteractionType": "SinglePlayerDuty",
"Comment": "Estinien vs. Arch Ultima" "Comment": "Estinien vs. Arch Ultima",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMG110_03682_Q1_100_125",
"Yes": true
}
]
} }
] ]
}, },

View File

@ -33,7 +33,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] Aetheryte Plaza", "[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
}, },
{ {
"DataId": 1032121, "DataId": 1032121,

View File

@ -28,7 +28,8 @@
"Z": 14.22064 "Z": 14.22064
}, },
"TerritoryId": 844, "TerritoryId": 844,
"InteractionType": "Interact" "InteractionType": "Interact",
"TargetTerritoryId": 819
}, },
{ {
"DataId": 1030370, "DataId": 1030370,
@ -38,7 +39,14 @@
"Z": 13.321045 "Z": 13.321045
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMH103_03763_Q1_000_500",
"Answer": "TEXT_LUCKMH103_03763_A1_000_500"
}
]
} }
] ]
}, },

View File

@ -33,7 +33,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] Aetheryte Plaza", "[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
}, },
{ {
"DataId": 1031722, "DataId": 1031722,
@ -58,7 +59,8 @@
"Z": 14.206055 "Z": 14.206055
}, },
"TerritoryId": 844, "TerritoryId": 844,
"InteractionType": "Interact" "InteractionType": "Interact",
"TargetTerritoryId": 819
}, },
{ {
"DataId": 1027248, "DataId": 1027248,
@ -71,7 +73,15 @@
"InteractionType": "Interact", "InteractionType": "Interact",
"Comment": "Chessamile", "Comment": "Chessamile",
"$.0": "[1]", "$.0": "[1]",
"$.1": "QuestVariables if done first: 1 0 0 0 0 64" "$.1": "QuestVariables if done first: 1 0 0 0 0 64",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 1027224, "DataId": 1027224,
@ -84,7 +94,15 @@
"InteractionType": "Interact", "InteractionType": "Interact",
"Comment": "Bragi", "Comment": "Bragi",
"$.0": "[2]", "$.0": "[2]",
"$.1": "QuestVariables if done after [1]: 2 0 0 0 0 192" "$.1": "QuestVariables if done after [1]: 2 0 0 0 0 192",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}, },
{ {
"DataId": 1027322, "DataId": 1027322,
@ -98,7 +116,15 @@
"InteractionType": "Interact", "InteractionType": "Interact",
"Comment": "Glynard", "Comment": "Glynard",
"$.0": "[3]", "$.0": "[3]",
"$.1": "QuestVariables if done after [1, 2]: 3 0 0 0 0 200" "$.1": "QuestVariables if done after [1, 2]: 3 0 0 0 0 200",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
8
]
}, },
{ {
"DataId": 1027232, "DataId": 1027232,
@ -115,7 +141,15 @@
"[Crystarium] The Crystalline Mean" "[Crystarium] The Crystalline Mean"
], ],
"$.0": "[4]", "$.0": "[4]",
"$.1": "QuestVariables if done after [1, 2, 3]: 4 0 0 0 0 216" "$.1": "QuestVariables if done after [1, 2, 3]: 4 0 0 0 0 216",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
16
]
}, },
{ {
"DataId": 1027226, "DataId": 1027226,
@ -130,6 +164,14 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] The Crystalline Mean", "[Crystarium] The Crystalline Mean",
"[Crystarium] The Cabinet of Curiosity" "[Crystarium] The Cabinet of Curiosity"
],
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
] ]
} }
] ]

View File

@ -47,7 +47,31 @@
"TerritoryId": 817, "TerritoryId": 817,
"InteractionType": "SinglePlayerDuty", "InteractionType": "SinglePlayerDuty",
"Fly": true, "Fly": true,
"Comment": "Duty - A Sleep Disturbed (Opo-Opo, Wolf, Serpent)" "Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)",
"$": "The dialogue choices and data ids here are recycled",
"DialogueChoices": [
{
"Type": "YesNo",
"DataId": 2011009,
"ExcelSheet": "GimmickYesNo",
"Prompt": 138,
"Yes": true
},
{
"Type": "YesNo",
"DataId": 2011006,
"ExcelSheet": "GimmickYesNo",
"Prompt": 139,
"Yes": true
},
{
"Type": "YesNo",
"DataId": 2011007,
"ExcelSheet": "GimmickYesNo",
"Prompt": 142,
"Yes": true
}
]
} }
] ]
}, },

View File

@ -36,6 +36,19 @@
{ {
"Sequence": 2, "Sequence": 2,
"Steps": [ "Steps": [
{
"Position": {
"X": -475.38354,
"Y": 400.55338,
"Z": -779.4299
},
"TerritoryId": 818,
"InteractionType": "WalkTo",
"Fly": true,
"SkipIf": [
"FlyingLocked"
]
},
{ {
"Position": { "Position": {
"X": -423.6145, "X": -423.6145,

View File

@ -28,7 +28,7 @@
}, },
"TerritoryId": 813, "TerritoryId": 813,
"InteractionType": "WalkTo", "InteractionType": "WalkTo",
"AetheryteShortcut": "Lakeland - Ostall Imperative", "AetheryteShortcut": "Lakeland - Fort Jobb",
"Fly": true "Fly": true
}, },
{ {
@ -95,7 +95,15 @@
"TerritoryId": 813, "TerritoryId": 813,
"InteractionType": "Interact", "InteractionType": "Interact",
"DisableNavmesh": true, "DisableNavmesh": true,
"$.1": "QuestVariables if done first: 1 0 0 0 0 64" "$.1": "QuestVariables if done first: 1 0 0 0 0 64",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 2010278, "DataId": 2010278,
@ -106,7 +114,15 @@
}, },
"TerritoryId": 813, "TerritoryId": 813,
"InteractionType": "Interact", "InteractionType": "Interact",
"DisableNavmesh": true "DisableNavmesh": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}, },
{ {
"DataId": 2010282, "DataId": 2010282,
@ -117,7 +133,16 @@
}, },
"TerritoryId": 813, "TerritoryId": 813,
"InteractionType": "Interact", "InteractionType": "Interact",
"DisableNavmesh": true "DisableNavmesh": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
],
"Comment": "TODO Check if pathfinding works automatically now"
} }
] ]
}, },

View File

@ -1,7 +1,6 @@
{ {
"$schema": "https://carvel.li/questionable/quest-1.0", "$schema": "https://carvel.li/questionable/quest-1.0",
"Author": "liza", "Author": "liza",
"Comment": "TODO Missing quest end",
"TerritoryBlacklist": [ "TerritoryBlacklist": [
898 898
], ],
@ -33,6 +32,7 @@
}, },
"TerritoryId": 814, "TerritoryId": 814,
"InteractionType": "Interact", "InteractionType": "Interact",
"AetheryteShortcut": "Kholusia - Wright",
"Fly": true "Fly": true
} }
] ]
@ -46,6 +46,21 @@
"ContentFinderConditionId": 714 "ContentFinderConditionId": 714
} }
] ]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1032549,
"Position": {
"X": -1.9074707,
"Y": -200.00002,
"Z": -425.10114
},
"TerritoryId": 918,
"InteractionType": "Interact"
}
]
} }
] ]
} }

View File

@ -18,8 +18,7 @@
] ]
}, },
{ {
"Sequence": 1, "Sequence": 2,
"Comment": "TODO verify this is the correct sequence and/or where sequence 2 went",
"Steps": [ "Steps": [
{ {
"DataId": 1032529, "DataId": 1032529,
@ -67,7 +66,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] The Amaro Launch", "[Crystarium] The Amaro Launch",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
}, },
{ {
"DataId": 1032121, "DataId": 1032121,
@ -92,7 +92,8 @@
"Z": 14.206055 "Z": 14.206055
}, },
"TerritoryId": 844, "TerritoryId": 844,
"InteractionType": "Interact" "InteractionType": "Interact",
"TargetTerritoryId": 819
}, },
{ {
"DataId": 1030610, "DataId": 1030610,
@ -107,6 +108,13 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] The Dossal Gate", "[Crystarium] The Dossal Gate",
"[Crystarium] The Pendants" "[Crystarium] The Pendants"
],
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMH110_03770_Q1_000_600",
"Yes": true
}
] ]
} }
] ]

View File

@ -48,7 +48,14 @@
"Z": -277.7906 "Z": -277.7906
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMI101_03771_Q3_000_148",
"Answer": "TEXT_LUCKMI101_03771_A3_000_149"
}
]
} }
] ]
}, },
@ -98,6 +105,14 @@
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact", "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
],
"$.1": "QuestVariables if done first: 1 16 0 0 0 64" "$.1": "QuestVariables if done first: 1 16 0 0 0 64"
}, },
{ {
@ -110,6 +125,14 @@
"StopDistance": 5, "StopDistance": 5,
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact", "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
],
"$.1": "QuestVariables if done after [1]: 2 32 0 0 0 192" "$.1": "QuestVariables if done after [1]: 2 32 0 0 0 192"
}, },
{ {
@ -120,7 +143,15 @@
"Z": 173.38818 "Z": 173.38818
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
} }
] ]
}, },
@ -142,15 +173,6 @@
{ {
"Sequence": 255, "Sequence": 255,
"Steps": [ "Steps": [
{
"Position": {
"X": -140.22343,
"Y": 0.05337119,
"Z": 34.20123
},
"TerritoryId": 819,
"InteractionType": "WalkTo"
},
{ {
"DataId": 1027248, "DataId": 1027248,
"Position": { "Position": {
@ -159,7 +181,8 @@
"Z": -51.438232 "Z": -51.438232
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"AetheryteShortcut": "Crystarium"
} }
] ]
} }

View File

@ -38,6 +38,7 @@
}, },
{ {
"Sequence": 2, "Sequence": 2,
"Comment": "This isn't solving for the 'best' results, but for the closest waypoints",
"Steps": [ "Steps": [
{ {
"DataId": 2011078, "DataId": 2011078,
@ -56,7 +57,15 @@
], ],
"Fly": true, "Fly": true,
"$.0": "[1]", "$.0": "[1]",
"$.1": "QuestVariables if done first: 0 0 0 3 0 0 during fight; 16 1 0 2 16 8 after" "$.1": "QuestVariables if done first: 0 0 0 3 0 0 during fight; 16 1 0 2 16 8 after",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
8
]
}, },
{ {
"DataId": 2011076, "DataId": 2011076,
@ -69,10 +78,19 @@
"InteractionType": "Combat", "InteractionType": "Combat",
"EnemySpawnType": "AfterItemUse", "EnemySpawnType": "AfterItemUse",
"ItemId": 2003001, "ItemId": 2003001,
"KillEnemyDataIds": [], "KillEnemyDataIds": [
"Comment": "TODO Missing enemy ids", 12166
],
"Fly": true, "Fly": true,
"$.1": "QuestVariables if done after [1]: 34 1 0 1 48 40" "$.1": "QuestVariables if done after [1]: 34 1 0 1 48 40",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"DataId": 2011079, "DataId": 2011079,
@ -86,10 +104,11 @@
"EnemySpawnType": "AfterItemUse", "EnemySpawnType": "AfterItemUse",
"ItemId": 2003001, "ItemId": 2003001,
"KillEnemyDataIds": [ "KillEnemyDataIds": [
12168
], ],
"Comment": "TODO Missing enemy ids", "Comment": "TODO Missing enemy ids",
"Fly": true, "Fly": true,
"$.2": "QuestVariables if done after [1, 2]: 0 64 0 0 0 0" "$.2": "QuestVariables if done after [1, 2]: irrelevant because it automatically progresses to the next step"
} }
] ]
}, },

View File

@ -79,7 +79,8 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Crystarium] Aetheryte Plaza", "[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate" "[Crystarium] The Dossal Gate"
] ],
"TargetTerritoryId": 844
}, },
{ {
"DataId": 1033819, "DataId": 1033819,

View File

@ -42,7 +42,14 @@
"Z": 604.27246 "Z": 604.27246
}, },
"TerritoryId": 814, "TerritoryId": 814,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMI105_03775_Q2_000_052",
"Yes": true
}
]
} }
] ]
}, },

View File

@ -16,12 +16,19 @@
"Z": 1.6021729 "Z": 1.6021729
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMI108_03778_Q1_000_001",
"Answer": "TEXT_LUCKMI108_03778_A1_000_002"
}
]
} }
] ]
}, },
{ {
"Sequence": 1, "Sequence": 2,
"Steps": [ "Steps": [
{ {
"TerritoryId": 931, "TerritoryId": 931,

View File

@ -85,8 +85,13 @@
"Sequence": 5, "Sequence": 5,
"Steps": [ "Steps": [
{ {
"Position": {
"X": 0,
"Y": 0,
"Z": 0
},
"TerritoryId": 820, "TerritoryId": 820,
"InteractionType": "Interact", "InteractionType": "WalkTo",
"AetheryteShortcut": "Eulmore" "AetheryteShortcut": "Eulmore"
} }
] ]

View File

@ -75,7 +75,15 @@
"Z": 3.982544 "Z": 3.982544
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"AetheryteShortcut": "Crystarium",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMI111_03781_Q1_000_153",
"Answer": "TEXT_LUCKMI111_03781_A1_000_154"
}
]
} }
] ]
}, },

View File

@ -29,7 +29,14 @@
"Z": 7.156433 "Z": 7.156433
}, },
"TerritoryId": 819, "TerritoryId": 819,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMI112_03782_Q1_000_007",
"Yes": true
}
]
}, },
{ {
"DataId": 1033888, "DataId": 1033888,
@ -39,7 +46,14 @@
"Z": -5.081299 "Z": -5.081299
}, },
"TerritoryId": 844, "TerritoryId": 844,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMI112_03782_Q2_000_044",
"Yes": true
}
]
} }
] ]
}, },

View File

@ -64,7 +64,8 @@
"EnemySpawnType": "AfterInteraction", "EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [ "KillEnemyDataIds": [
12661 12661
] ],
"Fly": true
} }
] ]
}, },

View File

@ -55,7 +55,8 @@
"Z": -6.9733887 "Z": -6.9733887
}, },
"TerritoryId": 351, "TerritoryId": 351,
"InteractionType": "Interact" "InteractionType": "Interact",
"TargetTerritoryId": 351
}, },
{ {
"DataId": 2011332, "DataId": 2011332,

View File

@ -14,7 +14,14 @@
}, },
"StopDistance": 5, "StopDistance": 5,
"TerritoryId": 351, "TerritoryId": 351,
"InteractionType": "Interact" "InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMJ104_04010_Q1_000_000",
"Answer": "TEXT_LUCKMJ104_04010_A1_000_002"
}
]
} }
] ]
}, },

View File

@ -29,7 +29,14 @@
}, },
"TerritoryId": 129, "TerritoryId": 129,
"InteractionType": "Interact", "InteractionType": "Interact",
"AetheryteShortcut": "Limsa Lominsa" "AetheryteShortcut": "Limsa Lominsa",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMJ108_04014_SYSTEM_100_010",
"Yes": true
}
]
}, },
{ {
"DataId": 1002694, "DataId": 1002694,

View File

@ -39,6 +39,26 @@
{ {
"Sequence": 2, "Sequence": 2,
"Steps": [ "Steps": [
{
"Position": {
"X": 46.600548,
"Y": 77.45801,
"Z": -366.82053
},
"TerritoryId": 180,
"InteractionType": "WalkTo",
"Fly": true
},
{
"Position": {
"X": 111.927666,
"Y": 26.050894,
"Z": -612.8873
},
"TerritoryId": 180,
"InteractionType": "WalkTo",
"Fly": true
},
{ {
"Position": { "Position": {
"X": 82.19566, "X": 82.19566,

View File

@ -79,7 +79,15 @@
"TerritoryId": 402, "TerritoryId": 402,
"InteractionType": "Interact", "InteractionType": "Interact",
"Fly": true, "Fly": true,
"$.1": "QuestVariables if done first: 1 16 0 0 0 64" "$.1": "QuestVariables if done first: 1 16 0 0 0 64",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 1036359, "DataId": 1036359,
@ -92,7 +100,15 @@
"InteractionType": "Interact", "InteractionType": "Interact",
"Mount": true, "Mount": true,
"Fly": true, "Fly": true,
"$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96" "$.1": "QuestVariables if done after [1]: 2 16 0 0 0 96",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"DataId": 1036357, "DataId": 1036357,
@ -103,7 +119,15 @@
}, },
"TerritoryId": 402, "TerritoryId": 402,
"InteractionType": "Interact", "InteractionType": "Interact",
"DisableNavmesh": true "DisableNavmesh": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
} }
] ]
}, },
@ -119,7 +143,14 @@
}, },
"TerritoryId": 402, "TerritoryId": 402,
"InteractionType": "Interact", "InteractionType": "Interact",
"Fly": true "Fly": true,
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMK103_04060_Q1_000_100",
"Answer": "TEXT_LUCKMK103_04060_A2_000_100"
}
]
} }
] ]
}, },

View File

@ -33,6 +33,13 @@
"AethernetShortcut": [ "AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza", "[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Alchemists' Guild" "[Ul'dah] Alchemists' Guild"
],
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_LUCKMK106_04063_Q1_000_100",
"Answer": "TEXT_LUCKMK106_04063_A2_000_100"
}
] ]
} }
] ]

View File

@ -672,49 +672,40 @@
"type": "string", "type": "string",
"enum": [ "enum": [
"YesNo", "YesNo",
"List", "List"
"ContentTalkYesNo",
"ContentTalkList"
] ]
}, },
"ExcelSheet": { "ExcelSheet": {
"type": "string" "type": "string"
},
"Prompt": {
"type": [
"string",
"null"
]
} }
}, },
"required": [ "required": [
"Type", "Type"
"Prompt"
], ],
"allOf": [ "allOf": [
{ {
"if": { "if": {
"properties": { "properties": {
"Type": { "Type": {
"anyOf": [
{
"const": "YesNo" "const": "YesNo"
},
{
"const": "ContentTalkYesNo"
}
]
} }
} }
}, },
"then": { "then": {
"properties": { "properties": {
"Prompt": {
"type": [
"string",
"integer"
]
},
"Yes": { "Yes": {
"type": "boolean", "type": "boolean",
"default": true "default": true
} }
}, },
"required": [ "required": [
"Prompt",
"Yes" "Yes"
] ]
} }
@ -723,24 +714,28 @@
"if": { "if": {
"properties": { "properties": {
"Type": { "Type": {
"anyOf": [
{
"const": "List" "const": "List"
},
{
"const": "ContentTalkList"
}
]
} }
} }
}, },
"then": { "then": {
"properties": { "properties": {
"Prompt": {
"type": [
"string",
"integer",
"null"
]
},
"Answer": { "Answer": {
"type": "string" "type": [
"string",
"integer"
]
} }
}, },
"required": [ "required": [
"Prompt",
"Answer" "Answer"
] ]
} }

View File

@ -6,5 +6,19 @@ namespace Questionable;
internal sealed class Configuration : IPluginConfiguration internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public WindowConfig DebugWindowConfig { get; set; } = new(); public GeneralConfiguration General { get; } = new();
public AdvancedConfiguration Advanced { get; } = new();
public WindowConfig DebugWindowConfig { get; } = new();
public WindowConfig ConfigWindowConfig { get; } = new();
internal sealed class GeneralConfiguration
{
public bool AutoAcceptNextQuest { get; set; }
public uint MountId { get; set; } = 71;
}
internal sealed class AdvancedConfiguration
{
public bool NeverFly { get; set; }
}
} }

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@ -23,16 +24,19 @@ internal sealed class GameUiController : IDisposable
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly QuestController _questController; private readonly QuestController _questController;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly ITargetManager _targetManager;
private readonly ILogger<GameUiController> _logger; private readonly ILogger<GameUiController> _logger;
public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions, public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions,
QuestController questController, IGameGui gameGui, ILogger<GameUiController> logger) QuestController questController, IGameGui gameGui, ITargetManager targetManager,
ILogger<GameUiController> logger)
{ {
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_dataManager = dataManager; _dataManager = dataManager;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_questController = questController; _questController = questController;
_gameGui = gameGui; _gameGui = gameGui;
_targetManager = targetManager;
_logger = logger; _logger = logger;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
@ -181,40 +185,26 @@ internal sealed class GameUiController : IDisposable
foreach (var dialogueChoice in dialogueChoices) foreach (var dialogueChoice in dialogueChoices)
{ {
if (dialogueChoice.Type != EDialogChoiceType.List)
continue;
if (dialogueChoice.Answer == null) if (dialogueChoice.Answer == null)
{ {
_logger.LogInformation("Ignoring entry in DialogueChoices, no answer"); _logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
continue; continue;
} }
string? excelPrompt = null, excelAnswer; if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
switch (dialogueChoice.Type)
{ {
case EDialogChoiceType.ContentTalkList: _logger.LogDebug(
if (dialogueChoice.Prompt != null) "Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
{ dialogueChoice.DataId, _targetManager.Target?.DataId);
excelPrompt =
_gameFunctions.GetContentTalk(uint.Parse(dialogueChoice.Prompt,
CultureInfo.InvariantCulture));
}
excelAnswer =
_gameFunctions.GetContentTalk(uint.Parse(dialogueChoice.Answer, CultureInfo.InvariantCulture));
break;
case EDialogChoiceType.List:
if (dialogueChoice.Prompt != null)
{
excelPrompt =
_gameFunctions.GetDialogueText(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
}
excelAnswer =
_gameFunctions.GetDialogueText(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer);
break;
default:
continue; continue;
} }
string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
string? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer);
if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt)) if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
{ {
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt); _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
@ -288,29 +278,24 @@ internal sealed class GameUiController : IDisposable
_logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count); _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
foreach (var dialogueChoice in dialogueChoices) foreach (var dialogueChoice in dialogueChoices)
{ {
string? excelPrompt; if (dialogueChoice.Type != EDialogChoiceType.YesNo)
if (dialogueChoice.Prompt != null)
{
switch (dialogueChoice.Type)
{
case EDialogChoiceType.ContentTalkYesNo:
excelPrompt =
_gameFunctions.GetContentTalk(uint.Parse(dialogueChoice.Prompt,
CultureInfo.InvariantCulture));
break;
case EDialogChoiceType.YesNo:
excelPrompt =
_gameFunctions.GetDialogueText(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
break;
default:
continue; continue;
}
}
else
excelPrompt = null;
if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)) 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; continue;
}
string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
excelPrompt, actualPrompt);
continue;
}
addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1); addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
if (!checkAllSteps) if (!checkAllSteps)
@ -343,7 +328,8 @@ internal sealed class GameUiController : IDisposable
increaseStepCount = false; increaseStepCount = false;
if (step != null) if (step != null)
_logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, step.TargetTerritoryId); _logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId,
step.TargetTerritoryId);
} }
if (step == null || step.TargetTerritoryId == null) if (step == null || step.TargetTerritoryId == null)
@ -403,6 +389,19 @@ internal sealed class GameUiController : IDisposable
return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-'); return a.ReplaceLineEndings().Replace('\u2013', '-') == b.ReplaceLineEndings().Replace('\u2013', '-');
} }
private string? ResolveReference(Quest quest, string? excelSheet, ExcelRef? excelRef)
{
if (excelRef == null)
return null;
if (excelRef.Type == ExcelRef.EType.Key)
return _gameFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey());
else if (excelRef.Type == ExcelRef.EType.RowId)
return _gameFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId());
return null;
}
public void Dispose() public void Dispose()
{ {
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);

View File

@ -76,7 +76,7 @@ internal sealed class MovementController : IDisposable
} }
} }
else if (!Destination.IsFlying && !_condition[ConditionFlag.Mounted] && navPoints.Count > 0 && else if (!Destination.IsFlying && !_condition[ConditionFlag.Mounted] && navPoints.Count > 0 &&
!_gameFunctions.HasStatusPreventingSprintOrMount() && Destination.CanSprint) !_gameFunctions.HasStatusPreventingSprintOrMount(true) && Destination.CanSprint)
{ {
float actualDistance = 0; float actualDistance = 0;
foreach (Vector3 end in navPoints) foreach (Vector3 end in navPoints)
@ -210,7 +210,7 @@ internal sealed class MovementController : IDisposable
_logger.LogInformation("Pathfinding to {Destination}", Destination); _logger.LogInformation("Pathfinding to {Destination}", Destination);
_cancellationTokenSource = new(); _cancellationTokenSource = new();
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10)); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
_pathfindTask = _pathfindTask =
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token); _navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token);
} }

View File

@ -10,6 +10,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Data; using Questionable.Data;
@ -28,6 +29,7 @@ internal sealed class QuestController
private readonly ILogger<QuestController> _logger; private readonly ILogger<QuestController> _logger;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState; private readonly IKeyState _keyState;
private readonly Configuration _configuration;
private readonly IReadOnlyList<ITaskFactory> _taskFactories; private readonly IReadOnlyList<ITaskFactory> _taskFactories;
private readonly Queue<ITask> _taskQueue = new(); private readonly Queue<ITask> _taskQueue = new();
@ -41,6 +43,7 @@ internal sealed class QuestController
ILogger<QuestController> logger, ILogger<QuestController> logger,
QuestRegistry questRegistry, QuestRegistry questRegistry,
IKeyState keyState, IKeyState keyState,
Configuration configuration,
IEnumerable<ITaskFactory> taskFactories) IEnumerable<ITaskFactory> taskFactories)
{ {
_clientState = clientState; _clientState = clientState;
@ -49,6 +52,7 @@ internal sealed class QuestController
_logger = logger; _logger = logger;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_keyState = keyState; _keyState = keyState;
_configuration = configuration;
_taskFactories = taskFactories.ToList().AsReadOnly(); _taskFactories = taskFactories.ToList().AsReadOnly();
} }
@ -79,6 +83,20 @@ internal sealed class QuestController
if (CurrentQuest != null && CurrentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType)) if (CurrentQuest != null && CurrentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType))
return; return;
// not verified to work
if (_automatic && _currentTask == null && _taskQueue.Count == 0 && CurrentQuest is { Sequence: 0, Step: 255 }
&& DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
{
_logger.LogWarning("Quest accept apparently didn't work out, resetting progress");
CurrentQuest = CurrentQuest with
{
Step = 0
};
ExecuteNextStep(true);
return;
}
UpdateCurrentTask(); UpdateCurrentTask();
} }
@ -102,7 +120,13 @@ internal sealed class QuestController
{ {
_logger.LogInformation("New quest: {QuestName}", quest.Name); _logger.LogInformation("New quest: {QuestName}", quest.Name);
CurrentQuest = new QuestProgress(quest, currentSequence, 0); CurrentQuest = new QuestProgress(quest, currentSequence, 0);
Stop("Different Quest");
bool continueAutomatically = _configuration.General.AutoAcceptNextQuest;
if (_clientState.LocalPlayer?.Level < quest.Level)
continueAutomatically = false;
Stop("Different Quest", continueAutomatically);
} }
else if (CurrentQuest != null) else if (CurrentQuest != null)
{ {
@ -208,7 +232,7 @@ internal sealed class QuestController
CurrentQuest = CurrentQuest with CurrentQuest = CurrentQuest with
{ {
Step = CurrentQuest.Step + 1, Step = CurrentQuest.Step + 1,
StepProgress = new() StepProgress = new(DateTime.Now),
}; };
} }
else else
@ -216,7 +240,7 @@ internal sealed class QuestController
CurrentQuest = CurrentQuest with CurrentQuest = CurrentQuest with
{ {
Step = 255, Step = 255,
StepProgress = new() StepProgress = new(DateTime.Now),
}; };
} }
@ -416,6 +440,8 @@ internal sealed class QuestController
public bool HasCurrentTaskMatching<T>() => public bool HasCurrentTaskMatching<T>() =>
_currentTask is T; _currentTask is T;
public bool IsRunning => _currentTask != null || _taskQueue.Count > 0;
public sealed record QuestProgress( public sealed record QuestProgress(
Quest Quest, Quest Quest,
byte Sequence, byte Sequence,
@ -423,12 +449,13 @@ internal sealed class QuestController
StepProgress StepProgress) StepProgress StepProgress)
{ {
public QuestProgress(Quest quest, byte sequence, int step) public QuestProgress(Quest quest, byte sequence, int step)
: this(quest, sequence, step, new StepProgress()) : this(quest, sequence, step, new StepProgress(DateTime.Now))
{ {
} }
} }
// TODO is this still required? // TODO is this still required?
public sealed record StepProgress( public sealed record StepProgress(
DateTime StartedAt,
int DialogueChoicesSelected = 0); int DialogueChoicesSelected = 0);
} }

View File

@ -57,6 +57,7 @@ internal sealed class QuestRegistry
continue; continue;
quest.Name = questData.Name.ToString(); quest.Name = questData.Name.ToString();
quest.Level = questData.ClassJobLevel0;
} }
} }

View File

@ -108,7 +108,13 @@ internal static class AethernetShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (aetheryteData.IsCityAetheryte(To)) if (aetheryteData.IsAirshipLanding(To))
{
if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, To) > 5)
return ETaskResult.StillRunning;
}
else if (aetheryteData.IsCityAetheryte(To))
{ {
if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, To) > 11) clientState.TerritoryType, To) > 11)

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.BaseTasks; using Questionable.Controller.Steps.BaseTasks;
using Questionable.Model; using Questionable.Model;
@ -23,7 +24,7 @@ internal static class WaitAtEnd
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>() var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
.With(quest, step); .With(quest, step);
var delay = serviceProvider.GetRequiredService<WaitDelay>(); var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, new NextStep()]; return [task, delay, Next(quest, sequence, step)];
} }
switch (step.InteractionType) switch (step.InteractionType)
@ -41,7 +42,7 @@ internal static class WaitAtEnd
case EInteractionType.WalkTo: case EInteractionType.WalkTo:
case EInteractionType.Jump: case EInteractionType.Jump:
// no need to wait if we're just moving around // no need to wait if we're just moving around
return [new NextStep()]; return [Next(quest, sequence, step)];
case EInteractionType.WaitForObjectAtPosition: case EInteractionType.WaitForObjectAtPosition:
ArgumentNullException.ThrowIfNull(step.DataId); ArgumentNullException.ThrowIfNull(step.DataId);
@ -52,7 +53,7 @@ internal static class WaitAtEnd
serviceProvider.GetRequiredService<WaitObjectAtPosition>() serviceProvider.GetRequiredService<WaitObjectAtPosition>()
.With(step.DataId.Value, step.Position.Value), .With(step.DataId.Value, step.Position.Value),
serviceProvider.GetRequiredService<WaitDelay>(), serviceProvider.GetRequiredService<WaitDelay>(),
new NextStep() Next(quest, sequence, step)
]; ];
case EInteractionType.Interact when step.TargetTerritoryId != null: case EInteractionType.Interact when step.TargetTerritoryId != null:
@ -81,17 +82,40 @@ internal static class WaitAtEnd
[ [
waitInteraction, waitInteraction,
serviceProvider.GetRequiredService<WaitDelay>(), serviceProvider.GetRequiredService<WaitDelay>(),
new NextStep() Next(quest, sequence, step)
]; ];
case EInteractionType.Interact: case EInteractionType.Interact:
default: default:
return [serviceProvider.GetRequiredService<WaitDelay>(), new NextStep()]; return [serviceProvider.GetRequiredService<WaitDelay>(), Next(quest, sequence, step)];
} }
} }
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
public ITask Next(Quest quest, QuestSequence sequence, QuestStep step)
{
bool lastStep = step == sequence.Steps.LastOrDefault();
if (sequence.Sequence == 0 && lastStep)
{
return new WaitConditionTask(() =>
{
unsafe
{
var questManager = QuestManager.Instance();
return questManager != null && questManager->IsQuestAccepted(quest.QuestId);
}
}, "Wait(questAccepted)");
}
else if (sequence.Sequence == 255 && lastStep)
{
return new WaitConditionTask(() => QuestManager.IsQuestComplete(quest.QuestId),
"Wait(questComplete)");
}
else
return new NextStep();
}
} }
internal sealed class WaitDelay() : AbstractDelayedTask(TimeSpan.FromSeconds(1)) internal sealed class WaitDelay() : AbstractDelayedTask(TimeSpan.FromSeconds(1))

View File

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Conditions; using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -8,6 +9,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
: ITask : ITask
{ {
private bool _unmountTriggered; private bool _unmountTriggered;
private DateTime _unmountedAt = DateTime.MinValue;
public bool Start() public bool Start()
{ {
@ -16,6 +18,8 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
logger.LogInformation("Step explicitly wants no mount, trying to unmount..."); logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
_unmountTriggered = gameFunctions.Unmount(); _unmountTriggered = gameFunctions.Unmount();
if (_unmountTriggered)
_unmountedAt = DateTime.Now;
return true; return true;
} }
@ -24,9 +28,15 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
if (!_unmountTriggered) if (!_unmountTriggered)
{ {
_unmountTriggered = gameFunctions.Unmount(); _unmountTriggered = gameFunctions.Unmount();
if (_unmountTriggered)
_unmountedAt = DateTime.Now;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (DateTime.Now < _unmountedAt.AddSeconds(1))
return ETaskResult.StillRunning;
return condition[ConditionFlag.Mounted] return condition[ConditionFlag.Mounted]
? ETaskResult.StillRunning ? ETaskResult.StillRunning
: ETaskResult.TaskComplete; : ETaskResult.TaskComplete;

View File

@ -45,8 +45,38 @@ internal static class UseItem
=> throw new InvalidOperationException(); => throw new InvalidOperationException();
} }
internal abstract class UseItemBase : ITask
{
private bool _usedItem;
private DateTime _continueAt;
internal sealed class UseOnGround(GameFunctions gameFunctions) : AbstractDelayedTask protected abstract bool UseItem();
public bool Start()
{
_usedItem = UseItem();
_continueAt = DateTime.Now.AddSeconds(2);
return true;
}
public ETaskResult Update()
{
if (DateTime.Now > _continueAt)
return ETaskResult.StillRunning;
if (!_usedItem)
{
_usedItem = UseItem();
_continueAt = DateTime.Now.AddSeconds(2);
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
}
internal sealed class UseOnGround(GameFunctions gameFunctions) : UseItemBase
{ {
public uint DataId { get; set; } public uint DataId { get; set; }
public uint ItemId { get; set; } public uint ItemId { get; set; }
@ -58,16 +88,12 @@ internal static class UseItem
return this; return this;
} }
protected override bool StartInternal() protected override bool UseItem() => gameFunctions.UseItemOnGround(DataId, ItemId);
{
gameFunctions.UseItemOnGround(DataId, ItemId);
return true;
}
public override string ToString() => $"UseItem({ItemId} on ground at {DataId})"; public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
} }
internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask internal sealed class UseOnObject(GameFunctions gameFunctions) : UseItemBase
{ {
public uint DataId { get; set; } public uint DataId { get; set; }
public uint ItemId { get; set; } public uint ItemId { get; set; }
@ -79,16 +105,12 @@ internal static class UseItem
return this; return this;
} }
protected override bool StartInternal() protected override bool UseItem() => gameFunctions.UseItem(DataId, ItemId);
{
gameFunctions.UseItem(DataId, ItemId);
return true;
}
public override string ToString() => $"UseItem({ItemId} on {DataId})"; public override string ToString() => $"UseItem({ItemId} on {DataId})";
} }
internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask internal sealed class Use(GameFunctions gameFunctions) : UseItemBase
{ {
public uint ItemId { get; set; } public uint ItemId { get; set; }
@ -98,11 +120,7 @@ internal static class UseItem
return this; return this;
} }
protected override bool StartInternal() protected override bool UseItem() => gameFunctions.UseItem(ItemId);
{
gameFunctions.UseItem(ItemId);
return true;
}
public override string ToString() => $"UseItem({ItemId})"; public override string ToString() => $"UseItem({ItemId})";
} }

View File

@ -18,10 +18,12 @@ internal sealed class DalamudInitializer : IDisposable
private readonly NavigationShortcutController _navigationShortcutController; private readonly NavigationShortcutController _navigationShortcutController;
private readonly WindowSystem _windowSystem; private readonly WindowSystem _windowSystem;
private readonly DebugWindow _debugWindow; private readonly DebugWindow _debugWindow;
private readonly ConfigWindow _configWindow;
public DalamudInitializer(DalamudPluginInterface pluginInterface, IFramework framework, public DalamudInitializer(DalamudPluginInterface pluginInterface, IFramework framework,
ICommandManager commandManager, QuestController questController, MovementController movementController, ICommandManager commandManager, QuestController questController, MovementController movementController,
GameUiController gameUiController, NavigationShortcutController navigationShortcutController, WindowSystem windowSystem, DebugWindow debugWindow) GameUiController gameUiController, NavigationShortcutController navigationShortcutController,
WindowSystem windowSystem, DebugWindow debugWindow, ConfigWindow configWindow)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_framework = framework; _framework = framework;
@ -31,9 +33,14 @@ internal sealed class DalamudInitializer : IDisposable
_navigationShortcutController = navigationShortcutController; _navigationShortcutController = navigationShortcutController;
_windowSystem = windowSystem; _windowSystem = windowSystem;
_debugWindow = debugWindow; _debugWindow = debugWindow;
_configWindow = configWindow;
_windowSystem.AddWindow(debugWindow);
_windowSystem.AddWindow(configWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += _debugWindow.Toggle; _pluginInterface.UiBuilder.OpenMainUi += _debugWindow.Toggle;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
_framework.Update += FrameworkUpdate; _framework.Update += FrameworkUpdate;
_commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)
{ {
@ -60,6 +67,9 @@ internal sealed class DalamudInitializer : IDisposable
private void ProcessCommand(string command, string arguments) private void ProcessCommand(string command, string arguments)
{ {
if (arguments is "c" or "config")
_configWindow.Toggle();
else
_debugWindow.Toggle(); _debugWindow.Toggle();
} }
@ -69,5 +79,7 @@ internal sealed class DalamudInitializer : IDisposable
_framework.Update -= FrameworkUpdate; _framework.Update -= FrameworkUpdate;
_pluginInterface.UiBuilder.OpenMainUi -= _debugWindow.Toggle; _pluginInterface.UiBuilder.OpenMainUi -= _debugWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_windowSystem.RemoveAllWindows();
} }
} }

View File

@ -217,6 +217,18 @@ internal sealed class AetheryteData
} }
.AsReadOnly(); .AsReadOnly();
/// <summary>
/// Airship landings are special as they're one-way only (except for Radz-at-Han, which is a normal aetheryte).
/// </summary>
public ReadOnlyDictionary<EAetheryteLocation, Vector3> AirshipLandingLocations { get; } =
new Dictionary<EAetheryteLocation, Vector3>
{
{ EAetheryteLocation.LimsaAirship, new(-19.44352f, 91.99999f, -9.892939f) },
{ EAetheryteLocation.GridaniaAirship, new(24.86354f, -19.000002f, 96f) },
{ EAetheryteLocation.UldahAirship, new(-16.954851f, 82.999985f, -9.421141f) },
{ EAetheryteLocation.KuganeAirship, new(-55.72525f, 79.10602f, 46.23109f) },
}.AsReadOnly();
public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; } public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; }
public ReadOnlyDictionary<EAetheryteLocation, ushort> TerritoryIds { get; } public ReadOnlyDictionary<EAetheryteLocation, ushort> TerritoryIds { get; }
public IReadOnlyList<ushort> TownTerritoryIds { get; set; } public IReadOnlyList<ushort> TownTerritoryIds { get; set; }
@ -232,9 +244,22 @@ internal sealed class AetheryteData
return (fromPosition - toPosition).Length(); return (fromPosition - toPosition).Length();
} }
public float CalculateAirshipLandingDistance(Vector3 fromPosition, ushort fromTerritoryType, EAetheryteLocation to)
{
if (!TerritoryIds.TryGetValue(to, out ushort toTerritoryType) || fromTerritoryType != toTerritoryType)
return float.MaxValue;
if (!AirshipLandingLocations.TryGetValue(to, out Vector3 toPosition))
return float.MaxValue;
return (fromPosition - toPosition).Length();
}
public bool IsCityAetheryte(EAetheryteLocation aetheryte) public bool IsCityAetheryte(EAetheryteLocation aetheryte)
{ {
var territoryId = TerritoryIds[aetheryte]; var territoryId = TerritoryIds[aetheryte];
return TownTerritoryIds.Contains(territoryId); return TownTerritoryIds.Contains(territoryId);
} }
public bool IsAirshipLanding(EAetheryteLocation aetheryte) => AirshipLandingLocations.ContainsKey(aetheryte);
} }

View File

@ -23,13 +23,17 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using Lumina.Excel.CustomSheets; using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets2;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Model.V1; using Questionable.Model.V1;
using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara; using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition;
using ContentTalk = Lumina.Excel.GeneratedSheets.ContentTalk;
using Emote = Lumina.Excel.GeneratedSheets.Emote;
using GameObject = Dalamud.Game.ClientState.Objects.Types.GameObject; using GameObject = Dalamud.Game.ClientState.Objects.Types.GameObject;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType;
namespace Questionable; namespace Questionable;
@ -56,11 +60,12 @@ internal sealed unsafe class GameFunctions
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly Configuration _configuration;
private readonly ILogger<GameFunctions> _logger; private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry, ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
IGameGui gameGui, ILogger<GameFunctions> logger) IGameGui gameGui, Configuration configuration, ILogger<GameFunctions> logger)
{ {
_dataManager = dataManager; _dataManager = dataManager;
_objectTable = objectTable; _objectTable = objectTable;
@ -69,6 +74,7 @@ internal sealed unsafe class GameFunctions
_clientState = clientState; _clientState = clientState;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_gameGui = gameGui; _gameGui = gameGui;
_configuration = configuration;
_logger = logger; _logger = logger;
_processChatBox = _processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat)); Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
@ -364,29 +370,33 @@ internal sealed unsafe class GameFunctions
return false; return false;
} }
public void UseItem(uint itemId) public bool UseItem(uint itemId)
{ {
AgentInventoryContext.Instance()->UseItem(itemId); return AgentInventoryContext.Instance()->UseItem(itemId) == 0;
} }
public void UseItem(uint dataId, uint itemId) public bool UseItem(uint dataId, uint itemId)
{ {
GameObject? gameObject = FindObjectByDataId(dataId); GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null) if (gameObject != null)
{ {
_targetManager.Target = gameObject; _targetManager.Target = gameObject;
AgentInventoryContext.Instance()->UseItem(itemId); return AgentInventoryContext.Instance()->UseItem(itemId) == 0;
}
} }
public void UseItemOnGround(uint dataId, uint itemId) return false;
}
public bool UseItemOnGround(uint dataId, uint itemId)
{ {
GameObject? gameObject = FindObjectByDataId(dataId); GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null) if (gameObject != null)
{ {
var position = (FFXIVClientStructs.FFXIV.Common.Math.Vector3)gameObject.Position; var position = (FFXIVClientStructs.FFXIV.Common.Math.Vector3)gameObject.Position;
ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position); return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, location: &position);
} }
return false;
} }
public void UseEmote(uint dataId, EEmote emote) public void UseEmote(uint dataId, EEmote emote)
@ -410,8 +420,11 @@ internal sealed unsafe class GameFunctions
return gameObject != null && (gameObject.Position - position).Length() < 0.05f; return gameObject != null && (gameObject.Position - position).Length() < 0.05f;
} }
public bool HasStatusPreventingSprintOrMount() public bool HasStatusPreventingSprintOrMount(bool skipConfigCheck = false)
{ {
if (!skipConfigCheck && _configuration.Advanced.NeverFly)
return true;
if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone()) if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
return true; return true;
@ -437,13 +450,14 @@ internal sealed unsafe class GameFunctions
return true; return true;
var playerState = PlayerState.Instance(); var playerState = PlayerState.Instance();
if (playerState != null && playerState->IsMountUnlocked(71)) if (playerState != null && _configuration.General.MountId != 0 &&
playerState->IsMountUnlocked(_configuration.General.MountId))
{ {
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0) if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId) == 0)
{ {
if (ActionManager.Instance()->UseAction(ActionType.Mount, 71)) if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId))
{ {
_logger.LogInformation("Using SDS Fenrir as mount"); _logger.LogInformation("Using preferred mount");
return true; return true;
} }
@ -526,11 +540,21 @@ internal sealed unsafe class GameFunctions
return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString(); return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString();
} }
public string? GetContentTalk(uint rowId) public string? GetDialogueTextByRowId(string? excelSheet, uint rowId)
{
if (excelSheet == "GimmickYesNo")
{
var questRow = _dataManager.GetExcelSheet<GimmickYesNo>()!.GetRow(rowId);
return questRow?.Unknown0?.ToString();
}
else if (excelSheet is "ContentTalk" or null)
{ {
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId); var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
return questRow?.Text?.ToString(); return questRow?.Text?.ToString();
} }
else
throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}");
}
public bool IsOccupied() public bool IsOccupied()
{ {

View File

@ -7,6 +7,7 @@ internal sealed class Quest
{ {
public required ushort QuestId { get; init; } public required ushort QuestId { get; init; }
public required string Name { get; set; } public required string Name { get; set; }
public ushort Level { get; set; }
public required QuestData Data { get; init; } public required QuestData Data { get; init; }
public QuestSequence? FindSequence(byte currentSequence) public QuestSequence? FindSequence(byte currentSequence)

View File

@ -8,7 +8,5 @@ internal sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoic
{ {
{ EDialogChoiceType.YesNo, "YesNo" }, { EDialogChoiceType.YesNo, "YesNo" },
{ EDialogChoiceType.List, "List" }, { EDialogChoiceType.List, "List" },
{ EDialogChoiceType.ContentTalkYesNo, "ContentTalkYesNo" },
{ EDialogChoiceType.ContentTalkList, "ContentTalkList" },
}; };
} }

View File

@ -0,0 +1,30 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.V1.Converter;
internal sealed class ExcelRefConverter : JsonConverter<ExcelRef>
{
public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
return new ExcelRef(reader.GetString()!);
else if (reader.TokenType == JsonTokenType.Number)
return new ExcelRef(reader.GetUInt32());
else
return null;
}
public override void Write(Utf8JsonWriter writer, ExcelRef? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else if (value.Type == ExcelRef.EType.Key)
writer.WriteStringValue(value.AsKey());
else if (value.Type == ExcelRef.EType.RowId)
writer.WriteNumberValue(value.AsRowId());
else
throw new JsonException();
}
}

View File

@ -10,7 +10,17 @@ internal sealed class DialogueChoice
[JsonConverter(typeof(DialogueChoiceTypeConverter))] [JsonConverter(typeof(DialogueChoiceTypeConverter))]
public EDialogChoiceType Type { get; set; } public EDialogChoiceType Type { get; set; }
public string? ExcelSheet { get; set; } public string? ExcelSheet { get; set; }
public string? Prompt { get; set; }
[JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Prompt { get; set; }
public bool Yes { get; set; } = true; public bool Yes { get; set; } = true;
public string? Answer { get; set; }
[JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Answer { get; set; }
/// <summary>
/// If set, only applies when focusing the given target id.
/// </summary>
public uint? DataId { get; set; }
} }

View File

@ -5,6 +5,4 @@ internal enum EDialogChoiceType
None, None,
YesNo, YesNo,
List, List,
ContentTalkYesNo,
ContentTalkList,
} }

View File

@ -0,0 +1,48 @@
using System;
namespace Questionable.Model.V1;
public class ExcelRef
{
private readonly string? _stringValue;
private readonly uint? _rowIdValue;
public ExcelRef(string value)
{
_stringValue = value;
_rowIdValue = null;
Type = EType.Key;
}
public ExcelRef(uint value)
{
_stringValue = null;
_rowIdValue = value;
Type = EType.RowId;
}
public EType Type { get; }
public string AsKey()
{
if (Type != EType.Key)
throw new InvalidOperationException();
return _stringValue!;
}
public uint AsRowId()
{
if (Type != EType.RowId)
throw new InvalidOperationException();
return _rowIdValue!.Value;
}
public enum EType
{
None,
Key,
RowId,
}
}

View File

@ -106,6 +106,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<NavigationShortcutController>(); serviceCollection.AddSingleton<NavigationShortcutController>();
serviceCollection.AddSingleton<DebugWindow>(); serviceCollection.AddSingleton<DebugWindow>();
serviceCollection.AddSingleton<ConfigWindow>();
serviceCollection.AddSingleton<DalamudInitializer>(); serviceCollection.AddSingleton<DalamudInitializer>();
_serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider = serviceCollection.BuildServiceProvider();

View File

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Interface.Colors;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using LLib.ImGui;
using Lumina.Excel.GeneratedSheets;
namespace Questionable.Windows;
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly uint[] _mountIds;
private readonly string[] _mountNames;
[SuppressMessage("Performance", "CA1861", Justification = "One time initialization")]
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager)
: base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize)
{
_pluginInterface = pluginInterface;
_configuration = configuration;
var mounts = dataManager.GetExcelSheet<Mount>()!
.Where(x => x is { RowId: > 0, Icon: > 0 })
.Select(x => (MountId: x.RowId, Name: x.Singular.ToString()))
.Where(x => !string.IsNullOrEmpty(x.Name))
.OrderBy(x => x.Name)
.ToList();
_mountIds = new uint[] { 0 }.Concat(mounts.Select(x => x.MountId)).ToArray();
_mountNames = new[] { "Mount Roulette" }.Concat(mounts.Select(x => x.Name)).ToArray();
}
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw()
{
if (ImGui.BeginTabBar("QuestionableConfigTabs"))
{
if (ImGui.BeginTabItem("General"))
{
int selectedMount = Array.FindIndex(_mountIds, x => x == _configuration.General.MountId);
if (selectedMount == -1)
{
selectedMount = 0;
_configuration.General.MountId = _mountIds[selectedMount];
Save();
}
if (ImGui.Combo("Preferred Mount", ref selectedMount, _mountNames, _mountNames.Length))
{
_configuration.General.MountId = _mountIds[selectedMount];
Save();
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Advanced"))
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"Enabling any option here may cause unexpected behavior. Use at your own risk.");
ImGui.Separator();
bool neverFly = _configuration.Advanced.NeverFly;
if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly))
{
_configuration.Advanced.NeverFly = neverFly;
Save();
}
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
}
private void Save() => _pluginInterface.SavePluginConfig(_configuration);
public void SaveWindowConfig() => Save();
}

View File

@ -23,10 +23,9 @@ using Questionable.Model.V1;
namespace Questionable.Windows; namespace Questionable.Windows;
internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposable internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
{ {
private readonly DalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
private readonly WindowSystem _windowSystem;
private readonly MovementController _movementController; private readonly MovementController _movementController;
private readonly QuestController _questController; private readonly QuestController _questController;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
@ -37,14 +36,19 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger<DebugWindow> _logger; private readonly ILogger<DebugWindow> _logger;
public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem, public DebugWindow(DalamudPluginInterface pluginInterface,
MovementController movementController, QuestController questController, GameFunctions gameFunctions, MovementController movementController,
IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController, QuestController questController,
Configuration configuration, ILogger<DebugWindow> logger) GameFunctions gameFunctions,
IClientState clientState,
IFramework framework,
ITargetManager targetManager,
GameUiController gameUiController,
Configuration configuration,
ILogger<DebugWindow> logger)
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize) : base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_windowSystem = windowSystem;
_movementController = movementController; _movementController = movementController;
_questController = questController; _questController = questController;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
@ -61,8 +65,6 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
MinimumSize = new Vector2(200, 30), MinimumSize = new Vector2(200, 30),
MaximumSize = default MaximumSize = default
}; };
_windowSystem.AddWindow(this);
} }
public WindowConfig WindowConfig => _configuration.DebugWindowConfig; public WindowConfig WindowConfig => _configuration.DebugWindowConfig;
@ -127,6 +129,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.Text(_questController.ToStatString()); ImGui.Text(_questController.ToStatString());
//ImGui.EndDisabled(); //ImGui.EndDisabled();
ImGui.BeginDisabled(_questController.IsRunning);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{ {
_questController.ExecuteNextStep(true); _questController.ExecuteNextStep(true);
@ -139,6 +142,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
_questController.ExecuteNextStep(false); _questController.ExecuteNextStep(false);
} }
ImGui.EndDisabled();
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
@ -151,7 +155,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
.FindSequence(currentQuest.Sequence) .FindSequence(currentQuest.Sequence)
?.FindStep(currentQuest.Step); ?.FindStep(currentQuest.Step);
bool colored = currentStep != null && currentStep.InteractionType == EInteractionType.Instruction bool colored = currentStep != null && currentStep.InteractionType == EInteractionType.Instruction
&& _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>(); && _questController
.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
if (colored) if (colored)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
@ -161,8 +166,16 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
_questController.Stop("Manual"); _questController.Stop("Manual");
_questController.IncreaseStepCount(); _questController.IncreaseStepCount();
} }
if (colored) if (colored)
ImGui.PopStyleColor(); ImGui.PopStyleColor();
bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;
if (ImGui.Checkbox("Automatically accept next quest", ref autoAcceptNextQuest))
{
_configuration.General.AutoAcceptNextQuest = autoAcceptNextQuest;
_pluginInterface.SavePluginConfig(_configuration);
}
} }
else else
ImGui.Text("No active quest"); ImGui.Text("No active quest");
@ -261,7 +274,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
} }
else else
{ {
if (ImGui.Button($"Copy")) ImGui.Button($"Copy");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{ {
ImGui.SetClipboardText($$""" ImGui.SetClipboardText($$"""
"Position": { "Position": {
@ -273,6 +287,12 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
"InteractionType": "" "InteractionType": ""
"""); """);
} }
else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
Vector3 position = _clientState.LocalPlayer!.Position;
ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
$"new({position.X}f, {position.Y}f, {position.Z}f)"));
}
} }
} }
@ -317,9 +337,4 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.EndDisabled(); ImGui.EndDisabled();
} }
} }
public void Dispose()
{
_windowSystem.RemoveWindow(this);
}
} }