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:
@@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::atlas::body_world_state::{BodyWorldStateCache, SimTick};
|
||||
use crate::atlas::cascade::CascadeLayer;
|
||||
use crate::atlas::city_context_reader::CityContextReader;
|
||||
use crate::atlas::gen_queue::{GenPriority, GenWorkItem, GenerationQueue};
|
||||
use crate::atlas::layer1::Layer1Output;
|
||||
use crate::atlas::source_resolver::{BodySourceResolver, SourceResolveError};
|
||||
@@ -55,11 +56,16 @@ pub struct AtlasLayerResponse {
|
||||
|
||||
/// Serve one layer request (D-225). `current_tick` stamps the cache LRU on hit;
|
||||
/// `world_seed` derives the body's `SeedChain` for the enqueued analysis.
|
||||
///
|
||||
/// `city_reader` supplies the body's settlements for Layer-3 placement (#955),
|
||||
/// read on a cache miss. `None` (or a read failure) places no cities — the
|
||||
/// cascade still runs Layer 1; the body just gets no settlement placements.
|
||||
pub fn handle_atlas_request(
|
||||
req: &AtlasLayerRequest,
|
||||
cache: &mut BodyWorldStateCache,
|
||||
queue: &GenerationQueue,
|
||||
resolver: &BodySourceResolver,
|
||||
city_reader: Option<&CityContextReader>,
|
||||
world_seed: u64,
|
||||
current_tick: SimTick,
|
||||
) -> AtlasLayerResponse {
|
||||
@@ -85,12 +91,29 @@ pub fn handle_atlas_request(
|
||||
// Miss — resolve the source heightmap and enqueue background analysis.
|
||||
match resolver.resolve(&req.body_id) {
|
||||
Ok(heightmap_path) => {
|
||||
// Pre-resolve this body's settlements so the Rayon work item stays
|
||||
// DB-free (#955, D-225). A read failure is non-fatal: log and place
|
||||
// no cities (Layer 1 still runs).
|
||||
let cities = match city_reader {
|
||||
Some(reader) => reader
|
||||
.read_body_settlements(&req.body_id)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
body_id = %req.body_id,
|
||||
error = %e,
|
||||
"settlement read failed; placing no cities"
|
||||
);
|
||||
Vec::new()
|
||||
}),
|
||||
None => Vec::new(),
|
||||
};
|
||||
queue.submit(
|
||||
GenWorkItem::AnalyzeBody {
|
||||
body_id: req.body_id.clone(),
|
||||
heightmap_path,
|
||||
sea_level: DEFAULT_SEA_LEVEL,
|
||||
body_seed: SeedChain::for_body(world_seed, &req.body_id),
|
||||
cities,
|
||||
},
|
||||
GenPriority::Immediate,
|
||||
);
|
||||
@@ -207,7 +230,7 @@ mod tests {
|
||||
let (_db, resolver) = empty_resolver();
|
||||
let queue = GenerationQueue::with_threads(1);
|
||||
|
||||
let resp = handle_atlas_request(&req("GJ1c"), &mut cache, &queue, &resolver, 42, 1);
|
||||
let resp = handle_atlas_request(&req("GJ1c"), &mut cache, &queue, &resolver, None, 42, 1);
|
||||
assert_eq!(resp.status, AtlasLayerStatus::Ready);
|
||||
assert_eq!(resp.layer1.expect("layer1").body_id, "GJ1c");
|
||||
}
|
||||
@@ -218,7 +241,7 @@ mod tests {
|
||||
let (_db, resolver) = resolver_with_body("GJ1c");
|
||||
let queue = GenerationQueue::with_threads(1);
|
||||
|
||||
let resp = handle_atlas_request(&req("GJ1c"), &mut cache, &queue, &resolver, 42, 1);
|
||||
let resp = handle_atlas_request(&req("GJ1c"), &mut cache, &queue, &resolver, None, 42, 1);
|
||||
assert_eq!(resp.status, AtlasLayerStatus::Pending);
|
||||
assert!(resp.layer1.is_none());
|
||||
|
||||
@@ -239,7 +262,7 @@ mod tests {
|
||||
let (_db, resolver) = empty_resolver();
|
||||
let queue = GenerationQueue::with_threads(1);
|
||||
|
||||
let resp = handle_atlas_request(&req("ghost"), &mut cache, &queue, &resolver, 42, 1);
|
||||
let resp = handle_atlas_request(&req("ghost"), &mut cache, &queue, &resolver, None, 42, 1);
|
||||
assert_eq!(resp.status, AtlasLayerStatus::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user