feat(simulation): dispatch RegionProfile layer in production (T-1032)

The D-239 carrier layer (T-1023/1024/1026) only ran in tests because every
production AnalyzeBody enqueue passed body_params: None. Wire the real path:

- New BodyParamsReader (server/src/atlas/body_params_reader.rs): read-only
  systems.db reader, joins bodies -> star_systems (LEFT JOIN) for the climate/
  tectonic inputs. SQL verified against systems-schema.sql. All fields Option,
  NULLs handled; tectonic_activity absent from schema -> None (derives from
  planet_class). 5 unit tests.
- layer_proxy.rs: on cache miss, read the body's params and pass
  Some(Box::new(..)). On read error, warn + fall back to None (cascade stops at
  Settlement, no panic) — graceful degradation.
- plugin.rs / main.rs: register BodyParamsReaderResource (CityContextReader
  pattern) and thread it through serve_atlas_requests.

BodyWorldState.regions now populates for real bodies in the D-206 background
pass. End-to-end tests cover wired (regions populated) + unwired (empty) paths.

cargo test 1504 pass, clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 12:28:01 +02:00
co-authored by Claude Opus 4.8
parent a516b06de9
commit 1887c886ef
5 changed files with 581 additions and 7 deletions
+13 -3
View File
@@ -12,6 +12,7 @@ use bevy_ecs::prelude::*;
use bevy_ecs::schedule::IntoScheduleConfigs;
use crate::atlas::attractor_matching::CityPlacement;
use crate::atlas::body_params_reader::BodyParamsReaderResource;
use crate::atlas::body_world_state::{BodyWorldStateCache, CACHE_CAPACITY};
use crate::atlas::city_context_reader::{
context_from_read_set, CityContextReaderResource, CityEconomicReadSet,
@@ -50,6 +51,7 @@ fn serve_atlas_requests(
queue: Res<GenerationQueue>,
resolver: Option<Res<BodySourceResolverResource>>,
city_reader: Option<Res<CityContextReaderResource>>,
body_params_reader: Option<Res<BodyParamsReaderResource>>,
rng: Option<Res<SimRng>>,
time: Option<Res<SimulationTime>>,
) {
@@ -59,12 +61,20 @@ fn serve_atlas_requests(
let world_seed = rng.as_ref().map(|r| r.seed()).unwrap_or(0);
let tick = time.as_ref().map(|t| t.tick).unwrap_or(0);
let reader = city_reader.as_ref().map(|r| &r.0);
let params_reader = body_params_reader.as_ref().map(|r| &r.0);
let pending: Vec<_> = requests.0.drain(..).collect();
for req in pending {
let resp = match resolver.as_ref() {
Some(r) => {
handle_atlas_request(&req, &mut cache, &queue, &r.0, reader, world_seed, tick)
}
Some(r) => handle_atlas_request(
&req,
&mut cache,
&queue,
&r.0,
reader,
params_reader,
world_seed,
tick,
),
None => AtlasLayerResponse {
body_id: req.body_id.clone(),
status: AtlasLayerStatus::Error("no body source resolver".to_string()),