feat(simulation): run Layer-3 settlement placement in the runtime cascade (#955)
Wire the existing D-211 attractor-matching engine into the live generation cascade so settlements are placed in-game, not just in tests. - CityContextReader::read_body_settlements reads a body's settlements from atlas_city_names (ordered by id for determinism). NULL settlement_class defaults to PopulationBudget, not NameLocked: the class is NULL until placement runs, and NameLocked would force every settlement Tier-A in match_cities and collapse population tiering (D-211). NULL economic_role falls back to residential. - The AnalyzeBody work item carries the body's Vec<CityRecord>, and run_work_item now runs up_to Settlement (was Topography). A body with no settlements yields empty placements at negligible cost. - The atlas layer proxy reads settlements on a cache miss and pins them onto the work item, keeping the Rayon task DB-free (D-225). A read failure is non-fatal: log and place no cities (Layer 1 still runs). Threaded through a new CityContextReaderResource Bevy resource opened in main.rs, mirroring BodySourceResolverResource. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ use std::sync::{Arc, Mutex};
|
||||
use bevy_ecs::prelude::Resource;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
use crate::atlas::attractor_matching::CityRecord;
|
||||
use crate::atlas::body_world_state::BodyWorldState;
|
||||
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
|
||||
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
|
||||
@@ -67,6 +68,10 @@ pub enum GenWorkItem {
|
||||
heightmap_path: PathBuf,
|
||||
sea_level: f32,
|
||||
body_seed: SeedChain,
|
||||
/// The body's settlements (from `atlas_city_names`), pre-resolved at
|
||||
/// dispatch time so the cascade stays DB-free. Fed to Layer-3 placement
|
||||
/// (#955); empty if the body has no settlements (cascade stops at Layer 1).
|
||||
cities: Vec<CityRecord>,
|
||||
},
|
||||
/// Generate a Phase 1 DistrictSkeleton for this city.
|
||||
///
|
||||
@@ -347,6 +352,7 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
|
||||
heightmap_path,
|
||||
sea_level,
|
||||
body_seed,
|
||||
cities,
|
||||
} => match load_heightmap_png(heightmap_path, body_id, *sea_level) {
|
||||
Ok(hm) => {
|
||||
// Layer 1 runs at the GRID_W×GRID_H working resolution (D-202):
|
||||
@@ -356,10 +362,15 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
|
||||
} else {
|
||||
hm
|
||||
};
|
||||
// Cities (&[]) get supplied once the Layer-3 settlement read is
|
||||
// wired through the work item (#955 follow-on); Topography needs none.
|
||||
let snapshot =
|
||||
run_cascade_from_heightmap(*body_seed, working, &[], CascadeLayer::Topography);
|
||||
// Run through Layer 3 (settlement placement, #955): the enqueuer
|
||||
// pre-resolved this body's settlements onto `cities`. A body with
|
||||
// no settlements yields empty placements at negligible cost.
|
||||
let snapshot = run_cascade_from_heightmap(
|
||||
*body_seed,
|
||||
working,
|
||||
cities,
|
||||
CascadeLayer::Settlement,
|
||||
);
|
||||
GenCompletion::BodyAnalyzed {
|
||||
body_id: body_id.clone(),
|
||||
state: snapshot.into_body_world_state(),
|
||||
@@ -451,6 +462,7 @@ mod tests {
|
||||
heightmap_path: test_heightmap_path(),
|
||||
sea_level: 0.3,
|
||||
body_seed: SeedChain::for_body(42, body_id),
|
||||
cities: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user