kitgit

tirbofish/dropbear · diff

60aeb39 · tk

Merge pull request #3 from 4tkbytes/build-system feature: Build system

Unverified

diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0fce33b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "redback-runtime"]
+	path = redback-runtime
+	url = https://github.com/4tkbytes/redback-runtime
diff --git a/Cargo.toml b/Cargo.toml
index 763103b..13f2a5d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,11 +6,12 @@ package.repository = "https://github.com/4tkbytes/dropbear-engine"
 package.readme = "README.md"
 
 resolver = "3"
-members = ["dropbear-engine", "eucalyptus"]
+members = ["dropbear-engine", "eucalyptus", "redback-runtime"]
 
 [workspace.dependencies]
 anyhow = { version = "1.0", features = ["backtrace"] }
 app_dirs2 = "2.5"
+bincode = { version = "2.0", features = ["serde"] }
 bytemuck = { version = "1.23", features = ["derive"] }
 chrono = "0.4"
 clap = "4.5"
@@ -39,21 +40,23 @@ pollster = "0.4"
 rfd = "0.15.4"
 rhai = "1.22"
 ron = "0.10.1"
-russimp = { version = "3.2" }
+russimp = { version = "3.2", features = ["prebuilt"] }
 serde = { version = "1.0.219", features = ["derive"] }
 spin_sleep = "1.3"
 tokio = { version = "1", features = ["full"] }
 transform-gizmo-egui = { git = "https://github.com/4tkbytes/transform-gizmo"}
 wgpu = "25"
 winit = { version = "0.30", features = [] }
+zip = "0.6"
+walkdir = "2.3"
 
 [workspace.dependencies.image]
 version = "0.25"
 default-features = false
 features = ["png"]
 
-[patch.crates-io]
-russimp-sys = { git = "https://github.com/4tkbytes/russimp-sys" }
+# [patch.crates-io]
+# russimp-sys = { git = "https://github.com/4tkbytes/russimp-sys" }
 
 [profile.dev]
 opt-level = 0
diff --git a/README.md b/README.md
index bf5232c..0ab1a29 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ If you might have not realised, all the crates/projects names are after Australi
 ## Related projects
 
 - [eucalyptus](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus) is the visual editor used to create games visually, taking inspiration from Unity and other engines.
-- [redback](https://github.com/4tkbytes/dropbear/tree/main/redback) is the build system used by [eucalyptus](https://github.com/4tkbytes/dropbear/tree/main/eucalyptus) to bind, build and ship games made with the engine.
+- [redback](https://github.com/4tkbytes/dropbear/tree/main/redback) is the runtime used to load .eupak files and run the games loaded on them.
 
 ## Build
 
diff --git a/dropbear-engine/Cargo.toml b/dropbear-engine/Cargo.toml
index c9b9028..1673308 100644
--- a/dropbear-engine/Cargo.toml
+++ b/dropbear-engine/Cargo.toml
@@ -26,7 +26,7 @@ serde.workspace = true
 spin_sleep.workspace = true
 wgpu.workspace = true
 winit.workspace = true
- 
+
 [dependencies.image]
 version = "0.25"
 default-features = false
diff --git a/dropbear-engine/src/camera.rs b/dropbear-engine/src/camera.rs
index 62cb006..54e904c 100644
--- a/dropbear-engine/src/camera.rs
+++ b/dropbear-engine/src/camera.rs
@@ -14,7 +14,7 @@ pub const OPENGL_TO_WGPU_MATRIX: [[f64; 4]; 4] = [
     [0.0, 0.0, 0.5, 1.0],
 ];
 
-#[derive(Default)]
+#[derive(Default, Debug)]
 pub struct Camera {
     pub eye: DVec3,
     pub target: DVec3,
diff --git a/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index cdf4809..3cc840d 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -36,6 +36,10 @@ use crate::{
     graphics::{Graphics, Texture},
 };
 
+pub use winit;
+pub use wgpu;
+pub use gilrs;
+
 /// The backend information, such as the device, queue, config, surface, renderer, window and more.
 pub struct State {
     pub surface: Surface<'static>,
@@ -334,7 +338,7 @@ impl App {
             unsafe { std::env::set_var("RUST_LOG", log_config) };
         }
 
-        env_logger::init();
+        let _ = env_logger::try_init();
 
         // log::debug!("OUT_DIR: {}", std::env!("OUT_DIR"));
 
@@ -371,7 +375,7 @@ macro_rules! run_app {
 
 impl ApplicationHandler for App {
     fn resumed(&mut self, event_loop: &ActiveEventLoop) {
-        let mut window_attributes = Window::default_attributes().with_title(self.config.title);
+        let mut window_attributes = Window::default_attributes().with_title(self.config.title.clone());
 
         if self.config.windowed_mode.is_windowed() {
             if let Some((width, height)) = self.config.windowed_mode.windowed_size() {
@@ -515,7 +519,7 @@ impl ApplicationHandler for App {
 #[derive(Debug, Clone)]
 pub struct WindowConfiguration {
     pub windowed_mode: WindowedModes,
-    pub title: &'static str,
+    pub title: String,
     /// This reads from a config file.
     /// This will read from a client config file under {exe}/client.props, and on game exit will save the properties to the file.
     ///
diff --git a/eucalyptus/Cargo.toml b/eucalyptus/Cargo.toml
index 5a44f9d..486a081 100644
--- a/eucalyptus/Cargo.toml
+++ b/eucalyptus/Cargo.toml
@@ -11,37 +11,98 @@ repository.workspace = true
 readme.workspace = true
 
 [dependencies]
-anyhow.workspace = true
-app_dirs2.workspace = true
-chrono.workspace = true
-dropbear-engine.workspace = true
-egui-toast-fork.workspace = true
-egui.workspace = true
-egui_dnd.workspace = true
-egui_dock-fork.workspace = true
-egui_extras.workspace = true
-gilrs.workspace = true
-git2.workspace = true
-glam.workspace = true
-hecs.workspace = true
-log.workspace = true
-log-once.workspace = true
-model_to_image.workspace = true
-once_cell.workspace = true
-open.workspace = true
-parking_lot.workspace = true
-rfd.workspace = true
-rhai.workspace = true
-ron.workspace = true
-serde.workspace = true
-tokio.workspace = true
-transform-gizmo-egui.workspace = true
-wgpu.workspace = true
-winit.workspace = true
-clap.workspace = true
+anyhow = { workspace = true, optional = true }
+app_dirs2 = { workspace = true, optional = true }
+bincode = { workspace = true, optional = true }
+chrono = { workspace = true, optional = true }
+dropbear-engine = { workspace = true, optional = true }
+egui-toast-fork = { workspace = true, optional = true }
+egui = { workspace = true, optional = true }
+egui_dnd = { workspace = true, optional = true }
+egui_dock-fork = { workspace = true, optional = true }
+egui_extras = { workspace = true, optional = true }
+gilrs = { workspace = true, optional = true }
+git2 = { workspace = true, optional = true }
+glam = { workspace = true, optional = true }
+hecs = { workspace = true, optional = true }
+log = { workspace = true, optional = true }
+log-once = { workspace = true, optional = true }
+model_to_image = { workspace = true, optional = true }
+once_cell = { workspace = true, optional = true }
+open = { workspace = true, optional = true }
+parking_lot = { workspace = true, optional = true }
+rfd = { workspace = true, optional = true }
+rhai = { workspace = true, optional = true }
+ron = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+tokio = { workspace = true, optional = true }
+transform-gizmo-egui = { workspace = true, optional = true }
+wgpu = { workspace = true, optional = true }
+winit = { workspace = true, optional = true }
+clap = { workspace = true, optional = true }
+walkdir = { workspace = true, optional = true }
+zip = { workspace = true, optional = true }
+
+[features]
+editor = [
+    "anyhow",
+    "app_dirs2",
+    "bincode",
+    "chrono",
+    "dropbear-engine",
+    "egui-toast-fork",
+    "egui",
+    "egui_dnd",
+    "egui_dock-fork",
+    "egui_extras",
+    "gilrs",
+    "git2",
+    "glam",
+    "hecs",
+    "log",
+    "log-once",
+    "model_to_image",
+    "once_cell",
+    "open",
+    "parking_lot",
+    "rfd",
+    "rhai",
+    "ron",
+    "serde",
+    "tokio",
+    "transform-gizmo-egui",
+    "wgpu",
+    "winit",
+    "clap",
+    "walkdir",
+    "zip"
+]
+default = ["editor"]
+
+data-only = [
+    "serde",
+    "bincode",
+    "anyhow",
+    "tokio",
+    "glam",
+    "log",
+    "dropbear-engine",
+    "rhai",
+    "winit",
+    "hecs",
+    "ron",
+    "git2",
+    "log-once",
+    "once_cell",
+    "egui",
+    "chrono",
+    "parking_lot",
+    "walkdir",
+    "zip"
+]
 
 [build-dependencies]
 anyhow = "1.0"
 app_dirs2 = "2.5"
 reqwest = { version = "0.12", features = ["blocking"] }
-zip = "4.3"
+zip = "4.3"
\ No newline at end of file
diff --git a/eucalyptus/README.md b/eucalyptus/README.md
index 351a158..5fe767a 100644
--- a/eucalyptus/README.md
+++ b/eucalyptus/README.md
@@ -1,3 +1,3 @@
 # eucalyptus
 
-The game editor for the dropbear game engine. The game engine is powered by [dropbear](https://github.com/4tkbytes/dropbear/tree/main/dropbear-engine) and uses the [redback](https://github.com/4tkbytes/dropbear/tree/main/redback) to bind, build and ship your game. 
\ No newline at end of file
+The game editor for the dropbear game engine. The game engine is powered by [dropbear](https://github.com/4tkbytes/dropbear/tree/main/dropbear-engine) and uses the [redback](https://github.com/4tkbytes/dropbear/tree/main/redback) runtime to run and load your game in an easy way.
diff --git a/eucalyptus/src/build/mod.rs b/eucalyptus/src/build/mod.rs
index b20e171..ffb67cc 100644
--- a/eucalyptus/src/build/mod.rs
+++ b/eucalyptus/src/build/mod.rs
@@ -1,18 +1,452 @@
-use std::path::PathBuf;
+use std::{collections::HashMap, fs, path::PathBuf, process::Command};
 
 use clap::ArgMatches;
 
-pub(crate) fn package(_project_path: PathBuf, _sub_matches: &ArgMatches) {
-    todo!()
+use crate::states::{ProjectConfig, RuntimeData, SCENES, SOURCE};
+
+pub fn package(project_path: PathBuf, _sub_matches: &ArgMatches) -> anyhow::Result<()> {
+    if !project_path.exists() {
+        return Err(anyhow::anyhow!("Unable to locate project config file"));
+    }
+
+    let build_dir = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build");
+
+    // check health
+    println!("Checking health (checking if commands exist)");
+    health()?;
+    println!("Health check completed!");
+
+    let clone_dir = build_dir.join("redback-runtime");
+    
+    if clone_dir.exists() {
+        println!("Repository directory exists, checking for updates...");
+        if should_update_repository(&clone_dir)? {
+            println!("Repository has changes or is outdated, removing and re-cloning...");
+            std::fs::remove_dir_all(&clone_dir)?;
+            clone_repository(&build_dir)?;
+        } else {
+            println!("Repository is up to date, skipping clone");
+        }
+    } else {
+        println!("Cloning repository");
+        clone_repository(&build_dir)?;
+    }
+
+    let project_config = ProjectConfig::read_from(&project_path)?;
+    let project_name = project_config.project_name.clone();
+
+    // cd into redback-runtime folder and compile redback-runtime using cargo
+    let runtime_dir = build_dir.join("redback-runtime");
+    if !runtime_dir.exists() {
+        return Err(anyhow::anyhow!("redback-runtime directory not found after cloning"));
+    }
+
+    let cargo_toml_path = runtime_dir.join("Cargo.toml");
+    if cargo_toml_path.exists() {
+        let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?;
+        let modified_content = cargo_toml_content.replace(
+            r#"name = "redback-runtime""#, 
+            &format!(r#"name = "{}""#, project_name)
+        );
+        std::fs::write(&cargo_toml_path, modified_content)?;
+        println!("Updated Cargo.toml with project name: {}", project_name);
+    }
+
+    println!("Building {} for release", project_name);
+    let cargo_build = Command::new("cargo")
+        .args(&["build", "--release"])
+        .current_dir(&runtime_dir)
+        .output()?;
+
+    if !cargo_build.status.success() {
+        let stderr = String::from_utf8_lossy(&cargo_build.stderr);
+        return Err(anyhow::anyhow!("Failed to build {}:\n{}", project_name, stderr));
+    }
+    println!("{} built successfully!", project_name);
+
+    let target_dir = runtime_dir.join("target").join("release");
+    let exe_name = if cfg!(target_os = "windows") {
+        format!("{}.exe", project_name)
+    } else {
+        project_name.clone()
+    };
+    
+    let built_exe = target_dir.join(&exe_name);
+    if !built_exe.exists() {
+        return Err(anyhow::anyhow!("Built executable not found at: {}", built_exe.display()));
+    }
+
+    println!("Building project data (.eupak file)");
+    build(project_path.clone(), _sub_matches)?;
+
+    let output_dir = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join("package");
+    std::fs::create_dir_all(&output_dir)?;
+
+    let output_exe = output_dir.join(&exe_name);
+    
+    println!("Copying executable to: {}", output_exe.display());
+    std::fs::copy(&built_exe, &output_exe)?;
+
+    let eupak_source = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join("output").join(format!("{}.eupak", project_name));
+    let eupak_dest = output_dir.join(format!("{}.eupak", project_name));
+    
+    if !eupak_source.exists() {
+        return Err(anyhow::anyhow!("Expected .eupak file not found at: {}", eupak_source.display()));
+    }
+    
+    println!("Copying .eupak file to: {}", eupak_dest.display());
+    std::fs::copy(&eupak_source, &eupak_dest)?;
+
+    let project_resources = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("resources");
+    if project_resources.exists() {
+        println!("Copying resources folder...");
+        let output_resources = output_dir.join("resources");
+        copy_resources_folder(&project_resources, &output_resources)?;
+    }
+
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::PermissionsExt;
+        let mut perms = std::fs::metadata(&output_exe)?.permissions();
+        perms.set_mode(0o755); // rwxr-xr-x
+        std::fs::set_permissions(&output_exe, perms)?;
+    }
+
+    copy_system_libraries(&output_dir)?;
+
+    println!("Creating zip package...");
+    let zip_path = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join(format!("{}.zip", project_name));
+    create_zip_package(&output_dir, &zip_path)?;
+
+    println!("Cleaning up temporary files");
+    if build_dir.join("redback-runtime").exists() {
+        std::fs::remove_dir_all(build_dir.join("redback-runtime"))?;
+    }
+    if build_dir.join("output").exists() {
+        std::fs::remove_dir_all(build_dir.join("output"))?;
+    }
+
+    println!("\n✓ Package completed successfully!");
+    println!("Output directory: {}", output_dir.display());
+    println!("Zip package: {}", zip_path.display());
+    println!("Executable: {}", exe_name);
+    println!("Data file: {}.eupak", project_name);
+
+    Ok(())
 }
 
-pub(crate) fn build(_project_path: PathBuf, _sub_matches: &ArgMatches) {
-    todo!()
+fn clone_repository(build_dir: &PathBuf) -> anyhow::Result<()> {
+    git2::build::RepoBuilder::new()
+        .clone("https://github.com/4tkbytes/redback-runtime", &build_dir.join("redback-runtime"))?;
+    println!("Repository cloned successfully!");
+    Ok(())
 }
 
-pub(crate) fn health() {
-    todo!()
+fn should_update_repository(repo_dir: &PathBuf) -> anyhow::Result<bool> {
+    let repo = match git2::Repository::open(repo_dir) {
+        Ok(repo) => repo,
+        Err(_) => {
+            return Ok(true);
+        }
+    };
+
+    let statuses = repo.statuses(None)?;
+    if !statuses.is_empty() {
+        println!("Found local changes in repository");
+        return Ok(true);
+    }
+
+    let mut remote = repo.find_remote("origin")?;
+    remote.fetch(&["refs/heads/*:refs/remotes/origin/*"], None, None)?;
+
+    let head = repo.head()?.target().ok_or(anyhow::anyhow!("No HEAD commit"))?;
+    
+    let remote_ref = if let Ok(remote_main) = repo.find_reference("refs/remotes/origin/main") {
+        remote_main
+    } else if let Ok(remote_master) = repo.find_reference("refs/remotes/origin/master") {
+        remote_master
+    } else {
+        return Ok(true);
+    };
+
+    let remote_commit = remote_ref.target().ok_or(anyhow::anyhow!("No remote commit"))?;
+
+    Ok(head != remote_commit)
 }
 
-#[allow(dead_code)]
-pub(crate) fn play(_project_path: &PathBuf) {}
+fn copy_resources_folder(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> {
+    std::fs::create_dir_all(dest)?;
+
+    for entry in fs::read_dir(src)? {
+        let entry = entry?;
+        let src_path = entry.path();
+        let file_name = entry.file_name();
+
+        if file_name == "resources.eucc" {
+            continue;
+        }
+
+        let dest_path = dest.join(&file_name);
+
+        if src_path.is_dir() {
+            copy_resources_folder(&src_path, &dest_path)?;
+        } else {
+            std::fs::copy(&src_path, &dest_path)?;
+        }
+    }
+
+    Ok(())
+}
+
+fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> {
+    #[cfg(target_os = "windows")]
+    {
+        let dll_paths = vec![
+            "C:\\vcpkg\\installed\\x64-windows\\bin\\assimp-vc143-mt.dll",
+            "C:\\vcpkg\\installed\\x64-windows\\bin\\assimp.dll",
+            "C:\\Program Files\\Assimp\\bin\\assimp.dll",
+            "C:\\Program Files (x86)\\Assimp\\bin\\assimp.dll",
+        ];
+
+        for dll_path in dll_paths {
+            if std::path::Path::new(dll_path).exists() {
+                let dll_name = std::path::Path::new(dll_path).file_name().unwrap();
+                let dest = output_dir.join(dll_name);
+                std::fs::copy(dll_path, dest)?;
+                println!("Copied system library: {}", dll_name.to_string_lossy());
+                break;
+            }
+        }
+    }
+
+    #[cfg(any(target_os = "linux", target_os = "macos"))]
+    {
+        let lib_dir = output_dir.join("lib");
+        std::fs::create_dir_all(&lib_dir)?;
+
+        let lib_paths = vec![
+            "/usr/lib/libassimp.so",
+            "/usr/lib/x86_64-linux-gnu/libassimp.so",
+            "/usr/lib64/libassimp.so",
+            "/usr/local/lib/libassimp.so",
+            "/opt/homebrew/lib/libassimp.dylib",
+            "/usr/local/lib/libassimp.dylib",
+        ];
+
+        for lib_path in lib_paths {
+            if std::path::Path::new(lib_path).exists() {
+                let lib_name = std::path::Path::new(lib_path).file_name().unwrap();
+                let dest = lib_dir.join(lib_name);
+                std::fs::copy(lib_path, dest)?;
+                println!("Copied system library: {}", lib_name.to_string_lossy());
+                break;
+            }
+        }
+    }
+
+    Ok(())
+}
+
+fn create_zip_package(source_dir: &PathBuf, zip_path: &PathBuf) -> anyhow::Result<()> {
+    let file = std::fs::File::create(zip_path)?;
+    let mut zip = zip::ZipWriter::new(file);
+
+    let walkdir = walkdir::WalkDir::new(source_dir);
+    for entry in walkdir {
+        let entry = entry?;
+        let path = entry.path();
+        
+        if path.is_file() {
+            let relative_path = path.strip_prefix(source_dir)?;
+            let name = relative_path.to_string_lossy();
+            
+            zip.start_file(name, zip::write::FileOptions::default())?;
+            let mut file = std::fs::File::open(path)?;
+            std::io::copy(&mut file, &mut zip)?;
+        }
+    }
+
+    zip.finish()?;
+    println!("Created zip package: {}", zip_path.display());
+    Ok(())
+}
+
+pub fn read_from_eupak(eupak_path: PathBuf) -> anyhow::Result<()> {
+    let bytes = std::fs::read(&eupak_path)?;
+    let (content, _): (RuntimeData, usize) = bincode::decode_from_slice(&bytes, bincode::config::standard())?;
+    println!("{} contents: {:#?}", eupak_path.display(), content);
+    Ok(())
+}
+
+pub fn build(project_path: PathBuf, _sub_matches: &ArgMatches) -> anyhow::Result<()> {
+    if !project_path.exists() {
+        return Err(anyhow::anyhow!("Unable to locate project config file"));
+    }
+    ProjectConfig::read_from(&project_path)?.load_config_to_memory()?;
+    
+    let mut project_config = ProjectConfig::read_from(&project_path)?;
+    project_config.load_config_to_memory()?;
+
+    let source_config = {
+        let source_guard = SOURCE.read().map_err(|_| anyhow::anyhow!("Unable to lock SOURCE"))?;
+        source_guard.clone()
+    };
+
+    let scene_data = {
+        let scenes_guard = SCENES.read().map_err(|_| anyhow::anyhow!("Unable to lock SCENES"))?;
+        scenes_guard.clone()
+    };
+
+    let build_dir = project_path.parent().unwrap().join("build").join("output");
+    std::fs::create_dir_all(&build_dir)?;
+
+    let project_name = project_config.project_name.clone();
+
+    let mut scripts = HashMap::new();
+    let script_dir = project_path.parent().unwrap().join("src");
+    if script_dir.exists() {
+        for entry in fs::read_dir(&script_dir)? {
+            let entry = entry?;
+            let path = entry.path();
+            if let Some(ext) = path.extension() {
+                if ext == "rhai" {
+                    let name = path.file_name().unwrap().to_string_lossy().to_string();
+                    let contents = fs::read_to_string(&path)?;
+                    scripts.insert(name, contents);
+                }
+            }
+        }
+    }
+
+    let runtime_data = RuntimeData {
+        project_config,
+        source_config,
+        scene_data,
+        scripts
+    };
+
+    let runtime_file = build_dir.join(format!("{}.eupak", project_name));
+    let serialized = bincode::serde::encode_to_vec(runtime_data, bincode::config::standard())?;
+    std::fs::write(&runtime_file, serialized)?;
+
+    println!("Build completed successfully. Output at {:?}", runtime_file.display());
+    Ok(())
+}
+
+pub fn health() -> anyhow::Result<()> {
+    let mut all_healthy = true;
+    
+    match Command::new("cargo").arg("--version").output() {
+        Ok(output) => {
+            if output.status.success() {
+                let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
+                println!("Does cargo exist? ✓ YES - {}", version);
+                
+                match Command::new("rustc").arg("--version").output() {
+                    Ok(rustc_output) => {
+                        if rustc_output.status.success() {
+                            let rustc_version = String::from_utf8_lossy(&rustc_output.stdout).trim().to_string();
+                            println!("Does rustc compiler exist? ✓ YES - {}", rustc_version);
+                        } else {
+                            println!("Does rustc compiler exist? ✗ NO - rustc command failed");
+                            all_healthy = false;
+                        }
+                    }
+                    Err(_) => {
+                        println!("Does rustc compiler exist? ✗ NO - rustc not found in PATH");
+                        all_healthy = false;
+                    }
+                }
+            } else {
+                println!("Does cargo exist? ✗ NO - cargo command failed");
+                println!("Does rustc compiler exist? ✗ NO - cargo failed, cannot check rustc");
+                all_healthy = false;
+            }
+        }
+        Err(_) => {
+            println!("Does cargo exist? ✗ NO - cargo not found in PATH");
+            println!("Does rustc compiler exist? ✗ NO - cargo not found, cannot check rustc");
+            all_healthy = false;
+        }
+    }
+    
+    let assimp_found = check_assimp_availability();
+    if assimp_found {
+        println!("Does assimp lib exist? ✓ YES - Found assimp library");
+    } else {
+        println!("Does assimp lib exist? ⚠ MAYBE - Could not definitively locate assimp, but it may be available through system package manager or vcpkg");
+    }
+    
+    if all_healthy {
+        println!("\n✓ All core tools are available!");
+    } else {
+        println!("\n✗ Some required tools are missing. Please install Rust and Cargo.");
+        return Err(anyhow::anyhow!("Health check failed - missing required tools"));
+    }
+    
+    Ok(())
+}
+
+fn check_assimp_availability() -> bool {
+    #[cfg(target_os = "windows")]
+    {
+        let common_paths = vec![
+            "C:\\vcpkg\\installed\\x64-windows\\lib\\assimp-vc143-mt.lib",
+            "C:\\vcpkg\\installed\\x64-windows\\lib\\assimp.lib",
+            "C:\\vcpkg\\installed\\x86-windows\\lib\\assimp-vc143-mt.lib",
+            "C:\\vcpkg\\installed\\x86-windows\\lib\\assimp.lib",
+            "C:\\Program Files\\Assimp\\lib\\assimp.lib",
+            "C:\\Program Files (x86)\\Assimp\\lib\\assimp.lib",
+        ];
+        
+        for path in common_paths {
+            if std::path::Path::new(path).exists() {
+                return true;
+            }
+        }
+        
+        if let Ok(output) = Command::new("where").arg("assimp.dll").output() {
+            if output.status.success() {
+                return true;
+            }
+        }
+    }
+    
+    #[cfg(any(target_os = "linux", target_os = "macos"))]
+    {
+        let common_paths = vec![
+            "/usr/lib/libassimp.so",
+            "/usr/lib/x86_64-linux-gnu/libassimp.so",
+            "/usr/lib64/libassimp.so",
+            "/usr/local/lib/libassimp.so",
+            "/opt/homebrew/lib/libassimp.dylib",
+            "/usr/local/lib/libassimp.dylib",
+        ];
+        
+        for path in common_paths {
+            if std::path::Path::new(path).exists() {
+                return true;
+            }
+        }
+        
+        if let Ok(output) = Command::new("pkg-config").args(&["--exists", "assimp"]).output() {
+            if output.status.success() {
+                return true;
+            }
+        }
+        
+        #[cfg(target_os = "linux")]
+        {
+            if let Ok(output) = Command::new("ldconfig").args(&["-p"]).output() {
+                if output.status.success() {
+                    let output_str = String::from_utf8_lossy(&output.stdout);
+                    if output_str.contains("libassimp") {
+                        return true;
+                    }
+                }
+            }
+        }
+    }
+    
+    false
+}
diff --git a/eucalyptus/src/camera.rs b/eucalyptus/src/camera.rs
index 899410c..5a24362 100644
--- a/eucalyptus/src/camera.rs
+++ b/eucalyptus/src/camera.rs
@@ -5,7 +5,7 @@ use glam::DVec3;
 use serde::{Deserialize, Serialize};
 use winit::keyboard::KeyCode;
 
-pub trait CameraController {
+pub trait CameraController: std::fmt::Debug {
     fn update(&mut self, camera: &mut Camera, dt: f32);
     fn handle_keyboard_input(&mut self, camera: &mut Camera, pressed_keys: &HashSet<KeyCode>);
     fn handle_mouse_input(&mut self, camera: &mut Camera, mouse_delta: Option<(f64, f64)>);
@@ -14,6 +14,7 @@ pub trait CameraController {
     fn as_any_mut(&mut self) -> &mut dyn Any;
 }
 
+#[derive(Debug)]
 pub struct DebugCameraController {
     #[allow(dead_code)]
     pub speed: f64,
@@ -68,6 +69,7 @@ impl CameraController for DebugCameraController {
 }
 
 #[allow(dead_code)]
+#[derive(Debug)]
 pub struct PlayerCameraController {
     pub follow_target: Option<hecs::Entity>,
     pub offset: DVec3,
@@ -150,6 +152,7 @@ pub enum CameraType {
     Player,
 }
 
+#[derive(Debug)]
 pub struct CameraManager {
     cameras: HashMap<CameraType, Camera>,
     controllers: HashMap<CameraType, Box<dyn CameraController>>,
diff --git a/eucalyptus/src/editor/dock.rs b/eucalyptus/src/editor/dock.rs
index 8c0b013..cce1b45 100644
--- a/eucalyptus/src/editor/dock.rs
+++ b/eucalyptus/src/editor/dock.rs
@@ -100,7 +100,7 @@ pub static TABS_GLOBAL: LazyLock<Mutex<INeedABetterNameForThisStruct>> =
     LazyLock::new(|| Mutex::new(INeedABetterNameForThisStruct::default()));
 
 #[derive(Default)]
-pub(crate) struct INeedABetterNameForThisStruct {
+pub struct INeedABetterNameForThisStruct {
     show_context_menu: bool,
     context_menu_pos: egui::Pos2,
     context_menu_tab: Option<EditorTab>,
diff --git a/eucalyptus/src/editor/input.rs b/eucalyptus/src/editor/input.rs
index 676bacc..210a86d 100644
--- a/eucalyptus/src/editor/input.rs
+++ b/eucalyptus/src/editor/input.rs
@@ -228,8 +228,9 @@ impl Keyboard for Editor {
 
 impl Mouse for Editor {
     fn mouse_move(&mut self, position: PhysicalPosition<f64>) {
-        if self.is_viewport_focused
-            && matches!(self.viewport_mode, crate::utils::ViewportMode::CameraMove)
+        if (self.is_viewport_focused
+            && matches!(self.viewport_mode, crate::utils::ViewportMode::CameraMove))
+            || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked)
         {
             if let Some(window) = &self.window {
                 let size = window.inner_size();
diff --git a/eucalyptus/src/lib.rs b/eucalyptus/src/lib.rs
new file mode 100644
index 0000000..bccce4b
--- /dev/null
+++ b/eucalyptus/src/lib.rs
@@ -0,0 +1,19 @@
+#[cfg(feature = "editor")]
+pub mod editor;
+#[cfg(feature = "editor")]
+pub mod menu;
+
+#[cfg(feature = "editor")]
+pub mod build;
+
+pub mod camera;
+pub mod logging;
+pub mod scripting;
+pub mod states;
+pub mod utils;
+
+#[cfg(feature = "editor")]
+pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo {
+    name: "Eucalyptus",
+    author: "4tkbytes",
+};
\ No newline at end of file
diff --git a/eucalyptus/src/logging.rs b/eucalyptus/src/logging.rs
index dc0f3d4..ebed13e 100644
--- a/eucalyptus/src/logging.rs
+++ b/eucalyptus/src/logging.rs
@@ -5,11 +5,18 @@
 //! - Console
 //! - File (to be implemented)
 
+#[cfg(feature = "editor")]
 use egui::Context;
+
+#[cfg(feature = "editor")]
 use egui_toast_fork::Toasts;
+
+#[cfg(feature = "editor")]
 use once_cell::sync::Lazy;
+#[cfg(feature = "editor")]
 use parking_lot::Mutex;
 
+#[cfg(feature = "editor")]
 pub static GLOBAL_TOASTS: Lazy<Mutex<Toasts>> = Lazy::new(|| {
     Mutex::new(
         Toasts::new()
@@ -21,6 +28,7 @@ pub static GLOBAL_TOASTS: Lazy<Mutex<Toasts>> = Lazy::new(|| {
 /// Renders the toasts. Requires an egui context.
 ///
 /// Useful when paired with a function that contains [`crate`]
+#[cfg(feature = "editor")]
 pub(crate) fn render(context: &Context) {
     let mut toasts = GLOBAL_TOASTS.lock();
     toasts.show(context);
@@ -38,6 +46,7 @@ macro_rules! fatal {
         let _msg = format!($($arg)*);
         log::error!("{}", _msg);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
@@ -66,6 +75,7 @@ macro_rules! success {
         let _msg = format!($($arg)*);
         log::debug!("{}", _msg);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
@@ -95,6 +105,7 @@ macro_rules! warn {
         let _msg = format!($($arg)*);
         log::warn!("{}", _msg);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
@@ -123,6 +134,7 @@ macro_rules! info {
         let _msg = format!($($arg)*);
         log::debug!("{}", _msg);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
@@ -152,6 +164,7 @@ macro_rules! info_without_console {
     ($($arg:tt)*) => {
         let _msg = format!($($arg)*);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
@@ -181,6 +194,7 @@ macro_rules! success_without_console {
     ($($arg:tt)*) => {
         let _msg = format!($($arg)*);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
@@ -210,6 +224,7 @@ macro_rules! warn_without_console {
     ($($arg:tt)*) => {
         let _msg = format!($($arg)*);
 
+        #[cfg(feature = "editor")]
         {
             use egui_toast_fork::{Toast, ToastKind};
             use crate::logging::GLOBAL_TOASTS;
diff --git a/eucalyptus/src/main.rs b/eucalyptus/src/main.rs
index f8f1ce3..9dbf4fa 100644
--- a/eucalyptus/src/main.rs
+++ b/eucalyptus/src/main.rs
@@ -1,25 +1,14 @@
-mod editor;
-mod menu;
-
-pub(crate) mod build;
-pub(crate) mod camera;
-pub(crate) mod logging;
-pub(crate) mod scripting;
-pub(crate) mod states;
-pub(crate) mod utils;
-
-use std::{cell::RefCell, fs, path::PathBuf, rc::Rc};
-
+#[cfg(feature = "editor")]
+use std::{cell::RefCell, rc::Rc, fs, path::PathBuf};
+#[cfg(feature = "editor")]
 use clap::{Arg, Command};
-use dropbear_engine::{WindowConfiguration, scene};
 
-pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo {
-    name: "Eucalyptus",
-    author: "4tkbytes",
-};
+#[cfg(feature = "editor")]
+use dropbear_engine::{WindowConfiguration, scene};
 
 #[tokio::main]
-async fn main() {
+#[cfg(feature = "editor")]
+async fn main() -> anyhow::Result<()> {
     let matches = Command::new("eucalyptus")
         .about("A visual game editor")
         .version("1.0.0")
@@ -27,7 +16,7 @@ async fn main() {
         .arg_required_else_help(false)
         .subcommand(
             Command::new("build")
-                .about("Build a eucalyptus project")
+                .about("Build a eucalyptus project, but only the .eupak file and its resources")
                 .arg(
                     Arg::new("project")
                         .help("Path to the .eucp project file")
@@ -37,7 +26,7 @@ async fn main() {
         )
         .subcommand(
             Command::new("package")
-                .about("Package a eucalyptus project")
+                .about("Package a eucalyptus project, which compiles the runtime and the resource .eupak file")
                 .arg(
                     Arg::new("project")
                         .help("Path to the .eucp project file")
@@ -45,6 +34,15 @@ async fn main() {
                         .required(false),
                 ),
         )
+        .subcommand(Command::new("read")
+            .about("Reads and displays the contents of a .eupak file for debugging")
+            .arg(
+                    Arg::new("eupak_file")
+                        .help("Path to the .eupak file")
+                        .value_name("RESOURCE_FILE")
+                        .required(false),
+                ),
+        )
         .subcommand(Command::new("health").about("Check the health of the eucalyptus installation"))
         .get_matches();
 
@@ -61,7 +59,7 @@ async fn main() {
                 },
             };
 
-            crate::build::build(project_path, sub_matches);
+            eucalyptus::build::build(project_path, sub_matches)?;
         }
         Some(("package", sub_matches)) => {
             let project_path = match sub_matches.get_one::<String>("project") {
@@ -75,21 +73,35 @@ async fn main() {
                 },
             };
 
-            crate::build::package(project_path, sub_matches);
+            eucalyptus::build::package(project_path, sub_matches)?;
         }
         Some(("health", _)) => {
-            crate::build::health();
+            eucalyptus::build::health()?;
+        }
+        Some(("read", sub_matches)) => {
+            let project_path = match sub_matches.get_one::<String>("eupak_file") {
+                Some(path) => PathBuf::from(path),
+                None => match find_eucp_file() {
+                    Ok(path) => path,
+                    Err(e) => {
+                        eprintln!("Error: {}", e);
+                        std::process::exit(1);
+                    }
+                },
+            };
+
+            eucalyptus::build::read_from_eupak(project_path)?;
         }
         None => {
             let config = WindowConfiguration {
-                title: "Eucalyptus, built with dropbear",
+                title: "Eucalyptus, built with dropbear".into(),
                 windowed_mode: dropbear_engine::WindowedModes::Maximised,
                 max_fps: dropbear_engine::App::NO_FPS_CAP,
             };
 
             let _app = dropbear_engine::run_app!(config, |mut scene_manager, mut input_manager| {
-                let main_menu = Rc::new(RefCell::new(menu::MainMenu::new()));
-                let editor = Rc::new(RefCell::new(editor::Editor::new()));
+                let main_menu = Rc::new(RefCell::new(eucalyptus::menu::MainMenu::new()));
+                let editor = Rc::new(RefCell::new(eucalyptus::editor::Editor::new()));
 
                 scene::add_scene_with_input(
                     &mut scene_manager,
@@ -112,8 +124,16 @@ async fn main() {
         }
         _ => unreachable!(),
     }
+    Ok(())
+}
+
+#[cfg(not(feature = "editor"))]
+fn main() {
+    panic!("You have not enabled the \"editor\" feature, therefore cannot use the eucalyptus editor. 
+Ether import as a lib to use its structs and enums or enable the editor feature");
 }
 
+#[cfg(feature = "editor")]
 fn find_eucp_file() -> Result<PathBuf, String> {
     let current_dir = std::env::current_dir().map_err(|_| "Failed to get current directory")?;
 
diff --git a/eucalyptus/src/scene1.rs b/eucalyptus/src/scene1.rs
index 07949c4..e5efb8d 100644
--- a/eucalyptus/src/scene1.rs
+++ b/eucalyptus/src/scene1.rs
@@ -48,7 +48,6 @@ impl TestingScene1 {
     }
 }
 
-#[async_trait]
 impl Scene for TestingScene1 {
     fn load(&mut self, graphics: &mut Graphics) {
         let shader = Shader::new(
@@ -121,6 +120,22 @@ impl Scene for TestingScene1 {
             a: 1.0,
         };
 
+        // Note: new versions use egui rendering for viewport instead
+        // of using the full window
+        let texture_id = graphics.state.texture_id.clone();
+        let (display_width, display_height) = graphics.screen_size;
+        egui::CentralPanel::default()
+            .frame(egui::Frame::new())
+            .show(graphics.get_egui_context(), |ui| {
+                let rect = ui.max_rect();
+                ui.put(
+                    rect,
+                    egui::Image::new((texture_id, [display_width, display_height].into()))
+                        .fit_to_exact_size([display_width, display_height].into()),
+                );
+            });
+
+        // render everything here
         if let Some(pipeline) = &self.render_pipeline {
             {
                 let mut query = self.world.query::<(&AdoptedEntity, &Transform)>();
diff --git a/eucalyptus/src/scripting/input.rs b/eucalyptus/src/scripting/input.rs
index 3d81e05..416d11b 100644
--- a/eucalyptus/src/scripting/input.rs
+++ b/eucalyptus/src/scripting/input.rs
@@ -11,6 +11,7 @@ pub struct InputState {
     pub mouse_button: HashSet<MouseButton>,    
     pub pressed_keys: HashSet<KeyCode>,
     pub mouse_delta: Option<(f64, f64)>,
+    pub is_cursor_locked: bool,
 }
 
 impl Default for InputState {
@@ -28,9 +29,16 @@ impl InputState {
             last_key_press_times: HashMap::new(),
             double_press_threshold: Duration::from_millis(300),
             mouse_delta: None,
+            is_cursor_locked: false,
         }
     }
 
+    /// Locks the cursor. If its true, it locks the cursor, hiding the mouse and setting the value
+    /// or is_cursor_locked true, which can be used for mouse movement in the scene. 
+    pub fn lock_cursor(&mut self, toggle: bool) {
+        self.is_cursor_locked = toggle;
+    }
+
     pub fn register_input_modules(engine: &mut Engine) {
         engine.register_type_with_name::<InputState>("InputState");
         engine.register_type_with_name::<Key>("Key");
@@ -41,7 +49,8 @@ impl InputState {
             .register_get("mouse_y", |s: &mut InputState| s.mouse_pos.1)
             .register_get("has_mouse_delta", |s: &mut InputState| s.mouse_delta.is_some())
             .register_fn("mouse_dx", |s: &mut InputState| s.mouse_delta.map(|d| d.0).unwrap_or(0.0))
-            .register_fn("mouse_dy", |s: &mut InputState| s.mouse_delta.map(|d| d.1).unwrap_or(0.0));
+            .register_fn("mouse_dy", |s: &mut InputState| s.mouse_delta.map(|d| d.1).unwrap_or(0.0))
+            .register_fn("lock_cursor", |s: &mut InputState, value: bool| s.is_cursor_locked = value);
 
         let mut kmod = Module::new();
 
diff --git a/eucalyptus/src/scripting/mod.rs b/eucalyptus/src/scripting/mod.rs
index ad720ae..ff60a19 100644
--- a/eucalyptus/src/scripting/mod.rs
+++ b/eucalyptus/src/scripting/mod.rs
@@ -333,6 +333,17 @@ impl ScriptManager {
         Ok(())
     }
 
+    pub fn load_script_from_source(&mut self, script_name: &String, script_content: &String) -> anyhow::Result<String> {
+        let ast = self.engine.compile(&script_content).map_err(|e| {
+            log::error!("Compiling AST for script [{}] returned an error: {}", script_name, e);
+            e
+        })?;
+        self.compiled_scripts.insert(script_name.clone(), ast);
+
+        log::info!("Loaded script: {}", script_name);
+        Ok(script_name.to_string())
+    }
+
     pub fn load_script(&mut self, script_path: &PathBuf) -> anyhow::Result<String> {
         let script_content = fs::read_to_string(script_path)?;
         let script_name = script_path
diff --git a/eucalyptus/src/states.rs b/eucalyptus/src/states.rs
index c2adc71..577f0b1 100644
--- a/eucalyptus/src/states.rs
+++ b/eucalyptus/src/states.rs
@@ -12,20 +12,26 @@ use std::{
     sync::RwLock,
 };
 
+use bincode::{Encode, Decode};
 use chrono::Utc;
 use dropbear_engine::{
     camera::Camera,
     entity::{AdoptedEntity, Transform},
     graphics::Graphics,
 };
+
+#[cfg(feature = "editor")]
 use egui_dock_fork::DockState;
+
 use hecs;
 use log;
 use once_cell::sync::Lazy;
 use ron::ser::PrettyConfig;
 use serde::{Deserialize, Serialize};
 
-use crate::{camera::CameraType, editor::EditorTab};
+use crate::camera::CameraType;
+#[cfg(feature = "editor")]
+use crate::editor::EditorTab;
 
 pub static PROJECT: Lazy<RwLock<ProjectConfig>> =
     Lazy::new(|| RwLock::new(ProjectConfig::default()));
@@ -42,6 +48,7 @@ pub static SCENES: Lazy<RwLock<Vec<SceneConfig>>> = Lazy::new(|| RwLock::new(Vec
 /// # Location
 /// This file is {project_name}.eucp and is located at {project_dir}/
 #[derive(Debug, Deserialize, Serialize, Default)]
+#[cfg(feature = "editor")]
 pub struct ProjectConfig {
     pub project_name: String,
     pub project_path: PathBuf,
@@ -51,21 +58,47 @@ pub struct ProjectConfig {
     pub dock_layout: Option<DockState<EditorTab>>,
 }
 
+#[derive(Debug, Deserialize, Serialize, Default)]
+#[cfg(not(feature = "editor"))]
+pub struct ProjectConfig {
+    pub project_name: String,
+    pub project_path: PathBuf,
+    pub date_created: String,
+    pub date_last_accessed: String,
+    // #[serde(default)]
+    // pub dock_layout: Option<DockState<EditorTab>>,
+}
+
 impl ProjectConfig {
     /// Creates a new instance of the ProjectConfig. This function is typically used when creating
     /// a new project, with it creating new defaults for everything.
     pub fn new(project_name: String, project_path: &PathBuf) -> Self {
         let date_created = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
         let date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
-        let mut result = Self {
-            project_name,
-            project_path: project_path.to_path_buf(),
-            date_created,
-            date_last_accessed,
-            dock_layout: None,
-        };
-        let _ = result.load_config_to_memory(); // TODO: Deal with later...
-        result
+        #[cfg(not(feature = "editor"))]
+        {
+            let mut result = Self {
+                project_name,
+                project_path: project_path.to_path_buf(),
+                date_created,
+                date_last_accessed,
+            };
+            let _ = result.load_config_to_memory(); // TODO: Deal with later...
+            result
+        }
+
+        #[cfg(feature = "editor")]
+        {
+            let mut result = Self {
+                project_name,
+                project_path: project_path.to_path_buf(),
+                date_created,
+                date_last_accessed,
+                dock_layout: None,
+            };
+            let _ = result.load_config_to_memory(); // TODO: Deal with later...
+            result
+        }
     }
 
     /// This function writes the [`ProjectConfig`] struct (and other PathBufs) to a file of the choice
@@ -240,20 +273,20 @@ pub(crate) fn path_contains_folder(path: &PathBuf, folder: &str) -> bool {
     path.components().any(|comp| comp.as_os_str() == folder)
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub enum Node {
     File(File),
     Folder(Folder),
 }
 
-#[derive(Default, Debug, Serialize, Deserialize)]
+#[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct File {
     pub name: String,
     pub path: PathBuf,
     pub resource_type: Option<ResourceType>,
 }
 
-#[derive(Default, Debug, Serialize, Deserialize)]
+#[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct Folder {
     pub name: String,
     pub path: PathBuf,
@@ -337,7 +370,7 @@ impl ResourceConfig {
     }
 }
 
-#[derive(Default, Debug, Serialize, Deserialize)]
+#[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct SourceConfig {
     /// The path to the resource folder.
     pub path: PathBuf,
@@ -510,7 +543,6 @@ pub struct SceneConfig {
     pub scene_name: String,
     pub path: PathBuf,
     pub entities: Vec<SceneEntity>,
-    // pub camera: (SceneCameraConfig, CameraType),
     pub camera: HashMap<CameraType, SceneCameraConfig>,
     // todo later
     // pub settings: SceneSettings,
@@ -546,6 +578,12 @@ impl Default for SceneCameraConfig {
     }
 }
 
+impl SceneCameraConfig {
+    pub fn into_camera(&self, graphics: &mut Graphics) -> Camera {
+        Camera::new(graphics, self.position.into(), self.target.into(), self.up.into(), self.aspect.into(), self.fov.into(), self.near.into(), self.far.into(), 5.0, 0.0125)
+    }
+}
+
 #[derive(Debug, Serialize, Deserialize, Clone, Default)]
 pub struct SceneEntity {
     pub model_path: PathBuf,
@@ -664,7 +702,6 @@ impl SceneConfig {
         world: &mut hecs::World,
         graphics: &Graphics,
     ) -> anyhow::Result<()> {
-        // Clear the world
         log::info!(
             "Loading scene [{}], clearing world with {} entities",
             self.scene_name,
@@ -845,3 +882,15 @@ impl SceneConfig {
         }
     }
 }
+
+#[derive(Decode, Encode, serde::Serialize, serde::Deserialize, Debug)]
+pub struct RuntimeData {
+    #[bincode(with_serde)]
+    pub project_config: ProjectConfig,
+    #[bincode(with_serde)]
+    pub source_config: SourceConfig,
+    #[bincode(with_serde)]
+    pub scene_data: Vec<SceneConfig>,
+    #[bincode(with_serde)]
+    pub scripts: HashMap<String, String>, // name, script_content
+}
\ No newline at end of file
diff --git a/eucalyptus/src/utils.rs b/eucalyptus/src/utils.rs
index 4b01906..ea8c4c0 100644
--- a/eucalyptus/src/utils.rs
+++ b/eucalyptus/src/utils.rs
@@ -1,16 +1,26 @@
+#[cfg(feature = "editor")]
 use std::{
-    fs,
-    path::PathBuf,
-    sync::mpsc::{self, Receiver},
+    fs, sync::mpsc::{self, Receiver},
 };
 
+use std::path::PathBuf;
+
+#[cfg(feature = "editor")]
 use anyhow::anyhow;
-use dropbear_engine::{camera::Camera, entity::Transform, scene::SceneCommand};
+use dropbear_engine::{camera::Camera, entity::Transform};
+#[cfg(feature = "editor")]
+use dropbear_engine::scene::SceneCommand;
+#[cfg(feature = "editor")]
 use egui::Context;
+
+#[cfg(feature = "editor")]
 use egui_toast_fork::{Toast, ToastOptions, Toasts};
+#[cfg(feature = "editor")]
 use git2::Repository;
 
-use crate::states::{ModelProperties, Node, PROJECT, ProjectConfig};
+use crate::states::{ModelProperties, Node};
+#[cfg(feature = "editor")]
+use crate::states::{PROJECT, ProjectConfig};
 
 pub fn search_nodes_recursively<'a, F>(nodes: &'a [Node], matcher: &F, results: &mut Vec<&'a Node>)
 where
@@ -43,6 +53,7 @@ pub enum ProjectProgress {
 /// Open a project file and update the global state.
 /// Returns Ok(Some(SceneCommand::SwitchScene)) on success, or an error string on failure.
 #[allow(dead_code)]
+#[cfg(feature = "editor")]
 pub fn open_project(
     scene_command: &mut SceneCommand,
     toast: &mut Toasts,
@@ -79,6 +90,8 @@ pub fn open_project(
 
 /// Start creating a new project in a background thread.
 /// Returns a Receiver for progress updates.
+/// 
+#[cfg(feature = "editor")]
 pub fn start_project_creation(
     project_name: String,
     project_path: Option<PathBuf>,
@@ -159,6 +172,7 @@ pub fn start_project_creation(
     Some(rx)
 }
 
+#[cfg(feature = "editor")]
 pub fn show_new_project_window<F>(
     ctx: &Context,
     show_new_project: &mut bool,
diff --git a/redback-runtime b/redback-runtime
new file mode 160000
index 0000000..81b4ef5
--- /dev/null
+++ b/redback-runtime
@@ -0,0 +1 @@
+Subproject commit 81b4ef5bd0c8b98a7983ef0668ea913658285a39