fix(vtank): TryLoadNav refuses any STATE: block instead of skipping it

Slice 1c review D1 / slice 7 item 7: TryLoadNav tolerated (and silently
skipped) STATE:/IF:/DO: rule sections so a route load out of a full
Meta .af would still succeed against whichever NAV: block came first.
That is exactly the failure mode content-sanity elsewhere in this file
already guards against (a misplaced file in the wrong folder silently
"succeeding" against the wrong document) — a file with a STATE: block
is a Meta profile, possibly one with an embedded nav of its own, and
belongs in metas/, not navs/.

TryLoadNav now throws (refuses) the instant it sees a STATE: line,
with a message naming the metas/ folder, instead of walking past it
via the now-deleted SkipState helper.

SynthesizedGetOptFollowAndJumpFixtureRoundTrips previously used a
STATE:+embedded-NAV: fixture to assert TryLoadNav's OLD skip-and-load
behavior; that assertion is now inverted to assert refusal (shown to
fail against the prior MetafSerializer.cs: True vs expected False).
The "flw" nav-node parsing coverage that assertion also carried is
preserved in a new dedicated NAV:-only fixture,
FollowNavNodeParsesAsANavOnlyDocument.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 07:11:33 +02:00
parent e72a64a311
commit f6eebf8c50
2 changed files with 45 additions and 26 deletions

View file

@ -766,15 +766,24 @@ internal static class MetafSerializer
{
Match lead = LeadIn.Match(cursor.Lines[cursor.L]);
if (!lead.Success)
throw cursor.Error("expected 'NAV:' (or a STATE: rule section, ignored for a route load).");
throw cursor.Error("expected 'NAV:' (or a STATE: rule section, refused for a route load).");
if (lead.Groups["type"].Value == "NAV:")
{
ReadNavBlock(cursor, navs, spells);
continue;
}
// A nav route load tolerates (and ignores) STATE:/IF:/DO:
// sections, e.g. loading a route out of a full meta .af.
SkipState(cursor);
// Slice 1c review D1: a file containing a STATE: rule section
// is a Meta profile, possibly one with one or more embedded
// NAV: blocks of its own. TryLoadNav previously skipped every
// STATE:/IF:/DO: rule and silently loaded whichever embedded
// NAV: block happened to come first — a route load that
// "succeeds" against the wrong document instead of refusing
// it outright. Refuse the whole file the same way the
// "no NAV: block found" case below already does.
throw cursor.Error(
"found a STATE: rule section — this is a Meta profile "
+ "(possibly with an embedded nav), not a stand-alone "
+ "route; load it from the metas/ folder instead.");
}
if (navs.Count == 0)
throw cursor.Error(
@ -803,26 +812,6 @@ internal static class MetafSerializer
return string.Join("\r\n", lines) + "\r\n";
}
private static void SkipState(Cursor cursor)
{
Match header = LeadIn.Match(cursor.Lines[cursor.L]);
_ = header;
cursor.L++;
cursor.SkipBlank();
while (cursor.L < cursor.Lines.Length)
{
Match lead = LeadIn.Match(cursor.Lines[cursor.L]);
if (!lead.Success)
throw cursor.Error("expected 'STATE:', 'IF:', or 'NAV:'.");
if (lead.Groups["type"].Value is "STATE:" or "NAV:")
return;
cursor.C = 0;
_ = ReadTopLevelCondition(cursor);
_ = ReadTopLevelAction(cursor);
cursor.SkipBlank();
}
}
private static void ReadNavBlock(
Cursor cursor,
Dictionary<string, (string NavType, List<RouteWaypoint> Nodes, uint FollowTargetId, string FollowTargetName)> navs,