diff --git a/client/ui/implant/apps/atlas/atlas_viewer.gd b/client/ui/implant/apps/atlas/atlas_viewer.gd index 2d4230172..e967eee2e 100644 --- a/client/ui/implant/apps/atlas/atlas_viewer.gd +++ b/client/ui/implant/apps/atlas/atlas_viewer.gd @@ -262,12 +262,15 @@ func _load_heightmap() -> void: var ref: Variant = _body.get("terrain_reference") if ref == null or str(ref).is_empty(): return - var path: String = str(ref) + # terrain_reference points at
/heightmap.png — now the canonical + # 16-bit GRAYSCALE elevation (D-202 amended, #963). For display we load the + # color reliefmap.png that sits beside it. + var path: String = str(ref).get_base_dir() + "/reliefmap.png" if not path.begins_with("/"): var project_root: String = ProjectSettings.globalize_path("res://").get_base_dir().get_base_dir() path = project_root + "/" + path if not FileAccess.file_exists(path): - push_warning("AtlasViewer: terrain_reference not found at %s" % path) + push_warning("AtlasViewer: reliefmap not found at %s" % path) return var img := Image.load_from_file(path) if img != null: diff --git a/decisions/architecture.md b/decisions/architecture.md index 5bbacea8a..c7bba1022 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -927,6 +927,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **Multi-resolution:** the stored heightmap is high-res for the lower layers (region/block/tile sample local detail); **Layer 1** (continental drainage/basins/mountain-ranges) calls `BodyHeightmap::downsample` to the `GRID_W×GRID_H = 512×256` working resolution first, decoupling continental compute cost (~45ms) from stored resolution. - **Rust loader:** `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG (via the `png` crate), normalizes to f32 [0,1]; rejects RGB (a reliefmap can't be misread as elevation). `sea_level` becomes body metadata carried alongside (not in the PNG). - **`atlas_body_heightmaps` is dropped**; `import_heightmaps.py` writes the PNG file instead of a DB row. The bake runs in the content pipeline (numpy/scipy) once; the runtime cascade is pure Rust loading the file. + - **Implementation status (#963):** Consumer done — `heightmap.rs::load_heightmap_png` reads the 16-bit grayscale PNG + `sea_level` tEXt chunk, rejects RGB, downsamples for Layer 1. Producer done — `import_heightmaps.py` is the bake (rename legacy `heightmap.png`→`reliefmap.png` for all bodies incl. Sol; for non-Sol inhabited bodies write a fresh clean `reliefmap.png` + 16-bit `heightmap.png` from `simulate()` at the bumped 1024×512 grid). `atlas_body_heightmaps` dropped via MIGRATION_SQL + removed from `systems-schema.sql`. Godot client (`atlas_viewer.gd`) loads `reliefmap.png` for display. Sim determinism guarded by `test_sim_determinism.py`. - **Rationale:** Storing heightmaps in `systems.db` keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline. *(Superseded by the #963 amendment above — the file-based model won out because the DB-as-single-source goal conflicts with binary-merge-conflict avoidance and DB size; a per-body committed PNG is itself a queryable, diffable-by-render asset.)* - **Ticket:** #901 (schema), #906 (import), #916 (Rust loader) - **Raised by:** Generation cascade workshop (#897) diff --git a/server/data/systems-schema.sql b/server/data/systems-schema.sql index 021842951..29ec070f6 100644 --- a/server/data/systems-schema.sql +++ b/server/data/systems-schema.sql @@ -462,17 +462,10 @@ CREATE INDEX IF NOT EXISTS idx_atlas_pois_kind ON atlas_pois(kind); CREATE INDEX IF NOT EXISTS idx_atlas_rivers_body ON atlas_rivers(body_id); CREATE INDEX IF NOT EXISTS idx_atlas_oceans_body ON atlas_oceans(body_id); CREATE INDEX IF NOT EXISTS idx_atlas_mountain_ranges_body ON atlas_mountain_ranges(body_id); --- Heightmap BLOB storage — float32 LE, row-major (D-202, #901) --- Only inhabited bodies receive rows at build time; uninhabited bodies are --- generated on-demand by the runtime-background tier. -CREATE TABLE IF NOT EXISTS atlas_body_heightmaps ( - body_id TEXT PRIMARY KEY REFERENCES bodies(body_id) ON DELETE CASCADE, - width INTEGER NOT NULL DEFAULT 512, - height INTEGER NOT NULL DEFAULT 256, - data BLOB NOT NULL, -- float32 LE, row-major, width×height values - sea_level REAL NOT NULL DEFAULT 0.0, - imported_at TEXT NOT NULL DEFAULT (datetime('now')) -); +-- Heightmap BLOB storage REMOVED (D-202 amended, #963): canonical elevation is +-- now a per-body 16-bit grayscale heightmap.png file (sea_level in a tEXt +-- chunk), not a systems.db BLOB. The atlas_body_heightmaps table is dropped via +-- MIGRATION_SQL in import_economics.py. -- City name reservations — replaces authored city positions in markers.json (D-207, #902) -- Position is generated by the city placement algorithm; name is authored or LLM-generated. @@ -521,7 +514,6 @@ CREATE TABLE IF NOT EXISTS atlas_city_positions ( score REAL NOT NULL -- match quality [0.0, 1.0] ); -CREATE INDEX IF NOT EXISTS idx_atlas_body_heightmaps_body ON atlas_body_heightmaps(body_id); CREATE INDEX IF NOT EXISTS idx_atlas_city_names_body ON atlas_city_names(body_id); CREATE INDEX IF NOT EXISTS idx_atlas_city_names_kind ON atlas_city_names(kind); CREATE INDEX IF NOT EXISTS idx_atlas_city_names_corp ON atlas_city_names(corp_id); diff --git a/server/src/atlas/heightmap.rs b/server/src/atlas/heightmap.rs index 57aedfcc0..f45724997 100644 --- a/server/src/atlas/heightmap.rs +++ b/server/src/atlas/heightmap.rs @@ -110,23 +110,32 @@ pub enum HeightmapLoadError { pub fn load_heightmap_png( path: &Path, body_id: &str, - sea_level: f32, + default_sea_level: f32, ) -> Result