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 <menu> 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 <noreply@anthropic.com>