Compare commits

...

11 Commits
Ixal ... master

93 changed files with 950 additions and 158 deletions

View File

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

View File

@ -21,8 +21,7 @@ internal static class SinglePlayerDutyOptionsExtensions
Assignment(nameof(SinglePlayerDutyOptions.Enabled),
dutyOptions.Enabled, emptyOptions.Enabled)
.AsSyntaxNodeOrToken(),
Assignment(nameof(SinglePlayerDutyOptions.Notes),
dutyOptions.Notes, emptyOptions.Notes)
AssignmentList(nameof(SinglePlayerDutyOptions.Notes), dutyOptions.Notes)
.AsSyntaxNodeOrToken(),
Assignment(nameof(SinglePlayerDutyOptions.Index),
dutyOptions.Index, emptyOptions.Index)

View File

@ -141,7 +141,8 @@
"TerritoryId": 141,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}

View File

@ -29,13 +29,7 @@
},
"TerritoryId": 129,
"InteractionType": "Interact",
"DialogueChoices": [
{
"Type": "YesNo",
"Prompt": "TEXT_CLSROG011_00102_Q9_000_901",
"Yes": true
}
]
"TargetTerritoryId": 129
},
{
"DataId": 1009943,

View File

@ -5,6 +5,26 @@
{
"Sequence": 0,
"Steps": [
{
"TerritoryId": 129,
"InteractionType": "EquipItem",
"ItemId": 7952,
"AetheryteShortcut": "Limsa Lominsa",
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
},
"StepIf": {
"Item": {
"NotInInventory": true
}
}
}
},
{
"TerritoryId": 129,
"InteractionType": "EquipRecommended"
},
{
"DataId": 1009944,
"Position": {
@ -14,16 +34,12 @@
},
"TerritoryId": 129,
"InteractionType": "Interact",
"AetheryteShortcut": "Limsa Lominsa",
"TargetTerritoryId": 129,
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Fishermens' Guild"
],
"SkipConditions": {
"AetheryteShortcutIf": {
"InSameTerritory": true
},
"StepIf": {
"ExtraCondition": "RoguesGuild"
}
@ -45,6 +61,25 @@
{
"Sequence": 1,
"Steps": [
{
"DataId": 2004936,
"Position": {
"X": -151.90363,
"Y": -128.16058,
"Z": 256.8551
},
"TerritoryId": 129,
"InteractionType": "Interact",
"TargetTerritoryId": 129,
"SkipConditions": {
"StepIf": {
"InTerritory": [
134
],
"ExtraCondition": "NotRoguesGuild"
}
}
},
{
"Position": {
"X": 31.662792,

View File

@ -152,15 +152,9 @@
"TerritoryId": 129,
"InteractionType": "Interact",
"TargetTerritoryId": 129,
"AethernetShortcut": [
"[Limsa Lominsa] Fishermens' Guild",
"[Limsa Lominsa] The Aftcastle"
],
"SkipConditions": {
"StepIf": {
"InTerritory": [
128
]
"ExtraCondition": "NotRoguesGuild"
}
}
},
@ -173,6 +167,10 @@
},
"TerritoryId": 128,
"InteractionType": "Interact",
"AethernetShortcut": [
"[Limsa Lominsa] Fishermens' Guild",
"[Limsa Lominsa] The Aftcastle"
],
"DialogueChoices": [
{
"Type": "List",

View File

@ -291,7 +291,17 @@
"Z": 239.30713
},
"TerritoryId": 129,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"(phase 1) AI doesn't move or pick up the stolen firearms",
"(phase 1 + 2) AI automatically removes Hidden status",
"(phase 2) AI only moves while targeted enemies are in range + gets stuck on corners while trying to get to irrelevant enemies",
"(phase 2) AI doesn't even attempt to navigate to the end of the quest"
]
}
}
]
},

View File

@ -36,7 +36,8 @@
"TerritoryId": 137,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "Eastern La Noscea - Wineport",
"Fly": true

View File

@ -117,7 +117,8 @@
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}

View File

@ -119,7 +119,8 @@
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -251,7 +251,8 @@
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"Fly": true
}

View File

@ -140,7 +140,8 @@
"TerritoryId": 148,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -114,6 +114,7 @@
"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

@ -31,6 +31,7 @@
"InteractionType": "SinglePlayerDuty",
"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

@ -79,6 +79,7 @@
"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"
]

View File

@ -71,6 +71,7 @@
"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)"
]

View File

@ -75,7 +75,8 @@
"TerritoryId": 134,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -31,6 +31,7 @@
"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

@ -60,6 +60,7 @@
"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"
]

View File

@ -46,7 +46,8 @@
"TerritoryId": 145,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -80,7 +80,8 @@
"TerritoryId": 130,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "Ul'dah",
"AethernetShortcut": [

View File

@ -90,7 +90,8 @@
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "East Shroud - Hawthorne Hut",
"SkipConditions": {

View File

@ -65,7 +65,8 @@
"TerritoryId": 135,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"AethernetShortcut": [
"[Limsa Lominsa] The Aftcastle",

View File

@ -60,7 +60,8 @@
"TerritoryId": 140,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
},
"AetheryteShortcut": "Western Thanalan - Horizon"
}

View File

@ -160,7 +160,8 @@
"TerritoryId": 141,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -39,7 +39,8 @@
"TerritoryId": 140,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -30,7 +30,8 @@
"TerritoryId": 141,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -67,6 +67,7 @@
"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"
]

View File

@ -66,7 +66,8 @@
"TerritoryId": 152,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -86,6 +86,7 @@
"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

@ -105,7 +105,8 @@
"TerritoryId": 1053,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"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

@ -97,7 +97,8 @@
"InteractionType": "SinglePlayerDuty",
"Fly": true,
"SinglePlayerDutyOptions": {
"Enabled": true
"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

@ -60,7 +60,8 @@
"TerritoryId": 401,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -80,7 +80,8 @@
"[Ishgard] The Tribunal"
],
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

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

View File

@ -80,7 +80,8 @@
"InteractionType": "SinglePlayerDuty",
"DisableNavmesh": true,
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -76,7 +76,8 @@
"TerritoryId": 418,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -58,7 +58,8 @@
"Emote": "lookout",
"StopDistance": 0.25,
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -49,7 +49,8 @@
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
],
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -70,7 +70,8 @@
"TerritoryId": 402,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true
"Enabled": true,
"TestedBossModVersion": 292
}
}
]

View File

@ -87,6 +87,7 @@
"Comment": "Estinien vs. Arch Ultima",
"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",

View File

@ -48,6 +48,7 @@
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"Doesn't walk to the teleporter to finish the duty"
]

View File

@ -78,6 +78,14 @@
},
"TerritoryId": 918,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 292,
"Notes": [
"(phase 2) AI doesn't target Ardbert to start combat",
"(phase 2) VBM module: Elidibus' line cleave only covers half the length of the actual line (survivable)"
]
},
"Comment": "Fight NPCs, then Elidibus",
"DialogueChoices": [
{

View File

@ -84,7 +84,10 @@
},
"TerritoryId": 180,
"InteractionType": "SinglePlayerDuty",
"Comment": "Great Ship Vylbrand"
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 292
}
}
]
},

View File

@ -46,6 +46,15 @@
"StopDistance": 7,
"TerritoryId": 132,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 293,
"Notes": [
"(Lunar Odin) AI doesn't pull Odin to start combat",
"(Lunar Ravana) AI doesn't pull Ravana to start combat",
"(Lunar Ravana) AI doesn't move out of directional parry directions"
]
},
"Comment": "Death Unto Dawn"
}
]

View File

@ -99,6 +99,9 @@
}
]
},
{
"Sequence": 5
},
{
"Sequence": 255,
"Steps": [

View File

@ -54,6 +54,11 @@
},
"TerritoryId": 816,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 293,
"$": "test: Redacted"
},
"ItemId": 2002569
}
]

View File

@ -153,7 +153,7 @@
"Y": -14.169313,
"Z": 114.76306
},
"StopDistance": 7,
"StopDistance": 6.9,
"TerritoryId": 962,
"InteractionType": "Interact"
}

View File

@ -146,7 +146,8 @@
"TerritoryId": 956,
"InteractionType": "UseItem",
"ItemId": 2003129,
"Mount": false
"Mount": false,
"DelaySecondsAtStart": 2
}
]
},

View File

@ -30,6 +30,13 @@
"TerritoryId": 621,
"InteractionType": "SinglePlayerDuty",
"Comment": "A Frosty Reception",
"SinglePlayerDutyOptions": {
"Enabled": false,
"TestedBossModVersion": 293,
"Notes": [
"(Thancred) How many enemies get pulled during the stealth section is random; if you pull multiple you can die here. Can probably be fixed by retrying on very easy."
]
},
"DialogueChoices": [
{
"Type": "List",

View File

@ -59,7 +59,15 @@
},
"TerritoryId": 958,
"InteractionType": "SinglePlayerDuty",
"Comment": "In from the Cold",
"SinglePlayerDutyOptions": {
"Enabled": false,
"Notes": [
"Instance probably only works on very easy difficulty",
"AI doesn't move to first enemy",
"AI doesn't unmount from the Magitek Reaper",
"Navmesh takes 5+ minutes to build"
]
},
"DialogueChoices": [
{
"Type": "YesNo",

View File

@ -54,7 +54,10 @@
},
"TerritoryId": 961,
"InteractionType": "SinglePlayerDuty",
"Comment": "Venat"
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 294
}
}
]
},

View File

@ -29,8 +29,11 @@
},
"TerritoryId": 958,
"InteractionType": "SinglePlayerDuty",
"AetheryteShortcut": "Garlemald - Camp Broken Glass",
"Comment": "As the Heavens Burn"
"SinglePlayerDutyOptions": {
"Enabled": true,
"TestedBossModVersion": 294
},
"AetheryteShortcut": "Garlemald - Camp Broken Glass"
}
]
},

View File

@ -302,6 +302,7 @@
"WakingSandsSolar",
"RisingStonesSolar",
"RoguesGuild",
"NotRoguesGuild",
"DockStorehouse"
]
}
@ -1285,6 +1286,10 @@
"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"
}

View File

@ -11,6 +11,7 @@ public sealed class SkipConditionConverter() : EnumConverter<EExtraSkipCondition
{ EExtraSkipCondition.WakingSandsSolar, "WakingSandsSolar" },
{ EExtraSkipCondition.RisingStonesSolar, "RisingStonesSolar"},
{ EExtraSkipCondition.RoguesGuild, "RoguesGuild"},
{ EExtraSkipCondition.NotRoguesGuild, "NotRoguesGuild"},
{ EExtraSkipCondition.DockStorehouse, "DockStorehouse"},
};
}

View File

@ -15,6 +15,7 @@ public enum EExtraSkipCondition
/// Location for ROG quests in Limsa Lominsa; located far underneath the actual lower decks.
/// </summary>
RoguesGuild,
NotRoguesGuild,
/// <summary>
/// Location for NIN quests in Eastern La Noscea; located far underneath the actual zone.

View File

@ -113,12 +113,15 @@ public sealed class QuestStep
public float CalculateActualStopDistance()
{
if (InteractionType == EInteractionType.WalkTo)
return StopDistance ?? 0.25f;
if (InteractionType == EInteractionType.AttuneAetheryte)
return StopDistance ?? 10f;
else
return StopDistance ?? DefaultStopDistance;
if (StopDistance is { } stopDistance)
return stopDistance;
return InteractionType switch
{
EInteractionType.WalkTo => 0.25f,
EInteractionType.AttuneAetheryte => 10f,
_ => DefaultStopDistance
};
}
/// <summary>

View File

@ -37,6 +37,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=tertium/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tural/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=urqopacha/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=vnavmesh/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=wachumeqimeqi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=wachunpelo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=wolekdorf/@EntryIndexedValue">True</s:Boolean>

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

@ -1,14 +1,7 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services;
using Json.Schema;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using System;
using System.IO;
using System.Numerics;
using Questionable.External;
namespace Questionable.Controller.CombatModules;
@ -19,8 +12,6 @@ internal sealed class BossModModule : ICombatModule, IDisposable
private readonly BossModIpc _bossModIpc;
private readonly Configuration _configuration;
private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!;
public BossModModule(
ILogger<BossModModule> logger,
BossModIpc bossModIpc,
@ -43,12 +34,7 @@ internal sealed class BossModModule : ICombatModule, IDisposable
{
try
{
if (_bossModIpc.GetPreset("Questionable") == null)
{
using var reader = new StreamReader(Preset);
_logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _bossModIpc.CreatePreset(reader.ReadToEnd(), true));
}
_bossModIpc.SetPreset("Questionable");
_bossModIpc.SetPreset(BossModIpc.EPreset.Overworld);
return true;
}
catch (IpcError e)

View File

@ -0,0 +1,293 @@
{
"Name": "Questionable",
"Modules": {
"BossMod.Autorotation.xan.DNC": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.MCH": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.MNK": [
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.PCT": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
},
{
"Track": "Motifs",
"Option": "Downtime"
}
],
"BossMod.Autorotation.xan.PLD": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SAM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SGE": [
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.VPR": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.NIN": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.GNB": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SMN": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.DRK": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.RPR": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.WHM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.AST": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.BRD": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SCH": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.BLM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.RDM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.DRG": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.VeynWAR": [
{
"Track": "AOE",
"Option": "AutoFinishCombo"
}
],
"BossMod.Autorotation.MiscAI.NormalMovement": [
{
"Track": "Destination",
"Option": "Pathfind"
}
]
}
}

View File

@ -1,5 +1,5 @@
{
"Name": "Questionable",
"Name": "Questionable - Quest Battles",
"Modules": {
"BossMod.Autorotation.MiscAI.AutoFarm": [],
"BossMod.Autorotation.MiscAI.AutoPull": [

View File

@ -89,6 +89,7 @@ internal sealed class MovementController : IDisposable
public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
public DestinationData? Destination { get; set; }
public DateTime MovementStartedAt { get; private set; } = DateTime.Now;
public int BuiltNavmeshPercent => _navmeshIpc.GetBuildProgress();
public void Update()
{

View File

@ -687,6 +687,17 @@ internal sealed class QuestController : MiniTaskController<QuestController>
public bool IsRunning => !_taskQueue.AllTasksComplete;
public TaskQueue TaskQueue => _taskQueue;
public string? CurrentTaskState
{
get
{
if (_taskQueue.CurrentTaskExecutor is IDebugStateProvider debugStateProvider)
return debugStateProvider.GetDebugState();
else
return null;
}
}
public sealed class QuestProgress
{
public Quest Quest { get; }

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

@ -0,0 +1,27 @@
namespace Questionable.Controller.Steps.Common;
internal sealed class WaitNavmesh
{
internal sealed record Task : ITask
{
public override string ToString() => "Wait(navmesh)";
}
internal sealed class Executor(MovementController movementController) : TaskExecutor<Task>, IDebugStateProvider
{
protected override bool Start() => true;
public override ETaskResult Update() =>
movementController.IsNavmeshReady ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
public override bool ShouldInterruptOnDamage() => false;
public string? GetDebugState()
{
if (!movementController.IsNavmeshReady)
return $"Navmesh: {movementController.BuiltNavmeshPercent}%";
else
return 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)
{
if (delayedFinalCheck)
return ETaskResult.TaskComplete;
_continueAt = DateTime.Now.AddSeconds(0.2);
delayedFinalCheck = true;
return ETaskResult.StillRunning;
}
}
IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);

View File

@ -96,7 +96,9 @@ internal static class SinglePlayerDuty
}
internal sealed class WaitSinglePlayerDutyExecutor(
BossModIpc bossModIpc) : TaskExecutor<WaitSinglePlayerDuty>, IStoppableTaskExecutor
BossModIpc bossModIpc,
MovementController movementController)
: TaskExecutor<WaitSinglePlayerDuty>, IStoppableTaskExecutor, IDebugStateProvider
{
protected override bool Start() => true;
@ -110,6 +112,14 @@ internal static class SinglePlayerDuty
public void StopNow() => bossModIpc.DisableAi();
public override bool ShouldInterruptOnDamage() => false;
public string? GetDebugState()
{
if (!movementController.IsNavmeshReady)
return $"Navmesh: {movementController.BuiltNavmeshPercent}%";
else
return null;
}
}
internal sealed record DisableAi : ITask

View File

@ -45,12 +45,13 @@ internal static class QuestCleanUp
}
// have any of the previous sequences interacted with the issuer?
var previousSequences =
var previousSteps =
quest.AllSequences()
.Where(x => x.Sequence > 0 // quest accept doesn't ever put us into a mount
&& x.Sequence < sequence.Sequence)
.SelectMany(x => x.Steps)
.ToList();
if (previousSequences.SelectMany(x => x.Steps).All(x => x.DataId != mountConfiguration.IssuerDataId))
if (!previousSteps.Any(x => x.DataId != null && mountConfiguration.IssuerDataIds.Contains(x.DataId.Value)))
{
// this quest hasn't given us a mount yet
logger.LogInformation("Haven't talked to mount NPC for this allied society quest; {Aetheryte}", mountConfiguration.ClosestAetheryte);

View File

@ -20,7 +20,6 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AethernetShortcut
{
internal sealed class Factory(
MovementController movementController,
AetheryteData aetheryteData,
TerritoryData territoryData,
IClientState clientState)
@ -31,8 +30,7 @@ internal static class AethernetShortcut
if (step.AethernetShortcut == null)
yield break;
yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)");
yield return new WaitNavmesh.Task();
yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To,
step.SkipConditions?.AethernetShortcutIf ?? new());

View File

@ -38,7 +38,6 @@ internal static class Gather
}
internal sealed class DelayedGatheringExecutor(
MovementController movementController,
GatheringData gatheringData,
GatheringPointRegistry gatheringPointRegistry,
TerritoryData territoryData,
@ -85,8 +84,7 @@ internal static class Gather
yield return new WaitCondition.Task(() => clientState.TerritoryType == territoryId,
$"Wait(territory: {territoryData.GetNameAndId(territoryId)})");
yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)");
yield return new WaitNavmesh.Task();
yield return new GatheringTask(gatheringPointId, Task.GatheredItem);
yield return new WaitAtEnd.WaitDelay();

View File

@ -25,7 +25,6 @@ namespace Questionable.Controller.Steps.Shared;
internal static class MoveTo
{
internal sealed class Factory(
MovementController movementController,
IClientState clientState,
AetheryteData aetheryteData,
TerritoryData territoryData,
@ -67,10 +66,7 @@ internal static class MoveTo
$"Wait(territory: {territoryData.GetNameAndId(step.TerritoryId)})");
if (!step.DisableNavmesh)
{
yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)");
}
yield return new WaitNavmesh.Task();
yield return new MoveTask(step, destination);

View File

@ -310,6 +310,7 @@ internal static class SkipCondition
EExtraSkipCondition.WakingSandsSolar => territoryType == 212 && position.X >= 24,
EExtraSkipCondition.RisingStonesSolar => territoryType == 351 && position.Z <= -28,
EExtraSkipCondition.RoguesGuild => territoryType == 129 && position.Y <= -115,
EExtraSkipCondition.NotRoguesGuild => territoryType == 129 && position.Y > -115,
EExtraSkipCondition.DockStorehouse => territoryType == 137 && position.Y <= -20,
_ => throw new ArgumentOutOfRangeException(nameof(condition), condition, null)
};

View File

@ -30,6 +30,11 @@ internal interface IStoppableTaskExecutor : ITaskExecutor
void StopNow();
}
internal interface IDebugStateProvider : ITaskExecutor
{
string? GetDebugState();
}
internal abstract class TaskExecutor<T> : ITaskExecutor
where T : class, ITask
{

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

@ -13,12 +13,12 @@ internal sealed class AlliedSocietyData
public ReadOnlyDictionary<ushort, AlliedSocietyMountConfiguration> Mounts { get; } =
new Dictionary<ushort, AlliedSocietyMountConfiguration>
{
{ 66, new(1016093, EAetheryteLocation.SeaOfCloudsOkZundu) },
{ 79, new(1017031, EAetheryteLocation.DravanianForelandsAnyxTrine) },
{ 88, new(1017470, EAetheryteLocation.ChurningMistsZenith) },
{ 89, new(1017322, EAetheryteLocation.ChurningMistsZenith) },
{ 147, new(1024777, EAetheryteLocation.FringesPeeringStones) },
{ 369, new(1051798, EAetheryteLocation.KozamaukaDockPoga) },
{ 66, new([1016093], EAetheryteLocation.SeaOfCloudsOkZundu) },
{ 79, new([1017031], EAetheryteLocation.DravanianForelandsAnyxTrine) },
{ 88, new([1017470, 1017432], EAetheryteLocation.ChurningMistsZenith) },
{ 89, new([1017322], EAetheryteLocation.ChurningMistsZenith) },
{ 147, new([1024777], EAetheryteLocation.FringesPeeringStones) },
{ 369, new([1051798], EAetheryteLocation.KozamaukaDockPoga) },
}.AsReadOnly();
public EAlliedSociety GetCommonAlliedSocietyTurnIn(ElementId elementId)
@ -63,4 +63,4 @@ internal sealed class AlliedSocietyData
}
}
public sealed record AlliedSocietyMountConfiguration(uint IssuerDataId, EAetheryteLocation ClosestAetheryte);
public sealed record AlliedSocietyMountConfiguration(List<uint> IssuerDataIds, EAetheryteLocation ClosestAetheryte);

View File

@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
@ -9,7 +13,13 @@ namespace Questionable.External;
internal sealed class BossModIpc
{
private const string Name = "BossMod";
private const string PluginName = "BossMod";
private static readonly ReadOnlyDictionary<EPreset, PresetDefinition> PresetDefinitions = new Dictionary<EPreset, PresetDefinition>
{
{ EPreset.Overworld, new PresetDefinition("Questionable", "Overworld") },
{ EPreset.QuestBattle, new PresetDefinition("Questionable - Quest Battles", "QuestBattle") },
}.AsReadOnly();
private readonly Configuration _configuration;
private readonly ICommandManager _commandManager;
@ -29,10 +39,10 @@ internal sealed class BossModIpc
_commandManager = commandManager;
_territoryData = territoryData;
_getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get");
_createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>($"{Name}.Presets.Create");
_setPreset = pluginInterface.GetIpcSubscriber<string, bool>($"{Name}.Presets.SetActive");
_clearPreset = pluginInterface.GetIpcSubscriber<bool>($"{Name}.Presets.ClearActive");
_getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{PluginName}.Presets.Get");
_createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>($"{PluginName}.Presets.Create");
_setPreset = pluginInterface.GetIpcSubscriber<string, bool>($"{PluginName}.Presets.SetActive");
_clearPreset = pluginInterface.GetIpcSubscriber<bool>($"{PluginName}.Presets.ClearActive");
}
public bool IsSupported()
@ -47,19 +57,13 @@ internal sealed class BossModIpc
}
}
public string? GetPreset(string name)
public void SetPreset(EPreset preset)
{
return _getPreset.InvokeFunc(name);
}
var definition = PresetDefinitions[preset];
if (_getPreset.InvokeFunc(definition.Name) == null)
_createPreset.InvokeFunc(definition.Content, true);
public bool CreatePreset(string name, bool overwrite)
{
return _createPreset.InvokeFunc(name, overwrite);
}
public void SetPreset(string name)
{
_setPreset.InvokeFunc(name);
_setPreset.InvokeFunc(definition.Name);
}
public void ClearPreset()
@ -68,11 +72,11 @@ internal sealed class BossModIpc
}
// TODO this should use your actual rotation plugin, not always vbm
public void EnableAi(string presetName = "VBM Default")
public void EnableAi()
{
_commandManager.ProcessCommand("/vbmai on");
_commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles true");
SetPreset(presetName);
SetPreset(EPreset.QuestBattle);
}
public void DisableAi()
@ -94,12 +98,36 @@ internal sealed class BossModIpc
if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyOptions.Index, out var cfcData))
return false;
if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData
.ContentFinderConditionId))
return false;
if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData
.ContentFinderConditionId))
return true;
return dutyOptions.Enabled;
}
public enum EPreset
{
Overworld,
QuestBattle,
}
private sealed class PresetDefinition(string name, string fileName)
{
public string Name { get; } = name;
public string Content { get; } = LoadPreset(fileName);
private static string LoadPreset(string name)
{
Stream stream =
typeof(BossModIpc).Assembly.GetManifestResourceStream(
$"Questionable.Controller.CombatModules.BossModPreset.{name}") ??
throw new InvalidOperationException($"Preset {name} was not found");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
}

View File

@ -20,6 +20,7 @@ internal sealed class NavmeshIpc
private readonly ICallGateSubscriber<List<Vector3>> _pathListWaypoints;
private readonly ICallGateSubscriber<float, object> _pathSetTolerance;
private readonly ICallGateSubscriber<Vector3, bool, float, Vector3?> _queryPointOnFloor;
private readonly ICallGateSubscriber<float> _buildProgress;
public NavmeshIpc(IDalamudPluginInterface pluginInterface, ILogger<NavmeshIpc> logger)
{
@ -35,6 +36,7 @@ internal sealed class NavmeshIpc
_pathSetTolerance = pluginInterface.GetIpcSubscriber<float, object>("vnavmesh.Path.SetTolerance");
_queryPointOnFloor =
pluginInterface.GetIpcSubscriber<Vector3, bool, float, Vector3?>("vnavmesh.Query.Mesh.PointOnFloor");
_buildProgress = pluginInterface.GetIpcSubscriber<float>("vnavmesh.Nav.BuildProgress");
}
public bool IsReady
@ -136,4 +138,19 @@ internal sealed class NavmeshIpc
else
return [];
}
public int GetBuildProgress()
{
try
{
float progress = _buildProgress.InvokeFunc();
if (progress < 0)
return 100;
return (int)(progress * 100);
}
catch (IpcError)
{
return 0;
}
}
}

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

@ -10,24 +10,28 @@
<ItemGroup>
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1"/>
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" ExcludeAssets="runtime" />
<PackageReference Include="JsonSchema.Net" Version="7.1.2" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" ExcludeAssets="runtime"/>
<PackageReference Include="JsonSchema.Net" Version="7.1.2"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GatheringPaths\GatheringPaths.csproj" />
<ProjectReference Include="..\GatheringPaths\GatheringPaths.csproj"/>
<ProjectReference Include="..\LLib\LLib.csproj"/>
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
<ProjectReference Include="..\vendor\NotificationMasterAPI\NotificationMasterAPI\NotificationMasterAPI.csproj" />
<ProjectReference Include="..\vendor\NotificationMasterAPI\NotificationMasterAPI\NotificationMasterAPI.csproj"/>
</ItemGroup>
<ItemGroup>
<None Remove="Controller\CombatModules\BossModPreset.json" />
<EmbeddedResource Include="Controller\CombatModules\BossModPreset.json">
<LogicalName>Questionable.Controller.CombatModules.BossModPreset</LogicalName>
<None Remove="Controller\CombatModules\BossModPreset_Overworld.json"/>
<None Remove="Controller\CombatModules\BossModPreset.QuestBattle.json"/>
<EmbeddedResource Include="Controller\CombatModules\BossModPreset_Overworld.json">
<LogicalName>Questionable.Controller.CombatModules.BossModPreset.Overworld</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Controller\CombatModules\BossModPreset_QuestBattle.json">
<LogicalName>Questionable.Controller.CombatModules.BossModPreset.QuestBattle</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Project>

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;
@ -234,6 +235,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTaskExecutor<SinglePlayerDuty.SetTarget, SinglePlayerDuty.SetTargetExecutor>();
serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>();
serviceCollection.AddTaskExecutor<WaitNavmesh.Task, WaitNavmesh.Executor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitNextStepOrSequence, WaitAtEnd.WaitNextStepOrSequenceExecutor>();
@ -260,6 +262,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;
@ -263,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))
@ -277,6 +286,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
Save();
}
}
#endif
}
ImGui.Separator();

View File

@ -73,25 +73,34 @@ internal sealed partial class ActiveQuestComponent
if (_combatController.IsRunning)
ImGui.TextColored(ImGuiColors.DalamudOrange, "In Combat");
else if (_questController.CurrentTaskState is { } currentTaskState)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange);
ImGui.TextUnformatted(currentTaskState);
}
else
{
ImGui.BeginDisabled();
using var _ = ImRaii.Disabled();
ImGui.TextUnformatted(_questController.DebugState ?? string.Empty);
ImGui.EndDisabled();
}
QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
if (!isMinimized)
{
using (var color = new ImRaii.Color())
{
bool colored = currentStep is
{ InteractionType: EInteractionType.Instruction or EInteractionType.WaitForManualProgress or EInteractionType.Snipe };
{
InteractionType: EInteractionType.Instruction or EInteractionType.WaitForManualProgress
or EInteractionType.Snipe
};
if (colored)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudOrange);
color.Push(ImGuiCol.Text, ImGuiColors.DalamudOrange);
ImGui.TextUnformatted(currentStep?.Comment ??
currentSequence?.Comment ?? currentQuest.Quest.Root.Comment ?? string.Empty);
if (colored)
ImGui.PopStyleColor();
}
//var nextStep = _questController.GetNextStep();
//ImGui.BeginDisabled(nextStep.Step == null);