islaApocalypse/Server
beezm 9c255a4fc6 feat: render the water we already had (terrain-water task 13, Phase C1)
The blueprint has carried water since task 03 -- WBID per-pixel body id,
WBTB body table, WSRF per-pixel surface level. Nothing ever drew it. Now
the runtime does. No new water data: the blueprint is the authority on
where water is and at what level, and this only reads it.

WHY WATER IS ITS OWN MESH, not a block in the terrain field. The terrain
is a Marching-Cubes iso-surface over ChunkData.Densities. Writing WATER
into that field would not lay a sheet on top of the seabed -- it would
move the iso-surface, fusing the sea into the terrain as if it were solid
ground. A second surface is the only way water can sit at ITS level
independent of the ground under it. So ChunkRenderer builds a water
MeshInstance3D as a child of the chunk's terrain mesh.

TRANSPARENCY IS SHIPPED, NOT DEFERRED (Step 2.4's cheap branch). Because
water is a distinct MeshInstance3D it carries its own StandardMaterial3D,
so alpha is one flag and Godot sorts transparent surfaces after opaque
ones by itself. Zero mesher changes. Alpha runs 0.62 shallow -> 0.97 deep,
so the shelved coast stays readable and open ocean closes up.

WHICH DATA DROVE IT: WSRF for the level (per-pixel, quantised to 1/32768
raw ~ 7.7 mm, far under the 1 m voxel, and no table lookup), WBID for
presence (it is the water stage's own classification output -- the same
set the biome grid's Ocean/Lake pixels form by construction), WBTB as the
fallback when a column is flagged wet but carries the WSRF no-water
sentinel. A body the table does not know leaves the column DRY rather
than guessing a level.

Details worth keeping:
- Water Y uses Constants.HEIGHT_SCALE, the SAME mapping as the terrain
  surface, named so the sheet and the seabed cannot drift apart if the
  vertical band is ever retuned.
- Depth for shading comes from BLUEPRINT heights, not rendered geometry:
  the terrain render clamps its floor at Y=2, so every deep-ocean column
  would otherwise read as one flat ~36 m and the gradient would die
  exactly where the ocean gets interesting.
- A cell is drawn if ANY corner is wet, flat at the highest wet level.
  Drawing onto a partly-dry cell is deliberate -- it carries the sheet
  under the shoreline where the opaque terrain hides it. Only-fully-wet
  cells retreat the waterline a metre and leave a dry gap around every
  coast and lake.
- Non-metallic, roughness 0.35: the scene environment is minimal, and a
  metallic surface reads near-black when there is nothing to reflect.

BlockRegistry gains WATER (id 11). Wire-safe: block IDs are never
serialized -- the blueprint's section table stores heights, biome ordinals
and water data, never block IDs, and chunks are not persisted yet.

MEASURED on seed 1825907253: 241,415 water columns across 454 of 4096
chunks; surface Y 37.6..37.6 m (flat, = 0.15 x 251 -- the flat sea model,
rendered); depth to 32.3 m, matching an independent read of the blueprint
exactly. Both an ocean and a lake fall in the default view.

NO REGRESSION to the 2D pipeline, verified section by section: a
regeneration of the working seed is byte-identical to task 12's run in
TCRV, TDTL, HGTS, BIOM, WBID, WBTB, WSRF, TOWN and all four road
sections; only PRMS differs, and only in its write timestamp. All four
snapshot PNGs md5-identical.

No storms, no waves, no animation, no flow -- water at rest. Those are C2+.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 02:32:04 -04:00
..
Scripts feat: render the water we already had (terrain-water task 13, Phase C1) 2026-08-09 02:32:04 -04:00
README.md docs: BLUEPRINT_FORMAT.md + README updates for the v2 container (terrain-water task 02) 2026-08-06 07:31:03 -04:00

Server Module

Authoritative world building. Turns the static WorldBlueprint in RAM into physical 3D chunks. In a future multiplayer setup this is the side that dictates terrain and ships chunk data to clients.

ServerChunkManager.cs

Attached to the World root node of Scenes/Main.tscn. Runs the whole 3D world at boot.

Startup

  1. Loads ServerConfig.json and the seed's .dat blueprint (v2 or legacy v1 — the parser dispatches automatically; see Core/Scripts/BLUEPRINT_FORMAT.md).
  2. Cross-checks the blueprint's embedded params (v2 only) against the config and logs a prominent BLUEPRINT/CONFIG DESYNC warning if the seed or MapSize disagree — a config edited after generation is loud now, not silent.
  3. Finds the Capitol in the parsed town list and uses it as the world origin point.
  4. Converts its pixel position to chunk coordinates (pixel / CHUNK_SIZE).
  5. Builds a (2 × ChunkRadius)² grid of chunks around it — synchronously, all at boot.
  6. Teleports the Camera3D to 120 m above the Capitol, looking down.

Two things to know about startup. The chunk grid is built in one blocking pass with no streaming or unloading, so ChunkRadius directly controls boot cost — 32 means 4,096 chunks and roughly 3.6 GB. And the camera LookAt points straight down, which is a degenerate case: the up vector ends up parallel to the view direction, so camera roll is undefined and Godot logs a warning.

Per-chunk generation

  • Road culling first. Only the road segments whose bounding box reaches this chunk are kept, each tagged with its RoadTier. The padding is derived from the widest shoulder any tier has plus a margin, so widening a road cannot silently truncate it at chunk edges.
  • Surface height per column (GetExactSurface) — the blueprint height scaled into the chunk's usable vertical band, then modified by any road carving.
  • Density per voxel(y surfaceY) normalised by the local slope, giving a signed distance to the surface. Positive above, negative below.
  • Block IDs per voxel via BiomePalette, plus the per-column data the renderer needs.
  • Hands the finished chunk to a ChunkRenderer.

Road carving

All four tiers carve, each with its own character (widest and smoothest for highways, narrow and terrain-hugging for trails — values in Constants.cs).

For each column, the carve finds the nearest road whose shoulder actually reaches it — not simply the nearest road, since tiers have different reach and a nearby footpath must not shadow a highway still covering the column. It then reads the roadbed height at the closest point along that segment, blending between a straight ramp between the segment's endpoints ("holds a grade") and the terrain directly beneath ("hugs the land") according to the tier.

Inside the road radius the column is flattened to that height and flagged with the tier's surface material; out to the shoulder radius it eases back to natural ground with a smoothstep.

HeightAtPixel samples the heightmap bilinearly — road path points are fractional, and nearest-cell sampling produced a metre-scale staircase along the roadbed.

Not here yet

No chunk streaming or unloading, no collision, no networking, no player. The "server" is currently a node in the same scene as the renderer — the server/client split is structural, not a process boundary.