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
|
// Schema: server/src/npc/blueprint.rs :: ZoneTypeTemplate
|
||||||
// Ticket: #661 (copy team)
|
// 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).
|
// Sources: D-142 (zone-type template architecture).
|
||||||
//
|
//
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// Schema: server/src/npc/blueprint.rs :: ZoneTypeTemplate
|
// Schema: server/src/npc/blueprint.rs :: ZoneTypeTemplate
|
||||||
// Ticket: #661 (copy team)
|
// 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).
|
// Sources: D-142 (zone-type template architecture).
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Drama Module Schema — Tier 1 Content (D-023)
|
# Drama Module Schema — Tier 1 Content (D-023)
|
||||||
# YAML expression of JSON Schema 2020-12
|
# 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:
|
# Ownership:
|
||||||
# Dramatic structure (this file): Paula
|
# Dramatic structure (this file): Paula
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Run twice, compare side-by-side:
|
//! Run twice, compare side-by-side:
|
||||||
//! ```sh
|
//! ```sh
|
||||||
//! cargo run --bin generator_spike -- --zone rural --seed 42
|
//! cargo run --bin generator_spike -- --zone-type rural --seed 42
|
||||||
//! cargo run --bin generator_spike -- --zone industrial --seed 42
|
//! cargo run --bin generator_spike -- --zone-type industrial --seed 42
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The test: can you tell which is which from the output alone?
|
//! 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.
|
//! functions that work without ECS. Full ECS integration is deferred.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -31,9 +31,9 @@ use rand::SeedableRng;
|
|||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
use settled_reach_server::npc::blueprint::{
|
use settled_reach_server::npc::blueprint::{
|
||||||
assemble_behaviors, BlueprintRelationship, CultureProfile, CulturalMarkers, CulturalValues,
|
assemble_behaviors, BlueprintRelationship, CulturalMarkers, CulturalValues, CultureProfile,
|
||||||
NamingConventions, NpcBlueprint, NpcWant, RoleSpec, SocialSiteSpec, SpeechPatterns,
|
NamingConventions, NpcBlueprint, NpcWant, RoleSpec, SocialSiteSpec, SpeechPatterns,
|
||||||
SpikeOutput, ZoneSpec,
|
SpikeOutput, ZoneSpec, ZoneTypeTemplate,
|
||||||
};
|
};
|
||||||
use settled_reach_server::npc::PersonalityTrait;
|
use settled_reach_server::npc::PersonalityTrait;
|
||||||
use settled_reach_server::simulation::rng::SimRng;
|
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)"
|
about = "NPC generator proof-of-life — Sprint 25 (#612)"
|
||||||
)]
|
)]
|
||||||
struct Args {
|
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)]
|
#[arg(long)]
|
||||||
zone: String,
|
zone_type: String,
|
||||||
|
|
||||||
/// Deterministic seed — same seed produces identical output.
|
/// Deterministic seed — same seed produces identical output.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -60,8 +62,8 @@ struct Args {
|
|||||||
#[arg(long, default_value = "van-maanens-star")]
|
#[arg(long, default_value = "van-maanens-star")]
|
||||||
culture: String,
|
culture: String,
|
||||||
|
|
||||||
/// Phase 2: load zone spec and culture from RON files on disk.
|
/// Phase 2: load zone-type template and culture from RON files on disk.
|
||||||
/// Requires content/global/<zone>-zone-spec.ron and content/global/culture-<culture>.ron.
|
/// Requires server/content/global/zone-types/<zone_type>.ron and server/content/global/culture-<culture>.ron.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
from_files: bool,
|
from_files: bool,
|
||||||
|
|
||||||
@@ -268,21 +270,60 @@ fn hardcoded_van_maanens_star_culture() -> CultureProfile {
|
|||||||
// Phase 2: file loading
|
// Phase 2: file loading
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn load_zone_from_file(content_root: &PathBuf, zone_type: &str) -> ZoneSpec {
|
fn load_zone_from_file(content_root: &Path, zone_type: &str) -> ZoneSpec {
|
||||||
let path = content_root.join("global").join(format!("{}-zone-spec.ron", zone_type));
|
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| {
|
let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||||
eprintln!("Error loading zone spec from {:?}: {}", path, e);
|
eprintln!("Error loading zone-type template from {:?}: {}", path, e);
|
||||||
eprintln!("Tip: copy team fills this file (ticket #609).");
|
eprintln!("Tip: zone-type RON files live in content/global/zone-types/.");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
});
|
});
|
||||||
ron::from_str(&content).unwrap_or_else(|e| {
|
let template: ZoneTypeTemplate = ron::from_str(&content).unwrap_or_else(|e| {
|
||||||
eprintln!("Invalid ZoneSpec in {:?}: {}", path, e);
|
eprintln!("Invalid ZoneTypeTemplate in {:?}: {}", path, e);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
})
|
});
|
||||||
|
zone_type_template_to_zone_spec(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_culture_from_file(content_root: &PathBuf, culture_id: &str) -> CultureProfile {
|
/// Convert a `ZoneTypeTemplate` to a `ZoneSpec` compatible with the spike generation pipeline.
|
||||||
let path = content_root.join("global").join(format!("culture-{}.ron", culture_id));
|
///
|
||||||
|
/// `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| {
|
let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||||
eprintln!("Error loading culture from {:?}: {}", path, e);
|
eprintln!("Error loading culture from {:?}: {}", path, e);
|
||||||
eprintln!("Tip: copy team fills this file (ticket #610).");
|
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.
|
/// main-RNG word before reaching the Fisher-Yates step.
|
||||||
fn name_pool_seed(seed: u64, zone_type: &str, culture_id: &str) -> u64 {
|
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
|
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 ^= b as u64;
|
||||||
h = h.wrapping_mul(0x100000001b3); // FNV-1a prime
|
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 {
|
fn gen_cultural_markers(rng: &mut SimRng, culture: &CultureProfile) -> CulturalMarkers {
|
||||||
let filler_count = if culture.speech.filler_words.len() > 1 {
|
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 {
|
} else {
|
||||||
culture.speech.filler_words.len()
|
culture.speech.filler_words.len()
|
||||||
};
|
};
|
||||||
@@ -595,7 +641,10 @@ fn gen_relationships(
|
|||||||
) -> Vec<BlueprintRelationship> {
|
) -> Vec<BlueprintRelationship> {
|
||||||
use settled_reach_server::npc::blueprint::RelationshipValence;
|
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() {
|
if other_names.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
@@ -649,9 +698,7 @@ fn generate_npc_blueprint(
|
|||||||
let role = pick_role(rng, zone);
|
let role = pick_role(rng, zone);
|
||||||
let traits = gen_traits(rng, culture);
|
let traits = gen_traits(rng, culture);
|
||||||
let want = gen_want(rng, &traits, role);
|
let want = gen_want(rng, &traits, role);
|
||||||
let pool = behavior_pools
|
let pool = behavior_pools.entry(role.id.clone()).or_default();
|
||||||
.entry(role.id.clone())
|
|
||||||
.or_insert_with(VecDeque::new);
|
|
||||||
let observable_behaviors = gen_behaviors(rng, role, pool);
|
let observable_behaviors = gen_behaviors(rng, role, pool);
|
||||||
let cultural_markers = gen_cultural_markers(rng, culture);
|
let cultural_markers = gen_cultural_markers(rng, culture);
|
||||||
// Relationships and Want tells assigned in later passes.
|
// 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 {
|
AvoidingSomeone => match avoid_target {
|
||||||
Some(name) => {
|
Some(name) => {
|
||||||
let options = [
|
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!("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()
|
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),
|
("friend", _) => format!("drifts toward {} between tasks", target),
|
||||||
("subordinate", RelationshipValence::Negative) => {
|
("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),
|
("subordinate", _) => format!("defers to {} before moving on", target),
|
||||||
("superior", RelationshipValence::Negative) => {
|
("superior", RelationshipValence::Negative) => {
|
||||||
@@ -992,17 +1048,20 @@ fn main() {
|
|||||||
// Load zone spec and culture profile
|
// Load zone spec and culture profile
|
||||||
let (zone, culture) = if args.from_files {
|
let (zone, culture) = if args.from_files {
|
||||||
eprintln!("Phase 2: loading 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);
|
let culture = load_culture_from_file(&args.content_root, &args.culture);
|
||||||
(zone, culture)
|
(zone, culture)
|
||||||
} else {
|
} else {
|
||||||
// Phase 1: hardcoded stubs
|
// Phase 1: hardcoded stubs
|
||||||
let zone = match args.zone.as_str() {
|
let zone = match args.zone_type.as_str() {
|
||||||
"rural" => hardcoded_rural_zone(),
|
"rural" => hardcoded_rural_zone(),
|
||||||
"industrial" => hardcoded_industrial_zone(),
|
"industrial" => hardcoded_industrial_zone(),
|
||||||
other => {
|
other => {
|
||||||
eprintln!("Unknown zone type: '{}'. Use 'rural' or 'industrial'.", other);
|
eprintln!(
|
||||||
eprintln!("(Phase 2 with --from-files supports additional zone types from disk)");
|
"Unknown zone type: '{}'. Use 'rural' or 'industrial'.",
|
||||||
|
other
|
||||||
|
);
|
||||||
|
eprintln!("(Phase 2 with --from-files supports zone-type IDs like 'rural_agricultural')");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1046,7 +1105,7 @@ fn main() {
|
|||||||
// Pre-generate unique names using a derived RNG (#628 fix: first-pick bias).
|
// 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
|
// 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.
|
// 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).
|
// Pre-shuffle behavior pools per role (#629 fix: dedup within a zone run).
|
||||||
// D-139: uses assemble_behaviors() when behavior_primitives are present.
|
// D-139: uses assemble_behaviors() when behavior_primitives are present.
|
||||||
@@ -1055,9 +1114,7 @@ fn main() {
|
|||||||
// First pass: generate all NPCs (no relationships yet)
|
// First pass: generate all NPCs (no relationships yet)
|
||||||
let mut npcs: Vec<NpcBlueprint> = name_pool
|
let mut npcs: Vec<NpcBlueprint> = name_pool
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|name| {
|
.map(|name| generate_npc_blueprint(&mut rng, &zone, &culture, name, &mut behavior_pools))
|
||||||
generate_npc_blueprint(&mut rng, &zone, &culture, name, &mut behavior_pools)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Collect all names for relationship pass.
|
// Collect all names for relationship pass.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
//!
|
//!
|
||||||
//! ```sh
|
//! ```sh
|
||||||
//! # Via wrapper script (recommended):
|
//! # Via wrapper script (recommended):
|
||||||
//! tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
//! tooling/validate-ron server/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/culture-van-maanens-star.example.ron culture
|
||||||
//!
|
//!
|
||||||
//! # Direct:
|
//! # Direct:
|
||||||
//! cargo run --bin validate_ron -- <file.ron> <zone|culture>
|
//! cargo run --bin validate_ron -- <file.ron> <zone|culture>
|
||||||
@@ -19,7 +19,7 @@ use std::process;
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec};
|
use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec, ZoneTypeTemplate};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
@@ -29,7 +29,7 @@ use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec};
|
|||||||
struct Args {
|
struct Args {
|
||||||
/// Path to the RON file to validate.
|
/// Path to the RON file to validate.
|
||||||
file: String,
|
file: String,
|
||||||
/// Schema type: "zone" (ZoneSpec) or "culture" (CultureProfile).
|
/// Schema type: "zone" (ZoneSpec), "zone_type" (ZoneTypeTemplate), or "culture" (CultureProfile).
|
||||||
schema: String,
|
schema: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,11 @@ fn main() {
|
|||||||
"zone" => match ron::from_str::<ZoneSpec>(&content) {
|
"zone" => match ron::from_str::<ZoneSpec>(&content) {
|
||||||
Ok(spec) => {
|
Ok(spec) => {
|
||||||
println!("Valid ZoneSpec: {} ({})", spec.label, spec.zone_type);
|
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;
|
let mut warnings = 0;
|
||||||
if spec.roles.is_empty() {
|
if spec.roles.is_empty() {
|
||||||
eprintln!(" WARNING: no roles defined — generator will reject this");
|
eprintln!(" WARNING: no roles defined — generator will reject this");
|
||||||
@@ -105,8 +109,43 @@ fn main() {
|
|||||||
process::exit(1);
|
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 => {
|
other => {
|
||||||
eprintln!("Unknown schema type: '{}'. Use 'zone' or 'culture'.", other);
|
eprintln!(
|
||||||
|
"Unknown schema type: '{}'. Use 'zone', 'zone_type', or 'culture'.",
|
||||||
|
other
|
||||||
|
);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
KNOWLEDGE_DIR="$REPO_ROOT/content/global/knowledge"
|
KNOWLEDGE_DIR="$REPO_ROOT/server/content/global/knowledge"
|
||||||
CONTENT_DIR="$REPO_ROOT/content/campaigns"
|
CONTENT_DIR="$REPO_ROOT/server/content/campaigns"
|
||||||
|
|
||||||
# --- Extract canonical fact_ids from knowledge catalogs ---
|
# --- Extract canonical fact_ids from knowledge catalogs ---
|
||||||
# Matches YAML lines like: fact_id: some_value or fact_id: "some_value"
|
# Matches YAML lines like: fact_id: some_value or fact_id: "some_value"
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import jsonschema
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
SCHEMA_PATH = ROOT / "content" / "_schema" / "checklist.schema.json"
|
SCHEMA_PATH = ROOT / "server" / "content" / "_schema" / "checklist.schema.json"
|
||||||
GAUNTLET_DIR = ROOT / "content" / "gauntlet"
|
GAUNTLET_DIR = ROOT / "server" / "content" / "gauntlet"
|
||||||
|
|
||||||
|
|
||||||
def load_schema():
|
def load_schema():
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from pathlib import Path
|
|||||||
import jsonschema
|
import jsonschema
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
CONTENT_DIR = Path(__file__).resolve().parent.parent / "content"
|
CONTENT_DIR = Path(__file__).resolve().parent.parent / "server" / "content"
|
||||||
SCHEMA_DIR = CONTENT_DIR / "_schema"
|
SCHEMA_DIR = CONTENT_DIR / "_schema"
|
||||||
|
|
||||||
# Map directory parent name (or filename) to schema file
|
# Map directory parent name (or filename) to schema file
|
||||||
|
|||||||
@@ -2,21 +2,23 @@
|
|||||||
# RON content validator — wrapper for the Rust validate_ron binary (#611).
|
# RON content validator — wrapper for the Rust validate_ron binary (#611).
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# tooling/validate-ron <file.ron> <zone|culture>
|
# tooling/validate-ron <file.ron> <zone|zone_type|culture>
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
# tooling/validate-ron server/content/global/zone-types/rural_agricultural.ron zone_type
|
||||||
# tooling/validate-ron content/global/culture-van-maanens-star.example.ron culture
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
if [ $# -lt 2 ]; then
|
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 ""
|
||||||
echo "Validates a RON file against the Rust struct schema."
|
echo "Validates a RON file against the Rust struct schema."
|
||||||
echo "Schema types:"
|
echo "Schema types:"
|
||||||
echo " zone — ZoneSpec (zone identity spec)"
|
echo " zone_type — ZoneTypeTemplate (D-142 zone-type template)"
|
||||||
echo " culture — CultureProfile (culture profile)"
|
echo " zone — ZoneSpec (legacy zone identity spec)"
|
||||||
|
echo " culture — CultureProfile (culture profile)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user