Compare commits

...

11 Commits
v4.6 ... master

36 changed files with 2204 additions and 529 deletions

View File

@ -48,6 +48,9 @@ internal static class SkipConditionsExtensions
Assignment(nameof(SkipStepConditions.Flying), skipStepConditions.Flying, Assignment(nameof(SkipStepConditions.Flying), skipStepConditions.Flying,
emptyStep.Flying) emptyStep.Flying)
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),
Assignment(nameof(SkipStepConditions.Diving), skipStepConditions.Diving,
emptyStep.Diving)
.AsSyntaxNodeOrToken(),
Assignment(nameof(SkipStepConditions.Chocobo), skipStepConditions.Chocobo, Assignment(nameof(SkipStepConditions.Chocobo), skipStepConditions.Chocobo,
emptyStep.Chocobo) emptyStep.Chocobo)
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),

View File

@ -1,7 +1,6 @@
{ {
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza", "Author": "liza",
"Disabled": true,
"QuestSequence": [ "QuestSequence": [
{ {
"Sequence": 0, "Sequence": 0,
@ -14,7 +13,17 @@
"Z": 59.952637 "Z": 59.952637
}, },
"TerritoryId": 130, "TerritoryId": 130,
"InteractionType": "AcceptQuest" "InteractionType": "AcceptQuest",
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Thaumaturges' Guild"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
} }
] ]
}, },
@ -40,6 +49,126 @@
"NextQuestId": 351 "NextQuestId": 351
} }
] ]
},
{
"Sequence": 2,
"Steps": [
{
"Position": {
"X": -71.92632,
"Y": 9.839797,
"Z": 283.98495
},
"TerritoryId": 141,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"ComplexCombatData": [
{
"DataId": 351,
"MinimumKillCount": 3,
"$": "Slay star marmots as a thaumaturge",
"CompletionQuestVariablesFlags": [
{
"Low": 3
},
null,
null,
null,
null,
null
]
},
{
"DataId": 385,
"MinimumKillCount": 3,
"$": "Slay huge hornets as a thaumaturge",
"CompletionQuestVariablesFlags": [
null,
{
"High": 3
},
null,
null,
null,
null
]
}
],
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Thaumaturges' Guild",
"[Ul'dah] Gate of Nald (Central Thanalan)"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true,
"InTerritory": [
141
]
}
},
"CompletionQuestVariablesFlags": [
{
"Low": 3
},
{
"High": 3
},
null,
null,
null,
null
]
},
{
"Position": {
"X": 39.449,
"Y": 3.082914,
"Z": 272.46896
},
"TerritoryId": 141,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"ComplexCombatData": [
{
"DataId": 205,
"MinimumKillCount": 3,
"$": "Slay snapping shrews as a thaumaturge"
}
],
"CompletionQuestVariablesFlags": [
null,
{
"Low": 3
},
null,
null,
null,
null
]
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1001708,
"Position": {
"X": -250.3548,
"Y": 18,
"Z": 80.88806
},
"TerritoryId": 130,
"InteractionType": "CompleteQuest",
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Thaumaturges' Guild"
],
"NextQuestId": 351
}
]
} }
] ]
} }

View File

@ -0,0 +1,231 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": ["liza", "pot0to"],
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1001708,
"Position": {
"X": -250.3548,
"Y": 18,
"Z": 80.88806
},
"TerritoryId": 130,
"InteractionType": "AcceptQuest",
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Thaumaturges' Guild"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
}
]
},
{
"Sequence": 1,
"Steps": [
{
"DataId": 1001710,
"Position": {
"X": -240.2533,
"Y": 18.8,
"Z": 86.900024
},
"TerritoryId": 130,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 2,
"Steps": [
{
"TerritoryId": 130,
"InteractionType": "None",
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Thaumaturges' Guild",
"[Ul'dah] Gate of Nald (Central Thanalan)"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true,
"InTerritory": [
141
],
"AetheryteUnlocked": "Central Thanalan - Black Brush Station"
},
"AethernetShortcutIf": {
"AetheryteUnlocked": "Central Thanalan - Black Brush Station"
}
}
},
{
"Position": {
"X": -62.183617,
"Y": -3.6582246,
"Z": 145.88391
},
"InteractionType": "WalkTo",
"TerritoryId": 141,
"SkipConditions": {
"StepIf": {
"Flying": "Unlocked",
"AetheryteUnlocked": "Central Thanalan - Black Brush Station"
}
}
},
{
"Position": {
"X": 149.07747,
"Y": -2,
"Z": -225.21188
},
"TerritoryId": 141,
"AetheryteShortcut": "Central Thanalan - Black Brush Station",
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
},
"Fly": true,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"ComplexCombatData": [
{
"DataId": 160,
"MinimumKillCount": 8,
"$": "Slay efts",
"CompletionQuestVariablesFlags": [
{
"Low": 8
},
null,
null,
null,
null,
null
]
}
]
}
]
},
{
"Sequence": 3,
"Steps": [
{
"DataId": 1001710,
"Position": {
"X": -240.2533,
"Y": 18.8,
"Z": 86.900024
},
"TerritoryId": 130,
"InteractionType": "Interact",
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Thaumaturges' Guild"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
},
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_CLSTHM100_00348_Q_000_1",
"Answer": "TEXT_CLSTHM100_00348_A_000_3"
}
]
}
]
},
{
"Sequence": 4,
"Steps": [
{
"DataId": 2001511,
"Position": {
"X": -366.29285,
"Y": -34.989014,
"Z": 293.56824
},
"TerritoryId": 145,
"InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [
166,
1238
],
"Fly": true,
"AetheryteShortcut": "Eastern Thanalan - Camp Drybone",
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
}
]
},
{
"Sequence": 5,
"Steps": [
{
"DataId": 2001498,
"Position": {
"X": -366.29285,
"Y": -34.989014,
"Z": 293.56824
},
"TerritoryId": 145,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 6,
"Steps": [
{
"DataId": 1001710,
"Position": {
"X": -240.2533,
"Y": 18.8,
"Z": 86.900024
},
"TerritoryId": 130,
"InteractionType": "Interact",
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Thaumaturges' Guild"
]
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1001708,
"Position": {
"X": -250.3548,
"Y": 18,
"Z": 80.88806
},
"TerritoryId": 130,
"InteractionType": "CompleteQuest",
"NextQuestId": 350
}
]
}
]
}

View File

@ -163,7 +163,8 @@
"InteractionType": "Combat", "InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction", "EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [ "KillEnemyDataIds": [
166 166,
1238
], ],
"Fly": true, "Fly": true,
"AetheryteShortcut": "Eastern Thanalan - Camp Drybone", "AetheryteShortcut": "Eastern Thanalan - Camp Drybone",

View File

@ -6,14 +6,13 @@
"Sequence": 0, "Sequence": 0,
"Steps": [ "Steps": [
{ {
"DataId": 1000895,
"Position": { "Position": {
"X": -335.74432, "X": -335.8204,
"Y": 12.899764, "Y": 12.899764,
"Z": 1.3884888 "Z": 4.017052
}, },
"TerritoryId": 129, "TerritoryId": 129,
"InteractionType": "AcceptQuest", "InteractionType": "WalkTo",
"AetheryteShortcut": "Limsa Lominsa", "AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [ "AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza", "[Limsa Lominsa] Aetheryte Plaza",
@ -24,6 +23,16 @@
"InSameTerritory": true "InSameTerritory": true
} }
} }
},
{
"DataId": 1000895,
"Position": {
"X": -335.74432,
"Y": 12.899764,
"Z": 1.3884888
},
"TerritoryId": 129,
"InteractionType": "AcceptQuest"
} }
] ]
}, },

View File

@ -0,0 +1,181 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"Position": {
"X": -335.8204,
"Y": 12.899764,
"Z": 4.017052
},
"TerritoryId": 129,
"InteractionType": "WalkTo",
"AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanists' Guild"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
},
{
"DataId": 1000895,
"Position": {
"X": -335.74432,
"Y": 12.899764,
"Z": 1.3884888
},
"TerritoryId": 129,
"InteractionType": "AcceptQuest"
}
]
},
{
"Sequence": 1,
"Steps": [
{
"DataId": 1000909,
"Position": {
"X": -326.37524,
"Y": 12.899658,
"Z": 9.994568
},
"TerritoryId": 129,
"InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_CLSACN020_00453_Q1_000_000",
"Yes": true
}
]
}
]
},
{
"Sequence": 2,
"Steps": [
{
"Position": {
"X": -1.5568014,
"Y": 66.12121,
"Z": 102.35001
},
"TerritoryId": 135,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"ComplexCombatData": [
{
"DataId": 347,
"MinimumKillCount": 3,
"CompletionQuestVariablesFlags": [
{
"Low": 3
},
null,
null,
null,
null,
null
]
},
{
"DataId": 49,
"MinimumKillCount": 3,
"CompletionQuestVariablesFlags": [
null,
{
"Low": 3
},
null,
null,
null,
null
]
}
],
"AethernetShortcut": [
"[Limsa Lominsa] Arcanists' Guild",
"[Limsa Lominsa] Tempest Gate (Lower La Noscea)"
],
"CompletionQuestVariablesFlags": [
{
"Low": 3
},
{
"Low": 3
},
null,
null,
null,
null
]
},
{
"Position": {
"X": 157.63565,
"Y": 38.01287,
"Z": 48.92973
},
"TerritoryId": 135,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"ComplexCombatData": [
{
"DataId": 324,
"MinimumKillCount": 3,
"CompletionQuestVariablesFlags": [
null,
{
"High": 3
},
null,
null,
null,
null
]
}
],
"Fly": true,
"CompletionQuestVariablesFlags": [
null,
{
"High": 3
},
null,
null,
null,
null
]
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1000909,
"Position": {
"X": -326.37524,
"Y": 12.899658,
"Z": 9.994568
},
"TerritoryId": 129,
"InteractionType": "CompleteQuest",
"AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanists' Guild"
],
"NextQuestId": 455
}
]
}
]
}

View File

@ -0,0 +1,179 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1000909,
"Position": {
"X": -326.37524,
"Y": 12.899658,
"Z": 9.994568
},
"TerritoryId": 129,
"InteractionType": "AcceptQuest",
"AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanists' Guild"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
}
]
},
{
"Sequence": 1,
"Steps": [
{
"Position": {
"X": 667.4784,
"Y": 15.36824,
"Z": 443.4379
},
"TerritoryId": 138,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"KillEnemyDataIds": [
22
],
"AetheryteShortcut": "Western La Noscea - Swiftperch",
"CompletionQuestVariablesFlags": [
{
"Low": 3
},
null,
null,
null,
null,
null
]
},
{
"Position": {
"X": 686.6034,
"Y": 23.682272,
"Z": 422.78772
},
"TerritoryId": 138,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"KillEnemyDataIds": [
138
],
"CompletionQuestVariablesFlags": [
null,
{
"High": 3
},
null,
null,
null,
null
]
}
]
},
{
"Sequence": 2,
"Steps": [
{
"DataId": 1000909,
"Position": {
"X": -326.37524,
"Y": 12.899658,
"Z": 9.994568
},
"TerritoryId": 129,
"InteractionType": "Interact",
"AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanists' Guild"
]
}
]
},
{
"Sequence": 3,
"Steps": [
{
"DataId": 1005199,
"Position": {
"X": -348.3177,
"Y": -2.3744698,
"Z": 11.917236
},
"TerritoryId": 129,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 4,
"Steps": [
{
"DataId": 1005200,
"Position": {
"X": -106.79791,
"Y": 45.688404,
"Z": -252.33844
},
"TerritoryId": 134,
"InteractionType": "SinglePlayerDuty",
"AetheryteShortcut": "Middle La Noscea - Summerford Farms",
"Fly": true
}
]
},
{
"Sequence": 5,
"Steps": [
{
"DataId": 1005201,
"Position": {
"X": -98.49707,
"Y": 47.27536,
"Z": -262.31793
},
"StopDistance": 5,
"TerritoryId": 134,
"InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_CLSACN100_00456_Q1_000_000",
"Answer": "TEXT_CLSACN100_00456_A1_000_001"
}
]
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1000909,
"Position": {
"X": -326.37524,
"Y": 12.899658,
"Z": 9.994568
},
"TerritoryId": 129,
"InteractionType": "CompleteQuest",
"AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanists' Guild"
],
"NextQuestId": 1103
}
]
}
]
}

View File

@ -0,0 +1,160 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1017169,
"Position": {
"X": -342.15308,
"Y": 59.03801,
"Z": 315.14453
},
"TerritoryId": 400,
"InteractionType": "AcceptQuest"
}
]
},
{
"Sequence": 1,
"Steps": [
{
"DataId": 1017170,
"Position": {
"X": -344.22827,
"Y": 59.038006,
"Z": 317.1587
},
"StopDistance": 7,
"TerritoryId": 400,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 2,
"Steps": [
{
"DataId": 2007198,
"Position": {
"X": -339.65057,
"Y": 58.976074,
"Z": 319.50854
},
"StopDistance": 4.5,
"TerritoryId": 400,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 3,
"Steps": [
{
"DataId": 1017432,
"Position": {
"X": -743.6179,
"Y": 81,
"Z": 372.1521
},
"TerritoryId": 400,
"InteractionType": "Interact",
"Fly": true
}
]
},
{
"Sequence": 4,
"Steps": [
{
"DataId": 1017169,
"Position": {
"X": -342.15308,
"Y": 59.03801,
"Z": 315.14453
},
"TerritoryId": 400,
"InteractionType": "Interact",
"Fly": true,
"DialogueChoices": [
{
"Type": "List",
"Prompt": "TEXT_BANMOG003_02322_Q1_000_000",
"Answer": "TEXT_BANMOG003_02322_A1_000_001"
}
]
}
]
},
{
"Sequence": 5,
"Steps": [
{
"DataId": 1017172,
"Position": {
"X": -329.3355,
"Y": 59.056396,
"Z": 298.23755
},
"TerritoryId": 400,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 6,
"Steps": [
{
"TerritoryId": 400,
"InteractionType": "Craft",
"ItemId": 15721,
"ItemCount": 1
},
{
"DataId": 1017169,
"Position": {
"X": -342.15308,
"Y": 59.03801,
"Z": 315.14453
},
"TerritoryId": 400,
"InteractionType": "Interact"
}
]
},
{
"Sequence": 7,
"Steps": [
{
"DataId": 1017432,
"Position": {
"X": -743.6179,
"Y": 81,
"Z": 372.1521
},
"TerritoryId": 400,
"InteractionType": "Interact",
"Fly": true
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1017170,
"Position": {
"X": -344.22827,
"Y": 59.038006,
"Z": 317.1587
},
"TerritoryId": 400,
"InteractionType": "CompleteQuest",
"Fly": true
}
]
}
]
}

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"StopDistance": 3,
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"MaximumDistance": 50,
"TerritoryId": 401
}
}
}
},
{ {
"DataId": 1016093, "DataId": 1016093,
"Position": { "Position": {
@ -57,7 +81,14 @@
"Action": "Buffet (Sanuwa)", "Action": "Buffet (Sanuwa)",
"StopDistance": 3, "StopDistance": 3,
"$": "0 0 0 0 0 0 --> 1 0 0 0 0 16", "$": "0 0 0 0 0 0 --> 1 0 0 0 0 16",
"CompletionQuestVariablesFlags": [null,null,null,null,null,16] "CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
16
]
}, },
{ {
"Position": { "Position": {
@ -81,7 +112,14 @@
"Action": "Buffet (Sanuwa)", "Action": "Buffet (Sanuwa)",
"StopDistance": 3, "StopDistance": 3,
"$": "1 0 0 0 0 16 --> 2 0 0 0 0 80", "$": "1 0 0 0 0 16 --> 2 0 0 0 0 80",
"CompletionQuestVariablesFlags": [null,null,null,null,null,64] "CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"Position": { "Position": {
@ -105,7 +143,14 @@
"Action": "Buffet (Sanuwa)", "Action": "Buffet (Sanuwa)",
"StopDistance": 3, "StopDistance": 3,
"$": "2 0 0 0 0 80 --> 3 0 0 0 0 112", "$": "2 0 0 0 0 80 --> 3 0 0 0 0 112",
"CompletionQuestVariablesFlags": [null,null,null,null,null,32] "CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"Position": { "Position": {

View File

@ -20,16 +20,6 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": -797.36957,
"Y": -130.0076,
"Z": -404.2746
},
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true
},
{ {
"DataId": 2006651, "DataId": 2006651,
"Position": { "Position": {

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"StopDistance": 3,
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"MaximumDistance": 50,
"TerritoryId": 401
}
}
}
},
{ {
"DataId": 1016093, "DataId": 1016093,
"Position": { "Position": {

View File

@ -37,6 +37,28 @@
{ {
"Sequence": 2, "Sequence": 2,
"Steps": [ "Steps": [
{
"Position": {
"X": 151.96031,
"Y": -135.00737,
"Z": 611.10016
},
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
8
]
}
}
},
{ {
"DataId": 2006662, "DataId": 2006662,
"Position": { "Position": {
@ -46,7 +68,6 @@
}, },
"TerritoryId": 401, "TerritoryId": 401,
"InteractionType": "Interact", "InteractionType": "Interact",
"Fly": true,
"CompletionQuestVariablesFlags": [ "CompletionQuestVariablesFlags": [
null, null,
null, null,
@ -56,6 +77,28 @@
8 8
] ]
}, },
{
"Position": {
"X": 208.36453,
"Y": -125.28946,
"Z": 630.55835
},
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
16
]
}
}
},
{ {
"DataId": 2006661, "DataId": 2006661,
"Position": { "Position": {
@ -65,7 +108,6 @@
}, },
"TerritoryId": 401, "TerritoryId": 401,
"InteractionType": "Interact", "InteractionType": "Interact",
"Fly": true,
"CompletionQuestVariablesFlags": [ "CompletionQuestVariablesFlags": [
null, null,
null, null,
@ -75,6 +117,16 @@
16 16
] ]
}, },
{
"Position": {
"X": 402.99722,
"Y": -125.28946,
"Z": 724.59906
},
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true
},
{ {
"DataId": 2006658, "DataId": 2006658,
"Position": { "Position": {
@ -83,7 +135,8 @@
"Z": 724.0558 "Z": 724.0558
}, },
"TerritoryId": 401, "TerritoryId": 401,
"InteractionType": "Interact" "InteractionType": "Interact",
"Mount": false
} }
] ]
}, },

View File

@ -0,0 +1,100 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1016089,
"Position": {
"X": -799.46594,
"Y": -133.2695,
"Z": -404.1352
},
"TerritoryId": 401,
"InteractionType": "AcceptQuest"
}
]
},
{
"Sequence": 1,
"Steps": [
{
"DataId": 2006663,
"Position": {
"X": 665.0034,
"Y": -158.7702,
"Z": 662.2567
},
"TerritoryId": 401,
"InteractionType": "Interact",
"Fly": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
},
{
"DataId": 2006665,
"Position": {
"X": 728.1146,
"Y": -157.45789,
"Z": 815.5488
},
"TerritoryId": 401,
"InteractionType": "Interact",
"Fly": true,
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
],
"Comment": "Enemy despawns when out of range"
},
{
"DataId": 2006664,
"Position": {
"X": 625.3605,
"Y": -160.29602,
"Z": 835.62976
},
"TerritoryId": 401,
"InteractionType": "Interact",
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1016089,
"Position": {
"X": -799.46594,
"Y": -133.2695,
"Z": -404.1352
},
"TerritoryId": 401,
"InteractionType": "CompleteQuest",
"AetheryteShortcut": "The Sea of Clouds - Ok' Zundu",
"Fly": true
}
]
}
]
}

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"StopDistance": 3,
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"MaximumDistance": 50,
"TerritoryId": 401
}
}
}
},
{ {
"DataId": 1016093, "DataId": 1016093,
"Position": { "Position": {

View File

@ -35,6 +35,30 @@
{ {
"Sequence": 2, "Sequence": 2,
"Steps": [ "Steps": [
{
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"StopDistance": 3,
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"MaximumDistance": 50,
"TerritoryId": 401
}
}
}
},
{ {
"DataId": 1016093, "DataId": 1016093,
"Position": { "Position": {
@ -72,7 +96,14 @@
"InteractionType": "Action", "InteractionType": "Action",
"Action": "Buffet (Sanuwa)", "Action": "Buffet (Sanuwa)",
"Fly": true, "Fly": true,
"CompletionQuestVariablesFlags": [null,null,null,null,null,32] "CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}, },
{ {
"DataId": 1016221, "DataId": 1016221,
@ -85,7 +116,14 @@
"InteractionType": "Action", "InteractionType": "Action",
"Action": "Buffet (Sanuwa)", "Action": "Buffet (Sanuwa)",
"Fly": true, "Fly": true,
"CompletionQuestVariablesFlags": [null,null,null,null,null,64] "CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}, },
{ {
"DataId": 1016220, "DataId": 1016220,

View File

@ -36,6 +36,30 @@
{ {
"Sequence": 2, "Sequence": 2,
"Steps": [ "Steps": [
{
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"StopDistance": 3,
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"MaximumDistance": 50,
"TerritoryId": 401
}
}
}
},
{ {
"DataId": 1016093, "DataId": 1016093,
"Position": { "Position": {

View File

@ -36,6 +36,30 @@
{ {
"Sequence": 2, "Sequence": 2,
"Steps": [ "Steps": [
{
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"StopDistance": 3,
"TerritoryId": 401,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": -776.0281,
"Y": -133.35559,
"Z": -414.32825
},
"MaximumDistance": 50,
"TerritoryId": 401
}
}
}
},
{ {
"DataId": 1016093, "DataId": 1016093,
"Position": { "Position": {

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"StopDistance": 3,
"TerritoryId": 398,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"MaximumDistance": 50,
"TerritoryId": 398
}
}
}
},
{ {
"DataId": 1017031, "DataId": 1017031,
"Position": { "Position": {

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"StopDistance": 3,
"TerritoryId": 398,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"MaximumDistance": 50,
"TerritoryId": 398
}
}
}
},
{ {
"DataId": 1017031, "DataId": 1017031,
"Position": { "Position": {

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"StopDistance": 3,
"TerritoryId": 398,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"MaximumDistance": 50,
"TerritoryId": 398
}
}
}
},
{ {
"DataId": 1017031, "DataId": 1017031,
"Position": { "Position": {

View File

@ -20,6 +20,30 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"StopDistance": 3,
"TerritoryId": 398,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"NearPosition": {
"Position": {
"X": 74.20459,
"Y": -48.533592,
"Z": -171.12994
},
"MaximumDistance": 50,
"TerritoryId": 398
}
}
}
},
{ {
"DataId": 1017031, "DataId": 1017031,
"Position": { "Position": {

View File

@ -211,6 +211,9 @@
"Unlocked" "Unlocked"
] ]
}, },
"Diving": {
"type": ["boolean", "null"]
},
"NotTargetable": { "NotTargetable": {
"type": "boolean" "type": "boolean"
}, },

View File

@ -12,6 +12,7 @@ public sealed class SkipStepConditions
public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>(); public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
public ELockedSkipCondition? Flying { get; set; } public ELockedSkipCondition? Flying { get; set; }
public ELockedSkipCondition? Chocobo { get; set; } public ELockedSkipCondition? Chocobo { get; set; }
public bool? Diving { get; set; }
public bool NotTargetable { get; set; } public bool NotTargetable { get; set; }
public List<ushort> InTerritory { get; set; } = []; public List<ushort> InTerritory { get; set; } = [];
public List<ushort> NotInTerritory { get; set; } = []; public List<ushort> NotInTerritory { get; set; } = [];
@ -37,6 +38,7 @@ public sealed class SkipStepConditions
return (CompletionQuestVariablesFlags.Count > 0 && CompletionQuestVariablesFlags.Any(x => x != null)) || return (CompletionQuestVariablesFlags.Count > 0 && CompletionQuestVariablesFlags.Any(x => x != null)) ||
Flying != null || Flying != null ||
Chocobo != null || Chocobo != null ||
Diving != null ||
NotTargetable || NotTargetable ||
InTerritory.Count > 0 || InTerritory.Count > 0 ||
NotInTerritory.Count > 0 || NotInTerritory.Count > 0 ||
@ -53,6 +55,6 @@ public sealed class SkipStepConditions
public override string ToString() public override string ToString()
{ {
return return
$"{nameof(Never)}: {Never}, {nameof(CompletionQuestVariablesFlags)}: {CompletionQuestVariablesFlags}, {nameof(Flying)}: {Flying}, {nameof(Chocobo)}: {Chocobo}, {nameof(NotTargetable)}: {NotTargetable}, {nameof(InTerritory)}: {string.Join(" ", InTerritory)}, {nameof(NotInTerritory)}: {string.Join(" ", NotInTerritory)}, {nameof(Item)}: {Item}, {nameof(QuestsAccepted)}: {string.Join(" ", QuestsAccepted)}, {nameof(QuestsCompleted)}: {string.Join(" ", QuestsCompleted)}, {nameof(NotNamePlateIconId)}: {string.Join(" ", NotNamePlateIconId)}, {nameof(NearPosition)}: {NearPosition}, {nameof(ExtraCondition)}: {ExtraCondition}"; $"{nameof(Never)}: {Never}, {nameof(CompletionQuestVariablesFlags)}: {CompletionQuestVariablesFlags}, {nameof(Flying)}: {Flying}, {nameof(Chocobo)}: {Chocobo}, {nameof(Diving)}: {Diving}, {nameof(NotTargetable)}: {NotTargetable}, {nameof(InTerritory)}: {string.Join(" ", InTerritory)}, {nameof(NotInTerritory)}: {string.Join(" ", NotInTerritory)}, {nameof(Item)}: {Item}, {nameof(QuestsAccepted)}: {string.Join(" ", QuestsAccepted)}, {nameof(QuestsCompleted)}: {string.Join(" ", QuestsCompleted)}, {nameof(NotNamePlateIconId)}: {string.Join(" ", NotNamePlateIconId)}, {nameof(NearPosition)}: {NearPosition}, {nameof(ExtraCondition)}: {ExtraCondition}";
} }
} }

View File

@ -762,7 +762,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null) if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null)
return false; return false;
ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestThatCanBeAccepted(); ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestsThatCanBeAccepted().FirstOrDefault();
if (priorityQuestId == null) if (priorityQuestId == null)
return false; return false;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
@ -54,7 +55,8 @@ internal static class SkipCondition
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
IClientState clientState) : TaskExecutor<SkipTask> IClientState clientState,
ICondition condition) : TaskExecutor<SkipTask>
{ {
protected override unsafe bool Start() protected override unsafe bool Start()
{ {
@ -85,6 +87,18 @@ internal static class SkipCondition
return true; return true;
} }
if (skipConditions.Diving == true && condition[ConditionFlag.Diving])
{
logger.LogInformation("Skipping step, as you're currently diving underwater");
return true;
}
if (skipConditions.Diving == false && !condition[ConditionFlag.Diving])
{
logger.LogInformation("Skipping step, as you're not currently diving underwater");
return true;
}
if (skipConditions.InTerritory.Count > 0 && if (skipConditions.InTerritory.Count > 0 &&
skipConditions.InTerritory.Contains(clientState.TerritoryType)) skipConditions.InTerritory.Contains(clientState.TerritoryType))
{ {

View File

@ -235,6 +235,16 @@ internal sealed class QuestData
.ToList(); .ToList();
} }
public List<QuestInfo> GetAllByAlliedSociety(EAlliedSociety alliedSociety)
{
return _quests.Values
.Where(x => x is QuestInfo)
.Cast<QuestInfo>()
.Where(x => x.AlliedSociety == alliedSociety)
.OrderBy(x => x.QuestId)
.ToList();
}
public List<QuestInfo> GetClassJobQuests(EClassJob classJob) public List<QuestInfo> GetClassJobQuests(EClassJob classJob)
{ {
List<uint> chapterIds = classJob switch List<uint> chapterIds = classJob switch

View File

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Functions;
internal sealed class AlliedSocietyQuestFunctions
{
private readonly ILogger<AlliedSocietyQuestFunctions> _logger;
private readonly Dictionary<EAlliedSociety, List<NpcData>> _questsByAlliedSociety = [];
private readonly Dictionary<(uint NpcDataId, byte Seed, bool OutranksAll), List<QuestId>> _dailyQuests = [];
public AlliedSocietyQuestFunctions(QuestData questData, ILogger<AlliedSocietyQuestFunctions> logger)
{
_logger = logger;
foreach (var alliedSociety in Enum.GetValues<EAlliedSociety>().Where(x => x != EAlliedSociety.None))
{
var allQuests = questData.GetAllByAlliedSociety(alliedSociety);
var questsByIssuer = allQuests
.Where(x => x.IsRepeatable)
.GroupBy(x => x.IssuerDataId)
.ToDictionary(x => x.Key,
x => x.OrderBy(y => y.AlliedSocietyQuestGroup == 3).ThenBy(y => y.QuestId).ToList());
foreach ((uint issuerDataId, List<QuestInfo> quests) in questsByIssuer)
{
var npcData = new NpcData { IssuerDataId = issuerDataId, AllQuests = quests };
if (_questsByAlliedSociety.TryGetValue(alliedSociety, out List<NpcData>? existingNpcs))
existingNpcs.Add(npcData);
else
_questsByAlliedSociety[alliedSociety] = [npcData];
}
}
}
public unsafe List<QuestId> GetAvailableAlliedSocietyQuests(EAlliedSociety alliedSociety)
{
byte rankData = QuestManager.Instance()->BeastReputation[(int)alliedSociety - 1].Rank;
byte currentRank = (byte)(rankData & 0x7F);
if (currentRank == 0)
return [];
bool rankedUp = (rankData & 0x80) != 0;
byte seed = Marshal.ReadByte((nint)QuestManager.Instance() + 0x698); // TODO Use clientstructs
List<QuestId> result = [];
foreach (NpcData npcData in _questsByAlliedSociety[alliedSociety])
{
bool outranksAll = npcData.AllQuests.All(x => currentRank > x.AlliedSocietyRank);
var key = (NpcDataId: npcData.IssuerDataId, seed, outranksAll);
if (_dailyQuests.TryGetValue(key, out List<QuestId>? questIds))
result.AddRange(questIds);
else
{
var quests = CalculateAvailableQuests(npcData.AllQuests, seed, outranksAll, currentRank, rankedUp);
_logger.LogInformation("Available for {Tribe} (Seed: {Seed}, Issuer: {IssuerId}): {Quests}", alliedSociety, seed, npcData.IssuerDataId, string.Join(", ", quests));
_dailyQuests[key] = quests;
result.AddRange(quests);
}
}
return result;
}
private static List<QuestId> CalculateAvailableQuests(List<QuestInfo> allQuests, byte seed, bool outranksAll,
byte currentRank, bool rankedUp)
{
List<QuestInfo> eligible = [.. allQuests.Where(q => IsEligible(q, currentRank, rankedUp))];
List<QuestInfo> available = [];
if (eligible.Count == 0)
return [];
var rng = new Rng(seed);
if (outranksAll)
{
for (int i = 0, cnt = Math.Min(eligible.Count, 3); i < cnt; ++i)
{
var index = rng.Next(eligible.Count);
while (available.Contains(eligible[index]))
index = (index + 1) % eligible.Count;
available.Add(eligible[index]);
}
}
else
{
var firstExclusive = eligible.FindIndex(q => q.AlliedSocietyQuestGroup == 3);
if (firstExclusive >= 0)
available.Add(eligible[firstExclusive + rng.Next(eligible.Count - firstExclusive)]);
else
firstExclusive = eligible.Count;
for (int i = available.Count, cnt = Math.Min(firstExclusive, 3); i < cnt; ++i)
{
var index = rng.Next(firstExclusive);
while (available.Contains(eligible[index]))
index = (index + 1) % firstExclusive;
available.Add(eligible[index]);
}
}
return available.Select(x => (QuestId)x.QuestId).ToList();
}
private static bool IsEligible(QuestInfo questInfo, byte currentRank, bool rankedUp)
{
return rankedUp ? questInfo.AlliedSocietyRank == currentRank : questInfo.AlliedSocietyRank <= currentRank;
}
private sealed class NpcData
{
public required uint IssuerDataId { get; init; }
public required List<QuestInfo> AllQuests { get; init; } = [];
}
private record struct Rng(uint S0, uint S1 = 0, uint S2 = 0, uint S3 = 0)
{
public int Next(int range)
{
(S0, S1, S2, S3) = (S3, Transform(S0, S1), S1, S2);
return (int)(S1 % range);
}
// returns new value for s1
private static uint Transform(uint s0, uint s1)
{
var temp = s0 ^ (s0 << 11);
return s1 ^ temp ^ ((temp ^ (s1 >> 11)) >> 8);
}
}
}

View File

@ -28,6 +28,7 @@ internal sealed unsafe class QuestFunctions
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly AetheryteFunctions _aetheryteFunctions; private readonly AetheryteFunctions _aetheryteFunctions;
private readonly AlliedSocietyQuestFunctions _alliedSocietyQuestFunctions;
private readonly AlliedSocietyData _alliedSocietyData; private readonly AlliedSocietyData _alliedSocietyData;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
@ -38,6 +39,7 @@ internal sealed unsafe class QuestFunctions
QuestRegistry questRegistry, QuestRegistry questRegistry,
QuestData questData, QuestData questData,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
AlliedSocietyQuestFunctions alliedSocietyQuestFunctions,
AlliedSocietyData alliedSocietyData, AlliedSocietyData alliedSocietyData,
Configuration configuration, Configuration configuration,
IDataManager dataManager, IDataManager dataManager,
@ -47,6 +49,7 @@ internal sealed unsafe class QuestFunctions
_questRegistry = questRegistry; _questRegistry = questRegistry;
_questData = questData; _questData = questData;
_aetheryteFunctions = aetheryteFunctions; _aetheryteFunctions = aetheryteFunctions;
_alliedSocietyQuestFunctions = alliedSocietyQuestFunctions;
_alliedSocietyData = alliedSocietyData; _alliedSocietyData = alliedSocietyData;
_configuration = configuration; _configuration = configuration;
_dataManager = dataManager; _dataManager = dataManager;
@ -152,11 +155,13 @@ internal sealed unsafe class QuestFunctions
{ {
// if we have multiple quests to turn in for an allied society, try and complete all of them // if we have multiple quests to turn in for an allied society, try and complete all of them
var (firstTrackedQuest, firstTrackedSequence) = trackedQuests.First(); var (firstTrackedQuest, firstTrackedSequence) = trackedQuests.First();
EAlliedSociety firstTrackedAlliedSociety = _alliedSocietyData.GetCommonAlliedSocietyTurnIn(firstTrackedQuest); EAlliedSociety firstTrackedAlliedSociety =
_alliedSocietyData.GetCommonAlliedSocietyTurnIn(firstTrackedQuest);
if (firstTrackedAlliedSociety != EAlliedSociety.None) if (firstTrackedAlliedSociety != EAlliedSociety.None)
{ {
var alliedQuestsForSameSociety = trackedQuests.Skip(1) var alliedQuestsForSameSociety = trackedQuests.Skip(1)
.Where(quest => _alliedSocietyData.GetCommonAlliedSocietyTurnIn(quest.Quest) == firstTrackedAlliedSociety) .Where(quest =>
_alliedSocietyData.GetCommonAlliedSocietyTurnIn(quest.Quest) == firstTrackedAlliedSociety)
.ToList(); .ToList();
if (alliedQuestsForSameSociety.Count > 0) if (alliedQuestsForSameSociety.Count > 0)
{ {
@ -177,14 +182,15 @@ internal sealed unsafe class QuestFunctions
// also include the first quest in the list for those // also include the first quest in the list for those
alliedQuestsForSameSociety.Insert(0, (firstTrackedQuest, firstTrackedSequence)); alliedQuestsForSameSociety.Insert(0, (firstTrackedQuest, firstTrackedSequence));
_alliedSocietyData.GetCommonAlliedSocietyNpcs(firstTrackedAlliedSociety, out uint[]? normalNpcs, _alliedSocietyData.GetCommonAlliedSocietyNpcs(firstTrackedAlliedSociety,
out uint[]? normalNpcs,
out _); out _);
if (normalNpcs.Length > 0) if (normalNpcs.Length > 0)
{ {
var talkToNormalNpcs = alliedQuestsForSameSociety var talkToNormalNpcs = alliedQuestsForSameSociety
.Where(x => x.Sequence < 255) .Where(x => x.Sequence < 255)
.Where(x => IsInteractStep(x.Quest, x.Sequence, normalNpcs)) .Where(x => IsInteractSequence(x.Quest, x.Sequence, normalNpcs))
.Cast<(ElementId, byte)?>() .Cast<(ElementId, byte)?>()
.FirstOrDefault(); .FirstOrDefault();
if (talkToNormalNpcs != null) if (talkToNormalNpcs != null)
@ -215,7 +221,7 @@ internal sealed unsafe class QuestFunctions
return (firstTrackedQuest, firstTrackedSequence); return (firstTrackedQuest, firstTrackedSequence);
} }
ElementId? priorityQuest = GetNextPriorityQuestThatCanBeAccepted(); ElementId? priorityQuest = GetNextPriorityQuestsThatCanBeAccepted().FirstOrDefault();
if (priorityQuest != null) if (priorityQuest != null)
{ {
// if we have an accepted msq quest, and know of no quest of those currently in the to-do list... // if we have an accepted msq quest, and know of no quest of those currently in the to-do list...
@ -299,13 +305,16 @@ internal sealed unsafe class QuestFunctions
_alliedSocietyData.Mounts.ContainsKey(battleChara->Mount.MountId); _alliedSocietyData.Mounts.ContainsKey(battleChara->Mount.MountId);
} }
private bool IsInteractStep(ElementId questId, byte sequence, uint[] dataIds) private bool IsInteractSequence(ElementId questId, byte sequenceNo, uint[] dataIds)
{ {
if (_questRegistry.TryGetQuest(questId, out var quest)) if (_questRegistry.TryGetQuest(questId, out var quest))
{ {
QuestStep? firstStepOfSequence = quest.FindSequence(sequence)?.FindStep(0); QuestSequence? sequence = quest.FindSequence(sequenceNo);
return firstStepOfSequence is { InteractionType: EInteractionType.Interact, DataId: { } dataId } && return sequence != null &&
dataIds.Contains(dataId); sequence.Steps.All(x =>
x is { InteractionType: EInteractionType.WalkTo } ||
(x is { InteractionType: EInteractionType.Interact, DataId: { } dataId } &&
dataIds.Contains(dataId)));
} }
return false; return false;
@ -327,12 +336,12 @@ internal sealed unsafe class QuestFunctions
return null; return null;
} }
public ElementId? GetNextPriorityQuestThatCanBeAccepted() public List<ElementId> GetNextPriorityQuestsThatCanBeAccepted()
{ {
// all priority quests assume we're able to teleport to the beginning (and for e.g. class quests, the end) // all priority quests assume we're able to teleport to the beginning (and for e.g. class quests, the end)
// ideally without having to wait 15m for Return. // ideally without having to wait 15m for Return.
if (!_aetheryteFunctions.IsTeleportUnlocked()) if (!_aetheryteFunctions.IsTeleportUnlocked())
return null; return [];
// ideally, we'd also be able to afford *some* teleports // ideally, we'd also be able to afford *some* teleports
// this implicitly makes sure we're not starting one of the lv1 class quests if we can't afford to teleport back // this implicitly makes sure we're not starting one of the lv1 class quests if we can't afford to teleport back
@ -354,7 +363,7 @@ internal sealed unsafe class QuestFunctions
return firstStep.IsTeleportableForPriorityQuests(); return firstStep.IsTeleportableForPriorityQuests();
}) })
.FirstOrDefault(x => .Where(x =>
{ {
if (!_questRegistry.TryGetQuest(x, out Quest? quest)) if (!_questRegistry.TryGetQuest(x, out Quest? quest))
return false; return false;
@ -381,7 +390,8 @@ internal sealed unsafe class QuestFunctions
return true; return true;
}); });
}); })
.ToList();
} }
private static int EstimateTeleportCosts(Quest quest) private static int EstimateTeleportCosts(Quest quest)
@ -441,9 +451,12 @@ internal sealed unsafe class QuestFunctions
if (IsQuestAccepted(questId)) if (IsQuestAccepted(questId))
return false; return false;
if (quest.Info.AlliedSociety != EAlliedSociety.None)
{
if (QuestManager.Instance()->IsDailyQuestCompleted(questId.Value)) if (QuestManager.Instance()->IsDailyQuestCompleted(questId.Value))
return false; return false;
} }
}
else else
{ {
if (IsQuestAcceptedOrComplete(questId)) if (IsQuestAcceptedOrComplete(questId))
@ -540,6 +553,9 @@ internal sealed unsafe class QuestFunctions
if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany()) if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
return true; return true;
if (questInfo.AlliedSociety != EAlliedSociety.None && questInfo.IsRepeatable)
return !IsDailyAlliedSocietyQuestAndAvailableToday(questId);
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
} }
@ -563,6 +579,21 @@ internal sealed unsafe class QuestFunctions
return !HasCompletedPreviousQuests(questInfo, null); return !HasCompletedPreviousQuests(questInfo, null);
} }
public bool IsDailyAlliedSocietyQuest(QuestId questId)
{
var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
return questInfo.AlliedSociety != EAlliedSociety.None && questInfo.IsRepeatable;
}
public bool IsDailyAlliedSocietyQuestAndAvailableToday(QuestId questId)
{
if (!IsDailyAlliedSocietyQuest(questId))
return false;
var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
return _alliedSocietyQuestFunctions.GetAvailableAlliedSocietyQuests(questInfo.AlliedSociety).Contains(questId);
}
public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null) public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null)
{ {
if (elementId is QuestId questId) if (elementId is QuestId questId)

View File

@ -60,6 +60,8 @@ internal sealed class QuestInfo : IQuestInfo
PreviousInstanceContentJoin = (EQuestJoin)quest.InstanceContentJoin; PreviousInstanceContentJoin = (EQuestJoin)quest.InstanceContentJoin;
GrandCompany = (GrandCompany)quest.GrandCompany.RowId; GrandCompany = (GrandCompany)quest.GrandCompany.RowId;
AlliedSociety = (EAlliedSociety)quest.BeastTribe.RowId; AlliedSociety = (EAlliedSociety)quest.BeastTribe.RowId;
AlliedSocietyQuestGroup = quest.Unknown11;
AlliedSocietyRank = (int)quest.BeastReputationRank.RowId;
ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.ValueNullable!); ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.ValueNullable!);
IsSeasonalEvent = quest.Festival.RowId != 0; IsSeasonalEvent = quest.Festival.RowId != 0;
NewGamePlusChapter = newGamePlusChapter; NewGamePlusChapter = newGamePlusChapter;
@ -85,6 +87,8 @@ internal sealed class QuestInfo : IQuestInfo
public bool CompletesInstantly { get; } public bool CompletesInstantly { get; }
public GrandCompany GrandCompany { get; } public GrandCompany GrandCompany { get; }
public EAlliedSociety AlliedSociety { get; } public EAlliedSociety AlliedSociety { get; }
public byte AlliedSocietyQuestGroup { get; }
public int AlliedSocietyRank { get; }
public IReadOnlyList<EClassJob> ClassJobs { get; } public IReadOnlyList<EClassJob> ClassJobs { get; }
public bool IsSeasonalEvent { get; } public bool IsSeasonalEvent { get; }
public uint NewGamePlusChapter { get; } public uint NewGamePlusChapter { get; }

View File

@ -111,6 +111,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<GameFunctions>(); serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>(); serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<QuestFunctions>(); serviceCollection.AddSingleton<QuestFunctions>();
serviceCollection.AddSingleton<AlliedSocietyQuestFunctions>();
serviceCollection.AddSingleton<DalamudReflector>(); serviceCollection.AddSingleton<DalamudReflector>();
serviceCollection.AddSingleton<AetherCurrentData>(); serviceCollection.AddSingleton<AetherCurrentData>();
@ -261,8 +262,10 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<QuickAccessButtonsComponent>(); serviceCollection.AddSingleton<QuickAccessButtonsComponent>();
serviceCollection.AddSingleton<RemainingTasksComponent>(); serviceCollection.AddSingleton<RemainingTasksComponent>();
serviceCollection.AddSingleton<QuestJournalUtils>();
serviceCollection.AddSingleton<QuestJournalComponent>(); serviceCollection.AddSingleton<QuestJournalComponent>();
serviceCollection.AddSingleton<GatheringJournalComponent>(); serviceCollection.AddSingleton<GatheringJournalComponent>();
serviceCollection.AddSingleton<AlliedSocietyJournalComponent>();
serviceCollection.AddSingleton<OneTimeSetupWindow>(); serviceCollection.AddSingleton<OneTimeSetupWindow>();
serviceCollection.AddSingleton<QuestWindow>(); serviceCollection.AddSingleton<QuestWindow>();

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Windows.QuestComponents;
namespace Questionable.Windows.JournalComponents;
internal sealed class AlliedSocietyJournalComponent
{
private static readonly string[] RankNames =
["Neutral", "Recognized", "Friendly", "Trusted", "Respected", "Honored", "Sworn", "Allied"];
private readonly QuestFunctions _questFunctions;
private readonly AlliedSocietyQuestFunctions _alliedSocietyQuestFunctions;
private readonly QuestData _questData;
private readonly QuestRegistry _questRegistry;
private readonly QuestJournalUtils _questJournalUtils;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils;
public AlliedSocietyJournalComponent(
QuestFunctions questFunctions,
AlliedSocietyQuestFunctions alliedSocietyQuestFunctions,
QuestData questData,
QuestRegistry questRegistry,
QuestJournalUtils questJournalUtils,
QuestTooltipComponent questTooltipComponent,
UiUtils uiUtils)
{
_questFunctions = questFunctions;
_alliedSocietyQuestFunctions = alliedSocietyQuestFunctions;
_questData = questData;
_questRegistry = questRegistry;
_questJournalUtils = questJournalUtils;
_questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils;
}
public void DrawAlliedSocietyQuests()
{
using var tab = ImRaii.TabItem("Allied Societies");
if (!tab)
return;
foreach (EAlliedSociety alliedSociety in Enum.GetValues<EAlliedSociety>().Where(x => x != EAlliedSociety.None))
{
List<QuestInfo> quests = _alliedSocietyQuestFunctions.GetAvailableAlliedSocietyQuests(alliedSociety)
.Select(x => (QuestInfo)_questData.GetQuestInfo(x))
.ToList();
if (quests.Count == 0)
continue;
bool containsNewQuests = quests.Any(x => !_questFunctions.IsQuestComplete(x.QuestId));
if (containsNewQuests)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
if (!ImGui.CollapsingHeader($"{alliedSociety}###AlliedSociety{(int)alliedSociety}"))
continue;
if (containsNewQuests)
ImGui.PopStyleColor();
if (alliedSociety <= EAlliedSociety.Ixal)
{
for (byte i = 1; i <= 8; ++i)
{
var questsByRank = quests.Where(x => x.AlliedSocietyRank == i).ToList();
if (questsByRank.Count == 0)
continue;
ImGui.Text(RankNames[i - 1]);
foreach (var quest in questsByRank)
DrawQuest(quest);
}
}
else
{
foreach (var quest in quests)
DrawQuest(quest);
}
}
}
private void DrawQuest(QuestInfo questInfo)
{
var (color, icon, tooltipText) = _uiUtils.GetQuestStyle(questInfo.QuestId);
if (!_questRegistry.TryGetQuest(questInfo.QuestId, out var quest))
color = ImGuiColors.DalamudGrey;
if (_uiUtils.ChecklistItem($"{questInfo.Name} ({tooltipText})", color, icon))
_questTooltipComponent.Draw(questInfo);
_questJournalUtils.ShowContextMenu(questInfo, quest, nameof(AlliedSocietyJournalComponent));
}
}

View File

@ -30,8 +30,7 @@ internal sealed class QuestJournalComponent
private readonly UiUtils _uiUtils; private readonly UiUtils _uiUtils;
private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestTooltipComponent _questTooltipComponent;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestController _questController; private readonly QuestJournalUtils _questJournalUtils;
private readonly ICommandManager _commandManager;
private readonly QuestValidator _questValidator; private readonly QuestValidator _questValidator;
private List<FilteredSection> _filteredSections = []; private List<FilteredSection> _filteredSections = [];
@ -39,7 +38,7 @@ internal sealed class QuestJournalComponent
public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions, public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions,
UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface,
QuestController questController, ICommandManager commandManager, QuestValidator questValidator) QuestJournalUtils questJournalUtils, QuestValidator questValidator)
{ {
_journalData = journalData; _journalData = journalData;
_questRegistry = questRegistry; _questRegistry = questRegistry;
@ -47,8 +46,7 @@ internal sealed class QuestJournalComponent
_uiUtils = uiUtils; _uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent; _questTooltipComponent = questTooltipComponent;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_questController = questController; _questJournalUtils = questJournalUtils;
_commandManager = commandManager;
_questValidator = questValidator; _questValidator = questValidator;
} }
@ -184,23 +182,7 @@ internal sealed class QuestJournalComponent
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
_questTooltipComponent.Draw(questInfo); _questTooltipComponent.Draw(questInfo);
if (ImGui.BeginPopupContextItem($"##QuestPopup{questInfo.QuestId}", ImGuiPopupFlags.MouseButtonRight)) _questJournalUtils.ShowContextMenu(questInfo, quest, nameof(QuestJournalComponent));
{
if (ImGui.MenuItem("Start as next quest", _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId)))
{
_questController.SetNextQuest(quest);
_questController.Start("SeasonalEventSelection");
}
bool openInQuestMap = _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo);
if (ImGui.MenuItem("View in Quest Map", questInfo.QuestId is QuestId && openInQuestMap))
{
_commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty,
commandInfo!);
}
ImGui.EndPopup();
}
ImGui.TableNextColumn(); ImGui.TableNextColumn();
float spacing; float spacing;

View File

@ -0,0 +1,44 @@
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Questionable.Controller;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Windows.JournalComponents;
internal sealed class QuestJournalUtils
{
private readonly QuestController _questController;
private readonly QuestFunctions _questFunctions;
private readonly ICommandManager _commandManager;
public QuestJournalUtils(QuestController questController, QuestFunctions questFunctions,
ICommandManager commandManager)
{
_questController = questController;
_questFunctions = questFunctions;
_commandManager = commandManager;
}
public void ShowContextMenu(IQuestInfo questInfo, Quest? quest, string label)
{
using var popup = ImRaii.ContextPopup($"##QuestPopup{questInfo.QuestId}", ImGuiPopupFlags.MouseButtonRight);
if (!popup)
return;
if (ImGui.MenuItem("Start as next quest", _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId)))
{
_questController.SetNextQuest(quest);
_questController.Start(label);
}
bool openInQuestMap = _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo);
if (ImGui.MenuItem("View in Quest Map", questInfo.QuestId is QuestId && openInQuestMap))
{
_commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty,
commandInfo!);
}
}
}

View File

@ -11,18 +11,21 @@ namespace Questionable.Windows;
internal sealed class JournalProgressWindow : LWindow, IDisposable internal sealed class JournalProgressWindow : LWindow, IDisposable
{ {
private readonly QuestJournalComponent _questJournalComponent; private readonly QuestJournalComponent _questJournalComponent;
private readonly AlliedSocietyJournalComponent _alliedSocietyJournalComponent;
private readonly GatheringJournalComponent _gatheringJournalComponent; private readonly GatheringJournalComponent _gatheringJournalComponent;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IClientState _clientState; private readonly IClientState _clientState;
public JournalProgressWindow( public JournalProgressWindow(
QuestJournalComponent questJournalComponent, QuestJournalComponent questJournalComponent,
AlliedSocietyJournalComponent alliedSocietyJournalComponent,
GatheringJournalComponent gatheringJournalComponent, GatheringJournalComponent gatheringJournalComponent,
QuestRegistry questRegistry, QuestRegistry questRegistry,
IClientState clientState) IClientState clientState)
: base("Journal Progress###QuestionableJournalProgress") : base("Journal Progress###QuestionableJournalProgress")
{ {
_questJournalComponent = questJournalComponent; _questJournalComponent = questJournalComponent;
_alliedSocietyJournalComponent = alliedSocietyJournalComponent;
_gatheringJournalComponent = gatheringJournalComponent; _gatheringJournalComponent = gatheringJournalComponent;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_clientState = clientState; _clientState = clientState;
@ -60,6 +63,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
return; return;
_questJournalComponent.DrawQuests(); _questJournalComponent.DrawQuests();
_alliedSocietyJournalComponent.DrawAlliedSocietyQuests();
_gatheringJournalComponent.DrawGatheringItems(); _gatheringJournalComponent.DrawGatheringItems();
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -167,8 +168,29 @@ internal sealed partial class ActiveQuestComponent
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudYellow, SeIconChar.Hyadelyn.ToIconString()); ImGui.TextColored(ImGuiColors.DalamudYellow, SeIconChar.Hyadelyn.ToIconString());
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip( {
"This quest sequence starts with a teleport to an Aetheryte.\nCertain priority quest (e.g. class quests) may be started/completed by the plugin prior to continuing with this quest."); using var tooltip = ImRaii.Tooltip();
if (tooltip)
{
ImGui.Text("This quest sequence starts with a teleport to an Aetheryte.");
ImGui.Text(
"Certain priority quest (e.g. class quests) may be started/completed by the plugin prior to continuing with this quest.");
ImGui.Separator();
ImGui.Text("Available priority quests:");
List<ElementId> priorityQuests = _questFunctions.GetNextPriorityQuestsThatCanBeAccepted();
if (priorityQuests.Count > 0)
{
foreach (var questId in priorityQuests)
{
if (_questRegistry.TryGetQuest(questId, out var quest))
ImGui.BulletText($"{quest.Info.Name} ({questId})");
}
}
else
ImGui.BulletText("(none)");
}
}
} }
} }

View File

@ -24,6 +24,15 @@ internal sealed class UiUtils
{ {
if (_questFunctions.IsQuestAccepted(elementId)) if (_questFunctions.IsQuestAccepted(elementId))
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active"); return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
else if (elementId is QuestId questId && _questFunctions.IsDailyAlliedSocietyQuestAndAvailableToday(questId))
{
if (!_questFunctions.IsReadyToAcceptQuest(questId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_questFunctions.IsQuestComplete(questId))
return (ImGuiColors.ParsedBlue, FontAwesomeIcon.Running, "Available (Complete)");
else
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");
}
else if (_questFunctions.IsQuestAcceptedOrComplete(elementId)) else if (_questFunctions.IsQuestAcceptedOrComplete(elementId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_questFunctions.IsQuestUnobtainable(elementId)) else if (_questFunctions.IsQuestUnobtainable(elementId))