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;
mod modal;
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;
mod world_files;
mod worlds;
use crate::app::state::App;
use crate::debug_hook::DebugHook;
use crate::ecs::World;
use crate::world::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, pick_a_world) = resolve_edit_target(json_path);
concinnity_engine::app::dev_flags::set_world_jsonl_path(Some(world_path.clone()));
let (entries, previewing) = if pick_a_world {
(Vec::new(), start_screen_pick())
} else if std::path::Path::new(&world_path).exists() {
let content = std::fs::read_to_string(&world_path)?;
let entries = crate::world::parse_world_jsonl(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
(entries, None)
} else {
(Vec::new(), None)
};
let mut app = crate::project::app();
boot_world(&mut app, &entries)?;
inject::editor_hud(app.world_mut());
let mut editor_hook = EditorHook::new(world_path, entries).with_console_sink(console_sink);
if pick_a_world {
editor_hook = editor_hook.with_start_screen(previewing);
}
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(), false),
None => (unsaved_world_path(), true),
}
}
fn start_screen_pick() -> Option<String> {
let world = world_files::newest(
crate::project::worlds_dir().as_deref(),
crate::project::content_root().as_deref(),
)?;
Some(world.path.to_string_lossy().into_owned())
}
pub(crate) fn unsaved_world_path() -> String {
crate::project::worlds_dir()
.map(|dir| dir.join(WORLD_JSONL).to_string_lossy().into_owned())
.unwrap_or_else(|| WORLD_JSONL.to_string())
}
fn boot_world(app: &mut App, entries: &[serde_json::Value]) -> std::io::Result<()> {
let jsonl = crate::world::write_world_jsonl(entries)
.map_err(|e| std::io::Error::other(e.to_string()))?;
let (world, _) = build_renderable(&jsonl)?;
app.load_world(world);
Ok(())
}
fn build_renderable(
jsonl: &str,
) -> std::io::Result<(World, Vec<concinnity_cook::build_only::ShadowedAsset>)> {
match crate::authoring::build_world_and_shadows(jsonl) {
Ok(built) if concinnity_engine::ecs::renders(&built.0) => Ok(built),
_ => crate::authoring::build_world_and_shadows(&seeded_content(jsonl)),
}
}
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");
}
fn open_project(dir: &std::path::Path) -> std::path::PathBuf {
let build_root = dir.join(".concinnity");
crate::project::open(
concinnity_host::store::paths::StateTree::at(dir)
.with_build(&build_root)
.with_cache(concinnity_testing::shared_cache_dir(
"concinnity-dev-tests-cache",
)),
);
build_root
}
fn entry(name: &str, ty: &str, args: serde_json::Value) -> serde_json::Value {
serde_json::json!({"name": name, "type": ty, "args": args})
}
fn renderable_entries(label: &str) -> Vec<serde_json::Value> {
vec![
entry("cam", "Camera3D", serde_json::json!({})),
entry("room", "Room", serde_json::json!({})),
entry("hint", "TextLabel", serde_json::json!({"content": label})),
]
}
fn booted_label(app: &App) -> String {
app.world()
.query::<crate::components::TextLabel>()
.next()
.expect("the authored label is in the booted world")
.content
.clone()
}
#[test]
fn boot_compiles_the_authored_entries_without_touching_the_build_root() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
let build_root = open_project(dir.path());
let mut app = crate::project::app();
boot_world(&mut app, &renderable_entries("authored")).expect("the world builds");
assert!(concinnity_engine::ecs::renders(app.world()));
assert_eq!(booted_label(&app), "authored");
assert!(
!build_root.join("data").exists() && !build_root.join("world-lock.json").exists(),
"boot writes no blobs and no lock"
);
crate::test_support::isolate_state_dir();
}
#[test]
fn boot_ignores_blobs_that_no_longer_match_the_entries() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
let build_root = open_project(dir.path());
let world_path = dir.path().join("worlds").join(WORLD_JSONL);
std::fs::create_dir_all(world_path.parent().unwrap()).unwrap();
std::fs::write(
&world_path,
crate::world::write_world_jsonl(&renderable_entries("stale")).unwrap(),
)
.unwrap();
crate::build_world_to_disk(world_path.to_str().unwrap()).expect("the build writes blobs");
let blob = concinnity_host::store::blob::primary_in(&build_root.join("data"));
let before = std::fs::read(&blob).expect("the build wrote a primary blob");
let mut app = crate::project::app();
boot_world(&mut app, &renderable_entries("edited")).expect("the world builds");
assert_eq!(
booted_label(&app),
"edited",
"the entries win over the blobs the last build left"
);
assert_eq!(
std::fs::read(&blob).unwrap(),
before,
"boot leaves the build output untouched"
);
crate::test_support::isolate_state_dir();
}
#[test]
fn boot_seeds_a_render_marker_for_an_empty_entry_list() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let mut app = crate::project::app();
boot_world(&mut app, &[]).expect("an empty world seeds");
assert!(concinnity_engine::ecs::renders(app.world()));
}
#[test]
fn resolve_edit_target_honors_an_explicit_path() {
let (path, pick) = resolve_edit_target(Some("/no/such/cn-editor-world.jsonl"));
assert_eq!(path, "/no/such/cn-editor-world.jsonl");
assert!(!pick, "a named world loads instead of the Worlds panel");
}
#[test]
fn resolve_edit_target_without_a_path_opens_the_worlds_panel() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let (path, pick) = resolve_edit_target(None);
assert!(pick, "no named world opens the Worlds panel");
assert_eq!(path, unsaved_world_path());
}
#[test]
fn an_unsaved_world_is_named_inside_the_projects_worlds_directory() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let worlds = crate::project::worlds_dir().expect("the harness opened a project");
assert_eq!(
unsaved_world_path(),
worlds.join(WORLD_JSONL).to_string_lossy()
);
}
}