//! 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::() { 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::().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);