tirbofish/dropbear · commit
3182754efc3c7cef4c48345b966ad0712c35faf9
new package command, redback-runtime scripting works now, other features as well
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -1,2 +1,2 @@ [target.x86_64-pc-windows-msvc] -rustflags = ["-C", "target-feature=+crt-static", "-C", "prefer-dynamic=no"] +rustflags = ["-C", "target-feature=+crt-static", "-C", "prefer-dynamic"] @@ -25,7 +25,7 @@ arboard = "3.6" bincode = { version = "2.0", features = ["serde"] } bytemuck = { version = "1.23", features = ["derive"] } chrono = "0.4" -clap = "4.5" +clap = { version = "4.5", features = ["derive"] } colored = "3.0" crossbeam-channel = "0.5" dropbear-engine = { path = "dropbear-engine" } @@ -40,7 +40,7 @@ impl CameraSettings { impl Default for CameraSettings { fn default() -> Self { - Self::new(1.0, 0.1, 45.0) + Self::new(1.0, 0.002, 45.0) } } @@ -105,7 +105,7 @@ impl SceneConfig { /// Helper function to load a component and add it to the entity builder async fn load_component( - component: Box<dyn dropbear_traits::SerializableComponent>, + component: Box<dyn SerializableComponent>, builder: &mut hecs::EntityBuilder, graphics: Arc<SharedGraphicsContext>, registry: Option<&ComponentRegistry>, @@ -15,6 +15,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; +use magna_carta::Target; /// The target of the script. This can be either a JVM or a native library. #[derive(Default, Clone)] @@ -323,6 +324,15 @@ pub async fn build_jvm( return Err(anyhow::anyhow!(err)); } + let _ = status_sender.send(BuildStatus::Started); + + let _ = status_sender.send(BuildStatus::Building(format!("Building manifest at {}", project_root.join("build/magna-carta/jvmMain/RunnableRegistry.kt").display()))); + if let Err(e) = magna_carta::parse(project_root.join("src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) { + let _ = status_sender.send(BuildStatus::Failed(format!("Failed to build manifest: {}", e))); + return Err(e); + } + let _ = status_sender.send(BuildStatus::Building(String::from("Successfully built manifest!"))); + if !(project_root.join("build.gradle").exists() || project_root.join("build.gradle.kts").exists()) { @@ -331,8 +341,6 @@ pub async fn build_jvm( 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: {}", gradle_cmd))); @@ -442,6 +450,8 @@ pub async fn build_native( return Err(anyhow::anyhow!(err)); } + let _ = status_sender.send(BuildStatus::Started); + let _ = status_sender.send(BuildStatus::Building("Copying core library...".to_string())); let libs_dir = project_root.join("libs"); if !libs_dir.exists() { @@ -479,7 +489,12 @@ pub async fn build_native( } } - let _ = status_sender.send(BuildStatus::Started); + let _ = status_sender.send(BuildStatus::Building(format!("Building manifest at {}", project_root.join("build/magna-carta/jvmMain/RunnableRegistry.kt").display()))); + if let Err(e) = magna_carta::parse(project_root.join("src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) { + let _ = status_sender.send(BuildStatus::Failed(format!("Failed to build manifest: {}", e))); + return Err(e); + } + let _ = status_sender.send(BuildStatus::Building(String::from("Successfully built manifest!"))); let gradle_cmd = get_gradle_command(project_root); let _ = status_sender.send(BuildStatus::Building(format!( @@ -164,16 +164,17 @@ pub fn Java_com_dropbear_ffi_JNINative_getTransform( return match env.new_object( &entity_transform_class, - "(Lcom/dropbear/math/Transform;Lcom/dropbear/math/Transform)V", + "(Lcom/dropbear/math/Transform;Lcom/dropbear/math/Transform;)V", &[ JValue::Object(&local_transform_java), JValue::Object(&world_transform_java), ], ) { Ok(java_transform) => java_transform, - Err(_) => { + Err(e) => { println!( - "[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create Transform object" + "[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create Transform object: {}", + e ); JObject::null() } @@ -10,6 +10,7 @@ use crate::scripting::error::LastErrorMessage; use crate::scripting::native::sig::{ DestroyAll, DestroyTagged, Init, LoadTagged, UpdateAll, UpdateTagged, }; +use anyhow::anyhow; use libloading::{Library, Symbol}; use std::ffi::CString; use std::path::Path; @@ -36,8 +37,15 @@ impl NativeLibrary { /// Creates a new instance of [`NativeLibrary`] pub fn new(lib_path: impl AsRef<Path>) -> anyhow::Result<Self> { let lib_path = lib_path.as_ref(); + if !lib_path.exists() { + anyhow::bail!( + "Native script library missing at '{}'. Expected this file to be copied next to the runtime executable or inside its 'libs' directory.", + lib_path.display() + ); + } unsafe { - let library: Library = Library::new(lib_path)?; + let library: Library = Library::new(lib_path) + .map_err(|err| enhance_library_error(lib_path, err))?; let init_fn = load_symbol(&library, &[b"dropbear_init\0"], "dropbear_init")?; let load_systems_fn = load_symbol( @@ -152,6 +160,26 @@ impl NativeLibrary { } } +fn enhance_library_error(path: &Path, err: libloading::Error) -> anyhow::Error { + #[cfg(windows)] + { + let err_str = err.to_string(); + if err_str.contains("os error 126") { + return anyhow!( + "Failed to load native script library '{}': {}. Windows error 126 means a dependent DLL is missing—copy every *.dll (and matching *.dll.lib) produced by your Gradle native build next to the runtime or into its 'libs' folder.", + path.display(), + err + ); + } + } + + anyhow!( + "Failed to load native script library '{}': {}", + path.display(), + err + ) +} + fn load_symbol<T>( library: &Library, candidates: &[&[u8]], @@ -1,6 +1,6 @@ use crate::states::Node; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use winit::keyboard::KeyCode; pub const PROTO_TEXTURE: &[u8] = include_bytes!("../../resources/textures/proto.png"); @@ -261,25 +261,21 @@ impl ResolveReference for ResourceReference { #[cfg(feature = "editor")] { - let project_config = { + let project_path = { use crate::states::PROJECT; let cfg = PROJECT.read(); cfg.project_path.clone() }; - let path = project_config.join("resources").join(relative); - return Ok(path); + if !project_path.as_os_str().is_empty() { + let root = project_path.join("resources"); + return resolve_resource_from_root(relative, &root); + } } - #[cfg(not(feature = "editor"))] - { - let current_exe = std::env::current_exe()?; - let dir = current_exe - .parent() - .ok_or_else(|| anyhow::anyhow!("Unable to get path"))?; - return Ok(dir.join("resources").join(relative)); - } + let root = runtime_resources_dir()?; + return resolve_resource_from_root(relative, &root); } _ => { anyhow::bail!("Cannot resolve any other ResourceReferenceType that is not File") @@ -288,6 +284,44 @@ impl ResolveReference for ResourceReference { } } +fn runtime_resources_dir() -> anyhow::Result<PathBuf> { + let current_exe = std::env::current_exe()?; + let dir = current_exe + .parent() + .ok_or_else(|| anyhow::anyhow!("Unable to locate parent folder of runtime executable"))?; + let resources_dir = dir.join("resources"); + if !resources_dir.exists() { + anyhow::bail!( + "Runtime resources directory is missing at '{}'. Ensure the packaged build includes a 'resources' folder next to the executable (current exe: {}).", + resources_dir.display(), + current_exe.display() + ); + } + Ok(resources_dir) +} + +fn resolve_resource_from_root(relative: &str, root: &Path) -> anyhow::Result<PathBuf> { + let resolved = root.join(relative); + if resolved.exists() { + return Ok(resolved); + } + + if !root.exists() { + anyhow::bail!( + "Resource '{}' could not be resolved because the base directory '{}' does not exist", + relative, + root.display() + ); + } + + anyhow::bail!( + "Resource '{}' was resolved to '{}' but the file does not exist. Ensure the asset is packaged under '{}'", + relative, + resolved.display(), + root.display() + ); +} + /// Validates and converts a raw pointer to a reference. /// Returns early if the pointer is null. /// @@ -1,9 +1,15 @@ +use anyhow::{bail, Context}; +use app_dirs2::{app_root, AppDataType}; +use crossbeam_channel::Sender; use eucalyptus_core::config::ProjectConfig; use eucalyptus_core::runtime::RuntimeProjectConfig; use eucalyptus_core::scene::SceneConfig; +use eucalyptus_core::APP_INFO; use semver::Version; use std::fs; use std::path::{Path, PathBuf}; +use std::time::UNIX_EPOCH; +use tokio::{fs as tokio_fs, process::Command, task}; /// Builds a eucalyptus project into a single bundle. /// @@ -110,3 +116,494 @@ pub fn read(eupak: PathBuf) -> anyhow::Result<RuntimeProjectConfig> { println!("{} contents: {:#?}", eupak.display(), content); Ok(content) } + +#[derive(Debug, Clone)] +pub enum PackageStatus { + Info(String), + Progress { step: &'static str, detail: String }, + Error(String), +} + +fn emit_status(tx: &Option<Sender<PackageStatus>>, status: PackageStatus) { + if let Some(sender) = tx { + let _ = sender.send(status); + } +} + +pub async fn package( + project_config: PathBuf, + status_tx: Option<Sender<PackageStatus>>, +) -> anyhow::Result<PathBuf> { + if !project_config.exists() { + bail!("Project config not found: {}", project_config.display()); + } + + emit_status( + &status_tx, + PackageStatus::Info(format!("Packaging project at {}", project_config.display())), + ); + + let project_root = project_config + .parent() + .ok_or_else(|| anyhow::anyhow!("Unable to locate parent folder of config"))? + .to_path_buf(); + + let project_contents = tokio_fs::read_to_string(&project_config).await?; + let project_data: ProjectConfig = ron::de::from_str(&project_contents)?; + let project_name = sanitize_filename(&project_data.project_name); + + let templates_dir = app_root(AppDataType::UserData, &APP_INFO)?.join("templates"); + tokio_fs::create_dir_all(&templates_dir).await?; + + emit_status( + &status_tx, + PackageStatus::Progress { + step: "Locating runtime", + detail: format!("Searching for runtime executable in {}", templates_dir.display()), + }, + ); + + let runtime_source = { + let dir = templates_dir.clone(); + task::spawn_blocking(move || locate_runtime_binary(&dir)).await?? + }; + + let package_dir = project_root.join("build/package"); + if package_dir.exists() { + tokio_fs::remove_dir_all(&package_dir).await?; + } + tokio_fs::create_dir_all(&package_dir).await?; + + let runtime_filename = runtime_source + .file_name() + .ok_or_else(|| anyhow::anyhow!("Runtime template missing filename"))?; + let runtime_dest = package_dir.join(runtime_filename); + tokio_fs::copy(&runtime_source, &runtime_dest).await?; + emit_status( + &status_tx, + PackageStatus::Info(format!( + "Copied runtime template to {}", + runtime_dest.display() + )), + ); + + emit_status( + &status_tx, + PackageStatus::Progress { + step: "Build project", + detail: "Building runtime configuration".to_string(), + }, + ); + + let build_dir = { + let config_path = project_config.clone(); + task::spawn_blocking(move || build(config_path)).await?? + }; + + let data_src = build_dir.join("data.eupak"); + if !data_src.exists() { + bail!("Expected {} to exist", data_src.display()); + } + tokio_fs::copy(&data_src, package_dir.join("data.eupak")).await?; + + let resources_src = build_dir.join("resources"); + let resources_dst = package_dir.join("resources"); + if resources_src.exists() && !resources_dst.exists() { + let src = resources_src.clone(); + let dst = resources_dst.clone(); + task::spawn_blocking(move || copy_dir_recursive(&src, &dst)).await??; + } + + emit_status( + &status_tx, + PackageStatus::Progress { + step: "magna-carta", + detail: "Generating script bindings via magna-carta".to_string(), + }, + ); + + let magna_output_dir = project_root.join("build/magna-carta/nativeLibMain"); + tokio_fs::create_dir_all(&magna_output_dir).await?; + + let magna_status = Command::new("magna-carta") + .arg("--input") + .arg(project_root.join("src")) + .arg("--target") + .arg("native") + .arg("--output") + .arg(&magna_output_dir) + .current_dir(&project_root) + .status() + .await + .context("Failed to execute magna-carta")?; + + if !magna_status.success() { + bail!("magna-carta failed with status {magna_status:?}"); + } + + emit_status( + &status_tx, + PackageStatus::Progress { + step: "Gradle", + detail: "Running Gradle build".to_string(), + }, + ); + run_gradle_build(&project_root).await?; + log::info!("Gradle build completed successfully"); + + emit_status( + &status_tx, + PackageStatus::Progress { + step: "NativeLibrary", + detail: "Copying native library artifact".to_string(), + }, + ); + log::info!("Copying native library artifact"); + + let release_dir = project_root.join("build/bin/nativeLib/releaseShared"); + let extension = native_library_extension(); + let library_source = { + let dir = release_dir.clone(); + let ext = extension.to_string(); + task::spawn_blocking(move || pick_latest_library(&dir, &ext)).await?? + }; + + let library_dest = package_dir.join(format!("{}.{}", project_name, extension)); + tokio_fs::copy(&library_source, &library_dest).await?; + + emit_status( + &status_tx, + PackageStatus::Progress { + step: "RuntimeLibs", + detail: "Copying runtime dependencies".to_string(), + }, + ); + + let libs_dir = project_root.join("libs"); + tokio_fs::create_dir_all(&libs_dir).await?; + + let discovered_libs = task::spawn_blocking(discover_runtime_libraries).await??; + + #[cfg(windows)] + let (shared_objects, import_libs) = { + let DiscoveredLibraries { + shared_objects, + import_libs, + } = discovered_libs; + (shared_objects, import_libs) + }; + + #[cfg(not(windows))] + let shared_objects = { + let DiscoveredLibraries { shared_objects, .. } = discovered_libs; + shared_objects + }; + + for lib in shared_objects { + let file_name = lib + .file_name() + .ok_or_else(|| anyhow::anyhow!("Runtime dependency missing filename: {}", lib.display()))? + .to_owned(); + let libs_target = libs_dir.join(&file_name); + if !format!("{}", file_name.display()).contains("eucalyptus_core") { continue; } + tokio_fs::copy(&lib, &libs_target).await?; + let package_target = package_dir.join(&file_name); + tokio_fs::copy(&lib, &package_target).await?; + log::info!( + "Copied runtime dependency {} to {} and {}", + lib.display(), + libs_target.display(), + package_target.display() + ); + } + + #[cfg(windows)] + { + for import_lib in import_libs { + let file_name = import_lib + .file_name() + .ok_or_else(|| anyhow::anyhow!("Import library missing filename: {}", import_lib.display()))? + .to_owned(); + let libs_target = libs_dir.join(&file_name); + tokio_fs::copy(&import_lib, &libs_target).await?; + log::info!( + "Copied Windows import library {} to {}", + import_lib.display(), + libs_target.display() + ); + } + } + + emit_status( + &status_tx, + PackageStatus::Info(format!( + "Packaged build available at {}", + package_dir.display() + )), + ); + + log::info!("Packaged build available at {}", package_dir.display()); + + Ok(package_dir) +} + +fn locate_runtime_binary(templates_dir: &Path) -> anyhow::Result<PathBuf> { + for name in runtime_name_candidates() { + let candidate = templates_dir.join(name); + if is_runtime_binary(&candidate) { + return Ok(candidate); + } + } + + for entry in fs::read_dir(templates_dir)? { + let entry = entry?; + let path = entry.path(); + if !is_runtime_binary(&path) { + continue; + } + if entry + .file_name() + .to_string_lossy() + .to_ascii_lowercase() + .contains("redback-runtime") + { + return Ok(path); + } + } + + let current_exe = std::env::current_exe()?; + let current_dir = current_exe.parent().ok_or(anyhow::anyhow!("Unable to locate parent folder of current executable"))?; + for entry in fs::read_dir(current_dir)? { + let entry = entry?; + let path = entry.path(); + if !is_runtime_binary(&path) { + continue; + } + if entry + .file_name() + .to_string_lossy() + .to_ascii_lowercase() + .contains("redback-runtime") + { + return Ok(path); + } + } + + bail!( + "Unable to locate redback runtime executable in {}", + templates_dir.display() + ) +} + +fn runtime_name_candidates() -> &'static [&'static str] { + #[cfg(windows)] + { + &["redback-runtime.exe", "redback-runtime"] + } + #[cfg(target_os = "macos")] + { + &["redback-runtime.app", "redback-runtime"] + } + #[cfg(all(not(windows), not(target_os = "macos")))] + { + &["redback-runtime"] + } +} + +fn is_runtime_binary(path: &Path) -> bool { + let Ok(metadata) = path.metadata() else { + return false; + }; + + #[cfg(windows)] + { + if !metadata.is_file() { + return false; + } + return path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("exe")) + .unwrap_or(false); + } + + #[cfg(target_os = "macos")] + { + if metadata.is_dir() { + return path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("app")) + .unwrap_or(false); + } + metadata.is_file() + } + + #[cfg(all(not(windows), not(target_os = "macos")))] + { + metadata.is_file() + } +} + +fn sanitize_filename(name: &str) -> String { + let mut result = String::new(); + for ch in name.chars() { + if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') { + result.push(ch); + } else if ch.is_whitespace() { + result.push('_'); + } + } + if result.is_empty() { + "project".to_string() + } else { + result + } +} + +fn native_library_extension() -> &'static str { + if cfg!(windows) { + "dll" + } else if cfg!(target_os = "macos") { + "dylib" + } else { + "so" + } +} + +fn pick_latest_library(dir: &Path, extension: &str) -> anyhow::Result<PathBuf> { + if !dir.exists() { + bail!("Native library directory does not exist: {}", dir.display()); + } + + let mut matches = Vec::new(); + for entry in fs::read_dir(dir)? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let path = entry.path(); + let ext_matches = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case(extension)) + .unwrap_or(false); + if !ext_matches { + continue; + } + let modified = entry + .metadata() + .ok() + .and_then(|meta| meta.modified().ok()) + .unwrap_or(UNIX_EPOCH); + matches.push((modified, path)); + } + + if matches.is_empty() { + bail!( + "Unable to locate any *.{extension} libraries in {}", + dir.display() + ); + } + + matches.sort_by_key(|(modified, _)| *modified); + let (_, path) = matches + .pop() + .expect("matches is not empty so pop must succeed"); + Ok(path) +} + +struct DiscoveredLibraries { + shared_objects: Vec<PathBuf>, + #[cfg_attr(not(windows), allow(dead_code))] + import_libs: Vec<PathBuf>, +} + +fn discover_runtime_libraries() -> anyhow::Result<DiscoveredLibraries> { + let current_exe = std::env::current_exe()?; + let current_dir = current_exe + .parent() + .ok_or_else(|| anyhow::anyhow!("Unable to locate parent folder of current executable"))? + .to_path_buf(); + + let mut libs = DiscoveredLibraries { + shared_objects: Vec::new(), + import_libs: Vec::new(), + }; + + for entry in fs::read_dir(¤t_dir)? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + + let path = entry.path(); + let extension = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()) + .unwrap_or_default(); + + if matches!(extension.as_str(), "dll" | "dylib" | "so") { + libs.shared_objects.push(path); + continue; + } + + if extension.as_str() == "lib" { + if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) { + if file_name.to_ascii_lowercase().ends_with(".dll.lib") { + libs.import_libs.push(path); + } + } + } + } + + if libs.shared_objects.is_empty() { + bail!( + "Unable to locate any runtime libraries next to {}", + current_dir.display() + ); + } + + Ok(libs) +} + +async fn run_gradle_build(project_root: &Path) -> anyhow::Result<()> { + #[cfg(windows)] + { + let script = project_root.join("gradlew.bat"); + if !script.exists() { + bail!("Gradle wrapper not found at {}", script.display()); + } + let status = Command::new("cmd") + .arg("/C") + .arg(script) + .arg("build") + .current_dir(project_root) + .status() + .await + .context("Failed to run gradlew.bat")?; + if !status.success() { + bail!("Gradle build failed (status {status:?})"); + } + } + + #[cfg(not(windows))] + { + let script = project_root.join("gradlew"); + if !script.exists() { + bail!("Gradle wrapper not found at {}", script.display()); + } + let status = Command::new(script) + .arg("build") + .current_dir(project_root) + .status() + .await + .context("Failed to run ./gradlew")?; + if !status.success() { + bail!("Gradle build failed (status {status:?})"); + } + } + + Ok(()) +} @@ -96,6 +96,13 @@ impl Keyboard for Editor { self.selected_entity = None; log::debug!("Deselected entity"); } + } else if matches!(self.editor_state, EditorState::Playing) && shift_pressed { + self.viewport_mode = ViewportMode::None; + info!("Switched to Viewport::None"); + if let Some(window) = &self.window { + window.set_cursor_visible(true); + let _ = window.set_cursor_grab(CursorGrabMode::None); + } } else if self.is_viewport_focused && !is_playing { self.viewport_mode = ViewportMode::None; info!("Switched to Viewport::None"); @@ -293,10 +300,7 @@ impl Mouse for Editor { && let Some((camera, _)) = q.get() { if let Some((dx, dy)) = delta { - camera.track_mouse_delta( - dx * camera.settings.sensitivity, - dy * camera.settings.sensitivity, - ); + camera.track_mouse_delta(dx, dy); self.input_state.mouse_delta = Some((dx, dy)); } else { log_once::warn_once!("Unable to track mouse delta, attempting fallback"); @@ -304,10 +308,7 @@ impl Mouse for Editor { if let Some(old_mouse_pos) = self.input_state.last_mouse_pos { let dx = position.x - old_mouse_pos.0; let dy = position.y - old_mouse_pos.1; - camera.track_mouse_delta( - dx * camera.settings.sensitivity, - dy * camera.settings.sensitivity, - ); + camera.track_mouse_delta(dx, dy); self.input_state.mouse_delta = Some((dx, dy)); log_once::debug_once!("Fallback mouse tracking used"); } else { @@ -1182,7 +1182,7 @@ impl Editor { mesh_renderer.sync_asset_registry(); } - if let Ok(mut transform) = self.world.get::<&mut Transform>(*entity_id) { + if let Ok(mut transform) = self.world.get::<&mut EntityTransform>(*entity_id) { *transform = *original_transform; } @@ -1232,7 +1232,7 @@ impl Editor { for (entity_id, (mesh_renderer, transform, properties)) in self .world - .query::<(&MeshRenderer, &Transform, &ModelProperties)>() + .query::<(&MeshRenderer, &EntityTransform, &ModelProperties)>() .iter() { let script = self @@ -1254,23 +1254,22 @@ impl Editor { for (entity_id, (camera, component)) in self.world.query::<(&Camera, &CameraComponent)>().iter() { - camera_data.push(( - entity_id, - camera.clone(), - component.clone(), - // follow_target.cloned(), - )); + camera_data.push((entity_id, camera.clone(), component.clone())); } - self.play_mode_backup = Some(PlayModeBackup { + let backup = PlayModeBackup { entities, camera_data, - }); + }; + + let entity_count = backup.entities.len(); + let camera_count = backup.camera_data.len(); + self.play_mode_backup = Some(backup); log::info!( "Created play mode backup with {} entities and {} cameras", - self.play_mode_backup.as_ref().unwrap().entities.len(), - self.play_mode_backup.as_ref().unwrap().camera_data.len() + entity_count, + camera_count ); Ok(()) } @@ -1582,7 +1581,7 @@ pub struct PlayModeBackup { entities: Vec<( Entity, MeshRenderer, - Transform, + EntityTransform, ModelProperties, Option<Script>, )>, @@ -1,6 +1,7 @@ // #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // note to self: when it becomes release, remember to re-add this back +use anyhow::{bail, Context}; use clap::{Arg, Command}; use dropbear_engine::future::FutureQueue; use dropbear_engine::{MutableWindowConfiguration, WindowConfiguration, scene}; @@ -8,7 +9,7 @@ use eucalyptus_core::APP_INFO; use eucalyptus_editor::{build, editor, menu}; use parking_lot::RwLock; use std::sync::Arc; -use std::{fs, path::PathBuf, rc::Rc}; +use std::{fs, path::{Path, PathBuf}, rc::Rc}; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -115,6 +116,16 @@ async fn main() -> anyhow::Result<()> { ), ) .subcommand( + Command::new("package") + .about("Package a eucalyptus project into a runnable bundle") + .arg( + Arg::new("project") + .help("Path to the project directory or .eucp file") + .value_name("PROJECT_PATH") + .required(true), + ), + ) + .subcommand( Command::new("read").about("Reads a .eupak file").arg( Arg::new("eupak_file") .help("Path to the .eupak data file") @@ -126,44 +137,15 @@ async fn main() -> anyhow::Result<()> { match matches.subcommand() { Some(("build", 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) => { - log::error!("Error: {}", e); - std::process::exit(1); - } - }, - }; - - let path = if project_path.is_dir() { - log::warn!("Path provided is a directory, checking if eucalyptus project file may be there"); - log::debug!("Locating"); - let file = fs::read_dir(&project_path)?.filter_map(|entry| { - if let Ok(entry) = entry { - if entry.file_name().to_str().unwrap().ends_with(".eucp") { - Some(entry.path()) - } else { - None - } - } else { - None - } - }) - .collect::<Vec<_>>() - .first() - .cloned() - .ok_or(anyhow::anyhow!("No .eucp file found in directory"))?; - file - } else { - project_path - }; - + let path = resolve_project_argument(sub_matches.get_one::<String>("project"))?; log::info!("Building project at {:?}", path); - build::build(path)?; } + Some(("package", sub_matches)) => { + let path = resolve_project_argument(sub_matches.get_one::<String>("project"))?; + log::info!("Packaging project at {:?}", path); + build::package(path, None).await?; + } Some(("read", sub_matches)) => { let eupak = match sub_matches.get_one::<String>("eupak_file") { Some(path) => PathBuf::from(path), @@ -226,10 +208,58 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -fn find_eucp_file() -> Result<PathBuf, String> { - let current_dir = std::env::current_dir().map_err(|_| "Failed to get current directory")?; +fn resolve_project_argument(arg: Option<&String>) -> anyhow::Result<PathBuf> { + match arg { + Some(path) => { + let provided = PathBuf::from(path); + if provided.is_dir() { + find_eucp_in_dir(&provided) + } else if provided.exists() { + Ok(provided) + } else { + bail!("Provided project path does not exist: {}", provided.display()); + } + } + None => find_eucp_file(), + } +} + +fn find_eucp_in_dir(dir: &Path) -> anyhow::Result<PathBuf> { + if !dir.exists() { + bail!("Directory does not exist: {}", dir.display()); + } + + let mut matches = Vec::new(); + for entry in fs::read_dir(dir).with_context(|| format!("Unable to read {}", dir.display()))? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + if entry + .path() + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("eucp")) + .unwrap_or(false) + { + matches.push(entry.path()); + } + } + + match matches.len() { + 0 => bail!("No .eucp file found in {}", dir.display()), + 1 => Ok(matches.remove(0)), + _ => bail!( + "Multiple .eucp files found in {}. Please specify one explicitly.", + dir.display() + ), + } +} - let entries = fs::read_dir(¤t_dir).map_err(|_| "Failed to read current directory")?; +fn find_eucp_file() -> anyhow::Result<PathBuf> { + let current_dir = std::env::current_dir().context("Failed to get current directory")?; + let entries = + fs::read_dir(¤t_dir).context("Failed to read current directory for .eucp files")?; let mut eucp_files = Vec::new(); @@ -243,11 +273,11 @@ fn find_eucp_file() -> Result<PathBuf, String> { } match eucp_files.len() { - 0 => Err("No .eucp files found in current directory".to_string()), + 0 => bail!("No .eucp files found in current directory"), 1 => Ok(eucp_files[0].clone()), - _ => Err(format!( + _ => bail!( "Multiple .eucp files found: {:#?}. Please specify which one to use.", eucp_files - )), + ), } } @@ -10,5 +10,6 @@ readme = "README.md" anyhow.workspace = true tree-sitter.workspace = true tree-sitter-kotlin.workspace = true -clap = { version = "4.0", features = ["derive"] } -chrono = "0.4" +clap.workspace = true +chrono.workspace = true +log.workspace = true @@ -1,7 +1,12 @@ pub mod generator; -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; +use clap::ValueEnum; use tree_sitter::{Parser, Query, QueryCursor}; +use crate::generator::Generator; +use crate::generator::jvm::KotlinJVMGenerator; +use crate::generator::native::KotlinNativeGenerator; /// A group of manifests. #[derive(Debug, Clone)] @@ -291,6 +296,94 @@ impl KotlinProcessor { } } +/// The target +#[derive(ValueEnum, Clone, Debug)] +pub enum Target { + Jvm, + Native, +} + +/// Walks through all the input kotlin files and generates a manifest file for the target platform +/// at the directory provided by output. +/// +/// Identically the same thing as the executable, except as a function in a Rust library instead. +/// +/// # Target Behaviours +/// - [Target::Jvm] - Stores the manifest in `{output}/RunnableRegistry.kt` +/// - [Target::Native] - Stored the manifest in `{output}/ScriptManifest.kt` +pub fn parse(input: impl AsRef<Path>, target: Target, output: impl AsRef<Path>) -> anyhow::Result<()> { + let input = input.as_ref().to_path_buf(); + let output = output.as_ref().to_path_buf(); + + let mut processor = KotlinProcessor::new()?; + let mut manifest = ScriptManifest::new(); + + if !input.exists() { + return Err(anyhow::anyhow!( + "Input directory does not exist: {:?}", + input + )); + } + + visit_kotlin_files(&input, &mut processor, &mut manifest)?; + + let generated_content = match target { + Target::Jvm => { + let generator = KotlinJVMGenerator; + generator.generate(&manifest)? + } + Target::Native => { + let generator = KotlinNativeGenerator; + generator.generate(&manifest)? + } + }; + + fs::create_dir_all(&output)?; + + let filename = match target { + Target::Jvm => "RunnableRegistry.kt", + Target::Native => "ScriptManifest.kt", + }; + let output_path = output.join(filename); + fs::write(&output_path, generated_content)?; + log::info!( + "Generated {:?} manifest at: {}", + target, + output_path.display() + ); + + log::debug!("Found {} script classes", manifest.items().len()); + Ok(()) +} + +/// Helper function that visits all kotlin files in a directory recursively and processes them with +/// the [KotlinProcesser] +pub fn visit_kotlin_files( + dir: &PathBuf, + processor: &mut KotlinProcessor, + manifest: &mut ScriptManifest, +) -> anyhow::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + visit_kotlin_files(&path, processor, manifest)?; + } else if path.extension() == Some(std::ffi::OsStr::new("kt")) { + let source_code = fs::read_to_string(&path)?; + + if let Some(item) = processor.process_file(&source_code, path.clone())? { + manifest.add_item(item); + } + } + } + } + + Ok(()) +} + + #[cfg(test)] mod tests { use super::*; @@ -1,6 +1,6 @@ -use clap::{Parser, ValueEnum}; +use clap::{Parser}; use magna_carta::generator::{Generator, jvm::KotlinJVMGenerator, native::KotlinNativeGenerator}; -use magna_carta::{KotlinProcessor, ScriptManifest}; +use magna_carta::{KotlinProcessor, ScriptManifest, Target}; use std::fs; use std::path::PathBuf; @@ -31,12 +31,6 @@ struct Cli { raw: bool, } -#[derive(ValueEnum, Clone, Debug)] -enum Target { - Jvm, - Native, -} - fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -62,7 +56,7 @@ fn main() -> anyhow::Result<()> { )); } - visit_kotlin_files(&cli.input, &mut processor, &mut manifest)?; + magna_carta::visit_kotlin_files(&cli.input, &mut processor, &mut manifest)?; let generated_content = match cli.target { Target::Jvm => { @@ -100,28 +94,3 @@ fn main() -> anyhow::Result<()> { println!("Found {} script classes", manifest.items().len()); Ok(()) } - -fn visit_kotlin_files( - dir: &PathBuf, - processor: &mut KotlinProcessor, - manifest: &mut ScriptManifest, -) -> anyhow::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - visit_kotlin_files(&path, processor, manifest)?; - } else if path.extension() == Some(std::ffi::OsStr::new("kt")) { - let source_code = fs::read_to_string(&path)?; - - if let Some(item) = processor.process_file(&source_code, path.clone())? { - manifest.add_item(item); - } - } - } - } - - Ok(()) -} @@ -202,8 +202,7 @@ impl RuntimeScene { self.on_scene_loaded(loaded, pending.name, graphics); } Err(err) => { - log::error!("Failed to load scene: {err:?}"); - self.scene_command = SceneCommand::Quit; + panic!("Failed to load scene: {err:?}"); } } } @@ -211,12 +210,11 @@ impl RuntimeScene { self.pending_scene = Some(pending); } Err(TryRecvError::Closed) => { - log::error!("Scene load task for '{}' closed unexpectedly", pending.name); let _ = graphics .shared .future_queue .exchange_owned_as::<()>(&pending.handle); - self.scene_command = SceneCommand::Quit; + panic!("Scene load task for '{}' closed unexpectedly", pending.name); } } }