chore(engine): server hygiene batch — workers tests, surname dedup, save pin (T-1063, T-1064)
- workers/pool.rs: 5 new tests; catch_unwind keeps worker threads alive on handler panic (in-flight request loss unchanged, pinned by test + #843 docs); stubs.rs no longer falsely claims the pool is tested - save/load: execute_save_load pinned .after(Storyteller) so the scheduler cannot legally save pre-Input state; exclusive-system exception recorded in tick_phases.rs rules - surname corpus extracted to bin/shared/surname_corpus.rs (both economy generators import it; byte-identical output verified on 23.6MB+1.45MB TOMLs); all three stamp/watch registries updated - generator_spike gated behind non-default 'generator-spike' feature - economy.rs: 11 new D-181 signal-derivation tests on the new econ_sim Simulation::from_economy in-memory constructor - perception exemption comments now state the consumer sort contract; unused bytemuck removed; rayon comment corrected; the 22 allow(dead_code) documented as serde schema enforcement Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -235,6 +235,7 @@ git diff --name-only origin/main...HEAD -- \
|
|||||||
tooling/planet-gen/import_province_boundaries.py \
|
tooling/planet-gen/import_province_boundaries.py \
|
||||||
server/src/bin/generate_brands/main.rs \
|
server/src/bin/generate_brands/main.rs \
|
||||||
server/src/bin/generate_brands/names.rs \
|
server/src/bin/generate_brands/names.rs \
|
||||||
|
server/src/bin/shared/surname_corpus.rs \
|
||||||
tooling/generate-brands \
|
tooling/generate-brands \
|
||||||
server/data/systems-schema.sql \
|
server/data/systems-schema.sql \
|
||||||
wiki/star-systems/ \
|
wiki/star-systems/ \
|
||||||
|
|||||||
Generated
-1
@@ -1476,7 +1476,6 @@ dependencies = [
|
|||||||
"bevy_app",
|
"bevy_app",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
"bevy_tasks",
|
"bevy_tasks",
|
||||||
"bytemuck",
|
|
||||||
"clap",
|
"clap",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"econ-sim",
|
"econ-sim",
|
||||||
|
|||||||
+17
-3
@@ -10,8 +10,9 @@ bevy_app = "0.18"
|
|||||||
# multi_threaded enables Bevy's parallel system executor (no explicit import needed —
|
# multi_threaded enables Bevy's parallel system executor (no explicit import needed —
|
||||||
# bevy_ecs detects the feature on its bevy_tasks dependency at compile time).
|
# bevy_ecs detects the feature on its bevy_tasks dependency at compile time).
|
||||||
bevy_tasks = { version = "0.18", features = ["multi_threaded"] }
|
bevy_tasks = { version = "0.18", features = ["multi_threaded"] }
|
||||||
# par_iter infrastructure for data-parallel systems. Not yet called — candidate
|
# Thread-pool/parallelism infrastructure. Used by the atlas generation worker
|
||||||
# systems marked with TODO comments. Active use begins when profiling shows bottlenecks.
|
# pool (atlas/gen_queue.rs); data-parallel (par_iter) use in systems begins when
|
||||||
|
# profiling shows bottlenecks.
|
||||||
rayon = "1"
|
rayon = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
ron = "0.8"
|
ron = "0.8"
|
||||||
@@ -27,7 +28,6 @@ crossbeam-channel = "0.5"
|
|||||||
sysinfo = "0.35"
|
sysinfo = "0.35"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
bytemuck = "1"
|
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
aho-corasick = "1"
|
aho-corasick = "1"
|
||||||
@@ -37,6 +37,20 @@ econ-sim = { path = "../tooling/econ-sim" }
|
|||||||
[features]
|
[features]
|
||||||
default = ["gauntlet"]
|
default = ["gauntlet"]
|
||||||
gauntlet = []
|
gauntlet = []
|
||||||
|
# Gates the generator_spike binary (Sprint 25 proof-of-life, #612). The spike
|
||||||
|
# reimplements npc/generate.rs axes outside ECS and is kept for reference only —
|
||||||
|
# excluded from default builds so it cannot drift into production paths (T-1064).
|
||||||
|
generator-spike = []
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# generator_spike is declared explicitly (instead of relying on src/bin auto-
|
||||||
|
# discovery) so it can be feature-gated: `cargo build --bin generator_spike
|
||||||
|
# --features generator-spike`. Other binaries remain auto-discovered.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
[[bin]]
|
||||||
|
name = "generator_spike"
|
||||||
|
path = "src/bin/generator_spike.rs"
|
||||||
|
required-features = ["generator-spike"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Maintained drop-in fork of the deprecated serde_yaml 0.9 (#966). Test-only:
|
# Maintained drop-in fork of the deprecated serde_yaml 0.9 (#966). Test-only:
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ use rusqlite::Connection;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod names;
|
mod names;
|
||||||
|
#[path = "../shared/surname_corpus.rs"]
|
||||||
|
mod surname_corpus;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CLI
|
// CLI
|
||||||
|
|||||||
@@ -1,334 +1,17 @@
|
|||||||
//! Deterministic product name generation for minor brand instances.
|
//! Deterministic product name generation for minor brand instances.
|
||||||
//!
|
//!
|
||||||
//! Reuses the corridor surname pools from generate_corporations/names.rs
|
//! Reuses the corridor surname pools shared with generate_corporations
|
||||||
//! but combines them with category-specific product descriptors rather
|
//! (shared/surname_corpus.rs) but combines them with category-specific
|
||||||
//! than business suffixes. Halo and volume tiers get distinct descriptor
|
//! product descriptors rather than business suffixes. Halo and volume
|
||||||
//! pools so the output sounds differentiated.
|
//! tiers get distinct descriptor pools so the output sounds differentiated.
|
||||||
|
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rand_chacha::ChaCha8Rng;
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
use crate::surname_corpus::{
|
||||||
// Surname pools (same corpus as generate_corporations/names.rs)
|
CORE_NAMES, EAST_REACH_NAMES, FRONTIER_NAMES, NORTH_REACH_NAMES, SOUTH_REACH_NAMES,
|
||||||
// ---------------------------------------------------------------------------
|
WEST_REACH_NAMES,
|
||||||
|
};
|
||||||
const CORE_NAMES: &[&str] = &[
|
|
||||||
"Alvarez",
|
|
||||||
"Benoit",
|
|
||||||
"Carvalho",
|
|
||||||
"Durand",
|
|
||||||
"Eriksen",
|
|
||||||
"Fournier",
|
|
||||||
"Gao",
|
|
||||||
"Hartmann",
|
|
||||||
"Ishida",
|
|
||||||
"Johansson",
|
|
||||||
"Kirchner",
|
|
||||||
"Lemaire",
|
|
||||||
"Moreau",
|
|
||||||
"Nakamura",
|
|
||||||
"Olsson",
|
|
||||||
"Pelletier",
|
|
||||||
"Richter",
|
|
||||||
"Saito",
|
|
||||||
"Torres",
|
|
||||||
"Ueda",
|
|
||||||
"Vasquez",
|
|
||||||
"Werner",
|
|
||||||
"Xu",
|
|
||||||
"Yamada",
|
|
||||||
"Zhou",
|
|
||||||
"Andersen",
|
|
||||||
"Beaumont",
|
|
||||||
"Costa",
|
|
||||||
"Delacroix",
|
|
||||||
"Engel",
|
|
||||||
"Fujita",
|
|
||||||
"Gutierrez",
|
|
||||||
"Hayashi",
|
|
||||||
"Ibarra",
|
|
||||||
"Jensen",
|
|
||||||
"Klein",
|
|
||||||
"Laurent",
|
|
||||||
"Mercier",
|
|
||||||
"Novak",
|
|
||||||
"Ortiz",
|
|
||||||
"Park",
|
|
||||||
"Reuter",
|
|
||||||
"Suzuki",
|
|
||||||
"Takahashi",
|
|
||||||
"Ulrich",
|
|
||||||
"Valentin",
|
|
||||||
"Wagner",
|
|
||||||
"Xie",
|
|
||||||
"Yilmaz",
|
|
||||||
"Zhang",
|
|
||||||
];
|
|
||||||
|
|
||||||
const NORTH_REACH_NAMES: &[&str] = &[
|
|
||||||
"Andersson",
|
|
||||||
"Bjornsson",
|
|
||||||
"Calloway",
|
|
||||||
"Dalsgaard",
|
|
||||||
"Eklund",
|
|
||||||
"Falk",
|
|
||||||
"Grimstad",
|
|
||||||
"Hedlund",
|
|
||||||
"Ivarsson",
|
|
||||||
"Jonasson",
|
|
||||||
"Kirkpatrick",
|
|
||||||
"Lindqvist",
|
|
||||||
"MacLeod",
|
|
||||||
"Nordstrom",
|
|
||||||
"Olafsson",
|
|
||||||
"Pettersson",
|
|
||||||
"Rehn",
|
|
||||||
"Strandberg",
|
|
||||||
"Thorsen",
|
|
||||||
"Ulvskog",
|
|
||||||
"Vikstrom",
|
|
||||||
"Wahlberg",
|
|
||||||
"Aberg",
|
|
||||||
"Berglund",
|
|
||||||
"Carlsen",
|
|
||||||
"Dalgaard",
|
|
||||||
"Engstrom",
|
|
||||||
"Forsell",
|
|
||||||
"Gustafsson",
|
|
||||||
"Halvorsen",
|
|
||||||
"Ingvarsson",
|
|
||||||
"Jansson",
|
|
||||||
"Knudsen",
|
|
||||||
"Lundin",
|
|
||||||
"MacPherson",
|
|
||||||
"Nylund",
|
|
||||||
"Ostergaard",
|
|
||||||
"Palsson",
|
|
||||||
"Rasmussen",
|
|
||||||
"Sjoberg",
|
|
||||||
"Toft",
|
|
||||||
"Ulfsson",
|
|
||||||
"Vestergaard",
|
|
||||||
"Wiklund",
|
|
||||||
"Aasen",
|
|
||||||
"Brannstrom",
|
|
||||||
"Dahl",
|
|
||||||
"Eide",
|
|
||||||
"Friberg",
|
|
||||||
"Gren",
|
|
||||||
];
|
|
||||||
|
|
||||||
const SOUTH_REACH_NAMES: &[&str] = &[
|
|
||||||
"Adamski",
|
|
||||||
"Baranov",
|
|
||||||
"Chernov",
|
|
||||||
"Dubois",
|
|
||||||
"Egorov",
|
|
||||||
"Filipov",
|
|
||||||
"Gromov",
|
|
||||||
"Horvat",
|
|
||||||
"Ivanova",
|
|
||||||
"Jankovic",
|
|
||||||
"Kowalski",
|
|
||||||
"Lazarev",
|
|
||||||
"Morozov",
|
|
||||||
"Novikov",
|
|
||||||
"Ostrowski",
|
|
||||||
"Petrov",
|
|
||||||
"Reznik",
|
|
||||||
"Sokolov",
|
|
||||||
"Tkachenko",
|
|
||||||
"Uvarov",
|
|
||||||
"Volkov",
|
|
||||||
"Wojcik",
|
|
||||||
"Yakimov",
|
|
||||||
"Zheng",
|
|
||||||
"Babic",
|
|
||||||
"Chernyshev",
|
|
||||||
"Dragunov",
|
|
||||||
"Fedorov",
|
|
||||||
"Grushevsky",
|
|
||||||
"Havel",
|
|
||||||
"Ito",
|
|
||||||
"Jovanovic",
|
|
||||||
"Katsaros",
|
|
||||||
"Lebedev",
|
|
||||||
"Mazur",
|
|
||||||
"Nemec",
|
|
||||||
"Ochoa",
|
|
||||||
"Popov",
|
|
||||||
"Radic",
|
|
||||||
"Smirnov",
|
|
||||||
"Tanaka",
|
|
||||||
"Urasawa",
|
|
||||||
"Vasiliev",
|
|
||||||
"Watanabe",
|
|
||||||
"Xiang",
|
|
||||||
"Yegorov",
|
|
||||||
"Zaytsev",
|
|
||||||
"Borysko",
|
|
||||||
"Chen",
|
|
||||||
"Dimitrov",
|
|
||||||
];
|
|
||||||
|
|
||||||
const WEST_REACH_NAMES: &[&str] = &[
|
|
||||||
"Albrecht",
|
|
||||||
"Baumann",
|
|
||||||
"Christensen",
|
|
||||||
"Dietrich",
|
|
||||||
"Eisenberg",
|
|
||||||
"Fischer",
|
|
||||||
"Gruber",
|
|
||||||
"Hoffmann",
|
|
||||||
"Ingolstadt",
|
|
||||||
"Jaeger",
|
|
||||||
"Kessler",
|
|
||||||
"Lehmann",
|
|
||||||
"Mueller",
|
|
||||||
"Neumann",
|
|
||||||
"Obermann",
|
|
||||||
"Pfeiffer",
|
|
||||||
"Quandt",
|
|
||||||
"Roth",
|
|
||||||
"Schaefer",
|
|
||||||
"Thiel",
|
|
||||||
"Urban",
|
|
||||||
"Vogt",
|
|
||||||
"Weidenfeld",
|
|
||||||
"Ziegler",
|
|
||||||
"Becker",
|
|
||||||
"Claussen",
|
|
||||||
"Dorfmann",
|
|
||||||
"Eberhardt",
|
|
||||||
"Fleischer",
|
|
||||||
"Gerstner",
|
|
||||||
"Haber",
|
|
||||||
"Imhof",
|
|
||||||
"Jung",
|
|
||||||
"Kraemer",
|
|
||||||
"Linden",
|
|
||||||
"Metzger",
|
|
||||||
"Niedermann",
|
|
||||||
"Opitz",
|
|
||||||
"Preuss",
|
|
||||||
"Raabe",
|
|
||||||
"Steinbach",
|
|
||||||
"Trautmann",
|
|
||||||
"Unger",
|
|
||||||
"Vollmer",
|
|
||||||
"Winterberg",
|
|
||||||
"Zahn",
|
|
||||||
"Auerbach",
|
|
||||||
"Bruckner",
|
|
||||||
"Dahlem",
|
|
||||||
"Eckhardt",
|
|
||||||
];
|
|
||||||
|
|
||||||
const EAST_REACH_NAMES: &[&str] = &[
|
|
||||||
"Aquino",
|
|
||||||
"Bautista",
|
|
||||||
"Cruz",
|
|
||||||
"Dalisay",
|
|
||||||
"Espiritu",
|
|
||||||
"Flores",
|
|
||||||
"Garcia",
|
|
||||||
"Hernandez",
|
|
||||||
"Ilagan",
|
|
||||||
"Jeon",
|
|
||||||
"Kim",
|
|
||||||
"Lim",
|
|
||||||
"Magalang",
|
|
||||||
"Navarro",
|
|
||||||
"Ocampo",
|
|
||||||
"Park",
|
|
||||||
"Quijano",
|
|
||||||
"Reyes",
|
|
||||||
"Santos",
|
|
||||||
"Tan",
|
|
||||||
"Uy",
|
|
||||||
"Villanueva",
|
|
||||||
"Wong",
|
|
||||||
"Yoo",
|
|
||||||
"Aguilar",
|
|
||||||
"Buenaventura",
|
|
||||||
"Castillo",
|
|
||||||
"Dizon",
|
|
||||||
"Enriquez",
|
|
||||||
"Fernandez",
|
|
||||||
"Gonzales",
|
|
||||||
"Hwang",
|
|
||||||
"Ignacio",
|
|
||||||
"Jeong",
|
|
||||||
"Kwon",
|
|
||||||
"Lee",
|
|
||||||
"Marasigan",
|
|
||||||
"Nakamura",
|
|
||||||
"Oh",
|
|
||||||
"Perez",
|
|
||||||
"Ramos",
|
|
||||||
"Son",
|
|
||||||
"Tolentino",
|
|
||||||
"Umali",
|
|
||||||
"Valdez",
|
|
||||||
"Yun",
|
|
||||||
"Zamora",
|
|
||||||
"Baek",
|
|
||||||
"Choi",
|
|
||||||
"Dela Cruz",
|
|
||||||
];
|
|
||||||
|
|
||||||
const FRONTIER_NAMES: &[&str] = &[
|
|
||||||
"Adeyemi",
|
|
||||||
"Bergstrom",
|
|
||||||
"Chandra",
|
|
||||||
"Duval",
|
|
||||||
"Emeka",
|
|
||||||
"Fonseca",
|
|
||||||
"Gupta",
|
|
||||||
"Hassan",
|
|
||||||
"Ibrahim",
|
|
||||||
"Jansson",
|
|
||||||
"Kovac",
|
|
||||||
"Liu",
|
|
||||||
"Martinez",
|
|
||||||
"Nkosi",
|
|
||||||
"Okafor",
|
|
||||||
"Patel",
|
|
||||||
"Quinn",
|
|
||||||
"Rodriguez",
|
|
||||||
"Sousa",
|
|
||||||
"Thorne",
|
|
||||||
"Uddin",
|
|
||||||
"Varga",
|
|
||||||
"Wu",
|
|
||||||
"Xiong",
|
|
||||||
"Yoshida",
|
|
||||||
"Zhao",
|
|
||||||
"Abara",
|
|
||||||
"Beaumont",
|
|
||||||
"Cardenas",
|
|
||||||
"Doyle",
|
|
||||||
"Ekwueme",
|
|
||||||
"Ferreira",
|
|
||||||
"Gomes",
|
|
||||||
"Henriksen",
|
|
||||||
"Idris",
|
|
||||||
"Juma",
|
|
||||||
"Kato",
|
|
||||||
"Larsen",
|
|
||||||
"Morales",
|
|
||||||
"Ndlovu",
|
|
||||||
"Osei",
|
|
||||||
"Petrov",
|
|
||||||
"Ruiz",
|
|
||||||
"Singh",
|
|
||||||
"Tavares",
|
|
||||||
"Uchida",
|
|
||||||
"Volkov",
|
|
||||||
"Wang",
|
|
||||||
"Yang",
|
|
||||||
"Zaman",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn names_for_corridor(corridor: &str) -> &'static [&'static str] {
|
fn names_for_corridor(corridor: &str) -> &'static [&'static str] {
|
||||||
match corridor {
|
match corridor {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ use rusqlite::Connection;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod names;
|
mod names;
|
||||||
|
#[path = "../shared/surname_corpus.rs"]
|
||||||
|
mod surname_corpus;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CLI
|
// CLI
|
||||||
@@ -66,6 +68,15 @@ struct Cli {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Archetype data structures (deserialized from TOML)
|
// Archetype data structures (deserialized from TOML)
|
||||||
|
//
|
||||||
|
// On the `#[allow(dead_code)]` fields below (T-1064): serde requires every
|
||||||
|
// non-`#[serde(default)]` field to be present and well-typed, so fields the
|
||||||
|
// generator never reads still act as schema validation for the archetype
|
||||||
|
// TOMLs — a missing or mistyped field in wiki/economics/archetypes/ fails
|
||||||
|
// loudly here instead of silently producing a half-parsed archetype. Do not
|
||||||
|
// prune them; they are kept deliberately even though the Rust code never
|
||||||
|
// reads them (the economy simulation consumes these values via the importer,
|
||||||
|
// not via this binary).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -91,6 +102,10 @@ struct LoreArchetype {
|
|||||||
generation_notes: String,
|
generation_notes: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every field except the map key is unused by this binary: behavioral
|
||||||
|
/// archetypes are picked by id via `LoreArchetype::behavioral_affinity`.
|
||||||
|
/// The struct exists to schema-validate behavioral.toml (see the comment on
|
||||||
|
/// the section header above).
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct BehavioralArchetype {
|
struct BehavioralArchetype {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -135,12 +150,14 @@ struct Location {
|
|||||||
system_id: String,
|
system_id: String,
|
||||||
population: i64,
|
population: i64,
|
||||||
geographic_sector: String,
|
geographic_sector: String,
|
||||||
|
// Unused, but selecting it asserts the systems.db column exists (T-1064).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
economic_role: Option<String>,
|
economic_role: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExistingCorp {
|
struct ExistingCorp {
|
||||||
corp_id: String,
|
corp_id: String,
|
||||||
|
// Unused, but selecting it asserts the systems.db column exists (T-1064).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
proper_name: String,
|
proper_name: String,
|
||||||
specialization: Option<String>,
|
specialization: Option<String>,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
//! Names are composed from culture-specific pools matching the Settled Reach's
|
//! Names are composed from culture-specific pools matching the Settled Reach's
|
||||||
//! geographic sectors. Each sector has dominant cultural influences derived
|
//! geographic sectors. Each sector has dominant cultural influences derived
|
||||||
//! from lore (wiki settlements, founding cultures, corridor identities).
|
//! from lore (wiki settlements, founding cultures, corridor identities).
|
||||||
|
//! The surname pools live in shared/surname_corpus.rs (also used by
|
||||||
|
//! generate_brands).
|
||||||
//!
|
//!
|
||||||
//! Pattern: `{surname/word} {business_suffix}` where surname draws from
|
//! Pattern: `{surname/word} {business_suffix}` where surname draws from
|
||||||
//! the sector's cultural pool and suffix from the lore category.
|
//! the sector's cultural pool and suffix from the lore category.
|
||||||
@@ -10,333 +12,10 @@
|
|||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rand_chacha::ChaCha8Rng;
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
use crate::surname_corpus::{
|
||||||
// Surname pools by sector (drawn from founding cultures in wiki canon)
|
CORE_NAMES, EAST_REACH_NAMES, FRONTIER_NAMES, NORTH_REACH_NAMES, SOUTH_REACH_NAMES,
|
||||||
// ---------------------------------------------------------------------------
|
WEST_REACH_NAMES,
|
||||||
|
};
|
||||||
/// Core systems: cosmopolitan mix — the Reach's center of gravity.
|
|
||||||
const CORE_NAMES: &[&str] = &[
|
|
||||||
"Alvarez",
|
|
||||||
"Benoit",
|
|
||||||
"Carvalho",
|
|
||||||
"Durand",
|
|
||||||
"Eriksen",
|
|
||||||
"Fournier",
|
|
||||||
"Gao",
|
|
||||||
"Hartmann",
|
|
||||||
"Ishida",
|
|
||||||
"Johansson",
|
|
||||||
"Kirchner",
|
|
||||||
"Lemaire",
|
|
||||||
"Moreau",
|
|
||||||
"Nakamura",
|
|
||||||
"Olsson",
|
|
||||||
"Pelletier",
|
|
||||||
"Richter",
|
|
||||||
"Saito",
|
|
||||||
"Torres",
|
|
||||||
"Ueda",
|
|
||||||
"Vasquez",
|
|
||||||
"Werner",
|
|
||||||
"Xu",
|
|
||||||
"Yamada",
|
|
||||||
"Zhou",
|
|
||||||
"Andersen",
|
|
||||||
"Beaumont",
|
|
||||||
"Costa",
|
|
||||||
"Delacroix",
|
|
||||||
"Engel",
|
|
||||||
"Fujita",
|
|
||||||
"Gutierrez",
|
|
||||||
"Hayashi",
|
|
||||||
"Ibarra",
|
|
||||||
"Jensen",
|
|
||||||
"Klein",
|
|
||||||
"Laurent",
|
|
||||||
"Mercier",
|
|
||||||
"Novak",
|
|
||||||
"Ortiz",
|
|
||||||
"Park",
|
|
||||||
"Reuter",
|
|
||||||
"Suzuki",
|
|
||||||
"Takahashi",
|
|
||||||
"Ulrich",
|
|
||||||
"Valentin",
|
|
||||||
"Wagner",
|
|
||||||
"Xie",
|
|
||||||
"Yilmaz",
|
|
||||||
"Zhang",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// North reach: Nordic, Scottish, northern European — Calloway heritage.
|
|
||||||
const NORTH_REACH_NAMES: &[&str] = &[
|
|
||||||
"Andersson",
|
|
||||||
"Bjornsson",
|
|
||||||
"Calloway",
|
|
||||||
"Dalsgaard",
|
|
||||||
"Eklund",
|
|
||||||
"Falk",
|
|
||||||
"Grimstad",
|
|
||||||
"Hedlund",
|
|
||||||
"Ivarsson",
|
|
||||||
"Jonasson",
|
|
||||||
"Kirkpatrick",
|
|
||||||
"Lindqvist",
|
|
||||||
"MacLeod",
|
|
||||||
"Nordstrom",
|
|
||||||
"Olafsson",
|
|
||||||
"Pettersson",
|
|
||||||
"Rehn",
|
|
||||||
"Strandberg",
|
|
||||||
"Thorsen",
|
|
||||||
"Ulvskog",
|
|
||||||
"Vikstrom",
|
|
||||||
"Wahlberg",
|
|
||||||
"Aberg",
|
|
||||||
"Berglund",
|
|
||||||
"Carlsen",
|
|
||||||
"Dalgaard",
|
|
||||||
"Engstrom",
|
|
||||||
"Forsell",
|
|
||||||
"Gustafsson",
|
|
||||||
"Halvorsen",
|
|
||||||
"Ingvarsson",
|
|
||||||
"Jansson",
|
|
||||||
"Knudsen",
|
|
||||||
"Lundin",
|
|
||||||
"MacPherson",
|
|
||||||
"Nylund",
|
|
||||||
"Ostergaard",
|
|
||||||
"Palsson",
|
|
||||||
"Rasmussen",
|
|
||||||
"Sjoberg",
|
|
||||||
"Toft",
|
|
||||||
"Ulfsson",
|
|
||||||
"Vestergaard",
|
|
||||||
"Wiklund",
|
|
||||||
"Aasen",
|
|
||||||
"Brannstrom",
|
|
||||||
"Dahl",
|
|
||||||
"Eide",
|
|
||||||
"Friberg",
|
|
||||||
"Gren",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// South reach: Eastern European, East Asian industrial — Stalownia corridor.
|
|
||||||
const SOUTH_REACH_NAMES: &[&str] = &[
|
|
||||||
"Adamski",
|
|
||||||
"Baranov",
|
|
||||||
"Chernov",
|
|
||||||
"Dubois",
|
|
||||||
"Egorov",
|
|
||||||
"Filipov",
|
|
||||||
"Gromov",
|
|
||||||
"Horvat",
|
|
||||||
"Ivanova",
|
|
||||||
"Jankovic",
|
|
||||||
"Kowalski",
|
|
||||||
"Lazarev",
|
|
||||||
"Morozov",
|
|
||||||
"Novikov",
|
|
||||||
"Ostrowski",
|
|
||||||
"Petrov",
|
|
||||||
"Reznik",
|
|
||||||
"Sokolov",
|
|
||||||
"Tkachenko",
|
|
||||||
"Uvarov",
|
|
||||||
"Volkov",
|
|
||||||
"Wojcik",
|
|
||||||
"Yakimov",
|
|
||||||
"Zheng",
|
|
||||||
"Babic",
|
|
||||||
"Chernyshev",
|
|
||||||
"Dragunov",
|
|
||||||
"Fedorov",
|
|
||||||
"Grushevsky",
|
|
||||||
"Havel",
|
|
||||||
"Ito",
|
|
||||||
"Jovanovic",
|
|
||||||
"Katsaros",
|
|
||||||
"Lebedev",
|
|
||||||
"Mazur",
|
|
||||||
"Nemec",
|
|
||||||
"Ochoa",
|
|
||||||
"Popov",
|
|
||||||
"Radic",
|
|
||||||
"Smirnov",
|
|
||||||
"Tanaka",
|
|
||||||
"Urasawa",
|
|
||||||
"Vasiliev",
|
|
||||||
"Watanabe",
|
|
||||||
"Xiang",
|
|
||||||
"Yegorov",
|
|
||||||
"Zaytsev",
|
|
||||||
"Borysko",
|
|
||||||
"Chen",
|
|
||||||
"Dimitrov",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// West reach: German, Central European — Compact territory, Westphalian influence.
|
|
||||||
const WEST_REACH_NAMES: &[&str] = &[
|
|
||||||
"Albrecht",
|
|
||||||
"Baumann",
|
|
||||||
"Christensen",
|
|
||||||
"Dietrich",
|
|
||||||
"Eisenberg",
|
|
||||||
"Fischer",
|
|
||||||
"Gruber",
|
|
||||||
"Hoffmann",
|
|
||||||
"Ingolstadt",
|
|
||||||
"Jaeger",
|
|
||||||
"Kessler",
|
|
||||||
"Lehmann",
|
|
||||||
"Mueller",
|
|
||||||
"Neumann",
|
|
||||||
"Obermann",
|
|
||||||
"Pfeiffer",
|
|
||||||
"Quandt",
|
|
||||||
"Roth",
|
|
||||||
"Schaefer",
|
|
||||||
"Thiel",
|
|
||||||
"Urban",
|
|
||||||
"Vogt",
|
|
||||||
"Weidenfeld",
|
|
||||||
"Ziegler",
|
|
||||||
"Becker",
|
|
||||||
"Claussen",
|
|
||||||
"Dorfmann",
|
|
||||||
"Eberhardt",
|
|
||||||
"Fleischer",
|
|
||||||
"Gerstner",
|
|
||||||
"Haber",
|
|
||||||
"Imhof",
|
|
||||||
"Jung",
|
|
||||||
"Kraemer",
|
|
||||||
"Linden",
|
|
||||||
"Metzger",
|
|
||||||
"Niedermann",
|
|
||||||
"Opitz",
|
|
||||||
"Preuss",
|
|
||||||
"Raabe",
|
|
||||||
"Steinbach",
|
|
||||||
"Trautmann",
|
|
||||||
"Unger",
|
|
||||||
"Vollmer",
|
|
||||||
"Winterberg",
|
|
||||||
"Zahn",
|
|
||||||
"Auerbach",
|
|
||||||
"Bruckner",
|
|
||||||
"Dahlem",
|
|
||||||
"Eckhardt",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// East reach: Filipino, Korean, maritime Asian — distinctive identity.
|
|
||||||
const EAST_REACH_NAMES: &[&str] = &[
|
|
||||||
"Aquino",
|
|
||||||
"Bautista",
|
|
||||||
"Cruz",
|
|
||||||
"Dalisay",
|
|
||||||
"Espiritu",
|
|
||||||
"Flores",
|
|
||||||
"Garcia",
|
|
||||||
"Hernandez",
|
|
||||||
"Ilagan",
|
|
||||||
"Jeon",
|
|
||||||
"Kim",
|
|
||||||
"Lim",
|
|
||||||
"Magalang",
|
|
||||||
"Navarro",
|
|
||||||
"Ocampo",
|
|
||||||
"Park",
|
|
||||||
"Quijano",
|
|
||||||
"Reyes",
|
|
||||||
"Santos",
|
|
||||||
"Tan",
|
|
||||||
"Uy",
|
|
||||||
"Villanueva",
|
|
||||||
"Wong",
|
|
||||||
"Yoo",
|
|
||||||
"Aguilar",
|
|
||||||
"Buenaventura",
|
|
||||||
"Castillo",
|
|
||||||
"Dizon",
|
|
||||||
"Enriquez",
|
|
||||||
"Fernandez",
|
|
||||||
"Gonzales",
|
|
||||||
"Hwang",
|
|
||||||
"Ignacio",
|
|
||||||
"Jeong",
|
|
||||||
"Kwon",
|
|
||||||
"Lee",
|
|
||||||
"Marasigan",
|
|
||||||
"Nakamura",
|
|
||||||
"Oh",
|
|
||||||
"Perez",
|
|
||||||
"Ramos",
|
|
||||||
"Son",
|
|
||||||
"Tolentino",
|
|
||||||
"Umali",
|
|
||||||
"Valdez",
|
|
||||||
"Yun",
|
|
||||||
"Zamora",
|
|
||||||
"Baek",
|
|
||||||
"Choi",
|
|
||||||
"Dela Cruz",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Deep frontier: mixed backgrounds from all settler waves — no dominant culture.
|
|
||||||
const FRONTIER_NAMES: &[&str] = &[
|
|
||||||
"Adeyemi",
|
|
||||||
"Bergstrom",
|
|
||||||
"Chandra",
|
|
||||||
"Duval",
|
|
||||||
"Emeka",
|
|
||||||
"Fonseca",
|
|
||||||
"Gupta",
|
|
||||||
"Hassan",
|
|
||||||
"Ibrahim",
|
|
||||||
"Jansson",
|
|
||||||
"Kovac",
|
|
||||||
"Liu",
|
|
||||||
"Martinez",
|
|
||||||
"Nkosi",
|
|
||||||
"Okafor",
|
|
||||||
"Patel",
|
|
||||||
"Quinn",
|
|
||||||
"Rodriguez",
|
|
||||||
"Sousa",
|
|
||||||
"Thorne",
|
|
||||||
"Uddin",
|
|
||||||
"Varga",
|
|
||||||
"Wu",
|
|
||||||
"Xiong",
|
|
||||||
"Yoshida",
|
|
||||||
"Zhao",
|
|
||||||
"Abara",
|
|
||||||
"Beaumont",
|
|
||||||
"Cardenas",
|
|
||||||
"Doyle",
|
|
||||||
"Ekwueme",
|
|
||||||
"Ferreira",
|
|
||||||
"Gomes",
|
|
||||||
"Henriksen",
|
|
||||||
"Idris",
|
|
||||||
"Juma",
|
|
||||||
"Kato",
|
|
||||||
"Larsen",
|
|
||||||
"Morales",
|
|
||||||
"Ndlovu",
|
|
||||||
"Osei",
|
|
||||||
"Petrov",
|
|
||||||
"Ruiz",
|
|
||||||
"Singh",
|
|
||||||
"Tavares",
|
|
||||||
"Uchida",
|
|
||||||
"Volkov",
|
|
||||||
"Wang",
|
|
||||||
"Yang",
|
|
||||||
"Zaman",
|
|
||||||
];
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Business suffix pools by lore category
|
// Business suffix pools by lore category
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
//! Shared surname corpus for the economy generator binaries (T-1064).
|
||||||
|
//!
|
||||||
|
//! Single source of truth for the corridor/sector surname pools used by both
|
||||||
|
//! `generate_brands` (product names) and `generate_corporations` (business
|
||||||
|
//! names). The pools were previously duplicated in each binary's `names.rs`;
|
||||||
|
//! they are extracted here so the corpora cannot drift apart.
|
||||||
|
//!
|
||||||
|
//! Included via `#[path = "../shared/surname_corpus.rs"]` from each binary's
|
||||||
|
//! `main.rs` — this directory is not a cargo bin target (no `main.rs`).
|
||||||
|
//!
|
||||||
|
//! NOTE: both binaries feed the systems.db meta stamp — this file is part of
|
||||||
|
//! the `import_economics` source set in `tooling/check-systems-db-stamp`
|
||||||
|
//! (GENERATOR_SOURCES). Changing any pool requires `make regen-db`.
|
||||||
|
//! Pool contents are surname data drawn from founding cultures in wiki canon;
|
||||||
|
//! ordering is load-bearing (index-based RNG draws), so never reorder.
|
||||||
|
|
||||||
|
/// Core systems: cosmopolitan mix — the Reach's center of gravity.
|
||||||
|
pub const CORE_NAMES: &[&str] = &[
|
||||||
|
"Alvarez",
|
||||||
|
"Benoit",
|
||||||
|
"Carvalho",
|
||||||
|
"Durand",
|
||||||
|
"Eriksen",
|
||||||
|
"Fournier",
|
||||||
|
"Gao",
|
||||||
|
"Hartmann",
|
||||||
|
"Ishida",
|
||||||
|
"Johansson",
|
||||||
|
"Kirchner",
|
||||||
|
"Lemaire",
|
||||||
|
"Moreau",
|
||||||
|
"Nakamura",
|
||||||
|
"Olsson",
|
||||||
|
"Pelletier",
|
||||||
|
"Richter",
|
||||||
|
"Saito",
|
||||||
|
"Torres",
|
||||||
|
"Ueda",
|
||||||
|
"Vasquez",
|
||||||
|
"Werner",
|
||||||
|
"Xu",
|
||||||
|
"Yamada",
|
||||||
|
"Zhou",
|
||||||
|
"Andersen",
|
||||||
|
"Beaumont",
|
||||||
|
"Costa",
|
||||||
|
"Delacroix",
|
||||||
|
"Engel",
|
||||||
|
"Fujita",
|
||||||
|
"Gutierrez",
|
||||||
|
"Hayashi",
|
||||||
|
"Ibarra",
|
||||||
|
"Jensen",
|
||||||
|
"Klein",
|
||||||
|
"Laurent",
|
||||||
|
"Mercier",
|
||||||
|
"Novak",
|
||||||
|
"Ortiz",
|
||||||
|
"Park",
|
||||||
|
"Reuter",
|
||||||
|
"Suzuki",
|
||||||
|
"Takahashi",
|
||||||
|
"Ulrich",
|
||||||
|
"Valentin",
|
||||||
|
"Wagner",
|
||||||
|
"Xie",
|
||||||
|
"Yilmaz",
|
||||||
|
"Zhang",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// North reach: Nordic, Scottish, northern European — Calloway heritage.
|
||||||
|
pub const NORTH_REACH_NAMES: &[&str] = &[
|
||||||
|
"Andersson",
|
||||||
|
"Bjornsson",
|
||||||
|
"Calloway",
|
||||||
|
"Dalsgaard",
|
||||||
|
"Eklund",
|
||||||
|
"Falk",
|
||||||
|
"Grimstad",
|
||||||
|
"Hedlund",
|
||||||
|
"Ivarsson",
|
||||||
|
"Jonasson",
|
||||||
|
"Kirkpatrick",
|
||||||
|
"Lindqvist",
|
||||||
|
"MacLeod",
|
||||||
|
"Nordstrom",
|
||||||
|
"Olafsson",
|
||||||
|
"Pettersson",
|
||||||
|
"Rehn",
|
||||||
|
"Strandberg",
|
||||||
|
"Thorsen",
|
||||||
|
"Ulvskog",
|
||||||
|
"Vikstrom",
|
||||||
|
"Wahlberg",
|
||||||
|
"Aberg",
|
||||||
|
"Berglund",
|
||||||
|
"Carlsen",
|
||||||
|
"Dalgaard",
|
||||||
|
"Engstrom",
|
||||||
|
"Forsell",
|
||||||
|
"Gustafsson",
|
||||||
|
"Halvorsen",
|
||||||
|
"Ingvarsson",
|
||||||
|
"Jansson",
|
||||||
|
"Knudsen",
|
||||||
|
"Lundin",
|
||||||
|
"MacPherson",
|
||||||
|
"Nylund",
|
||||||
|
"Ostergaard",
|
||||||
|
"Palsson",
|
||||||
|
"Rasmussen",
|
||||||
|
"Sjoberg",
|
||||||
|
"Toft",
|
||||||
|
"Ulfsson",
|
||||||
|
"Vestergaard",
|
||||||
|
"Wiklund",
|
||||||
|
"Aasen",
|
||||||
|
"Brannstrom",
|
||||||
|
"Dahl",
|
||||||
|
"Eide",
|
||||||
|
"Friberg",
|
||||||
|
"Gren",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// South reach: Eastern European, East Asian industrial — Stalownia corridor.
|
||||||
|
pub const SOUTH_REACH_NAMES: &[&str] = &[
|
||||||
|
"Adamski",
|
||||||
|
"Baranov",
|
||||||
|
"Chernov",
|
||||||
|
"Dubois",
|
||||||
|
"Egorov",
|
||||||
|
"Filipov",
|
||||||
|
"Gromov",
|
||||||
|
"Horvat",
|
||||||
|
"Ivanova",
|
||||||
|
"Jankovic",
|
||||||
|
"Kowalski",
|
||||||
|
"Lazarev",
|
||||||
|
"Morozov",
|
||||||
|
"Novikov",
|
||||||
|
"Ostrowski",
|
||||||
|
"Petrov",
|
||||||
|
"Reznik",
|
||||||
|
"Sokolov",
|
||||||
|
"Tkachenko",
|
||||||
|
"Uvarov",
|
||||||
|
"Volkov",
|
||||||
|
"Wojcik",
|
||||||
|
"Yakimov",
|
||||||
|
"Zheng",
|
||||||
|
"Babic",
|
||||||
|
"Chernyshev",
|
||||||
|
"Dragunov",
|
||||||
|
"Fedorov",
|
||||||
|
"Grushevsky",
|
||||||
|
"Havel",
|
||||||
|
"Ito",
|
||||||
|
"Jovanovic",
|
||||||
|
"Katsaros",
|
||||||
|
"Lebedev",
|
||||||
|
"Mazur",
|
||||||
|
"Nemec",
|
||||||
|
"Ochoa",
|
||||||
|
"Popov",
|
||||||
|
"Radic",
|
||||||
|
"Smirnov",
|
||||||
|
"Tanaka",
|
||||||
|
"Urasawa",
|
||||||
|
"Vasiliev",
|
||||||
|
"Watanabe",
|
||||||
|
"Xiang",
|
||||||
|
"Yegorov",
|
||||||
|
"Zaytsev",
|
||||||
|
"Borysko",
|
||||||
|
"Chen",
|
||||||
|
"Dimitrov",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// West reach: German, Central European — Compact territory, Westphalian influence.
|
||||||
|
pub const WEST_REACH_NAMES: &[&str] = &[
|
||||||
|
"Albrecht",
|
||||||
|
"Baumann",
|
||||||
|
"Christensen",
|
||||||
|
"Dietrich",
|
||||||
|
"Eisenberg",
|
||||||
|
"Fischer",
|
||||||
|
"Gruber",
|
||||||
|
"Hoffmann",
|
||||||
|
"Ingolstadt",
|
||||||
|
"Jaeger",
|
||||||
|
"Kessler",
|
||||||
|
"Lehmann",
|
||||||
|
"Mueller",
|
||||||
|
"Neumann",
|
||||||
|
"Obermann",
|
||||||
|
"Pfeiffer",
|
||||||
|
"Quandt",
|
||||||
|
"Roth",
|
||||||
|
"Schaefer",
|
||||||
|
"Thiel",
|
||||||
|
"Urban",
|
||||||
|
"Vogt",
|
||||||
|
"Weidenfeld",
|
||||||
|
"Ziegler",
|
||||||
|
"Becker",
|
||||||
|
"Claussen",
|
||||||
|
"Dorfmann",
|
||||||
|
"Eberhardt",
|
||||||
|
"Fleischer",
|
||||||
|
"Gerstner",
|
||||||
|
"Haber",
|
||||||
|
"Imhof",
|
||||||
|
"Jung",
|
||||||
|
"Kraemer",
|
||||||
|
"Linden",
|
||||||
|
"Metzger",
|
||||||
|
"Niedermann",
|
||||||
|
"Opitz",
|
||||||
|
"Preuss",
|
||||||
|
"Raabe",
|
||||||
|
"Steinbach",
|
||||||
|
"Trautmann",
|
||||||
|
"Unger",
|
||||||
|
"Vollmer",
|
||||||
|
"Winterberg",
|
||||||
|
"Zahn",
|
||||||
|
"Auerbach",
|
||||||
|
"Bruckner",
|
||||||
|
"Dahlem",
|
||||||
|
"Eckhardt",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// East reach: Filipino, Korean, maritime Asian — distinctive identity.
|
||||||
|
pub const EAST_REACH_NAMES: &[&str] = &[
|
||||||
|
"Aquino",
|
||||||
|
"Bautista",
|
||||||
|
"Cruz",
|
||||||
|
"Dalisay",
|
||||||
|
"Espiritu",
|
||||||
|
"Flores",
|
||||||
|
"Garcia",
|
||||||
|
"Hernandez",
|
||||||
|
"Ilagan",
|
||||||
|
"Jeon",
|
||||||
|
"Kim",
|
||||||
|
"Lim",
|
||||||
|
"Magalang",
|
||||||
|
"Navarro",
|
||||||
|
"Ocampo",
|
||||||
|
"Park",
|
||||||
|
"Quijano",
|
||||||
|
"Reyes",
|
||||||
|
"Santos",
|
||||||
|
"Tan",
|
||||||
|
"Uy",
|
||||||
|
"Villanueva",
|
||||||
|
"Wong",
|
||||||
|
"Yoo",
|
||||||
|
"Aguilar",
|
||||||
|
"Buenaventura",
|
||||||
|
"Castillo",
|
||||||
|
"Dizon",
|
||||||
|
"Enriquez",
|
||||||
|
"Fernandez",
|
||||||
|
"Gonzales",
|
||||||
|
"Hwang",
|
||||||
|
"Ignacio",
|
||||||
|
"Jeong",
|
||||||
|
"Kwon",
|
||||||
|
"Lee",
|
||||||
|
"Marasigan",
|
||||||
|
"Nakamura",
|
||||||
|
"Oh",
|
||||||
|
"Perez",
|
||||||
|
"Ramos",
|
||||||
|
"Son",
|
||||||
|
"Tolentino",
|
||||||
|
"Umali",
|
||||||
|
"Valdez",
|
||||||
|
"Yun",
|
||||||
|
"Zamora",
|
||||||
|
"Baek",
|
||||||
|
"Choi",
|
||||||
|
"Dela Cruz",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Deep frontier: mixed backgrounds from all settler waves — no dominant culture.
|
||||||
|
pub const FRONTIER_NAMES: &[&str] = &[
|
||||||
|
"Adeyemi",
|
||||||
|
"Bergstrom",
|
||||||
|
"Chandra",
|
||||||
|
"Duval",
|
||||||
|
"Emeka",
|
||||||
|
"Fonseca",
|
||||||
|
"Gupta",
|
||||||
|
"Hassan",
|
||||||
|
"Ibrahim",
|
||||||
|
"Jansson",
|
||||||
|
"Kovac",
|
||||||
|
"Liu",
|
||||||
|
"Martinez",
|
||||||
|
"Nkosi",
|
||||||
|
"Okafor",
|
||||||
|
"Patel",
|
||||||
|
"Quinn",
|
||||||
|
"Rodriguez",
|
||||||
|
"Sousa",
|
||||||
|
"Thorne",
|
||||||
|
"Uddin",
|
||||||
|
"Varga",
|
||||||
|
"Wu",
|
||||||
|
"Xiong",
|
||||||
|
"Yoshida",
|
||||||
|
"Zhao",
|
||||||
|
"Abara",
|
||||||
|
"Beaumont",
|
||||||
|
"Cardenas",
|
||||||
|
"Doyle",
|
||||||
|
"Ekwueme",
|
||||||
|
"Ferreira",
|
||||||
|
"Gomes",
|
||||||
|
"Henriksen",
|
||||||
|
"Idris",
|
||||||
|
"Juma",
|
||||||
|
"Kato",
|
||||||
|
"Larsen",
|
||||||
|
"Morales",
|
||||||
|
"Ndlovu",
|
||||||
|
"Osei",
|
||||||
|
"Petrov",
|
||||||
|
"Ruiz",
|
||||||
|
"Singh",
|
||||||
|
"Tavares",
|
||||||
|
"Uchida",
|
||||||
|
"Volkov",
|
||||||
|
"Wang",
|
||||||
|
"Yang",
|
||||||
|
"Zaman",
|
||||||
|
];
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
//!
|
//!
|
||||||
//! Note: HashSet is used as a per-frame lookup table (visible tiles/NPCs).
|
//! Note: HashSet is used as a per-frame lookup table (visible tiles/NPCs).
|
||||||
//! Only membership checks — iteration order is irrelevant. Not simulation state.
|
//! Only membership checks — iteration order is irrelevant. Not simulation state.
|
||||||
|
//! Consumer contract: every consumer must sort (or otherwise impose a
|
||||||
|
//! deterministic order on) this data before it touches simulation state or
|
||||||
|
//! the wire.
|
||||||
#![allow(clippy::disallowed_types)]
|
#![allow(clippy::disallowed_types)]
|
||||||
|
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
//! Note: HashMap is used for `sector_lookup` — a per-frame scratch buffer
|
//! Note: HashMap is used for `sector_lookup` — a per-frame scratch buffer
|
||||||
//! looked up only by key. Iteration order is irrelevant here. Not subject to
|
//! looked up only by key. Iteration order is irrelevant here. Not subject to
|
||||||
//! the simulation determinism constraint (see server/.clippy.toml).
|
//! the simulation determinism constraint (see server/.clippy.toml).
|
||||||
|
//! Consumer contract: every consumer must sort (or otherwise impose a
|
||||||
|
//! deterministic order on) this data before it touches simulation state or
|
||||||
|
//! the wire.
|
||||||
#![allow(clippy::disallowed_types)]
|
#![allow(clippy::disallowed_types)]
|
||||||
|
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
//! tile positions during the FOV sweep. Only `insert` and `contains` are used;
|
//! tile positions during the FOV sweep. Only `insert` and `contains` are used;
|
||||||
//! iteration order never affects the output (results are handed to BTreeSet in
|
//! iteration order never affects the output (results are handed to BTreeSet in
|
||||||
//! query.rs). Not simulation state — exempt from the determinism constraint.
|
//! query.rs). Not simulation state — exempt from the determinism constraint.
|
||||||
|
//! Consumer contract: every consumer must sort (or otherwise impose a
|
||||||
|
//! deterministic order on) this data before it touches simulation state or
|
||||||
|
//! the wire.
|
||||||
#![allow(clippy::disallowed_types)]
|
#![allow(clippy::disallowed_types)]
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|||||||
@@ -380,3 +380,257 @@ pub fn try_load_economy(run_seed: u64) -> Option<(EconSimResource, EconStateReso
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests (T-1064): D-181 signal derivation in rebuild_signals.
|
||||||
|
//
|
||||||
|
// Built on a minimal in-memory econ_sim::Simulation (one system, one
|
||||||
|
// commodity) via Simulation::from_economy — no systems.db involved. Node
|
||||||
|
// commodity state is set directly through the public `sim.nodes` field so
|
||||||
|
// each test controls the exact price/supply/stockpile/demand sequence the
|
||||||
|
// signals are derived from.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use econ_sim::db::{Commodity, Economy, SystemInfo};
|
||||||
|
|
||||||
|
const SYS: &str = "sys-test";
|
||||||
|
const COM: &str = "ore";
|
||||||
|
|
||||||
|
fn minimal_economy() -> Economy {
|
||||||
|
let commodity = Commodity {
|
||||||
|
id: COM.to_string(),
|
||||||
|
name: "Test Ore".to_string(),
|
||||||
|
tier: "raw".to_string(),
|
||||||
|
base_price: 10.0,
|
||||||
|
elasticity: "normal".to_string(),
|
||||||
|
production_ubiquity: None,
|
||||||
|
demand_model: "population".to_string(),
|
||||||
|
};
|
||||||
|
let system = SystemInfo {
|
||||||
|
system_id: SYS.to_string(),
|
||||||
|
proper_name: None,
|
||||||
|
// Non-zero population activates the node in model::init_nodes.
|
||||||
|
population: 1_000,
|
||||||
|
cultural_corridor: None,
|
||||||
|
gate_energy_connected: true,
|
||||||
|
currency_zone: "TRACTUS_PRIMARY".to_string(),
|
||||||
|
hop_distance: 0,
|
||||||
|
gate_topology: None,
|
||||||
|
political_zone: None,
|
||||||
|
};
|
||||||
|
Economy {
|
||||||
|
commodities: vec![commodity.clone()],
|
||||||
|
commodity_map: BTreeMap::from([(COM.to_string(), commodity)]),
|
||||||
|
chains: Vec::new(),
|
||||||
|
chains_by_output: BTreeMap::new(),
|
||||||
|
systems: BTreeMap::from([(SYS.to_string(), system)]),
|
||||||
|
corp_presences: Vec::new(),
|
||||||
|
presences_by_system: BTreeMap::new(),
|
||||||
|
gate_links: Vec::new(),
|
||||||
|
corp_archetype_data: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_resources() -> (EconSimResource, EconStateResource) {
|
||||||
|
let sim = Simulation::from_economy(minimal_economy(), 42);
|
||||||
|
let econ = EconSimResource::new(sim);
|
||||||
|
assert!(
|
||||||
|
econ.sim.nodes.contains_key(SYS),
|
||||||
|
"test system must be an active node"
|
||||||
|
);
|
||||||
|
(econ, EconStateResource::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the test node's commodity state, then rebuild signals as one
|
||||||
|
/// economy tick would.
|
||||||
|
fn set_state_and_rebuild(
|
||||||
|
econ: &mut EconSimResource,
|
||||||
|
state: &mut EconStateResource,
|
||||||
|
econ_tick: u64,
|
||||||
|
price: f64,
|
||||||
|
supply: f64,
|
||||||
|
stockpile: f64,
|
||||||
|
demand: f64,
|
||||||
|
) {
|
||||||
|
let cs = econ
|
||||||
|
.sim
|
||||||
|
.nodes
|
||||||
|
.get_mut(SYS)
|
||||||
|
.expect("test node active")
|
||||||
|
.commodities
|
||||||
|
.get_mut(COM)
|
||||||
|
.expect("test commodity present");
|
||||||
|
cs.price = price;
|
||||||
|
cs.supply = supply;
|
||||||
|
cs.stockpile = stockpile;
|
||||||
|
cs.demand = demand;
|
||||||
|
rebuild_signals(econ, state, econ_tick, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signals(state: &EconStateResource) -> &EconNodeSignals {
|
||||||
|
state
|
||||||
|
.signals
|
||||||
|
.get(&(SYS.to_string(), COM.to_string()))
|
||||||
|
.expect("signals present for active node")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Signal 2: price trend windowing --------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trend_is_zero_with_single_history_entry() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 1, 10.0, 1.0, 1.0, 1.0);
|
||||||
|
assert_eq!(signals(&state).price_trend, 0.0);
|
||||||
|
assert_eq!(signals(&state).price_current, 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trend_under_window_spans_full_history() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// 3 ticks (< TREND_WINDOW = 5): trend = current − oldest = 16 − 10.
|
||||||
|
for (tick, price) in [(1u64, 10.0), (2, 12.0), (3, 16.0)] {
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, tick, price, 1.0, 1.0, 1.0);
|
||||||
|
}
|
||||||
|
assert_eq!(signals(&state).price_trend, 6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trend_at_exact_window_spans_window() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// Exactly TREND_WINDOW prices: 10..14 → trend = 14 − 10.
|
||||||
|
for i in 0..TREND_WINDOW {
|
||||||
|
let price = 10.0 + i as f64;
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, i as u64 + 1, price, 1.0, 1.0, 1.0);
|
||||||
|
}
|
||||||
|
assert_eq!(signals(&state).price_trend, (TREND_WINDOW - 1) as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trend_over_window_slides_oldest_out() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// 7 monotonically rising prices 10..16; the ring buffer keeps the last
|
||||||
|
// TREND_WINDOW (5) entries [12..16] → trend = 16 − 12, NOT 16 − 10.
|
||||||
|
for i in 0..7u64 {
|
||||||
|
let price = 10.0 + i as f64;
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, i + 1, price, 1.0, 1.0, 1.0);
|
||||||
|
}
|
||||||
|
assert_eq!(signals(&state).price_trend, (TREND_WINDOW - 1) as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trend_is_negative_when_price_falls() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
for (tick, price) in [(1u64, 20.0), (2, 15.0)] {
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, tick, price, 1.0, 1.0, 1.0);
|
||||||
|
}
|
||||||
|
assert_eq!(signals(&state).price_trend, -5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Signal 6: first-tick baseline capture --------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn baseline_captured_on_first_tick_and_held() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// First rebuild records supply 50 as the permanent baseline.
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 1, 10.0, 50.0, 1.0, 1.0);
|
||||||
|
assert_eq!(signals(&state).production_vs_baseline, 1.0);
|
||||||
|
|
||||||
|
// Later supply is measured against that first-tick baseline.
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 2, 10.0, 25.0, 1.0, 1.0);
|
||||||
|
assert_eq!(signals(&state).production_vs_baseline, 0.5);
|
||||||
|
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 3, 10.0, 100.0, 1.0, 1.0);
|
||||||
|
assert_eq!(signals(&state).production_vs_baseline, 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_baseline_yields_unity_ratio() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// init_nodes warm-starts supply at 0.0 — a zero first-tick baseline
|
||||||
|
// must not divide; the signal pins at 1.0 (at-baseline) instead.
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 1, 10.0, 0.0, 1.0, 1.0);
|
||||||
|
assert_eq!(signals(&state).production_vs_baseline, 1.0);
|
||||||
|
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 2, 10.0, 37.0, 1.0, 1.0);
|
||||||
|
let s = signals(&state);
|
||||||
|
assert_eq!(s.production_vs_baseline, 1.0);
|
||||||
|
assert!(s.production_vs_baseline.is_finite());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Signal 5: stockpile_weeks zero-demand edge ----------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_demand_stockpile_weeks_is_zero_not_infinite() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 1, 10.0, 1.0, 500.0, 0.0);
|
||||||
|
let s = signals(&state);
|
||||||
|
assert_eq!(s.stockpile_weeks, 0.0);
|
||||||
|
assert!(s.stockpile_weeks.is_finite());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stockpile_weeks_divides_by_weekly_demand() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// demand 10/tick → 70/week; stockpile 140 → 2 weeks.
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 1, 10.0, 1.0, 140.0, 10.0);
|
||||||
|
assert_eq!(signals(&state).stockpile_weeks, 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Cross-cutting: remaining signals + state bookkeeping ------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rebuild_populates_remaining_signals_and_metadata() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 9, 12.5, 33.0, 70.0, 10.0);
|
||||||
|
|
||||||
|
assert_eq!(state.econ_tick, 9);
|
||||||
|
assert_eq!(state.tractus_mark_rate, 1.0);
|
||||||
|
assert_eq!(state.signals.len(), 1);
|
||||||
|
|
||||||
|
let shadow_intensity = econ
|
||||||
|
.sim
|
||||||
|
.shadow()
|
||||||
|
.intensity
|
||||||
|
.get(SYS)
|
||||||
|
.copied()
|
||||||
|
.expect("shadow intensity seeded for test system");
|
||||||
|
let s = signals(&state);
|
||||||
|
assert_eq!(s.system_id, SYS);
|
||||||
|
assert_eq!(s.commodity_id, COM);
|
||||||
|
assert_eq!(s.price_current, 12.5);
|
||||||
|
// Signal 3 is a supply proxy in Phase 2.
|
||||||
|
assert_eq!(s.trade_flow_volume, 33.0);
|
||||||
|
// No corp presences in the minimal economy.
|
||||||
|
assert_eq!(s.corporate_presence, 0);
|
||||||
|
// Signal 7 derives directly from the seeded shadow intensity.
|
||||||
|
assert_eq!(s.official_coverage_ratio, 1.0 - shadow_intensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rebuild_clears_stale_signals() {
|
||||||
|
let (mut econ, mut state) = test_resources();
|
||||||
|
// Seed a stale entry for a node that no longer exists.
|
||||||
|
state.signals.insert(
|
||||||
|
("ghost-system".to_string(), COM.to_string()),
|
||||||
|
EconNodeSignals {
|
||||||
|
system_id: "ghost-system".to_string(),
|
||||||
|
commodity_id: COM.to_string(),
|
||||||
|
price_current: 0.0,
|
||||||
|
price_trend: 0.0,
|
||||||
|
trade_flow_volume: 0.0,
|
||||||
|
corporate_presence: 0,
|
||||||
|
stockpile_weeks: 0.0,
|
||||||
|
production_vs_baseline: 0.0,
|
||||||
|
official_coverage_ratio: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
set_state_and_rebuild(&mut econ, &mut state, 1, 10.0, 1.0, 1.0, 1.0);
|
||||||
|
assert_eq!(state.signals.len(), 1);
|
||||||
|
assert!(!state
|
||||||
|
.signals
|
||||||
|
.contains_key(&("ghost-system".to_string(), COM.to_string())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,10 +107,16 @@ impl Plugin for SimulationPlugin {
|
|||||||
|
|
||||||
// save_load is an exclusive system (takes &mut World).
|
// save_load is an exclusive system (takes &mut World).
|
||||||
// Exclusive systems in Bevy 0.18 cannot use .in_set() — use .before()/.after()
|
// Exclusive systems in Bevy 0.18 cannot use .in_set() — use .before()/.after()
|
||||||
// to position it within the Snapshot phase window.
|
// to position it within the Snapshot phase window (the sanctioned exception
|
||||||
|
// to the tick_phases.rs rules; see the note there).
|
||||||
|
// Both bounds matter: `.after(Storyteller)` pins the lower bound so the
|
||||||
|
// scheduler cannot legally run the save before Input/Simulation/Storyteller
|
||||||
|
// have executed — a save must capture post-Storyteller state, matching what
|
||||||
|
// compute_observer_snapshot is about to serialize (T-1064 / audit S-09).
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
save_io::execute_save_load
|
save_io::execute_save_load
|
||||||
|
.after(crate::tick_phases::TickPhase::Storyteller)
|
||||||
.before(crate::perception::observer::compute_observer_snapshot),
|
.before(crate::perception::observer::compute_observer_snapshot),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,16 @@
|
|||||||
//! the system is in the wrong phase.
|
//! the system is in the wrong phase.
|
||||||
//! 3. Intra-phase `.after()`/`.before()` is allowed for systems within
|
//! 3. Intra-phase `.after()`/`.before()` is allowed for systems within
|
||||||
//! the same phase that have a real data dependency.
|
//! the same phase that have a real data dependency.
|
||||||
|
//!
|
||||||
|
//! **Exception — exclusive systems.** Exclusive systems (`&mut World`) cannot
|
||||||
|
//! use `.in_set()` in Bevy 0.18, so rule 1 is unsatisfiable for them. Instead
|
||||||
|
//! they MUST be pinned into their phase window with explicit constraints on
|
||||||
|
//! BOTH sides: `.after(TickPhase::<previous phase>)` for the lower bound and
|
||||||
|
//! `.before(...)`/`.after(...)` against systems or the phase sets around them
|
||||||
|
//! for the upper bound. A one-sided constraint leaves the scheduler free to
|
||||||
|
//! run the system anywhere earlier/later in the tick. Sole current instance:
|
||||||
|
//! `save_io::execute_save_load` (simulation/mod.rs), pinned after
|
||||||
|
//! `Storyteller` and before `compute_observer_snapshot` (Snapshot phase).
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
|
|||||||
+208
-9
@@ -5,12 +5,17 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Worker crash behavior
|
//! ## Worker crash behavior
|
||||||
//!
|
//!
|
||||||
//! TODO (#843): If a worker thread panics, in-flight requests on that thread
|
//! Handler panics are contained per-request (T-1063): the worker thread
|
||||||
//! are lost. The pool does NOT currently re-enqueue them. For Fallback-strategy
|
//! catches the unwind, logs an error, and keeps serving the queue — a
|
||||||
//! workers (voice, LLM) this is acceptable — the tick loop uses the default.
|
//! poisoned request cannot permanently shrink the pool. The in-flight
|
||||||
//! For ModalLock-strategy workers (chunk gen on gate transit), a lost request
|
//! request is still LOST: no result is ever delivered for it.
|
||||||
//! means the player waits forever. Future: add heartbeat monitoring and
|
//!
|
||||||
//! automatic re-enqueue on worker death.
|
//! TODO (#843): re-enqueue lost requests (needs heartbeat/ack bookkeeping).
|
||||||
|
//! For Fallback-strategy workers (voice, LLM) the loss is acceptable — the
|
||||||
|
//! tick loop uses the default. For ModalLock-strategy workers (chunk gen on
|
||||||
|
//! gate transit), a lost request means the player waits forever. Before any
|
||||||
|
//! ModalLock consumer ships (Phase 5), implement re-enqueue or downgrade
|
||||||
|
//! ModalLock to an error path.
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -88,9 +93,27 @@ where
|
|||||||
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
||||||
Ok(work) => {
|
Ok(work) => {
|
||||||
let strategy = work.strategy;
|
let strategy = work.strategy;
|
||||||
let result = handler(work.payload);
|
// Contain handler panics (T-1063): the thread must
|
||||||
if tx.send((result, strategy)).is_err() {
|
// survive a poisoned request or the pool permanently
|
||||||
break; // response channel closed
|
// loses capacity. The in-flight request is still lost
|
||||||
|
// (no result delivered) — re-enqueue is future work,
|
||||||
|
// see the module-level TODO (#843).
|
||||||
|
let result = std::panic::catch_unwind(
|
||||||
|
std::panic::AssertUnwindSafe(|| handler(work.payload)),
|
||||||
|
);
|
||||||
|
match result {
|
||||||
|
Ok(resp) => {
|
||||||
|
if tx.send((resp, strategy)).is_err() {
|
||||||
|
break; // response channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::error!(
|
||||||
|
strategy = ?strategy,
|
||||||
|
"worker handler panicked — in-flight request lost, \
|
||||||
|
no result will be delivered (#843)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
|
Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
|
||||||
@@ -159,3 +182,179 @@ impl<Req, Resp> Drop for BackgroundWorkerPool<Req, Resp> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests (T-1063): spawn/push/poll/shutdown lifecycle + worker-panic behavior.
|
||||||
|
//
|
||||||
|
// Every test also exercises Drop implicitly — a pool whose threads fail to
|
||||||
|
// exit would hang the test binary at scope end.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const WAIT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Spin until `cond` holds, panicking after `WAIT`. Worker completion is
|
||||||
|
/// timing-dependent (thread scheduling), so tests wait on observable
|
||||||
|
/// state instead of sleeping fixed amounts.
|
||||||
|
fn wait_for(what: &str, mut cond: impl FnMut() -> bool) {
|
||||||
|
let start = Instant::now();
|
||||||
|
while !cond() {
|
||||||
|
if start.elapsed() > WAIT {
|
||||||
|
panic!("timed out after {WAIT:?} waiting for: {what}");
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_push_poll_round_trip() {
|
||||||
|
let pool = BackgroundWorkerPool::spawn(2, |x: u32| x * 2);
|
||||||
|
assert!(pool.poll().is_none(), "fresh pool has no results");
|
||||||
|
assert!(!pool.has_results());
|
||||||
|
assert_eq!(pool.pending_count(), 0);
|
||||||
|
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: 21,
|
||||||
|
strategy: DeliveryStrategy::Fallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
wait_for("result ready", || pool.has_results());
|
||||||
|
let (resp, strategy) = pool.poll().expect("has_results implies poll succeeds");
|
||||||
|
assert_eq!(resp, 42);
|
||||||
|
assert_eq!(strategy, DeliveryStrategy::Fallback);
|
||||||
|
assert!(pool.poll().is_none(), "single request yields single result");
|
||||||
|
assert_eq!(pool.pending_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drain_collects_all_results_and_preserves_strategies() {
|
||||||
|
let pool = BackgroundWorkerPool::spawn(2, |x: u32| x + 1);
|
||||||
|
for i in 0..10u32 {
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: i,
|
||||||
|
strategy: if i % 2 == 0 {
|
||||||
|
DeliveryStrategy::GracefulDegrade
|
||||||
|
} else {
|
||||||
|
DeliveryStrategy::ModalLock
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results: Vec<(u32, DeliveryStrategy)> = Vec::new();
|
||||||
|
wait_for("all 10 results drained", || {
|
||||||
|
results.extend(pool.drain());
|
||||||
|
results.len() == 10
|
||||||
|
});
|
||||||
|
|
||||||
|
// Two worker threads — completion order is not guaranteed. Each
|
||||||
|
// request must come back exactly once, paired with its own strategy.
|
||||||
|
results.sort_by_key(|(resp, _)| *resp);
|
||||||
|
for (i, (resp, strategy)) in results.iter().enumerate() {
|
||||||
|
assert_eq!(*resp, i as u32 + 1);
|
||||||
|
let expected = if i % 2 == 0 {
|
||||||
|
DeliveryStrategy::GracefulDegrade
|
||||||
|
} else {
|
||||||
|
DeliveryStrategy::ModalLock
|
||||||
|
};
|
||||||
|
assert_eq!(*strategy, expected, "strategy must travel with request {i}");
|
||||||
|
}
|
||||||
|
assert!(!pool.has_results(), "drain leaves the queue empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shutdown_stops_workers_and_drops_later_submissions() {
|
||||||
|
let pool = BackgroundWorkerPool::spawn(2, |x: u32| x);
|
||||||
|
pool.shutdown();
|
||||||
|
wait_for("all workers exited", || {
|
||||||
|
pool.workers.iter().all(|h| h.is_finished())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Once every worker has exited, all Receiver clones are gone (spawn
|
||||||
|
// drops the original), so the request channel is disconnected:
|
||||||
|
// submit() silently discards the request — by design (`let _ =` on
|
||||||
|
// send). Nothing is queued and no result ever appears.
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: 7,
|
||||||
|
strategy: DeliveryStrategy::Fallback,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
pool.pending_count(),
|
||||||
|
0,
|
||||||
|
"post-shutdown submissions are silently dropped, not queued"
|
||||||
|
);
|
||||||
|
assert!(pool.poll().is_none());
|
||||||
|
assert!(!pool.has_results());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Worker-panic behavior (T-1063) ----------------------------------------
|
||||||
|
|
||||||
|
/// Handler that panics on payload 0 and echoes anything else.
|
||||||
|
fn poison_handler(x: u32) -> u32 {
|
||||||
|
if x == 0 {
|
||||||
|
panic!("poisoned request");
|
||||||
|
}
|
||||||
|
x
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handler_panic_does_not_kill_the_worker_thread() {
|
||||||
|
// Single thread: if the panic killed it, the second request would
|
||||||
|
// never be processed and this test would time out.
|
||||||
|
let pool = BackgroundWorkerPool::spawn(1, poison_handler);
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: 0, // poison
|
||||||
|
strategy: DeliveryStrategy::ModalLock,
|
||||||
|
});
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: 5,
|
||||||
|
strategy: DeliveryStrategy::Fallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
wait_for("post-panic result", || pool.has_results());
|
||||||
|
let (resp, _) = pool.poll().expect("worker survived the panic");
|
||||||
|
assert_eq!(resp, 5);
|
||||||
|
assert!(
|
||||||
|
!pool.workers[0].is_finished(),
|
||||||
|
"worker thread must survive a handler panic"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// KNOWN LIMITATION (#843): the panicked request is silently lost — no
|
||||||
|
/// result is ever delivered for it and it is not re-enqueued. For a
|
||||||
|
/// ModalLock consumer this means the player waits forever; heartbeat +
|
||||||
|
/// re-enqueue (or downgrading ModalLock to an error path) must land
|
||||||
|
/// before any ModalLock consumer ships (Phase 5). This test pins the
|
||||||
|
/// current loss behavior so the eventual fix has to update it visibly.
|
||||||
|
#[test]
|
||||||
|
fn handler_panic_loses_the_inflight_request() {
|
||||||
|
let pool = BackgroundWorkerPool::spawn(1, poison_handler);
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: 0, // poison — would be the ModalLock request a player waits on
|
||||||
|
strategy: DeliveryStrategy::ModalLock,
|
||||||
|
});
|
||||||
|
// Sentinel: once its result arrives, the poison request has
|
||||||
|
// definitely been taken off the queue (single worker, FIFO channel).
|
||||||
|
pool.submit(WorkRequest {
|
||||||
|
payload: 99,
|
||||||
|
strategy: DeliveryStrategy::Fallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
wait_for("sentinel result", || pool.has_results());
|
||||||
|
let results = pool.drain();
|
||||||
|
assert_eq!(
|
||||||
|
results.len(),
|
||||||
|
1,
|
||||||
|
"only the sentinel completes — the poisoned request produced no result"
|
||||||
|
);
|
||||||
|
assert_eq!(results[0].0, 99);
|
||||||
|
assert_eq!(
|
||||||
|
pool.pending_count(),
|
||||||
|
0,
|
||||||
|
"the poisoned request is gone from the queue, not re-enqueued"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
//! The actual computation logic is a no-op placeholder — implementations
|
//! The actual computation logic is a no-op placeholder — implementations
|
||||||
//! plug in when the phases that need them arrive (Phase 5+).
|
//! plug in when the phases that need them arrive (Phase 5+).
|
||||||
//!
|
//!
|
||||||
//! The infrastructure (channels, threads, push/poll) is real and tested.
|
//! The infrastructure (channels, threads, push/poll) is real — unit tests
|
||||||
|
//! live in `pool.rs` (T-1063). Known limitation (#843): a handler panic
|
||||||
|
//! loses the in-flight request (no result is ever delivered, no re-enqueue);
|
||||||
|
//! see the crash-behavior notes in `pool.rs`.
|
||||||
|
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ GENERATOR_SOURCES: dict[str, list[Path]] = {
|
|||||||
REPO_ROOT / "tooling" / "economy-db" / "import_economics.py",
|
REPO_ROOT / "tooling" / "economy-db" / "import_economics.py",
|
||||||
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs",
|
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs",
|
||||||
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs",
|
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs",
|
||||||
|
# Shared surname corpus extracted from the two names.rs copies (T-1064).
|
||||||
|
REPO_ROOT / "server" / "src" / "bin" / "shared" / "surname_corpus.rs",
|
||||||
REPO_ROOT / "tooling" / "generate-brands",
|
REPO_ROOT / "tooling" / "generate-brands",
|
||||||
REPO_ROOT / "tooling" / "schema_version.py",
|
REPO_ROOT / "tooling" / "schema_version.py",
|
||||||
# D-237 authored specialization layer data TOMLs (#1013). Keep in sync
|
# D-237 authored specialization layer data TOMLs (#1013). Keep in sync
|
||||||
|
|||||||
@@ -54,13 +54,23 @@ impl Simulation {
|
|||||||
|
|
||||||
let conn = db::open_db(&db_pathbuf);
|
let conn = db::open_db(&db_pathbuf);
|
||||||
let economy = db::load_economy(&conn);
|
let economy = db::load_economy(&conn);
|
||||||
|
Ok(Self::from_economy(economy, run_seed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a simulation directly from an in-memory [`db::Economy`] — no DB file.
|
||||||
|
///
|
||||||
|
/// Identical initialization to [`Simulation::load`] after the DB read:
|
||||||
|
/// deterministic productivity/shadow seeding from `run_seed`, warm-start
|
||||||
|
/// node states. Intended for unit tests and embedded callers that
|
||||||
|
/// construct small, fully-controlled economies (T-1064).
|
||||||
|
pub fn from_economy(economy: db::Economy, run_seed: u64) -> Self {
|
||||||
let productivity = seed::seed_all_productivity(&economy, run_seed);
|
let productivity = seed::seed_all_productivity(&economy, run_seed);
|
||||||
let shadow = currency::seed_shadow_economy(&economy, run_seed);
|
let shadow = currency::seed_shadow_economy(&economy, run_seed);
|
||||||
let adjacency = trade::build_adjacency(&economy);
|
let adjacency = trade::build_adjacency(&economy);
|
||||||
let archetypes = agents::build_archetype_map(economy.corp_archetype_data.clone());
|
let archetypes = agents::build_archetype_map(economy.corp_archetype_data.clone());
|
||||||
let nodes = model::init_nodes(&economy);
|
let nodes = model::init_nodes(&economy);
|
||||||
|
|
||||||
Ok(Simulation {
|
Simulation {
|
||||||
economy,
|
economy,
|
||||||
productivity,
|
productivity,
|
||||||
shadow,
|
shadow,
|
||||||
@@ -72,7 +82,7 @@ impl Simulation {
|
|||||||
tick: 0,
|
tick: 0,
|
||||||
alpha: model::ALPHA,
|
alpha: model::ALPHA,
|
||||||
beta: trade::BETA,
|
beta: trade::BETA,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to load from the auto-detected DB path (same search as the CLI binary).
|
/// Try to load from the auto-detected DB path (same search as the CLI binary).
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ GENERATED_BRANDS_TOML = REPO_ROOT / "wiki" / "economics" / "corporations" / "gen
|
|||||||
# either must invalidate the meta stamp even though Python hasn't changed.
|
# either must invalidate the meta stamp even though Python hasn't changed.
|
||||||
GENERATE_BRANDS_RS = REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs"
|
GENERATE_BRANDS_RS = REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs"
|
||||||
GENERATE_BRANDS_NAMES_RS = REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs"
|
GENERATE_BRANDS_NAMES_RS = REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs"
|
||||||
|
# Shared surname corpus extracted from the two names.rs copies (T-1064).
|
||||||
|
GENERATE_BRANDS_SURNAMES_RS = REPO_ROOT / "server" / "src" / "bin" / "shared" / "surname_corpus.rs"
|
||||||
GENERATE_BRANDS_WRAPPER = REPO_ROOT / "tooling" / "generate-brands"
|
GENERATE_BRANDS_WRAPPER = REPO_ROOT / "tooling" / "generate-brands"
|
||||||
|
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = (
|
|||||||
Path(__file__),
|
Path(__file__),
|
||||||
GENERATE_BRANDS_RS,
|
GENERATE_BRANDS_RS,
|
||||||
GENERATE_BRANDS_NAMES_RS,
|
GENERATE_BRANDS_NAMES_RS,
|
||||||
|
GENERATE_BRANDS_SURNAMES_RS,
|
||||||
GENERATE_BRANDS_WRAPPER,
|
GENERATE_BRANDS_WRAPPER,
|
||||||
REPO_ROOT / "tooling" / "schema_version.py",
|
REPO_ROOT / "tooling" / "schema_version.py",
|
||||||
# D-237 authored specialization layer: these data TOMLs feed the DB, so a
|
# D-237 authored specialization layer: these data TOMLs feed the DB, so a
|
||||||
@@ -2155,8 +2158,8 @@ def _specialization_checks(conn, vocab, systems, strict):
|
|||||||
print(f" economic: {n_econ} authored ({n_inhabited} inhabited; rest on fallback once #1014 lands)")
|
print(f" economic: {n_econ} authored ({n_inhabited} inhabited; rest on fallback once #1014 lands)")
|
||||||
print(f" cultural: {n_cult} authored ({n_named} named; rest on corridor default)")
|
print(f" cultural: {n_cult} authored ({n_named} named; rest on corridor default)")
|
||||||
print(f" faction: {n_fac} authored ({n_named} named; rest on derivation)")
|
print(f" faction: {n_fac} authored ({n_named} named; rest on derivation)")
|
||||||
print(f" BulkClass: " + " | ".join(f"{k}={v}" for k, v in sorted(bulk_dist.items())))
|
print(" BulkClass: " + " | ".join(f"{k}={v}" for k, v in sorted(bulk_dist.items())))
|
||||||
print(f" ProductionUbiquity: " + " | ".join(f"{k}={v}" for k, v in sorted(pu_dist.items())))
|
print(" ProductionUbiquity: " + " | ".join(f"{k}={v}" for k, v in sorted(pu_dist.items())))
|
||||||
if warnings:
|
if warnings:
|
||||||
print(f" Specialization warnings ({len(warnings)}):")
|
print(f" Specialization warnings ({len(warnings)}):")
|
||||||
for w in warnings:
|
for w in warnings:
|
||||||
@@ -2363,10 +2366,10 @@ def main():
|
|||||||
print(f"error: {db_path} not found", file=sys.stderr)
|
print(f"error: {db_path} not found", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"\n Economics Import Pipeline")
|
print("\n Economics Import Pipeline")
|
||||||
print(f" DB: {db_path}")
|
print(f" DB: {db_path}")
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print(f" Mode: DRY RUN")
|
print(" Mode: DRY RUN")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Load wiki corps before opening DB — allows early exit on parse failures
|
# Load wiki corps before opening DB — allows early exit on parse failures
|
||||||
|
|||||||
Reference in New Issue
Block a user