tirbofish/dropbear · diff
Merge pull request #3 from 4tkbytes/build-system
feature: Build system
Signature present but could not be verified.
Unverified
@@ -0,0 +1,3 @@ +[submodule "redback-runtime"] + path = redback-runtime + url = https://github.com/4tkbytes/redback-runtime @@ -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 @@ -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 @@ -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 @@ -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, @@ -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. /// @@ -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" @@ -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. +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. @@ -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 +} @@ -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>>, @@ -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>, @@ -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(); @@ -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", +}; @@ -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; @@ -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")?; @@ -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)>(); @@ -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(); @@ -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 @@ -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 +} @@ -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, @@ -0,0 +1 @@ +Subproject commit 81b4ef5bd0c8b98a7983ef0668ea913658285a39