From: Liza Carvelli Date: Mon, 18 Aug 2025 14:36:16 +0000 (+0200) Subject: Automatically register Limsa/Gridania/Ul'dah as free/favored aetherytes X-Git-Tag: v6.3~16 X-Git-Url: https://git.jacobcasper.com/?a=commitdiff_plain;h=a3e33551adeed97f31c7246c3681a3ff0f01a90e;p=Questionable.git Automatically register Limsa/Gridania/Ul'dah as free/favored aetherytes --- diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/123_ARC_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/123_ARC_Close to Home.json index d3190709..33a17610 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/123_ARC_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/123_ARC_Close to Home.json @@ -33,6 +33,11 @@ 128 ] }, + { + "TerritoryId": 132, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Gridania" + }, { "TerritoryId": 132, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/124_CNJ_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/124_CNJ_Close to Home.json index 17cac484..4d4aae56 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/124_CNJ_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/124_CNJ_Close to Home.json @@ -33,6 +33,11 @@ 128 ] }, + { + "TerritoryId": 132, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Gridania" + }, { "TerritoryId": 132, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/176_On to Bentbranch.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/176_On to Bentbranch.json index bc5ad425..ae830dc1 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/176_On to Bentbranch.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/176_On to Bentbranch.json @@ -29,6 +29,11 @@ "[Gridania] Blue Badger Gate (Central Shroud)" ] }, + { + "TerritoryId": 148, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Central Shroud - Bentbranch Meadows" + }, { "DataId": 1002980, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/507_The Gridanian Envoy.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/507_The Gridanian Envoy.json index 79af52a9..cfabb55c 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/507_The Gridanian Envoy.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/507_The Gridanian Envoy.json @@ -166,6 +166,11 @@ "InteractionType": "AttuneAetheryte", "Aetheryte": "Limsa Lominsa" }, + { + "TerritoryId": 129, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Limsa Lominsa" + }, { "TerritoryId": 129, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/85_LNC_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/85_LNC_Close to Home.json index 3f09f4c7..0ea91e5e 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/85_LNC_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/85_LNC_Close to Home.json @@ -33,6 +33,11 @@ 128 ] }, + { + "TerritoryId": 132, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Gridania" + }, { "TerritoryId": 132, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/108_MRD_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/108_MRD_Close to Home.json index 4fbdedb2..5b54a344 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/108_MRD_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/108_MRD_Close to Home.json @@ -44,6 +44,11 @@ 128 ] }, + { + "TerritoryId": 129, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Limsa Lominsa" + }, { "DataId": 1001217, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/109_ACN_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/109_ACN_Close to Home.json index d650d378..1ad87f24 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/109_ACN_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/109_ACN_Close to Home.json @@ -44,6 +44,11 @@ 128 ] }, + { + "TerritoryId": 129, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Limsa Lominsa" + }, { "DataId": 1001217, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/462_On to Summerford.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/462_On to Summerford.json index 89a62ac7..56eec4fe 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/462_On to Summerford.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/462_On to Summerford.json @@ -34,6 +34,11 @@ "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)" ] }, + { + "TerritoryId": 134, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Middle La Noscea - Summerford Farms" + }, { "DataId": 1002626, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/546_The Lominsan Envoy.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/546_The Lominsan Envoy.json index 204918e2..48ea52b9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/546_The Lominsan Envoy.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/546_The Lominsan Envoy.json @@ -166,6 +166,11 @@ "InteractionType": "AttuneAetheryte", "Aetheryte": "Gridania" }, + { + "TerritoryId": 132, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Gridania" + }, { "TerritoryId": 132, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/674_Call of the Sea.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/674_Call of the Sea.json index eb355ac2..66d42ea8 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/674_Call of the Sea.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/674_Call of the Sea.json @@ -92,6 +92,11 @@ "InteractionType": "AttuneAetheryte", "Aetheryte": "Ul'dah" }, + { + "TerritoryId": 130, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Ul'dah" + }, { "TerritoryId": 130, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3852_Prudence at This Junction.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3852_Prudence at This Junction.json index 3dcf41e2..a358cbd0 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3852_Prudence at This Junction.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3852_Prudence at This Junction.json @@ -25,6 +25,11 @@ "InteractionType": "AttuneAetheryte", "Aetheryte": "Central Thanalan - Black Brush Station" }, + { + "TerritoryId": 141, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Central Thanalan - Black Brush Station" + }, { "DataId": 1001447, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/528_The Ul'dahn Envoy.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/528_The Ul'dahn Envoy.json index b4ed9f12..45dce15f 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/528_The Ul'dahn Envoy.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/528_The Ul'dahn Envoy.json @@ -154,6 +154,11 @@ "InteractionType": "AttuneAetheryte", "Aetheryte": "Limsa Lominsa" }, + { + "TerritoryId": 129, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Limsa Lominsa" + }, { "TerritoryId": 129, "InteractionType": "AttuneAethernetShard", @@ -289,6 +294,11 @@ "InteractionType": "AttuneAetheryte", "Aetheryte": "Gridania" }, + { + "TerritoryId": 132, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Gridania" + }, { "TerritoryId": 132, "InteractionType": "AttuneAethernetShard", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/568_GLA_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/568_GLA_Close to Home.json index a17e28e7..5b503001 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/568_GLA_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/568_GLA_Close to Home.json @@ -140,6 +140,11 @@ { "Sequence": 255, "Steps": [ + { + "TerritoryId": 130, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Ul'dah" + }, { "DataId": 1001353, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/569_PGL_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/569_PGL_Close to Home.json index cd0ddea6..64ee24de 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/569_PGL_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/569_PGL_Close to Home.json @@ -140,6 +140,11 @@ { "Sequence": 255, "Steps": [ + { + "TerritoryId": 130, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Ul'dah" + }, { "DataId": 1001353, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/570_THM_Close to Home.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/570_THM_Close to Home.json index 07690a8d..09a3d93c 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/570_THM_Close to Home.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/570_THM_Close to Home.json @@ -140,6 +140,11 @@ { "Sequence": 255, "Steps": [ + { + "TerritoryId": 130, + "InteractionType": "RegisterFreeOrFavoredAetheryte", + "Aetheryte": "Ul'dah" + }, { "DataId": 1001353, "Position": { diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 3df1453d..e7a05509 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -119,6 +119,7 @@ "WalkTo", "AttuneAethernetShard", "AttuneAetheryte", + "RegisterFreeOrFavoredAetheryte", "AttuneAetherCurrent", "Combat", "UseItem", @@ -557,7 +558,14 @@ "if": { "properties": { "InteractionType": { - "const": "AttuneAetheryte" + "anyOf": [ + { + "const": "AttuneAetheryte" + }, + { + "const": "RegisterFreeOrFavoredAetheryte" + } + ] } } }, diff --git a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs index d5ebaaa4..6187300f 100644 --- a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs +++ b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs @@ -12,6 +12,7 @@ public sealed class InteractionTypeConverter() : EnumConverter { EInteractionType.WalkTo, "WalkTo" }, { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" }, { EInteractionType.AttuneAetheryte, "AttuneAetheryte" }, + { EInteractionType.RegisterFreeOrFavoredAetheryte, "RegisterFreeOrFavoredAetheryte" }, { EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" }, { EInteractionType.Combat, "Combat" }, { EInteractionType.UseItem, "UseItem" }, diff --git a/Questionable.Model/Questing/EInteractionType.cs b/Questionable.Model/Questing/EInteractionType.cs index a0514cd6..4784f8ce 100644 --- a/Questionable.Model/Questing/EInteractionType.cs +++ b/Questionable.Model/Questing/EInteractionType.cs @@ -11,6 +11,7 @@ public enum EInteractionType WalkTo, AttuneAethernetShard, AttuneAetheryte, + RegisterFreeOrFavoredAetheryte, AttuneAetherCurrent, Combat, UseItem, diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index 374c8251..872a77d9 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -119,7 +119,7 @@ public sealed class QuestStep return InteractionType switch { EInteractionType.WalkTo => 0.25f, - EInteractionType.AttuneAetheryte => 10f, + EInteractionType.AttuneAetheryte or EInteractionType.RegisterFreeOrFavoredAetheryte => 10f, _ => DefaultStopDistance }; } diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 1c4a4083..e5bb01fe 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -20,6 +20,7 @@ using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; +using Questionable.Model.Common; using Questionable.Model.Gathering; using Questionable.Model.Questing; using Quest = Questionable.Model.Quest; @@ -307,6 +308,7 @@ internal sealed class InteractionUiController : IDisposable { var quest = currentQuest.Quest; bool isTaxiStandUnlock = false; + List freeOrFavoredAetheryteRegistrations = []; if (checkAllSteps) { var sequence = quest.FindSequence(currentQuest.Sequence); @@ -314,7 +316,17 @@ internal sealed class InteractionUiController : IDisposable if (choices != null) dialogueChoices.AddRange(choices.Select(x => new DialogueChoiceInfo(quest, x))); - isTaxiStandUnlock = sequence?.Steps.Any(x => x.InteractionType == EInteractionType.UnlockTaxiStand) ?? false; + isTaxiStandUnlock = sequence?.Steps.Any(x => x.InteractionType == EInteractionType.UnlockTaxiStand) ?? + false; + freeOrFavoredAetheryteRegistrations = sequence?.Steps + .Where(x => x is + { + InteractionType: EInteractionType + .RegisterFreeOrFavoredAetheryte, + Aetheryte: not null + }) + .Select(x => x.Aetheryte!.Value).ToList() + ?? []; } else { @@ -342,6 +354,9 @@ internal sealed class InteractionUiController : IDisposable Answer = step.PurchaseMenu.Key, })); + if (step is { InteractionType: EInteractionType.RegisterFreeOrFavoredAetheryte, Aetheryte: {} aetheryte }) + freeOrFavoredAetheryteRegistrations = [aetheryte]; + isTaxiStandUnlock = step.InteractionType == EInteractionType.UnlockTaxiStand; } } @@ -358,6 +373,36 @@ internal sealed class InteractionUiController : IDisposable })); } + if (freeOrFavoredAetheryteRegistrations.Any(x => + _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(x) == + AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable)) + { + _logger.LogInformation("Adding security token aetheryte unlock dialogue choice"); + dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice + { + Type = EDialogChoiceType.List, + ExcelSheet = "transport/Aetheryte", + Prompt = ExcelRef.FromKey("TEXT_AETHERYTE_MAINMENU_TITLE"), + PromptIsRegularExpression = true, + Answer = ExcelRef.FromKey("TEXT_AETHERYTE_REGISTER_TOKEN_FAVORITE"), + AnswerIsRegularExpression = true, + })); + } else if (freeOrFavoredAetheryteRegistrations.Any(x => + _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(x) == + AetheryteRegistrationResult.FavoredDestinationAvailable)) + { + _logger.LogInformation("Adding favored aetheryte unlock dialogue choice"); + dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice + { + Type = EDialogChoiceType.List, + ExcelSheet = "transport/Aetheryte", + Prompt = ExcelRef.FromKey("TEXT_AETHERYTE_MAINMENU_TITLE"), + PromptIsRegularExpression = true, + Answer = ExcelRef.FromKey("TEXT_AETHERYTE_REGISTER_FAVORITE"), + AnswerIsRegularExpression = true, + })); + } + // add all travel dialogue choices var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest); if (targetTerritoryId != null) @@ -589,6 +634,39 @@ internal sealed class InteractionUiController : IDisposable private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest, QuestStep? step, List dialogueChoices, string actualPrompt) { + if (step is { InteractionType: EInteractionType.RegisterFreeOrFavoredAetheryte, Aetheryte: {} aetheryteLocation }) + { + var registrationResult = _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(aetheryteLocation); + if (registrationResult == AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable) + { + dialogueChoices = + [ + ..dialogueChoices, + new DialogueChoice + { + Type = EDialogChoiceType.YesNo, + ExcelSheet = "Addon", + Prompt = ExcelRef.FromRowId(102334), + Yes = true + } + ]; + } + else if (registrationResult == AetheryteRegistrationResult.FavoredDestinationAvailable) + { + dialogueChoices = + [ + ..dialogueChoices, + new DialogueChoice + { + Type = EDialogChoiceType.YesNo, + ExcelSheet = "Addon", + Prompt = ExcelRef.FromRowId(102306), + Yes = true + } + ]; + } + } + _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count); foreach (var dialogueChoice in dialogueChoices) { diff --git a/Questionable/Controller/Steps/Interactions/AetheryteFreeOrFavored.cs b/Questionable/Controller/Steps/Interactions/AetheryteFreeOrFavored.cs new file mode 100644 index 00000000..d2058547 --- /dev/null +++ b/Questionable/Controller/Steps/Interactions/AetheryteFreeOrFavored.cs @@ -0,0 +1,65 @@ +using System; +using Dalamud.Game.ClientState.Objects.Enums; +using Microsoft.Extensions.Logging; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Common; +using Questionable.Model.Questing; + +namespace Questionable.Controller.Steps.Interactions; + +internal static class AetheryteFreeOrFavored +{ + internal sealed class Factory : SimpleTaskFactory + { + public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.InteractionType != EInteractionType.RegisterFreeOrFavoredAetheryte) + return null; + + ArgumentNullException.ThrowIfNull(step.Aetheryte); + + return new Register(step.Aetheryte.Value); + } + } + + internal sealed record Register(EAetheryteLocation AetheryteLocation) : ITask + { + public bool ShouldRedoOnInterrupt() => true; + public override string ToString() => $"RegisterFreeOrFavoredAetheryte({AetheryteLocation})"; + } + + internal sealed class DoRegister( + AetheryteFunctions aetheryteFunctions, + GameFunctions gameFunctions, + ILogger logger) : TaskExecutor + { + protected override bool Start() + { + if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)) + throw new TaskException($"Aetheryte {Task.AetheryteLocation} is not attuned"); + + if (aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(Task.AetheryteLocation) == + AetheryteRegistrationResult.NotPossible) + { + logger.LogInformation("Could not register aetheryte {AetheryteLocation} as free or favored", + Task.AetheryteLocation); + return false; + } + + ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => + gameFunctions.InteractWith((uint)Task.AetheryteLocation, ObjectKind.Aetheryte)); + return true; + } + + public override ETaskResult Update() + { + return aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(Task.AetheryteLocation) == + AetheryteRegistrationResult.NotPossible + ? ETaskResult.TaskComplete + : ETaskResult.StillRunning; + } + + public override bool ShouldInterruptOnDamage() => true; + } +} diff --git a/Questionable/Controller/Steps/Movement/MoveTo.cs b/Questionable/Controller/Steps/Movement/MoveTo.cs index a44bee9a..492440da 100644 --- a/Questionable/Controller/Steps/Movement/MoveTo.cs +++ b/Questionable/Controller/Steps/Movement/MoveTo.cs @@ -27,13 +27,18 @@ internal static class MoveTo { return [new WaitForNearDataId(step.DataId.Value, step.StopDistance.Value)]; } - else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null }) + else if (step is + { + InteractionType: EInteractionType.AttuneAetheryte + or EInteractionType.RegisterFreeOrFavoredAetheryte, + Aetheryte: {} aetheryteLocation + }) { - return CreateMoveTasks(step, aetheryteData.Locations[step.Aetheryte.Value]); + return CreateMoveTasks(step, aetheryteData.Locations[aetheryteLocation]); } - else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null }) + else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: {} aethernetShard }) { - return CreateMoveTasks(step, aetheryteData.Locations[step.AethernetShard.Value]); + return CreateMoveTasks(step, aetheryteData.Locations[aethernetShard]); } return []; diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index 1347ee78..e50ecefa 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -13,7 +13,6 @@ using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Functions; using Questionable.Model; -using Questionable.Model.Common; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Shared; @@ -289,6 +288,16 @@ internal static class SkipCondition return true; } + if (step is + { + Aetheryte: { } favoredAetheryte, InteractionType: EInteractionType.RegisterFreeOrFavoredAetheryte + } && + aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(favoredAetheryte) is AetheryteRegistrationResult.NotPossible) + { + logger.LogInformation("Skipping step, already registered all possible free or favored aetherytes"); + return true; + } + if (skipConditions.AetheryteLocked != null && !aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value)) { diff --git a/Questionable/Functions/AetheryteFunctions.cs b/Questionable/Functions/AetheryteFunctions.cs index 65b075d0..14f011a6 100644 --- a/Questionable/Functions/AetheryteFunctions.cs +++ b/Questionable/Functions/AetheryteFunctions.cs @@ -18,13 +18,15 @@ internal sealed unsafe class AetheryteFunctions private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly IDataManager _dataManager; + private readonly IClientState _clientState; public AetheryteFunctions(IServiceProvider serviceProvider, ILogger logger, - IDataManager dataManager) + IDataManager dataManager, IClientState clientState) { _serviceProvider = serviceProvider; _logger = logger; _dataManager = dataManager; + _clientState = clientState; } public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue; @@ -91,4 +93,52 @@ internal sealed unsafe class AetheryteFunctions public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation) => TeleportAetheryte((uint)aetheryteLocation); + + public AetheryteRegistrationResult CanRegisterFreeOrFavoriteAetheryte(EAetheryteLocation aetheryteLocation) + { + if (_clientState.LocalPlayer == null) + return AetheryteRegistrationResult.NotPossible; + + var playerState = PlayerState.Instance(); + if (playerState == null) + return AetheryteRegistrationResult.NotPossible; + + // if we have a free or favored aetheryte assigned to this location, we don't override it (and don't upgrade + // favored to free, either). + if (playerState->FreeAetheryteId == (uint)aetheryteLocation || + playerState->FreeAetherytePlayStationPlus == (uint)aetheryteLocation) + return AetheryteRegistrationResult.NotPossible; + + bool freeFavoredSlotsAvailable = false; + for (int i = 0; i < playerState->FavouriteAetheryteCount; i++) + { + if (playerState->FavouriteAetherytes[i] == (ushort)aetheryteLocation) + return AetheryteRegistrationResult.NotPossible; + else if (playerState->FavouriteAetherytes[i] == 0) + { + freeFavoredSlotsAvailable = true; + break; + } + } + + // probably can't register a ps plus aetheryte on pc, so we don't check for that + if (playerState->IsPlayerStateFlagSet(PlayerStateFlag.IsLoginSecurityToken) && + playerState->FreeAetheryteId == 0) + return AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable; + + return freeFavoredSlotsAvailable + ? AetheryteRegistrationResult.FavoredDestinationAvailable + : AetheryteRegistrationResult.NotPossible; + } +} + +/// +/// The whole free/favored aetheryte situation is primarily relevant for early ARR anyhow, since teleporting to +/// each class quest the moment it becomes available might end up with the character running out of gil. +/// +public enum AetheryteRegistrationResult +{ + NotPossible, + SecurityTokenFreeDestinationAvailable, + FavoredDestinationAvailable, } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index d130b513..6528c540 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -191,6 +191,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection .AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactoryAndExecutor(); serviceCollection .AddTaskFactoryAndExecutor(); diff --git a/Questionable/Windows/DebugOverlay.cs b/Questionable/Windows/DebugOverlay.cs index a40a4f34..e366234c 100644 --- a/Questionable/Windows/DebugOverlay.cs +++ b/Questionable/Windows/DebugOverlay.cs @@ -161,14 +161,14 @@ internal sealed class DebugOverlay : Window position = step.Position; return true; } - else if (step.InteractionType == EInteractionType.AttuneAetheryte && step.Aetheryte != null) + else if (step is { InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.RegisterFreeOrFavoredAetheryte, Aetheryte: {} aetheryteLocation }) { - position = _aetheryteData.Locations[step.Aetheryte.Value]; + position = _aetheryteData.Locations[aetheryteLocation]; return true; } - else if (step.InteractionType == EInteractionType.AttuneAethernetShard && step.AethernetShard != null) + else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: {} aethernetShard }) { - position = _aetheryteData.Locations[step.AethernetShard.Value]; + position = _aetheryteData.Locations[aethernetShard]; return true; } else