Compare commits

...

9 Commits

77 changed files with 852 additions and 188 deletions

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup Condition="$(MSBuildProjectName) != 'GatheringPathRenderer'">
<Version>4.19</Version>
<Version>4.20</Version>
</PropertyGroup>
</Project>

View File

@ -108,7 +108,7 @@ internal static class QuestStepExtensions
AssignmentList(nameof(QuestStep.ComplexCombatData), step.ComplexCombatData)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.CombatItemUse), step.CombatItemUse,
emptyStep.CombatItemUse)
emptyStep.CombatItemUse)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.CombatDelaySecondsAtStart),
step.CombatDelaySecondsAtStart,
@ -123,14 +123,8 @@ internal static class QuestStepExtensions
Assignment(nameof(QuestStep.AutoDutyEnabled),
step.AutoDutyEnabled, emptyStep.AutoDutyEnabled)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.BossModEnabled),
step.BossModEnabled, emptyStep.BossModEnabled)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.BossModNotes),
step.BossModNotes, emptyStep.BossModNotes)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.SinglePlayerDutyIndex),
step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex)
Assignment(nameof(QuestStep.SinglePlayerDutyOptions), step.SinglePlayerDutyOptions,
emptyStep.SinglePlayerDutyOptions)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
emptyStep.SkipConditions)

View File

@ -0,0 +1,30 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Questionable.Model.Questing;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Questionable.QuestPathGenerator.RoslynShortcuts;
namespace Questionable.QuestPathGenerator.RoslynElements;
internal static class SinglePlayerDutyOptionsExtensions
{
public static ExpressionSyntax ToExpressionSyntax(this SinglePlayerDutyOptions dutyOptions)
{
var emptyOptions = new SinglePlayerDutyOptions();
return ObjectCreationExpression(
IdentifierName(nameof(SinglePlayerDutyOptions)))
.WithInitializer(
InitializerExpression(
SyntaxKind.ObjectInitializerExpression,
SeparatedList<ExpressionSyntax>(
SyntaxNodeList(
Assignment(nameof(SinglePlayerDutyOptions.Enabled),
dutyOptions.Enabled, emptyOptions.Enabled)
.AsSyntaxNodeOrToken(),
AssignmentList(nameof(SinglePlayerDutyOptions.Notes), dutyOptions.Notes)
.AsSyntaxNodeOrToken(),
Assignment(nameof(SinglePlayerDutyOptions.Index),
dutyOptions.Index, emptyOptions.Index)
.AsSyntaxNodeOrToken()))));
}
}

View File

@ -62,6 +62,7 @@ public static class RoslynShortcuts
ComplexCombatData complexCombatData => complexCombatData.ToExpressionSyntax(),
QuestWorkValue questWorkValue => questWorkValue.ToExpressionSyntax(),
List<QuestWorkValue> list => list.ToExpressionSyntax(), // TODO fix in AssignmentList
SinglePlayerDutyOptions dutyOptions => dutyOptions.ToExpressionSyntax(),
SkipConditions skipConditions => skipConditions.ToExpressionSyntax(),
SkipStepConditions skipStepConditions => skipStepConditions.ToExpressionSyntax(),
SkipItemConditions skipItemCondition => skipItemCondition.ToExpressionSyntax(),

View File

@ -57,7 +57,9 @@
},
"TerritoryId": 153,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyIndex": 1,
"SinglePlayerDutyOptions": {
"Index": 1
},
"Fly": true
}
]

View File

@ -62,7 +62,9 @@
},
"TerritoryId": 154,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyIndex": 1,
"SinglePlayerDutyOptions": {
"Index": 1
},
"AetheryteShortcut": "North Shroud - Fallgourd Float",
"Fly": true
}

View File

@ -120,7 +120,9 @@
},
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyIndex": 1
"SinglePlayerDutyOptions": {
"Index": 1
}
}
]
},

View File

@ -140,6 +140,10 @@
},
"TerritoryId": 141,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}
]

View File

@ -92,7 +92,9 @@
},
"TerritoryId": 130,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyIndex": 1,
"SinglePlayerDutyOptions": {
"Index": 1
},
"AetheryteShortcut": "Ul'dah"
}
]

View File

@ -35,6 +35,10 @@
},
"TerritoryId": 137,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "Eastern La Noscea - Wineport",
"Fly": true
}

View File

@ -116,6 +116,10 @@
},
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}
]

View File

@ -65,7 +65,8 @@
"AetheryteShortcut": "East Shroud - Hawthorne Hut",
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
"InSameTerritory": true,
"AetheryteLocked": "East Shroud - Hawthorne Hut"
}
}
}
@ -116,7 +117,11 @@
"Z": 35.568726
},
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -33,6 +33,39 @@
{
"Sequence": 1,
"Steps": [
{
"DataId": 1001263,
"Position": {
"X": 181.41443,
"Y": -2.3519497,
"Z": -240.40594
},
"TerritoryId": 133,
"InteractionType": "Interact",
"TargetTerritoryId": 152,
"AethernetShortcut": [
"[Gridania] Conjurers' Guild",
"[Gridania] Lancers' Guild"
],
"SkipConditions": {
"StepIf": {
"AetheryteUnlocked": "East Shroud - Hawthorne Hut",
"InTerritory": [
152
]
}
}
},
{
"TerritoryId": 152,
"InteractionType": "AttuneAetheryte",
"Aetheryte": "East Shroud - Hawthorne Hut",
"SkipConditions": {
"StepIf": {
"AetheryteUnlocked": "East Shroud - Hawthorne Hut"
}
}
},
{
"Position": {
"X": -276.804,
@ -42,7 +75,12 @@
"TerritoryId": 152,
"InteractionType": "WalkTo",
"AetheryteShortcut": "East Shroud - Hawthorne Hut",
"Fly": true
"Fly": true,
"SkipConditions": {
"AetheryteShortcutIf": {
"AetheryteLocked": "East Shroud - Hawthorne Hut"
}
}
},
{
"DataId": 2000889,
@ -212,6 +250,10 @@
},
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}
]

View File

@ -138,7 +138,11 @@
"Z": 192.2179
},
"TerritoryId": 148,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -111,7 +111,14 @@
"Z": 295.52136
},
"TerritoryId": 148,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"Healer NPC is only killed after the boss dies; all NPCs need to be killed for the duty to complete"
]
}
}
]
},

View File

@ -29,10 +29,13 @@
},
"TerritoryId": 148,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": false,
"BossModNotes": [
"AI doesn't automatically target newly spawning adds and dies until after the boss died (tested on CNJ)"
]
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"AI doesn't automatically target newly spawning adds until after the boss died, and dies (tested on CNJ)"
]
}
}
]
},

View File

@ -77,6 +77,13 @@
},
"TerritoryId": 148,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete"
]
},
"AetheryteShortcut": "Central Shroud - Bentbranch Meadows"
}
]

View File

@ -69,6 +69,13 @@
},
"TerritoryId": 135,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"(Phase 1, second enemy group) Stuck with enemy being out of sight -- but still able to attack you (tested on ACN)"
]
},
"AetheryteShortcut": "Lower La Noscea - Moraby Drydocks"
}
]

View File

@ -45,8 +45,11 @@
"TerritoryId": 134,
"InteractionType": "Combat",
"EnemySpawnType": "AutoOnEnterArea",
"KillEnemyDataIds": [
52
"ComplexCombatData": [
{
"DataId": 52,
"IgnoreQuestMarker": true
}
]
},
{

View File

@ -73,7 +73,11 @@
"Z": -432.15082
},
"TerritoryId": 134,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -28,7 +28,14 @@
"Z": -141.7716
},
"TerritoryId": 134,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"AI doesn't automatically target newly spawning adds until after the boss died (requires healing luck on ACN)"
]
}
}
]
},

View File

@ -58,6 +58,13 @@
},
"TerritoryId": 138,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"(Phase 1) Kills PGL NPCs and then the boss - allied NPCs will kill most other NPCs eventually; all NPCs need to be killed for the duty to complete"
]
},
"AetheryteShortcut": "Western La Noscea - Swiftperch"
}
]

View File

@ -44,7 +44,11 @@
"Z": -242.51166
},
"TerritoryId": 145,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -79,6 +79,10 @@
},
"TerritoryId": 130,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [
"[Ul'dah] Aetheryte Plaza",
@ -87,6 +91,9 @@
}
]
},
{
"Sequence": 5
},
{
"Sequence": 255,
"Steps": [

View File

@ -63,12 +63,22 @@
"AethernetShortcut": [
"[Gridania] Aetheryte Plaza",
"[Gridania] Lancers' Guild"
]
],
"SkipConditions": {
"StepIf": {
"AetheryteUnlocked": "East Shroud - Hawthorne Hut"
}
}
},
{
"TerritoryId": 152,
"InteractionType": "AttuneAetheryte",
"Aetheryte": "East Shroud - Hawthorne Hut"
"Aetheryte": "East Shroud - Hawthorne Hut",
"SkipConditions": {
"StepIf": {
"AetheryteUnlocked": "East Shroud - Hawthorne Hut"
}
}
},
{
"DataId": 1004886,
@ -78,7 +88,17 @@
"Z": 475.30322
},
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "East Shroud - Hawthorne Hut",
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
}
]
},

View File

@ -64,6 +64,10 @@
},
"TerritoryId": 135,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"AethernetShortcut": [
"[Limsa Lominsa] The Aftcastle",
"[Limsa Lominsa] Tempest Gate (Lower La Noscea)"

View File

@ -59,6 +59,10 @@
},
"TerritoryId": 140,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "Western Thanalan - Horizon"
}
]

View File

@ -46,7 +46,8 @@
},
"StopDistance": 7,
"TerritoryId": 141,
"InteractionType": "Interact"
"InteractionType": "Interact",
"DelaySecondsAtStart": 2
}
]
},

View File

@ -158,7 +158,11 @@
"Z": 117.29602
},
"TerritoryId": 141,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -21,6 +21,15 @@
{
"Sequence": 255,
"Steps": [
{
"Position": {
"X": -174.73444,
"Y": 15.450659,
"Z": -266.76144
},
"TerritoryId": 140,
"InteractionType": "WalkTo"
},
{
"Position": {
"X": -289.1099,

View File

@ -37,7 +37,11 @@
"Z": -293.1411
},
"TerritoryId": 140,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -29,7 +29,7 @@
},
"TerritoryId": 141,
"InteractionType": "Combat",
"EnemySpawnType": "OverworldEnemies",
"EnemySpawnType": "FinishCombatIfAny",
"KillEnemyDataIds": [
352,
353
@ -53,6 +53,25 @@
{
"Sequence": 255,
"Steps": [
{
"Position": {
"X": 131.78122,
"Y": 20.119337,
"Z": -115.27284
},
"TerritoryId": 141,
"InteractionType": "WalkTo"
},
{
"Position": {
"X": 127.7017,
"Y": -0.15994573,
"Z": -161.89238
},
"TerritoryId": 141,
"InteractionType": "WalkTo",
"DisableNavmesh": true
},
{
"DataId": 1001605,
"Position": {

View File

@ -28,7 +28,11 @@
"Z": 536.88855
},
"TerritoryId": 141,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -64,7 +64,14 @@
"Z": -131.48706
},
"TerritoryId": 141,
"InteractionType": "Interact",
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete"
]
},
"AetheryteShortcut": "Central Thanalan - Black Brush Station"
}
]

View File

@ -73,13 +73,23 @@
},
"TerritoryId": 133,
"InteractionType": "Interact",
"TargetTerritoryId": 152
"TargetTerritoryId": 152,
"SkipConditions": {
"StepIf": {
"AetheryteUnlocked": "East Shroud - Hawthorne Hut"
}
}
},
{
"TerritoryId": 152,
"InteractionType": "AttuneAetheryte",
"Aetheryte": "East Shroud - Hawthorne Hut",
"StopDistance": 5
"StopDistance": 5,
"SkipConditions": {
"StepIf": {
"AetheryteUnlocked": "East Shroud - Hawthorne Hut"
}
}
},
{
"DataId": 1006188,
@ -89,7 +99,13 @@
"Z": 283.4973
},
"TerritoryId": 152,
"InteractionType": "CompleteQuest"
"InteractionType": "CompleteQuest",
"AetheryteShortcut": "East Shroud - Hawthorne Hut",
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
}
}
}
]
}

View File

@ -64,7 +64,11 @@
"Z": -39.383606
},
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -83,7 +83,14 @@
"Z": -12.985474
},
"TerritoryId": 153,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"AI will kill initial adds before the boss, but not switch target whenever new enemies spawn; all NPCs need to be killed for the duty to complete"
]
}
}
]
},

View File

@ -159,7 +159,11 @@
"Z": -805.478
},
"TerritoryId": 140,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -104,10 +104,10 @@
},
"TerritoryId": 1053,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": false,
"BossModNotes": [
"Doesn't handle death properly"
]
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -68,6 +68,15 @@
{
"Sequence": 3,
"Steps": [
{
"Position": {
"X": -561.9863,
"Y": 9.919454,
"Z": 66.29564
},
"TerritoryId": 152,
"InteractionType": "WalkTo"
},
{
"DataId": 1008276,
"Position": {

View File

@ -78,6 +78,10 @@
"StopDistance": 1,
"TerritoryId": 156,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}
]

View File

@ -100,6 +100,28 @@
2
]
},
{
"Position": {
"X": 86.662384,
"Y": 28.34813,
"Z": -627.5218
},
"TerritoryId": 156,
"InteractionType": "WalkTo",
"Fly": true,
"SkipConditions": {
"StepIf": {
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
32
]
}
}
},
{
"DataId": 1009143,
"Position": {
@ -109,7 +131,6 @@
},
"TerritoryId": 156,
"InteractionType": "Interact",
"Fly": true,
"$": "1 112 0 0 0 2 -> 2 96 0 0 0 34",
"CompletionQuestVariablesFlags": [
null,

View File

@ -71,6 +71,14 @@
},
"TerritoryId": 147,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"Will target Eline first (other NPCs later), and move to some -other- group of NPCs; only re-targets once they're at 1 HP (for Eline) or die",
"If the target isn't in melee range but other NPCs are, whether any AOEs are used for nearby enemies seems random"
]
},
"Fly": true,
"AetheryteShortcut": "Northern Thanalan - Ceruleum Processing Plant"
}

View File

@ -28,7 +28,16 @@
"Z": -328.66406
},
"TerritoryId": 155,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"WIP: Needs to be re-tested",
"AI doesn't move after starting the instance, so enemies won't be triggered",
"(First Barrier) If the player is too far south, after being stunned by Vishap's roar, AI doesn't move out of the AOE and dies to the Cauterize"
]
}
}
]
},

View File

@ -46,6 +46,10 @@
},
"TerritoryId": 155,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}
]

View File

@ -96,7 +96,10 @@
"TerritoryId": 138,
"InteractionType": "SinglePlayerDuty",
"Fly": true,
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -30,7 +30,11 @@
},
"TerritoryId": 397,
"InteractionType": "SinglePlayerDuty",
"Comment": "Walk straight to Gorgagne Mills basement, ignore footprints"
"Comment": "Walk straight to Gorgagne Mills basement, ignore footprints",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -59,7 +59,10 @@
},
"TerritoryId": 401,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -79,7 +79,10 @@
"[Ishgard] The Forgotten Knight",
"[Ishgard] The Tribunal"
],
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -29,7 +29,13 @@
},
"TerritoryId": 145,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292,
"Notes": [
"Will not move into melee range to kill the gate; Alphinaud will kill it after a while"
]
}
}
]
},

View File

@ -79,7 +79,10 @@
"TerritoryId": 397,
"InteractionType": "SinglePlayerDuty",
"DisableNavmesh": true,
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -75,7 +75,10 @@
},
"TerritoryId": 418,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -57,7 +57,10 @@
"InteractionType": "SinglePlayerDuty",
"Emote": "lookout",
"StopDistance": 0.25,
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -48,7 +48,10 @@
"[Idyllshire] Aetheryte Plaza",
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
],
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -69,7 +69,10 @@
},
"TerritoryId": 402,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": true
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -85,14 +85,17 @@
"TerritoryId": 351,
"InteractionType": "SinglePlayerDuty",
"Comment": "Estinien vs. Arch Ultima",
"BossModEnabled": false,
"BossModNotes": [
"AI doesn't move automatically for the first boss",
"AI doesn't move automatically for the dialogue with gaius on the bridge",
"After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)",
"After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking"
],
"$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one"
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"AI doesn't move automatically for the first boss",
"AI doesn't move automatically for the dialogue with gaius on the bridge",
"After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)",
"After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking"
]
},
"$": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one"
}
]
},

View File

@ -46,10 +46,13 @@
},
"TerritoryId": 817,
"InteractionType": "SinglePlayerDuty",
"BossModEnabled": false,
"BossModNotes": [
"Doesn't walk to the teleporter to finish the duty"
],
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"Doesn't walk to the teleporter to finish the duty"
]
},
"Fly": true,
"Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)",
"$": "The dialogue choices and data ids here are recycled",

View File

@ -104,7 +104,9 @@
"StopDistance": 5,
"TerritoryId": 829,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyIndex": 1,
"SinglePlayerDutyOptions": {
"Index": 1
},
"DialogueChoices": [
{
"Type": "List",

View File

@ -1267,20 +1267,36 @@
},
"then": {
"properties": {
"BossModEnabled": {
"type": "boolean"
},
"BossModNotes": {
"type": "array",
"items": {
"type": "string"
}
},
"SinglePlayerDutyIndex": {
"type": "integer",
"minimum": 0,
"maximum": 1,
"description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is"
"SinglePlayerDutyOptions": {
"type": "object",
"properties": {
"Enabled": {
"type": "boolean"
},
"Notes": {
"type": "array",
"items": {
"type": "string"
}
},
"Index": {
"type": "integer",
"minimum": 0,
"maximum": 1,
"description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is"
},
"TestedBossModVersion": {
"type": "number",
"minimum": 292
},
"$": {
"type": "string"
}
},
"TODO_required": [
"Enabled"
],
"additionalProperties": false
}
}
}

View File

@ -75,9 +75,8 @@ public sealed class QuestStep
public JumpDestination? JumpDestination { get; set; }
public uint? ContentFinderConditionId { get; set; }
public bool AutoDutyEnabled { get; set; }
public bool BossModEnabled { get; set; }
public List<string> BossModNotes { get; set; } = [];
public byte SinglePlayerDutyIndex { get; set; }
public SinglePlayerDutyOptions? SinglePlayerDutyOptions { get; set; }
public byte SinglePlayerDutyIndex => SinglePlayerDutyOptions?.Index ?? 0;
public SkipConditions? SkipConditions { get; set; }
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Questionable.Model.Questing;
public sealed class SinglePlayerDutyOptions
{
public bool Enabled { get; set; }
public List<string> Notes { get; set; } = [];
public byte Index { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Dalamud.Configuration;
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -45,7 +46,10 @@ internal sealed class Configuration : IPluginConfiguration
internal sealed class SinglePlayerDutyConfiguration
{
public bool RunSoloInstancesWithBossMod { get; set; }
public byte RetryDifficulty { get; set; } = 2;
[SuppressMessage("Performance", "CA1822", Justification = "Will be fixed when no longer WIP")]
public byte RetryDifficulty => 0;
public HashSet<uint> WhitelistedSinglePlayerDutyCfcIds { get; set; } = [];
public HashSet<uint> BlacklistedSinglePlayerDutyCfcIds { get; set; } = [];
}

View File

@ -691,7 +691,7 @@ internal sealed class InteractionUiController : IDisposable
private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step)
{
if (step is { InteractionType: EInteractionType.SinglePlayerDuty } &&
_bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyIndex, step.BossModEnabled))
_bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyOptions))
{
// Most of these are yes/no dialogs "Duty calls, ...".
//

View File

@ -10,6 +10,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared;
using Questionable.External;
using Questionable.Functions;
@ -200,7 +201,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (!_clientState.IsLoggedIn || _condition[ConditionFlag.Unconscious])
{
if (!_taskQueue.AllTasksComplete)
if (_condition[ConditionFlag.Unconscious] &&
_condition[ConditionFlag.SufferingStatusAffliction63] &&
_clientState.TerritoryType == SinglePlayerDuty.LahabreaTerritoryId)
{
// ignore, we're in the lahabrea fight
}
else if (!_taskQueue.AllTasksComplete)
{
Stop("HP = 0");
_movementController.Stop();

View File

@ -1,4 +1,6 @@
using Microsoft.Extensions.Logging;
using System.Linq;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
@ -21,7 +23,7 @@ internal static class NextQuest
return null;
// probably irrelevant, since pick up is handled elsewhere (and, in particular, checks for aetherytes and stuff)
if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId))
if (questFunctions.GetPriorityQuests(onlyClassAndRoleQuests: true).Contains(step.NextQuestId))
return null;
return new SetQuestTask(step.NextQuestId, quest.Id);

View File

@ -27,7 +27,7 @@ internal static class SendNotification
new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name
: step.Comment),
EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled) =>
EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions) =>
new Task(step.InteractionType, quest.Info.Name),
_ => null,
};

View File

@ -96,6 +96,12 @@ internal static class Interact
private EInteractionState _interactionState = EInteractionState.None;
private DateTime _continueAt = DateTime.MinValue;
/// <summary>
/// A slight delay when we think an interaction has ended, to make sure that we're processing "Action cancelled"
/// prior to the next step (in case we're attacked).
/// </summary>
private bool delayedFinalCheck;
public Quest? Quest => Task.Quest;
public EInteractionType InteractionType { get; set; }
@ -179,7 +185,14 @@ internal static class Interact
return ETaskResult.StillRunning;
else if (ProgressContext.WasSuccessful() ||
_interactionState == EInteractionState.InteractionConfirmed)
return ETaskResult.TaskComplete;
{
if (delayedFinalCheck)
return ETaskResult.TaskComplete;
_continueAt = DateTime.Now.AddSeconds(0.2);
delayedFinalCheck = true;
return ETaskResult.StillRunning;
}
}
IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);

View File

@ -1,7 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.External;
@ -12,22 +17,34 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class SinglePlayerDuty
{
public const int LahabreaTerritoryId = 1052;
internal sealed class Factory(
BossModIpc bossModIpc,
TerritoryData territoryData) : ITaskFactory
TerritoryData territoryData,
ICondition condition,
IClientState clientState) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SinglePlayerDuty)
yield break;
if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled))
if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{
if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out var cfcData))
throw new TaskException("Failed to get content finder condition for solo instance");
yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new EnableAi();
if (cfcData.TerritoryId == LahabreaTerritoryId)
{
yield return new SetTarget(14643);
yield return new WaitCondition.Task(() => condition[ConditionFlag.Unconscious] || clientState.TerritoryType != LahabreaTerritoryId, "Wait(death)");
yield return new DisableAi();
yield return new WaitCondition.Task(() => !condition[ConditionFlag.Unconscious] || clientState.TerritoryType != LahabreaTerritoryId, "Wait(resurrection)");
yield return new EnableAi();
}
yield return new WaitSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new DisableAi();
yield return new WaitAtEnd.WaitNextStepOrSequence();
@ -113,4 +130,32 @@ internal static class SinglePlayerDuty
public override bool ShouldInterruptOnDamage() => false;
}
// TODO this should be handled in VBM
internal sealed record SetTarget(uint DataId) : ITask
{
public override string ToString() => $"SetTarget({DataId})";
}
internal sealed class SetTargetExecutor(
ITargetManager targetManager,
IObjectTable objectTable) : TaskExecutor<SetTarget>
{
protected override bool Start() => true;
public override ETaskResult Update()
{
if (targetManager.Target?.DataId == Task.DataId)
return ETaskResult.TaskComplete;
IGameObject? gameObject = objectTable.FirstOrDefault(x => x.DataId == Task.DataId);
if (gameObject == null)
return ETaskResult.StillRunning;
targetManager.Target = gameObject;
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage() => false;
}
}

View File

@ -54,7 +54,7 @@ internal static class WaitAtEnd
return [new WaitNextStepOrSequence()];
case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled):
case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions):
return [new EndAutomation()];
case EInteractionType.WalkTo:

View File

@ -0,0 +1,169 @@
using System;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.Utils;
internal sealed class PartyWatchDog : IDisposable
{
private readonly QuestController _questController;
private readonly IClientState _clientState;
private readonly IChatGui _chatGui;
private readonly ILogger<PartyWatchDog> _logger;
private ushort? _uncheckedTeritoryId;
public PartyWatchDog(QuestController questController, IClientState clientState, IChatGui chatGui,
ILogger<PartyWatchDog> logger)
{
_questController = questController;
_clientState = clientState;
_chatGui = chatGui;
_logger = logger;
_clientState.TerritoryChanged += TerritoryChanged;
}
private unsafe void TerritoryChanged(ushort newTerritoryId)
{
var intendedUse = (ETerritoryIntendedUseEnum)GameMain.Instance()->CurrentTerritoryIntendedUseId;
switch (intendedUse)
{
case ETerritoryIntendedUseEnum.Gaol:
case ETerritoryIntendedUseEnum.Frontline:
case ETerritoryIntendedUseEnum.LordOfVerminion:
case ETerritoryIntendedUseEnum.Diadem:
case ETerritoryIntendedUseEnum.CrystallineConflict:
case ETerritoryIntendedUseEnum.Battlehall:
case ETerritoryIntendedUseEnum.CrystallineConflict2:
case ETerritoryIntendedUseEnum.DeepDungeon:
case ETerritoryIntendedUseEnum.TreasureMapDuty:
case ETerritoryIntendedUseEnum.Diadem2:
case ETerritoryIntendedUseEnum.RivalWings:
case ETerritoryIntendedUseEnum.Eureka:
case ETerritoryIntendedUseEnum.LeapOfFaith:
case ETerritoryIntendedUseEnum.OceanFishing:
case ETerritoryIntendedUseEnum.Diadem3:
case ETerritoryIntendedUseEnum.Bozja:
case ETerritoryIntendedUseEnum.Battlehall2:
case ETerritoryIntendedUseEnum.Battlehall3:
case ETerritoryIntendedUseEnum.LargeScaleRaid:
case ETerritoryIntendedUseEnum.LargeScaleSavageRaid:
case ETerritoryIntendedUseEnum.Blunderville:
StopIfRunning($"Unsupported Area entered ({newTerritoryId})");
break;
case ETerritoryIntendedUseEnum.Dungeon:
case ETerritoryIntendedUseEnum.VariantDungeon:
case ETerritoryIntendedUseEnum.AllianceRaid:
case ETerritoryIntendedUseEnum.Trial:
case ETerritoryIntendedUseEnum.Raid:
case ETerritoryIntendedUseEnum.Raid2:
case ETerritoryIntendedUseEnum.SeasonalEvent:
case ETerritoryIntendedUseEnum.SeasonalEvent2:
case ETerritoryIntendedUseEnum.CriterionDuty:
case ETerritoryIntendedUseEnum.CriterionSavageDuty:
_uncheckedTeritoryId = newTerritoryId;
_logger.LogInformation("Will check territory {TerritoryId} after loading", newTerritoryId);
break;
}
}
public unsafe void Update()
{
if (_uncheckedTeritoryId == _clientState.TerritoryType && GameMain.Instance()->TerritoryLoadState == 2)
{
var groupManager = GroupManager.Instance();
if (groupManager == null)
return;
byte memberCount = groupManager->MainGroup.MemberCount;
bool isInAlliance = groupManager->MainGroup.IsAlliance;
_logger.LogDebug("Territory {TerritoryId} with {MemberCount} members, alliance: {IsInAlliance}",
_uncheckedTeritoryId, memberCount, isInAlliance);
if (memberCount > 1 || isInAlliance)
StopIfRunning("Other party members present");
_uncheckedTeritoryId = null;
}
}
private void StopIfRunning(string reason)
{
if (_questController.IsRunning || _questController.AutomationType != QuestController.EAutomationType.Manual)
{
_chatGui.PrintError(
$"Stopping Questionable: {reason}. If you believe this to be correct, please restart Questionable manually.",
CommandHandler.MessageTag, CommandHandler.TagColor);
_questController.Stop(reason);
}
}
public void Dispose()
{
_clientState.TerritoryChanged -= TerritoryChanged;
}
// from https://github.com/NightmareXIV/ECommons/blob/f69e460e95134c72592654059843b138b4c01a9e/ECommons/ExcelServices/TerritoryIntendedUseEnum.cs#L5
[UsedImplicitly(ImplicitUseTargetFlags.Members, Reason = "game data")]
private enum ETerritoryIntendedUseEnum : byte
{
CityArea = 0,
OpenWorld = 1,
Inn = 2,
Dungeon = 3,
VariantDungeon = 4,
Gaol = 5,
StartingArea = 6,
QuestArea = 7,
AllianceRaid = 8,
QuestBattle = 9,
Trial = 10,
QuestArea2 = 12,
ResidentialArea = 13,
HousingInstances = 14,
QuestArea3 = 15,
Raid = 16,
Raid2 = 17,
Frontline = 18,
ChocoboSquare = 20,
RestorationEvent = 21,
Sanctum = 22,
GoldSaucer = 23,
LordOfVerminion = 25,
Diadem = 26,
HallOfTheNovice = 27,
CrystallineConflict = 28,
QuestBattle2 = 29,
Barracks = 30,
DeepDungeon = 31,
SeasonalEvent = 32,
TreasureMapDuty = 33,
SeasonalEventDuty = 34,
Battlehall = 35,
CrystallineConflict2 = 37,
Diadem2 = 38,
RivalWings = 39,
Unknown1 = 40,
Eureka = 41,
SeasonalEvent2 = 43,
LeapOfFaith = 44,
MaskedCarnivale = 45,
OceanFishing = 46,
Diadem3 = 47,
Bozja = 48,
IslandSanctuary = 49,
Battlehall2 = 50,
Battlehall3 = 51,
LargeScaleRaid = 52,
LargeScaleSavageRaid = 53,
QuestArea4 = 54,
TribalInstance = 56,
CriterionDuty = 57,
CriterionSavageDuty = 58,
Blunderville = 59,
}
}

View File

@ -7,6 +7,7 @@ using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Controller.GameUi;
using Questionable.Controller.Utils;
using Questionable.Windows;
namespace Questionable;
@ -23,6 +24,7 @@ internal sealed class DalamudInitializer : IDisposable
private readonly ConfigWindow _configWindow;
private readonly IToastGui _toastGui;
private readonly Configuration _configuration;
private readonly PartyWatchDog _partyWatchDog;
private readonly ILogger<DalamudInitializer> _logger;
public DalamudInitializer(
@ -42,6 +44,7 @@ internal sealed class DalamudInitializer : IDisposable
PriorityWindow priorityWindow,
IToastGui toastGui,
Configuration configuration,
PartyWatchDog partyWatchDog,
ILogger<DalamudInitializer> logger)
{
_pluginInterface = pluginInterface;
@ -54,6 +57,7 @@ internal sealed class DalamudInitializer : IDisposable
_configWindow = configWindow;
_toastGui = toastGui;
_configuration = configuration;
_partyWatchDog = partyWatchDog;
_logger = logger;
_windowSystem.AddWindow(oneTimeSetupWindow);
@ -77,6 +81,7 @@ internal sealed class DalamudInitializer : IDisposable
private void FrameworkUpdate(IFramework framework)
{
_partyWatchDog.Update();
_questController.Update();
try

View File

@ -82,7 +82,7 @@ internal sealed class BossModIpc
ClearPreset();
}
public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
public bool IsConfiguredToRunSoloInstance(ElementId questId, SinglePlayerDutyOptions? dutyOptions)
{
if (!IsSupported())
return false;
@ -90,7 +90,8 @@ internal sealed class BossModIpc
if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
return false;
if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData))
dutyOptions ??= new();
if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyOptions.Index, out var cfcData))
return false;
if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
@ -99,6 +100,6 @@ internal sealed class BossModIpc
if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
return true;
return enabledByDefault;
return dutyOptions.Enabled;
}
}

View File

@ -16,6 +16,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Interactions;
using Questionable.Model;
using Questionable.Model.Questing;
using Action = Lumina.Excel.Sheets.Action;
@ -427,6 +428,11 @@ internal sealed unsafe class GameFunctions
return true;
}
if (_condition[ConditionFlag.Unconscious] &&
_condition[ConditionFlag.SufferingStatusAffliction63] &&
_clientState.TerritoryType == SinglePlayerDuty.LahabreaTerritoryId)
return false; // needed to process the tasks
return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||

View File

@ -401,14 +401,15 @@ internal sealed unsafe class QuestFunctions
return 1000 * quest.AllSteps().Count(x => x.Step.AetheryteShortcut != null);
}
public List<ElementId> GetPriorityQuests()
public List<ElementId> GetPriorityQuests(bool onlyClassAndRoleQuests = false)
{
List<ElementId> priorityQuests =
[
new QuestId(1157), // Garuda (Hard)
new QuestId(1158), // Titan (Hard)
..QuestData.CrystalTowerQuests
];
List<ElementId> priorityQuests = [];
if (!onlyClassAndRoleQuests)
{
priorityQuests.Add(new QuestId(1157)); // Garuda (Hard)
priorityQuests.Add(new QuestId(1158)); // Titan (Hard)
priorityQuests.AddRange(QuestData.CrystalTowerQuests);
}
EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId ?? EClassJob.Adventurer;
uint[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray();

View File

@ -20,6 +20,7 @@ using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Leves;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
@ -231,6 +232,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection
.AddTaskExecutor<SinglePlayerDuty.WaitSinglePlayerDuty, SinglePlayerDuty.WaitSinglePlayerDutyExecutor>();
serviceCollection.AddTaskExecutor<SinglePlayerDuty.DisableAi, SinglePlayerDuty.DisableAiExecutor>();
serviceCollection.AddTaskExecutor<SinglePlayerDuty.SetTarget, SinglePlayerDuty.SetTargetExecutor>();
serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
@ -259,6 +261,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<ShopController>();
serviceCollection.AddSingleton<InterruptHandler>();
serviceCollection.AddSingleton<PartyWatchDog>();
serviceCollection.AddSingleton<CraftworksSupplyController>();
serviceCollection.AddSingleton<CreditsController>();
serviceCollection.AddSingleton<HelpUiController>();

View File

@ -9,7 +9,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (quest.Id is SatisfactionSupplyNpcId)
if (quest.Id is SatisfactionSupplyNpcId or AlliedSocietyDailyId)
yield break;
var questAccepts =

View File

@ -34,7 +34,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
(EClassJob.BlackMage, "Magical Ranged Role Quests"),
];
#if false
private readonly string[] _retryDifficulties = ["Normal", "Easy", "Very Easy"];
#endif
private readonly TerritoryData _territoryData;
private readonly QuestRegistry _questRegistry;
@ -122,70 +124,24 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
foreach (var (questId, index, cfcData) in _territoryData.GetAllQuestsWithQuestBattles())
{
IQuestInfo questInfo = _questData.GetQuestInfo(questId);
QuestStep questStep = new QuestStep
{
SinglePlayerDutyIndex = 0,
BossModEnabled = false,
};
bool enabled;
if (_questRegistry.TryGetQuest(questId, out var quest))
{
if (quest.Root.Disabled)
{
_logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId);
enabled = false;
}
else
{
var foundStep = quest.AllSteps().FirstOrDefault(x =>
x.Step.InteractionType == EInteractionType.SinglePlayerDuty &&
x.Step.SinglePlayerDutyIndex == index);
if (foundStep == default)
{
_logger.LogWarning(
"Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId,
index);
enabled = false;
}
else
{
questStep = foundStep.Step;
enabled = true;
}
}
}
else
{
_logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId);
enabled = false;
}
(bool enabled, SinglePlayerDutyOptions options) = FindDutyOptions(questId, index);
string name = $"{FormatLevel(questInfo.Level)} {questInfo.Name}";
if (!string.IsNullOrEmpty(cfcData.Name) && !questInfo.Name.EndsWith(cfcData.Name, StringComparison.Ordinal))
name += $" ({cfcData.Name})";
if (questsWithMultipleBattles.Contains(questId))
name += $" (Part {questStep.SinglePlayerDutyIndex + 1})";
name += $" (Part {options.Index + 1})";
else if (cfcData.ContentFinderConditionId is 674 or 691)
name += " (Melee/Phys. Ranged)";
var dutyInfo = new SinglePlayerDutyInfo(
cfcData.ContentFinderConditionId,
cfcData.TerritoryId,
name,
questInfo.Expansion,
questInfo.JournalGenre ?? uint.MaxValue,
questInfo.SortKey,
questStep.SinglePlayerDutyIndex,
enabled,
questStep.BossModEnabled,
questStep.BossModNotes);
var dutyInfo = new SinglePlayerDutyInfo(name, questInfo, cfcData, options, enabled);
if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334)
if (dutyInfo.IsLimsaStart)
startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo);
else if (cfcData.ContentFinderConditionId is 296 or 297 or 299 or 298)
else if (dutyInfo.IsGridaniaStart)
startingCityBattles[EAetheryteLocation.Gridania].Add(dutyInfo);
else if (cfcData.ContentFinderConditionId is 335 or 312 or 337 or 336)
else if (dutyInfo.IsUldahStart)
startingCityBattles[EAetheryteLocation.Uldah].Add(dutyInfo);
else if (questInfo.IsMainScenarioQuest)
mainScenarioBattles.Add(dutyInfo);
@ -196,7 +152,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
foreach (var roleClassJob in classJobs)
roleQuestBattles[roleClassJob].Add(dutyInfo);
}
else if (dutyInfo.CfcId is 845 or 1016)
else if (dutyInfo.IsOtherRoleQuest)
otherRoleQuestBattles.Add(dutyInfo);
else
otherBattles.Add(dutyInfo);
@ -220,7 +176,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
x =>
x.Value
// level 10 quests use the same quest battle for [you started as this class] and [you picked this class up later]
.DistinctBy(y => y.CfcId)
.DistinctBy(y => y.ContentFinderConditionId)
.OrderBy(y => y.JournalGenreId)
.ThenBy(y => y.SortKey)
.ThenBy(y => y.Index)
@ -242,6 +198,47 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
.ToImmutableList();
}
private (bool Enabled, SinglePlayerDutyOptions Options) FindDutyOptions(ElementId questId, byte index)
{
SinglePlayerDutyOptions options = new()
{
Index = 0,
Enabled = false,
};
if (_questRegistry.TryGetQuest(questId, out var quest))
{
if (quest.Root.Disabled)
{
_logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId);
return (false, options);
}
else
{
var foundStep = quest.AllSteps()
.Select(x => x.Step)
.FirstOrDefault(x =>
x.InteractionType == EInteractionType.SinglePlayerDuty &&
x.SinglePlayerDutyIndex == index);
if (foundStep == null)
{
_logger.LogWarning(
"Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId,
index);
return (false, options);
}
else
{
return (true, foundStep.SinglePlayerDutyOptions ?? options);
}
}
}
else
{
_logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId);
return (false, options);
}
}
private string BuildJournalGenreLabel(uint journalGenreId)
{
var journalGenre = _dataManager.GetExcelSheet<JournalGenre>().GetRow(journalGenreId);
@ -250,7 +247,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
string genreName = journalGenre.Name.ExtractText();
string categoryName = journalCategory.Name.ExtractText();
return $"{categoryName} {SeIconChar.ArrowRight.ToIconString()} {genreName}";
return $"{categoryName} \u203B {genreName}";
}
public override void DrawTab()
@ -268,12 +265,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
using (ImRaii.PushIndent(ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X))
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed,
"Work in Progress: For now, this will always use BossMod for combat.");
using (_ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextUnformatted("Work in Progress:");
ImGui.BulletText("Will always use BossMod for combat (ignoring the configured combat module).");
ImGui.BulletText("Only a small subset of quest battles have been tested - most of which are in the MSQ.");
ImGui.BulletText("When retrying a failed battle, it will always start at 'Normal' difficulty.");
ImGui.BulletText("Please don't enable this option when using a BossMod fork (such as Reborn);\nwith the missing combat module configuration, it is unlikely to be compatible.");
}
#if false
using (ImRaii.Disabled(!runSoloInstancesWithBossMod))
{
ImGui.Spacing();
int retryDifficulty = Configuration.SinglePlayerDuties.RetryDifficulty;
if (ImGui.Combo("Difficulty when retrying a quest battle", ref retryDifficulty, _retryDifficulties,
_retryDifficulties.Length))
@ -282,6 +286,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
Save();
}
}
#endif
}
ImGui.Separator();
@ -423,13 +428,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
{
ImGui.TableNextRow();
string[] labels = dutyInfo.BossModEnabledByDefault
string[] labels = dutyInfo.EnabledByDefault
? SupportedCfcOptions
: UnsupportedCfcOptions;
int value = 0;
if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.ContentFinderConditionId))
value = 1;
if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.ContentFinderConditionId))
value = 2;
if (ImGui.TableNextColumn())
@ -445,7 +450,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
ImGui.TextUnformatted(dutyInfo.Name);
ImGui.Separator();
ImGui.BulletText($"TerritoryId: {dutyInfo.TerritoryId}");
ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.CfcId}");
ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.ContentFinderConditionId}");
}
}
@ -457,11 +462,18 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
else if (dutyInfo.Notes.Count > 0)
{
using var color = new ImRaii.Color();
color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow);
if (!dutyInfo.EnabledByDefault)
color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow);
else
color.Push(ImGuiCol.TextDisabled, ImGuiColors.ParsedBlue);
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString());
if (!dutyInfo.EnabledByDefault)
ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString());
else
ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
@ -478,19 +490,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
if (ImGui.TableNextColumn())
{
using var _ = ImRaii.PushId($"##Duty{dutyInfo.CfcId}");
using var _ = ImRaii.PushId($"##Duty{dutyInfo.ContentFinderConditionId}");
using (ImRaii.Disabled(!dutyInfo.Enabled))
{
ImGui.SetNextItemWidth(200);
if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
{
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.ContentFinderConditionId);
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.ContentFinderConditionId);
if (value == 1)
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.ContentFinderConditionId);
else if (value == 2)
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.ContentFinderConditionId);
Save();
}
@ -519,14 +531,28 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
}
private sealed record SinglePlayerDutyInfo(
uint CfcId,
uint TerritoryId,
string Name,
EExpansionVersion Expansion,
uint JournalGenreId,
ushort SortKey,
byte Index,
bool Enabled,
bool BossModEnabledByDefault,
List<string> Notes);
IQuestInfo QuestInfo,
TerritoryData.ContentFinderConditionData ContentFinderConditionData,
SinglePlayerDutyOptions Options,
bool Enabled)
{
public EExpansionVersion Expansion => QuestInfo.Expansion;
public uint JournalGenreId => QuestInfo.JournalGenre ?? uint.MaxValue;
public ushort SortKey => QuestInfo.SortKey;
public uint ContentFinderConditionId => ContentFinderConditionData.ContentFinderConditionId;
public uint TerritoryId => ContentFinderConditionData.TerritoryId;
public byte Index => Options.Index;
public bool EnabledByDefault => Options.Enabled;
public IReadOnlyList<string> Notes => Options.Notes;
public bool IsLimsaStart => ContentFinderConditionId is 332 or 333 or 313 or 334;
public bool IsGridaniaStart => ContentFinderConditionId is 296 or 297 or 299 or 298;
public bool IsUldahStart => ContentFinderConditionId is 335 or 312 or 337 or 336;
/// <summary>
/// 'Other' role quest is the post-EW/DT role quests.
/// </summary>
public bool IsOtherRoleQuest => ContentFinderConditionId is 845 or 1016;
}
}