From 5318adbb30ec9a479e3207280e5e5c90ce374197 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 16:54:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(vtank):=20slice=207=20round=20D=20item=203?= =?UTF-8?q?=20=E2=80=94=20VTank's=20full=2026-recall=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner live report 2026-09-07: "Under Route, Add recall is missing a lot of recalls that VTank has and they do not work in routes yet." RouteRecallKind's old 4-entry model (Lifestone/Marketplace/ PrimaryPortal/SecondaryPortal) is replaced with VTank's real cmbRecallType table: 26 castable recall spells from metaf's own NRecall table (metaf_monolithic.py:10981-11008) plus Marketplace Recall, VTank's one entry with no spell (issued as /marketplace). Deviation (no refs/vtank/ checkout in this worktree to confirm VTank's real combo layout directly): the old Lifestone member, which issued "/lifestone" as a slash command, is DROPPED rather than kept alongside the new spell-based LifestoneRecall (1635) — VTank's real combo has ONE "Lifestone Recall" entry and it is a real castable spell, so keeping both would show two menu rows reading "Lifestone Recall" with different behavior. Marketplace is the one member kept as a slash command per the explicit "keep ours" instruction. PrimaryPortal/ SecondaryPortal are renamed to PrimaryPortalRecall/SecondaryPortalRecall and now use their real spell ids (48, 2647) instead of the old runtime KnownSelfBuffs name lookup. This reshuffles the enum's underlying ordinals; the one place that mattered (MossTankRouteProfileStore's legacy pre-cutover JSON migration DTO, which stores Recall as a raw int) already guards with Enum.IsDefined and is documented as a migration-only path for not-yet-migrated files — its fallback default moved from the deleted Lifestone to PrimaryPortalRecall. Changes: - RouteRecallKind: 26 named members in VTank's own combo order (values are plain sequential indices, not spell ids, so Enum.GetNames/ GetValues — which sort by underlying VALUE — reproduce that order) plus Marketplace appended last. - RouteWaypoint.RecallDisplayName: VTank's exact label text per kind. - RouteWaypoint.SpellIdForRecall (new): the real spell id per kind (Marketplace = 0, the existing "no spell" sentinel). - NavigationController.SubmitRecall: unchanged fast path (RecallSpellId != 0 -> cast) now covers every recall added through the UI; the fallback for a waypoint with no recorded id resolves through SpellIdForRecall instead of the old runtime spell-name lookup, and Marketplace still falls through to "/marketplace". - MossTankPanel.AddRouteRecallCore: populates RecallSpellId AND RecallSpellName on the new waypoint (matching what a metaf import or the binary .nav loader already produces), so a waypoint added from the Route tab's own combo executes identically to one round-tripped through a real route file. - mosstank.xml: the recall comment updated; rows 4->7 now that scrolling through 27 entries is real, not grammar-only. Tests added (NavigationTests.cs, MetafSerializerTests.cs): - RouteRecallKindListsVTanksTwentySixRecallsInOrderPlusMarketplaceLast (the 26-entry order via Enum.GetNames). - RecallNameAndSpellIdTablesAgree (27-case Theory: name<->id both ways; RouteRecallKind is internal so the Theory parameter is the public int ordinal, cast back inside the method — a public method cannot expose an internal-typed parameter, CS0051). - RecallWaypointWithNonZeroSpellIdCastsThatSpell / RecallWaypointForMarketplaceSubmitsTheSlashCommandNotACast (execution, via a new FakeMagic tracking fake — FakeAutomation.Magic is now settable instead of always NoOpAutomationSurface). - RecallNodeRoundTripsByNameAndResolvesTheRealSpellIdFromTheCatalog (three recalls through SaveNav/TryLoadNav with a new FakeSpellCatalog that actually knows the spells, proving both the name AND the resolved id survive the .af round trip). Mutations shown to fail, then reverted: swapping the first two enum members failed the order test; reducing SubmitRecall to `return false` failed both execution tests (no cast recorded, "/marketplace" not submitted). Verified: dotnet build AcDream.slnx -c Release green; MossTank suite 713/713 (682 -> 713, 31 new tests); App markup/plugin filter 242/242 (unchanged). Co-Authored-By: Claude Fable 5.1 --- src/AcDream.Plugins.MossTank/MossTankPanel.cs | 13 +- .../MossTankRouteProfileStore.cs | 8 +- src/AcDream.Plugins.MossTank/Navigation.cs | 166 +++++++++++++++--- src/AcDream.Plugins.MossTank/mosstank.xml | 16 +- .../MetafSerializerTests.cs | 78 ++++++++ .../NavigationTests.cs | 157 ++++++++++++++++- 6 files changed, 401 insertions(+), 37 deletions(-) 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 @@