7c471d2
· Thribhu K
feature: euca-runner now works successfully.
fix: got windowing options available for the redback-runtime
fix: some general fixes here and there.
jarvis, run github actions.
Unverified
diff --git a/Cargo.toml b/Cargo.toml
index ff19719..1aecd11 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -72,7 +72,7 @@ dyn-hash = "1.0"
semver = { version = "1.0", features = ["serde"] }
rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] }
cbindgen = { version = "0.29.2" }
-postcard = { version = "1.1"}
+postcard = { version = "1.1", features = ["use-std"]}
pollster = "0.4"
yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] }
yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" }
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index bcf8a9f..5c18635 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -7,7 +7,7 @@ use crate::{
use dropbear_future_queue::FutureQueue;
use egui::{Context, TextureId};
use glam::{DMat4, DQuat, DVec3, Mat3};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
use std::sync::Arc;
use wgpu::*;
use winit::window::Window;
@@ -21,6 +21,7 @@ pub struct SharedGraphicsContext {
pub queue: Arc<Queue>,
pub surface: Arc<Surface<'static>>,
pub surface_format: TextureFormat,
+ pub surface_config: Arc<RwLock<SurfaceConfiguration>>,
pub instance: Arc<wgpu::Instance>,
pub layouts: Arc<BindGroupLayouts>,
pub window: Arc<Window>,
@@ -77,6 +78,7 @@ impl SharedGraphicsContext {
mipmapper: state.mipmapper.clone(),
yakui_renderer: state.yakui_renderer.clone(),
yakui_texture: state.yakui_texture.clone(),
+ surface_config: state.config.clone(),
}
}
}
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index 0028298..6da1a98 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -85,7 +85,7 @@ pub struct State {
pub surface_format: TextureFormat,
pub device: Arc<Device>,
pub queue: Arc<Queue>,
- pub config: SurfaceConfiguration,
+ pub config: Arc<RwLock<SurfaceConfiguration>>,
pub is_surface_configured: bool,
pub layouts: Arc<BindGroupLayouts>,
pub egui_renderer: Arc<Mutex<EguiRenderer>>,
@@ -351,7 +351,7 @@ Hardware:
surface_format: Texture::TEXTURE_FORMAT,
device,
queue,
- config,
+ config: Arc::new(RwLock::new(config)),
is_surface_configured,
depth_texture,
window,
@@ -383,16 +383,19 @@ Hardware:
/// A helper function that changes the surface config when resized (+ depth texture).
pub fn resize(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 {
- self.config.width = width;
- self.config.height = height;
- self.surface.configure(&self.device, &self.config);
+ {
+ let mut config = self.config.write();
+ config.width = width;
+ config.height = height;
+ }
+ self.surface.configure(&self.device, &self.config.read());
self.is_surface_configured = true;
}
self.depth_texture =
- Texture::depth_texture(&self.config, &self.device, Some("depth texture"));
+ Texture::depth_texture(&self.config.read(), &self.device, Some("depth texture"));
self.viewport_texture =
- Texture::viewport(&self.config, &self.device, Some("viewport texture"));
+ Texture::viewport(&self.config.read(), &self.device, Some("viewport texture"));
self.egui_renderer
.lock()
.renderer()
@@ -415,19 +418,21 @@ Hardware:
if !self.is_surface_configured {
return Ok(Vec::new());
}
-
+
+ let config = self.config.read().clone();
+
let output = match self.surface.get_current_texture() {
Ok(val) => val,
Err(e) => {
return match e {
SurfaceError::Lost => {
log_once::warn_once!("Surface lost, reconfiguring...");
- self.surface.configure(&self.device, &self.config);
+ self.surface.configure(&self.device, &config);
Ok(Vec::new())
}
SurfaceError::Outdated => {
log_once::warn_once!("Surface outdated, reconfiguring...");
- self.surface.configure(&self.device, &self.config);
+ self.surface.configure(&self.device, &config);
Ok(Vec::new())
}
SurfaceError::Timeout => {
@@ -446,7 +451,7 @@ Hardware:
};
let screen_descriptor = ScreenDescriptor {
- size_in_pixels: [self.config.width, self.config.height],
+ size_in_pixels: [config.width, config.height],
pixels_per_point: self.window.scale_factor() as f32,
};
diff --git a/crates/euca-runner/Cargo.toml b/crates/euca-runner/Cargo.toml
index 5cbba10..0cd33f3 100644
--- a/crates/euca-runner/Cargo.toml
+++ b/crates/euca-runner/Cargo.toml
@@ -21,11 +21,11 @@ chrono.workspace = true
env_logger.workspace = true
colored.workspace = true
ron.workspace = true
-hecs.workspace = true
-bytemuck.workspace = true
-log-once.workspace = true
winit.workspace = true
serde.workspace = true
-crossbeam-channel.workspace = true
-gilrs.workspace = true
-futures.workspace = true
+
+[features]
+default = ["jvm"]
+
+jvm = ["redback-runtime/jvm", "eucalyptus-core/jvm"]
+debug = ["eucalyptus-core/jvm_debug"]
diff --git a/crates/euca-runner/src/main.rs b/crates/euca-runner/src/main.rs
index b3bc66c..23ceec6 100644
--- a/crates/euca-runner/src/main.rs
+++ b/crates/euca-runner/src/main.rs
@@ -1,3 +1,5 @@
+#![windows_subsystem = "windows"]
+
use app_dirs2::AppInfo;
use dropbear_engine::future::FutureQueue;
use eucalyptus_core::runtime::RuntimeProjectConfig;
@@ -8,13 +10,104 @@ use std::fs;
use std::rc::Rc;
use std::sync::Arc;
use ron::ser::PrettyConfig;
-use winit::window::WindowAttributes;
+use winit::window::{Fullscreen, WindowAttributes};
use dropbear_engine::{DropbearAppBuilder, DropbearWindowBuilder};
use serde::{Deserialize, Serialize};
+use winit::dpi::PhysicalSize;
+use eucalyptus_core::scripting::jni::{RuntimeMode, RUNTIME_MODE};
#[tokio::main]
async fn main() {
- env_logger::init();
+ // env_logger::init();
+
+ #[cfg(all(not(target_os = "android"), feature = "debug"))]
+ {
+ use colored::Colorize;
+ use env_logger::Builder;
+ use log::LevelFilter;
+ use std::fs::OpenOptions;
+
+ let log_dir =
+ app_dirs2::app_root(app_dirs2::AppDataType::UserData, &eucalyptus_core::APP_INFO)
+ .expect("Failed to get app data directory")
+ .join("logs");
+ fs::create_dir_all(&log_dir).expect("Failed to create log dir");
+
+ let datetime_str = chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S");
+ let log_filename = format!("{}.{}.log", "eucalyptus-editor", datetime_str);
+ let log_path = log_dir.join(log_filename);
+
+ let file = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(&log_path)
+ .expect("Failed to open log file");
+ let file = parking_lot::Mutex::new(file);
+
+ let app_target = "eucalyptus-editor".replace('-', "_");
+ let log_config = format!("dropbear_engine=trace,{}=debug,warn", app_target);
+ unsafe { std::env::set_var("RUST_LOG", log_config) };
+
+ Builder::new()
+ .format(move |buf, record| {
+ use std::io::Write;
+
+ let ts = chrono::offset::Local::now().format("%Y-%m-%dT%H:%M:%S");
+
+ let colored_level = match record.level() {
+ log::Level::Error => record.level().to_string().red().bold(),
+ log::Level::Warn => record.level().to_string().yellow().bold(),
+ log::Level::Info => record.level().to_string().green().bold(),
+ log::Level::Debug => record.level().to_string().blue().bold(),
+ log::Level::Trace => record.level().to_string().cyan().bold(),
+ };
+
+ let colored_timestamp = ts.to_string().bright_black();
+
+ let file_info = format!(
+ "{}:{}",
+ record.file().unwrap_or("unknown"),
+ record.line().unwrap_or(0)
+ )
+ .bright_black();
+
+ let console_line = format!(
+ "{} {} [{}] - {}\n",
+ file_info,
+ colored_timestamp,
+ colored_level,
+ record.args()
+ );
+
+ let file_line = format!(
+ "{}:{} {} [{}] - {}\n",
+ record.file().unwrap_or("unknown"),
+ record.line().unwrap_or(0),
+ ts,
+ record.level(),
+ record.args()
+ );
+
+ write!(buf, "{}", console_line)?;
+
+ let mut fh = file.lock();
+ let _ = fh.write_all(file_line.as_bytes());
+
+ Ok(())
+ })
+ .filter_level(LevelFilter::Warn)
+ .filter(Some("dropbear_engine"), LevelFilter::Trace)
+ .filter(
+ Some("eucalyptus_editor"),
+ LevelFilter::Debug,
+ )
+ .filter(Some("eucalyptus_core"), LevelFilter::Debug)
+ .filter(Some("dropbear_traits"), LevelFilter::Debug)
+ .filter(Some("euca_runner"), LevelFilter::Debug)
+ .init();
+ log::info!("Initialised logger");
+ }
+
dropbear_engine::panic::set_hook();
log::debug!("Set panic hook");
@@ -65,10 +158,13 @@ async fn main() {
log::debug!("Located scene config file: [{}] ({} bytes)", path.display(), scene_config.len());
let scene_config: RuntimeProjectConfig = postcard::from_bytes(&scene_config).unwrap();
+ scene_config.populate().unwrap();
log::debug!("Converted scene config file to RuntimeProjectConfig");
- let runtime_scene = Rc::new(RwLock::new(PlayMode::new(None).unwrap()));
+ let _ = RUNTIME_MODE.set(RuntimeMode::Runtime);
+
+ let runtime_scene = Rc::new(RwLock::new(PlayMode::new(Some(scene_config.initial_scene)).unwrap()));
let future_queue = Arc::new(FutureQueue::new());
let authors = scene_config.authors.developer.clone();
@@ -81,11 +177,11 @@ async fn main() {
let attributes = WindowAttributes::default();
- match config.target_resolution {
- WindowModes::Windowed(_, _) => {}
- WindowModes::Maximised => {}
- WindowModes::Fullscreen => {}
- }
+ let attributes = match config.target_resolution {
+ WindowModes::Windowed(x, y) => {attributes.with_inner_size(PhysicalSize::new(x, y))}
+ WindowModes::Maximised => {attributes.with_maximized(true)}
+ WindowModes::Fullscreen => {attributes.with_fullscreen(Some(Fullscreen::Borderless(None)))}
+ };
let window = DropbearWindowBuilder::new()
.with_attributes(attributes)
diff --git a/crates/eucalyptus-core/src/runtime.rs b/crates/eucalyptus-core/src/runtime.rs
index 9532ce5..609525d 100644
--- a/crates/eucalyptus-core/src/runtime.rs
+++ b/crates/eucalyptus-core/src/runtime.rs
@@ -1,10 +1,12 @@
//! Configuration and metadata information about redback-runtime based data.
+use crate::config::ProjectConfig;
use crate::scene::SceneConfig;
use crate::states::{PROJECT, SCENES};
+use crate::utils::option::HistoricalOption;
use anyhow::Context;
+use chrono::Utc;
use semver::Version;
-use crate::utils::option::HistoricalOption;
/// The settings of a project in its runtime.
///
@@ -119,4 +121,44 @@ impl RuntimeProjectConfig {
Ok(result)
}
+
+ /// Populates the states (such as [PROJECT]) with all the context from the RuntimeProjectConfig.
+ pub fn populate(&self) -> anyhow::Result<()> {
+ let exe_dir = std::env::current_exe()
+ .context("Unable to locate runtime executable")?
+ .parent()
+ .ok_or_else(|| anyhow::anyhow!("Unable to locate parent directory of runtime executable"))?
+ .to_path_buf();
+
+ let now = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
+
+ let mut runtime_settings = self.runtime_settings.clone();
+ if runtime_settings.initial_scene.is_none() {
+ runtime_settings.initial_scene = Some(self.initial_scene.clone());
+ }
+
+ let project_config = ProjectConfig {
+ project_name: self.project_name.clone(),
+ project_path: exe_dir,
+ date_created: now.clone(),
+ date_last_accessed: now,
+ project_version: self.project_version.to_string(),
+ authors: self.authors.clone(),
+ runtime_settings,
+ last_opened_scene: Some(self.initial_scene.clone()),
+ };
+
+ {
+ let mut project = PROJECT.write();
+ *project = project_config;
+ }
+
+ {
+ let mut scenes = SCENES.write();
+ scenes.clear();
+ scenes.extend(self.scenes.iter().cloned());
+ }
+
+ Ok(())
+ }
}
diff --git a/crates/eucalyptus-core/src/scripting.rs b/crates/eucalyptus-core/src/scripting.rs
index 760b394..f4e89c9 100644
--- a/crates/eucalyptus-core/src/scripting.rs
+++ b/crates/eucalyptus-core/src/scripting.rs
@@ -119,6 +119,9 @@ impl ScriptManager {
log::debug!("Created new JVM instance");
}
+ #[cfg(not(feature = "jvm"))]
+ log::debug!("JVM feature not enabled");
+
Ok(result)
}
diff --git a/crates/eucalyptus-core/src/scripting/jni.rs b/crates/eucalyptus-core/src/scripting/jni.rs
index c311882..81cd304 100644
--- a/crates/eucalyptus-core/src/scripting/jni.rs
+++ b/crates/eucalyptus-core/src/scripting/jni.rs
@@ -144,8 +144,6 @@ impl JavaContext {
let mut args_log = Vec::new();
-
-
if let Some(args) = external_vm_args {
args_log.push(args.clone());
jvm_args = jvm_args.option(args);
@@ -163,30 +161,8 @@ impl JavaContext {
}
RuntimeMode::Editor => {
log::debug!("JDB is not used in the editor (as there is no need for so)");
-
- // let (start_port, end_port) = (6741, 6761);
- // let mut port = 0000;
- // let mut debug_arg = String::new();
- //
- // for p in start_port..end_port {
- // if is_port_available(p) {
- // port = p;
- // debug_arg = format!("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:{}", port);
- // break;
- // } else {
- // log::debug!("Port {} is not available", p);
- // }
- // }
- //
- // if debug_arg.is_empty() {
- // log::warn!("Could not find an available port for JDWP debugger (tried 6741-6760). Debugging will be disabled.");
- // } else {
- // args_log.push(debug_arg.clone());
- // jvm_args = jvm_args.option(debug_arg);
- // log::info!("JDWP debugger enabled on port {}", port);
- // }
}
- RuntimeMode::PlayMode => {
+ RuntimeMode::PlayMode | RuntimeMode::Runtime => {
let (start_port, end_port) = (6751, 6771);
let mut port = 0000;
let mut debug_arg = String::new();
@@ -218,9 +194,6 @@ impl JavaContext {
log::info!("JDWP debugger enabled on port {}", port);
}
}
- RuntimeMode::Runtime => {
- log::warn!("Runtime mode JWDB not implemented yet...");
- }
}
}
@@ -288,9 +261,6 @@ impl JavaContext {
let jvm_init_args = jvm_args.build()?;
let jvm = JavaVM::new(jvm_init_args)?;
- #[cfg(feature = "jvm_debug")]
- crate::success!("JDB debugger enabled on localhost:6741");
-
log::info!("Created JVM instance");
Ok(Self {
diff --git a/crates/eucalyptus-editor/src/build.rs b/crates/eucalyptus-editor/src/build.rs
index e86eb37..a6ff345 100644
--- a/crates/eucalyptus-editor/src/build.rs
+++ b/crates/eucalyptus-editor/src/build.rs
@@ -76,8 +76,7 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> {
// export to .eupak
let eupak_path = build_dir.join("data.eupak");
- const SIZE_OF_RUNTIME_PROJECT_CONFIG: usize = size_of::<RuntimeProjectConfig>();
- let config_bytes = postcard::to_vec::<RuntimeProjectConfig, SIZE_OF_RUNTIME_PROJECT_CONFIG>(&runtime_config)?;
+ let config_bytes = postcard::to_stdvec::<RuntimeProjectConfig>(&runtime_config)?;
fs::write(&eupak_path, config_bytes)?;
log::debug!("Exported scene config to {:?}", eupak_path);
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 2ada09a..903169b 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -17,14 +17,17 @@ use dropbear_engine::graphics::SharedGraphicsContext;
use dropbear_engine::scene::SceneCommand;
use eucalyptus_core::input::InputState;
use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
-use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT};
-use eucalyptus_core::scene::loading::SCENE_LOADER;
+use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script};
+use eucalyptus_core::scene::loading::{SceneLoadResult, SCENE_LOADER};
use eucalyptus_core::traits::registry::ComponentRegistry;
use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, WorldPtr};
use eucalyptus_core::command::COMMAND_BUFFER;
use eucalyptus_core::scene::loading::IsSceneLoaded;
use std::collections::HashMap;
+use std::env::current_exe;
use std::path::PathBuf;
+use wgpu::SurfaceConfiguration;
+use winit::window::Fullscreen;
use yakui_winit::YakuiWinit;
use eucalyptus_core::physics::PhysicsState;
use eucalyptus_core::rapier3d::prelude::*;
@@ -34,8 +37,9 @@ mod scene;
mod input;
mod command;
+#[cfg(feature = "debug")]
fn find_jvm_library_path() -> PathBuf {
- let proj = PROJECT.read();
+ let proj = eucalyptus_core::states::PROJECT.read();
let project_path = if !proj.project_path.is_dir() {
proj.project_path
.parent()
@@ -44,7 +48,7 @@ fn find_jvm_library_path() -> PathBuf {
} else {
proj.project_path.clone()
}
- .join("build/libs");
+ .join("build/libs");
let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
@@ -73,6 +77,34 @@ fn find_jvm_library_path() -> PathBuf {
.expect("No suitable candidate for a JVM targeted play mode session available")
}
+#[cfg(not(feature = "debug"))]
+fn find_jvm_library_path() -> PathBuf {
+ let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None;
+
+ for entry in std::fs::read_dir(current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") {
+ let entry = entry.expect("Unable to get directory entry");
+ let path = entry.path();
+
+ if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
+ if filename.ends_with("-all.jar") {
+ let metadata = entry.metadata().expect("Unable to get file metadata");
+ let modified = metadata.modified().expect("Unable to get file modified time");
+
+ match latest_jar {
+ None => latest_jar = Some((path.clone(), modified)),
+ Some((_, latest_time)) if modified > latest_time => {
+ latest_jar = Some((path.clone(), modified));
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+
+ latest_jar
+ .map(|(path, _)| path)
+ .expect("No suitable candidate for a JVM targeted play mode session available")
+}
pub struct PlayMode {
scene_command: SceneCommand,
input_state: InputState,
@@ -80,6 +112,7 @@ pub struct PlayMode {
world: Box<World>,
component_registry: Arc<ComponentRegistry>,
active_camera: Option<Entity>,
+ display_settings: DisplaySettings,
// rendering
light_cube_pipeline: Option<LightCubePipeline>,
@@ -160,6 +193,14 @@ impl PlayMode {
collision_force_event_receiver: Some(cfe_r),
event_collector,
yakui_winit: None,
+ display_settings: DisplaySettings {
+ window_mode: WindowMode::Windowed,
+ maintain_aspect_ratio: true,
+ vsync: false,
+ last_window_mode: WindowMode::BorderlessFullscreen,
+ last_vsync: true,
+ last_size: (0, 0),
+ },
};
log::debug!("Created new play mode instance");
@@ -216,6 +257,8 @@ impl PlayMode {
}
/// Requests an asynchronous scene load, returning immediately and loading the scene in the background.
+ ///
+ /// It will not request the scene load if the currently rendered scene is the same as the requested scene.
pub fn request_async_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) {
log::debug!("Requested async scene load: {}", requested_scene.requested_scene);
let scene_name = requested_scene.requested_scene.clone();
@@ -233,8 +276,18 @@ impl PlayMode {
if let Some(id) = progress.id {
let mut loader = SCENE_LOADER.lock();
if let Some(entry) = loader.get_entry_mut(id) {
- if entry.status.is_none() {
- entry.status = self.world_loading_progress.as_ref().cloned();
+ if let Some(scene) = &self.current_scene && scene == &self.scene_progress.as_ref().unwrap().requested_scene {
+ log::debug!("Load scene async request cancelled because scene name is current");
+ entry.result = SceneLoadResult::Error("Currently rendered scene name is the same as the requested scene".to_string());
+ self.world_loading_progress = None;
+ self.world_receiver = None;
+ self.physics_receiver = None;
+ self.scene_progress = None;
+ return
+ } else {
+ if entry.status.is_none() {
+ entry.status = self.world_loading_progress.as_ref().cloned();
+ }
}
}
}
@@ -284,6 +337,11 @@ impl PlayMode {
/// Requests an immediate scene load, blocking the current thread until the scene is fully loaded.
pub fn request_immediate_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) {
+ if let Some(scene) = &self.current_scene && scene == &requested_scene.requested_scene {
+ log::debug!("Immediate scene load request cancelled because scene name is current");
+ return
+ }
+
let scene_name = requested_scene.requested_scene.clone();
log::debug!("Immediate scene load requested: {}", scene_name);
@@ -368,4 +426,90 @@ impl PlayMode {
self.current_scene = Some(scene_progress.requested_scene.clone());
}
}
+}
+
+pub struct DisplaySettings {
+ pub window_mode: WindowMode,
+ pub maintain_aspect_ratio: bool,
+ pub vsync: bool,
+ last_window_mode: WindowMode,
+ last_vsync: bool,
+ last_size: (u32, u32),
+}
+
+impl DisplaySettings {
+ pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) {
+ let window = graphics.window.clone();
+ let size = (
+ graphics.viewport_texture.size.width,
+ graphics.viewport_texture.size.height,
+ );
+
+ let needs_update = self.window_mode != self.last_window_mode
+ || self.vsync != self.last_vsync
+ || size != self.last_size;
+
+ if !needs_update {
+ return;
+ }
+
+ match self.window_mode {
+ WindowMode::Windowed => {
+ window.set_fullscreen(None);
+ window.set_maximized(false);
+ }
+ WindowMode::Maximized => {
+ window.set_fullscreen(None);
+ window.set_maximized(true);
+ }
+ WindowMode::Fullscreen => {
+ let monitor = window.current_monitor();
+ let fullscreen = monitor
+ .as_ref()
+ .and_then(|m| m.video_modes().next())
+ .map(Fullscreen::Exclusive)
+ .or_else(|| Some(Fullscreen::Borderless(monitor)));
+
+ window.set_fullscreen(fullscreen);
+ window.set_maximized(false);
+ }
+ WindowMode::BorderlessFullscreen => {
+ let monitor = window.current_monitor();
+ window.set_fullscreen(Some(Fullscreen::Borderless(monitor)));
+ window.set_maximized(false);
+ }
+ }
+
+ let config = SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format: graphics.surface_format,
+ width: graphics.viewport_texture.size.width,
+ height: graphics.viewport_texture.size.height,
+ present_mode: if self.vsync {
+ wgpu::PresentMode::AutoVsync
+ } else {
+ wgpu::PresentMode::AutoNoVsync
+ },
+ alpha_mode: wgpu::CompositeAlphaMode::Auto,
+ view_formats: vec![],
+ desired_maximum_frame_latency: 2,
+ };
+
+ {
+ let mut cfg = graphics.surface_config.write();
+ *cfg = config;
+ }
+
+ self.last_window_mode = self.window_mode;
+ self.last_vsync = self.vsync;
+ self.last_size = size;
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum WindowMode {
+ Windowed,
+ Maximized,
+ Fullscreen,
+ BorderlessFullscreen,
}
\ No newline at end of file
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 5a2d151..8002c8f 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -64,6 +64,12 @@ impl Scene for PlayMode {
let _ = self.script_manager.physics_update_script(self.world.as_mut(), dt as f64);
}
+ let world = self.world.iter().map(|e| (e.get::<&Label>().unwrap().to_string(), e.entity())).collect::<Vec<_>>();
+ log::info!("World contents [len={}]: ", world.len());
+ for (l, e) in world {
+ log::info!("{} -> {:?}", l, e);
+ }
+
for kcc in self.world.query::<&mut KCC>().iter() {
kcc.collisions.clear();
}
@@ -242,6 +248,8 @@ impl Scene for PlayMode {
graphics.future_queue.poll();
self.poll(graphics.clone());
+ self.display_settings.update(graphics.clone());
+
{
if let Some(fps) = PROJECT.read().runtime_settings.target_fps.get() {
log_once::debug_once!("setting new fps for play mode session: {}", fps);
@@ -381,6 +389,40 @@ impl Scene for PlayMode {
#[cfg(feature = "debug")]
egui::TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| {
egui::MenuBar::new().ui(ui, |ui| {
+ use crate::WindowMode;
+ ui.menu_button("Window", |ui| {
+ ui.menu_button("Window Mode", |ui| {
+ let is_windowed = matches!(self.display_settings.window_mode, WindowMode::Windowed);
+ if ui.selectable_label(is_windowed, "Windowed").clicked() {
+ self.display_settings.window_mode = WindowMode::Windowed;
+ ui.close();
+ }
+
+ let is_maximized = matches!(self.display_settings.window_mode, WindowMode::Maximized);
+ if ui.selectable_label(is_maximized, "Maximized").clicked() {
+ self.display_settings.window_mode = WindowMode::Maximized;
+ ui.close();
+ }
+
+ let is_fullscreen = matches!(self.display_settings.window_mode, WindowMode::Fullscreen);
+ if ui.selectable_label(is_fullscreen, "Fullscreen").clicked() {
+ self.display_settings.window_mode = WindowMode::Fullscreen;
+ ui.close();
+ }
+
+ let is_borderless = matches!(self.display_settings.window_mode, WindowMode::BorderlessFullscreen);
+ if ui.selectable_label(is_borderless, "Borderless Fullscreen").clicked() {
+ self.display_settings.window_mode = WindowMode::BorderlessFullscreen;
+ ui.close();
+ }
+ });
+
+ ui.separator();
+
+ ui.checkbox(&mut self.display_settings.maintain_aspect_ratio, "Maintain aspect ratio");
+ ui.checkbox(&mut self.display_settings.vsync, "VSync").clicked();
+ });
+
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.group(|ui| {
ui.add_enabled_ui(true, |ui| {
@@ -403,7 +445,6 @@ impl Scene for PlayMode {
CentralPanel::default().show(&graphics.get_egui_context(), |ui| {
if let Some(p) = &self.scene_progress {
if !p.is_everything_loaded() && p.is_first_scene {
- // todo: change from label to "splashscreen"
ui.centered_and_justified(|ui| {
egui_extras::install_image_loaders(&graphics.get_egui_context());
ui.image(egui::include_image!("../../../resources/eucalyptus-editor.png"))
@@ -425,20 +466,19 @@ impl Scene for PlayMode {
self.has_initial_resize_done = true;
}
- // if !self.display_settings.maintain_aspect_ratio {
- // cam.aspect = (available_size.x / available_size.y) as f64;
- // }
+ if !self.display_settings.maintain_aspect_ratio {
+ cam.aspect = (available_size.x / available_size.y) as f64;
+ }
cam.update_view_proj();
cam.update(graphics.clone());
- let (display_width, display_height) = (available_size.x, available_size.y);
- // if self.display_settings.maintain_aspect_ratio {
- // let width = available_size.x;
- // let height = width / cam.aspect as f32;
- // (width, height)
- // } else {
-
- // };
+ let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio {
+ let width = available_size.x;
+ let height = width / cam.aspect as f32;
+ (width, height)
+ } else {
+ (available_size.x, available_size.y)
+ };
let center_x = available_rect.center().x;
let center_y = available_rect.center().y;
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index 295a6ac..9ed9e3e 100644
--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -6,6 +6,7 @@ import com.dropbear.input.InputState
import com.dropbear.logging.Logger
import com.dropbear.scene.SceneManager
import com.dropbear.ui.UIInstruction
+import com.dropbear.ui.UIInstructionSet
internal var exceptionOnError: Boolean = false
var lastErrorMessage: String? = null
@@ -75,18 +76,40 @@ class DropbearEngine(val native: NativeEngine) {
fun callExceptionOnError(toggle: Boolean) {
}
- fun renderUI(uiInstructionSet: List<UIInstruction>) {
- Logger.trace("instructions: $uiInstructionSet")
- renderUI(instructions = uiInstructionSet)
+ /**
+ * Renders a set of UI instructions to be displayed onto the screen.
+ *
+ * This uses the rust crate `yakui` to power the UI. You can get a [UIInstructionSet]
+ * by either doing one of two ways:
+ *
+ * ## Method 1 (recommended)
+ * ```kt
+ * val instructions: UIInstructionSet = buildUI {
+ * label("hello world!")
+ * }
+ * engine.renderUI(instructions)
+ * ```
+ *
+ * ## Method 2 (the non-dsl way)
+ * ```kt
+ * val builder = UIBuilder()
+ * builder.add(Text.label("hello world!").toInstruction())
+ * engine.renderUI(builder.build())
+ * ```
+ */
+ fun renderUI(uiInstructionSet: UIInstructionSet?) {
+ if (uiInstructionSet != null) {
+ renderUI(instructions = uiInstructionSet)
+ }
}
/**
* Quits the currently running app or game elegantly.
*
- * This function can have different behaviours depending on where it is ran.
+ * This function can have different behaviours depending on where it is run.
* - eucalyptus-editor - When called, this exits your Play Mode session and returns you back to
* `EditorState::Editing`
- * - redback-runtime - When called, this will exit your current process and kill the app as is. It will
+ * - euca-runner - When called, this will exit your current process and kill the app as is. It will
* also drop any pointers and do any additional cleanup.
*/
fun quit() {
diff --git a/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
new file mode 100644
index 0000000..218ee65
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/components/camera/SpringyCameraController.kt
@@ -0,0 +1,50 @@
+package com.dropbear.components.camera
+
+import com.dropbear.math.Vector3d
+import com.dropbear.physics.ColliderShape
+import com.dropbear.physics.Physics
+
+/**
+ * A springy camera that is used for any camera. It uses ray casting to check if the distance from the player to the
+ * camera is obstructed by a physics object, and if so set the distance to the last "safe" spot.
+ *
+ * Inspired by Godot's SpringyCameraController (other engines probably have their own, never used them before).
+ */
+class SpringyCameraController {
+ private var currentDistance: Double = 5.0
+ private val margin = 0.3
+ private val sphereRadius = 0.2
+
+ fun getSpringyPosition(
+ playerPos: Vector3d,
+ targetPos: Vector3d,
+ deltaTime: Double
+ ): Vector3d {
+ val vectorToCam = targetPos - playerPos
+ val maxDist = vectorToCam.length()
+ val dir = vectorToCam.normalize()
+
+ val hit = Physics.shapeCast(
+ origin = playerPos,
+ shape = ColliderShape.Sphere(sphereRadius.toFloat()),
+ direction = dir,
+ maxDistance = maxDist,
+ solid = false
+ )
+
+ val targetDist = if (hit != null) {
+ (hit.distance - margin).coerceAtLeast(0.1)
+ } else {
+ maxDist
+ }
+
+ if (targetDist < currentDistance) {
+ currentDistance = targetDist
+ } else {
+ val returnSpeed = 5.0
+ currentDistance += (targetDist - currentDistance) * (returnSpeed * deltaTime).coerceIn(0.0, 1.0)
+ }
+
+ return playerPos + (dir * currentDistance)
+ }
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
index f4f5eee..2e74526 100644
--- a/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/ui/UIBuilder.kt
@@ -1,14 +1,16 @@
package com.dropbear.ui
+typealias UIInstructionSet = List<UIInstruction>
+
class UIBuilder {
val instructions: MutableList<UIInstruction> = mutableListOf()
private var nextId = 0
internal fun generateId(): Int = nextId++
- fun build(): List<UIInstruction> = instructions.toList()
+ fun build(): UIInstructionSet = instructions.toList()
}
-inline fun buildUI(block: UIBuilder.() -> Unit): List<UIInstruction> {
+inline fun buildUI(block: UIBuilder.() -> Unit): UIInstructionSet {
return UIBuilder().apply(block).build()
}