Skip to main content

concinnity_dev/
run.rs

1// src/run.rs
2//
3// The interpreted (`cn debug`) run path: compiles world.jsonl fully in memory
4// and drives the system loop with the per-frame debug hook. The production
5// `cn run` path (compiled-blob playback) lives in the runtime crate's `app::run`.
6
7use crate::app::state::App;
8use crate::debug_hook::DebugHook;
9use crate::world::find_world_jsonl;
10
11/// The `cn debug` server path: start the localhost debug server on `port`,
12/// then run interpreted with it as the per-frame hook. This is the entry point
13/// the CLI binary calls; the hook assembly stays inside this crate.
14pub fn run_debug(json_path: Option<&str>, port: u16) -> std::io::Result<()> {
15    let debug_hook: Box<dyn DebugHook> = match crate::debug::DebugServer::start(port) {
16        Ok(srv) => Box::new(srv),
17        Err(e) => {
18            eprintln!("error: could not start debug server: {e}");
19            return Err(e);
20        }
21    };
22    run_interpreted(json_path, Some(debug_hook))
23}
24
25// The world an interpreted run should load: the `-f` path when the caller gave
26// one, otherwise whatever discovery finds.
27//
28// A `-f` that does not exist is an error rather than a fall back to discovery.
29// Falling back runs a different world under the name the caller asked for, and
30// says nothing: the loop starts, frames advance, and every trust check passes
31// against a scene nobody asked for.
32fn resolve_world_path(json_path: Option<&str>) -> std::io::Result<String> {
33    match json_path {
34        Some(p) if std::path::Path::new(p).exists() => Ok(p.to_string()),
35        Some(p) => Err(std::io::Error::new(
36            std::io::ErrorKind::NotFound,
37            format!("world file not found: {p}"),
38        )),
39        None => find_world_jsonl(crate::project::worlds_dir().as_deref(), None),
40    }
41}
42
43// Interpreted entry point (`cn debug`). Compiles world.jsonl fully in memory
44// -- shaders, meshes, textures, and all -- then runs the app without reading
45// or writing any binary blob files. Always paired with the localhost debug
46// server.
47pub(crate) fn run_interpreted(
48    json_path: Option<&str>,
49    debug: Option<Box<dyn DebugHook>>,
50) -> std::io::Result<()> {
51    concinnity_engine::app::run::init_logging();
52
53    let resolved = resolve_world_path(json_path)?;
54    let json_path = resolved.as_str();
55
56    // Hand the resolved world path to the engine so its hot-reload watcher can
57    // subscribe to world.jsonl. The engine no longer discovers it (that lookup
58    // is authoring I/O in concinnity-cook, which the runtime does not link).
59    concinnity_engine::app::dev_flags::set_world_jsonl_path(Some(json_path.to_string()));
60
61    let mut app = crate::project::app();
62    *app.world_mut() = crate::build_world_from_path(json_path).map_err(|e| {
63        tracing::error!("Could not build world from {json_path}: {e}");
64        e
65    })?;
66
67    start_app(app, debug)
68}
69
70// Shared startup and loop entry once the App's world is populated. The world
71// loop itself (and the platform event-pump + window activation) is the shared
72// `concinnity_engine::app::runloop` driver; the interpreted path's only
73// addition is ticking the debug hook each frame.
74pub(crate) fn start_app(
75    mut app: App,
76    mut debug: Option<Box<dyn DebugHook>>,
77) -> std::io::Result<()> {
78    use crate::app::runloop;
79
80    let shutdown = app.shutdown_token();
81    runloop::install_ctrlc_handler(&app);
82
83    // Hand the shutdown token to the debug hook so a debug client can request
84    // a clean exit (the `shutdown` WS command). No-op when no hook is present.
85    if let Some(hook) = debug.as_mut() {
86        hook.attach_shutdown(shutdown.clone());
87    }
88
89    // Resolved before `start()` (while the GraphicsConfig is still present),
90    // so the render-loop choice doesn't depend on the config component, which
91    // `start()` drains. Only the macOS path branches on it.
92    #[cfg(target_os = "macos")]
93    let renders = concinnity_engine::ecs::renders(app.world());
94
95    // On macOS, NSApplication is a per-process singleton. Activate it once
96    // before the first NSWindow is created.
97    #[cfg(target_os = "macos")]
98    if renders {
99        runloop::activate_app_macos();
100    }
101
102    if let Err(e) = app.start() {
103        // Returned rather than exiting the process, so the world's systems
104        // (and the GPU resources they hold) still drop on the way out.
105        tracing::error!("failed to start app: {e}");
106        return Err(std::io::Error::other(format!("failed to start app: {e}")));
107    }
108
109    // The interpreted path ticks its debug hook each frame before the world step.
110    // After the tick (which sees only `&mut World`), the hook is given the whole
111    // App so it can apply a pending world swap (the `cn editor` live SAVE).
112    let on_tick = |app: &mut App| {
113        if let Some(hook) = debug.as_deref_mut() {
114            hook.tick(app.world_mut());
115            hook.apply_world_swap(app);
116        }
117    };
118
119    #[cfg(target_os = "macos")]
120    runloop::run_loop(&mut app, renders, on_tick);
121    #[cfg(not(target_os = "macos"))]
122    runloop::run_loop(&mut app, false, on_tick);
123
124    Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129    use super::resolve_world_path;
130
131    #[test]
132    fn an_existing_path_is_taken_as_given() {
133        let dir = tempfile::tempdir().expect("temp dir");
134        let world = dir.path().join("scene.jsonl");
135        std::fs::write(&world, "").expect("write");
136        let given = world.to_string_lossy().into_owned();
137        assert_eq!(resolve_world_path(Some(&given)).expect("resolves"), given);
138    }
139
140    // The whole point of the arm: a named world that is not there fails loudly
141    // instead of silently becoming whatever discovery turns up.
142    #[test]
143    fn a_missing_path_errors_rather_than_falling_back() {
144        let dir = tempfile::tempdir().expect("temp dir");
145        let missing = dir.path().join("absent.jsonl");
146        let given = missing.to_string_lossy().into_owned();
147        let err = resolve_world_path(Some(&given)).expect_err("missing world is an error");
148        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
149        assert!(err.to_string().contains("absent.jsonl"), "{err}");
150    }
151}