refactor(simulation): move content/ under server/ (#669)
Content files (RON templates, YAML campaigns, gauntlet data) are consumed exclusively by the server. Colocate them at server/content/ and update all path references in Rust source, tooling scripts, and schema files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: ZoneTypeTemplate
|
||||
// Ticket: #661 (copy team)
|
||||
// Validate: tooling/validate-ron content/global/zone-types/industrial_freight.ron zone_type
|
||||
// Validate: tooling/validate-ron server/content/global/zone-types/industrial_freight.ron zone_type
|
||||
//
|
||||
// Sources: D-142 (zone-type template architecture).
|
||||
//
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: ZoneTypeTemplate
|
||||
// Ticket: #661 (copy team)
|
||||
// Validate: tooling/validate-ron content/global/zone-types/rural_agricultural.ron zone_type
|
||||
// Validate: tooling/validate-ron server/content/global/zone-types/rural_agricultural.ron zone_type
|
||||
//
|
||||
// Sources: D-142 (zone-type template architecture).
|
||||
//
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Drama Module Schema — Tier 1 Content (D-023)
|
||||
# YAML expression of JSON Schema 2020-12
|
||||
# Validated against this schema: content/modules/tier1/*.yaml
|
||||
# Validated against this schema: server/content/modules/tier1/*.yaml
|
||||
#
|
||||
# Ownership:
|
||||
# Dramatic structure (this file): Paula
|
||||
@@ -8,8 +8,8 @@
|
||||
//!
|
||||
//! Run twice, compare side-by-side:
|
||||
//! ```sh
|
||||
//! cargo run --bin generator_spike -- --zone rural --seed 42
|
||||
//! cargo run --bin generator_spike -- --zone industrial --seed 42
|
||||
//! cargo run --bin generator_spike -- --zone-type rural --seed 42
|
||||
//! cargo run --bin generator_spike -- --zone-type industrial --seed 42
|
||||
//! ```
|
||||
//!
|
||||
//! The test: can you tell which is which from the output alone?
|
||||
@@ -22,7 +22,7 @@
|
||||
//! functions that work without ECS. Full ECS integration is deferred.
|
||||
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
@@ -31,9 +31,9 @@ use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use settled_reach_server::npc::blueprint::{
|
||||
assemble_behaviors, BlueprintRelationship, CultureProfile, CulturalMarkers, CulturalValues,
|
||||
assemble_behaviors, BlueprintRelationship, CulturalMarkers, CulturalValues, CultureProfile,
|
||||
NamingConventions, NpcBlueprint, NpcWant, RoleSpec, SocialSiteSpec, SpeechPatterns,
|
||||
SpikeOutput, ZoneSpec,
|
||||
SpikeOutput, ZoneSpec, ZoneTypeTemplate,
|
||||
};
|
||||
use settled_reach_server::npc::PersonalityTrait;
|
||||
use settled_reach_server::simulation::rng::SimRng;
|
||||
@@ -48,9 +48,11 @@ use settled_reach_server::simulation::rng::SimRng;
|
||||
about = "NPC generator proof-of-life — Sprint 25 (#612)"
|
||||
)]
|
||||
struct Args {
|
||||
/// Zone type to generate ("rural" or "industrial").
|
||||
/// Zone type to generate.
|
||||
/// Phase 1 (hardcoded): "rural" or "industrial".
|
||||
/// Phase 2 (--from-files): a zone-type ID like "rural_agricultural" or "industrial_freight".
|
||||
#[arg(long)]
|
||||
zone: String,
|
||||
zone_type: String,
|
||||
|
||||
/// Deterministic seed — same seed produces identical output.
|
||||
#[arg(long)]
|
||||
@@ -60,8 +62,8 @@ struct Args {
|
||||
#[arg(long, default_value = "van-maanens-star")]
|
||||
culture: String,
|
||||
|
||||
/// Phase 2: load zone spec and culture from RON files on disk.
|
||||
/// Requires content/global/<zone>-zone-spec.ron and content/global/culture-<culture>.ron.
|
||||
/// Phase 2: load zone-type template and culture from RON files on disk.
|
||||
/// Requires server/content/global/zone-types/<zone_type>.ron and server/content/global/culture-<culture>.ron.
|
||||
#[arg(long)]
|
||||
from_files: bool,
|
||||
|
||||
@@ -268,21 +270,60 @@ fn hardcoded_van_maanens_star_culture() -> CultureProfile {
|
||||
// Phase 2: file loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn load_zone_from_file(content_root: &PathBuf, zone_type: &str) -> ZoneSpec {
|
||||
let path = content_root.join("global").join(format!("{}-zone-spec.ron", zone_type));
|
||||
fn load_zone_from_file(content_root: &Path, zone_type: &str) -> ZoneSpec {
|
||||
let path = content_root
|
||||
.join("global")
|
||||
.join("zone-types")
|
||||
.join(format!("{}.ron", zone_type));
|
||||
let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||
eprintln!("Error loading zone spec from {:?}: {}", path, e);
|
||||
eprintln!("Tip: copy team fills this file (ticket #609).");
|
||||
eprintln!("Error loading zone-type template from {:?}: {}", path, e);
|
||||
eprintln!("Tip: zone-type RON files live in content/global/zone-types/.");
|
||||
process::exit(1);
|
||||
});
|
||||
ron::from_str(&content).unwrap_or_else(|e| {
|
||||
eprintln!("Invalid ZoneSpec in {:?}: {}", path, e);
|
||||
let template: ZoneTypeTemplate = ron::from_str(&content).unwrap_or_else(|e| {
|
||||
eprintln!("Invalid ZoneTypeTemplate in {:?}: {}", path, e);
|
||||
process::exit(1);
|
||||
})
|
||||
});
|
||||
zone_type_template_to_zone_spec(template)
|
||||
}
|
||||
|
||||
fn load_culture_from_file(content_root: &PathBuf, culture_id: &str) -> CultureProfile {
|
||||
let path = content_root.join("global").join(format!("culture-{}.ron", culture_id));
|
||||
/// Convert a `ZoneTypeTemplate` to a `ZoneSpec` compatible with the spike generation pipeline.
|
||||
///
|
||||
/// `ZoneTypeTemplate` roles have no weight (weights live on `LocationSpec`).
|
||||
/// The spike assigns weight=1 uniformly — all roles equally likely.
|
||||
fn zone_type_template_to_zone_spec(template: ZoneTypeTemplate) -> ZoneSpec {
|
||||
ZoneSpec {
|
||||
zone_type: template.id,
|
||||
label: template.label,
|
||||
description: template.description,
|
||||
economic_level: template.economic_level,
|
||||
population_density: template.population_density,
|
||||
roles: template
|
||||
.roles
|
||||
.into_iter()
|
||||
.map(|mut r| {
|
||||
r.weight = 1; // uniform weight — location specs provide real weights
|
||||
r
|
||||
})
|
||||
.collect(),
|
||||
social_sites: template
|
||||
.social_site_types
|
||||
.into_iter()
|
||||
.map(|s| SocialSiteSpec {
|
||||
site_type: s.site_type,
|
||||
label: String::new(),
|
||||
roles: s.eligible_roles,
|
||||
min_npcs: 1,
|
||||
max_npcs: 6,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_culture_from_file(content_root: &Path, culture_id: &str) -> CultureProfile {
|
||||
let path = content_root
|
||||
.join("global")
|
||||
.join(format!("culture-{}.ron", culture_id));
|
||||
let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||
eprintln!("Error loading culture from {:?}: {}", path, e);
|
||||
eprintln!("Tip: copy team fills this file (ticket #610).");
|
||||
@@ -386,7 +427,11 @@ fn gen_traits(rng: &mut SimRng, culture: &CultureProfile) -> Vec<PersonalityTrai
|
||||
/// main-RNG word before reaching the Fisher-Yates step.
|
||||
fn name_pool_seed(seed: u64, zone_type: &str, culture_id: &str) -> u64 {
|
||||
let mut h: u64 = seed ^ 0xcbf29ce484222325; // FNV-1a offset basis, XOR'd with seed
|
||||
for b in zone_type.bytes().chain(std::iter::once(b':')).chain(culture_id.bytes()) {
|
||||
for b in zone_type
|
||||
.bytes()
|
||||
.chain(std::iter::once(b':'))
|
||||
.chain(culture_id.bytes())
|
||||
{
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(0x100000001b3); // FNV-1a prime
|
||||
}
|
||||
@@ -549,7 +594,8 @@ const MAX_FILLER_WORDS: usize = 2;
|
||||
|
||||
fn gen_cultural_markers(rng: &mut SimRng, culture: &CultureProfile) -> CulturalMarkers {
|
||||
let filler_count = if culture.speech.filler_words.len() > 1 {
|
||||
rng.rng.random_range(1_usize..=MAX_FILLER_WORDS.min(culture.speech.filler_words.len()))
|
||||
rng.rng
|
||||
.random_range(1_usize..=MAX_FILLER_WORDS.min(culture.speech.filler_words.len()))
|
||||
} else {
|
||||
culture.speech.filler_words.len()
|
||||
};
|
||||
@@ -595,7 +641,10 @@ fn gen_relationships(
|
||||
) -> Vec<BlueprintRelationship> {
|
||||
use settled_reach_server::npc::blueprint::RelationshipValence;
|
||||
|
||||
let other_names: Vec<&String> = all_names.iter().filter(|n| n.as_str() != this_name).collect();
|
||||
let other_names: Vec<&String> = all_names
|
||||
.iter()
|
||||
.filter(|n| n.as_str() != this_name)
|
||||
.collect();
|
||||
if other_names.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -649,9 +698,7 @@ fn generate_npc_blueprint(
|
||||
let role = pick_role(rng, zone);
|
||||
let traits = gen_traits(rng, culture);
|
||||
let want = gen_want(rng, &traits, role);
|
||||
let pool = behavior_pools
|
||||
.entry(role.id.clone())
|
||||
.or_insert_with(VecDeque::new);
|
||||
let pool = behavior_pools.entry(role.id.clone()).or_default();
|
||||
let observable_behaviors = gen_behaviors(rng, role, pool);
|
||||
let cultural_markers = gen_cultural_markers(rng, culture);
|
||||
// Relationships and Want tells assigned in later passes.
|
||||
@@ -807,9 +854,15 @@ fn gen_want_tell(rng: &mut SimRng, npc: &NpcBlueprint) -> Option<String> {
|
||||
AvoidingSomeone => match avoid_target {
|
||||
Some(name) => {
|
||||
let options = [
|
||||
format!("routes around {}'s usual area without looking toward it", name),
|
||||
format!(
|
||||
"routes around {}'s usual area without looking toward it",
|
||||
name
|
||||
),
|
||||
format!("takes the long way past {}'s position", name),
|
||||
format!("keeps a surface or cluster of people between them and {}", name),
|
||||
format!(
|
||||
"keeps a surface or cluster of people between them and {}",
|
||||
name
|
||||
),
|
||||
];
|
||||
options[rng.rng.random_range(0..options.len())].clone()
|
||||
}
|
||||
@@ -870,7 +923,10 @@ fn apply_relationship_behaviors(rng: &mut SimRng, npc: &mut NpcBlueprint) {
|
||||
}
|
||||
("friend", _) => format!("drifts toward {} between tasks", target),
|
||||
("subordinate", RelationshipValence::Negative) => {
|
||||
format!("complies when {} speaks, but doesn't volunteer anything", target)
|
||||
format!(
|
||||
"complies when {} speaks, but doesn't volunteer anything",
|
||||
target
|
||||
)
|
||||
}
|
||||
("subordinate", _) => format!("defers to {} before moving on", target),
|
||||
("superior", RelationshipValence::Negative) => {
|
||||
@@ -992,17 +1048,20 @@ fn main() {
|
||||
// Load zone spec and culture profile
|
||||
let (zone, culture) = if args.from_files {
|
||||
eprintln!("Phase 2: loading from files...");
|
||||
let zone = load_zone_from_file(&args.content_root, &args.zone);
|
||||
let zone = load_zone_from_file(&args.content_root, &args.zone_type);
|
||||
let culture = load_culture_from_file(&args.content_root, &args.culture);
|
||||
(zone, culture)
|
||||
} else {
|
||||
// Phase 1: hardcoded stubs
|
||||
let zone = match args.zone.as_str() {
|
||||
let zone = match args.zone_type.as_str() {
|
||||
"rural" => hardcoded_rural_zone(),
|
||||
"industrial" => hardcoded_industrial_zone(),
|
||||
other => {
|
||||
eprintln!("Unknown zone type: '{}'. Use 'rural' or 'industrial'.", other);
|
||||
eprintln!("(Phase 2 with --from-files supports additional zone types from disk)");
|
||||
eprintln!(
|
||||
"Unknown zone type: '{}'. Use 'rural' or 'industrial'.",
|
||||
other
|
||||
);
|
||||
eprintln!("(Phase 2 with --from-files supports zone-type IDs like 'rural_agricultural')");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
@@ -1046,7 +1105,7 @@ fn main() {
|
||||
// Pre-generate unique names using a derived RNG (#628 fix: first-pick bias).
|
||||
// Name ordering is a pure function of (seed, zone_type, culture) — independent
|
||||
// of how many words the main RNG has consumed for NPC count or other purposes.
|
||||
let name_pool = build_name_pool(args.seed, &args.zone, &culture, npc_count);
|
||||
let name_pool = build_name_pool(args.seed, &args.zone_type, &culture, npc_count);
|
||||
|
||||
// Pre-shuffle behavior pools per role (#629 fix: dedup within a zone run).
|
||||
// D-139: uses assemble_behaviors() when behavior_primitives are present.
|
||||
@@ -1055,9 +1114,7 @@ fn main() {
|
||||
// First pass: generate all NPCs (no relationships yet)
|
||||
let mut npcs: Vec<NpcBlueprint> = name_pool
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
generate_npc_blueprint(&mut rng, &zone, &culture, name, &mut behavior_pools)
|
||||
})
|
||||
.map(|name| generate_npc_blueprint(&mut rng, &zone, &culture, name, &mut behavior_pools))
|
||||
.collect();
|
||||
|
||||
// Collect all names for relationship pass.
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//!
|
||||
//! ```sh
|
||||
//! # Via wrapper script (recommended):
|
||||
//! tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
||||
//! tooling/validate-ron content/global/culture-van-maanens-star.example.ron culture
|
||||
//! tooling/validate-ron server/content/global/zone-identity-spec.example.ron zone
|
||||
//! tooling/validate-ron server/content/global/culture-van-maanens-star.example.ron culture
|
||||
//!
|
||||
//! # Direct:
|
||||
//! cargo run --bin validate_ron -- <file.ron> <zone|culture>
|
||||
@@ -19,7 +19,7 @@ use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec};
|
||||
use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec, ZoneTypeTemplate};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
@@ -29,7 +29,7 @@ use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec};
|
||||
struct Args {
|
||||
/// Path to the RON file to validate.
|
||||
file: String,
|
||||
/// Schema type: "zone" (ZoneSpec) or "culture" (CultureProfile).
|
||||
/// Schema type: "zone" (ZoneSpec), "zone_type" (ZoneTypeTemplate), or "culture" (CultureProfile).
|
||||
schema: String,
|
||||
}
|
||||
|
||||
@@ -48,7 +48,11 @@ fn main() {
|
||||
"zone" => match ron::from_str::<ZoneSpec>(&content) {
|
||||
Ok(spec) => {
|
||||
println!("Valid ZoneSpec: {} ({})", spec.label, spec.zone_type);
|
||||
println!(" {} roles, {} social sites", spec.roles.len(), spec.social_sites.len());
|
||||
println!(
|
||||
" {} roles, {} social sites",
|
||||
spec.roles.len(),
|
||||
spec.social_sites.len()
|
||||
);
|
||||
let mut warnings = 0;
|
||||
if spec.roles.is_empty() {
|
||||
eprintln!(" WARNING: no roles defined — generator will reject this");
|
||||
@@ -105,8 +109,43 @@ fn main() {
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
"zone_type" => match ron::from_str::<ZoneTypeTemplate>(&content) {
|
||||
Ok(template) => {
|
||||
println!("Valid ZoneTypeTemplate: {} ({})", template.label, template.id);
|
||||
println!(
|
||||
" {} roles, {} social site types",
|
||||
template.roles.len(),
|
||||
template.social_site_types.len()
|
||||
);
|
||||
let mut warnings = 0;
|
||||
if template.roles.is_empty() {
|
||||
eprintln!(" WARNING: no roles defined — generator will reject this");
|
||||
warnings += 1;
|
||||
}
|
||||
for role in &template.roles {
|
||||
if role.behavior_primitives.is_empty() {
|
||||
eprintln!(
|
||||
" WARNING: role '{}' has no behavior_primitives",
|
||||
role.id
|
||||
);
|
||||
warnings += 1;
|
||||
}
|
||||
}
|
||||
if warnings > 0 {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Invalid ZoneTypeTemplate in {}:", args.file);
|
||||
eprintln!(" {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
other => {
|
||||
eprintln!("Unknown schema type: '{}'. Use 'zone' or 'culture'.", other);
|
||||
eprintln!(
|
||||
"Unknown schema type: '{}'. Use 'zone', 'zone_type', or 'culture'.",
|
||||
other
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
KNOWLEDGE_DIR="$REPO_ROOT/content/global/knowledge"
|
||||
CONTENT_DIR="$REPO_ROOT/content/campaigns"
|
||||
KNOWLEDGE_DIR="$REPO_ROOT/server/content/global/knowledge"
|
||||
CONTENT_DIR="$REPO_ROOT/server/content/campaigns"
|
||||
|
||||
# --- Extract canonical fact_ids from knowledge catalogs ---
|
||||
# Matches YAML lines like: fact_id: some_value or fact_id: "some_value"
|
||||
|
||||
@@ -16,8 +16,8 @@ import jsonschema
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCHEMA_PATH = ROOT / "content" / "_schema" / "checklist.schema.json"
|
||||
GAUNTLET_DIR = ROOT / "content" / "gauntlet"
|
||||
SCHEMA_PATH = ROOT / "server" / "content" / "_schema" / "checklist.schema.json"
|
||||
GAUNTLET_DIR = ROOT / "server" / "content" / "gauntlet"
|
||||
|
||||
|
||||
def load_schema():
|
||||
|
||||
@@ -29,7 +29,7 @@ from pathlib import Path
|
||||
import jsonschema
|
||||
import yaml
|
||||
|
||||
CONTENT_DIR = Path(__file__).resolve().parent.parent / "content"
|
||||
CONTENT_DIR = Path(__file__).resolve().parent.parent / "server" / "content"
|
||||
SCHEMA_DIR = CONTENT_DIR / "_schema"
|
||||
|
||||
# Map directory parent name (or filename) to schema file
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
# RON content validator — wrapper for the Rust validate_ron binary (#611).
|
||||
#
|
||||
# Usage:
|
||||
# tooling/validate-ron <file.ron> <zone|culture>
|
||||
# tooling/validate-ron <file.ron> <zone|zone_type|culture>
|
||||
#
|
||||
# Examples:
|
||||
# tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
||||
# tooling/validate-ron content/global/culture-van-maanens-star.example.ron culture
|
||||
# tooling/validate-ron server/content/global/zone-types/rural_agricultural.ron zone_type
|
||||
# tooling/validate-ron server/content/global/culture-van-maanens-star.example.ron culture
|
||||
# tooling/validate-ron server/content/global/zone-identity-spec.example.ron zone
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: tooling/validate-ron <file.ron> <zone|culture>"
|
||||
echo "Usage: tooling/validate-ron <file.ron> <zone|zone_type|culture>"
|
||||
echo ""
|
||||
echo "Validates a RON file against the Rust struct schema."
|
||||
echo "Schema types:"
|
||||
echo " zone — ZoneSpec (zone identity spec)"
|
||||
echo " culture — CultureProfile (culture profile)"
|
||||
echo " zone_type — ZoneTypeTemplate (D-142 zone-type template)"
|
||||
echo " zone — ZoneSpec (legacy zone identity spec)"
|
||||
echo " culture — CultureProfile (culture profile)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user