mod asset_list;
mod asset_tree;
mod axes;
mod behavior;
mod behavior_chart;
mod behavior_panel;
mod billboards;
mod character_shape;
mod character_shape_panel;
mod console;
mod console_panel;
mod content_panel;
mod create_menu;
mod cursor;
mod file_dialog;
mod filter;
mod form;
mod form_panel;
mod framing;
mod gizmo;
mod gltf_export;
mod group_transform;
mod health;
mod health_panel;
mod highlight;
mod history;
mod hook;
mod hud;
mod import_panel;
mod inject;
mod lighting;
mod lighting_panel;
mod list_panel;
mod live;
mod marquee;
pub(crate) mod notify;
mod orbit;
mod outlines;
mod overrides;
mod palette;
mod palette_panel;
mod panel;
mod preview;
mod registry;
mod resize;
mod select_related;
mod selection;
mod session_store;
mod sim;
mod snap;
mod story;
mod story_panel;
mod template_panel;
mod templates;
mod theme;
mod thumbs;
mod toast_overlay;
mod variables;
mod variables_panel;
mod view;
mod view_menu;
mod visibility;
mod widget;
mod widget_slider;
use crate::app::state::App;
use crate::debug_hook::DebugHook;
use crate::ecs::World;
use crate::world::{WORLD_JSONL, find_world_jsonl};
use concinnity_engine::shutdown::ShutdownToken;
use hook::EditorHook;
const SEED_GRAPHICS_CONFIG: &str =
"{\"name\":\"editor_default_gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}";
pub fn run_editor(json_path: Option<&str>, debug_port: Option<u16>) -> std::io::Result<()> {
let console_sink = console::ConsoleSink::default();
console::install_tracing(console_sink.clone());
let (world_path, world_exists) = resolve_edit_target(json_path);
concinnity_engine::app::dev_flags::set_world_jsonl_path(Some(world_path.clone()));
let entries = if world_exists {
let content = std::fs::read_to_string(&world_path)?;
crate::world::parse_world_jsonl(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
} else {
Vec::new()
};
let mut app = App::new();
boot_world(&mut app, &world_path, world_exists, &entries)?;
inject::editor_hud(app.world_mut());
let editor_hook = EditorHook::new(world_path, entries).with_console_sink(console_sink);
let hook: Box<dyn DebugHook> = match debug_port {
Some(port) => {
let server =
crate::debug::DebugServer::start(port)?.with_notifier(editor_hook.notifier());
MultiHook::boxed(vec![Box::new(editor_hook), Box::new(server)])
}
None => {
let reload = crate::debug::hot_reload::HotReloadDriver::new()
.with_notifier(editor_hook.notifier());
MultiHook::boxed(vec![Box::new(editor_hook), Box::new(reload)])
}
};
crate::run::start_app(app, Some(hook))
}
fn resolve_edit_target(json_path: Option<&str>) -> (String, bool) {
match json_path {
Some(p) => (p.to_string(), std::path::Path::new(p).exists()),
None => match find_world_jsonl(None) {
Ok(p) => (p, true),
Err(_) => (WORLD_JSONL.to_string(), false),
},
}
}
fn boot_world(
app: &mut App,
world_path: &str,
world_exists: bool,
entries: &[serde_json::Value],
) -> std::io::Result<()> {
let blobs_present =
|| concinnity_host::store::paths::data_dir().is_some_and(|d| d.join("0").exists());
if world_exists && !blobs_present() {
crate::build_world_to_disk(world_path)?;
}
if blobs_present() {
app.load_blob().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to load compiled world data: {e:?}"),
)
})?;
match crate::authoring::name_table::prime_from_lock_file() {
Ok(n) if n > 0 => tracing::info!("editor: primed {n} asset names from the build lock"),
Ok(_) => {}
Err(e) => tracing::warn!("editor: could not prime asset names: {e}"),
}
match crate::authoring::reload_sources::install_from_lock(app.world_mut(), entries) {
Ok(n) if n > 0 => {
tracing::info!("editor: recovered {n} hot-reload source(s) from the build lock");
}
Ok(_) => {}
Err(e) => tracing::warn!("editor: could not recover hot-reload sources: {e}"),
}
}
if !concinnity_engine::ecs::renders(app.world()) {
let base = if world_exists {
std::fs::read_to_string(world_path)?
} else {
String::new()
};
let world = crate::build_world_from_str(&seeded_content(&base))?;
app.load_world(world);
}
Ok(())
}
fn seeded_content(base: &str) -> String {
if base.trim().is_empty() {
SEED_GRAPHICS_CONFIG.to_string()
} else {
format!("{base}\n{SEED_GRAPHICS_CONFIG}")
}
}
struct MultiHook {
hooks: Vec<Box<dyn DebugHook>>,
}
impl MultiHook {
fn boxed(hooks: Vec<Box<dyn DebugHook>>) -> Box<dyn DebugHook> {
Box::new(Self { hooks })
}
}
impl DebugHook for MultiHook {
fn tick(&mut self, world: &mut World) {
for hook in &mut self.hooks {
hook.tick(world);
}
}
fn apply_world_swap(&mut self, app: &mut crate::app::state::App) {
for hook in &mut self.hooks {
hook.apply_world_swap(app);
}
}
fn attach_shutdown(&mut self, shutdown: ShutdownToken) {
for hook in &mut self.hooks {
hook.attach_shutdown(shutdown.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seeded_content_of_empty_is_the_render_marker() {
assert_eq!(seeded_content(""), SEED_GRAPHICS_CONFIG);
assert_eq!(seeded_content(" \n"), SEED_GRAPHICS_CONFIG);
}
#[test]
fn seeded_content_appends_marker_to_authored_content() {
let base = "{\"name\":\"phys\",\"type\":\"PhysicsConfig\",\"args\":{}}";
let seeded = seeded_content(base);
let parsed = crate::world::parse_world_jsonl(&seeded).unwrap();
assert_eq!(parsed.len(), 2, "authored entry plus the seed marker");
assert_eq!(parsed[0]["name"], "phys");
assert_eq!(parsed[1]["type"], "GraphicsConfig");
}
#[test]
fn seed_marker_is_a_graphics_config() {
let parsed = crate::world::parse_world_jsonl(SEED_GRAPHICS_CONFIG).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["type"], "GraphicsConfig");
}
#[test]
fn resolve_edit_target_honors_an_explicit_path() {
let (path, exists) = resolve_edit_target(Some("/no/such/cn-editor-world.jsonl"));
assert_eq!(path, "/no/such/cn-editor-world.jsonl");
assert!(
!exists,
"a missing explicit path is reported absent, not an error"
);
}
}