Skip to content

Plan: run the in-engine suites on CI without the extraction - #102

Merged
Laeresh merged 82 commits into
mainfrom
claude/engine-test-assets-vgbml9
Oct 8, 2026
Merged

Laeresh merged 82 commits into
mainfrom
claude/engine-test-assets-vgbml9

Conversation

@Laeresh

@Laeresh Laeresh commented Oct 1, 2026

Copy link
Copy Markdown
Owner

docs/PLAN-ci-engine-suites.md lays out three waves: a ci tier that
fails on a skip and a headless Linux CI job for the suites that already
read nothing from the install (Wave A), a synthetic data root with an
invented stand-in plane, weapons and generated texture and sound
archives (Wave B), and arena and Original-shell fixtures for the
suites that need a few authored records (Wave C).

The plan rests on analysis/ci-suite-survey/, a per-suite classification
of all 495 suites: 56 read nothing from the install, 3 use a chapter
only as ground, 83 need only a plane, 87 need a few inventable records,
and 266 check the shipped data and stay with the local battery. The
rows are inferred from each suite's gates, helpers and assertions, not
from a run.

PROJECT_CONTEXT.md's "Current status" names the plan.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK

claude and others added 30 commits October 1, 2026 15:12
docs/PLAN-ci-engine-suites.md lays out three waves: a `ci` tier that
fails on a skip and a headless Linux CI job for the suites that already
read nothing from the install (Wave A), a synthetic data root with an
invented stand-in plane, weapons and generated texture and sound
archives (Wave B), and arena and Original-shell fixtures for the
suites that need a few authored records (Wave C).

The plan rests on analysis/ci-suite-survey/, a per-suite classification
of all 495 suites: 56 read nothing from the install, 3 use a chapter
only as ground, 83 need only a plane, 87 need a few inventable records,
and 266 check the shipped data and stay with the local battery. The
rows are inferred from each suite's gates, helpers and assertions, not
from a run.

PROJECT_CONTEXT.md's "Current status" names the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
CheckCommentCaps.ps1 and CheckDocEntries.ps1 excluded build output with
a backslash-only path regex, so on a non-Windows tree that had been
built they scanned CSVM.Tests/obj and blocked every commit on the
generated AssemblyInfo.cs. CI never saw it because the checks run there
before the build. The pattern now takes either separator.

Measured on Ubuntu 24.04 with pwsh 7.6 after a dotnet build:
CheckCommitContent.ps1 exited 2 on CSVM.Tests/obj before the change and
0 after; CheckCommitContent.ps1 -SelfTest 70 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…run without one

Nine suites loaded the zrdr archive with no RequireData and threw a
DirectoryNotFoundException when nothing was extracted: campaign-danger-zones,
campaign-mission-cash, campaign-mission-end, campaign-objectives-hud,
campaign-persistence, hangar-door-wake, mission-radio, music-states and
persist-chain-kill. Each now requires every install input it reads before the
first read (the zrdr archive, plus the sound archive for music-states, the
messages file for campaign-objectives-hud and the mission zrdr where a suite
reads it before building a world), so an empty data root ends them as SKIP.

Eight menu suites carried a zrdr gate their body never needs, and the gate is
gone: menu-coop-door, menu-host-pointer, menu-host-tracer, menu-host-address,
menu-net-door, menu-controls-seats, menu-player-setup-journey and
menu-zone-layout. The built-in launchscreen degrades without the install
(plane stats read "n/a", the instant action defs are InstantAction.Defaults(),
stock loadouts come from res://, the langui table is UiStrings.Empty), and none
of the eight asserts on a value that differs between the two, so each tests the
same thing on the real install.

Two suites keep their gate on purpose. menu-hangar-journey asserts that the
paint screen composes a preview and that the decal row shows its tile, both of
which read the extraction. menu-instant-action-journey asserts that the ace is
the environment's own and not the built-in default. Each is a split candidate:
the rest of the journey reads nothing from the install. render-thread-handoffs
has no data gate; it skips headless for want of a rendering device.

Verified on Linux with an empty data root: the full catalog went from 43 PASS,
11 FAIL, 441 SKIP to 51 PASS, 2 FAIL, 442 SKIP. The two FAILs are the
container's own limits and not this change: build-stamp-focus (a folder opened
through the OS reads "nothing opened" headless) and enet-dual-stack (no IPv6
loopback). The 20 suites named above ran as PASS or SKIP with no FAIL. Build has
no new warnings, dotnet format --verify-no-changes and the StyleCop rebuild are
clean, and the unit run is 5477 passed, 305 skipped, 1 failed
(StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname,
because this container has a system SDL2; the test does not touch this change).
menu-player-setup-journey prints 21 text-server error lines headless (a zero
font size); they are already on HeadlessEngineErrors in sandbox/LinuxRelease.ps1
and the CI job needs the same allowance.

Honest limit: there is no extraction in the container, so the real-install
battery was not run. It should be unchanged: the nine guarded suites and the
eight ungated suites PASS as before, menu-hangar-journey and
menu-instant-action-journey still PASS, and the full run keeps its counts.
menu-zone-layout and the other ungated suites run without the decal tile art on
CI, so the tile leg of the paint screen is covered only by the real-install run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
The ci tier is SuiteCatalog.CiTier, 43 suites, one name per line and sorted
so later items can add theirs. SuiteCatalog.Tier now returns a SuiteTier
(the names plus SkipFails) instead of a bare list, and only the ci tier sets
SkipFails. Under a selector naming such a tier, a listed suite that SKIPs is
reported FAIL: the row keeps the skip reason as its detail, adds a "!!" line
"skipped, and tier:ci counts a skip as a failure: <reason>", logs an
ERROR [test] line naming the suite, and the process exits 1. Every other
selector keeps today's SKIP semantics, including suite:<name> on a listed
suite and a union such as "suite:x,tier:ci" for x off the list, so the local
battery is unchanged.

Where the rule lives: a property the tier carries in SuiteCatalog, read by
the harness through the pure TestHarness.SkipFailures(spec). The tier is
what knows its members are required to run, so the rule sits with the
membership and needs no new flag. Reading it in the harness rather than in
a CI script over test-report.json keeps one verdict: the console table, the
report row and the exit code all say FAIL, so A3's job needs only the exit
code, and a local `--run-tests=tier:ci` reproduces CI exactly. The skip is
turned into a failure after the orphan check, so a skipped body stays exempt
from the leak count as before.

Membership is every suite that passes headless with an empty data root on
this tree: the full catalog here gave 43 PASS, 11 FAIL, 441 SKIP, and all 43
passes are on the list. None is on sandbox/LinuxRelease.ps1's $HeadlessOnly,
and enet-dual-stack, enet-stable-ipv6-reply and build-stamp-focus are out
(IPv6 loopback, stable global IPv6, an OS shell). enet-transport,
enet-load-stall and lan-discovery bind 127.0.0.1 only and passed three
consecutive runs (3/0/0, exit 0, engine errors clean each time), so they
stay. The selection rule is in docs/tooling.md beside the quick tier's;
docs/cli.md's --run-tests bullet and the SuiteCatalog/TestHarness
architecture entries say what the tier does.

Verified on Linux (Godot 4.7 .NET headless, CSVM_DATA_ROOT an empty folder):
tier:ci 43 passed, 0 failed, 0 skipped, exit 0, four runs. Shown able to
fail with a temporary ctx.RequireData("/nonexistent", $"probe") at the top
of target-ref: tier:ci went 42/1/0 and exited 1, naming target-ref with the
skip reason; the full catalog with the same probe reported target-ref SKIP
(42/11/442, the 11 being the baseline's failures), and both
--run-tests=target-ref and suite:target-ref reported it SKIP and exited 0.
The probe was reverted. tier:quick gave the same statuses as the baseline
full run for its 13 suites (target-selection PASS, music-states FAIL on its
unguarded zrdr load, 11 SKIP). dotnet build: 0 errors, the 2 existing
ShaderTwins warnings. dotnet format --verify-no-changes clean; the -t:Rebuild
StyleCop step reports no SA warnings. dotnet test: 5479 passed, 305 skipped,
1 failed, StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticks...,
which fails because this container has a system SDL2 and is unrelated. New
units pin CiTier as a sorted registered subset without the three
environment suites, SkipFails per tier, and SkipFailures per selector.

The honest limit: no real-install run here. The author's full RunTests.ps1
battery should be unchanged (no suite body changed and no other selector's
semantics moved), and a windowed --run-tests=tier:ci on the real install
should pass all 43.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
menu-coop-door, menu-controls-seats, menu-host-address, menu-host-pointer,
menu-host-tracer, menu-net-door and menu-zone-layout lost a data gate their
body never reads, and pass headless with an empty data root. They join the
ci tier, which now holds 50 suites. menu-player-setup-journey also passes but
prints headless text-server error lines that the harness's engine-error
screen does not yet allow, so it waits for the CI job's headless allowance.

Verified on the merged tree, Linux, Godot 4.7 headless, empty data root:
--run-tests=tier:ci 50 passed, 0 failed, 0 skipped, engine errors clean,
exit 0; the SuiteCatalog unit tests 9 passed. The author's windowed
tier:ci run should pass all 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
… world

Both suites launch hand-built OBJECT_MOTION bodies with no gravity block
from a probe node and read back only that node's local transform, so the
chapter world they built (C1, collision off) was a parent node and an
AnimRuntime and nothing more. They now run on WithMotionHost in
AnimationAndEffectsSuites.cs: a plain parent node and a bare
new AnimRuntime() bound to it over an empty AnimProgram. An AnimRuntime
stands up without a WorldSession through its existing internal
constructor and Bind, as CoopCutsceneSuites and TargetingSuites already
do, so no production code changed. EmptyStage itself is not used: neither
suite is a contact case, so neither needs the grid or its collider.

ground-contact stays install-bound. It reads the chapter's own program,
world.Session.Program.ByAnimName("gunshell") at
AnimationAndEffectsSuites.cs:382, and asserts that the extracted gunshell
event still authors no_altitude (:403), a check on the shipped data. It
also takes world.Runtime.Destructibles.All.First().Def as the motion's
owner (:264, :439), which a hand-built AnimDefinition could replace. A
reverted trial with the gunshell leg removed and a hand-built owner
passed every remaining leg on EmptyStage.Build(collision: true) with an
empty data root, so splitting that leg into its own suite would bring the
rest onto the CI tier; that adds a suite and is left for a decision.

Verified here, on Linux with Godot 4.7 .NET headless and an empty data
root: forward-rotation PASS and launch-direction-cache PASS, run by name.
Each was shown able to fail and then reverted: the +X throw launched at
azimuth 90 failed "a throw along +X pitches forward over it", and the
cruise flown along +X failed both the end-position and the
identical-path checks. The full catalog went from 51 PASS, 2 FAIL,
442 SKIP to 53 PASS, 2 FAIL, 440 SKIP; the two failures are
build-stamp-focus and enet-dual-stack, both environment limits of this
container, and the 25 unexpected engine-error lines are the 4 ENet IPv6
lines and the 21 text-server lines of menu-player-setup-journey already
seen on the run branch. dotnet build: 0 errors, no new warnings.
dotnet format --verify-no-changes clean, the StyleCop rebuild reports no
SA warning. Unit tests: 5477 passed, 305 skipped, 1 failed
(StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname,
which fails because this container has a system SDL2; unrelated).

Not run: the real-install battery. On the author's machine
forward-rotation and launch-direction-cache should PASS with the same
notes (1.000 and 0.333 rad/s; the cruise ends 1600 m along +Z), now
without building or reusing C1, and ground-contact should PASS on C1 as
before. No coverage is lost: neither moved suite used C1's terrain, their
ranged launches draw from min = max ranges so the runtime's seed decides
nothing, and the chapter runtime's contact mask and inherited velocity
never reach a body with no gravity block.

Both suites join the ci tier, which now holds 52. On the merged tree with
an empty data root, --run-tests=tier:ci passed 52, failed 0, skipped 0,
engine errors clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…re or a listed skip

checks.yml gains an engine job on ubuntu-latest beside the unchanged three-OS checks job (only its
header comment changed). It sets up .NET 8, downloads Godot 4.7-stable mono for Linux x86_64 from
the official GitHub release, checks the zip against the release's SHA512-SUMS.txt, and caches the
unpacked build under a key naming the release, so the key pins the version and the checksum is
checked once before a copy is stored. It installs libfontconfig1 only if the runner image lacks it,
builds CSVM.sln, imports once headless, and runs ./RunCiSuites.ps1 -Godot <exe>. The report,
Godot's stdout and stderr, the screened engine log and .scratch/logs/ are uploaded on every
outcome.

RunCiSuites.ps1 is a new root script that a contributor runs the same way on Linux or macOS. It
starts one headless Godot with --run-tests=<selector> (default tier:ci), CSVM_DATA_ROOT at a fresh
empty folder and the XDG folders inside .scratch/ci-suites/, under a watchdog, and draws its
verdict from the harness's report rather than from the exit code. The harness exits 1 on any
engine error line its windowed allowlist does not cover, and a headless process prints lines a
windowed one does not. The run fails on a FAIL (a listed tier:ci skip is already one), a missing
or stale report (selector, data root and port base are checked), a crash exit, an unscreened log,
an allowance over its cap, or an unexpected line no headless pattern matches.

The two headless lists moved out of sandbox/LinuxRelease.ps1 into analysis/headless-limits.json,
which both scripts read, with the old comments as the file's readme. LinuxRelease.ps1 builds the
same ordered $HeadlessOnly and $HeadlessEngineErrors from it; a parse of the old inline tables
against the JSON found keys, values and order identical (11 and 2 entries). RunCiSuites.ps1
applies headlessEngineErrors to the report's unexpected lines exactly as LinuxRelease.ps1 does;
the harness's in-process screen and its ErrorAllowlist are untouched, so no allowance widened. A
unit test keeps every headlessOnly name a registered suite, off CiTier, and every pattern a valid
regex; it went red when menu-screenshot-key was put on the tier and green when it came off.

menu-player-setup-journey joins CiTier (51 suites). Headless with an empty data root it passes
and prints 21 text-server lines, all matching the existing _shaped_text_add_string pattern, so
under the script it runs clean. A bare headless --run-tests=tier:ci now exits 1 on those lines,
which docs/tooling.md states: the script is the gate. No verification-budgets.json line, since
that file budgets RunTests.ps1's stages as measured on the development machine.

Verified on Linux (Ubuntu 24.04 container, Godot 4.7 mono, no install): build 0 errors (the two
existing ShaderTwins nullable warnings), dotnet format clean, the StyleCop rebuild with no SA
warnings, units 5480 passed, 305 skipped, 1 failed (StickRosterTests
ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname, which fails because this container
has a system SDL2; this change does not touch sticks). pwsh was refused in the agent sandbox, so
RunCiSuites.ps1 did not itself run here. Godot was run with the script's arguments and
environment (port base 48000 for the sandbox; XDG_CONFIG_HOME and XDG_CACHE_HOME were left unset
because the sandbox refused them), and a line-for-line port of the script's verdict judged the
reports: tier:ci 51/0/0 three times, Godot exit 1 each time, the 21 headless lines excused, no
other unexpected line, verdict pass, about 26 s wall per run. A broken target-ref input
(TargetRef.Fraction 70.2 to 60.2) gave 50/1/0 and a failing verdict, then was reverted. The
workflow YAML parses.

Still owed: the first real GitHub run on a PR, green and then red on a deliberately broken listed
suite; a run of RunCiSuites.ps1 under pwsh; and the author's Windows battery, which should be
unchanged (menu-player-setup-journey passes as before; no suite body changed), plus one windowed
--run-tests=tier:ci on the real install, which should pass all 51.

Run by the orchestrator with pwsh on the merged tree (A4 landed beside it):
RunCiSuites.ps1 passed 53, failed 0, skipped 0 and exited 0, the 21
text-server lines matched by the existing allowance; with a RequireData on a
missing path planted in target-ref it reported 50 passed, 1 failed and exited
1, then the probe was reverted. CheckCommitContent.ps1 exit 0.
PROJECT_CONTEXT.md's Current status moves to Wave B, next B11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…scratch

A new launcher switch, --synthetic-data, writes an invented extracted/ tree into
.scratch/synthetic-data/<pid>/ and reads it as the data root, so the loaders take their normal
paths with no install. The builder is CSVM/src/Tooling/SyntheticData.cs, engine-free so the unit
tests build the same tree. Its one family so far, SyntheticTextures.cs, writes the C1 texture
archive: the hand-authored CSVM.Tests/fixtures/synthetic/C1/texture/manifest.json (three probe_*
entries) plus one checker PNG per entry, encoded by the existing Extraction/PngWriter. Adding a
family is one SyntheticFamily entry and a writer that touches only its own folder.

Decisions. The switch is the only way in; nothing infers it from a tier or a missing install. With
the switch, the tree replaces whatever root --data-root=, CSVM_DATA_ROOT or the repo root named,
and an install there is not read: the author's checkout always has one at the repo root, so
refusing would make the CI run impossible to reproduce locally. It is logged as WARN [core]
SYNTHETIC DATA naming both roots, the harness adds a WARN [test] line and data=SYNTHETIC on its
summary line, and test-report.json gains "syntheticData" beside "dataRoot" (an added field, so no
schema bump). A data root pointed at a synthetic tree without the switch warns the same way. A tree
that cannot be written quits 1 rather than read the install; beside --extract the switch is
dropped with a note. The tree is stamped through ExtractionStampWriter.Merge with the current
schema and a "synthetic" field: ExtractionStamp.Check reads only schema, so the unknown field is
inert, the boot check is silent and Standing reads Current. Build deletes an existing extracted/
only when it carries that field, so a real extraction is never replaced. The records stay in
CSVM.Tests/fixtures/, read from the checkout; an export has no such folder and refuses the switch.

Verified on Linux, headless, empty data root, port base 50000: the full catalog without the switch
is unchanged at 53 PASS, 2 FAIL (build-stamp-focus, enet-dual-stack), 440 SKIP, syntheticData
false; tier:ci with the switch passes 52/0/0 with the synthetic root in log and report; a temporary
suite (reverted) opened SessionPaths.ChapterTextures(DataRoot, "C1") over the synthetic root and
decoded all three textures with no warning and no miss; a fake install (a schema-3 VERSION.json
alone) without the switch read only that root and wrote no synthetic tree; a missing manifest quit
1 naming it. The full catalog over the synthetic root reads 53/7/435: five suites gate on the C1
textures alone and then need real texture names or an unguarded planes.zip (cockpit-panel-staging,
damage-staging-pool, puffer-fire-glow, puffer-smoke-sun, tex-dropin), which is input for the
plane-only suites item; none is on the ci tier. Build clean, dotnet format clean, StyleCop rebuild
with no SA warnings, units 5486 passed, 305 skipped, 1 failed (StickRosterTests'
no-system-SDL2 case, which this container fails because it has a system libSDL2; untouched here).
Six new unit tests cover the stamp, the archive, the rebuild and both refusals.

Limit: no real-install run. The author's full RunTests.ps1 battery should be unchanged, since no
suite changed and nothing runs without the switch. Optionally, one --run-tests=tier:ci
--synthetic-data on the real checkout should pass the tier and name the repo root's install as
unread.

On the merged tree (Wave A complete): RunCiSuites.ps1 53 passed, 0 failed,
exit 0, unchanged; tier:ci with --synthetic-data 53 passed, 0 failed, 0
skipped, both SYNTHETIC warnings logged. The orchestrator reflowed two
SyntheticData.cs comment sentences over the 25-word cap; content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…uites

ground-contact, menu-hangar-journey and menu-instant-action-journey each
read the install in only a few legs. Each is now two suites: a new
<name>-core suite carries every leg that reads nothing from the install and
joins SuiteCatalog.CiTier, and the original name keeps exactly the
data-bound legs and its gates. No check runs in both halves, so the
real-install battery still makes every check once.

ground-contact (40 check sites before):
- ground-contact-core (37 sites): the sweep landing (tier, ended by contact,
  beat the run time, rests on the surface, default branch, tallied as a
  sweep), the water branch, the default column (5 checks), the body sunk
  under the surface (3), the no_altitude veto (2), do_intersections
  outranking no_altitude, the untimed launch (3), the 15 s watchdog (3),
  the bounce resume with the impact_force gate and the Callback 16 disarm
  (8), and the unmasked control (3). That is 35 moved checks plus the
  collision-space and downward-probe preconditions, asked of the stage.
  The host is WithMotionHost with a new ground: true, which parents a bare
  AnimRuntime to EmptyStage.Build(collision: true); a hand-built
  AnimDefinition replaces the chapter's first destructible as the motions'
  owner, which only keys them in the set.
- ground-contact (5 sites): the same two preconditions on C1, then the
  extracted gunshell leg: the chapter program carries its launch, the event
  still authors no_altitude, and it selects no tier with the mask wired.

menu-hangar-journey (127 check sites before):
- menu-hangar-journey-core (125): the empty scratch store, the Mode door,
  plane selection, airframe and the defaults ask, the build screens less
  the preview check, name and purchase, edit and cancel, the Instant Action
  plane screen's door, the --menu= aids less the decal tile check, the
  campaign door, the campaign pages, the slot cap and the delete stage.
- menu-hangar-journey (2): the paint preview, asked of a Devastator walked
  page by page to the paint screen with one pattern stepped on (the state
  the journey asked it of), and the decal tile beside the rows under
  --menu=paint. Each check also requires the paint screen to be showing.

menu-instant-action-journey (150 check sites before):
- menu-instant-action-journey-core (149): Environment, the Table of
  Contents, Mission type, the ace skip, Waves, Wingmen, the back paths, the
  launch less the ace check, the return from flight and the aids.
- menu-instant-action-journey (1): the ace on a Girl Trouble squadron
  launched from Sky Haven through the wizard is the environment's own, not
  the built-in default. The check also requires one launch, over C4.

Two code comments that named ground-contact as the caller of
MarkLandingResume now name ground-contact-core. engine-suite-weights.json
gives each core its original's measured figure, since the unit test over
that file requires every registered suite to carry one (SuiteShards itself
would fall back to the default); the originals keep theirs, now an
overestimate until the next regeneration. docs/tooling.md states the split
rule, and the plan carries a Splits note under A4.

Verified here, on Linux with an empty data root and no install:
- each core by name: PASS; each original by name: SKIP, as before.
- each core FAILs on a broken input (ground: false leaves the probe nothing;
  a plane seeded into the scratch store; the wingman aircraft left
  unstepped), reverted. With their RequireData lines removed, the two menu
  originals FAIL here on the missing art and the default ace ("1 launches,
  C4, Marshall Bill Redmann"), which shows their walks reach the states
  they assert on.
- --run-tests=tier:ci: 56 PASS, 0 FAIL, 0 SKIP; the 37 engine error lines
  all match the allowed headless text-server pattern.
- full catalog: 56 PASS, 2 FAIL (build-stamp-focus, enet-dual-stack, both
  environment limits), 440 SKIP, against 53/2/440 before.
- dotnet build: 0 errors; dotnet format --verify-no-changes: clean; the
  StyleCop rebuild: no SA warnings. Units: 5480 passed, 305 skipped, 1
  failed (StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticks...,
  which finds this container's system SDL2 and is unrelated).

Not run: the real-install battery. On the author's machine all six suites
should PASS. ground-contact-core's note reads its flights against the
stage's flat ground at y = 0 (here: sweep and column 3.08 s ending 0.06 m
from it, unmasked 20.02 s ending 2000 m below), where ground-contact used
to report them against C1's terrain at the origin; ground-contact now
prints no note. The three originals' check counts drop by exactly the legs
moved: ground-contact from 40 sites to 5, menu-hangar-journey from 127 to
2, menu-instant-action-journey from 150 to 1.

On the merged tree (B11 landed beside it): RunCiSuites.ps1 56 passed, 0
failed, exit 0; the catalog, shard and synthetic-data unit tests pass. The
orchestrator reworded one ground-contact comment sentence over the 25-word
cap; content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…the synthetic tree

The synthetic data root gains two families, written by the new
CSVM/src/Tooling/SyntheticPlane.cs from hand-authored records under
CSVM.Tests/fixtures/synthetic/. The plane family writes planes/ for probe_plane (a
legacy-shape node tree and materials, plus a models.json generated from boxes.json,
one outward-wound box per mesh index) and zrdr/vehicle.json, engines.json, player.json
and the unit tests' own maneuvers.json, plus an empty C1/zrdr scope. The tree carries
the documented shape: geometry, healthy with one LOD (fuselage with a canopy child,
wings, tail, pdp1-8 torn panels), the markers rig (eight firepoints in mirror pairs,
eight pylons odd to port with |x| falling, target, cockpit_camera, two exhausts,
ground_level), dontmove props, destroyed pieces and a cockpit1 interior with
pcdp4/pcdp6. The pprobe player def carries ten dynamics keys, four zones with pdpanelN
injure entries and six collision probes; the probe AI def carries skills and a weapons
block. The armament family writes weapons.json (wep_probe_gun and wep_probe_rocket),
the six shake sources and a messages.json of MSG_PROBE_* names and invented wordings of
the HUD keys. Every value is invented and chosen to fly: --dump-flight=probe_plane
reads a 232 mph level top speed against a 268 mph fd_speed, a 2.9 s roll and a level
eighth-throttle cruise.

Two process-wide seams, both set only by Launcher under --synthetic-data:
SessionSpec.DefaultPlane (and Parse applies the stand-in when the switch names no
--plane=), so a spec a suite builds without a plane flies what the tree holds; and
StockLoadouts.Supplement, a second stock-loadout file read in place from the fixtures,
whose guns name wep_probe_gun through a new optional guns[].weapon key
(docs/formats/loadouts.md). TestContext.RequireTexture skips a suite under the switch
when the synthetic archive lacks a shipped texture it reads by name; puffer-fire-glow,
puffer-smoke-sun and tex-dropin use it and SKIP with the switch, and a real install
keeps failing on such a miss as before.

RunCiSuites.ps1 now passes --synthetic-data (-NoSyntheticData opts out) and accepts a
report only when it names the tree written for its own Godot process id and
syntheticData true; every other check stands. SuiteCatalog.CiTier grows from 53 to 75:
ai-actor, ai-pursues-structure, bindings-prompt-device, cockpit-overlay-pass,
cockpit-panel-staging, damage-staging-pool, death-camera, empty-stage-net,
flight-live-respawn-gate, flight-roster-transaction, flight-telemetry-gate,
flyby-camera, gltf-export, hostile-marker-hud, hud-auto-dock-line, inert-aircraft,
plane-shader-reuse, remote-airframe, scene-build-throw-frees, target-input,
targeting-candidates, team-model.

Verified on Linux, headless, empty data root, port base 50000.
- --stage=empty --fly --synthetic-data --det with no --plane= boots the stand-in
  (51 gamez nodes, 19 mesh instances, two gun groups, two hardpoints) with zero engine
  error lines; with --fire --fire-rockets nose down from 150 m every gun and rocket
  impact lands on ground/col.
- --run-tests=tier:ci --synthetic-data: 75 PASS, 0 FAIL, 0 SKIP, report dataRoot the
  run's .scratch/synthetic-data/<pid>, syntheticData true, 21 unexpected engine lines,
  all the text-server pattern analysis/headless-limits.json excuses. Without the switch
  over the empty root: 53 PASS, 22 FAIL (the new plane suites skip, which the tier
  counts as failures).
- Each added suite went red on one broken input in the tree, then the fixtures were
  restored: crippled dynamics (ai-actor, ai-pursues-structure, death-camera,
  empty-stage-net, remote-airframe), brick physics (flyby-camera), zero weapon damage
  (inert-aircraft, target-input, team-model), markers and cockpit1 renamed away
  (cockpit-overlay-pass, targeting-candidates), no pdpanelN entries
  (cockpit-panel-staging, damage-staging-pool), every box a point (gltf-export,
  hostile-marker-hud), skins absent (plane-shader-reuse), no meshed parent
  (scene-build-throw-frees), no AI def (flight-roster-transaction,
  flight-telemetry-gate), no fuel tank (flight-live-respawn-gate skips), an empty
  message table (bindings-prompt-device, hud-auto-dock-line). Ten more pass with the
  kit but no input break turned them red, so they stay off the tier for B15:
  ai-far-field-plant, ai-spawn-jitter, cutscene-handoff-speed, debug-kill-target,
  look-stick, look-stick-edge, muzzle-flash-cockpit-hidden,
  muzzle-light-first-person-point-term, spyglass-marker-hud, menu-player-setup-seats.
- Full catalog, empty root, no switch: 53 PASS, 2 FAIL (build-stamp-focus,
  enet-dual-stack), 440 SKIP, unchanged. With the switch: 85 PASS, 136 FAIL, 274 SKIP;
  the zrdr/ folder now exists, so suites gated on it alone run and miss
  cm_sequence.json (63), ai.json (11), a shipped airframe (13) or assert shipped data
  (49, with the two environment failures and two headless-only suites). The plane
  suites that assert a real record's value are listed for B15 in the plan's B13 section
  (ai-gunnery's 89 degrees, ai-modes' 2000 m, air-to-air's 60 m blast precondition,
  graze-bounce's 0.6, plane-wobble-walk's 4 Hz, cockpit-interior's shipped interior set,
  hud-kill-line, menu-free-flight-journey); the ordnance suites need the shipped wep
  ids, not their values.
- Build: 0 errors, no new warnings. dotnet format --verify-no-changes clean. The
  -t:Rebuild StyleCop step: no SA warnings besides SA0001. Units: 5491 passed,
  305 skipped, 1 failed, StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticks
  AndNamesTheSoname, which fails because this container has a system SDL2 2.30.0 and
  is untouched here; SyntheticPlaneTests (4 new) pass.
- The worktree guard refused pwsh here, so RunCiSuites.ps1 itself and the content gate
  were not run in this container.

The honest limit: no real-install run. The author's Windows battery should be
unchanged: nothing reads the new families without the switch, DefaultPlane and
Supplement are set only under it, a stock file with no weapon key parses as before,
and RequireTexture never skips over a real extraction, so puffer-fire-glow,
puffer-smoke-sun and tex-dropin PASS as before. On the real checkout both
--run-tests=tier:ci and --run-tests=tier:ci --synthetic-data should pass 75/0/0, the
second naming the install as unread.

On the merged tree (the three -core splits landed beside it; CiTier is the
union, 78): RunCiSuites.ps1, which now passes --synthetic-data, reported 78
passed, 0 failed, 0 skipped and exited 0. Units 5491 passed, 305 skipped, 1
failed (StickRosterTests.ALinuxBuildWithNoSystemSdl2..., this container's
system SDL2). The orchestrator reworded three comment sentences over the
25-word cap; content gate exit 0. PROJECT_CONTEXT.md's Current status moves
to next B14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…ll suites join tier:ci

--synthetic-data gains an original-shell family (Tooling/SyntheticShell.cs)
that writes what OriginalAvailability.Load and the screens read under
extracted/rof/. The hand-authored fixtures/menu-layout-original/LAYOUT.CSV
goes through the extraction's own MenuLayoutDecoder into menu_layout.json,
joined to invented string rows (fixtures/synthetic/rof/ui_strings.json, one
row per layout IDS_ symbol on invented ids, plus invented text at the three
ids the lobby reads directly). Every picture the shell draws is generated at
an invented size from fixtures/synthetic/rof/art.json: PNG through PngWriter,
JPEG and TGA through the new Tooling/SyntheticImages.cs (a flat-block
baseline greyscale JPEG and an uncompressed TGA), because Godot picks its
decoder by extension and the manifest requires .JPG and .tga names. A
required file with no recorded size stops the build. The two backdrop movies
stay absent, which the manifest classes optional. All 137 pictures load
through Godot's Image.LoadFromFile at their recorded sizes.

The layout fixture gains the ammunition screen's backdrop, plane diagrams and
text rows, OL_T_PLANEINFO among them, which menu-player-setup-seats reads.
The Original* unit tests still pass on the extended fixture, and two new
SyntheticDataTests check the tree passes the availability check and that each
picture carries its recorded size in its own format.

Joined to tier:ci (now 73): display-render-scale, menu-backdrop,
menu-join-board, menu-launch-return, menu-player-setup-seats and
menu-original-boot, -builtin-host, -cheats, -connection, -controls,
-coop-boot, -coop-film, -coop-flow, -lobby, -lobby-teams, -outlaw-list and
-version. Sixteen lost a zrdr gate whose only use is the Built-in launch
menu's tolerant stats read, as A2 did for the Built-in menu suites. Three
checks that pinned shipped words now read them from the data root:
menu-original-controls takes the KEYS column heads from [@keys@]'s KB_T_
rows, menu-original-cheats names the cabin painting by PC_BACKGROUND's art,
and menu-original-lobby reads langui 10123 to 10125 from the string table
after checking the three are present and distinct. On a run without
--synthetic-data (TestContext.SyntheticData false) each of the three also
keeps its original literal pin as its own check under the old wording: the
heads "Action", "Control A", "Control B" and "Stick", the painting
"PC_BackGround.png", and the descriptions containing "Dogfight to the
death.", "Steal the opposing squadron's flag" and "Protect your zeppelin". A
decode that garbles or swaps the shipped words still goes red on the install.
With --synthetic-data the three pass with the pins not reached (they would
fail on the invented words); without it they skip as before.

Verified on Linux, Godot 4.7 .NET headless, empty data root, port base
52000. The 17 by name with --synthetic-data: 17 passed, 0 failed, 0
skipped, no engine error line. Each can fail: with [@mainmenu@] renamed the
other 16 failed; a dropped KB_T_CONTTITLEB failed the controls check; a 10124
row holding 10123's words failed the lobby check; menu-player-setup-seats
failed before OL_T_PLANEINFO existed. --run-tests=tier:ci --synthetic-data:
73 passed, 0 failed, 0 skipped; its 37 engine error lines all match the
text-server pattern in analysis/headless-limits.json. Full catalog with the
switch: 73 PASS, 10 FAIL, 415 SKIP (B11's five texture-gated suites,
build-stamp-focus, enet-dual-stack, and the headlessOnly display-mode,
display-monitor and display-vsync, which now reach their window legs).
Full catalog without the switch: 57 PASS, 2 FAIL (build-stamp-focus,
enet-dual-stack), 439 SKIP; the change from 56/2/440 is
menu-player-setup-seats, whose Built-in leg now runs ungated while its
Original leg notes the absent layout. dotnet build 0 errors, dotnet format
clean, no StyleCop warning besides SA0001; dotnet test 5489 passed, 305
skipped, 1 failed: StickRosterTests.ALinuxBuildWithNoSystemSdl2... expects no
system SDL2 and this container has libSDL2-2.0.so.0, unrelated to this
change. The worktree guard refused pwsh, so the content gate and
CheckCommentCaps.ps1 were not run here.

CI needs the switch: RunCiSuites.ps1 on this base does not pass
--synthetic-data, so its tier run reports the 17 as skips until it does.

Still off the tier: menu-original-hangar and menu-original-instant-action
assert shipped geometry, art and ia.zrd values and want a split;
menu-original-campaign needs a cm_sequence entry for seq 0 and its briefing
dialog under zrdr; menu-original-wrapup needs a C4 gamez with Danger Zone
nodes, a C4/IA1 zrdr with stunt zones and messages.json;
flight-mouse-scheme-live needs player_bhawk's plane records;
menu-original-ipv6-address needs an IPv6 loopback; display-mode,
-monitor, -resolution, -vsync and menu-original-tracer are headlessOnly.

Not run against a real install. The author's RunTests.ps1 battery should be
unchanged: each of the 17 PASS as before, since a dropped gate changes
nothing where zrdr exists and the three retargeted checks read the shipped
words they pinned. Optionally --run-tests=tier:ci --synthetic-data on the
real checkout should pass all 73.

On the merged tree (B12 and B13's plane and armament families landed beside
this one, so zrdr/ now exists in the synthetic tree; CiTier is the union,
95): RunCiSuites.ps1 95 passed, 0 failed, 0 skipped, exit 0, 37 text-server
lines inside the allowance. The orchestrator reflowed one comment sentence
over the 25-word cap and added the real-install pins to the plan's C23 text;
content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…ound and smoke suites join tier:ci

A new `sounds` family (Tooling/SyntheticSounds.cs, one Families line) copies the hand-authored
fixtures/synthetic/zrdr/sounds.json (SETS and SOUND_GROUPS) to zrdr/sounds.json and writes
soundsh/, the unpacked sibling of soundsh.zip, with one generated WAV per entry of
fixtures/synthetic/soundsh/manifest.json. Each WAV is the shape docs/formats/sounds.md gives the
shipped archive: MS ADPCM, mono, 22050 Hz, the seven standard coefficient pairs, 256-byte blocks
and a fact chunk. The encoder is the new engine-free Tooling/WavWriter.cs, which
CSVM.Tests/SyntheticSoundsTests.cs round-trips through WavFile. The build throws when the manifest
and the SETS WAV names differ. The texture manifest gains smoke101..103, the pool ExhaustSmoke
names in code.

The stand-in's records now name their sounds: the base def's cockpit_engine_sound and
damaged_engine_sound, player.json's warning_shot_sound and bullet_hit_sound groups and its rattle
block, the gun's LOOPED_SOUND_NAME and the rocket's FIRE sound. Every definition is snd_probe_*
except the music cues MusicPlayer looks up by literal name (snd_music_splash and the seven
music_*_sg groups), over snd_probe_music_* members and invented music_<family>_probe*.wav files.

Verified on Linux, headless, empty data root, port base 53000. Build clean apart from the two
existing ShaderTwins nullable warnings; dotnet format and the StyleCop rebuild clean; units 5496
passed, 305 skipped, 1 failed (StickRosterTests'
ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname, which finds a system SDL2 in this
container and touches nothing here). Nine suites pass with --synthetic-data and joined
SuiteCatalog.CiTier, each shown red on one broken input and restored: engine-damage-phases (no
damaged_engine_sound), engine-cockpit-pitch (FREQUENCY on the cockpit loop), ai-engine-listeners and
audio-buses (engine_sound naming no definition), ai-weapon-emitters (no LOOPED_SOUND_NAME),
music-states (one primary stinger take), world-sound-falloff (no 3D definition), exhaust-smoke and
exhaust-smoke-ai (smoke102 absent). --run-tests=tier:ci --synthetic-data passes 87/0/0 with 37
unexpected engine lines, all the text-server pattern RunCiSuites.ps1 excuses; the worktree guard
refused pwsh, so RunCiSuites.ps1 and the content gate were not run here. The full catalog with the
switch went from 88 PASS, 136 FAIL, 274 SKIP to 97, 141, 260; without it, 56 PASS, 2 FAIL
(build-stamp-focus, enet-dual-stack), 440 SKIP.

Seven suites that skipped on the missing archive now run and fail with the switch, none on the
tier. ai-engine-rearm filters its log on "snd_damagedengine"; engine-voice-duck asserts the shipped
voiceover_volume_limiter 0.4 and picks its combat line by the snd_id prefix; incoming-fire-cues
asserts bullet_hit_sg, snd_bulletpass, snd_ricochet and snd_windowhit. Those are B15's. ai-voice and
voice-runtime need zrdr/voice.json, campaign-capture-silences-guns needs cm_sequence.json.
puffer-smoke-sun now finds smoke101 and fails headless on MultiMesh custom data the dummy renderer
does not keep; it belongs in analysis/headless-limits.json once a Windows-export headless run
confirms it.

Not run against a real install. The author's RunTests.ps1 battery should be unchanged: no suite
body changed, and nothing reads the new files without --synthetic-data. On the real checkout
--run-tests=tier:ci and --run-tests=tier:ci --synthetic-data should both pass 87/0/0.

On the merged tree (C23's original-shell family landed beside this one;
CiTier is the union, 104): RunCiSuites.ps1 104 passed, 0 failed, 0 skipped,
exit 0. Units 5498 passed, 305 skipped, 1 failed (StickRosterTests'
system-SDL2 case, environmental). Content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…and 11 suites join tier:ci

A --vs launch on --stage=empty now walks EmptyStage.SpawnRing instead of falling back to the
--pos override over the origin. The ring is one whole free-for-all net.zrd block built in code:
16 entries, so every seat NetSeats admits opens apart, on a 600 m ring at the stage's 300 m spawn
altitude, each nose aimed 10 degrees right of the origin. The skew is deliberate: aimed dead at the
origin, a hands-off two-seat run rammed nose to nose about 6 s in, and with it the two opposite
seats pass about 208 m abeam. Entry i takes the bit-reversed slot, so entries 0 and 1 are opposite
and 0 to 3 a compass cross. The records are (position, heading) tuples because Mech3 names no
Flight type; SpawnPicker.LoadSpawnList maps them to SpawnPoint and sets NetSpawns, so StartState,
the seat walk, PlanTeams and the respawn rotation run unchanged. SessionSpec no longer defaults the
--pos override for --vs on the stage, and an explicit --pos still wins. The empty stage's log lines
name "stage=empty ... spawn ring"; a chapter's are identical to before, and no chapter path changed.

In the suites, NetCombatSuites.Arena launches a match on C1/MP1 when the data root carries its
mission zrdr and on --stage=empty when it does not, and notes the arena in the report. MatchSpec,
net-enet-join, net-soak and net-guest-keymap use it, and net-aircraft-replication and
net-pause-overlay now build through MatchSpec. AirframesFor seats the empty arena on the spec's
plane (the stand-in under --synthetic-data) and MP1 on the shipped pair as before; Roster takes the
spec. The synthetic player.json authors an invented score_suicide of -3, so net-combat-events'
authored-scores control reads a value unlike the executable's fallback. The new install-free suite
versus-spawn-empty-stage asserts the ring, the two- and four-seat openings, the team walk, the --pos
override and the single-player control.

tier:ci goes from 95 to 106: versus-spawn-empty-stage, net-combat-events, net-enet-join,
net-flight-chat, net-guest-keymap, net-match-state, net-pause-overlay, net-relay-star, net-soak,
net-spawn-rotation and net-start-together. Each passed three consecutive runs with --synthetic-data
and an empty data root on distinct port bases, errors clean, and each went red on a broken input,
then restored: a ring of radius 0 failed net-combat-events, net-match-state, net-pause-overlay,
net-relay-star, net-spawn-rotation and versus-spawn-empty-stage; a two-entry ring with seat 1 on
an airframe the tree lacks failed net-enet-join, net-flight-chat, net-soak and net-start-together
and skipped net-guest-keymap, which the tier counts as a failure.

Verified on Linux, headless, empty data root: --vs --stage=empty --synthetic-data --players=2 --det
spawns P1 at (0,300,-600) heading 170 and P2 at (0,300,600) heading -10, 1200 m apart and facing in.
The full catalog without the switch reads 58 PASS, 2 FAIL (build-stamp-focus, enet-dual-stack),
439 SKIP against 57/2/439 on the base, the one difference the new suite, with the same 41 engine
error lines. --run-tests=tier:ci --synthetic-data at port base 54000 passes 106/0/0, its 37 engine
error lines all the text-server pattern analysis/headless-limits.json excuses. The full catalog
with the switch reads 115 PASS, 155 FAIL, 229 SKIP. Build clean (the two
existing ShaderTwins warnings), dotnet format clean, no SA warnings on the Rebuild. dotnet test:
5493 passed, 1 failed, 305 skipped; the failure is StickRosterTests' no-system-SDL2 case, which
this container's system SDL2 2.30.0 contradicts. The guard on this container refused pwsh, so
RunCiSuites.ps1 and the content gate were not run here.

Still off the tier. Asserting a shipped value, for B15: net-versus-lives (the kill and lives lines
"guest1 / Destroyed by host / You Have ONE Life Left!" and "Game Over: / No Enemies Left / ..."),
net-kill-line ("host / Destroyed by guest1") and net-aircraft-replication ("flown > 200f && turn >
30f": the stand-in turns 28 degrees on the held stick). Needing something else: net-custom-planes
and net-versus-host-left seat the lobby door's starter airframe player_pfighter over
PlanePickerRoster.StockAirframes, which the synthetic tree does not carry; net-rearm-deathmatch
needs an MP map's rearm nodes and net-team-deathmatch its team blocks (C22). With the switch these
now FAIL where they skipped; none is on the tier.

The honest limit: nothing here ran on the real install. The author's RunTests.ps1 battery should be
unchanged plus one PASS (versus-spawn-empty-stage): the install carries C1/MP1, so every touched
network suite flies the shipped table as before and notes "arena: C1/MP1 and its net.zrd table".
Optionally, --run-tests=tier:ci --synthetic-data on the real checkout should pass all 106.

On the merged tree (B14's sounds family landed beside this one; CiTier is
the union, 115): RunCiSuites.ps1 115 passed, 0 failed, 0 skipped, exit 0,
twice in a row. Units 5498 passed, 305 skipped, 1 failed (StickRosterTests'
system-SDL2 case, environmental). Content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
With --synthetic-data the invented tree carries zrdr/, planes/, C1/texture, soundsh/ and rof/, so a
suite gated only on such a folder ran and then failed reading a record the tree does not carry.
Those FAILs said "the invented tree lacks X", not "the code is broken". Each now SKIPs naming X.

Two gates join RequireData and RequireTexture on TestContext. RequireZrdrEntry(zrdrPath, file)
skips when a zrdr ZIP or unpacked folder lacks a reader file, through the new engine-free
Zrdr.HasFile (the two stored names LoadFile accepts). It is checked on every tree: a suite names
only entries every install ships, so on a real extraction it passes wherever the body ran before.
RequirePlane(nodes) skips when the planes gamez lacks a shipped airframe node, only under the
switch, the RequireTexture kind, because every install carries every airframe. CampaignSequence
gains a FileName constant that Load reads through.

Gated 111 suites, each at the top or in the shared helper that does its first read: 69 on
cm_sequence.json (DriveMission covers 13 landings suites, four NetCoopMissionSuites helpers cover 5
co-op suites), 11 on ai.json, 2 on voice.json, 19 on a shipped airframe, 4 on the effect readers and
fire101, and one each on C1 neindex.json, C1/IA1 weather.json, C4/IA1 ia.json, ia_escape.json,
rimage/prog_red.png and PX_P_DECALS.TGA. No assertion changed and no tier member moved.

Verified on Linux, headless, empty data root, port base 55000. Full catalog with the switch: 113
PASS, 154 FAIL, 231 SKIP before; 113, 43, 342 after (111 FAIL to SKIP, no PASS changed). Without
the switch 57, 2, 439 before and after, every suite's status and detail the same. tier:ci with the
switch 104/0/0, its 37 unexpected lines all the text-server pattern RunCiSuites.ps1 excuses. dotnet
build 0 errors, no new warnings; dotnet format clean; the StyleCop rebuild has no SA warnings;
dotnet test 5501 passed, 305 skipped, 1 failed (StickRosterTests' no-system-SDL2 case, which this
container's system libSDL2 answers; nothing here touches it). The new ZrdrTests prove HasFile on the
folder and zip forms under both stored names, and on an absent archive. The worktree guard refused
pwsh, so RunCiSuites.ps1 and CheckCommitContent.ps1 were not run here.

The 43 FAILs left, listed in the plan's B15 section: 31 assert a shipped value on invented data and
are the B15b candidates (AI, combat and flight, sound and HUD, sixteen ordnance suites by shipped
weapon id, three Original-shell layout suites); 9 are headless only; 2 need an IPv6 loopback; and
weapons-fire shows a bug. Loadout.ForRig synthesizes each gun slot from the stock spec's Caliber and
Ammo and drops its WeaponId, so a stock fit naming its weapon outright binds wep_30 instead of
wep_probe_gun and throws. No shipped stock fit names a weapon, so the real battery cannot see it.

Honest limit: no real-install run. On the author's extraction every RequireZrdrEntry names a shipped
entry and RequirePlane returns at once, so the full RunTests.ps1 battery should show the same
PASS/FAIL/SKIP for every suite, and tier:ci with and without the switch 104/0/0.

On the merged tree (C21's arena landed beside this; tier:ci holds 115):
RunCiSuites.ps1 115 passed, 0 failed, exit 0. Full catalog with
--synthetic-data: 124 passed, 49 failed, 326 skipped; the 49 are this item's
43 plus six network suites C21 brought onto the arena, each owned (rearm by
C22, the lobby-door pair by the player_pfighter stand-in, three shipped-value
assertions by B15b). Content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…ide a flag and rearm arena, and net-team-deathmatch joins tier:ci

The empty stage now carries the team and arena records a team match reads, built in code in the
shapes the real loaders return, so no real-named map enters the synthetic tree.
EmptyStage.SpawnTable is the free-for-all ring as block 0 followed by four 16-entry team blocks in
net.zrd's staging-stack shape: block N at team N's base 1.5 km out on a compass point, a square of
four positions 100 m apart on four rungs 100 m apart from 300 m, facing the origin.
SpawnPicker.LoadSpawnList answers a teamed launch on the stage with that whole 80-entry table, so
PlanTeams, SpawnPoints.TeamBlocks, the respawn blocks and the rotation run unchanged; an un-teamed
launch still walks the ring. A --vs build of the stage also stands EmptyStage.ArenaNodes
(Build(arena: true)): two bases, each a cs_flag_n on the ground, its hidden carried cs_flg_lightn
and a rearm_node_n 300 m inward and 50 m up, bare nodes carrying the gamez name meta. FlagRuntime
finds them through the session tree search it already makes; RearmRuntime takes them as Arena after
its world lookup, and GameSession.WireRearmBases opens on either. Every chapter path is unchanged:
the arena is null there and the rearm lookup reads the world first.

In the suites, NetCombatSuites.Arena takes the mission, so net-capture-the-flag flies C1/MP2 when
the data root carries it (with the chapter gamez gate as before) and the stage otherwise.
net-team-deathmatch reads its table through a SpawnPicker seated on its teams, which on the install
is the same LoadNetTable of MP1. versus-spawn-empty-stage's team leg now asserts that four seats on
teams 1,2,1,2 open on entries 16,32,17,33, each at its own team's base. The synthetic player.json
adds an invented score_kill of 2: with the fallback kill of 1, its score_suicide of -3 made the team
suite's target (four kills and a teamkill) 1, so the first kill ended the match.

The tier goes from 115 to 116 with net-team-deathmatch, which passed three consecutive runs with
--synthetic-data beside versus-spawn-empty-stage. Both went red with the team blocks replaced by
copies of the ring (the "no team opening stands on any free-for-all entry's ground" control and the
stage suite's own-base check), and the team suite went red with score_kill removed.

Verified on Linux, headless, empty data root, port base 56000: build 0 errors, dotnet format clean,
the -t:Rebuild StyleCop step with no SA warning, units 5498 pass, 1 fail, 305 skip (the failure is
StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname, which finds this
container's system SDL2). --run-tests=tier:ci --synthetic-data passes 116/0/0, its 37 engine error
lines all the allowed text-server pattern. The full catalog without the switch reads 58 PASS,
2 FAIL (build-stamp-focus, enet-dual-stack), 439 SKIP before and after, suite by suite. The worktree
guard refused pwsh, so RunCiSuites.ps1 and the content gate were not run here.

Off the tier, for B15b: net-rearm-deathmatch passes on the stage except Restored's
own == "Rearmed!" (the synthetic messages carry no MSG_MP_REARMED); net-capture-the-flag passes
every flag rule on the stage and fails eight marker checks, which need the shipped wording and a
target-site feed on the stage (BindsMissionTargetTable excludes it and ObjectiveSites resolves
through an AnimRuntime). The plan's C22 section lists the rest with their needs.

Honest limit: no real-install run. The author's full RunTests.ps1 battery should be unchanged:
net-team-deathmatch reads the same MP1 table through the picker, net-capture-the-flag flies C1/MP2
behind the same gates, the rearm runtime reads the world before an arena that is null there, and
versus-spawn-empty-stage (install-free) passes with its new team leg. Optionally,
--run-tests=tier:ci --synthetic-data on the real checkout should pass all 116.

On the merged tree (B15a's gates landed beside this): RunCiSuites.ps1 116
passed, 0 failed, exit 0; the full catalog without the switch 58 passed, 2
failed (build-stamp-focus, enet-dual-stack), 439 skipped, unchanged. The
orchestrator rewrapped the SpawnPicker architecture entry to its 8-line cap;
content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…airframe and a rig slot keeps its named weapon

The synthetic tree gains player_pfighter, the lobby door's starter airframe from
PlanePickerRoster.StockAirframes. The name is the code's; everything under it is invented from
docs/formats/ and visibly unlike probe_plane: shorter fuselage, wider wings, twin fins, an olive
finish, its own marker positions and two wing_flare quads. It also has its own pprobefighter def
(dynamics, zones, collision probes), engine row and engine sound, a probefighter jet flavour, the
wingman def the campaign wingman reads, and a four-pylon fit naming the invented gun and rocket.
Under the switch, StockLoadouts.Overlay lays the supplement fit over the committed table and
replaces the committed def on the same model, so a lookup by model finds the invented fit.
Without the switch Supplement is null and Load reads the committed file alone, as before.

Loadout.ForRig dropped a stock gun slot's named WeaponId and rebuilt the slot from Caliber and
Ammo, so a fit naming its weapon bound wep_30 and threw (weapons-fire with --synthetic-data). Its
def now comes from the node-free RigDef, which carries the slot's WeaponId (a slot stock leaves
out takes the first gun's). A slot naming no weapon carries null exactly as before, and Bind
composes the matrix id. LoadoutTests pins the named weapon and checks that every shipped fit's rig
slots still carry none. SyntheticPlaneTests pins the second airframe's rig, stats and overlaid fit.

net-versus-host-left joins tier:ci (116). It passed three runs with --synthetic-data and an empty
data root. It went red when a released Dogfight guest stopped watching its host (the HostGone
branch in NetPlayFeature disabled).

Still failing with the switch, off the tier:
- wingman-station: "settled < baseline + NitroReformM", settled 608 m against 312 m, of 100 m.
  The nitro leg's recovery rests on the shipped airframe's speed margin.
- wing-flare-pose: headless only. Its pixel reads return -1 on the dummy renderer; the structure
  and lamp checks pass.
- net-custom-planes: needs player_fbrand and player_avenger, the custom planes' airframes.
- net-kill-line: its named-host leg now builds and fails on the shipped MSG_DESTROYED_BY_X wording.
- weapons-fire: fails only on the shipped 48-weapon count, a B15b retarget.
net-two-session still skips on C1/MP1.

Verified on Linux, headless: build has 0 errors (the 2 warnings in ShaderTwins.cs predate this
change). dotnet format is clean, and the StyleCop rebuild reports no SA warning. Units: 5504 pass,
305 skip, 1 fail, StickRosterTests' no-system-SDL2 case; this container ships libSDL2. The full
catalog with --synthetic-data is 125 PASS, 50 FAIL, 324 SKIP against the base's 124, 49, 326. The
only status changes are net-versus-host-left to PASS and wingman-station and wing-flare-pose from
SKIP to FAIL. Without the switch it is 58, 2, 439, unchanged suite by suite with the same engine
error lines. RunCiSuites.ps1 passes 116/0/0 and the content gate exits 0.

Limit: no real-install run here. On the author's battery nothing should change. No shipped fit
names a weapon, so every rig slot binds as before, and Overlay and the fixtures only run under the
switch. RunTests.ps1 should show the same PASS/FAIL/SKIP per suite, and tier:ci with the switch
116/0/0.

On the merged tree (C22 landed beside this; CiTier is the union, 117):
RunCiSuites.ps1 117 passed, 0 failed, exit 0. Units 5504 passed, 305
skipped, 1 failed (StickRosterTests' system-SDL2 case, environmental).
Content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…ier:ci

The synthetic weapons.json gains eleven wep_probe_* records, one per ordnance class the ordnance
suites fly: wep_probe_torpedo, _he, _choker, _flare, _sonic, _flash, _seeker, _beeper, _flak,
_incendiary and _smoker. Each value is invented and chosen so its suite's behaviour is reachable in
the suite's own layout (a 36 m torpedo blast wider than the blast lab's 23.5 m ring, a 34 m sonic
fuse whose fade band covers a hull 29 to 33 m off, a 960 m/s beeper whose 16 m steps put one sample
10 m short of a rig 410 m out, a motor whose cap falls between 2 s and 3 s); none is set to a
shipped value. An IMPACT row names the effect the engine's own catalogue keys on (he_ground_effect,
scatter_effect, flak_effect and the rest), the rule the fixtures README already states for names
the engine looks up. messages.json gains one MSG_PROBE_* wording per DESC, and player.json gains
the three smokescreen_stun_* keys (450 m, 160 degrees, 4 s). CSVM.Tests/fixtures/README.md
documents every record.

A suite picks its weapon through the new OrdnanceSuites.PickWeapon: the shipped id on a real
extraction, the first record carrying the class under test on the synthetic tree, so the real
battery flies exactly the weapons it flew before. PickFireballRocket and DefaultBurst serve the
three burst suites. TestContext.RunsChapterWorld lets a suite's chapter-world half sit out on the
synthetic tree with a note; on a real extraction it always answers true. SyntheticPlaneTests no
longer pins a two-weapon catalogue: the gun and the rocket keep their names, and every record is a
wep_probe_* entry naming itself through the table with no unhandled key.

Every changed check, as old literal -> relational form, and whether the literal still runs on the
install (under if (!ctx.SyntheticData), old wording):

blast-curve-cover-cap
- wep_14 -> PickWeapon(Torpedo).
- HEALTH_DAMAGE 200 and IMPACT_PROXIMITY 30 -> new precondition IMPACT_PROXIMITY > 24 (the lab's
  23.5 m layout); every curve value was already read off the record. Literal kept.
blast-neighbor-shape
- wep_14 -> PickWeapon(Torpedo). No literal: every check already read the record.
burst-light-envelope, heat-shimmer
- wep_06 -> PickFireballRocket (high explosive, default row a catalogue fireball).
- plays.Contains("he_ground_effect") -> plays.Contains(heFx), the rocket's own default-row effect.
  Literal kept as "wep_06's default row plays he_ground_effect".
scorch-decals
- wep_06 -> PickFireballRocket. No literal on the weapon.
impact-orientation
- default rows {null, "he_ground_effect"} and {"scatter_effect", null} -> {null, not null} and
  {not null, null}. Literal kept.
- live Name == "he_ground_effect" (three) and "scatter_effect" -> Name == the row's own name
  (pinned by the row literal).
- the upper-ring half on ctx.Chapter's world runs only when RunsChapterWorld.
disabling-hits
- picks: Sonic, Flash rocket without a timed fuse, Tangler.
- SONIC 35 m, FLASH 450 m, TANGLER RADIUS 35 TIME 2 -> sonic radius in (33, 37), flash radius > 0,
  RADIUS > 25, TIME > 1. Literal kept.
- ENGINE_DEAD (5, 13) -> the catalogue pair equals the choker's own, Min > 0, Max > Min. Literal kept.
- cloud 2 - steps*dt -> TIME - steps*dt; Advance(1.5)/Advance(0.6) -> TIME*0.75 / TIME*0.25+0.1
  (both 1.5 and 0.6 on the shipped TIME 2, pinned above).
- expected > 10 -> expected > ENGINE_DEAD min + 1. Literal kept.
- atRelease > 10 -> atRelease > max(min + 1, 3.5) (atRelease is the refreshed expected, pinned above).
launch-velocity-decay
- picks: Torpedo, Tangler, gun.
- VELOCITY 60 and LOCK_ON 2.5 -> VELOCITY > 0, LOCK_ON in (0, 4), no ACCELERATION. Literal kept.
- speeds 180/120/60/60, slow 70/60, untargeted 60 at 0/1.25/2.5/4 s -> VELOCITY+120,
  VELOCITY+60, VELOCITY, VELOCITY; VELOCITY+10, VELOCITY; VELOCITY at 0, LOCK_ON/2, LOCK_ON, 4 s
  (pinned by the VELOCITY/LOCK_ON literal).
motor-acceleration
- picks: wep_04 and wep_26 -> a motor round without LOCK_ON; wep_12 -> a motorless rocket without
  LOCK_ON; wep_00 -> a gun.
- ACCELERATION 150 and VELOCITY 450 -> 2a < VELOCITY <= 3a. Literal kept.
- 150/300 at 1/2 s -> a and 2a (pinned above).
- 250/400 off a 100 m/s launcher -> min(100 + a, VELOCITY + 100) and min(100 + 2a, VELOCITY + 100)
  off wep_26's own record. Literal kept.
ordnance-end-conditions
- picks: HE with LOCK_ON, RANGE and no motor; Tangler; a timed fuse; Sonic; Torpedo.
- RANGE 1000 and 1000 and the flare none -> RANGE > 0, > 0, none. Literal kept.
- DETONATION_TIME 2.0 -> > 0; fuse at |t - 2| -> |t - DETONATION_TIME|. Literal kept.
- DETONATION_DISTANCE 35 stored as 1225 -> > 30 and stored as its square. Literal kept.
- RANGE_MINIMUM 300 and FLYOUT_HEALTH 10 -> > 0 and > 0. Literal kept.
- flare travelled < 5 -> <= VELOCITY x DETONATION_TIME + 0.5 and under half the default range.
  Literal kept.
- own-target burst in (270, 295) -> in (edge - 0.5, edge + step + 0.5), edge = 300 -
  sqrt(DD^2 - 30^2), step = VELOCITY/60 (the fuse is read at each step's start). Literal kept.
- untargeted burst > 900 -> > RANGE - step - 0.5. Literal kept.
ordnance-guidance
- picks: BeeperSeeker, Torpedo, Beeper; wep_04 -> a LOCK_ON_LEAD carrier with a motor.
- TURN_RATE 1.25, the 0.001 sentinel, TIME 20 -> seeker TURN_RATE > 0.01 with LOCK_ON, torpedo
  TURN_RATE in (0, 0.01) with LOCK_ON, TIME > 1. Literal kept.
- three LOCK_ON_LEAD carriers -> at least one, all at a sentinel rate. Literal kept.
- dumbfire turn in (0.003, 0.005) -> within 25 % of MaxTurnRad x 240 steps (the same window on
  0.001 rad/s).
- untargeted decay after 2.5 s to 60 -> after LOCK_ON s to VELOCITY. Literal kept.
- paint on at 19.5 s, off past 20 s -> TIME - 0.5 and TIME (pinned by TIME 20).
ordnance-impact-effects
- picks: Beeper, BeeperSeeker, and for flak a LOCK_ON round with a fuse, a damage pair it keeps, and
  distinct default and player ANIMATIONs.
- rows flak_effect + snd_missile_flak, flak_effectplayer, large_fireball + snd_missile_beeper,
  ballflare.flt + snd_missile_seeker, large_fireball -> the flak's default and player ANIMATIONs
  set and distinct, the beeper's player ANIMATION set, the seeker's default and player ANIMATIONs
  set. All five literals kept.
- live Name == "flak_effect" -> Name == the flak's default ANIMATION (pinned).
- the seeker's ground-flare half on C1 runs only when RunsChapterWorld.
shootable-flyout
- picks: Torpedo; for wep_06 a fused high explosive that is not TARGETABLE.
- FLYOUT_HEALTH 10 and DESTROY_ANIMATION torpedo_destroy_effect -> FLYOUT_HEALTH > 4 and a
  DESTROY_ANIMATION. Literal kept.
- seeded health 10/10, name wep_14#3, 6 after the first hit, live health 10, ceil(10/HD) hits ->
  the pool, "<id>#3", pool - 4, the pool, ceil(pool/HD) (pinned by FLYOUT_HEALTH 10).
- round wep_14#0 labelled "Aerial torpedo" -> "<id>#0" labelled with the weapon's DisplayName.
  Literal kept.
- destroy effect torpedo_destroy_effect -> the weapon's DESTROY_ANIMATION (pinned).
- the real-hit-ray half on C1 runs only when RunsChapterWorld.
smoke-screen
- wep_13 -> PickWeapon(SMOKE_SCREEN).
- TIME 8, and player.json 600 m / 5 s / cos 85 -> TIME > 3, range > 400, half-angle in (1, 97)
  degrees, range not the loader's default. Both literals kept.
- generate_smokescreen's two authored states -> literal only (the synthetic tree has no chapter
  anim archive).
- re-washes >= 4 -> >= (int)((TIME - 1.5)/1.5); emitter steps > 400 -> > TIME x 50 (both 4 and 400
  on TIME 8, pinned).
ordnance-launch-axis (not on the tier)
- wep_13 -> PickWeapon(SMOKE_SCREEN); on the synthetic tree the census flies the stand-in alone.
- no shipped airframe cants a pylon and the rig has more than one -> more than one pylon. Literal
  kept.
flyout-rack-pose (not on the tier)
- a RequireData on C1's gamez up front, so the synthetic tree SKIPs it there; on the install the
  file exists and nothing changes.

Fourteen suites join tier:ci, sorted into SuiteCatalog.CiTier: blast-curve-cover-cap,
blast-neighbor-shape, burst-light-envelope, disabling-hits, heat-shimmer, impact-orientation,
launch-velocity-decay, motor-acceleration, ordnance-end-conditions, ordnance-guidance,
ordnance-impact-effects, scorch-decals, shootable-flyout, smoke-screen. Two do not:
flyout-rack-pose reads every body off C1's FLYOUT models, which no synthetic family carries; and
ordnance-launch-axis builds the stand-in's loadout through Loadout.ForRig, which still binds wep_30
for a stock gun slot that names its weapon (the weapons-fire bug B15a records). With that slot's
WeaponId carried through by hand it passed, and the edit was reverted; it joins once the fix lands.

Verified on Linux, headless, empty data root, port base 58000. Each of the fourteen passed by name
with --synthetic-data three runs in a row (14 PASS, flyout-rack-pose SKIP, ordnance-launch-axis
FAIL on the ForRig bug, each run), and went red on one broken invented input, then restored: torpedo
IMPACT_PROXIMITY 12 (blast-curve-cover-cap) and 1.5 (blast-neighbor-shape); wep_probe_he VELOCITY 1
(burst-light-envelope, heat-shimmer, scorch-decals, impact-orientation); TANGLER RADIUS 15
(disabling-hits); the torpedo's LOCK_ON removed (launch-velocity-decay); ACCELERATION 60
(motor-acceleration); LOCK_ON on the choker (ordnance-end-conditions); seeker TURN_RATE 0.3
(ordnance-guidance); a default row on the beeper (ordnance-impact-effects); the torpedo's
DESTROY_ANIMATION removed (shootable-flyout); smokescreen_stun_range 250 (smoke-screen); and, with
the ForRig edit applied, the smoker's SMOKE_SCREEN removed (ordnance-launch-axis). Without the
switch all sixteen SKIP on the empty root as before. tier:ci with the switch: 130/0/0, its 37 engine
error lines all the text-server pattern RunCiSuites.ps1 excuses. Full catalog with the switch: 125
PASS, 50 FAIL, 324 SKIP before and 139, 35, 325 after (the fourteen FAIL to PASS,
flyout-rack-pose FAIL to SKIP, nothing new; the same 53 engine error lines). Without the switch:
58, 2, 439 before and after, suite by suite. Build 0 errors (the two existing ShaderTwins
warnings), dotnet format clean, the -t:Rebuild StyleCop step reports no SA warning, units 5501
pass, 1 fail, 305 skip (StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname,
which finds this container's system SDL2). The worktree guard refused pwsh, so RunCiSuites.ps1,
CheckCommentCaps.ps1 and the content gate were not run here.

The honest limit: no run here reads a real extraction. On the shipped values every relational form
reduces to the window of the literal it replaced, and every literal still runs under the install, so
the author's full RunTests.ps1 battery should show the same PASS/FAIL/SKIP per suite as before, and
tier:ci with the switch 130/0/0.

On the merged tree (the player_pfighter stand-in and its Loadout.ForRig fix
landed beside this): ordnance-launch-axis passed three runs in a row with the
switch and joins the tier too, so CiTier holds 132; RunCiSuites.ps1 132
passed, 0 failed, exit 0. Units 5504 passed, 305 skipped, 1 failed
(StickRosterTests' system-SDL2 case, environmental). The orchestrator split
one ordnance comment sentence over the 25-word cap; content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
…pectations off the loaded records and join tier:ci

Each check that asserted a shipped value now compares against the record it loaded, so it passes
on any consistent tree and still fails when the code mishandles the record. The shipped literal
stays beside it under !ctx.SyntheticData with its old message, so a regression on the install
still goes red. SuiteConstants.PlayerGlobal reads one player.json float raw (optionally inside a
nested block) for the read-back checks. tier:ci grows from 117 to 129.

Changed checks, old literal -> relational form (literal pin kept?):

ai-engine-rearm
- log filter line.Contains("snd_damagedengine") -> "-> {damagedName} " where damagedName is the
  cached AI stats' DamagedEngineSound, plus a check that the airframe names one. Pin added under
  !SyntheticData: damagedName == "snd_damagedengine" (the filter used to imply it).
ai-gunnery
- "the same bearing is taken at rating 9 (89°)" at a fixed 80° bearing -> the same refuse/take pair
  at the midpoint of the record's QuickDrawAngleDeg(1) and (9), messages naming both cones. Pin
  kept: the 80° phase with its old two messages runs after it on the install.
ai-modes
- MinAiActiveDist == 2000 -> equal to player.json's own min_ai_active_dist. Pin kept.
- spawn at 2600 m, "Dist() > 2000", "Dist() <= 2010" -> AiAttackRange + 600, > AiAttackRange,
  <= AiAttackRange + 10 (the promotion reads the attack volume, not min_ai_active_dist; the suite
  pins that volume at 2000 on the install, so the install's geometry is unchanged). Message
  "outside min_ai_active_dist it stays on patrol" reworded to "outside the attack radius".
- "the plane is climbing out" after 4 s -> back past the order's altitude while avoid crash holds,
  inside 8 s (the stand-in enters the order mid-dive). Pin kept: the 4 s check on the install.
aim-assist (the golden function only)
- sticky_bullet_catchup_rate 5.0, forget_interval 1.5, dist_factor 0.0, inaccuracy 1.0 deg -> each
  typed field equal to player.json's own key, which must be authored. Pins kept.
- CANNON_SPREAD == 6.0 -> WeaponConeCos(gun) == cos(CANNON_SPREAD degrees). Pin kept.
air-to-air
- no literal changed. The fused-rocket pick (fuse > 8 m, blast >= 2x fuse, blast >= 60 m) is now
  reachable through an invented rocket. The Fury sponge phase (wep_06 against player_fury, <= 8
  rockets, whole pool 90) runs on the install only; it is one shipped airframe against one shipped
  rocket and has no relational form.
cockpit-interior
- the named set (gauges, lowalt_on, stallwarning_on, bullet1..5 with >= 3 parked quads, gauges /
  structure / nosedamage / ggindicator0 still rendering) -> ParkedStates: every node the builder's
  own IsInteriorDrivenState names in the loaded interior is hidden, the group over each hole quad
  is drawn, and meshes outside the set still render. Pin kept: the named set moved unchanged into
  CockpitNamedSet, run on the install. The gauge drive (DrivenPanel, DrivenBelts) runs when the
  interior carries a gauges panel, and always on the install.
engine-voice-duck
- voiceover_volume_limiter == 0.4 -> equal to player.json's own key, which must differ from the
  compiled 0.5. Pin kept.
- combat line picked by QueueSeconds < 1 and the snd_id prefix -> the snd_id line first, else any
  sub-second QUEUE bark. Pin kept: the snd_id line must exist on the install (old message).
graze-bounce
- BounceFactor == 0.6 -> equal to player.json's crash.bounce_factor. Pin kept.
hud-kill-line
- the three kill rows and the three notice rows by shipped wording -> each key resolves to a row of
  its own (not the key) and the rows differ. Pins kept.
- "P2 Self-Destroyed" / "Destroyed by P1" -> the table's MSG_MP_SUICIDE and MSG_DESTROYED_BY_X
  templates filled with P2 and P1. Pins kept.
- the spawn: player_kestrel/medkestrel on the install as before; on any other tree the default
  airframe under its first AI flavour. "Medusa Kestrel" title -> a non-empty title, and the kill
  line equals "{title} {shot-down row}". Pin kept: the title == "Medusa Kestrel" on the install.
incoming-fire-cues
- BulletHitSound == "bullet_hit_sg" -> warning_shot_sound, bullet_hit_sound and window_hit_sg each
  resolve to a group with members. Pin kept.
- snd=snd_bulletpass / snd=snd_ricochet / snd=snd_windowhit -> snd=<a member of that cue's own
  group>. Pins kept.
plane-wobble-walk
- swing gaps 8..11 ticks ("the authored 4 Hz") -> within [T - 1.5, T + 2] ticks of the ramp law's
  swing computed from the record's nitro, T = (1+k)/(4 f k) s with k = e^(-damp/2f). Pin kept.
weapons-fire
- weapons offered / mounted and fired == 48 -> every weapon the loaded table defines mounted and
  fired (Total > 0, Ok == Total). Pins kept: both 48 counts on the install.
ground-shadow
- no literal changed. RequirePlane("player_autogyro") makes it SKIP under the switch, naming the
  rotor airframe its turning phase flies; its terrain phase also needs C1's world.

Invented records added or retuned (CSVM.Tests/fixtures/README.md): player.json carries the four
sticky_bullet_* globals (4.0, 1.2, 0.0001, 0.8) and voiceover_volume_limiter 0.45, each unlike the
compiled fallback and the shipped figure; weapons.json adds wep_probe_blastrocket (HE, 11 m
trigger, 66 m blast, 620 m/s, 30/30 damage) with its MSG_PROBE_BLAST_ROCKET row; the nitro shake
moves to frequency 3, damp 4, so its ramp-law swing (14.7 ticks) stands apart from its authored
rate; sounds.json adds window_hit_sg over two invented members with their WAVs; messages.json adds
invented wordings of MSG_TIME_EXPIRED, MSG_MISSION_LOST, MSG_MP_SUICIDE and MSG_DESTROYED_BY_X;
the probe's cockpit1 gains probe_lamp_on and a probe_holes group over bul9a..bul9c.
SyntheticPlaneTests expects the third weapon; SuiteCatalogTests' union case names chapter-census,
since weapons-fire is now a tier member.

Verified on Linux, headless, empty data root, port base 59000. Each of the twelve passed three runs
by name with --synthetic-data, and went red on one broken input, then restored: ai-gunnery
(quick_draw_angle reversed), ai-modes (min_ai_active_dist renamed), ai-engine-rearm
(damaged_engine_sound renamed), aim-assist (sticky_bullet_dist_factor renamed), air-to-air
(blast 20 m), graze-bounce (crash bounce_factor renamed), plane-wobble-walk (nitro sawtooth 0:
gaps of 10 against 14.7), cockpit-interior (lamp and quads renamed out of the parked set),
engine-voice-duck (limiter 0.5), incoming-fire-cues (window_hit_sg renamed), hud-kill-line
(MSG_MP_SUICIDE renamed), weapons-fire (pylons renamed). Without the switch all fourteen SKIP as
before. tier:ci with the switch passes 129/0/0; its 37 unexpected engine lines are the text-server
pattern RunCiSuites.ps1 excuses. The worktree guard refused pwsh, so RunCiSuites.ps1 and the
content gate were not run here. Full catalog with the switch: 138 PASS, 38 FAIL, 323 SKIP against
the base's 126, 51, 322, changed suite by suite only in the thirteen above. Without it 58, 2, 439,
unchanged suite by suite. Build: 0 errors, the two existing ShaderTwins warnings. dotnet format
clean, no StyleCop warnings. Units: 5504 pass, 305 skip, 1 fail
(StickRosterTests' system-SDL2 case, which fails the same way on the base tree in this container).

Not brought on: wingman-station. Its nitro leg re-forms on the shipped airframe, but on the
stand-in fighter the escort and leader top out at the same thrust-limited speed after the burn,
so the escort regains only what the leader's turns give back (705 to 608 m over 80 s against a
312 m baseline). Making that reachable means retuning the stand-in fighter's dynamics, which
net-versus-host-left and the fighter's own legs share.

Limit: no real-install run. The author's full battery should show the same PASS/FAIL per suite.
On the install every relational check reads the shipped record and every literal pin still runs.
Two relational checks are new there: plane-wobble-walk's window around the shipped nitro's
computed swing, and cockpit-interior's drawn group over every bulNx quad. ai-gunnery's quick-draw
pair now also runs at the midpoint bearing before the old 80° one. tier:ci on the real checkout
should pass 129/0/0 with and without the switch.

On the merged tree (B15b-1's ordnance records landed beside these; the
weapons table, messages, fixture README and CiTier are unions, and the unit
test's three-weapon count gives way to B15b-1's every-record-is-invented
check, keeping this item's blast-rocket assertions): RunCiSuites.ps1 144
passed, 0 failed, exit 0, twice; the full catalog without the switch 58
passed, 2 failed, 439 skipped, unchanged. Units 5504 passed, 305 skipped, 1
failed (StickRosterTests' system-SDL2 case, environmental). The orchestrator
split two cockpit comment sentences over the 25-word cap; content gate exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
… arena, campaign-layout splits, and seven join tier:ci

The synthetic messages.json words the Dogfight lines and the Capture the Flag markers under the
keys the HUD and FlagMarkers name, in invented wording (ids 51 to 67): MSG_DESTROYED_BY_X,
MSG_MP_SUICIDE, MSG_MP_KILLED_X_TURRET, MSG_ONE_LIFE, MSG_STATE_GAMEOVER, MSG_MP_ALL_ALONE,
MSG_MP_REARMED, MSG_X_YOUR, MSG_X_ENEMY, MSG_MP_YOUR_BASE, MSG_MP_ENEMY_BASE,
MSG_MP_YOUR_FLAG_BASE, MSG_MP_ENEMY_FLAG_BASE, MSG_FLAG_CAP_BY, MSG_YOUR_FLAG_FLOAT,
MSG_ENEMY_FLAG_FLOAT and MSG_HOLDS_FLAG. Each side's pair is worded apart and each name row
carries its %1 (and %2). CSVM.Tests/fixtures/README.md documents them.

A production seam feeds the empty stage's flag markers, which read "off" there because
BindsMissionTargetTable excludes the stage and ObjectiveSites resolves a site through an
AnimRuntime. EmptyStage's arena stands a ctf_n node beside each base's cs_flag_n, lists the six
marker keys as ArenaTargets and finds an arena node by its gamez name (ArenaNode).
MissionTargets.Objectives builds a table in code, every key flagged objective. ObjectiveSites
gains a constructor taking a node finder in place of an AnimRuntime; Resolve uses it only where
no runtime exists. GameSession binds that feed in a new branch after the mission-table branch,
only for a --ctf launch whose stage arena exists, with FlagRuntime.SideOf labelling the sites.
Every chapter path is unchanged: the new constructor, the finder and the branch are reached only
on the stage, and the arena gains nodes only there. docs/architecture Mech3.md, Session.md
(ObjectiveSites and GameSession) and Flight.md (MissionTargets) say so.

Every changed check, as old literal -> relational form, and whether the literal still runs on the
install (under if (!ctx.SyntheticData), old wording):

NetCombatSuites.MatchWording reads the rows off ctx.MessagesPath; its Check precondition (new)
fails when a row is missing (it would read as its key) or a name row has no %1.
net-kill-line
- new precondition: the table words MSG_DESTROYED_BY_X, MSG_MP_SUICIDE, MSG_MP_KILLED_X_TURRET,
  each with %1.
- "host / Destroyed by guest1", "guest1 / Destroyed by host", "guest1 Self-Destroyed",
  "guest1 / Killed by host Turret" -> Fill(row, name) of each key ("both machines post the message
  table's ... once"). Literal kept: KillLines takes the shipped pair and, off the switch, checks
  the same panes against it with the old description.
- [named host] "Laeresh / Destroyed by Montgomery Fair" and the reverse -> the same, through
  DestroyedBy(named). Literal kept the same way.
net-versus-lives
- new precondition: MSG_ONE_LIFE, MSG_STATE_GAMEOVER, MSG_MP_ALL_ALONE worded, MSG_DESTROYED_BY_X
  with %1.
- StartsWith("guest1 / Destroyed by host / You Have ONE Life Left!") -> StartsWith("guest1 / " +
  DestroyedBy("host") + " / " + row(MSG_ONE_LIFE)). Literal kept.
- StartsWith("Game Over: / No Enemies Left / guest1 / Destroyed by host") -> the GAMEOVER and
  ALL_ALONE rows over the same kill pair. Literal kept.
net-rearm-deathmatch (Restored, shared by the three rearm suites)
- own == "Rearmed!" && hosts != "Rearmed!" -> the MSG_MP_REARMED row, which must be present (not
  the key), on the guest's pane and not the host's. Literal kept.
net-capture-the-flag
- new precondition (Worded): all ten marker rows present, and Your/Enemy, both base, both
  flag-at-base and both floating pairs worded apart.
- Reads on ctf_1 and ctf_2: "Your Base"/"Enemy Base" -> MSG_MP_YOUR_BASE/MSG_MP_ENEMY_BASE rows by
  side. Literal kept.
- cs_flag_1 and cs_flag_2 (the able-to-fail control): "Your Flag At Base"/"Enemy Flag At Base" ->
  MSG_MP_YOUR_FLAG_BASE/MSG_MP_ENEMY_FLAG_BASE. Literal kept.
- cs_flg_light1 held: "{Your|Enemy} Flag Captured by {carrier}" -> Fill(MSG_FLAG_CAP_BY,
  MSG_X_YOUR or MSG_X_ENEMY, carrier). Literal kept.
- cs_flg_light1 floating: "Your Flag Floating"/"Enemy Flag Floating" -> MSG_YOUR_FLAG_FLOAT/
  MSG_ENEMY_FLAG_FLOAT. Literal kept.
- carrier tag "{carrier}  Holds Your flag" -> Fill(MSG_HOLDS_FLAG, carrier, MSG_X_YOUR). Literal
  kept.
- the away marker's near < 30 f is unchanged; it passes once the stage feeds its sites.
- the two "off" Reads are unchanged.
net-aircraft-replication
- flight.Flying && flown > 200 && turn > 30 is unchanged. On the stage the roster flies
  DistinctAirframesFor (probe_plane as host, player_pfighter as guest) and the leg is
  ArenaFlightSteps = 360: 508 m and 41 degrees, where 240 steps on probe_plane alone turned 28.
  On MP1 the airframes are the shipped pair and the leg 240 steps, as before. No invented record
  changed.
net-two-session
- it builds through NetCombatSuites.MatchSpec (MP1 when the root carries it, else the empty
  stage's ring) instead of its own MP1-only RequireMatchData, and flies DistinctAirframesFor,
  gated by RequirePlane (a no-op on an install). The airframe check compares against that pair,
  which must hold two distinct names, rather than the class's own shipped list. On MP1 the spec
  gains only --no-pads, as MatchSpec gives every network suite.

campaign-layout-parity splits, leg by leg:
- campaign-layout-core (new, gate: the decoded layout): the layout loaded (precondition, also in
  the parity half); per aid, the pinned composition reads CampaignLayout.Fallback (new), the
  second composition reads the data root's layout (moved from parity), each source composes a
  board (new).
- campaign-layout-parity keeps the six pinned rows and the per-aid element comparison, and skips
  under --synthetic-data, since its hardcoded side copies the shipped layout. Never on an install.
  engine-suite-weights.json gives the core its original's 0.136.

Not brought on, and why:
- menu-original-hangar: 25 checks fail on the invented tree, spread over every leg of one walk:
  authored geometry (the centred name pane, the edit box and OK, the tab bar, the armour boxes,
  READY, Purchase Now, SELL PLANES), shipped art (PX_BackGround.jpg, PX_Tab.png, PX_P_Decals.tga,
  the PX_ICON_ composite sets), langui rows the code reads by id (1139, 1149, 1154, 1256, 3240 on)
  and the 66-pixel decal tile; with no 1154 row the heading split throws. A split would walk twice
  and run every walk check in both halves, against docs/tooling.md's rule, and relational forms
  need invented art, strings and the row-placement rules derived from code, not from the
  invented layout.
- menu-original-instant-action: 19 fail: the first environment's and four presets' own ia.zrd ace
  (no environment carries an ia.zrd on the synthetic tree; adding them un-gates other suites), the
  layout's fourteen contents rows, the arrow, thumb, dropdown, lives box and ammo field geometry,
  IA_BackGround.jpg and OL_BackGround.jpg, and the Hellhound's three pylons against an invented
  ammo layout with two rocket fields.
- net-custom-planes: player_fbrand and player_avenger, the author's call.

Verified on Linux, headless, empty data root, port base 61000: build 0 errors (the two existing
ShaderTwins warnings), dotnet format --verify-no-changes clean, the -t:Rebuild StyleCop step reports
no SA warning, units 5504 passed, 305 skipped, 1 failed
(StickRosterTests.ALinuxBuildWithNoSystemSdl2RunsWithoutSticksAndNamesTheSoname, this container's
system SDL2). The seven by name with --synthetic-data: PASS three runs in a row each. Able to fail,
each reverted: the kill arm posting MSG_DESTROYED_BY_X unfilled failed net-kill-line and
net-versus-lives; PostRearmed posting MSG_STATE_GAMEOVER failed net-rearm-deathmatch; the base
marker's Your/Enemy rows swapped failed net-capture-the-flag; the guest reading the airframe
order reversed failed net-two-session; probe_plane's pitch_torque 2.0 -> 0.8 failed
net-aircraft-replication (21 degrees); CampaignFlow.Layout pinned to the fallback failed
campaign-layout-core on every aid. Without the switch all seven and campaign-layout-parity SKIP on
the empty root. --run-tests=tier:ci --synthetic-data: 139 passed, 0 failed, 0 skipped, its 37
engine error lines all the text-server pattern RunCiSuites.ps1 excuses. Full catalog with the
switch: 148 PASS, 29 FAIL, 323 SKIP (against 141, 35, 323 inferred for the base: five network
suites FAIL to PASS, net-two-session SKIP to PASS, campaign-layout-parity FAIL to SKIP, the new
core PASS; the 29 are B15b-2's fourteen, ten headless-only, two IPv6, net-custom-planes and the
two shell suites above, nothing new). Without the switch: 58 PASS, 2 FAIL (build-stamp-focus,
enet-dual-stack), 440 SKIP against 58, 2, 439, the one change campaign-layout-core skipping on the
absent layout. The worktree guard refused pwsh, so RunCiSuites.ps1, CheckCommentCaps.ps1 and the
content gate were not run here.

The honest limit: no run here reads a real extraction. On the install every network suite flies
MP1 or MP2 behind the same gates, each relational line reduces to the shipped row, and each
literal still runs, so the author's full RunTests.ps1 battery should show the same PASS/FAIL/SKIP
per suite as before, plus campaign-layout-core passing. net-two-session now launches with
--no-pads, as the other network suites do. Optionally, --run-tests=tier:ci --synthetic-data on the
real checkout should pass all 139.

On the merged tree (B15b-2 landed beside this): MSG_DESTROYED_BY_X and
MSG_MP_SUICIDE were added by both items, so the synthetic messages keep
B15b-2's rows and drop this item's duplicates; every check reading them is
relational. RunCiSuites.ps1 151 passed, 0 failed, exit 0, twice. Full
catalog without the switch 58 passed, 2 failed, 440 skipped (the new
campaign-layout-core skips); with it 159 passed, 17 failed, 324 skipped.
scene-build-throw-frees fails only in that full run, on an object count an
earlier suite's deferred frees reach; it passes alone and inside tier:ci.
The orchestrator split nine comment sentences over the caps; content gate
exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
The B15 section now opens with the open work, each item with its owner:
the two author-approved invented airframes (player_fbrand, player_avenger),
C24 with an invented voice.json family, the suites kept off the tier and
why, the headless-only candidates that wait on a Windows-export headless
confirmation, the order fragility of scene-build-throw-frees in a full
catalog run with --synthetic-data, and the campaign family's need for an
invented campaign sequence. PROJECT_CONTEXT.md's Current status moves to
next B15.

State at hand-off, Linux headless with an empty data root: tier:ci 151
passed, 0 failed under RunCiSuites.ps1; full catalog with --synthetic-data
159 passed, 17 failed, 324 skipped; without it 58 passed, 2 failed, 440
skipped. The Windows RunTests.ps1 battery over the real extraction has not
been run on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UD5P8V4TzRi1WVhTXVdviK
Answers "does a wave-displaced ocean look right in Enhanced mode" on C1B. Only an Enhanced
session over C1B in --fly or --freecam builds it; CSVM_OCEAN=0 turns it off for a flat-sea
comparison shot.

- Effects/OceanPrototype.cs: a camera-centred polar grid (384 segments near the eye, halving
  at 700/2800/11000 m, about 134k vertices) with an 8-wave Gerstner swell (6.8-140 m, crest
  about 1-2 m) displaced in the vertex stage, plus 6 chop waves (0.5-4.4 m) in the fragment
  normals. Per-wave fades by grid spacing and by pixel footprint keep under-sampled waves out.
  Driven by csky_time, with omega rounded to whole cycles per 3600 s wrap.
- A mask baked once from the world's mesh instances (8 m texels): R sea coverage, G wave
  height after a 24-160 m shore fade, and a tint texture holding the base sheet's baked vertex
  colour, which carries the darker water around the islands. Wake sheets (wakefront) are
  found in the tree; their positions and the roster hulls' feed 16 calm discs (140-320 m).
- SceneBuilder: a key bit for the opaque wtr* base sheet. Its Enhanced text collapses a
  sea-level vertex while csky_ocean_on is 1 (csky_ocean.gdshaderinc). Original-mode text is
  unchanged; colliders are untouched.
- GameSession.FollowOcean builds the node after the world and on a switch to Enhanced, and
  drops it on a switch back; _ExitTree resets the global so the sheet draws again.

Verified:
- With waves, foam and detail neutralised and roughness at the sheet's 0.25, the ocean
  matches the flat sea to 0.65/255 mean luminance over the sea region.
- RunTests -SkipUnits -SkipEngine: 24 goldens hash-identical. No Enhanced golden shows the
  C1B sea, so none moved.
- Desktop gpu_ms at 1920x1080 (RTX 5080, another session's tests running alongside): cruise
  1.33 vs 1.03 flat; low pass 18.4/20.5 vs 13.4 flat. With --no-ssr the low pass is 7.8 vs 6.5,
  so most of the low-altitude cost is screen-space reflection over the wavy surface.
- Not verified: the live graphics switch (no scripted door), shimmer in motion, the Deck
  (unreachable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user read the cruise montage's foam as a pattern. The foam marked where the eight swell
crests coincide, and those coincidences repeat on a lattice. Two octaves of world-space value
noise (230 m and 71 m, drifting with the lead swell) now decide where whitecaps can form and
shift each crest's threshold. A 9 m octave breaks each fleck up. Coverage is sparser than
before.

Verified by eye on the cruise and coast poses (foam-before-after montage). No change outside
the ocean shader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turns the approved prototype (44e87ff, 38c9709) into an Enhanced-mode feature in three waves:
the port and its Water Quality setting, the cost (SSR, Deck, mask bake), and coverage (other
chapters, ship calm zones, swell regularity, an Enhanced golden). Decisions 10 and 11 are the
user's rulings for this run: Water Quality defaults to waves on the desktop and flat on the
Deck and Linux, and the low-pass cost stays within 1.5 ms over the flat sea.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prototype's OceanPrototype.cs is now Effects/Ocean.cs (the grid, the generated shader, the
wave tables, foam and calm discs) and Effects/OceanMask.cs (the shore mask bake, its walk state in
a nested walker instead of eleven ref parameters). The look is unchanged. PROTOTYPE wording is gone
from the code, the include, SceneBuilder and the architecture entries; log lines read "ocean: ...".

Env-var doors became flags or went: CSVM_OCEAN=0 is now --no-ocean, an EnhancedPasses bisect door
beside --no-ssr; CSVM_OCEAN_MASK_PNG is now --dump-ocean-mask=<path>, which writes the mask and
does not end the run; CSVM_OCEAN_PARAMS and CSVM_OCEAN_TRACE are deleted. Both flags are in
docs/cli.md and SessionSpecParserTests. The chapter gate (Ocean.Covers, C1B), the --fly/--freecam
gate and the Enhanced-only build/drop in GameSession.FollowOcean are as before.

The agent found that the prototype's mask bake broke TextureUploadSourceTests by calling
ImageTexture.CreateFromImage directly; the bake now goes through TextureUpload.Create, keeping its
image for the dump.

Verified:
- Cruise pose pixel-identical before and after the port (decoded md5 6E4CA61F...); --no-ocean
  draws the flat sheet with no hole.
- Complete battery on the plan tree: units 6275 passed, 3 skipped; engine 527 passed, 2 skipped,
  errors clean; 24 goldens hash-identical.
- The first engine run lost shard 1 to a 300 s timeout and logged a shader_rd.cpp cache-read
  error ("br != variant_size") while another session's six-shard battery ran beside it. Godot's
  shader cache is shared through the per-user app data folder. The graphics and vessel suites and
  then the whole engine stage re-ran clean once that battery had finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rvey

Base sheets, coast layers and coplanar land at y = 0 per chapter, with world y through the full node transform chain. The chapter scope is left to the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user chose every sea chapter (C1, C1C, C2, C2B, C3, C5); C4 keeps its flat lakes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lied live

--water-quality=flat|waves, the saved option waterQuality and the config key
graphics.waterQuality choose whether Enhanced draws the wave ocean. Precedence is the other
Enhanced options': flag, saved option, config key, then the machine's fallback; under --det only
the flag survives, over waves. The fallback (Decision 10) is flat on Linux, which covers the
Deck, and also on an integrated GPU, through the test shadow quality already makes (now exposed
as ShadowQualitySetting.IntegratedGpu); waves otherwise. The integrated-GPU half extends the
user's ruling and is flagged to them.

A "Water quality" row sits under Shadow quality in the Enhanced options, dead under Original
with a detail line. A live change calls EnhancedLook.ApplyWaterQuality, which runs
GameSession.FollowOcean: flat drops the ocean and the flat sheet draws again, waves rebuilds it.
--no-ocean still wins. The Original VIDEO page has no line for it and passes the saved word
through, as it does the view distance.

Agent verification (the plan's Verified paragraph waits for the battery on the merged tree):
- WaterQualitySettingTests 10 pass; units 6285 passed, 3 skipped; -Quick passes; zero StyleCop
  warnings.
- New engine suite graphics-water-quality on a real C1B Enhanced session: waves builds the ocean,
  a live change to flat removes it, back to waves rebuilds it, a session built at flat has none,
  --no-ocean wins at waves and after a live apply. display-det-guard, menu-original-tracer,
  menu-launch-return, pause-preferences, display-shadow-quality, graphics-view-distance pass.
- Cruise waves shot decoded md5 6E4CA61F..., the A1 baseline; the flat shot equals the
  --no-ocean control.

Known: a live apply to waves re-bakes the mask with no cover over the stall (2.75 s in the
agent's probe); B13 owns the bake time. RenderingServer.GlobalShaderParameterGet errors outside
the editor, so the suite checks the ocean node's presence rather than csky_ocean_on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of 3-5 s

The bake's time was ArrayMesh.SurfaceGetArrays: 2238 surfaces at about 1 ms each, each call
reading the surface back off the GPU and waiting on the render thread (1.9-3.6 s of a 3.0-4.8 s
bake, Debug build, shared machine at about 68 % load, three loads). Raster 0.7 s, encode 0.15 s,
chamfer 0.1 s, tree walks 0.13 s, wake search 0.03 s made up the rest.

- OceanMask reads each surface through SceneBuilder.SurfaceArrays, the arrays the builder already
  keeps. Positions, indices and UVs are bit-equal to the read-back; colours are truncated to
  RGBA8 the way Godot stores them. The wake search and the surface listing are one tree pass.
- New OceanMaskRaster holds the texel work, run in row bands on the thread pool; the distance
  pass reads 24 rows past each band edge and keeps the old operator order, so every texel rounds
  the same. OceanMaskRasterTests check it against the old algorithm at 1, 5 and 12 bands and fail
  if the halo or the row clipping is weakened.
- Each built world keeps its bake (a ConditionalWeakTable keyed on its SceneBuilder), so an ocean
  rebuilt over the same world, after a live switch or a Water Quality change, reuses it. That
  path is reasoned, not probed.
- No disk cache: nothing left after the read-back is worth one that has to be invalidated.
- The ocean: built line carries bake= and the phase split; --dump-ocean-mask also writes
  <name>.tint.png.

Agent verification: five Debug loads bake in 87-96 ms (walk 27-33, raster 26-36, distance
17-21, upload 7-8); a bake after the world merge, the order a live apply hits, took 107 ms with
the same mask. Decoded md5s before and after: mask 75EBB314..., tint 8742B5D0..., cruise
6E4CA61F... (BGRA order). -Quick passes (997 units, 13 engine suites). DefaultBands (16) is not a
measured choice. The plan's Verified paragraph waits for the battery on the merged tree.

Merge: docs/cli.md conflicted on the two ocean flag bullets; kept A2's --no-ocean wording (it
names the water quality) and B13's --dump-ocean-mask wording (the tint file and the split).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A whole C1B flight session at water quality waves, through graphics-live-switch's Open/Switch path: built Enhanced it holds exactly one ocean and the wtr00000 sheet carries the csky_ocean_hides_sea call; switched to Original the ocean leaves the tree and the sheet's text equals a fresh Original build's; back to Enhanced exactly one ocean is rebuilt and the text equals a fresh Enhanced build's; an Original-built session switched to Enhanced builds the ocean. After each session closes no ocean remains, and a following Enhanced C2B session (whose wtr00000 sheet would hole if csky_ocean_on leaked) builds none. A mutation that made FollowOcean forget the dropped ocean failed the leave-the-tree and exactly-once checks. The network refusal is net-pause-overlay's. One test seam: GameSession.WorldScene.

Agent verification: -Filter graphics 6/6, errors clean, the new suite 11.4 s; -Quick passes; zero StyleCop warnings. The motion half of A3 is the user's flight; the checklist line stays open. C21 lifting C2B's gate will need the leak check's follow-up chapter changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Laeresh and others added 29 commits October 8, 2026 07:05
…at holds at world scale

A3's finding: "the foam in c1b looks good from a distance but not really like foam if near it
(kinda glitchy)"; on the reference, "foram probably doesnt need to be so finely structured".

The glitch was the foam's hash: fract(sin(dot(c, k)) * 43758.5453) on world-scale cells (dot
products of 5e4 to 4e5 at C1B), so a corner computed from its two neighbouring cells rounded
differently and the value noise jumped at cell boundaries. Foam alone at the low-pass hold showed
straight-edged quads from the 230 m and 71 m patch octaves, sliding through the view on the patch
drift; the same frame on a sine-free hash has none. They were also D33's blocky whitecaps beside
the C1B rocks. Consecutive frames match: no frame-to-frame flicker.

Changes in Effects/Ocean.cs's fragment stage:
- Foam forms where the swell bunches the surface: the swell loop sums the horizontal displacement's
  slope and the foam reads the Jacobian's drop below 1 in standard deviations (FOAM_JAC_SIGMA),
  gated between 1.3 and 2.3, so a whitecap grows and fades with its crest. The crest-height gate
  (SWELL_CREST, CrestSigmas) is gone.
- All foam noise is gradient noise on sea_hash: the 230/71 m patch field drifting along the wind by
  one whole period per csky_time wrap, and a 12 m breakup riding the water.
- Up close (1.4 m cells at 4 to 9 pixels) a whitecap becomes a coarse sheet with round holes,
  warped by 3.2 m noise, covering about 30 % of the crest's foam so its mean is the far foam;
  coarser than the reference's lace, by the user's ruling. Foam takes roughness 0.6 where it covers.

Far share at C1B cruise (foam alone, upper half): 2.92 levels, against 2.66 before and 3.03 before
D31. gpu_ms at the low-pass hold, 1920x1080, over --no-ocean, two quiet rounds: before +0.98 /
+0.99 ms, after +1.03 / +1.02 ms. Montages flat / before / after at the low-pass hold, 10 m over
the water, C1B cruise and the C1B coast, with 1:1 crops and frame strips; the user accepted them.
Units 6338 passed, 3 skipped; graphics-ocean-switch and graphics-water-quality pass, engine errors
clean. The Enhanced ocean goldens are re-pinned after Wave D.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main brings PLAN-bots (computer-flown bots in Deathmatch, network and local) and its review. One
conflict, docs/architecture/Launch.md's GameSession entry: main's bots wording kept, with the
ocean's FollowWaterQuality sentence. PROJECT_CONTEXT's active-plan line moves to Wave D's re-check
flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RunTests -RegenGoldens on the plan tree with D31, D32, D33 and main merged: 25 shots rendered,
exactly the four Enhanced shots the ocean appears in moved, every other shot hash-identical.
- c1-lake-enhanced 60cafb7f... -> 1e8f90cc...: D33's look ramp puts waves on the lake's open
  water; D31's no-texture sea and noise detail.
- c5-city-night-enhanced cbbd3e40... -> d36e8944...: the far waterline's ocean (D31, D32).
- c1-rocket-hit-enhanced a6fa3739... -> 0e8b7627...: the ocean past the coast.
- c1b-ocean-enhanced a1d94a8a... -> 8ba6283a...: the subject; bent swell, noise detail, Jacobian
  foam, the new shore ramps.

Each moved shot's exercises field is rewritten, with its frame sensitivity re-measured (frame N
against N+1 at 1280x720): lake 24.09 % (was 23.60), city 24.69 % (unchanged), rocket 89.72 % (was
89.65), ocean 15.48 % (was 15.68); the ocean shot is 65.9 % off the same pose at flat water
quality (was 68 %).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chapter

The user: "How can i change the parameters for the chapters myself? Could you make me an ocean lab
for the --freecam mode so that i can test it live?" Decision 16, their answers: built before
landing; the values live in a shipped per-chapter data file the game reads at load; the lab
exposes the swell, the crest bending and noise detail, the foam, and the coast ramps, tint and
roughness; "Should only be available in --freecam use Shift+F1." Decision 14's per-region seas
move into this plan through the lab, which the user tunes with. Shipped defaults are today's values,
so nothing moves until the user saves.

Also recorded: the complete battery on the plan tree with Wave D, the re-pin and main merged
(aaa6ff5, a4ebec5): units 6437 passed, 3 skipped; engine 543 passed, 2 skipped, errors clean;
goldens 25/25 hash-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…state per chapter

The user: "How can i change the parameters for the chapters myself? Could you make me an ocean lab
for the --freecam mode so that i can test it live?" Decision 16 holds their answers.

- Effects/SeaState.cs: one immutable value per tunable with today's constants as defaults, each
  with its range, step and note; Clamped() is the only way into the shader (range, NaN to default,
  sharpness x height held at 1.0, coast ramps at least 4 m wide), plus the --debug-ocean grammar.
  Swell (height, length, wind, sharpness), bending and detail (bend metres and radians, detail
  slope and drift), foam (threshold, strength, patch size, near cover), coast and colour (swell
  and look ramps, tint, near and far roughness).
- Effects/OceanShader.cs: the generator, moved out of Ocean.cs and written from a SeaState. A
  length scale re-rounds every omega to whole cycles per 3600 s wrap; detail and foam-patch drifts
  stay whole periods per wrap. Height, foam strength and roughness stay uniforms.
- Effects/OceanSeas.cs and CSVM/data/ocean_seas.json: the shipped per-chapter file, an empty entry
  per sea chapter, so every chapter draws today's ocean. A bad entry warns and takes the default.
  Save rewrites one chapter's entry, only the fields that differ, in field order.
- UI/Labs/OceanLab.cs: --freecam only on a sea chapter, Shift+F1 toggles a slider panel in four
  groups with Reset to defaults, Revert to saved and Save for the chapter (the source tree's file;
  disabled in an export). An edit rebuilds the shader only when its text changes; a graphics or
  water-quality switch rebuilds with the edited sea. The scripted twin is
  --debug-ocean=<field>:<value>,...[,open].

The default sea's shader text is byte-identical to the constants' text (OceanShaderTests: SHA-256
of levels -1 to 3, zoned and unzoned, captured from HEAD's generator before the move, and a line
fixture). Goldens 25/25 hash-identical. gpu_ms at the low-pass hold unchanged (3.416-3.429 ms
both trees, two quiet rounds). New unit tests: OceanShaderTests, SeaStateTests, OceanSeasTests,
and the --debug-ocean spec test; engine suite ocean-lab (one lab over one ocean, a length edit
recompiles with the same ocean standing, a height edit sets only wave_scale, a graphics switch
rebuilds with the edited sea, a reset restores the text, a --fly session builds no lab), with its
weight in analysis/engine-suite-weights.json. In the agent tree: units 6503 passed, 3 skipped;
engine 544 passed, 2 skipped, errors clean; goldens 25/25. Montage of defaults, a rough and a calm
sea at C1B cruise, and the panel open at cruise.

Noticed: the fold depends on sharpness x height, not on length (Q k A is wavelength-free), so the
slider note says so; the coarse detail layers drift in whole noise periods per wrap, so the drift
slider steps visibly for the long layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…othing, wording and smells

From the second code review of the branch, every clear finding, the user's ruling.

Defects:
- OceanSeas.WithEntry parses with the loader's options (comments skipped); a file it still cannot
  read throws and stays untouched, and the lab's status line says Save was refused. A Save after a
  hand-added comment used to keep only the saved chapter.
- SeaState.Written() is what Save writes: clamped, changed fields rounded to 4 decimals toward any
  bound Clamped holds, unchanged fields exact. A height at the fold limit (1.818182 at sharpness
  0.55) wrote 1.8182, past the limit, and warned on every load.
- AnimRuntime.TargetsOf runs inside NameResolver.Peek: FindAll reads its memo but neither fires
  Claimed nor writes an entry, so the later real query computes and claims as if the peek never
  ran (no unclaimed memo entry can turn it into a cache hit); PeekAnchors computes anchors with no
  claim or census. Claimed nodes before: C2/M01 1083 at waves against 1081 at flat, C1B/M03 3146
  against 3145; after, equal at both. The world merge line and the movers census are unchanged.
- The fold wording says what is bounded (sharpness x height at the base gain) and that grouped
  crests can fold sooner.

Wording: the generated shader's comment names no plan item (the byte fixture and hashes re-pinned;
the text differs only in that comment line at every level); history phrasing gone from
OceanShader, SeaState, OceanSeas, OceanMaskRaster and the Effects.md entry; plain text for the
DebugOcean summary; the suites call the hide a fragment discard. Tests: byte identity at levels
-11 and -2 as well. Smells: DetailDrifts and Load's unused path removed, one Afloat helper,
FlatWaterChapter for the suites' flat-water chapter, OceanMovers.OriginBand used directly.

Verified in the agent tree: units 6517 passed, 3 skipped; 544 engine suites pass; goldens 25/25.
The engine stage's error check is red on 20 renderer lines in net-pause-overlay after its live
graphics switch ("Uniform buffer supplied (binding: 0) size (128) is smaller than size of shader
uniform: (144)"), in shard 3's order only. The plan tree before this change reproduces them with
the same suite list, so they predate it; they are diagnosed separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the second code review's judgement calls, settled with the user one by one:
- docs/controls.md: the Shift+F1 row gives the reason it sits outside the F13 to F24 debug range
  (the lab works without the extended keypad) in place of who chose it.
- docs/cli.md: --water-quality's fallback clause gets its verb (which covers the Steam Deck,
  logged as water_source=default_linux_or_integrated_gpu).
- WaterQualitySetting.LowPowerDefault: the doc names the real cost, the wave grid on a low-power
  GPU, where it said the reflection and quoted an fps figure.
- OceanMask.IsSea(Vector3) owns the texel lookup Ocean.OverSea did off six of the mask's members.
- SceneBuilder: the base sheet's world-position line has its own oceanVertex string, placed after
  the race ghost's, so the generated text is byte-identical.

Left as they are by the user's ruling: Ocean.Covers and FollowWaterQuality (each names its
boundary's question), Ocean.Create's eight parameters (one caller). Filed: #162, registering a word
setting once instead of at every settings site.

Units 6517 passed, 3 skipped. CheckDocEntries clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rer keeps stale code

The complete battery went red on 20 engine-error lines in net-pause-overlay ("Uniform buffer
supplied (binding: 0) size (128) is smaller than size of shader uniform: (144)"). Diagnosed:
ShaderTwins.RetextInPlace rewrites an Original shader to its Enhanced text on a first live switch;
Godot 4.7 resizes the uniform block at once but defers the recompile to a material update, and the
first Enhanced frame's advanced-variant enable clears the dirty flag without rebuilding, so a
rewritten shader nobody wears that frame keeps its Original code. It reproduces before this
branch's lab and on main; no issue tracked it. The user's ruling: "check if there is already an
issue for it, if not fix on this branch".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iled it

ShaderTwins.RetextInPlace puts each shader it rewrites on a ShaderMaterial of its own (asking for
its RID so the server material exists), and Regenerate and ReleaseUnused drop the pins once two
frames have drawn. Godot runs the queued material updates before any viewport renders, so the
pin's update recompiles the dirty version from the new text before the first TAA frame's
advanced-variant enable can clear it. Before, a rewritten shader nobody wore that frame kept its
Original code, and a material put on it later failed its uniform buffer ("Uniform buffer supplied
(binding: 0) size (128) is smaller than size of shader uniform: (144)").

New suite graphics-retext-compiles: a synthetic key drawn red in Original, left unworn, switched
to Enhanced, one TAA frame, then a fresh material reads green with no new engine error line. It
fails alone with the pin disabled (6 engine errors, the clear grey).

Minimal list: 16 engine-error lines to 0. Shard-3 list (62 suites): 20 to 0. Complete battery:
units 6517 passed, 3 skipped; engine 545 passed, 2 skipped, errors clean on all six shards;
goldens 25/25 hash-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan tree's battery went red on the new suite in shard 4: 50 "free_rid can only be called from
the render thread" lines, all printed inside the rewriting Regenerate. The suite's red control
draw had left Godot compiling pipelines on worker threads; ShaderData::set_code marks the version
dirty before clear_pipelines waits for them, so a compile in flight rebuilt the version on a worker
and failed the render-thread guard. The suite now compiles the key's Original shader through its
material's RID without drawing, and draws the red control after the green read on a shader of its
own. The production pin is unchanged.

s4 prefix (40 suites): 50 lines to 0, twice. Complete battery in the agent tree: units 6517 passed,
3 skipped; engine 545 passed, 2 skipped, errors clean on all six shards; goldens 25/25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Complete battery on the plan tree at a44b9b6: units 6517 passed, 3 skipped; engine 545 passed,
2 skipped, engine errors clean on all six shards (graphics-retext-compiles PASS in s4, 17.47 s);
goldens 25/25 hash-identical. 305.8 s total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver the ocean in a harbour

From the user's re-check flight in C2: a V-shaped patch in the Hollywood harbour (g36352, g36353)
draws the flat original water above the ocean and a dark wedge lies beside it. The user's words:
"there seems to be tiles that have some part of water texture that is not detected. and the
triangular field (consisting of two nodes) throws a shadow somehow". Ruled: fix on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1, C1B, C1C, C2, C2B and C3 take the user's own sea from the lab; C5 keeps the defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved GameSession to CSVM.Launch and split it. The branch's empty-stage
arena now lives there: BuildEmptyStage keeps the arena, ObjectiveReadouts
feeds the flag marker sites from Inputs.StageArena, and
VersusDirector.WireRearmBases opens on a world or an arena. The synthetic
shell family moves to CSVM.UI.Menu.Original (main's family order forbids
Tooling naming menu types); SyntheticData.Build takes the family list and
SyntheticShell.TreeFamilies composes it. analysis/headless-limits.json takes
main's new headless lists. The new wep_30 gun leg of ordnance-impact-effects
picks its gun through PickWeapon and sits out on the synthetic tree, which
carries no gun with an impact animation.

Verified on the merged tree: build 0 errors, 0 warnings; dotnet test 6415
pass, 0 fail, 3 skip; content checks clean; tier:ci headless with
--synthetic-data over an empty root 151/0/0, with one intermittent engine
line no headless pattern covers (material_set_shader with a null material),
absent on the pre-merge head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A headless tier run intermittently prints `Parameter "material" is null`
at material_set_shader in the dummy material storage, in whichever suite
is running. Godot 4.7's dummy renderer keeps materials and meshes in RID
owners without the thread-safe flag, and under the separate render thread
the main thread allocates while the render thread initializes and frees.
A game-free GDScript loop (ShaderMaterial, get_rid, set shader, drop the
oldest past 2000) reproduced it in 2 of 2 headless runs (thousands of
lines, incl. the :250 line) and 0 of 2 under --render-thread safe. Main's
own headless log shows the same family on material_get_instance_shader_
parameters and mesh_get_surface_count. Upstream: godotengine/godot#121949,
fixed by #121958 (milestone 4.8, no 4.7 cherry-pick); #124183 reports it
still in 4.7.2 and was closed as fixed in 4.8.

The pattern covers only the dummy material and mesh storage lookups. The
readme now accepts a game-free reproduction plus the upstream issue as the
evidence for an intermittent race, which cannot appear at fixed counts.
The entry goes when the project moves to 4.8. Allowance chosen by the
author over --render-thread safe, which breaks scene-build-throw-frees'
live-object count and drops the threaded hand-offs from headless runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ee network suites join tier:ci

Two more invented airframes under the code's names, the way player_pfighter
came in (author-approved names): each a rescaled box airframe on its own
colour with the full marker rig, its own player def, dynamics, zones,
collision probes, jet flavour, engine rows at CustomPlaneBuild's registry
ids, an engine loop and a supplement fit. A custom build composes its gun
ids from calibre (StockLoadouts.GunWeaponId, wep_30..wep_70) and its pylon
rocket as wep_06, so weapons.json carries invented records under those six
code-made ids, appended after the wep_probe_* records so PickWeapon still
takes the probes. net-seats picks its arena through NetCombatSuites.Arena,
MP1 on the install as before.

tier:ci 151 -> 154: net-custom-planes (red with wep_06 renamed: both seats
pylons=-1), net-seats (red with pprobebrand's nodename broken; check-level
breaks stay green since its airframe checks compare against the loaded
record), net-bot-respawn (flies the pfighter/fbrand pair; red the same way).
Each passed three runs by name with --synthetic-data and still SKIPs
without it. Left off: net-ai-world, net-ai-voice, net-lobby-deathmatch-mp1
(C1 gamez, not an airframe); versus-local-bot, versus-local-bot-graze,
net-bot-yield, graze-bounce-bot (shipped airframes the tree lacks).

Verified (agent, Windows headless, empty root): build 0/0, dotnet test
6416 pass 0 fail 3 skip, content checks clean, tier:ci --synthetic-data
154/0/0; full catalog with the switch 183/22/343 against a base of
180/24/344 (only the three suites move), without it 63/1/484 identical
suite by suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ice and one-off suites join tier:ci

Two families join the synthetic tree. `mission` writes one invented
mission scope, C1/PROBE1/zrdr (a name no shipped mission carries):
weather.json with two zones whose SUNLIGHT_ORIENTATION differ, fog and
sunlight levels under a 2200 to 2600 m cloud band with an 80 m core, and a
16-entry free-for-all net.json block. A folder rather than records the
empty stage carries, as C22 chose, because the weather suites hand
WeatherRig a zrdr path and the net-table suite's subject is reading the
file. `voice` writes zrdr/voice.json (three invented accent rows over VO
ids 2 and 26, the ids the code's PilotVoices table gives Jack and Gruff
Male) and extends the sounds family with 52 voice definitions, 38
snd_<FAMILY>-A_id<N>_random groups and their generated WAVs.

Joined (each three runs by name with --synthetic-data, red on one broken
input, SKIP as before without the switch): sun-orientation and
cockpit-sun-bearing (ZONE2's bearing equal to ZONE1's),
weather-cockpit-whiteout (CLOUD_COVER removed),
danger-zone-photograph-fill (ZONE1 SUNLIGHT_AMBIENT 1.0), voice-runtime
(snd_id2_WA-Enemy-3H renamed), versus-spawn-net-table (block cut to 15),
instant-action (the probe's AI def renamed), tex-dropin (empty texture
manifest), versus-spawn-rotation (ring radius 0).

Retargeted, the old literal still running under !SyntheticData:
sun-orientation reads its bearings off the zone record (AuthoredBearing)
and on PROBE1 requires the two zones to differ; cockpit-sun-bearing reads
its three beams off the record; weather-cockpit-whiteout's camera sits at
the record's CloudBandCentre instead of 1047 m; voice-runtime takes
Count > 0 for the 35 accent rows, the first one-pilot accent whose pilot
authors a DI-LowDmg group for accent 12 / VO id 2, decoded == subset for
>= 70, and another pilot's TA-SucShk clip for snd_id26_TA-SucShk-A;
instant-action flies ctx.PlaneName for the Warhawk, Fury and Brigand nodes
and reads the airframe's own AiAttackRange/AiReturnRange for 2000/1200,
plus a new check on every tree that they differ from the 10000 m actor
volume; tex-dropin flattens every name in the archive's manifest instead of
DropInSamples (alpha checks run on the install only);
versus-spawn-rotation uses the empty stage's ring when the root has no
C1/IA1; versus-spawn-net-table reads PROBE1. ai-voice gained the
RequireZrdrEntry("ai.json") and RequirePlane gates its turret leg needs.

Left off: net-player-voice (the empty stage builds no WorldSounds, so no
combat voice), ai-voice (the ai.json turret table), net-ai-voice and
net-ai-world (C1 world, MP1 destructible), instant-action-wave-net-seat
(display names resolve only to shipped airframes), instant-action-stunt-
summary and menu-original-wrapup (a chapter gamez with Danger Zone nodes),
clutter-cells and terrain-pick-export (C3/C4 worlds),
flight-mouse-scheme-live and pause-preferences-live-options
(player_bhawk), the campaign family.

Verified (agent, Windows headless, empty root, before the airframes item
was beside it): build 0/0, dotnet test 6417/0/3, content checks clean,
tier:ci --synthetic-data 160/0/0; full catalog with the switch 189/24/335
against a 180/24/344 base (only the nine move), without it 63/1/484
unchanged suite by suite. A one-process no-switch full run crashed at exit
in GodotObject.Finalize after writing its report (the teardown finalizer
crash analysis/bl-053-dense-rank/FINDINGS.md records on an unmodified
build). Merged tree: dotnet test 6418/0/3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…join tier:ci

Tooling/SyntheticEffects.cs is the `effects` family: a template gamez
generated from fixtures/synthetic/probe_effects/templates.json, a compiled
anim archive split out of cam_anim.json (compiled because the reader form
drops call offsets, start times and ranged launches, which the panel, burst
and debris suites need), and three reader files under zrdr/ (two
PERSIST_LOG destructibles, fierypuffer, smokepuffer/firepuffer). Records
carry the names the effect catalogue or a suite plays over invented
content, and every motion drives a child node, since a placement moves the
root. EffectStageSuiteHelper.WithAnimSource and WithAnimWorld hand a suite
the chapter world exactly as before on an extraction and the invented
records under the switch; TestContext.RunsChapterWorld is gone. weapons.json
gains wep_probe_gunhit (both IMPACT rows 3040slug_gunhit, among the probes
and before the custom-plane ids) and a FLYOUT body for the torpedo;
messages.json its name (id 76); sounds.json snd_propstart.

Joined (three runs by name with the switch, red on one broken input):
effect-template-mesh (upper-ring call renamed), effect-pool-spawn-pose
(debris def renamed), effect-pool-reset (ring reset_state removed),
wait-for-completion (flag dropped), emitter-host-deactivation (sparkout3
switches another part off), first-person-condition (Else removed),
damage-template-pool (pdpanel4 tears at pdp5), damage-template-freed-anchor
(pdpanel4 calls nothing), repeat-call-slots (-2 m offsets zeroed),
puffer-idle-process-gate (fierypuffer lives 3 to 4 s), start-state-swap-pool
(deaths swap no roles), carried-state-silent (one destructible loses
PERSIST_LOG), nodelab-visibility (no RESET_STATE), spawn-props-silent
(startprops sounds nothing). Three tier members now run their chapter-world
halves on the invented source: impact-orientation's upper-ring placement,
ordnance-impact-effects' gun leg and ballflare half, shootable-flyout's
real-hit-ray half, each red on its own input.

Retargeted, the install keeping the literal: spawn-props-silent flies the
root's plane under the switch (player_warhawk on an extraction);
spawn-props-by-def RequirePlanes the autogyro and Warhawk; shootable-flyout
reads the torpedo's own pool for 10f (already pinned to 10 off the
synthetic tree); ordnance-impact-effects requires the gun on both trees.

Left off: trail-world-anchor and clutter-cells (headlessOnly; the first now
runs and FAILs headless with the switch where it skipped), puffer-modes and
puffer-blend-flag (shipped readers and render flags), flyout-rack-pose
(bucket E), spawn-props-by-def (player_autogyro), sonic-ground-ring,
fbfx-flash, callback-events (authored geometry, colours, destroy defs).

Verified (agent, Windows headless, empty root, before the other two items
were beside it): tier:ci --synthetic-data 165/0/0; full catalog with the
switch 194/25/329 against 180/24/344 (the fourteen SKIP to PASS,
trail-world-anchor SKIP to FAIL), without it 63/1/484 unchanged suite by
suite. Merged tree: dotnet test 6422/0/3, content checks clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le, and nineteen suites join tier:ci

The author approved invented stand-ins under player_bhawk, player_autogyro,
player_fury, player_peacemaker, player_warhawk and player_kestrel. Each is
the fighter's invented subtree rescaled on its own colour with its own
player def, dynamics, zones, probes, engine rows, sound, jet flavour and
fit. Where the code keys on what an airframe is, the stand-in carries that
property over invented numbers: is_autogyro and an agyro_rotors spin def
(and a rotor1 blade) on the gyro, a thirdp rear mount on the kestrel (and a
dorsal one on the fbrand, ai-voice's turret carrier), an AI injure ladder
repeating random_remote_damage on the fury. The variants suites name come
too: wbloodhawk, bhatwarhawk, the Cabbie's autogyro flavour. New:
zrdr/ai.json (two carried entries and one dormant standalone under the
aagun** pattern) and wep_probe_zeptorp (DAMAGES_ZEPPELIN), last in
weapons.json. Some values sit where a suite's behaviour is reachable (the
fury's zone armour below the graze contact pair, the gyro's rudder torque,
the wbloodhawk biases); none is copied from shipped data, and the kestrel
turret's arc was moved off centres that matched the shipped 180/35.

tier:ci 177 -> 196, each three runs by name with the switch, red on one
broken input, SKIP without it: ai-target-rescore (wbloodhawk kind_of),
ai-voice (fbrand mount node renamed), aircraft-first-targeting
(struct_bias 0), carried-turrets and ranked-pool-carried-turret-dedup (rear
yaw ring renamed), damage-stage-slots (smoke threshold below the walk),
engine-note (rattle sound absent), flight-mouse-capture,
flight-mouse-scheme-live, pause-preferences-live-options (pprobehawk
nodename), flight-mouse-scheme (is_autogyro dropped),
gasbag-ordnance-gate and warhawk-torpedo-run (torpedo band inside its
minimum range), menu-free-flight-journey (pprobegyro nodename),
spawn-props-by-def (the Cabbie names no spin def),
turret-death-effect-world-anchor, turret-own-mount-sightline,
turret-grouped-site-sightline (standalone entry's weapon unresolved),
versus-local-bot-graze (fury nose armour above the contact pair).

Retargeted, the literal still running under !SyntheticData: engine-note's
rattle gate reads the rattle block's speed_range floor (== 1) and its sound
any library definition (snd_planeshake), probes at gate x 0.999/1/1.2;
damage-stage-slots counts 7/6/1/4/0 off the loaded ladder; carried-turrets
takes >= 1 carried entry (16), the def's rest yaw/pitch (180/35) and the
nearer end stop (105); ai-voice takes the table's first one-voice accent
on the synthetic tree (12 / VO id 2 on the install). graze-bounce-bot,
net-bot-yield and campaign-wingman-fit gate on all eleven stock airframes
under the switch (Random bots draw from every one), so they SKIP naming
player_balmoral.

Left off: player_balmoral and player_brigand are not approved, so the
eleven-airframe suites (ai-plane-defs, which also pins shipped pools,
airframe-collider-hit-rate, airframe-hull-coverage, loadout-bind,
loadout-forrig, graze-bounce-bot, net-bot-yield, campaign-wingman-fit)
still skip; versus-local-bot (its menu-launch leg opens C1); chapter-world
suites (ground-shadow, instant-action-wave-net-seat, world-turrets,
turret-self-fire, turret-hull-blocks-own-fire, turret-structure-targets,
turret-death-fire-follows-hull, target-pool, turret-vessel-targets,
turret-gun-voices); wingman-station.

Verified (agent, Windows headless, empty root): tier:ci --synthetic-data
196/0/0, its 39 engine lines all excused; full catalog with the switch
206/23/319 -> 225/20/303 (18 SKIP to PASS, versus-local-bot-graze FAIL to
PASS, graze-bounce-bot and net-bot-yield FAIL to SKIP), without it
63/1/484 identical suite by suite; build 0/0; dotnet test 6424/0/3;
content checks clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with the ocean lifted to their plane

C2's Hollywood harbour has two wtr00000 base-sheet triangles (g36352, g36353) that rise from sea level
to 1.03 m at the quay corner. The hide cleared only below 0.25 m, so the flat sheet with its sun streak
drew over the ocean as a V. The mask counted the triangles as surf water, which drew calm in the mean
tint as a dark wedge. The wedge was not a shadow: it stays with the sun shadow, SSAO and SSR off.

A base-sheet triangle whose foot is at sea level and whose top lies below 4 m is now a ramp:
- the mask marks its texels open sea (red byte 254);
- the hide clears the sheet up to 4 m over a ramp texel;
- an R8 lift texture raises the ocean grid to the ramp's plane, so no hole opens at its rim;
- ramps leave the sheet's mean tint and tile size to the sea-level sheet.
Only waves mode changes. Flat water and Original keep every triangle, and colliders are untouched.

Scan with the user's sea: C2 has 2 ramps, C5 has 8 (the water under the steinmann ship), and C3's
13.17 m chute stays refused by height. The ocean-harbour-ramp suite covers C2 and C5. With the ramp
rule disabled it fails in both (1.018 m and 3.012 m below a ramp); with the rule it passes (at most
0.004 m below). The battery is green and moves no golden.

Hiding alone was not enough: the hide-only montage column left the dark V and a light line at the
quay's base. The user chose the open-sea version from the three-column montage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r's sea moved are re-pinned

The user's sea (9b89a82) broke tests that assumed the shipped ocean_seas.json holds the defaults.
- OceanSeasTests: save and round-trip run against an all-defaults fixture; the shipped file must read
  clean, name every sea chapter in order, and survive saving what was loaded byte for byte. A Save of
  one chapter into the shipped file leaves the others as loaded.
- ocean-lab: the lab starts on the shipped C1B sea; a reset draws the defaults, and RevertToSaved
  brings back the shader text the session opened with.

Goldens moved by the sea alone (no Original shot moved, every other shot hash-identical):
- c1-lake-enhanced 1e8f90cc -> b7e66c00, 9.98 % of pixels
- c1-rocket-hit-enhanced 0e8b7627 -> e2646781, 2.12 %
- c1b-ocean-enhanced 8ba6283a -> 39600594, 50.61 %
The two freecam shots rendered with the default sea fields on the E43 tree reproduce the old hashes.
c1b's exercises drops its "65.9 % off the flat sea" and frame-sensitivity figures: both moved with
the sea and the method that produced them could not be reproduced.

Complete battery: units 6520 passed, 3 skipped; engine 546 passed, 2 skipped, engine errors clean on
all six shards; goldens 25/25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E41: the user tuned and saved their sea per chapter in the lab (9b89a82); the tests now hold any
shipped sea and the three Enhanced goldens it moved are re-pinned (81fc9ec).
E43: the user chose the open-sea version from the before / hide-only / after montage in their sea.
Complete battery on the plan tree: units 6520 passed, 3 skipped; engine 546 passed, 2 skipped,
engine errors clean on all six shards; goldens 25/25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- analysis/engine-suite-weights.json: main's re-measured weights, plus the plan's five new suites
  (graphics-ocean-switch, graphics-retext-compiles, graphics-water-quality, ocean-harbour-ramp,
  ocean-lab) at their measured weights.
- docs/architecture/Utils.md: OptionsStore keeps both the water quality field and main's `.bad` move
  of a malformed file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every chapter with a sea

Every item landed and verified; the plan file is deleted as its banner asks, and
PROJECT_CONTEXT.md's "Current status" names no active plan. Each item's record is its own commit
message (git log --grep=PLAN-enhanced-ocean, and the item ids).

What the plan delivered:
- Wave A: the C1B prototype as a production module; a Water Quality setting (flat or waves) that
  follows a live switch, waves on the desktop and flat on the Deck and Linux; the live switch and
  the motion look verified.
- Wave B: the low-altitude SSR cost within budget; the Deck measured and budgeted; the mask bake
  cached; no secondary viewport draws the ocean grid it does not need.
- Wave C: every chapter with a sea at y = 0 (C1, C1B, C1C, C2, C2B, C3, C5); calm zones for every
  hull, wake and mission-animated boat; the swell's regularity from altitude; the
  c1b-ocean-enhanced golden; the ocean matching the flat sheet at the shore and in fog; no hole
  where a mission shows a node hidden at the bake.
- Wave D: the swell's wave lattice broken up, foam that reads as foam up close, a gradual fade to
  flat water at the coast.
- Wave E: the ocean lab (Shift+F1 in --freecam) and the user's shipped sea per chapter; a shader
  rewritten in place for Enhanced compiles before Enhanced's first frame; a harbour's raised water
  triangles are open sea under the ocean.

Original mode is pixel-identical throughout and the colliders at y = 0 never moved. Follow-ups are
GitHub issues: #160 (procedural ship wakes), #164 (a live shader rewrite racing Godot's background
pipeline compiles) and #165 (the node lab's branch expand during a mouse selection).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…store

LauncherContext gains an optional Decode store. GameSession hands it to the
archive open, the world stage and the planes gamez load, so a suite's
sessions decode a chapter once instead of once per session. A launch leaves
it null and decodes afresh as before.

The five suite files that build whole sessions (net combat, enet session,
net session, net world, graphics switch) pass TestContext.Decode in.

Full gate (owner-35628), 543/543 engine suites, units and goldens pass.
Decode cache hits 338 -> 738, misses 146 -> 166, archive decode seconds
summed over the six shards 104.3 -> 66.4 against owner-39308. Wall times
from that run are not comparable: another session's test runs loaded the
machine, and untouched suites ran 44% slower.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame extraction

tier:ci (SuiteCatalog.CiTier) holds 196 suites, each passing headless on
the --synthetic-data tree with CSVM_DATA_ROOT empty, where a skip counts as
a fail. The invented tree covers textures, the probe plane and stand-ins
under fifteen airframe names, armament, sounds, voice, one mission, the
effects records and the Original shell. The plan file is removed and
PROJECT_CONTEXT's active plan is none.

Left off the tier, for a later plan:
- player_balmoral and player_brigand have no approved stand-ins, so the
  eleven-airframe suites skip: ai-plane-defs, airframe-collider-hit-rate,
  airframe-hull-coverage, loadout-bind, loadout-forrig, graze-bounce-bot,
  net-bot-yield, campaign-wingman-fit.
- The campaign family (cm_sequence.json), about 69 suites.
- net-player-voice: the empty stage has no WorldSounds.
- versus-local-bot (its menu-launch leg opens C1), wingman-station,
  menu-original-hangar, menu-original-instant-action.
- Chapter-world suites: ground-shadow, the turret world suites,
  target-pool and others.
- Headless-only candidates: puffer-smoke-sun, wing-flare-pose,
  muzzle-flash-rides-muzzle, trail-world-anchor.
- scene-build-throw-frees depends on suite order.
- FAIL with the switch across the whole catalog, before and after this
  plan: menu-join-board-bots, menu-original-lobby-bots, net-bot-rearm,
  net-crash-respawn, stunt-race-boards.
- Still owed: a GitHub run turned red by a broken ci suite, and a
  RunCiSuites.ps1 run under pwsh 7 on Windows (only CI's Linux pwsh has run
  it).

The headless null-material line is excused until Godot 4.8
(godotengine/godot#121949, fixed by #121958).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CSVM.Tests/fixtures/README.md
@Laeresh
Laeresh merged commit 01d94ff into main Oct 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants