Two Layer-1 generation fixes surfaced by the per-layer atlas viewer:
- Layer1Output now carries grid_w/grid_h (the downsampled working-grid the
positions live in). The client maps overlays from these, so the scale is
correct for any source heightmap resolution rather than assuming the texture
size — fixes overlays projecting at half scale into a corner.
- Drainage basin boundaries are traced as ordered, non-self-crossing contours
via Moore-neighbour tracing instead of an angle-from-centroid sort. The sort
produced star-shaped, self-crossing polygons for concave basins that rendered
as straight chords across the map.
Golden (cascade_layer1.json) and the cross-language atlas_response_ready
fixture regenerated. 100 atlas lib tests + the new tracer test pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ad-hoc capture scenario that overlays the ImplantPending "generating" indicator
on the booted scene for visual sign-off — `tests/run-visual --screenshot
implant_pending`. No golden committed; this is an inspection scenario, not a
regression gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
visual_capture.gd runs via `godot -s`, where the `class_name` registry isn't
populated — so the bare `Protocol.decode_snapshot()` reference failed to
compile, breaking ALL --screenshot/--movie/golden captures (not just the
replay scenarios that use it). Instantiate the script and call the static
decoder on the instance (then free), matching the file's existing -s-mode
load() workaround for VisualScenarios.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A reusable implant-layer busy indicator (Araminta's spec): an animated
bracket sweep `[ >>>>···· ]` with a caption, themed entirely via ImplantTheme.
The Atlas shows it centered over the canvas while the Layer-1 proxy is Pending
and hides it on Ready — so cache hits never flash it and serverless mode never
shows it.
Layer separation (per direction): ImplantPending belongs to the diegetic
implant UI only. The global, non-diegetic UI layer must use its own busy
indicator with its own visuals — sharing the sweep logic is fine, resting on
this theme/these glyphs is not. Documented in the component header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
show_body() now requests the body's Layer-1 cascade output via
SimBridge.request_atlas_layers(); the response feeds set_generation_layer1().
The proxy returns Pending on a cache miss and generates in the background
(D-225), so the viewer re-requests every 0.5s (20-retry ceiling) until Ready,
guarding against stale responses by body_id. Serverless/test mode is a no-op
(overlays stay empty). Connected in _ready, torn down in _exit_tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Formatting-only cleanup of the #969/#960-A codec + bridge scripts to match
gdformat output (the pre-push gdformat check is advisory; these landed
un-formatted). No behavior change — 70/70 protocol tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render the proxied Layer1Output (D-225) as toggleable Atlas overlays. The
viewer stores the decoded Layer1Output via set_generation_layer1() and the
marker overlay draws three new layers gated by their visibility flags:
RVR river_cells as dots, confluences as small circles, mouths as
double-ring sea-terminus markers
BAS drainage-basin boundaries as thin closed polylines + faint fill
ATR geographic attractors — shape by attractor_type (Araminta's 7-shape
vocabulary), color by sub_biome, size by strength; <0.15 culled
Layer-1 positions are [row, col] in the 512x256 working grid, which matches
the viewer's grid_to_canvas transform, so they project directly onto the
heightmap. OVERLAY_DEFS gains gen_l1_rivers/basins/attractors (toggle group)
so AtlasOverlayBar auto-exposes them as toggle buttons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client transport half of the layer-stream protocol.
- SimBridge.request_atlas_layers(body_id) sends an AtlasLayerRequest frame
(live mode only; no-op in test mode); responses arrive via a new
atlas_layers_received signal.
- receive_bytes now decodes each frame ONCE via Protocol.decode_inbound and
branches by shape (snapshot vs atlas response) — avoids double-decoding the
20 Hz snapshot path. decode_snapshot is split into decode_raw +
_decode_snapshot_from_raw (public decode_snapshot unchanged, so the 70 protocol
tests stay the regression guard); decode_inbound returns {kind, value}.
70/70 protocol tests pass, including the new decode_inbound classifier test.
(Pre-existing client-suite failures in server-dependent e2e/roundtrip + unrelated
audio/fog/dialogue suites are unchanged — verified identical at baseline.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client half of the layer-stream protocol (codec only; transport wiring next).
- protocol.gd: encode_atlas_layer_request (bare {body_id, up_to} map so the
server demux routes it to the proxy, not the PlayerInput array) and
decode_atlas_layer_response (-> {body_id, status, error, layer1}; returns null
for non-atlas frames, e.g. a snapshot, so receive_bytes can disambiguate).
- gen_fixtures.rs: cross-language fixtures (atlas_response_ready/pending/
not_found) from real rmp_serde output, matching the test_protocol.gd pattern.
- test_protocol.gd: 5 tests decode the fixtures + verify a snapshot is not
mistaken for a response + the request encodes to the right shape. 68/68 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the server-side layer-stream loop. serve_atlas_requests (PreInput) drains
the AtlasRequestBuffer and runs each through handle_atlas_request (cache hit ->
Ready; miss -> resolve via BodySourceResolver + enqueue an Immediate AnalyzeBody
-> Pending), buffering AtlasLayerResponses. send_atlas_responses (PostSnapshot)
flushes them to the client. main.rs wires BodySourceResolverResource (base root
= systems.db's 3rd ancestor; mod roots layer on later). Misses flow through the
#968 background tier and a re-request hits the now-warm cache.
Full path now live server-side: client request -> receive() demux -> serve ->
proxy -> (cache | queue+cascade) -> response -> client. The client half (send
request, decode response, render overlays) is #960.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the fixed-type SimBridge::receive_inputs() with a tagged
receive() -> Option<Inbound>, where Inbound is Inputs(Vec<PlayerInput>) or
AtlasRequest(AtlasLayerRequest). A shared decode_inbound() demuxes a frame by
shape (msgpack array = inputs, map = atlas request) — additive, no wire change
to existing input/snapshot frames. Adds send_atlas_response() to the trait
(both TcpBridge + LocalBridge impls). receive_bridge_inputs routes inputs to
the InputQueue as before; atlas requests to a new AtlasRequestBuffer (drained
by the serve system next). Integration tests (bridge_tcp/bridge_ipc) updated to
the tagged receive(); a demux unit test covers all three branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handle_atlas_request: cache hit -> serialize Layer1Output, reply Ready; miss ->
resolve the source heightmap (BodySourceResolver) + enqueue an Immediate
AnalyzeBody on the #968 background queue, reply Pending (client re-requests; the
drain system populates the cache so a later request hits); unknown/no-terrain ->
NotFound. Adds the AtlasLayerRequest/Response/Status wire types (+ Serialize on
CascadeLayer). Pure handler; the bridge routing is the proxy's other half.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First piece of the layer-stream proxy. Resolves body_id -> source heightmap.png
read-only from systems.db (terrain_reference, mirroring CultureResolver's
pattern), searching roots mod-first over the base install. Explicit errors
(UnknownBody / NoTerrainReference / SourceMissing). v1 callers pass the base
root only; the search-order logic is proven with synthetic mod roots so the
mod-first seam is ready (Q-099 covers registering a mod's new bodies).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A multi-layer debug/review harness that reuses the real client UI so a human and
an automated agent can inspect deterministic server-computed state by attaching
to the live (auto-pausing) server. Supersedes the offline file-dump idea from
the D-225 discussion — the review tool is the production tool, no divergence,
no stale dumps.
Stack (bottom-up): live-pause substrate (TickRate::Paused + paused-allowlist,
triggered by gameplay_occluded; freezes world phases, keeps the bridge/gen-drain
alive) -> layer-stream proxy (D-225) -> human-visual viewer (#960, additive
overlays; shape=attractor type, color=sub-biome) -> agent channel
(AtlasAgentInterface: JSON observe + named-intent act, headless) -> interactive
capture (reuses the existing tests/run-visual primitive; complements, does not
supersede, the visual-golden regression role).
Consumers: Layer-1 geography now; economics + save-state inspection adopt the
pattern in their phases. Designed with Tyre (channel/pause/headless) + Araminta
(overlay encoding + affordance UX). Rides existing seams — a naming-and-contract
exercise, not a new subsystem.
Tickets: #970 auto-pause, #969 proxy, #960 viewer, #971 agent channel,
#972 capture (build order bottom-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The D-206 queue (#924) and D-203 cache (#917) were built but never connected to
the running app — the whole background tier was inert. This closes the loop:
submit -> Rayon -> cascade -> completion -> drain -> cache.
- gen_queue.rs: GenWorkItem::AnalyzeBody now carries enqueuer-resolved inputs
{ body_id, heightmap_path, sea_level, body_seed: SeedChain } (D-225 boundary —
run_work_item stays pure compute, no path/DB resolution). run_work_item runs
the real cascade: load heightmap.png -> downsample to GRID_W×GRID_H working
grid (D-202) -> run_layer1 -> BodyWorldState; load failure -> Failed.
GenCompletion::BodyAnalyzed carries the computed BodyWorldState.
- cascade.rs: CascadeSnapshot::into_body_world_state() conversion.
- plugin.rs (new): GenerationPlugin registers GenerationQueue +
BodyWorldStateCache and adds a PreInput drain system that inserts BodyAnalyzed
states into the cache (off the Rayon workers — a cheap channel drain, never
the ~45ms cascade). Wired into both the production and test app setups.
Tests run the real cascade on a tiny temp heightmap PNG (no committed fixture);
the plugin test proves the full submit->...->cache loop. The proxy (#969) is the
production submitter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves how per-body cascade layer data reaches the Godot Atlas viewer (#960),
on the lead's mod-first directive + Tyre's design pass:
- No bake (privileges first-party content + ~100MB install bloat). A server-side
layer-stream proxy computes on demand from moddable source files and streams
Layer1Output to the client.
- Transport: additive message tag on the existing IPC stream (no second socket);
single framed MessagePack response; raster stays a disk load.
- Mod-first BodySourceResolver: mod dirs override base install; heightmap.png is
the sole source of truth.
- Cache via the D-206 background queue on miss (never blocks the tick thread,
D-203); eviction -> recompute (deterministic, ~45ms).
- Whole Layer1Output per response; client composites additive overlays.
Q-098 resolved against its own premise (no durable store needed — recompute on
eviction is fine for a viewer). Q-099 spun off: mods adding new bodies need
terrain_reference rows in the binary systems.db (D-189), out of scope for #960.
Implementation decomposed into #968 (activate AnalyzeBody cascade run + cache
populate — the long pole) -> #969 (proxy + bridge protocol) -> #960 (viewer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Pin the Body domain id scheme: body_id(String) -> FNV-1a(64) -> derive(Body, …),
via SeedChain::for_body. Closes the gap where "keyed by body StableId" named a
numeric id that doesn't exist (bodies are strings).
- Add for_body to the contract; document the stability guards (SeedDomain
#[repr(u64)] + pin test, AttractorType #[repr(u8)]).
- Note EntityRng keeps its domainless combine (re-expressing as derive(Npc, …)
is a stream-changing migration, deferred).
- Correct the implementation note: golden is JSON (matching golden_suite.rs),
not msgpack; run at 256x128 for a non-empty river network. Fix the stale
skeleton_gen.rs:248 line reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the Hoshe (QA) + Tyre (architecture) review of the SeedChain/cascade
work:
- Golden was pinning an empty river network (128x64 produced 0 river cells).
Bumped to 256x128, where GJ1c yields a real network (93 river cells, 19
mouths) — Layer 1's rivers are now actually guarded, not just attractors.
- SeedChain::for_body(world_seed, body_id) + fnv1a_64: the single canonical
body_id(String) -> u64 path (FNV-1a, the repo convention), so callers can't
derive divergent worlds from the same seed via different ad-hoc hashes. The
golden now uses it.
- Stability guards: seed_domain_discriminants_are_pinned test (CI fails if a
SeedDomain tag is renumbered); AttractorType gains #[repr(u8)] + explicit
discriminants (it's cast as a sort key in features.rs).
- Tests: SeedChain::root(0) non-degenerate; run_cascade error path (missing
file -> Err, not panic).
- Comments: clarified the id=0 derivations (sibling separation is caller-side
via the per-district/quarter chain; #957 threads the index), tightened the
all_district_types reachability comment (it pins the seed-0 sequence, not a
probabilistic claim), and noted the golden's WORLD_SEED is cosmetic at
Layers 0-1 + the x86_64 f32 capture caveat.
Deferred with reason: the run_cascade -> CascadeInputs struct refactor (Tyre)
is left for #954 — designing Layer-2's context shape now would be later-phase
detail, and there's a single caller to migrate then. EntityRng keeps its
domainless combine (migrating is stream-changing) — noted in D-224.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end determinism guard. cascade_golden.rs pins two artifacts for a real
committed body heightmap (GJ1c) in one diffable JSON golden
(server/tests/golden/cascade_layer1.json):
- Layer 0: SHA-256 of the source heightmap.png bytes (flips if the Python
heightmap generator or the file changes)
- Layer 1: the serialized Layer1Output of run_cascade on a 128x64 downsample
(flips if the Rust drainage/feature/sub-biome code changes)
JSON (not the msgpack discussed in refinement) to match the existing
golden_suite.rs convention and stay diffable — a failure shows what drifted.
UPDATE_GOLDEN=1 regenerates; wired into `make golden-update`.
Adds Serialize/Deserialize to RiverNetwork/DrainageBasin/Layer1Output and a
sha2 dev-dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
server/src/atlas/cascade.rs — run_cascade(body_seed, body_id, path,
default_sea_level, up_to) loads Layer 0 (heightmap.png) and runs every layer up
to the requested CascadeLayer, returning a CascadeSnapshot. The pure core
run_cascade_from_heightmap orchestrates the layers without file I/O (testable);
run_cascade is the thin path-loading wrapper.
CascadeSnapshot is extensible — each layer's artifact is an Option that becomes
Some once it runs (Layer 0 heightmap always present, Layer 1 topography next;
#954+ append their fields). The carried SeedChain is unused by the RNG-free
Layers 0-1 and feeds the RNG-using layers later (D-224). CascadeLayer is an
append-only ordered enum.
Tests: layer-gating, same-heightmap determinism, layer ordering. The golden-seed
regression fixture (the #952 deliverable) builds on this next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
generate_skeleton / compute_district_mix / derive_layout_mode /
organic_placements / derive_reservations now take a SeedChain instead of a
bare seed: u64. The two ad-hoc wrapping_add pre-mixing hacks are replaced with
real domain separation:
- district-type allocation draws from chain.derive(SeedDomain::Layer4Quarter, 0)
- organic block placement draws from chain.derive(SeedDomain::Block, 0)
DistrictSkeleton.seed now records chain.seed(); test call sites pass
SeedChain::root(N); stale seed-param docs updated.
Re-tuned all_district_types_can_appear: the seed-stream change exposed it as
latently fragile — every type has a clamped weight >= 1 (reachable), but 50
weighted draws can miss a low-weight type (Administrative) depending on the
sequence; the old seed got lucky. Raised the draw count to 500 so it tests
genuine reachability rather than a lucky sequence. Determinism-affecting by
design — this is the RNG-using layer D-224 flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New top-level seed module that owns every deterministic-RNG primitive:
- splitmix64 — the one canonical mixer (was duplicated as a private fn in
simulation/rng.rs; EntityRng now imports the shared one, no behavior change)
- AtlasRng — moved here from atlas/rng.rs (it is a generation-RNG primitive,
not atlas-specific); atlas now depends on seed, not the reverse
- SeedDomain — append-only domain tags (Body/Layer1Topography/Layer3Settlement/
Layer4Quarter/Block/Npc) for collision-proof per-domain seed separation
- SeedChain — root(world_seed) → derive(domain, id) → atlas_rng()/seed(), per
the D-224 formula splitmix64(self ^ splitmix64(domain)) ^ splitmix64(id)
Structural move only — SeedChain is not yet threaded through the cascade
callers (skeleton_gen still uses ad-hoc wrapping_add pre-mixing); that is the
next step. Unit tests cover the splitmix64 known-vector, avalanche,
determinism, domain/id separation, and chain composition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single typed seed-derivation chain (server/src/seed.rs) descending from the
master world seed via domain-separated splitmix64 mixing — the only sanctioned
way to derive a child seed. Promotes the existing splitmix64 (EntityRng's
mixer) to a shared pub(crate) function and removes ad-hoc wrapping_add
pre-mixing. Pins the derivation formula (load-bearing) and documents the
scope of effect: heightmaps (Python Layer 0) and Layer 1 (RNG-free) are
unaffected; only RNG-using layers (skeleton_gen, future settlement) change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre-push clippy invocation ran without --all-targets, so it only checked
lib + bins — test and example targets were never clippy-linted, which is how
the cfg(test)/test-target debt cleared in the previous commit accumulated
unflagged. Add --all-targets now that the debt is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manual clippy-1.93 fixes that the prior machine-applicable sweep couldn't auto-
apply, all in cfg(test) modules and tests/ targets (invisible to the lib-only
pre-push clippy, hence accumulated unflagged):
- disallowed_types HashSet/HashMap → BTreeSet/BTreeMap (determinism rule):
shadowcast_bench.rs (×8, (i32,i32) keys), mood.rs, sound.rs. SoundEventKind
gains a PartialOrd/Ord derive (fieldless Copy enum) so it is BTree-usable.
- field_reassign_with_default → struct-init: disclosure.rs, monologue.rs (×2),
save_io.rs (keeps `mut` for the deliberate last-write-wins overwrite).
- assertions_on_constants on the EAVESDROP_THRESHOLD invariant → compile-time
`const _: () = assert!(...)`: listening.rs, cross_room_transitions.rs. This is
stronger than the runtime assert and needs no #[allow].
- approx_constant: settings/types.rs round-trip literal 3.14 → 2.5 (the value is
arbitrary test data, never meant to be PI — change avoids both the lint and a
suppression).
- drop_non_drop: vision.rs early Mut<WalkabilityMap> release → scoped block.
- unnecessary_get_then_check → contains_key: information_boundaries.rs (×3).
- cloned_ref_to_slice_refs → std::slice::from_ref: triangle_validation.rs.
- unused_must_use: input.rs dropped the unused .id() on a spawn.
cargo clippy --all-targets -- -D warnings is clean; cargo test green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Focused Rust dependency-maintenance pass from the 2026-05-23 security/freshness
review. No CVEs; one advisory cleared and one deprecated crate replaced.
- rand 0.9.2 → 0.9.4 (lockfile): clears RUSTSEC-2026-0097 (unsound with a
custom logger using rand::rng()). Semver-compatible; rand 0.10 is a separate
major.
- Compatible-update sweep: ~90 lockfile-only patch/minor bumps (bevy 0.18.0→
0.18.1, clap 4.5→4.6, rayon 1.11→1.12, pathfinding 4.14→4.15, uuid 1.20→1.23,
zerocopy, serde_json, tracing-subscriber, etc.). cargo test green.
- serde_yaml 0.9 (deprecated/archived upstream) → serde_norway 0.9, an actively
maintained drop-in fork. In the server it is test-only (poi.rs round-trip,
trait_modifiers.rs fixture, tests/news_ticker.rs) so it moves to
dev-dependencies; line-previewer parses dialogue/monologue pool YAML at
runtime, so it keeps it as a normal dependency. API is identical (from_str/
to_string).
news_ticker.rs also picks up its share of the #967 clippy sweep (HashSet/HashMap
→ BTree, doc-list indent) since it is the same file as the serde rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cargo clippy --fix on files the new clippy (1.93) flags: unused imports
(name_index, storyteller), manual_range_contains (block_irregularity),
length-comparison/is_empty (layer3, serialization). All behavior-preserving.
Surfaced because a warm target/ makes the pre-push hook actually run clippy
(it skips on cold worktrees). Remaining non-auto-fixable test-code lints
tracked in #967.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-push surfaced fmt + clippy (-D warnings) failures in the new code:
- rustfmt the 6 new/changed atlas files + the bench example.
- features.rs: HashMap → BTreeMap (project bans HashMap for determinism via
clippy disallowed_types; the bucket map is lookup-only either way).
- attractor_matching.rs: drop now-redundant .clone() on AttractorType (it
became Copy in #953) — clippy clone_on_copy.
- drainage.rs tests: manual range → (1..=12).contains(&n).
- features.rs test: drop .clone() on Copy AttractorType.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clerk was net-negative on the #963 push: non-exhaustive (three distinct real
D-202 inconsistencies surfaced only on successive re-pushes, each run missing
the others — so APPROVED can't be trusted) and it re-reviews the whole range
every push (token burn, no verdict cache). Gate it behind SR_RUN_CLERK=1
(default off). Rework tracked in #965; decision-record consistency is the
author's responsibility until then.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clerk caught it: D-202 line 928 said sea_level is 'carried alongside (not in
the PNG)' while line 930 + the implementation store it IN the PNG (tEXt chunk).
Update line 928 to match — sea_level is a tEXt chunk, with a caller default
fallback. D-202 is now internally consistent (resolution, sea_level, table-drop).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The heightmap.rs module docstring still said 'canonically 2048×1024' (the
pre-decision figure); the canonical resolution is 1024×512 (D-202 amended).
Clerk caught the contradiction. Now consistent across D-200/201/202/208 + code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clerk pre-push review caught that the D-202 amendment introduced cross-record
contradictions + a wrong number. Fix:
- D-202: canonical heightmap is 1024×512 (not 2048×1024 — stale figure; the
bake and D-201's canonical PNG are both 1024×512).
- D-200: atlas_body_heightmaps no longer produced (elevation is a per-body file).
- D-201: amend the Tier-3 canonical-format lock — stored heightmap is now a
16-bit grayscale 1024×512 PNG; PNG dims unchanged, Layer 1 downsamples to the
512×256 working grid (satisfies the record's own 'deviation requires amending'
gate).
- D-208: clarify 512×256 is the Layer-1 working grid (downsampled from 1024×512).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The heightmap BLOB table is dropped (canonical elevation is now the per-body
16-bit heightmap.png file). Re-stamped by import_economics.
Clerk-Skip: binary DB build artifact — no D-record/code surface
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- heightmap.rs: read sea_level from the PNG tEXt chunk (bake writes it),
default-fallback param; new test reads_sea_level_from_text_chunk.
- client atlas_viewer.gd: load reliefmap.png (color display) instead of
heightmap.png (now 16-bit grayscale elevation, cascade-only).
- drop atlas_body_heightmaps: removed from systems-schema.sql; DROP TABLE in
import_economics MIGRATION_SQL (the PNG is the store now).
- D-202 amendment: implementation-status note (consumer + producer done),
resolving the review's 'reads done but producer pending' point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the dead atlas_body_heightmaps DB-BLOB importer (Tyre review B2 —
it asserted 256×512 and wrote a dropped table) with the canonical asset bake:
- one-time rename of the legacy color heightmap.png → reliefmap.png for ALL
bodies (incl. Sol — display-file rename only, no re-sim);
- for each non-Sol inhabited body: simulate() at 1024×512 → clean reliefmap.png
(render_heightmap, no painted features) + 16-bit grayscale heightmap.png with
sea_level in a tEXt chunk;
- no systems.db writes; uses parse_system for proper body defs; run via uv.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the Hoshe/Tyre review:
- drainage: assert flow_accumulation/max_accumulation determinism + clamp ≥ 1
(the D-209 strength denominator); isolated-basin merge path (no panic).
- subbiome: each derivable variant reachable + Volcanic never emitted.
- planet_simulation: new test_sim_determinism.py — same body simulates to a
bit-identical elevation array at 1024×512 (the 271-body bake can't be cheaply
re-run, so a silent drift = full re-bake). Verified PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review (Tyre) caught a determinism hole: corp-HQ→city matching iterated a
Python set (sys_body_ids), so on a name collision across bodies in the same
system, which row received corp_id depended on set order — nondeterministic
output landing in the committed DB (violates D-010 #4). Iterate sorted().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review (Hoshe) caught that the MAX_ATTRACTORS cap sorted by global strength,
and RiverMouth's normalized strength (accum/max_accum) is tiny — so on a
realistic body 108 river mouths produced 0 surviving RiverMouth attractors,
violating D-209 ('RiverMouth: always high-value') and starving #955 placement.
Replace the global-strength cap with group-by-type + round-robin so every
present type keeps representation (strongest-first within each type).
Deterministic. New test river_mouths_survive_cap locks it in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- planet_simulation: GRID 512×256 → 1024×512. The elevation noise is
resolution-independent (normalized coords + absolute freqs), so the
finer grid samples the SAME terrain — features keep physical size,
generation stays deterministic. Pixel-unit constants (gaussian sigma,
crater radii, peak-filter window, erosion slope) scale by GRID_W/512.
Validated: non-Sol bodies render same-world-crisper at 1024.
- Remove compute_rivers + _rivers_to_grid + the rivers/river_grid terrain
keys: rivers are the Rust cascade's job (D8 drainage, D-208), the single
source of river truth. The old heuristic didn't even reach the sea.
- render_heightmap: stop painting rivers onto the relief (cascade/Atlas
overlay computed rivers instead).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersede the atlas_body_heightmaps BLOB store with a per-body 16-bit
grayscale heightmap.png (2048x1024, canonical elevation) next to a renamed
reliefmap.png (color display render). Records the rationale (binary
merge-conflict avoidance + DB size + viewable/deterministic single source),
the multi-resolution split (Layer 1 downsamples to 512x256), and the
dropped table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the atlas_body_heightmaps DB-BLOB loader with a per-body file
loader (D-202 amendment): heightmap.rs reads the 16-bit grayscale
heightmap.png (via the new png dep) from the body's terrain_reference
path, normalizes to f32 [0,1], and rejects RGB so a reliefmap can't be
misread as elevation. Adds BodyHeightmap::downsample (box-average,
deterministic) so Layer 1 drops the high-res stored heightmap to the
512x256 working resolution. sea_level becomes body metadata carried
alongside, not in the PNG. 4 loader tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
examples/bench_layer1 times run_layer1 on a synthetic 512x256 heightmap
with a phase breakdown (drainage / terrain analysis / feature extraction).
Post-optimization: ~106ms/body total (drainage ~45ms). Documents the
synthetic-terrain caveat (real heightmaps via #963).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the empty-world topography cascade (D-208/209/210):
- generator.rs: add SubBiomeVariant (11 variants, D-210) + sub_biome and
terrain_modification_cost fields on GeographicAttractor; AttractorType
is now Copy.
- features.rs (new, D-209): extract the 7 attractor tags from heightmap +
drainage. Coast/lake derived from the heightmap (D-209/D-223
reconciliation — markers are names-only now, no polygons). Deterministic
(sorted seeds, integer keys, bucket-grid thinning); strength-capped at
MAX_ATTRACTORS preserving type diversity. Shared TerrainAnalysis
(masks/slope/moisture/percentile) feeds both extraction and sub-biome.
- subbiome.rs (new, D-210): classify sub-biome + terrain_modification_cost
from elevation/slope/moisture/latitude. Volcanic stays in the enum but
is not emitted (no heightmap signal).
- layer1.rs (new): run_layer1 orchestrator + attach_feature_names (D-223
pool names to largest rivers / Alpine peaks).
- attractor_matching constructors updated for the new fields.
76 atlas tests pass; run_layer1 determinism verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DrainageResult now exposes flow_accumulation + max_accumulation for
D-209 attractor-strength normalization.
- Rewrite merge_small_basins from an O(merges x n) loop (rescanned the
whole grid per merge) to an adjacency-graph + union-find pass: one grid
scan, lazy merges. Cuts D8 drainage at 512x256 from ~299ms to ~45ms,
meeting the D-208 ~50ms target (the module had never been run at
canonical resolution before — it was orphaned). Determinism preserved
(smallest by (size,id), largest neighbor by (size, lowest id)).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>