Files
settled-reach/server/src/content/mod.rs
T
jpmschweitzerandClaude Opus 4.6 91d6b1b1cb feat(simulation): add YAML content loader with hot-reload (#326)
LinePool system parses dialogue.yaml and monologue.yaml into indexed
BTreeMap structures. Supports 4-layer query filtering (access >
situation > trust > topic+mood) per D-028. Hot-reload via timestamp
polling every 20 ticks (dev-only). Graceful failure preserves
previous content on reload error.

30+ tests covering enum parsing, index building, query filtering,
monologue fallback, and content watching.

Ref: D-028, D-032, D-035, D-041

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:41:29 +01:00

113 lines
4.0 KiB
Rust

//! Content loading, indexing, and entity spawning system.
//!
//! Architecture (per Tyre's D-020 guidance):
//! 1. Deserialize YAML -> intermediate content types (types.rs)
//! 2. Content discovery + loading (loader.rs) -> ContentStore resource
//! 3. ContentStore -> ECS entity spawning (spawn.rs)
//! 4. ContentStore -> indexed line pools (line_pool.rs) -> LinePoolIndex resource
//! 5. Optional hot-reload (hot_reload.rs) for dev/authoring workflow
//!
//! Content schema is decoupled from ECS components. The spawn module
//! handles the mapping between the two representations.
pub mod hot_reload;
pub mod line_pool;
pub mod loader;
pub mod spawn;
pub mod types;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use std::path::PathBuf;
/// Configuration for the content loader.
/// Set the content root path before adding ContentPlugin.
#[derive(Resource, Debug, Clone)]
pub struct ContentConfig {
/// Root directory containing content.yaml and campaign directories.
pub content_root: PathBuf,
/// Enable hot-reload (timestamp polling). Dev-only, not for production.
pub hot_reload: bool,
}
impl Default for ContentConfig {
fn default() -> Self {
Self {
content_root: PathBuf::from("content"),
hot_reload: false,
}
}
}
/// Content loading plugin.
///
/// Loads content from YAML files at startup, spawns ECS entities,
/// and builds the indexed line pools for dialogue/monologue queries.
/// Optionally enables hot-reload for the authoring workflow.
pub struct ContentPlugin;
impl Plugin for ContentPlugin {
fn build(&self, app: &mut App) {
if !app.world().contains_resource::<ContentConfig>() {
app.insert_resource(ContentConfig::default());
}
app.add_systems(Startup, load_and_spawn_content);
app.add_systems(PostUpdate, hot_reload::hot_reload_content);
tracing::debug!("ContentPlugin initialized");
}
}
/// Startup system: load content from disk, spawn entities, and build line pool index.
fn load_and_spawn_content(world: &mut World) {
let config = world.resource::<ContentConfig>().clone();
tracing::info!("Loading content from: {:?}", config.content_root);
match loader::load_content(&config.content_root) {
Ok(store) => {
let result = spawn::spawn_content(world, &store);
tracing::info!("Content loaded and spawned: {} NPCs", result.npcs_spawned);
// Build line pool index
let index = line_pool::LinePoolIndex::build(&store);
tracing::info!(
"Line pool index built: {} dialogue lines, {} monologue lines",
index.dialogue_line_count(),
index.monologue_line_count()
);
world.insert_resource(ContentStoreResource(store));
world.insert_resource(LinePoolIndexResource(index));
}
Err(e) => {
tracing::error!("Failed to load content: {}", e);
world.insert_resource(ContentStoreResource(loader::ContentStore::default()));
world.insert_resource(LinePoolIndexResource(line_pool::LinePoolIndex::default()));
}
}
// Set up hot-reload if enabled
if config.hot_reload {
let watcher = hot_reload::ContentWatcher::new(&config.content_root);
tracing::info!(
"Content hot-reload enabled — tracking {} files, polling every ~2s",
watcher.tracked_file_count()
);
world.insert_resource(watcher);
}
}
/// Wrapper resource holding the loaded content store.
/// Available for runtime systems that need to query content data
/// (e.g., dialogue selection, triangle fork evaluation).
#[derive(Resource, Debug)]
pub struct ContentStoreResource(pub loader::ContentStore);
/// Wrapper resource holding the indexed line pools.
/// Available for runtime systems that need to query dialogue/monologue lines
/// through the D-028 four-layer filtering pipeline.
#[derive(Resource, Debug)]
pub struct LinePoolIndexResource(pub line_pool::LinePoolIndex);