Add chocobo porter unlocks for early Limsa
authorLiza Carvelli <liza@carvel.li>
Mon, 4 Aug 2025 20:24:42 +0000 (22:24 +0200)
committerLiza Carvelli <liza@carvel.li>
Mon, 4 Aug 2025 20:24:42 +0000 (22:24 +0200)
13 files changed:
QuestPathGenerator/RoslynElements/QuestStepExtensions.cs
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/397_Sky-high.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/402_Thanks a Million.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/406_On to the Drydocks.json
QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/245_It's Probably Pirates.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/Converter/InteractionTypeConverter.cs
Questionable.Model/Questing/EInteractionType.cs
Questionable.Model/Questing/QuestStep.cs
Questionable/Controller/CommandHandler.cs
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/Controller/Steps/Interactions/Interact.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs

index 00a36db..525c4d6 100644 (file)
@@ -100,6 +100,9 @@ internal static class QuestStepExtensions
                             Assignment(nameof(QuestStep.TargetClass), step.TargetClass,
                                     emptyStep.TargetClass)
                                 .AsSyntaxNodeOrToken(),
+                            Assignment(nameof(QuestStep.TaxiStandId), step.TaxiStandId,
+                                    emptyStep.TaxiStandId)
+                                .AsSyntaxNodeOrToken(),
                             Assignment(nameof(QuestStep.EnemySpawnType), step.EnemySpawnType,
                                     emptyStep.EnemySpawnType)
                                 .AsSyntaxNodeOrToken(),
index eb1b292..b5abc78 100644 (file)
     {
       "Sequence": 255,
       "Steps": [
+        {
+          "DataId": 1002721,
+          "Position": {
+            "X": 187.9148,
+            "Y": 98.5214,
+            "Z": -193.19452
+          },
+          "TerritoryId": 134,
+          "InteractionType": "UnlockTaxiStand",
+          "TaxiStandId": 22,
+          "AetheryteShortcut": "Middle La Noscea - Summerford Farms"
+        },
         {
           "DataId": 1003239,
           "Position": {
             "Z": -249.34778
           },
           "TerritoryId": 134,
-          "InteractionType": "CompleteQuest",
-          "AethernetShortcut": [
-            "[Limsa Lominsa] The Aftcastle",
-            "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)"
-          ]
+          "InteractionType": "CompleteQuest"
         }
       ]
     }
index 88a45ef..4fe80ee 100644 (file)
           "InteractionType": "WalkTo",
           "TargetTerritoryId": 138
         },
+        {
+          "DataId": 1002722,
+          "Position": {
+            "X": 667.68884,
+            "Y": 9.882242,
+            "Z": 487.32727
+          },
+          "TerritoryId": 138,
+          "InteractionType": "UnlockTaxiStand",
+          "TaxiStandId": 23
+        },
         {
           "TerritoryId": 138,
           "InteractionType": "AttuneAetheryte",
index e028ed7..86a3c01 100644 (file)
           "InteractionType": "WalkTo",
           "TargetTerritoryId": 135
         },
+        {
+          "DataId": 1002720,
+          "Position": {
+            "X": 49.271362,
+            "Y": 29.315498,
+            "Z": 605.27954
+          },
+          "TerritoryId": 135,
+          "InteractionType": "UnlockTaxiStand",
+          "TaxiStandId": 27
+        },
         {
           "TerritoryId": 135,
           "InteractionType": "AttuneAetheryte",
index 2104a8f..e6a0219 100644 (file)
           "InteractionType": "AttuneAetheryte",
           "Aetheryte": "Western La Noscea - Aleport"
         },
+        {
+          "DataId": 1002723,
+          "Position": {
+            "X": 298.63428,
+            "Y": -25.004364,
+            "Z": 233.14258
+          },
+          "TerritoryId": 138,
+          "InteractionType": "UnlockTaxiStand",
+          "TaxiStandId": 24
+        },
         {
           "DataId": 1017075,
           "Position": {
             "Z": 17.135864
           },
           "TerritoryId": 138,
-          "InteractionType": "Interact"
+          "InteractionType": "Interact",
+          "AetheryteShortcut": "Western La Noscea - Aleport",
+          "SkipConditions": {
+            "AetheryteShortcutIf": {
+              "InSameTerritory": true
+            }
+          }
         }
       ]
     },
index 84d6af8..72f765a 100644 (file)
             "Gather",
             "Snipe",
             "SwitchClass",
+            "UnlockTaxiStand",
             "Instruction",
             "AcceptQuest",
             "CompleteQuest",
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "InteractionType": {
+                "const": "UnlockTaxiStand"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "TaxiStandId": {
+                "type": "number"
+              }
+            },
+            "required": [
+              "TaxiStandId"
+            ]
+          }
+        },
         {
           "if": {
             "properties": {
index 9cfe2e3..d5ebaaa 100644 (file)
@@ -32,6 +32,7 @@ public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>
         { EInteractionType.Gather, "Gather" },
         { EInteractionType.Snipe, "Snipe" },
         { EInteractionType.SwitchClass, "SwitchClass" },
+        { EInteractionType.UnlockTaxiStand, "UnlockTaxiStand" },
         { EInteractionType.Instruction, "Instruction" },
         { EInteractionType.AcceptQuest, "AcceptQuest" },
         { EInteractionType.CompleteQuest, "CompleteQuest" },
index f1270d2..a0514cd 100644 (file)
@@ -31,6 +31,7 @@ public enum EInteractionType
     Gather,
     Snipe,
     SwitchClass,
+    UnlockTaxiStand,
 
     /// <summary>
     /// Needs to be manually continued.
index 26148ce..374c825 100644 (file)
@@ -65,6 +65,7 @@ public sealed class QuestStep
     public EAction? Action { get; set; }
     public EStatus? Status { get; set; }
     public EExtendedClassJob TargetClass { get; set; } = EExtendedClassJob.None;
+    public byte? TaxiStandId { get; set; }
 
     public EEnemySpawnType? EnemySpawnType { get; set; }
     public List<uint> KillEnemyDataIds { get; set; } = [];
index d3878eb..2c208b5 100644 (file)
@@ -1,8 +1,10 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using Dalamud.Game.ClientState.Objects;
 using Dalamud.Game.Command;
 using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using Lumina.Excel.Sheets;
 using Questionable.Functions;
 using Questionable.Model.Questing;
@@ -177,6 +179,24 @@ internal sealed class CommandHandler : IDisposable
                 else
                     _chatGui.PrintError("Could not query unlock links.", MessageTag, TagColor);
                 break;
+
+            case "taxi":
+                unsafe
+                {
+                    List<string> taxiStands = [];
+                    var taxiStandNames = _dataManager.GetExcelSheet<ChocoboTaxiStand>();
+                    var uiState = UIState.Instance();
+                    for (byte i = 0; i < uiState->ChocoboTaxiStandsBitmask.Length * 8; ++ i)
+                    {
+                        if (uiState->IsChocoboTaxiStandUnlocked(i))
+                            taxiStands.Add($"{taxiStandNames.GetRow(i + 0x120000u).PlaceName} ({i})");
+                    }
+
+                    _chatGui.Print("Unlocked taxi stands:", MessageTag, TagColor);
+                    foreach (var taxiStand in taxiStands)
+                        _chatGui.Print($"- {taxiStand}", MessageTag, TagColor);
+                }
+                break;
         }
     }
 
index 67df02b..212c5dc 100644 (file)
@@ -307,12 +307,15 @@ internal sealed class InteractionUiController : IDisposable
         if (currentQuest != null)
         {
             var quest = currentQuest.Quest;
+            bool isTaxiStandUnlock = false;
             if (checkAllSteps)
             {
                 var sequence = quest.FindSequence(currentQuest.Sequence);
                 var choices = sequence?.Steps.SelectMany(x => x.DialogueChoices);
                 if (choices != null)
                     dialogueChoices.AddRange(choices.Select(x => new DialogueChoiceInfo(quest, x)));
+
+                isTaxiStandUnlock = sequence?.Steps.Any(x => x.InteractionType == EInteractionType.UnlockTaxiStand) ?? false;
             }
             else
             {
@@ -339,9 +342,23 @@ internal sealed class InteractionUiController : IDisposable
                             Prompt = null,
                             Answer = step.PurchaseMenu.Key,
                         }));
+
+                    isTaxiStandUnlock = step.InteractionType == EInteractionType.UnlockTaxiStand;
                 }
             }
 
+            if (isTaxiStandUnlock)
+            {
+                _logger.LogInformation("Adding chocobo taxi stand unlock dialogue choices");
+                dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice
+                {
+                    Type = EDialogChoiceType.List,
+                    ExcelSheet = "transport/ChocoboTaxiStand",
+                    Prompt = ExcelRef.FromKey("TEXT_CHOCOBOTAXISTAND_00000_Q1_000_1"),
+                    Answer = ExcelRef.FromKey("TEXT_CHOCOBOTAXISTAND_00000_A1_000_3")
+                }));
+            }
+
             // add all travel dialogue choices
             var targetTerritoryId = FindTargetTerritoryFromQuestStep(currentQuest);
             if (targetTerritoryId != null)
@@ -635,6 +652,12 @@ internal sealed class InteractionUiController : IDisposable
             _logger.LogInformation("SinglePlayerDutyYesNo: probably Single Player Duty");
             return true;
         }
+            else
+            {
+                _logger.LogInformation("SinglePlayerDuty: not enabled");
+                return false;
+            }
+        }
 
         return false;
     }
index 6c19e0b..b6dfc36 100644 (file)
@@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects.Enums;
 using Dalamud.Game.ClientState.Objects.Types;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
 using Questionable.Controller.Utils;
@@ -46,6 +47,11 @@ internal static class Interact
                 if (!automatonIpc.IsAutoSnipeEnabled)
                     yield break;
             }
+            else if (step.InteractionType == EInteractionType.UnlockTaxiStand)
+            {
+                if (step.TaxiStandId == null)
+                    yield break;
+            }
             else if (step.InteractionType != EInteractionType.Interact)
                 yield break;
 
@@ -55,10 +61,16 @@ internal static class Interact
             if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
                 yield return new WaitAtEnd.WaitDelay();
 
-            yield return new Task(step.DataId.Value, quest, step.InteractionType,
-                step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId ||
-                step.SkipConditions is { StepIf.Never: true } || step.InteractionType == EInteractionType.PurchaseItem || step.DataId == 1052475,
-                step.PickUpItemId, step.SkipConditions?.StepIf, step.CompletionQuestVariablesFlags);
+            yield return new Task(
+                DataId: step.DataId.Value,
+                Quest: quest,
+                InteractionType: step.InteractionType,
+                SkipMarkerCheck: step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId ||
+                    step.SkipConditions is { StepIf.Never: true } || step.InteractionType == EInteractionType.PurchaseItem || step.DataId == 1052475,
+                PickUpItemId: step.PickUpItemId,
+                TaxiStandId: step.TaxiStandId,
+                SkipConditions: step.SkipConditions?.StepIf,
+                CompletionQuestVariablesFlags: step.CompletionQuestVariablesFlags);
         }
     }
 
@@ -68,6 +80,7 @@ internal static class Interact
         EInteractionType InteractionType,
         bool SkipMarkerCheck = false,
         uint? PickUpItemId = null,
+        byte? TaxiStandId = null,
         SkipStepConditions? SkipConditions = null,
         List<QuestWorkValue?>? CompletionQuestVariablesFlags = null) : ITask
     {
@@ -159,12 +172,21 @@ internal static class Interact
                     _needsUnmount = false;
             }
 
-            if (Task.PickUpItemId != null)
+            if (Task.PickUpItemId is { } pickUpItemId)
             {
                 unsafe
                 {
                     InventoryManager* inventoryManager = InventoryManager.Instance();
-                    if (inventoryManager->GetInventoryItemCount(Task.PickUpItemId.Value) > 0)
+                    if (inventoryManager->GetInventoryItemCount(pickUpItemId) > 0)
+                        return ETaskResult.TaskComplete;
+                }
+            }
+            else if (Task.TaxiStandId is { } taxiStandId)
+            {
+                unsafe
+                {
+                    UIState* uiState = UIState.Instance();
+                    if (uiState->IsChocoboTaxiStandUnlocked(taxiStandId))
                         return ETaskResult.TaskComplete;
                 }
             }
index ad2500f..f9f47b2 100644 (file)
@@ -31,6 +31,7 @@ internal static class SkipCondition
             if ((skipConditions == null || !skipConditions.HasSkipConditions()) &&
                 !QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags) &&
                 step.RequiredQuestVariables.Count == 0 &&
+                step.TaxiStandId == null &&
                 step.PickUpQuestId == null &&
                 step.NextQuestId == null &&
                 step.RequiredCurrentJob.Count == 0 &&
@@ -118,6 +119,9 @@ internal static class SkipCondition
             if (CheckPickUpTurnInQuestIds(step))
                 return true;
 
+            if (CheckTaxiStandUnlocked(step))
+                return true;
+
             return false;
         }
 
@@ -445,6 +449,19 @@ internal static class SkipCondition
             return false;
         }
 
+        private unsafe bool CheckTaxiStandUnlocked(QuestStep step)
+        {
+            UIState* uiState = UIState.Instance();
+            if (step.TaxiStandId is { } taxiStandId &&
+                uiState->IsChocoboTaxiStandUnlocked(taxiStandId))
+            {
+                logger.LogInformation("Skipping step, as taxi stand {TaxiStandId} is unlocked", taxiStandId);
+                return true;
+            }
+
+            return false;
+        }
+
         public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
 
         public override bool ShouldInterruptOnDamage() => false;