Automatically register Limsa/Gridania/Ul'dah as free/favored aetherytes
authorLiza Carvelli <liza@carvel.li>
Mon, 18 Aug 2025 14:36:16 +0000 (16:36 +0200)
committerLiza Carvelli <liza@carvel.li>
Mon, 18 Aug 2025 16:53:28 +0000 (18:53 +0200)
26 files changed:
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/123_ARC_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/124_CNJ_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/176_On to Bentbranch.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/507_The Gridanian Envoy.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/85_LNC_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/108_MRD_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/109_ACN_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/462_On to Summerford.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/546_The Lominsan Envoy.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/674_Call of the Sea.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3852_Prudence at This Junction.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/528_The Ul'dahn Envoy.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/568_GLA_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/569_PGL_Close to Home.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/570_THM_Close to Home.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/Converter/InteractionTypeConverter.cs
Questionable.Model/Questing/EInteractionType.cs
Questionable.Model/Questing/QuestStep.cs
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/Controller/Steps/Interactions/AetheryteFreeOrFavored.cs [new file with mode: 0644]
Questionable/Controller/Steps/Movement/MoveTo.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Functions/AetheryteFunctions.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/DebugOverlay.cs

index d319070..33a1761 100644 (file)
             128
           ]
         },
+        {
+          "TerritoryId": 132,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Gridania"
+        },
         {
           "TerritoryId": 132,
           "InteractionType": "AttuneAethernetShard",
index 17cac48..4d4aae5 100644 (file)
             128
           ]
         },
+        {
+          "TerritoryId": 132,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Gridania"
+        },
         {
           "TerritoryId": 132,
           "InteractionType": "AttuneAethernetShard",
index bc5ad42..ae830dc 100644 (file)
             "[Gridania] Blue Badger Gate (Central Shroud)"
           ]
         },
+        {
+          "TerritoryId": 148,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Central Shroud - Bentbranch Meadows"
+        },
         {
           "DataId": 1002980,
           "Position": {
index 79af52a..cfabb55 100644 (file)
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Limsa Lominsa"
         },
+        {
+          "TerritoryId": 129,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Limsa Lominsa"
+        },
         {
           "TerritoryId": 129,
           "InteractionType": "AttuneAethernetShard",
index 3f09f4c..0ea91e5 100644 (file)
             128
           ]
         },
+        {
+          "TerritoryId": 132,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Gridania"
+        },
         {
           "TerritoryId": 132,
           "InteractionType": "AttuneAethernetShard",
index 4fbdedb..5b54a34 100644 (file)
             128
           ]
         },
+        {
+          "TerritoryId": 129,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Limsa Lominsa"
+        },
         {
           "DataId": 1001217,
           "Position": {
index d650d37..1ad87f2 100644 (file)
             128
           ]
         },
+        {
+          "TerritoryId": 129,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Limsa Lominsa"
+        },
         {
           "DataId": 1001217,
           "Position": {
index 89a62ac..56eec4f 100644 (file)
             "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)"
           ]
         },
+        {
+          "TerritoryId": 134,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Middle La Noscea - Summerford Farms"
+        },
         {
           "DataId": 1002626,
           "Position": {
index 204918e..48ea52b 100644 (file)
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Gridania"
         },
+        {
+          "TerritoryId": 132,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Gridania"
+        },
         {
           "TerritoryId": 132,
           "InteractionType": "AttuneAethernetShard",
index eb355ac..66d42ea 100644 (file)
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Ul'dah"
         },
+        {
+          "TerritoryId": 130,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Ul'dah"
+        },
         {
           "TerritoryId": 130,
           "InteractionType": "AttuneAethernetShard",
index 3dcf41e..a358cbd 100644 (file)
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Central Thanalan - Black Brush Station"
         },
+        {
+          "TerritoryId": 141,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Central Thanalan - Black Brush Station"
+        },
         {
           "DataId": 1001447,
           "Position": {
index b4ed9f1..45dce15 100644 (file)
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Limsa Lominsa"
         },
+        {
+          "TerritoryId": 129,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Limsa Lominsa"
+        },
         {
           "TerritoryId": 129,
           "InteractionType": "AttuneAethernetShard",
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Gridania"
         },
+        {
+          "TerritoryId": 132,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Gridania"
+        },
         {
           "TerritoryId": 132,
           "InteractionType": "AttuneAethernetShard",
index a17e28e..5b50300 100644 (file)
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "TerritoryId": 130,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Ul'dah"
+        },
         {
           "DataId": 1001353,
           "Position": {
index cd0ddea..64ee24d 100644 (file)
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "TerritoryId": 130,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Ul'dah"
+        },
         {
           "DataId": 1001353,
           "Position": {
index 07690a8..09a3d93 100644 (file)
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "TerritoryId": 130,
+          "InteractionType": "RegisterFreeOrFavoredAetheryte",
+          "Aetheryte": "Ul'dah"
+        },
         {
           "DataId": 1001353,
           "Position": {
index 3df1453..e7a0550 100644 (file)
             "WalkTo",
             "AttuneAethernetShard",
             "AttuneAetheryte",
+            "RegisterFreeOrFavoredAetheryte",
             "AttuneAetherCurrent",
             "Combat",
             "UseItem",
           "if": {
             "properties": {
               "InteractionType": {
-                "const": "AttuneAetheryte"
+                "anyOf": [
+                  {
+                    "const": "AttuneAetheryte"
+                  },
+                  {
+                    "const": "RegisterFreeOrFavoredAetheryte"
+                  }
+                ]
               }
             }
           },
index d5ebaaa..6187300 100644 (file)
@@ -12,6 +12,7 @@ public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>
         { EInteractionType.WalkTo, "WalkTo" },
         { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },
         { EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
+        { EInteractionType.RegisterFreeOrFavoredAetheryte, "RegisterFreeOrFavoredAetheryte" },
         { EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
         { EInteractionType.Combat, "Combat" },
         { EInteractionType.UseItem, "UseItem" },
index a0514cd..4784f8c 100644 (file)
@@ -11,6 +11,7 @@ public enum EInteractionType
     WalkTo,
     AttuneAethernetShard,
     AttuneAetheryte,
+    RegisterFreeOrFavoredAetheryte,
     AttuneAetherCurrent,
     Combat,
     UseItem,
index 374c825..872a77d 100644 (file)
@@ -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
         };
     }
index 1c4a408..e5bb01f 100644 (file)
@@ -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<EAetheryteLocation> 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<DialogueChoice> 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 (file)
index 0000000..d205854
--- /dev/null
@@ -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<DoRegister> logger) : TaskExecutor<Register>
+    {
+        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;
+    }
+}
index a44bee9..492440d 100644 (file)
@@ -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 [];
index 1347ee7..e50ecef 100644 (file)
@@ -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))
             {
index 65b075d..14f011a 100644 (file)
@@ -18,13 +18,15 @@ internal sealed unsafe class AetheryteFunctions
     private readonly IServiceProvider _serviceProvider;
     private readonly ILogger<AetheryteFunctions> _logger;
     private readonly IDataManager _dataManager;
+    private readonly IClientState _clientState;
 
     public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> 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;
+    }
+}
+
+/// <remarks>
+/// 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.
+/// </remarks>
+public enum AetheryteRegistrationResult
+{
+    NotPossible,
+    SecurityTokenFreeDestinationAvailable,
+    FavoredDestinationAvailable,
 }
index d130b51..6528c54 100644 (file)
@@ -191,6 +191,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection
             .AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
         serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<AetheryteFreeOrFavored.Register, AetheryteFreeOrFavored.Factory,
+                AetheryteFreeOrFavored.DoRegister>();
         serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
         serviceCollection
             .AddTaskFactoryAndExecutor<Duty.OpenDutyFinderTask, Duty.Factory, Duty.OpenDutyFinderExecutor>();
index a40a4f3..e366234 100644 (file)
@@ -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