tirbofish/dropbear · diff
Merge branch 'main' of github.com:4tkbytes/dropbear
Signature present but could not be verified.
Unverified
@@ -8,7 +8,6 @@ package.readme = "README.md" resolver = "3" members = [ "dropbear-engine", "dropbear-shader", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "magna-carta", "redback-runtime"] - [workspace.dependencies] anyhow = { version = "1.0", features = ["backtrace"] } app_dirs2 = "2.5" @@ -34,6 +34,57 @@ pub static SOURCE: Lazy<RwLock<SourceConfig>> = Lazy::new(|| RwLock::new(SourceC pub static SCENES: Lazy<RwLock<Vec<SceneConfig>>> = Lazy::new(|| RwLock::new(Vec::new())); +/// Removes a scene with the provided name from the in-memory scene cache. +/// Returns `true` when a scene was removed and `false` when no matching scene existed. +pub fn unload_scene(scene_name: &str) -> bool { + let mut scenes = SCENES.write(); + let initial_len = scenes.len(); + scenes.retain(|scene| scene.scene_name != scene_name); + let removed = scenes.len() != initial_len; + + if removed { + log::info!("Unloaded scene '{}' from memory", scene_name); + } else { + log::debug!("Scene '{}' was not loaded; nothing to unload", scene_name); + } + + removed +} + +/// Reads a scene configuration from disk based on the active project's path. +pub fn load_scene(scene_name: &str) -> anyhow::Result<SceneConfig> { + let scene_path = { + let project = PROJECT.read(); + if project.project_path.as_os_str().is_empty() { + return Err(anyhow::anyhow!("Project path is not set; cannot load scenes")); + } + + project + .project_path + .join("scenes") + .join(format!("{}.eucs", scene_name)) + }; + + let scene = SceneConfig::read_from(&scene_path)?; + log::info!("Loaded scene '{}' from {}", scene_name, scene_path.display()); + Ok(scene) +} + +/// Reloads a scene into the in-memory cache by unloading any existing copy first. +pub fn load_scene_into_memory(scene_name: &str) -> anyhow::Result<()> { + unload_scene(scene_name); + + let scene = load_scene(scene_name)?; + { + let mut scenes = SCENES.write(); + scenes.insert(0, scene); + } + + log::info!("Scene '{}' loaded into memory", scene_name); + + Ok(()) +} + /// The root config file, responsible for building and other metadata. /// /// # Location @@ -16,7 +16,7 @@ use dropbear_engine::{ future::FutureHandle, graphics::{RenderContext, SharedGraphicsContext}, lighting::{Light, LightManager}, - model::ModelId, + model::{ModelId, MODEL_CACHE}, scene::SceneCommand, }; use egui::{self, Context}; @@ -25,7 +25,8 @@ use eucalyptus_core::{ camera::{CameraComponent, CameraType, DebugCamera}, fatal, info, input::InputState, ptr::{GraphicsPtr, InputStatePtr, WorldPtr}, scripting::{BuildStatus, ScriptManager, ScriptTarget}, states, states::{ - CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, SceneEntity, ScriptComponent, + CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, SceneConfig, SceneEntity, + ScriptComponent, WorldLoadingStatus, PROJECT, SCENES, }, success, @@ -36,9 +37,11 @@ use eucalyptus_core::{ }; use hecs::{Entity, World}; use parking_lot::Mutex; +use rfd::FileDialog; use std::path::Path; use std::{ collections::{HashMap, HashSet}, + fs, path::PathBuf, sync::{Arc, LazyLock}, time::{Duration, Instant}, @@ -119,6 +122,13 @@ pub struct Editor { pub plugin_registry: PluginRegistry, pub dock_state_shared: Option<Arc<Mutex<DockState<EditorTab>>>>, + + // scene creation + open_new_scene_window: bool, + new_scene_name: String, + current_scene_name: Option<String>, + pending_scene_load: Option<PendingSceneLoad>, + pending_scene_creation: Option<String>, } impl Editor { @@ -203,6 +213,11 @@ impl Editor { plugin_registry, dock_state_shared: None, outline_pipeline: None, + open_new_scene_window: false, + new_scene_name: String::new(), + current_scene_name: None, + pending_scene_load: None, + pending_scene_creation: None, }) } @@ -503,15 +518,228 @@ impl Editor { Ok(()) } + fn queue_scene_load_by_name(&mut self, scene_name: &str) -> anyhow::Result<()> { + if scene_name.trim().is_empty() { + return Err(anyhow::anyhow!("Scene name cannot be empty")); + } + + if let Some(current) = self.current_scene_name.as_deref() { + states::unload_scene(current); + } + + let scene = states::load_scene(scene_name)?; + + { + let mut scenes = SCENES.write(); + scenes.retain(|existing| existing.scene_name != scene.scene_name); + scenes.insert(0, scene.clone()); + } + + log::info!("Scene '{}' staged for loading", scene.scene_name); + + self.current_scene_name = Some(scene.scene_name.clone()); + self.pending_scene_load = Some(PendingSceneLoad { scene }); + + Ok(()) + } + + fn cleanup_scene_resources(&mut self, graphics: &mut RenderContext) { + if let Some(handle) = self.world_load_handle.take() { + graphics.shared.future_queue.cancel(&handle); + } + + self.light_spawn_queue.clear(); + self.progress_tx = None; + self.world_receiver = None; + self.current_state = WorldLoadingStatus::Idle; + + self.world.clear(); + self.selected_entity = None; + self.previously_selected_entity = None; + self.active_camera.lock().take(); + + self.render_pipeline = None; + self.outline_pipeline = None; + self.texture_id = None; + self.light_manager = LightManager::new(); + + { + let mut cache = MODEL_CACHE.lock(); + cache.clear(); + } + } + + fn start_async_scene_load(&mut self, scene: SceneConfig, graphics: &mut RenderContext) { + self.cleanup_scene_resources(graphics); + + let (progress_sender, progress_receiver) = + tokio::sync::mpsc::unbounded_channel::<WorldLoadingStatus>(); + self.progress_tx = Some(progress_receiver); + self.current_state = WorldLoadingStatus::Idle; + + let (world_sender, world_receiver) = oneshot::channel(); + self.world_receiver = Some(world_receiver); + + self.is_world_loaded = IsWorldLoadedYet::new(); + self.is_world_loaded.mark_scene_loaded(); + + let graphics_shared = graphics.shared.clone(); + let active_camera = self.active_camera.clone(); + let scene_name = scene.scene_name.clone(); + + let handle = graphics.shared.future_queue.push(async move { + let mut temp_world = World::new(); + + let load_result = scene + .load_into_world( + &mut temp_world, + graphics_shared.clone(), + Some(progress_sender.clone()), + ) + .await; + + match load_result { + Ok(active_entity) => { + let mut camera_lock = active_camera.lock(); + *camera_lock = Some(active_entity); + } + Err(err) => { + log::error!("Failed to load scene '{}': {}", scene_name, err); + } + } + + let _ = progress_sender.send(WorldLoadingStatus::Completed); + + if world_sender.send(temp_world).is_err() { + log::error!("Failed to deliver loaded world for scene '{}'", scene_name); + } + }); + + self.world_load_handle = Some(handle); + } + + fn create_new_scene(&mut self, name: &str) -> anyhow::Result<()> { + let trimmed_name = name.trim(); + if trimmed_name.is_empty() { + return Err(anyhow::anyhow!("Scene name cannot be empty")); + } + + if trimmed_name.contains('/') || trimmed_name.contains('\\') || trimmed_name.contains(':') { + return Err(anyhow::anyhow!( + "Scene name cannot contain path separator characters" + )); + } + + let scene_name_owned = trimmed_name.to_string(); + + let project_root = { + let cfg = PROJECT.read(); + cfg.project_path.clone() + }; + + if project_root.as_os_str().is_empty() { + return Err(anyhow::anyhow!("Project path is not set")); + } + + let scenes_dir = project_root.join("scenes"); + if !scenes_dir.exists() { + fs::create_dir_all(&scenes_dir)?; + } + + let target_path = scenes_dir.join(format!("{}.eucs", scene_name_owned)); + if target_path.exists() { + return Err(anyhow::anyhow!( + "Scene '{}' already exists", + scene_name_owned + )); + } + + let scene_config = SceneConfig::new(scene_name_owned.clone(), &target_path); + scene_config.write_to(&project_root)?; + + self.queue_scene_load_by_name(&scene_name_owned)?; + success!("Created scene '{}'", scene_name_owned); + Ok(()) + } + + fn open_scene_from_path(&mut self, path: PathBuf) -> anyhow::Result<()> { + if path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("eucs")) + != Some(true) + { + return Err(anyhow::anyhow!("Selected file is not an .eucs scene")); + } + + let project_root = { + let cfg = PROJECT.read(); + cfg.project_path.clone() + }; + + if project_root.as_os_str().is_empty() { + return Err(anyhow::anyhow!("Project path is not set")); + } + + let scenes_dir = project_root.join("scenes"); + if !path.starts_with(&scenes_dir) { + return Err(anyhow::anyhow!( + "Scene '{}' is outside of the current project", + path.display() + )); + } + + let scene_name = path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| anyhow::anyhow!("Scene file name is invalid"))?; + + self.queue_scene_load_by_name(scene_name)?; + info!("Queued scene '{}' for loading", scene_name); + Ok(()) + } + pub fn show_ui(&mut self, ctx: &Context) { + if let Some(scene_name) = self.pending_scene_creation.take() { + let result = self.create_new_scene(scene_name.as_str()); + self.new_scene_name.clear(); + if let Err(e) = result { + fatal!("Failed to create scene '{}': {}", scene_name, e); + } + } + egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { - if ui - .button("Main Menu (New + Open + Editor Settings)") - .clicked() - { - self.scene_command = SceneCommand::SwitchScene("main_menu".into()); + // if ui + // .button("Main Menu (New + Open + Editor Settings)") + // .clicked() + // { + // self.scene_command = SceneCommand::SwitchScene("main_menu".into()); + // } + + if ui.button("New Scene").clicked() { + self.open_new_scene_window = true; + } + + if ui.button("Open Scene").clicked() { + let scenes_dir = { + let project = PROJECT.read(); + project.project_path.join("scenes") + }; + + let mut dialog = FileDialog::new(); + if scenes_dir.exists() { + dialog = dialog.set_directory(&scenes_dir); + } + + let dialog = dialog.add_filter("Eucalyptus Scenes", &["eucs"]); + + if let Some(path) = dialog.pick_file() { + if let Err(e) = self.open_scene_from_path(path) { + fatal!("Failed to open scene: {}", e); + } + } } if ui.button("Save").clicked() { @@ -679,6 +907,29 @@ impl Editor { self.scene_command = SceneCommand::SwitchScene("editor".to_string()); self.pending_scene_switch = false; } + + let mut open_flag = self.open_new_scene_window; + let mut close_requested = false; + if open_flag { + egui::Window::new("New Scene") + .open(&mut open_flag) + .show(ctx, |ui| { + ui.vertical(|ui| { + ui.label("Name: "); + ui.text_edit_singleline(&mut self.new_scene_name); + if ui.button("Create").clicked() { + self.pending_scene_creation = Some(self.new_scene_name.clone()); + close_requested = true; + } + }); + }); + } + + if close_requested { + open_flag = false; + } + + self.open_new_scene_window = open_flag; } /// Restores transform components back to its original state before PlayMode. @@ -839,7 +1090,7 @@ impl Editor { /// /// **Note**: To be ran AFTER [`Editor::load_project_config`] pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) { - log::debug!("Contents of viewport shader: \n{:#?}", dropbear_engine::shader::shader_wesl::SHADER_SHADER); + // log::debug!("Contents of viewport shader: \n{:#?}", dropbear_engine::shader::shader_wesl::SHADER_SHADER); let shader = Shader::new( graphics.shared.clone(), dropbear_engine::shader::shader_wesl::SHADER_SHADER, @@ -866,7 +1117,7 @@ impl Editor { ); self.render_pipeline = Some(pipeline); - log::debug!("Contents of light shader: \n{:#?}", dropbear_engine::shader::shader_wesl::LIGHT_SHADER); + // log::debug!("Contents of light shader: \n{:#?}", dropbear_engine::shader::shader_wesl::LIGHT_SHADER); self.light_manager.create_render_pipeline( graphics.shared.clone(), dropbear_engine::shader::shader_wesl::LIGHT_SHADER, @@ -874,7 +1125,7 @@ impl Editor { Some("Light Pipeline"), ); - log::debug!("Contents of outline shader: \n{:#?}", dropbear_engine::shader::shader_wesl::OUTLINE_SHADER); + // log::debug!("Contents of outline shader: \n{:#?}", dropbear_engine::shader::shader_wesl::OUTLINE_SHADER); let outline_shader = OutlineShader::init(graphics.shared.clone(), camera.layout()); self.outline_pipeline = Some(outline_shader); } else { @@ -1303,6 +1554,10 @@ pub enum EditorState { Playing, } +struct PendingSceneLoad { + scene: SceneConfig, +} + pub enum PendingSpawn2 { Light, Plane, @@ -21,6 +21,11 @@ use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; impl Scene for Editor { fn load(&mut self, graphics: &mut RenderContext) { + self.current_scene_name = { + let scenes = SCENES.read(); + scenes.first().map(|scene| scene.scene_name.clone()) + }; + let (tx, rx) = unbounded_channel::<WorldLoadingStatus>(); let (tx2, rx2) = oneshot::channel::<World>(); self.progress_tx = Some(rx); @@ -59,6 +64,10 @@ impl Scene for Editor { } fn update(&mut self, dt: f32, graphics: &mut RenderContext) { + if let Some(request) = self.pending_scene_load.take() { + self.start_async_scene_load(request.scene, graphics); + } + if let Some(mut receiver) = self.world_receiver.take() { self.show_project_loading_window(&graphics.shared.get_egui_context()); if let Ok(loaded_world) = receiver.try_recv() { @@ -372,33 +381,33 @@ impl Scene for Editor { ); } - // outline rendering - let has_selected = entities.iter() - .any(|e| e.model.id == model_ptr && e.is_selected); - - if has_selected && self.outline_pipeline.is_some() { - let outline = self.outline_pipeline.as_ref().unwrap(); - let mut render_pass = graphics.continue_pass(); - render_pass.set_pipeline(&outline.pipeline); - - render_pass.set_bind_group(0, &outline.bind_group, &[]); - render_pass.set_bind_group(1, camera.bind_group(), &[]); - - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - - for mesh in &model.meshes { - render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - render_pass.set_index_buffer( - mesh.index_buffer.slice(..), - wgpu::IndexFormat::Uint32, - ); - render_pass.draw_indexed( - 0..mesh.num_elements, - 0, - 0..instances.len() as u32, - ); - } - } + // // outline rendering + // let has_selected = entities.iter() + // .any(|e| e.model.id == model_ptr && e.is_selected); + // + // if has_selected && self.outline_pipeline.is_some() { + // let outline = self.outline_pipeline.as_ref().unwrap(); + // let mut render_pass = graphics.continue_pass(); + // render_pass.set_pipeline(&outline.pipeline); + // + // render_pass.set_bind_group(0, &outline.bind_group, &[]); + // render_pass.set_bind_group(1, camera.bind_group(), &[]); + // + // render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + // + // for mesh in &model.meshes { + // render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + // render_pass.set_index_buffer( + // mesh.index_buffer.slice(..), + // wgpu::IndexFormat::Uint32, + // ); + // render_pass.draw_indexed( + // 0..mesh.num_elements, + // 0, + // 0..instances.len() as u32, + // ); + // } + // } log_once::debug_once!("Rendered {:?}", model_ptr); } else { log_once::error_once!("No such MODEL as {:?}", model_ptr); @@ -19,6 +19,12 @@ extern "C" { // =========================================== typedef struct { + float x; + float y; + float z; +} Vector3D; + +typedef struct { double position_x; double position_y; double position_z; @@ -31,6 +37,25 @@ typedef struct { double scale_z; } NativeTransform; +typedef struct { + const char* label; + int64_t entity_id; + + Vector3D eye; + Vector3D target; + Vector3D up; + + double aspect; + double fov_y; + double znear; + double zfar; + + double yaw; + double pitch; + double speed; + double sensitivity; +} NativeCamera; + // =========================================== int dropbear_get_entity(const char* label, const World* world_ptr, int64_t* out_entity); @@ -47,14 +72,40 @@ int dropbear_set_transform( const NativeTransform transform ); +// property management +int dropbear_get_string_property(const World* world_ptr, int64_t entity_handle, const char* label, char* out_value, int out_value_max_length); +int dropbear_get_int_property(const World* world_ptr, int64_t entity_handle, const char* label, int* out_value); +int dropbear_get_long_property(const World* world_ptr, int64_t entity_handle, const char* label, int64_t* out_value); +int dropbear_get_float_property(const World* world_ptr, int64_t entity_handle, const char* label, float* out_value); +int dropbear_get_double_property(const World* world_ptr, int64_t entity_handle, const char* label, double* out_value); +int dropbear_get_bool_property(const World* world_ptr, int64_t entity_handle, const char* label, int* out_value); // out_value = 0 or 1 +int dropbear_get_vec3_property(const World* world_ptr, int64_t entity_handle, const char* label, float* out_x, float* out_y, float* out_z); + +int dropbear_set_string_property(const World* world_ptr, int64_t entity_handle, const char* label, const char* value); +int dropbear_set_int_property(const World* world_ptr, int64_t entity_handle, const char* label, int value); +int dropbear_set_long_property(const World* world_ptr, int64_t entity_handle, const char* label, int64_t value); +int dropbear_set_float_property(const World* world_ptr, int64_t entity_handle, const char* label, float value); +int dropbear_set_double_property(const World* world_ptr, int64_t entity_handle, const char* label, double value); +int dropbear_set_bool_property(const World* world_ptr, int64_t entity_handle, const char* label, int value); // value = 0 or 1 +int dropbear_set_vec3_property(const World* world_ptr, int64_t entity_handle, const char* label, float x, float y, float z); + + +// input stuff void dropbear_print_input_state(const InputState* input_state_ptr); -int dropbear_is_key_pressed(const InputState* input_state_ptr, int keycode, int* out_value); // out_value is a boolean 0 or 1 +int dropbear_is_key_pressed(const InputState* input_state_ptr, int keycode, int* out_value); // out_value = 0 or 1 int dropbear_get_mouse_position(const InputState* input_state_ptr, float* out_x, float* out_y); int dropbear_is_mouse_button_pressed(const InputState* input_state_ptr, int button_code, int* out_pressed); int dropbear_get_mouse_delta(const InputState* input_state_ptr, float* out_delta_x, float* out_delta_y); int dropbear_is_cursor_locked(const InputState* input_state_ptr, int* out_locked); int dropbear_set_cursor_locked(const InputState* input_state_ptr, int locked); int dropbear_get_last_mouse_pos(const InputState* input_state_ptr, float* out_x, float* out_y); +int dropbear_is_cursor_hidden(const InputState* input_state_ptr, int* out_hidden); +int dropbear_set_cursor_hidden(const InputState* input_state_ptr, int hidden); + +// camera +int dropbear_get_camera(const World* world_ptr, const char* label, NativeCamera* out_camera); +int dropbear_get_attached_camera(const World* world_ptr, int64_t id, NativeCamera* out_camera); +int dropbear_set_camera(const World* world_ptr, const NativeCamera* camera); // =========================================== @@ -259,11 +259,50 @@ actual class NativeEngine { } actual fun getStringProperty(entityHandle: Long, label: String): String? { - TODO("Not yet implemented") + val world = worldHandle ?: return null + memScoped { + val bufferSize = 256 + val output = allocArray<ByteVar>(bufferSize) + + // warning: this could potentially cause a buffer overflow idk + val result = dropbear_get_string_property( + world.reinterpret(), + entityHandle, + label, + output, + bufferSize + ) + + if (result == 0) { + val string = output.toKString() + return string + } else { + println("getStringProperty failed with code: $result") + return null + } + } } actual fun getIntProperty(entityHandle: Long, label: String): Int? { - TODO("Not yet implemented") + val world = worldHandle ?: return null + memScoped { + val output = alloc<IntVar>() + + val result = dropbear_get_int_property( + world.reinterpret(), + entityHandle, + label, + output.ptr, + ) + + if (result == 0) { + val string = output.value + return string + } else { + println("getIntProperty failed with code: $result") + return null + } + } } actual fun getLongProperty(entityHandle: Long, label: String): Long? {