128
]
},
+ {
+ "TerritoryId": 132,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Gridania"
+ },
{
"TerritoryId": 132,
"InteractionType": "AttuneAethernetShard",
128
]
},
+ {
+ "TerritoryId": 132,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Gridania"
+ },
{
"TerritoryId": 132,
"InteractionType": "AttuneAethernetShard",
"[Gridania] Blue Badger Gate (Central Shroud)"
]
},
+ {
+ "TerritoryId": 148,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Central Shroud - Bentbranch Meadows"
+ },
{
"DataId": 1002980,
"Position": {
"InteractionType": "AttuneAetheryte",
"Aetheryte": "Limsa Lominsa"
},
+ {
+ "TerritoryId": 129,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Limsa Lominsa"
+ },
{
"TerritoryId": 129,
"InteractionType": "AttuneAethernetShard",
128
]
},
+ {
+ "TerritoryId": 132,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Gridania"
+ },
{
"TerritoryId": 132,
"InteractionType": "AttuneAethernetShard",
128
]
},
+ {
+ "TerritoryId": 129,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Limsa Lominsa"
+ },
{
"DataId": 1001217,
"Position": {
128
]
},
+ {
+ "TerritoryId": 129,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Limsa Lominsa"
+ },
{
"DataId": 1001217,
"Position": {
"[Limsa Lominsa] Zephyr Gate (Middle La Noscea)"
]
},
+ {
+ "TerritoryId": 134,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Middle La Noscea - Summerford Farms"
+ },
{
"DataId": 1002626,
"Position": {
"InteractionType": "AttuneAetheryte",
"Aetheryte": "Gridania"
},
+ {
+ "TerritoryId": 132,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Gridania"
+ },
{
"TerritoryId": 132,
"InteractionType": "AttuneAethernetShard",
"InteractionType": "AttuneAetheryte",
"Aetheryte": "Ul'dah"
},
+ {
+ "TerritoryId": 130,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Ul'dah"
+ },
{
"TerritoryId": 130,
"InteractionType": "AttuneAethernetShard",
"InteractionType": "AttuneAetheryte",
"Aetheryte": "Central Thanalan - Black Brush Station"
},
+ {
+ "TerritoryId": 141,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Central Thanalan - Black Brush Station"
+ },
{
"DataId": 1001447,
"Position": {
"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",
{
"Sequence": 255,
"Steps": [
+ {
+ "TerritoryId": 130,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Ul'dah"
+ },
{
"DataId": 1001353,
"Position": {
{
"Sequence": 255,
"Steps": [
+ {
+ "TerritoryId": 130,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Ul'dah"
+ },
{
"DataId": 1001353,
"Position": {
{
"Sequence": 255,
"Steps": [
+ {
+ "TerritoryId": 130,
+ "InteractionType": "RegisterFreeOrFavoredAetheryte",
+ "Aetheryte": "Ul'dah"
+ },
{
"DataId": 1001353,
"Position": {
"WalkTo",
"AttuneAethernetShard",
"AttuneAetheryte",
+ "RegisterFreeOrFavoredAetheryte",
"AttuneAetherCurrent",
"Combat",
"UseItem",
"if": {
"properties": {
"InteractionType": {
- "const": "AttuneAetheryte"
+ "anyOf": [
+ {
+ "const": "AttuneAetheryte"
+ },
+ {
+ "const": "RegisterFreeOrFavoredAetheryte"
+ }
+ ]
}
}
},
{ EInteractionType.WalkTo, "WalkTo" },
{ EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },
{ EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
+ { EInteractionType.RegisterFreeOrFavoredAetheryte, "RegisterFreeOrFavoredAetheryte" },
{ EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
{ EInteractionType.Combat, "Combat" },
{ EInteractionType.UseItem, "UseItem" },
WalkTo,
AttuneAethernetShard,
AttuneAetheryte,
+ RegisterFreeOrFavoredAetheryte,
AttuneAetherCurrent,
Combat,
UseItem,
return InteractionType switch
{
EInteractionType.WalkTo => 0.25f,
- EInteractionType.AttuneAetheryte => 10f,
+ EInteractionType.AttuneAetheryte or EInteractionType.RegisterFreeOrFavoredAetheryte => 10f,
_ => DefaultStopDistance
};
}
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;
{
var quest = currentQuest.Quest;
bool isTaxiStandUnlock = false;
+ List<EAetheryteLocation> freeOrFavoredAetheryteRegistrations = [];
if (checkAllSteps)
{
var sequence = quest.FindSequence(currentQuest.Sequence);
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
{
Answer = step.PurchaseMenu.Key,
}));
+ if (step is { InteractionType: EInteractionType.RegisterFreeOrFavoredAetheryte, Aetheryte: {} aetheryte })
+ freeOrFavoredAetheryteRegistrations = [aetheryte];
+
isTaxiStandUnlock = step.InteractionType == EInteractionType.UnlockTaxiStand;
}
}
}));
}
+ 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)
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)
{
--- /dev/null
+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;
+ }
+}
{
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 [];
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
-using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
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))
{
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;
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,
}
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>();
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