diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs index 61fcdf76..32fc9133 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs @@ -287,7 +287,7 @@ internal sealed partial class MossTankPanel private string _routeNotice = "Add the current position or a selected object."; private string _routeChatDraft = "/ls"; private int _routePauseSeconds = 5; - private RouteRecallKind _routeRecallKind = RouteRecallKind.PrimaryPortal; + private RouteRecallKind _routeRecallKind = RouteRecallKind.PrimaryPortalRecall; private RouteInsertMode _routeInsertMode = RouteInsertMode.AddToEnd; private IReadOnlyList _metaRows = Array.Empty(); // Fix round B item 12: the Meta grid's 6 columns, materialized once per @@ -2376,13 +2376,22 @@ internal sealed partial class MossTankPanel private void AddRouteRecallCore() { + // Round D item 3: "Add Recall" writes the spell id AND name onto + // the waypoint (RecallLabel and metaf's "rcl" export both prefer + // RecallSpellName over the bare enum), the same shape a metaf + // import or the binary .nav loader already produces — so a + // waypoint added right here from the combo executes identically + // to one round-tripped through a real route file. + string name = RouteWaypoint.RecallDisplayName(_routeRecallKind); AddRouteWaypoint(new RouteWaypoint { Type = RouteWaypointType.Recall, Recall = _routeRecallKind, + RecallSpellId = RouteWaypoint.SpellIdForRecall(_routeRecallKind), + RecallSpellName = name, Position = _host.Automation.Navigation.Snapshot.Position, }); - _routeNotice = $"Added {RouteWaypoint.RecallDisplayName(_routeRecallKind)}."; + _routeNotice = $"Added {name}."; } private void AddRoutePauseCore() diff --git a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs index d07eda50..c741ce04 100644 --- a/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs +++ b/src/AcDream.Plugins.MossTank/MossTankRouteProfileStore.cs @@ -600,7 +600,13 @@ internal sealed class MossTankRouteProfileStore LegacyReferenceValid = LegacyReferenceValid, Text = Text ?? string.Empty, DurationMilliseconds = Math.Clamp(DurationMilliseconds, 0, 3_600_000), - Recall = Enum.IsDefined(Recall) ? Recall : RouteRecallKind.Lifestone, + // Round D item 3 removed RouteRecallKind.Lifestone (the old + // /lifestone-slash-command member, superseded by the real + // LifestoneRecall spell) — a pre-cutover un-migrated JSON + // profile carrying that ordinal now falls back to + // PrimaryPortalRecall instead, the same "still a valid, + // harmless recall kind" sentinel role Lifestone played here. + Recall = Enum.IsDefined(Recall) ? Recall : RouteRecallKind.PrimaryPortalRecall, RecallSpellId = RecallSpellId, RecallSpellName = RecallSpellName ?? string.Empty, JumpHeadingDegrees = float.IsFinite(JumpHeadingDegrees) ? JumpHeadingDegrees : 0f, diff --git a/src/AcDream.Plugins.MossTank/Navigation.cs b/src/AcDream.Plugins.MossTank/Navigation.cs index 66361941..c3cec141 100644 --- a/src/AcDream.Plugins.MossTank/Navigation.cs +++ b/src/AcDream.Plugins.MossTank/Navigation.cs @@ -25,12 +25,65 @@ internal enum RouteWaypointType Jump = 9, } +/// +/// VTank's own cmbRecallType combo (Route tab, docs/research/ +/// vtank-kb/08-ui-views.md §1) — 26 real castable recall spells (metaf's +/// own NRecall table, metaf_monolithic.py:10981-11008 in +/// `C:\Users\erikn\source\repos\metas`) plus VTank's own +/// Marketplace Recall, which has no spell and is issued as the +/// /marketplace slash command instead — appended last since it is +/// not one of the 26 named spells. Declared in VTank's own combo order so +/// Enum.GetNames/GetValues (which sort by underlying VALUE, +/// not declaration order) reproduce that order directly: every member's +/// value is a plain sequential index (0..26), never a spell id — spell +/// ids live in the separate +/// lookup so this enum's own values stay index-shaped and sortable. +/// +/// Round D item 3 (owner: "Under Route, Add recall is missing a lot of +/// recalls that VTank has and they do not work in routes yet.") replaces +/// the old 4-member enum (Lifestone, Marketplace, PrimaryPortal, +/// SecondaryPortal). Deviation: the old Lifestone member (which +/// issued the /lifestone slash command) is DROPPED rather than kept +/// alongside the new spell-based — VTank's +/// real combo has ONE "Lifestone Recall" entry and it is a real castable +/// spell (id 1635, confirmed by metaf's own NRecall table), so keeping +/// both would show two menu rows reading "Lifestone Recall" with +/// different behavior. is the one member kept +/// as a slash command, per the explicit "keep ours" instruction (VTank's +/// own Marketplace Recall has no spell to cast either). PrimaryPortal and +/// SecondaryPortal are renamed to and +/// and now use their real spell ids +/// (48, 2647) instead of the old runtime KnownSelfBuffs name lookup. +/// internal enum RouteRecallKind { - Lifestone, + PrimaryPortalRecall, + SecondaryPortalRecall, + LifestoneRecall, + LifestoneSending, + PortalRecall, + RecallAphusLassel, + RecallTheSanctuary, + RecallToTheSingularityCaul, + GlendenWoodRecall, + AerlintheRecall, + MountLetheRecall, + UlgrimsRecall, + BurRecall, + ParadoxTouchedOlthoiInfestedAreaRecall, + CallOfTheMhoireForge, + ColosseumRecall, + FacilityHubRecall, + GearKnightInvasionAreaCampRecall, + LostCityOfNeftetRecall, + ReturnToTheKeep, + RynthidRecall, + ViridianRiseRecall, + ViridianRiseGreatTreeRecall, + CelestialHandStrongholdRecall, + RadiantBloodStrongholdRecall, + EldrytchWebStrongholdRecall, Marketplace, - PrimaryPortal, - SecondaryPortal, } internal enum RouteJumpDirection @@ -152,15 +205,80 @@ internal sealed class RouteWaypoint + ")"; } + /// VTank's own cmbRecallType label text (docs/research/vtank-kb/08-ui-views.md §1) for each . internal static string RecallDisplayName(RouteRecallKind value) => value switch { - RouteRecallKind.Lifestone => "Lifestone Recall", + RouteRecallKind.PrimaryPortalRecall => "Primary Portal Recall", + RouteRecallKind.SecondaryPortalRecall => "Secondary Portal Recall", + RouteRecallKind.LifestoneRecall => "Lifestone Recall", + RouteRecallKind.LifestoneSending => "Lifestone Sending", + RouteRecallKind.PortalRecall => "Portal Recall", + RouteRecallKind.RecallAphusLassel => "Recall Aphus Lassel", + RouteRecallKind.RecallTheSanctuary => "Recall the Sanctuary", + RouteRecallKind.RecallToTheSingularityCaul => "Recall to the Singularity Caul", + RouteRecallKind.GlendenWoodRecall => "Glenden Wood Recall", + RouteRecallKind.AerlintheRecall => "Aerlinthe Recall", + RouteRecallKind.MountLetheRecall => "Mount Lethe Recall", + RouteRecallKind.UlgrimsRecall => "Ulgrim's Recall", + RouteRecallKind.BurRecall => "Bur Recall", + RouteRecallKind.ParadoxTouchedOlthoiInfestedAreaRecall => + "Paradox-touched Olthoi Infested Area Recall", + RouteRecallKind.CallOfTheMhoireForge => "Call of the Mhoire Forge", + RouteRecallKind.ColosseumRecall => "Colosseum Recall", + RouteRecallKind.FacilityHubRecall => "Facility Hub Recall", + RouteRecallKind.GearKnightInvasionAreaCampRecall => + "Gear Knight Invasion Area Camp Recall", + RouteRecallKind.LostCityOfNeftetRecall => "Lost City of Neftet Recall", + RouteRecallKind.ReturnToTheKeep => "Return to the Keep", + RouteRecallKind.RynthidRecall => "Rynthid Recall", + RouteRecallKind.ViridianRiseRecall => "Viridian Rise Recall", + RouteRecallKind.ViridianRiseGreatTreeRecall => "Viridian Rise Great Tree Recall", + RouteRecallKind.CelestialHandStrongholdRecall => "Celestial Hand Stronghold Recall", + RouteRecallKind.RadiantBloodStrongholdRecall => "Radiant Blood Stronghold Recall", + RouteRecallKind.EldrytchWebStrongholdRecall => "Eldrytch Web Stronghold Recall", RouteRecallKind.Marketplace => "Marketplace Recall", - RouteRecallKind.PrimaryPortal => "Primary Portal Recall", - RouteRecallKind.SecondaryPortal => "Secondary Portal Recall", _ => value.ToString(), }; + /// + /// The real retail spell id VTank casts for each recall (metaf's own + /// NRecall table). has + /// no spell — it returns 0, the same "no spell, use the slash command" + /// sentinel and + /// NavigationController.SubmitRecall already treat as + /// "unset/none" everywhere else in this file. + /// + internal static uint SpellIdForRecall(RouteRecallKind value) => value switch + { + RouteRecallKind.PrimaryPortalRecall => 48u, + RouteRecallKind.SecondaryPortalRecall => 2647u, + RouteRecallKind.LifestoneRecall => 1635u, + RouteRecallKind.LifestoneSending => 1636u, + RouteRecallKind.PortalRecall => 2645u, + RouteRecallKind.RecallAphusLassel => 2931u, + RouteRecallKind.RecallTheSanctuary => 2023u, + RouteRecallKind.RecallToTheSingularityCaul => 2943u, + RouteRecallKind.GlendenWoodRecall => 3865u, + RouteRecallKind.AerlintheRecall => 2041u, + RouteRecallKind.MountLetheRecall => 2813u, + RouteRecallKind.UlgrimsRecall => 2941u, + RouteRecallKind.BurRecall => 4084u, + RouteRecallKind.ParadoxTouchedOlthoiInfestedAreaRecall => 4198u, + RouteRecallKind.CallOfTheMhoireForge => 4128u, + RouteRecallKind.ColosseumRecall => 4213u, + RouteRecallKind.FacilityHubRecall => 5175u, + RouteRecallKind.GearKnightInvasionAreaCampRecall => 5330u, + RouteRecallKind.LostCityOfNeftetRecall => 5541u, + RouteRecallKind.ReturnToTheKeep => 4214u, + RouteRecallKind.RynthidRecall => 6150u, + RouteRecallKind.ViridianRiseRecall => 6321u, + RouteRecallKind.ViridianRiseGreatTreeRecall => 6322u, + RouteRecallKind.CelestialHandStrongholdRecall => 6325u, + RouteRecallKind.RadiantBloodStrongholdRecall => 6327u, + RouteRecallKind.EldrytchWebStrongholdRecall => 6326u, + _ => 0u, + }; + private static string JumpDirectionDisplayName(RouteJumpDirection value) => value switch { @@ -927,30 +1045,30 @@ internal sealed class NavigationController return true; } + /// + /// Round D item 3: every recall except + /// now has a real spell id (either recorded directly on the waypoint — + /// AddRouteRecallCore populates it for anything added through the Route + /// tab's own combo, and the metaf/binary-.nav loaders populate it from + /// the file itself) so the common path is just "cast that spell," + /// exactly like every other retail spell-cast action in this plugin. + /// The two fallbacks only matter for a waypoint saved before this round + /// with no recorded id: Marketplace still has none to record (VTank's + /// own combo issues it as a slash command too), and any other kind + /// resolves its id from the SAME table AddRouteRecallCore uses, rather + /// than the old runtime KnownSelfBuffs name lookup (which depended on + /// the character already knowing the spell under that exact name). + /// private bool SubmitRecall(RouteWaypoint waypoint) { if (waypoint.RecallSpellId != 0u) return _host.Automation.Magic.Cast(waypoint.RecallSpellId); - RouteRecallKind recall = waypoint.Recall; - string? command = recall switch - { - RouteRecallKind.Lifestone => "/lifestone", - RouteRecallKind.Marketplace => "/marketplace", - _ => null, - }; - if (command is not null) - return _host.Automation.Chat.Submit(command); + if (waypoint.Recall == RouteRecallKind.Marketplace) + return _host.Automation.Chat.Submit("/marketplace"); - string needle = recall == RouteRecallKind.PrimaryPortal - ? "Primary Portal Recall" - : "Secondary Portal Recall"; - PluginSpellInfo? spell = _host.Automation.Spells.KnownSelfBuffs - .FirstOrDefault(value => value.Name.Equals( - needle, - StringComparison.OrdinalIgnoreCase)); - return spell is { SpellId: not 0u } found - && _host.Automation.Magic.Cast(found.SpellId); + uint spellId = RouteWaypoint.SpellIdForRecall(waypoint.Recall); + return spellId != 0u && _host.Automation.Magic.Cast(spellId); } private bool TickJump( diff --git a/src/AcDream.Plugins.MossTank/mosstank.xml b/src/AcDream.Plugins.MossTank/mosstank.xml index 81702a9a..eb011e18 100644 --- a/src/AcDream.Plugins.MossTank/mosstank.xml +++ b/src/AcDream.Plugins.MossTank/mosstank.xml @@ -619,16 +619,16 @@