tirbofish/dropbear · commit
98781d6454078b1f78b396b75c8f0d170b116559
created a new euca://internal URI for any models that are stored in the executables memory. first one i implemented was euca://internal/dropbear/cube
also created a way to package eucalyptus projects into one uniform data.eupak file.
also created a way to build for kotlin native.
also planning on creating a dropbear gradle plugin for depenedency management and neater building.
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -6,6 +6,8 @@ use std::path::Path; pub const EUCA_SCHEME: &str = "euca://"; +pub const INTERNAL_MODELS: &[&str] = &["cube"]; + /// Converts any supported resource reference into the canonical `euca://` form. /// /// The function trims whitespace, normalizes path separators, ensures the scheme @@ -0,0 +1,72 @@ +import org.gradle.kotlin.dsl.compileOnly + +plugins { + `kotlin-dsl` + `maven-publish` + id("com.gradle.plugin-publish") version "2.0.0" +} + +group = "com.dropbear" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation("de.undercouch:gradle-download-task:5.6.0") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${KotlinVersion.CURRENT}") +} + +gradlePlugin { + website.set("https://github.com/4tkbytes/dropbear") + vcsUrl.set("https://github.com/4tkbytes/dropbear") + plugins { + + create("dropbearGradlePlugin") { + id = "dropbear-gradle-plugin" + implementationClass = "com.dropbear.gradle.DropbearGradlePlugin" + displayName = "dropbear-gradle-plugin" + description = "Gradle plugin for dependency management for dropbear-based projects" + version = version as String + } + } +} + +publishing { + publications { + withType<MavenPublication>().configureEach { + pom { + name.set("dropbear-gradle-plugin") + description.set("Gradle plugin for dropbear dependency management") + url.set("https://4tkbytes.github.io/dropbear/") + + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + } + } + developers { + developer { + id.set("4tkbytes") + name.set("tk") + email.set("4tkbytes@pm.me") + } + } + scm { + connection.set("scm:git:git://github.com/4tkbytes/dropbear.git") + developerConnection.set("scm:git:ssh://github.com/4tkbytes/dropbear.git") + url.set("https://github.com/4tkbytes/dropbear") + } + } + } + } + + repositories { + maven { + name = "GitHubPages" + url = uri("${layout.buildDirectory}/repo") + } + } +} @@ -0,0 +1,10 @@ +package com.dropbear.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class DropbearGradlePlugin: Plugin<Project> { + override fun apply(target: Project) { + TODO("Not yet implemented") + } +} @@ -12,6 +12,7 @@ crate-type = ["rlib", "dylib"] [dependencies] dropbear-traits = { path = "../dropbear-traits" } dropbear-macro = { path = "../dropbear-macro" } +magna-carta = { path = "../magna-carta" } anyhow.workspace = true bincode.workspace = true @@ -8,6 +8,7 @@ use ron::ser::PrettyConfig; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; +use crate::runtime::RuntimeSettings; /// The root config file, responsible for building and other metadata. /// @@ -21,8 +22,13 @@ pub struct ProjectConfig { pub date_last_accessed: String, #[serde(default)] pub dock_layout: Option<DockState<EditorTab>>, + #[serde(default)] pub editor_settings: EditorSettings, + + #[serde(default)] + pub runtime_settings: RuntimeSettings, + #[serde(default)] pub last_opened_scene: Option<String>, } @@ -42,6 +48,7 @@ impl ProjectConfig { editor_settings: Default::default(), dock_layout: None, last_opened_scene: None, + runtime_settings: Default::default(), }; let _ = result.load_config_to_memory(); result @@ -1,27 +1,64 @@ -use crate::scene::SceneConfig; -use std::collections::HashMap; -// #[derive(bincode::Decode, bincode::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 -// } +use crate::scene::{SceneConfig}; +use crate::states::{PROJECT, SCENES}; +/// The settings of a project in its runtime. +/// +/// This is different to [`SceneSettings`], which contains settings for ONLY +/// that specific scene. This is for any configurations of the project during its runtime, +/// such as initial scene and stuff like that. +#[derive( + bincode::Decode, + bincode::Encode, + serde::Serialize, + serde::Deserialize, + Debug, + Clone, +)] +pub struct RuntimeSettings {} + +impl RuntimeSettings { + /// Creates a new [`RuntimeSettings`] config. + pub fn new() -> Self { + Self { } + } +} + +impl Default for RuntimeSettings { + fn default() -> Self { + Self::new() + } +} + +/// The configuration of a packaged eucalyptus project. +/// +/// Often stored as a single .eupak file, it contains all the scenes and the references of different +/// resources. #[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug)] pub struct RuntimeProjectConfig { #[bincode(with_serde)] pub project_name: String, - #[bincode(with_serde)] - pub scene_map: RuntimeSceneIndex, -} -#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug)] -pub struct RuntimeSceneIndex { + // authoring stuff needs to be added, maybe later + + // versioning stuff too + + #[bincode(with_serde)] + pub runtime_settings: RuntimeSettings, + #[bincode(with_serde)] - pub scene_index: HashMap<String, SceneConfig>, + pub scenes: Vec<SceneConfig>, } + +impl RuntimeProjectConfig { + /// Creates a [RuntimeProjectConfig] from a loaded [PROJECT] and [SCENES] states. + pub fn from_memory() -> Self { + let project = PROJECT.read(); + let scenes = SCENES.read(); + + Self { + project_name: project.project_name.clone(), + runtime_settings: project.runtime_settings.clone(), + scenes: scenes.to_vec(), + } + } +} @@ -11,7 +11,7 @@ use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light as EngineLight, LightComponent}; use dropbear_engine::model::Model; -use dropbear_engine::utils::ResourceReferenceType; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use dropbear_traits::SerializableComponent; use dropbear_traits::registry::ComponentRegistry; use glam::{DQuat, DVec3}; @@ -60,15 +60,19 @@ impl SceneEntity { } } +/// The specific settings of a scene. #[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct SceneSettings {/* *crickets* */} impl SceneSettings { + /// Creates a new [`SceneSettings`] config. pub fn new() -> Self { Self {} } } +/// Specifies the configuration of a scene, such as its entities, hierarchies and any settings that +/// may be necessary. #[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct SceneConfig { #[serde(default)] @@ -127,16 +131,34 @@ impl SceneConfig { return Ok(()); } ResourceReferenceType::File(reference) => { - let path = &renderer.handle.resolve()?; + if reference == "euca://internal/dropbear/models/cube" { + log::info!("Loading entity from internal cube reference"); + let mut loaded_model = Model::load_from_memory( + graphics.clone(), + include_bytes!("../../resources/models/cube.glb"), + Some(label), + ) + .await?; + + let model = loaded_model.make_mut(); + model.path = + ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + + loaded_model.refresh_registry(); + + MeshRenderer::from_handle(loaded_model) + } else { + let path = &renderer.handle.resolve()?; - log::debug!( - "Path for entity {} is {} from reference {}", - label, - path.display(), - reference - ); + log::debug!( + "Path for entity {} is {} from reference {}", + label, + path.display(), + reference + ); - MeshRenderer::from_path(graphics.clone(), &path, Some(label)).await? + MeshRenderer::from_path(graphics.clone(), &path, Some(label)).await? + } } ResourceReferenceType::Bytes(bytes) => { log::info!("Loading entity from bytes [Len: {}]", bytes.len()); @@ -149,13 +171,19 @@ impl SceneConfig { ResourceReferenceType::Cube => { log::info!("Loading entity from cube"); - let model = Model::load_from_memory( + let mut loaded_model = Model::load_from_memory( graphics.clone(), include_bytes!("../../resources/models/cube.glb"), Some(label), ) .await?; - MeshRenderer::from_handle(model) + + let model = loaded_model.make_mut(); + model.path = ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + + loaded_model.refresh_registry(); + + MeshRenderer::from_handle(loaded_model) } }; @@ -303,6 +303,7 @@ fn get_gradle_command(project_root: impl AsRef<Path>) -> String { } } +/// Asynchronously builds a project for the JVM using gradle. pub async fn build_jvm( project_root: impl AsRef<Path>, status_sender: Sender<BuildStatus>, @@ -420,3 +421,127 @@ pub async fn build_jvm( let _ = status_sender.send(BuildStatus::Completed); Ok(jar_path) } + +/// Asynchronously builds a project for Kotlin/Native using gradle. +pub async fn build_native( + project_root: impl AsRef<Path>, + status_sender: Sender<BuildStatus>, +) -> anyhow::Result<PathBuf> { + let project_root = project_root.as_ref(); + + if !project_root.exists() { + let err = format!("Project root does not exist: {:?}", project_root); + let _ = status_sender.send(BuildStatus::Failed(err.clone())); + return Err(anyhow::anyhow!(err)); + } + + let _ = status_sender.send(BuildStatus::Building("Copying core library...".to_string())); + let libs_dir = project_root.join("libs"); + if !libs_dir.exists() { + std::fs::create_dir_all(&libs_dir).context("Failed to create libs directory")?; + } + + let (lib_name, lib_ext) = if cfg!(target_os = "windows") { + ("eucalyptus_core", "dll") + } else if cfg!(target_os = "macos") { + ("libeucalyptus_core", "dylib") + } else { + ("libeucalyptus_core", "so") + }; + + let lib_filename = format!("{}.{}", lib_name, lib_ext); + + let current_exe = std::env::current_exe().context("Failed to get current executable path")?; + let exe_dir = current_exe.parent().context("Failed to get executable directory")?; + let source_lib_path = exe_dir.join(&lib_filename); + + if source_lib_path.exists() { + std::fs::copy(&source_lib_path, libs_dir.join(&lib_filename)) + .context(format!("Failed to copy {} to libs", lib_filename))?; + } else { + let cwd_lib_path = std::env::current_dir()?.join(&lib_filename); + if cwd_lib_path.exists() { + std::fs::copy(&cwd_lib_path, libs_dir.join(&lib_filename)) + .context(format!("Failed to copy {} to libs", lib_filename))?; + } else { + let err = format!("Could not find core library {} to copy", lib_filename); + let _ = status_sender.send(BuildStatus::Failed(err.clone())); + return Err(anyhow::anyhow!(err)); + } + } + + let _ = status_sender.send(BuildStatus::Started); + + let gradle_cmd = get_gradle_command(project_root); + let _ = status_sender.send(BuildStatus::Building(format!("Running: {} build", gradle_cmd))); + + let mut child = Command::new(&gradle_cmd) + .current_dir(project_root) + .args(["--console=plain", "build"]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .context(format!("Failed to spawn `{} build`", gradle_cmd))?; + + let stdout = child.stdout.take().expect("Stdout was piped"); + let stderr = child.stderr.take().expect("Stderr was piped"); + + let tx_out = status_sender.clone(); + let stdout_task = tokio::spawn(async move { + let mut reader = BufReader::new(stdout).lines(); + while let Ok(Some(line)) = reader.next_line().await { + let _ = tx_out.send(BuildStatus::Building(line)); + } + }); + + let tx_err = status_sender.clone(); + let stderr_task = tokio::spawn(async move { + let mut reader = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = reader.next_line().await { + let _ = tx_err.send(BuildStatus::Building(line)); + } + }); + + let status = child + .wait() + .await + .context("Failed to wait for gradle process")?; + + let _ = tokio::join!(stdout_task, stderr_task); + + if !status.success() { + let code = status.code().unwrap_or(-1); + let err_msg = format!("Gradle build failed with exit code {}", code); + let _ = status_sender.send(BuildStatus::Failed(err_msg.clone())); + return Err(anyhow::anyhow!(err_msg)); + } + + let output_dir = project_root.join("build/bin/nativeLib/releaseShared"); + if !output_dir.exists() { + let err = format!("Build succeeded but output directory missing: {:?}", output_dir); + let _ = status_sender.send(BuildStatus::Failed(err.clone())); + return Err(anyhow::anyhow!(err)); + } + + let mut found_lib = None; + if let Ok(entries) = std::fs::read_dir(&output_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext == lib_ext { + found_lib = Some(path); + break; + } + } + } + } + + if let Some(lib_path) = found_lib { + let _ = status_sender.send(BuildStatus::Completed); + Ok(lib_path) + } else { + let err = format!("No .{} file found in {:?}", lib_ext, output_dir); + let _ = status_sender.send(BuildStatus::Failed(err.clone())); + Err(anyhow::anyhow!(err)) + } +} @@ -18,6 +18,7 @@ use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; +/// A global "singleton" that contains the configuration of a project. pub static PROJECT: Lazy<RwLock<ProjectConfig>> = Lazy::new(|| RwLock::new(ProjectConfig::default())); @@ -46,8 +46,7 @@ env_logger.workspace = true colored.workspace = true chrono.workspace = true egui_ltreeview.workspace = true - -[target.'cfg(not(target_os = "android"))'.dependencies] +ron.workspace = true rfd.workspace = true [features] @@ -1,521 +1,101 @@ -use clap::ArgMatches; use eucalyptus_core::config::ProjectConfig; -use eucalyptus_core::states::{RuntimeData, SCENES, SOURCE}; -use std::path::Path; -use std::{collections::HashMap, fs, path::PathBuf, process::Command}; -use zip::write::SimpleFileOptions; - -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 mut cargo_build = Command::new("cargo") - .args(["build", "--release"]) - .current_dir(&runtime_dir) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()) - .spawn()?; - - let exit_status = cargo_build.wait()?; - - if !exit_status.success() { - return Err(anyhow::anyhow!("Failed to build {}", project_name)); - } - 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())?; - - 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 +use eucalyptus_core::runtime::RuntimeProjectConfig; +use eucalyptus_core::scene::SceneConfig; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Builds a eucalyptus project into a single bundle. +/// +/// Returns the path of the build directory +pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> { + log::info!("Started project building"); + // create a build directory + let project_root = project_config .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(()) -} - -fn clone_repository(build_dir: impl AsRef<Path>) -> anyhow::Result<()> { - git2::build::RepoBuilder::new().clone( - "https://github.com/4tkbytes/redback-runtime", - &build_dir.as_ref().join("redback-runtime"), - )?; - println!("Repository cloned successfully!"); - Ok(()) -} - -fn should_update_repository(repo_dir: impl AsRef<Path>) -> 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) -} - -fn copy_resources_folder(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> anyhow::Result<()> { - fs::create_dir_all(dest.as_ref())?; - - 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.as_ref().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: impl AsRef<Path>) -> 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.as_ref().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.as_ref().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: impl AsRef<Path>, - zip_path: impl AsRef<Path>, -) -> anyhow::Result<()> { - let file = fs::File::create(zip_path.as_ref())?; - let mut zip = zip::ZipWriter::new(file); - - let walkdir = walkdir::WalkDir::new(source_dir.as_ref()); - for entry in walkdir { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - let relative_path = path.strip_prefix(source_dir.as_ref())?; - let name = relative_path.to_string_lossy(); + .ok_or(anyhow::anyhow!("Unable to locate parent folder of config"))? + .to_path_buf(); + let build_dir = project_root.join("build/output"); - let options: SimpleFileOptions = Default::default(); - zip.start_file(name, options)?; - let mut file = std::fs::File::open(path)?; - std::io::copy(&mut file, &mut zip)?; - } - } - - zip.finish()?; - println!("Created zip package: {}", zip_path.as_ref().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<PathBuf> { - println!(" > Starting build"); - if !project_path.exists() { - return Err(anyhow::anyhow!(format!( - "Unable to locate project config file: [{}]", - project_path.display() - ))); + if build_dir.exists() { + fs::remove_dir_all(&build_dir)?; } - // ProjectConfig::read_from(&project_path)?.load_config_to_memory()?; - - let mut project_config = ProjectConfig::read_from(&project_path)?; - log::info!(" > Reading from project config"); - project_config.load_config_to_memory()?; - log::info!(" > Loading config to memory"); - - let scene_data = { - let scenes_guard = SCENES.read(); - scenes_guard.clone() - }; - log::info!(" > Copied scene data"); - - let source_config = { - let source_guard = SOURCE.read(); - source_guard.clone() - }; - log::info!(" > Captured source config"); - - let build_dir = project_path.parent().unwrap().join("build").join("output"); fs::create_dir_all(&build_dir)?; - log::info!(" > Created 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)? { + log::debug!("Readied build directory"); + + // load the project config manually to avoid overwriting global state + let ron_str = fs::read_to_string(&project_config)?; + let mut config: ProjectConfig = ron::de::from_str(&ron_str)?; + config.project_path = project_root.clone(); + log::debug!("Loaded project config"); + + // load scenes + let mut scenes = Vec::new(); + let scene_folder = project_root.join("scenes"); + if scene_folder.exists() { + for entry in fs::read_dir(scene_folder)? { let entry = entry?; let path = entry.path(); - if let Some(ext) = path.extension() - && ext == "rhai" - { - let name = path.file_name().unwrap().to_string_lossy().to_string(); - let contents = fs::read_to_string(&path)?; - println!(" > Copied script info from [{}]", name); - scripts.insert(name, contents); - } - } - } - - let runtime_data = RuntimeData { - project_config, - source_config, - scene_data, - scripts, - }; - log::info!(" > Created runtime data structures"); - - 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)?; - log::info!(" > Written the file to build location"); - - println!( - "Build completed successfully. Output at {:?}", - runtime_file.display() - ); - Ok(runtime_file) -} - -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; - } + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("eucs") { + match SceneConfig::read_from(&path) { + Ok(scene) => { + scenes.push(scene); } - Err(_) => { - println!("Does rustc compiler exist? ✗ NO - rustc not found in PATH"); - all_healthy = false; + Err(e) => { + log::warn!("Failed to load scene {:?} during build: {}", path, e); } } - } 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; - } - } + // convert to runtime project config + let runtime_config = RuntimeProjectConfig { + project_name: config.project_name.clone(), + runtime_settings: config.runtime_settings.clone(), + scenes, + }; + log::debug!("Converted to runtime project config"); - if let Ok(output) = Command::new("where").arg("assimp.dll").output() - && output.status.success() - { - return true; - } + // export to .eupak + let eupak_path = build_dir.join("data.eupak"); + let config_bytes = bincode::encode_to_vec(&runtime_config, bincode::config::standard())?; + fs::write(&eupak_path, config_bytes)?; + log::debug!("Exported scene config to {:?}", eupak_path); + + // copy resources + let resources_src = project_root.join("resources"); + let resources_dst = build_dir.join("resources"); + if resources_src.exists() { + copy_dir_recursive(&resources_src, &resources_dst)?; + log::debug!("Copied resources to {:?}", resources_dst); } - #[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", - ]; + log::info!("Done!"); - 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() - && output.status.success() - { - return true; - } + Ok(build_dir) +} - #[cfg(target_os = "linux")] - { - if let Ok(output) = Command::new("ldconfig").args(["-p"]).output() - && output.status.success() - { - let output_str = String::from_utf8_lossy(&output.stdout); - if output_str.contains("libassimp") { - return true; - } - } +fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_recursive(&entry.path(), &dst.join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.join(entry.file_name()))?; } } - - false + Ok(()) } + +/// Reads the contents of a data.eupak file into a pretty print format. +/// +/// Returns the contents of the project config. +pub fn read(eupak: PathBuf) -> anyhow::Result<RuntimeProjectConfig> { + let bytes = std::fs::read(&eupak)?; + let (content, _): (RuntimeProjectConfig, usize) = + bincode::decode_from_slice(&bytes, bincode::config::standard())?; + println!("{} contents: {:#?}", eupak.display(), content); + Ok(content) +} @@ -760,8 +760,16 @@ impl InspectableComponent for MeshRenderer { CollapsingHeader::new("Model").show(ui, |ui| { let mut selected_model: Option<AssetHandle> = None; + let selected_text = if let Some(uri) = self.handle().path.as_uri() + && uri == "euca://internal/dropbear/models/cube" + { + "Cube".to_string() + } else { + self.handle().label.clone() + }; + ComboBox::from_id_salt("model_dropdown") - .selected_text(format!("{}", self.handle().label)) + .selected_text(selected_text) .width(ui.available_width()) .show_ui(ui, |ui| { let iter = ASSET_REGISTRY.iter_model(); @@ -989,10 +989,31 @@ impl<'a> EditorTabViewer<'a> { Self::build_resource_branch(builder, &project_root); Self::build_scripts_branch(builder, &project_root); Self::build_scene_branch(builder, &project_root); + Self::build_internal_models_branch(builder); builder.close_dir(); }); } + fn build_internal_models_branch(builder: &mut TreeViewBuilder<u64>) { + let label = "euca://internal"; + builder.node(Self::dir_node_labeled(label, "internal")); + + let dropbear_label = "euca://internal/dropbear"; + builder.node(Self::dir_node_labeled(dropbear_label, "dropbear")); + + let models_label = "euca://internal/dropbear/models"; + builder.node(Self::dir_node_labeled(models_label, "models")); + + for model_name in dropbear_engine::utils::INTERNAL_MODELS { + let model_uri = format!("euca://internal/dropbear/models/{}", model_name); + builder.node(Self::leaf_node_labeled(&model_uri, model_name)); + } + + builder.close_dir(); // close models + builder.close_dir(); // close dropbear + builder.close_dir(); // close internal + } + fn build_resource_branch(builder: &mut TreeViewBuilder<u64>, project_root: &Path) { let label = "euca://resources"; builder.node(Self::dir_node_labeled(label, "resources")); @@ -115,34 +115,15 @@ async fn main() -> anyhow::Result<()> { ), ) .subcommand( - Command::new("package") - .about("Package a eucalyptus project, which compiles the runtime and the resource .eupak file") + Command::new("read") + .about("Reads a .eupak file") .arg( - Arg::new("project") - .help("Path to the .eucp project file") - .value_name("PROJECT_FILE") - .required(true), - ), - ) - .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(true), + .help("Path to the .eupak data file") + .value_name("EUPAK_FILE") + .required(true) ), ) - .subcommand(Command::new("health").about("Check the health of the eucalyptus installation")) - .subcommand(Command::new("compile") - .about("Compiles a project's script into WebAssembly, primarily used for testing") - .arg( - Arg::new("project") - .help("Path to the .eucp project file") - .value_name("PROJECT_FILE") - .required(true), - ) - ) .get_matches(); match matches.subcommand() { @@ -152,7 +133,7 @@ async fn main() -> anyhow::Result<()> { None => match find_eucp_file() { Ok(path) => path, Err(e) => { - eprintln!("Error: {}", e); + log::error!("Error: {}", e); std::process::exit(1); } }, @@ -160,51 +141,13 @@ async fn main() -> anyhow::Result<()> { build::build(project_path)?; } - Some(("package", sub_matches)) => { - let project_path = match sub_matches.get_one::<String>("project") { - Some(path) => PathBuf::from(path), - None => match find_eucp_file() { - Ok(path) => path, - Err(e) => { - eprintln!("Error: {}", e); - std::process::exit(1); - } - }, - }; - - build::package(project_path, sub_matches)?; - } - Some(("health", _)) => { - 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); - } - }, - }; - - crate::build::read_from_eupak(project_path)?; - } - Some(("compile", sub_matches)) => { - let _project_path = match sub_matches.get_one::<String>("project") { + let eupak = 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); - } - }, + None => {log::error!("Eupak file returned none"); std::process::exit(1)}, }; - println!("\"Compile\" command not implemented yet"); - // crate::build::compile(project_path).await?; + build::read(eupak)?; } None => { let config = WindowConfiguration { @@ -625,12 +625,22 @@ impl SignalController for Editor { if component_name == "MeshRenderer" { let graphics_clone = graphics.clone(); let future = async move { - let model= dropbear_engine::model::Model::load_from_memory( + let mut loaded_model = dropbear_engine::model::Model::load_from_memory( graphics_clone.clone(), include_bytes!("../../resources/models/cube.glb"), Some("Cube"), - ).await?; - Ok::<MeshRenderer, anyhow::Error>(dropbear_engine::entity::MeshRenderer::from_handle(model)) + ) + .await?; + + let model = loaded_model.make_mut(); + model.path = + ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + + loaded_model.refresh_registry(); + + Ok::<MeshRenderer, anyhow::Error>( + dropbear_engine::entity::MeshRenderer::from_handle(loaded_model), + ) }; let handle = graphics.future_queue.push(Box::pin(future)); self.pending_components.push((*entity, handle)); @@ -680,21 +690,42 @@ impl SignalController for Editor { let graphics_clone = graphics.clone(); let uri_clone = uri.clone(); let future = async move { - let path = if uri_clone.starts_with("euca://") { - let path_str = uri_clone.trim_start_matches("euca://"); - let project_path = PROJECT.read().project_path.clone(); - project_path.join(path_str) + if uri_clone == "euca://internal/dropbear/models/cube" { + let mut loaded_model = dropbear_engine::model::Model::load_from_memory( + graphics_clone, + include_bytes!("../../resources/models/cube.glb"), + Some("Cube"), + ) + .await?; + + let model = loaded_model.make_mut(); + model.path = ResourceReference::from_euca_uri(&uri_clone)?; + + loaded_model.refresh_registry(); + + Ok::<MeshRenderer, anyhow::Error>( + dropbear_engine::entity::MeshRenderer::from_handle(loaded_model), + ) } else { - PathBuf::from(&uri_clone) - }; - - let model = dropbear_engine::model::Model::load( - graphics_clone, - &path, - Some(&uri_clone) - ).await?; - - Ok::<MeshRenderer, anyhow::Error>(dropbear_engine::entity::MeshRenderer::from_handle(model)) + let path = if uri_clone.starts_with("euca://") { + let path_str = uri_clone.trim_start_matches("euca://"); + let project_path = PROJECT.read().project_path.clone(); + project_path.join(path_str) + } else { + PathBuf::from(&uri_clone) + }; + + let model = dropbear_engine::model::Model::load( + graphics_clone, + &path, + Some(&uri_clone), + ) + .await?; + + Ok::<MeshRenderer, anyhow::Error>( + dropbear_engine::entity::MeshRenderer::from_handle(model), + ) + } }; let handle = graphics.future_queue.push(Box::pin(future)); @@ -6,7 +6,7 @@ use dropbear_engine::future::FutureQueue; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::model::Model; -use dropbear_engine::utils::ResourceReferenceType; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::scene::SceneEntity; pub(crate) use eucalyptus_core::spawn::{PENDING_SPAWNS, PendingSpawnController}; @@ -218,9 +218,25 @@ async fn load_renderer_from_serialized( "Renderer for '{}' does not specify an asset reference", label ), - ResourceReferenceType::File(_) => { - let path = renderer.handle.resolve()?; - MeshRenderer::from_path(graphics.clone(), &path, Some(&label)).await? + ResourceReferenceType::File(reference) => { + if reference == "euca://internal/dropbear/models/cube" { + let mut loaded_model = Model::load_from_memory( + graphics.clone(), + include_bytes!("../../resources/models/cube.glb"), + Some(&label), + ) + .await?; + + let model = loaded_model.make_mut(); + model.path = ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + + loaded_model.refresh_registry(); + + MeshRenderer::from_handle(loaded_model) + } else { + let path = renderer.handle.resolve()?; + MeshRenderer::from_path(graphics.clone(), &path, Some(&label)).await? + } } ResourceReferenceType::Bytes(bytes) => { let model = @@ -231,13 +247,19 @@ async fn load_renderer_from_serialized( anyhow::bail!("Procedural planes are not supported in pending spawns yet"); } ResourceReferenceType::Cube => { - let model = Model::load_from_memory( + let mut loaded_model = Model::load_from_memory( graphics.clone(), include_bytes!("../../resources/models/cube.glb"), Some(&label), ) .await?; - MeshRenderer::from_handle(model) + + let model = loaded_model.make_mut(); + model.path = ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + + loaded_model.refresh_registry(); + + MeshRenderer::from_handle(loaded_model) } }; @@ -1,2 +1,4 @@ #[tokio::main] -async fn main() {} +async fn main() { + +} @@ -15,4 +15,5 @@ buildCache { local { isEnabled = true } -} +} +include("dropbear-gradle-plugin")