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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,8 @@
"AethernetShortcut": [
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
],
"TargetTerritoryId": 844
}
]
},
@ -82,7 +83,15 @@
"TerritoryId": 815,
"InteractionType": "UseItem",
"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,
@ -94,7 +103,15 @@
"TerritoryId": 815,
"InteractionType": "UseItem",
"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,
@ -105,7 +122,15 @@
},
"TerritoryId": 815,
"InteractionType": "UseItem",
"ItemId": 2002904
"ItemId": 2002904,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}
]
},

View File

@ -28,7 +28,14 @@
"Z": 305.19568
},
"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,
"TerritoryId": 815,
"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,
@ -72,7 +88,15 @@
},
"TerritoryId": 815,
"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,
@ -83,13 +107,31 @@
},
"TerritoryId": 815,
"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,
"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,
"Position": {
@ -99,7 +141,14 @@
},
"TerritoryId": 815,
"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,
"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,
@ -44,7 +52,15 @@
"[Eulmore] Aetheryte Plaza",
"[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,
@ -58,6 +74,14 @@
"AethernetShortcut": [
"[Eulmore] Southeast Derelicts",
"[Eulmore] The Glory Gate"
],
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}
]

View File

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

View File

@ -36,7 +36,8 @@
"AethernetShortcut": [
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
],
"TargetTerritoryId": 844
},
{
"DataId": 1032121,
@ -74,7 +75,14 @@
},
"TerritoryId": 351,
"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": [
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
],
"TargetTerritoryId": 844
},
{
"DataId": 1032121,

View File

@ -28,7 +28,8 @@
"Z": 14.22064
},
"TerritoryId": 844,
"InteractionType": "Interact"
"InteractionType": "Interact",
"TargetTerritoryId": 819
},
{
"DataId": 1030370,
@ -38,7 +39,14 @@
"Z": 13.321045
},
"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": [
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
],
"TargetTerritoryId": 844
},
{
"DataId": 1031722,
@ -58,7 +59,8 @@
"Z": 14.206055
},
"TerritoryId": 844,
"InteractionType": "Interact"
"InteractionType": "Interact",
"TargetTerritoryId": 819
},
{
"DataId": 1027248,
@ -71,7 +73,15 @@
"InteractionType": "Interact",
"Comment": "Chessamile",
"$.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,
@ -84,7 +94,15 @@
"InteractionType": "Interact",
"Comment": "Bragi",
"$.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,
@ -98,7 +116,15 @@
"InteractionType": "Interact",
"Comment": "Glynard",
"$.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,
@ -115,7 +141,15 @@
"[Crystarium] The Crystalline Mean"
],
"$.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,
@ -130,6 +164,14 @@
"AethernetShortcut": [
"[Crystarium] The Crystalline Mean",
"[Crystarium] The Cabinet of Curiosity"
],
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}
]

View File

@ -47,7 +47,31 @@
"TerritoryId": 817,
"InteractionType": "SinglePlayerDuty",
"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,
"Steps": [
{
"Position": {
"X": -475.38354,
"Y": 400.55338,
"Z": -779.4299
},
"TerritoryId": 818,
"InteractionType": "WalkTo",
"Fly": true,
"SkipIf": [
"FlyingLocked"
]
},
{
"Position": {
"X": -423.6145,

View File

@ -28,7 +28,7 @@
},
"TerritoryId": 813,
"InteractionType": "WalkTo",
"AetheryteShortcut": "Lakeland - Ostall Imperative",
"AetheryteShortcut": "Lakeland - Fort Jobb",
"Fly": true
},
{
@ -95,7 +95,15 @@
"TerritoryId": 813,
"InteractionType": "Interact",
"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,
@ -106,7 +114,15 @@
},
"TerritoryId": 813,
"InteractionType": "Interact",
"DisableNavmesh": true
"DisableNavmesh": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
},
{
"DataId": 2010282,
@ -117,7 +133,16 @@
},
"TerritoryId": 813,
"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",
"Author": "liza",
"Comment": "TODO Missing quest end",
"TerritoryBlacklist": [
898
],
@ -33,6 +32,7 @@
},
"TerritoryId": 814,
"InteractionType": "Interact",
"AetheryteShortcut": "Kholusia - Wright",
"Fly": true
}
]
@ -46,6 +46,21 @@
"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,
"Comment": "TODO verify this is the correct sequence and/or where sequence 2 went",
"Sequence": 2,
"Steps": [
{
"DataId": 1032529,
@ -67,7 +66,8 @@
"AethernetShortcut": [
"[Crystarium] The Amaro Launch",
"[Crystarium] The Dossal Gate"
]
],
"TargetTerritoryId": 844
},
{
"DataId": 1032121,
@ -92,7 +92,8 @@
"Z": 14.206055
},
"TerritoryId": 844,
"InteractionType": "Interact"
"InteractionType": "Interact",
"TargetTerritoryId": 819
},
{
"DataId": 1030610,
@ -107,6 +108,13 @@
"AethernetShortcut": [
"[Crystarium] The Dossal Gate",
"[Crystarium] The Pendants"
],
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMH110_03770_Q1_000_600",
"Yes": true
}
]
}
]

View File

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

View File

@ -38,6 +38,7 @@
},
{
"Sequence": 2,
"Comment": "This isn't solving for the 'best' results, but for the closest waypoints",
"Steps": [
{
"DataId": 2011078,
@ -56,7 +57,15 @@
],
"Fly": true,
"$.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,
@ -69,10 +78,19 @@
"InteractionType": "Combat",
"EnemySpawnType": "AfterItemUse",
"ItemId": 2003001,
"KillEnemyDataIds": [],
"Comment": "TODO Missing enemy ids",
"KillEnemyDataIds": [
12166
],
"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,
@ -86,10 +104,11 @@
"EnemySpawnType": "AfterItemUse",
"ItemId": 2003001,
"KillEnemyDataIds": [
12168
],
"Comment": "TODO Missing enemy ids",
"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": [
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
],
"TargetTerritoryId": 844
},
{
"DataId": 1033819,

View File

@ -42,7 +42,14 @@
"Z": 604.27246
},
"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
},
"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": [
{
"TerritoryId": 931,

View File

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

View File

@ -75,7 +75,15 @@
"Z": 3.982544
},
"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
},
"TerritoryId": 819,
"InteractionType": "Interact"
"InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMI112_03782_Q1_000_007",
"Yes": true
}
]
},
{
"DataId": 1033888,
@ -39,7 +46,14 @@
"Z": -5.081299
},
"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",
"KillEnemyDataIds": [
12661
]
],
"Fly": true
}
]
},

View File

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

View File

@ -14,7 +14,14 @@
},
"StopDistance": 5,
"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,
"InteractionType": "Interact",
"AetheryteShortcut": "Limsa Lominsa"
"AetheryteShortcut": "Limsa Lominsa",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_LUCKMJ108_04014_SYSTEM_100_010",
"Yes": true
}
]
},
{
"DataId": 1002694,

View File

@ -39,6 +39,26 @@
{
"Sequence": 2,
"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": {
"X": 82.19566,

View File

@ -79,7 +79,15 @@
"TerritoryId": 402,
"InteractionType": "Interact",
"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,
@ -92,7 +100,15 @@
"InteractionType": "Interact",
"Mount": 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,
@ -103,7 +119,15 @@
},
"TerritoryId": 402,
"InteractionType": "Interact",
"DisableNavmesh": true
"DisableNavmesh": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
}
]
},
@ -119,7 +143,14 @@
},
"TerritoryId": 402,
"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": [
"[Ul'dah] Aetheryte Plaza",
"[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",
"enum": [
"YesNo",
"List",
"ContentTalkYesNo",
"ContentTalkList"
"List"
]
},
"ExcelSheet": {
"type": "string"
},
"Prompt": {
"type": [
"string",
"null"
]
}
},
"required": [
"Type",
"Prompt"
"Type"
],
"allOf": [
{
"if": {
"properties": {
"Type": {
"anyOf": [
{
"const": "YesNo"
},
{
"const": "ContentTalkYesNo"
}
]
"const": "YesNo"
}
}
},
"then": {
"properties": {
"Prompt": {
"type": [
"string",
"integer"
]
},
"Yes": {
"type": "boolean",
"default": true
}
},
"required": [
"Prompt",
"Yes"
]
}
@ -723,24 +714,28 @@
"if": {
"properties": {
"Type": {
"anyOf": [
{
"const": "List"
},
{
"const": "ContentTalkList"
}
]
"const": "List"
}
}
},
"then": {
"properties": {
"Prompt": {
"type": [
"string",
"integer",
"null"
]
},
"Answer": {
"type": "string"
"type": [
"string",
"integer"
]
}
},
"required": [
"Prompt",
"Answer"
]
}

View File

@ -6,5 +6,19 @@ namespace Questionable;
internal sealed class Configuration : IPluginConfiguration
{
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 Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -23,16 +24,19 @@ internal sealed class GameUiController : IDisposable
private readonly GameFunctions _gameFunctions;
private readonly QuestController _questController;
private readonly IGameGui _gameGui;
private readonly ITargetManager _targetManager;
private readonly ILogger<GameUiController> _logger;
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;
_dataManager = dataManager;
_gameFunctions = gameFunctions;
_questController = questController;
_gameGui = gameGui;
_targetManager = targetManager;
_logger = logger;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
@ -181,40 +185,26 @@ internal sealed class GameUiController : IDisposable
foreach (var dialogueChoice in dialogueChoices)
{
if (dialogueChoice.Type != EDialogChoiceType.List)
continue;
if (dialogueChoice.Answer == null)
{
_logger.LogInformation("Ignoring entry in DialogueChoices, no answer");
_logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
continue;
}
string? excelPrompt = null, excelAnswer;
switch (dialogueChoice.Type)
if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
{
case EDialogChoiceType.ContentTalkList:
if (dialogueChoice.Prompt != null)
{
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;
_logger.LogDebug(
"Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
dialogueChoice.DataId, _targetManager.Target?.DataId);
continue;
}
string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
string? excelAnswer = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Answer);
if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
@ -288,30 +278,25 @@ internal sealed class GameUiController : IDisposable
_logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
foreach (var dialogueChoice in dialogueChoices)
{
string? excelPrompt;
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;
}
}
else
excelPrompt = null;
if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt))
if (dialogueChoice.Type != EDialogChoiceType.YesNo)
continue;
if (dialogueChoice.DataId != null && dialogueChoice.DataId != _targetManager.Target?.DataId)
{
_logger.LogDebug(
"Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}",
dialogueChoice.DataId, _targetManager.Target?.DataId);
continue;
}
string? excelPrompt = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt);
if (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
excelPrompt, actualPrompt);
continue;
}
addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
if (!checkAllSteps)
_questController.IncreaseDialogueChoicesSelected();
@ -343,7 +328,8 @@ internal sealed class GameUiController : IDisposable
increaseStepCount = false;
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)
@ -367,7 +353,7 @@ internal sealed class GameUiController : IDisposable
_logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
//if (increaseStepCount)
//_questController.IncreaseStepCount();
//_questController.IncreaseStepCount();
return;
}
}
@ -403,6 +389,19 @@ internal sealed class GameUiController : IDisposable
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()
{
_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 &&
!_gameFunctions.HasStatusPreventingSprintOrMount() && Destination.CanSprint)
!_gameFunctions.HasStatusPreventingSprintOrMount(true) && Destination.CanSprint)
{
float actualDistance = 0;
foreach (Vector3 end in navPoints)
@ -210,7 +210,7 @@ internal sealed class MovementController : IDisposable
_logger.LogInformation("Pathfinding to {Destination}", Destination);
_cancellationTokenSource = new();
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
_pathfindTask =
_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 FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Data;
@ -28,6 +29,7 @@ internal sealed class QuestController
private readonly ILogger<QuestController> _logger;
private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState;
private readonly Configuration _configuration;
private readonly IReadOnlyList<ITaskFactory> _taskFactories;
private readonly Queue<ITask> _taskQueue = new();
@ -41,6 +43,7 @@ internal sealed class QuestController
ILogger<QuestController> logger,
QuestRegistry questRegistry,
IKeyState keyState,
Configuration configuration,
IEnumerable<ITaskFactory> taskFactories)
{
_clientState = clientState;
@ -49,6 +52,7 @@ internal sealed class QuestController
_logger = logger;
_questRegistry = questRegistry;
_keyState = keyState;
_configuration = configuration;
_taskFactories = taskFactories.ToList().AsReadOnly();
}
@ -79,6 +83,20 @@ internal sealed class QuestController
if (CurrentQuest != null && CurrentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType))
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();
}
@ -102,7 +120,13 @@ internal sealed class QuestController
{
_logger.LogInformation("New quest: {QuestName}", quest.Name);
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)
{
@ -208,7 +232,7 @@ internal sealed class QuestController
CurrentQuest = CurrentQuest with
{
Step = CurrentQuest.Step + 1,
StepProgress = new()
StepProgress = new(DateTime.Now),
};
}
else
@ -216,7 +240,7 @@ internal sealed class QuestController
CurrentQuest = CurrentQuest with
{
Step = 255,
StepProgress = new()
StepProgress = new(DateTime.Now),
};
}
@ -416,6 +440,8 @@ internal sealed class QuestController
public bool HasCurrentTaskMatching<T>() =>
_currentTask is T;
public bool IsRunning => _currentTask != null || _taskQueue.Count > 0;
public sealed record QuestProgress(
Quest Quest,
byte Sequence,
@ -423,12 +449,13 @@ internal sealed class QuestController
StepProgress StepProgress)
{
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?
public sealed record StepProgress(
DateTime StartedAt,
int DialogueChoicesSelected = 0);
}

View File

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

View File

@ -108,7 +108,13 @@ internal static class AethernetShortcut
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,
clientState.TerritoryType, To) > 11)

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Numerics;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.BaseTasks;
using Questionable.Model;
@ -23,7 +24,7 @@ internal static class WaitAtEnd
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
.With(quest, step);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, new NextStep()];
return [task, delay, Next(quest, sequence, step)];
}
switch (step.InteractionType)
@ -41,7 +42,7 @@ internal static class WaitAtEnd
case EInteractionType.WalkTo:
case EInteractionType.Jump:
// no need to wait if we're just moving around
return [new NextStep()];
return [Next(quest, sequence, step)];
case EInteractionType.WaitForObjectAtPosition:
ArgumentNullException.ThrowIfNull(step.DataId);
@ -52,7 +53,7 @@ internal static class WaitAtEnd
serviceProvider.GetRequiredService<WaitObjectAtPosition>()
.With(step.DataId.Value, step.Position.Value),
serviceProvider.GetRequiredService<WaitDelay>(),
new NextStep()
Next(quest, sequence, step)
];
case EInteractionType.Interact when step.TargetTerritoryId != null:
@ -81,17 +82,40 @@ internal static class WaitAtEnd
[
waitInteraction,
serviceProvider.GetRequiredService<WaitDelay>(),
new NextStep()
Next(quest, sequence, step)
];
case EInteractionType.Interact:
default:
return [serviceProvider.GetRequiredService<WaitDelay>(), new NextStep()];
return [serviceProvider.GetRequiredService<WaitDelay>(), Next(quest, sequence, step)];
}
}
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
=> 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))

View File

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Conditions;
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
@ -8,6 +9,7 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
: ITask
{
private bool _unmountTriggered;
private DateTime _unmountedAt = DateTime.MinValue;
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...");
_unmountTriggered = gameFunctions.Unmount();
if (_unmountTriggered)
_unmountedAt = DateTime.Now;
return true;
}
@ -24,9 +28,15 @@ internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> log
if (!_unmountTriggered)
{
_unmountTriggered = gameFunctions.Unmount();
if (_unmountTriggered)
_unmountedAt = DateTime.Now;
return ETaskResult.StillRunning;
}
if (DateTime.Now < _unmountedAt.AddSeconds(1))
return ETaskResult.StillRunning;
return condition[ConditionFlag.Mounted]
? ETaskResult.StillRunning
: ETaskResult.TaskComplete;

View File

@ -45,8 +45,38 @@ internal static class UseItem
=> 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 ItemId { get; set; }
@ -58,16 +88,12 @@ internal static class UseItem
return this;
}
protected override bool StartInternal()
{
gameFunctions.UseItemOnGround(DataId, ItemId);
return true;
}
protected override bool UseItem() => gameFunctions.UseItemOnGround(DataId, ItemId);
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 ItemId { get; set; }
@ -79,16 +105,12 @@ internal static class UseItem
return this;
}
protected override bool StartInternal()
{
gameFunctions.UseItem(DataId, ItemId);
return true;
}
protected override bool UseItem() => gameFunctions.UseItem(DataId, ItemId);
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; }
@ -98,11 +120,7 @@ internal static class UseItem
return this;
}
protected override bool StartInternal()
{
gameFunctions.UseItem(ItemId);
return true;
}
protected override bool UseItem() => gameFunctions.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 WindowSystem _windowSystem;
private readonly DebugWindow _debugWindow;
private readonly ConfigWindow _configWindow;
public DalamudInitializer(DalamudPluginInterface pluginInterface, IFramework framework,
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;
_framework = framework;
@ -31,9 +33,14 @@ internal sealed class DalamudInitializer : IDisposable
_navigationShortcutController = navigationShortcutController;
_windowSystem = windowSystem;
_debugWindow = debugWindow;
_configWindow = configWindow;
_windowSystem.AddWindow(debugWindow);
_windowSystem.AddWindow(configWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += _debugWindow.Toggle;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
_framework.Update += FrameworkUpdate;
_commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)
{
@ -60,7 +67,10 @@ internal sealed class DalamudInitializer : IDisposable
private void ProcessCommand(string command, string arguments)
{
_debugWindow.Toggle();
if (arguments is "c" or "config")
_configWindow.Toggle();
else
_debugWindow.Toggle();
}
public void Dispose()
@ -69,5 +79,7 @@ internal sealed class DalamudInitializer : IDisposable
_framework.Update -= FrameworkUpdate;
_pluginInterface.UiBuilder.OpenMainUi -= _debugWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_windowSystem.RemoveAllWindows();
}
}

View File

@ -217,6 +217,18 @@ internal sealed class AetheryteData
}
.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, ushort> TerritoryIds { get; }
public IReadOnlyList<ushort> TownTerritoryIds { get; set; }
@ -232,9 +244,22 @@ internal sealed class AetheryteData
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)
{
var territoryId = TerritoryIds[aetheryte];
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 LLib.GameUI;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.GeneratedSheets2;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Model.V1;
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 Quest = Questionable.Model.Quest;
using TerritoryType = Lumina.Excel.GeneratedSheets.TerritoryType;
namespace Questionable;
@ -56,11 +60,12 @@ internal sealed unsafe class GameFunctions
private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry;
private readonly IGameGui _gameGui;
private readonly Configuration _configuration;
private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
IGameGui gameGui, ILogger<GameFunctions> logger)
IGameGui gameGui, Configuration configuration, ILogger<GameFunctions> logger)
{
_dataManager = dataManager;
_objectTable = objectTable;
@ -69,6 +74,7 @@ internal sealed unsafe class GameFunctions
_clientState = clientState;
_questRegistry = questRegistry;
_gameGui = gameGui;
_configuration = configuration;
_logger = logger;
_processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
@ -364,29 +370,33 @@ internal sealed unsafe class GameFunctions
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);
if (gameObject != null)
{
_targetManager.Target = gameObject;
AgentInventoryContext.Instance()->UseItem(itemId);
return AgentInventoryContext.Instance()->UseItem(itemId) == 0;
}
return false;
}
public void UseItemOnGround(uint dataId, uint itemId)
public bool UseItemOnGround(uint dataId, uint itemId)
{
GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null)
{
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)
@ -410,8 +420,11 @@ internal sealed unsafe class GameFunctions
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())
return true;
@ -437,13 +450,14 @@ internal sealed unsafe class GameFunctions
return true;
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;
}
@ -526,10 +540,20 @@ internal sealed unsafe class GameFunctions
return excelSheet.FirstOrDefault(x => x.Key == key)?.Value?.ToDalamudString().ToString();
}
public string? GetContentTalk(uint rowId)
public string? GetDialogueTextByRowId(string? excelSheet, uint rowId)
{
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
return questRow?.Text?.ToString();
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);
return questRow?.Text?.ToString();
}
else
throw new ArgumentOutOfRangeException(nameof(excelSheet), $"Unsupported excel sheet {excelSheet}");
}
public bool IsOccupied()

View File

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

View File

@ -8,7 +8,5 @@ internal sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoic
{
{ EDialogChoiceType.YesNo, "YesNo" },
{ 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))]
public EDialogChoiceType Type { 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 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,
YesNo,
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<DebugWindow>();
serviceCollection.AddSingleton<ConfigWindow>();
serviceCollection.AddSingleton<DalamudInitializer>();
_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;
internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposable
internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly WindowSystem _windowSystem;
private readonly MovementController _movementController;
private readonly QuestController _questController;
private readonly GameFunctions _gameFunctions;
@ -37,14 +36,19 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
private readonly Configuration _configuration;
private readonly ILogger<DebugWindow> _logger;
public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
MovementController movementController, QuestController questController, GameFunctions gameFunctions,
IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
Configuration configuration, ILogger<DebugWindow> logger)
public DebugWindow(DalamudPluginInterface pluginInterface,
MovementController movementController,
QuestController questController,
GameFunctions gameFunctions,
IClientState clientState,
IFramework framework,
ITargetManager targetManager,
GameUiController gameUiController,
Configuration configuration,
ILogger<DebugWindow> logger)
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{
_pluginInterface = pluginInterface;
_windowSystem = windowSystem;
_movementController = movementController;
_questController = questController;
_gameFunctions = gameFunctions;
@ -61,8 +65,6 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
MinimumSize = new Vector2(200, 30),
MaximumSize = default
};
_windowSystem.AddWindow(this);
}
public WindowConfig WindowConfig => _configuration.DebugWindowConfig;
@ -127,6 +129,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.Text(_questController.ToStatString());
//ImGui.EndDisabled();
ImGui.BeginDisabled(_questController.IsRunning);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
_questController.ExecuteNextStep(true);
@ -139,6 +142,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
_questController.ExecuteNextStep(false);
}
ImGui.EndDisabled();
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
@ -151,7 +155,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
.FindSequence(currentQuest.Sequence)
?.FindStep(currentQuest.Step);
bool colored = currentStep != null && currentStep.InteractionType == EInteractionType.Instruction
&& _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
&& _questController
.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
if (colored)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
@ -161,8 +166,16 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
_questController.Stop("Manual");
_questController.IncreaseStepCount();
}
if (colored)
ImGui.PopStyleColor();
bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;
if (ImGui.Checkbox("Automatically accept next quest", ref autoAcceptNextQuest))
{
_configuration.General.AutoAcceptNextQuest = autoAcceptNextQuest;
_pluginInterface.SavePluginConfig(_configuration);
}
}
else
ImGui.Text("No active quest");
@ -261,7 +274,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
}
else
{
if (ImGui.Button($"Copy"))
ImGui.Button($"Copy");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ImGui.SetClipboardText($$"""
"Position": {
@ -273,6 +287,12 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
"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();
}
}
public void Dispose()
{
_windowSystem.RemoveWindow(this);
}
}