tirbofish/dropbear · diff
started work on redback-runtime, added many values onto scenes, runtime configs. documented many different functions, structs and values. also just did a simple 4tkbytes -> tirbofish (to show my new github name and ensure all docs are correct).
last commit for the weekend.
assessment season coming up, and i CANNOT fuck this up for my HSC. looks like this is a public transport project for now, or a _potential_ weekend project.
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -57,12 +57,13 @@ jobs: with: targets: ${{ matrix.target }} - name: build eucalyptus packages - run: cargo build --release --package eucalyptus-core --package eucalyptus-editor --package magna-carta --target ${{ matrix.target }} + run: cargo build --release --package eucalyptus-core --package eucalyptus-editor --package magna-carta --package redback-runtime --target ${{ matrix.target }} - name: prepare the artifact run: | mkdir -p dist/libs cp target/${{ matrix.target }}/release/eucalyptus-editor${{ matrix.ext }} dist/ cp target/${{ matrix.target }}/release/magna-carta${{ matrix.ext }} dist/ + cp target/${{ matrix.target }}/release/redback-runtime${{ matrix.ext }} dist/ if [ "${{ matrix.os }}" = "windows-latest" ]; then cp target/${{ matrix.target }}/release/eucalyptus_core.dll dist/ 2>/dev/null || true cp target/${{ matrix.target }}/release/eucalyptus_core.dll.lib dist/ 2>/dev/null || true @@ -2,7 +2,7 @@ package.version = "0.1.2" package.edition = "2024" package.license = "MIT OR Apache-2.0" -package.repository = "https://github.com/4tkbytes/dropbear" +package.repository = "https://github.com/tirbofish/dropbear" package.readme = "README.md" resolver = "3" @@ -78,6 +78,7 @@ syn = { version = "2.0", features = ["full"] } quote = "1.0" egui_ltreeview = { version = "0.6", features = ["doc"] } dyn-hash = "1.0" +semver = { version = "1.0", features = ["serde"] } [workspace.dependencies.image] version = "0.25" @@ -165,27 +165,27 @@ publishing { pom { name.set("dropbear") description.set("The dropbear scripting part of the engine... uhh yeah!") - url.set("https://github.com/4tkbytes/dropbear") + url.set("https://github.com/tirbofish/dropbear") licenses { license { name.set("dropbear engine License, Version 1.2") - url.set("https://raw.githubusercontent.com/4tkbytes/dropbear/refs/heads/main/LICENSE.md") + url.set("https://raw.githubusercontent.com/tirbofish/dropbear/refs/heads/main/LICENSE.md") } } developers { developer { - id.set("4tkbytes") + id.set("tirbofish") name.set("tk") - email.set("4tkbytes@pm.me") + email.set("tirbofish@pm.me") } } scm { - url.set("https://github.com/4tkbytes/dropbear") - connection.set("scm:git:git://github.com/4tkbytes/dropbear.git") - developerConnection.set("scm:git:ssh://git@github.com/4tkbytes/dropbear.git") + url.set("https://github.com/tirbofish/dropbear") + connection.set("scm:git:git://github.com/tirbofish/dropbear.git") + developerConnection.set("scm:git:ssh://git@github.com/tirbofish/dropbear.git") } } } @@ -1,6 +1,6 @@ [package] name = "dropbear-engine" -description = "A game engine made by 4tkbytes. Thats really it..." +description = "A game engine made by tirbofish. Thats really it..." version.workspace = true edition.workspace = true @@ -145,7 +145,7 @@ impl Transform { /// A renderer for meshes and materials related to a model. /// /// It includes the instances as well as a handle. The reason for a handle is so the model being rendered can be swapped -/// to something else without deleting the entire renderer. Also saves memory by rendering anything that has been loaded. +/// to something else without deleting the entire renderer. Also saves memory by rendering anything that has been loaded. pub struct MeshRenderer { handle: LoadedModel, pub instance: Instance, @@ -533,7 +533,7 @@ impl App { env!("CARGO_PKG_VERSION"), rustc_version_runtime::version_meta().short_version_string ); - log::info!("Made by tk with love at https://github.com/4tkbytes/dropbear <3"); + log::info!("Made by tk with love at https://github.com/tirbofish/dropbear <3"); log::info!("======================================================================"); #[cfg(debug_assertions)] { @@ -577,7 +577,7 @@ impl App { /// /// # Parameters /// * config - [`WindowConfiguration`]: The configuration/settings of the window. -/// * queue - [`Option<Throwable<FutureQueue>>`]: An optional value for a [`FutureQueue`] +/// * queue - [`Option<Arc<FutureQueue>>`]: An optional value for a [`FutureQueue`] /// * setup - [`FnOnce`]: A function that sets up all the scenes. It shouldn't be loaded /// but instead be set as an [`Arc<Mutex<T>>`]. macro_rules! run_app { @@ -1,6 +1,6 @@ //! A straight plane (and some components). That's it. //! -//! Inspiration taken from `https://github.com/4tkbytes/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, +//! Inspiration taken from `https://github.com/tirbofish/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, //! my old game engine made in C sharp, where this is the plane "algorithm". use crate::asset::{ASSET_REGISTRY, AssetRegistry}; @@ -63,6 +63,7 @@ impl Manager { } } + /// Switches the scene from the current one to another. pub fn switch(&mut self, name: &str) { if self.scenes.contains_key(name) { self.next_scene = Some(name.to_string()); @@ -179,6 +180,11 @@ impl Manager { } } +/// Helper function that adds a struct that implements [`Scene`], [`input::Keyboard`], +/// [`input::Mouse`] and [`input::Controller`]. +/// +/// Specifically, it adds the struct as keyboard, mouse and controller, then it attaches +/// that input structs to the scene. pub fn add_scene_with_input< S: 'static + Scene + input::Keyboard + input::Mouse + input::Controller, >( @@ -19,8 +19,8 @@ dependencies { } gradlePlugin { - website.set("https://github.com/4tkbytes/dropbear") - vcsUrl.set("https://github.com/4tkbytes/dropbear") + website.set("https://github.com/tirbofish/dropbear") + vcsUrl.set("https://github.com/tirbofish/dropbear") plugins { create("dropbearGradlePlugin") { @@ -39,7 +39,7 @@ publishing { pom { name.set("dropbear-gradle-plugin") description.set("Gradle plugin for dropbear dependency management") - url.set("https://4tkbytes.github.io/dropbear/") + url.set("https://tirbofish.github.io/dropbear/") licenses { license { @@ -49,15 +49,15 @@ publishing { } developers { developer { - id.set("4tkbytes") + id.set("tirbofish") name.set("tk") - email.set("4tkbytes@pm.me") + email.set("tirbofish@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") + connection.set("scm:git:git://github.com/tirbofish/dropbear.git") + developerConnection.set("scm:git:ssh://github.com/tirbofish/dropbear.git") + url.set("https://github.com/tirbofish/dropbear") } } } @@ -20,7 +20,11 @@ pub trait SerializableComponent: Send + Sync + Debug { /// Returns the display name of the component. fn display_name(&self) -> String { let type_name = self.type_name(); - type_name.split("::").last().unwrap_or(type_name).to_string() + type_name + .split("::") + .last() + .unwrap_or(type_name) + .to_string() } } @@ -96,7 +96,6 @@ impl ComponentRegistry { }) } - // Register a custom converter for special cases pub fn register_converter<From, To, F>(&mut self, converter_fn: F) where @@ -1,6 +1,6 @@ //! Enabling multithreading for functions and apps that are purely single threaded. //! -//! This was originally a module in my [dropbear](https://github.com/4tkbytes/dropbear) game engine, +//! This was originally a module in my [dropbear](https://github.com/tirbofish/dropbear) game engine, //! however I thought there were barely any libraries that had future queuing. It takes inspiration //! from Unity and how they handle their events. //! @@ -39,6 +39,7 @@ app_dirs2.workspace = true log-once.workspace = true rfd = { workspace = true, optional = true } typetag.workspace = true +semver.workspace = true [features] # editor only stuff @@ -1,6 +1,6 @@ fn main() -> anyhow::Result<()> { // // todo: move this into the "setup" process - // let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; + // let repo_zip_url = "https://github.com/tirbofish/dropbear/archive/refs/heads/main.zip"; // let response = reqwest::blocking::get(repo_zip_url) // .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? // .bytes() @@ -12,7 +12,7 @@ fn main() -> anyhow::Result<()> { // // let app_info = app_dirs2::AppInfo { // name: "Eucalyptus", - // author: "4tkbytes", + // author: "tirbofish", // }; // let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) // .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; @@ -48,7 +48,7 @@ fn main() -> anyhow::Result<()> { // // if !found_resource { // return Err(anyhow::anyhow!( - // "No resources folder found in the github repository [4tkbytes/dropbear] :(" + // "No resources folder found in the github repository [tirbofish/dropbear] :(" // )); // } @@ -1,3 +1,4 @@ +use crate::runtime::{Authoring, RuntimeSettings}; use crate::scene::SceneConfig; use crate::states::{ EditorSettings, EditorTab, File, Folder, Node, RESOURCES, ResourceType, SCENES, SOURCE, @@ -8,7 +9,6 @@ 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. /// @@ -20,8 +20,13 @@ pub struct ProjectConfig { pub project_path: PathBuf, pub date_created: String, pub date_last_accessed: String, + + /// Semantic version of the project. Default is set to `0.1.0` #[serde(default)] - pub dock_layout: Option<DockState<EditorTab>>, + pub project_version: Option<String>, + + #[serde(default)] + pub authors: Authoring, #[serde(default)] pub editor_settings: EditorSettings, @@ -31,6 +36,10 @@ pub struct ProjectConfig { #[serde(default)] pub last_opened_scene: Option<String>, + + // ensure this is last otherwise it clutters the .eucp file + #[serde(default)] + pub dock_layout: Option<DockState<EditorTab>>, } impl ProjectConfig { @@ -45,10 +54,12 @@ impl ProjectConfig { project_path: project_path.as_ref().to_path_buf(), date_created, date_last_accessed, + project_version: None, editor_settings: Default::default(), dock_layout: None, last_opened_scene: None, runtime_settings: Default::default(), + authors: Default::default(), }; let _ = result.load_config_to_memory(); result @@ -203,13 +214,16 @@ impl ProjectConfig { log::error!("Failed to write new scene: {}", err); rfd::MessageDialog::new() .set_title("Write Error") - .set_description(&format!("Failed to create new scene file: {}", err)) + .set_description(&format!( + "Failed to create new scene file: {}", + err + )) .show(); std::process::exit(1); } return Some(new_scene); - }, + } _ => { std::process::exit(1); } @@ -24,5 +24,5 @@ pub use egui; /// By default, most of its items are located in [`app_dirs2::AppDataType::UserData`]. pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { name: "Eucalyptus", - author: "4tkbytes", + author: "tirbofish", }; @@ -1,25 +1,28 @@ -use crate::scene::{SceneConfig}; +use crate::scene::SceneConfig; use crate::states::{PROJECT, SCENES}; +use anyhow::Context; +use semver::Version; -/// The settings of a project in its runtime. -/// +/// 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 {} +/// 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 { + /// The first scene that shows up when redback-runtime is ran. + /// + /// The first scene is not set is expected to be the first scene out of the + /// projects scene list, or just a normal anyhow error. + pub initial_scene: Option<String>, +} impl RuntimeSettings { - /// Creates a new [`RuntimeSettings`] config. + /// Creates a new [`RuntimeSettings`] config. pub fn new() -> Self { - Self { } + Self { + initial_scene: None, + } } } @@ -29,36 +32,92 @@ impl Default for RuntimeSettings { } } -/// The configuration of a packaged eucalyptus project. -/// +/// A struct that represents the authors and creators of the eucalyptus project. +#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Authoring { + /// The team behind the game + pub developer: String, +} + +impl Default for Authoring { + fn default() -> Self { + Self { + developer: String::from("Unknown"), + } + } +} + +/// 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)] +/// resources. +#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct RuntimeProjectConfig { + /// The name of the project #[bincode(with_serde)] pub project_name: String, - // authoring stuff needs to be added, maybe later + /// The initial/first scene that will show up. + /// + /// Access to other scenes are done with the game's scripting. + #[bincode(with_serde)] + pub initial_scene: String, + + /// Authors and creators of the game + #[bincode(with_serde)] + pub authors: Authoring, + + /// The version of dropbear engine and eucalyptus-editor. + /// + /// dropbear and eucalyptus all share the same semver version. + #[bincode(with_serde)] + pub editor_version: Version, - // versioning stuff too + /// The version of the project. By default, it returns `0.1.0` if none has been specified. + #[bincode(with_serde)] + pub project_version: Version, + /// Any specific settings to do with the runtime. #[bincode(with_serde)] pub runtime_settings: RuntimeSettings, - + + /// All scenes that are available in the project. #[bincode(with_serde)] pub scenes: Vec<SceneConfig>, } impl RuntimeProjectConfig { - /// Creates a [RuntimeProjectConfig] from a loaded [PROJECT] and [SCENES] states. - pub fn from_memory() -> Self { + /// Creates a [RuntimeProjectConfig] from a loaded [PROJECT] and [SCENES] states. + pub fn from_memory() -> anyhow::Result<Self> { let project = PROJECT.read(); let scenes = SCENES.read(); - - Self { + + let initial_scene = match &project.runtime_settings.initial_scene { + Some(val) => val.clone(), + None => { + log::warn!("Unable to fetch initial settings, using first scene available"); + let scene = scenes.first().ok_or(anyhow::anyhow!("Unable to locate first scene in SCENES"))?; + scene.scene_name.clone() + }, + }; + + let result = Self { project_name: project.project_name.clone(), + authors: Authoring::default(), + editor_version: Version::parse(env!("CARGO_PKG_VERSION")).context("This should not happen, unless some issue is happening with env!(\"CARGO_PKG_VERSION\")")?, + project_version: Version::parse( + project + .project_version + .clone() + .unwrap_or("0.1.0".to_string()) + .as_str(), + ) + .unwrap_or(Version::new(0, 1, 0)), runtime_settings: project.runtime_settings.clone(), scenes: scenes.to_vec(), - } + initial_scene, + }; + + Ok(result) } -} +} @@ -1,8 +1,8 @@ use crate::camera::{CameraComponent, CameraType}; use crate::hierarchy::{Children, Parent, SceneHierarchy}; use crate::states::{ - Camera3D, Label, Light, ModelProperties, PROJECT, Script, - SerializedMeshRenderer, WorldLoadingStatus, + Camera3D, Label, Light, ModelProperties, PROJECT, Script, SerializedMeshRenderer, + WorldLoadingStatus, }; use crate::utils::ResolveReference; use dropbear_engine::asset::ASSET_REGISTRY; @@ -65,14 +65,14 @@ impl SceneEntity { pub struct SceneSettings {/* *crickets* */} impl SceneSettings { - /// Creates a new [`SceneSettings`] config. + /// 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. +/// 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)] @@ -141,8 +141,9 @@ impl SceneConfig { .await?; let model = loaded_model.make_mut(); - model.path = - ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + model.path = ResourceReference::from_euca_uri( + "euca://internal/dropbear/models/cube", + )?; loaded_model.refresh_registry(); @@ -179,7 +180,8 @@ impl SceneConfig { .await?; let model = loaded_model.make_mut(); - model.path = ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + model.path = + ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; loaded_model.refresh_registry(); @@ -554,7 +556,8 @@ impl SceneConfig { ..Default::default() }; let light = - EngineLight::new(graphics.clone(), comp.clone(), trans, Some("Default Light")).await; + EngineLight::new(graphics.clone(), comp.clone(), trans, Some("Default Light")) + .await; let light_config = Light { label: "Default Light".to_string(), @@ -66,7 +66,7 @@ pub struct ScriptManager { impl ScriptManager { /// Creates a new [`ScriptManager`] uninitialised instance, as well as a new - /// JVM instance. + /// JVM instance (if the JVM flag is enabled) pub fn new() -> anyhow::Result<Self> { #[allow(unused_mut)] let mut result = Self { @@ -284,6 +284,12 @@ impl ScriptManager { } } +/// Fetches the gradle command available for that operating system. +/// +/// # Platform-specific behaviours +/// - `windows` - Windows uses `gradlew.bat` +/// - `linux` - Linux uses `./gradlew` +/// - `macos` - macOS uses `./gradlew` fn get_gradle_command(project_root: impl AsRef<Path>) -> String { let project_root = project_root.as_ref().to_owned(); if cfg!(target_os = "windows") { @@ -303,7 +309,7 @@ fn get_gradle_command(project_root: impl AsRef<Path>) -> String { } } -/// Asynchronously builds a project for the JVM using gradle. +/// Asynchronously builds a project for the JVM using gradle. pub async fn build_jvm( project_root: impl AsRef<Path>, status_sender: Sender<BuildStatus>, @@ -450,9 +456,11 @@ pub async fn build_native( }; 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 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() { @@ -461,19 +469,22 @@ pub async fn build_native( } 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)) + 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 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 _ = status_sender.send(BuildStatus::Building(format!( + "Running: {} build", + gradle_cmd + ))); let mut child = Command::new(&gradle_cmd) .current_dir(project_root) @@ -518,7 +529,10 @@ pub async fn build_native( 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 err = format!( + "Build succeeded but output directory missing: {:?}", + output_dir + ); let _ = status_sender.send(BuildStatus::Failed(err.clone())); return Err(anyhow::anyhow!(err)); } @@ -20,7 +20,10 @@ use glam::{DQuat, DVec3}; use hecs::World; use jni::JNIEnv; use jni::objects::{JClass, JObject, JPrimitiveArray, JString, JValue}; -use jni::sys::{JNI_FALSE, jboolean, jclass, jdouble, jfloatArray, jint, jlong, jobject, jobjectArray, jstring, jlongArray}; +use jni::sys::{ + JNI_FALSE, jboolean, jclass, jdouble, jfloatArray, jint, jlong, jlongArray, jobject, + jobjectArray, jstring, +}; use parking_lot::Mutex; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -2490,7 +2493,9 @@ pub fn Java_com_dropbear_ffi_JNINative_getChildren( let world = convert_ptr!(world_handle, WorldPtr => World); let entity = convert_jlong_to_entity!(entity_id); - let entities = if let Ok(mut q) = world.query_one::<&Children>(entity) && let Some(children) = q.get() { + let entities = if let Ok(mut q) = world.query_one::<&Children>(entity) + && let Some(children) = q.get() + { let children = children.children(); let mut array = vec![]; for child in children { @@ -2529,7 +2534,9 @@ pub fn Java_com_dropbear_ffi_JNINative_getChildByLabel( let entity = convert_jlong_to_entity!(entity_id); let target = convert_jstring!(env, label); - if let Ok(mut q) = world.query_one::<&Children>(entity) && let Some(children) = q.get() { + if let Ok(mut q) = world.query_one::<&Children>(entity) + && let Some(children) = q.get() + { for child in children.children() { if let Ok(label) = world.get::<&Label>(entity) { if label.as_str() == target { @@ -2540,7 +2547,6 @@ pub fn Java_com_dropbear_ffi_JNINative_getChildByLabel( continue; } } - } else { // no children exist for the entity return -2 as jlong; @@ -2571,4 +2577,4 @@ pub fn Java_com_dropbear_ffi_JNINative_getParent( } else { crate::ffi_error_return!("No entity exists") } -} +} @@ -6,6 +6,7 @@ use dropbear_engine::camera::Camera; use dropbear_engine::entity::{MaterialOverride, MeshRenderer, Transform}; use dropbear_engine::lighting::LightComponent; use dropbear_engine::utils::ResourceReference; +use dropbear_engine::{MutableWindowConfiguration}; use dropbear_macro::SerializableComponent; use egui::Ui; use once_cell::sync::Lazy; @@ -18,7 +19,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. +/// A global "singleton" that contains the configuration of a project. pub static PROJECT: Lazy<RwLock<ProjectConfig>> = Lazy::new(|| RwLock::new(ProjectConfig::default())); @@ -182,18 +183,15 @@ impl Default for Camera3D { } impl Camera3D { - pub fn from_ecs_camera( - camera: &Camera, - component: &CameraComponent, - ) -> Self { + pub fn from_ecs_camera(camera: &Camera, component: &CameraComponent) -> Self { let position = glam::DVec3::from_array(camera.eye.to_array()); let target = glam::DVec3::from_array(camera.target.to_array()); let up = glam::DVec3::from_array(camera.up.to_array()); - + let rotation = if (target - position).length_squared() > 0.0001 { - glam::DQuat::from_mat4(&glam::DMat4::look_at_lh(position, target, up)).inverse() + glam::DQuat::from_mat4(&glam::DMat4::look_at_lh(position, target, up)).inverse() } else { - glam::DQuat::IDENTITY + glam::DQuat::IDENTITY }; let transform = Transform { @@ -542,14 +540,14 @@ impl SerializableComponent for SerializedMeshRenderer { self } - fn clone_boxed(&self) -> Box<dyn SerializableComponent> { - Box::new(self.clone()) - } - fn type_name(&self) -> &'static str { "SerializedMeshRenderer" } + fn clone_boxed(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + fn display_name(&self) -> String { "MeshRenderer".to_string() } @@ -565,3 +563,12 @@ impl SerializedMeshRenderer { } } } + +/// A file called `config.eucfg` that contains all the file-editable contents of +#[derive(Debug, Clone, Deserialize, Serialize, bincode::Encode, bincode::Decode)] +pub struct ConfigFile { + pub jvm_args: Option<String>, + + #[bincode(with_serde)] + pub window_configuration: MutableWindowConfiguration, +} @@ -48,6 +48,7 @@ chrono.workspace = true egui_ltreeview.workspace = true ron.workspace = true rfd.workspace = true +semver.workspace = true [features] default = ["editor"] @@ -1,11 +1,12 @@ use eucalyptus_core::config::ProjectConfig; use eucalyptus_core::runtime::RuntimeProjectConfig; use eucalyptus_core::scene::SceneConfig; +use semver::Version; use std::fs; use std::path::{Path, PathBuf}; -/// Builds a eucalyptus project into a single bundle. -/// +/// 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"); @@ -53,6 +54,16 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> { project_name: config.project_name.clone(), runtime_settings: config.runtime_settings.clone(), scenes, + authors: config.authors.clone(), + editor_version: Version::parse(env!("CARGO_PKG_VERSION"))?, + project_version: Version::parse( + config + .project_version + .clone() + .unwrap_or(String::from("0.1.0")) + .as_str(), + )?, + initial_scene: config.runtime_settings.initial_scene.ok_or(anyhow::anyhow!("Project was expected to be an initial scene"))?, }; log::debug!("Converted to runtime project config"); @@ -61,7 +72,7 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> { 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"); @@ -89,13 +100,13 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> { Ok(()) } -/// Reads the contents of a data.eupak file into a pretty print format. -/// -/// Returns the contents of the project config. +/// 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) -} +} @@ -8,8 +8,8 @@ use dropbear_engine::graphics::NO_TEXTURE; use dropbear_engine::lighting::{LightComponent, LightType}; use dropbear_engine::utils::ResourceReference; use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui, UiBuilder}; -use eucalyptus_core::states::{Camera3D, Light, ModelProperties, Property, Script, Value}; use eucalyptus_core::camera::CameraType; +use eucalyptus_core::states::{Camera3D, Light, ModelProperties, Property, Script, Value}; use eucalyptus_core::{fatal, warn}; use glam::{DVec3, Vec3}; use hecs::Entity; @@ -646,17 +646,7 @@ impl InspectableComponent for Transform { _signal: &mut Signal, label: &mut String, ) { - inspect_transform( - self, - entity, - cfg, - ui, - undo_stack, - label, - true, - true, - true, - ); + inspect_transform(self, entity, cfg, ui, undo_stack, label, true, true, true); } } @@ -833,14 +823,15 @@ impl InspectableComponent for MeshRenderer { ui.horizontal(|ui| { ui.label("URI:"); let id = ui.make_persistent_id("mesh_renderer_uri_input"); - let mut uri_string = ui.data_mut(|d| d.get_temp::<String>(id).unwrap_or_default()); - + let mut uri_string = + ui.data_mut(|d| d.get_temp::<String>(id).unwrap_or_default()); + ui.text_edit_singleline(&mut uri_string); - + if ui.button("Load").clicked() { *signal = Signal::LoadModel(*entity, uri_string.clone()); } - + ui.data_mut(|d| d.insert_temp(id, uri_string)); }); @@ -1177,7 +1168,10 @@ impl InspectableComponent for Light { signal: &mut Signal, label: &mut String, ) { - let show_position = matches!(self.light_component.light_type, LightType::Point | LightType::Spot); + let show_position = matches!( + self.light_component.light_type, + LightType::Point | LightType::Spot + ); inspect_transform( &mut self.transform, @@ -1191,7 +1185,8 @@ impl InspectableComponent for Light { true, ); - self.light_component.inspect(entity, cfg, ui, undo_stack, signal, label); + self.light_component + .inspect(entity, cfg, ui, undo_stack, signal, label); } } @@ -1205,7 +1200,8 @@ impl InspectableComponent for Camera3D { signal: &mut Signal, label: &mut String, ) { - self.transform.inspect(entity, cfg, ui, undo_stack, signal, label); + self.transform + .inspect(entity, cfg, ui, undo_stack, signal, label); ui.vertical(|ui| { CollapsingHeader::new("Camera Settings") @@ -1216,9 +1212,21 @@ impl InspectableComponent for Camera3D { ComboBox::from_id_salt("camera_type") .selected_text(format!("{:?}", self.camera_type)) .show_ui(ui, |ui| { - ui.selectable_value(&mut self.camera_type, CameraType::Normal, "Normal"); - ui.selectable_value(&mut self.camera_type, CameraType::Debug, "Debug"); - ui.selectable_value(&mut self.camera_type, CameraType::Player, "Player"); + ui.selectable_value( + &mut self.camera_type, + CameraType::Normal, + "Normal", + ); + ui.selectable_value( + &mut self.camera_type, + CameraType::Debug, + "Debug", + ); + ui.selectable_value( + &mut self.camera_type, + CameraType::Player, + "Player", + ); }); }); @@ -1229,12 +1237,20 @@ impl InspectableComponent for Camera3D { ui.horizontal(|ui| { ui.label("Near Plane"); - ui.add(egui::DragValue::new(&mut self.near).speed(0.1).range(0.01..=1000.0)); + ui.add( + egui::DragValue::new(&mut self.near) + .speed(0.1) + .range(0.01..=1000.0), + ); }); ui.horizontal(|ui| { ui.label("Far Plane"); - ui.add(egui::DragValue::new(&mut self.far).speed(1.0).range(0.1..=10000.0)); + ui.add( + egui::DragValue::new(&mut self.far) + .speed(1.0) + .range(0.1..=10000.0), + ); }); ui.separator(); @@ -24,11 +24,9 @@ use dropbear_engine::{ use egui::{self, Margin, RichText}; use egui_dock::TabViewer; use egui_ltreeview::{NodeBuilder, TreeViewBuilder}; -use eucalyptus_core::states::{ - Label, Light, ModelProperties, PROJECT, Script, -}; -use eucalyptus_core::traits::registry::ComponentRegistry; use eucalyptus_core::hierarchy::{Children, Hierarchy, Parent}; +use eucalyptus_core::states::{Label, Light, ModelProperties, PROJECT, Script}; +use eucalyptus_core::traits::registry::ComponentRegistry; use hecs::{Entity, EntityBuilder, World}; use indexmap::Equivalent; use log; @@ -334,9 +332,21 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let parent_pos = parent_transform.position; let safe_parent_scale = glam::DVec3::new( - if parent_scale.x.abs() < 1e-6 { 1.0 } else { parent_scale.x }, - if parent_scale.y.abs() < 1e-6 { 1.0 } else { parent_scale.y }, - if parent_scale.z.abs() < 1e-6 { 1.0 } else { parent_scale.z }, + if parent_scale.x.abs() < 1e-6 { + 1.0 + } else { + parent_scale.x + }, + if parent_scale.y.abs() < 1e-6 { + 1.0 + } else { + parent_scale.y + }, + if parent_scale.z.abs() < 1e-6 { + 1.0 + } else { + parent_scale.z + }, ); let local_transform = entity_transform.local_mut(); @@ -468,25 +478,46 @@ impl<'a> TabViewer for EditorTabViewer<'a> { if name.contains("EntityTransform") { continue; } - let short_name = name.split("::").last().unwrap_or(name); - let display_name = if short_name == "SerializedMeshRenderer" { - "MeshRenderer" - } else { - short_name - }; + let short_name = + name.split("::").last().unwrap_or(name); + let display_name = + if short_name == "SerializedMeshRenderer" { + "MeshRenderer" + } else { + short_name + }; if ui.button(display_name).clicked() { if name.contains("MeshRenderer") { - *signal = Signal::AddComponent(entity, "MeshRenderer".to_string()); - } else if name.contains("CameraComponent") || name.contains("Camera3D") { - *signal = Signal::AddComponent(entity, "CameraComponent".to_string()); + *signal = Signal::AddComponent( + entity, + "MeshRenderer".to_string(), + ); + } else if name.contains("CameraComponent") + || name.contains("Camera3D") + { + *signal = Signal::AddComponent( + entity, + "CameraComponent".to_string(), + ); } else if name.contains("Light") { - *signal = Signal::AddComponent(entity, "Light".to_string()); + *signal = Signal::AddComponent( + entity, + "Light".to_string(), + ); } else { - if let Some(comp) = registry.create_default_component(id) { + if let Some(comp) = + registry.create_default_component(id) + { let mut builder = EntityBuilder::new(); - if let Ok(_) = registry.deserialize_into_builder(comp.as_ref(), &mut builder) { - let _ = world.insert(entity, builder.build()); + if let Ok(_) = registry + .deserialize_into_builder( + comp.as_ref(), + &mut builder, + ) + { + let _ = world + .insert(entity, builder.build()); } } } @@ -514,21 +545,27 @@ impl<'a> TabViewer for EditorTabViewer<'a> { }; let component_node_id = cfg.component_node_id(entity, component_type_id); - let display = format!("{} (id #{component_type_id})", component.display_name()); + let display = + format!("{} (id #{component_type_id})", component.display_name()); builder.node( NodeBuilder::leaf(component_node_id) .label(display) .context_menu(|ui| { if ui.button("Remove Component").clicked() { - registry.remove_component_by_id(world, entity, component_type_id); + registry.remove_component_by_id( + world, + entity, + component_type_id, + ); ui.close(); } }), ); } - let children_entities = if let Ok(children) = world.get::<&Children>(entity) { + let children_entities = if let Ok(children) = world.get::<&Children>(entity) + { children.children().to_vec() } else { Vec::new() @@ -550,7 +587,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> { Ok(()) } - let root_entities: Vec<Entity> = self.world.query::<()>().without::<&Parent>().iter().map(|(e, _)| e).collect(); + let root_entities: Vec<Entity> = self + .world + .query::<()>() + .without::<&Parent>() + .iter() + .map(|(e, _)| e) + .collect(); for entity in root_entities { if let Err(e) = add_entity_to_tree( @@ -714,15 +757,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } } - if let Ok(mut q) = self - .world - .query_one::<( - &mut Light, - Option<&mut Transform>, - Option<&mut EntityTransform>, - Option<&mut LightComponent>, - Option<&mut EngineLight>, - )>(*entity) + if let Ok(mut q) = self.world.query_one::<( + &mut Light, + Option<&mut Transform>, + Option<&mut EntityTransform>, + Option<&mut LightComponent>, + Option<&mut EngineLight>, + )>(*entity) && let Some(( light, mut transform_opt, @@ -746,7 +787,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> { &mut String::new(), ); - if let Some(entity_transform) = entity_transform_opt.as_deref_mut() { + if let Some(entity_transform) = entity_transform_opt.as_deref_mut() + { let parent_transform = entity_transform.world(); let parent_rot = parent_transform.rotation; let parent_scale = parent_transform.scale; @@ -757,9 +799,21 @@ impl<'a> TabViewer for EditorTabViewer<'a> { let new_world_scale = light.transform.scale; let safe_parent_scale = glam::DVec3::new( - if parent_scale.x.abs() < 1e-6 { 1.0 } else { parent_scale.x }, - if parent_scale.y.abs() < 1e-6 { 1.0 } else { parent_scale.y }, - if parent_scale.z.abs() < 1e-6 { 1.0 } else { parent_scale.z }, + if parent_scale.x.abs() < 1e-6 { + 1.0 + } else { + parent_scale.x + }, + if parent_scale.y.abs() < 1e-6 { + 1.0 + } else { + parent_scale.y + }, + if parent_scale.z.abs() < 1e-6 { + 1.0 + } else { + parent_scale.z + }, ); let local_transform = entity_transform.local_mut(); @@ -1129,7 +1183,11 @@ impl<'a> EditorTabViewer<'a> { source_path.display(), err ); - Self::add_placeholder_leaf(builder, &format!("{source_label}/unreadable"), "unreadable"); + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/unreadable"), + "unreadable", + ); return true; } }; @@ -1176,7 +1234,11 @@ impl<'a> EditorTabViewer<'a> { dir_path.display(), err ); - Self::add_placeholder_leaf(builder, &format!("{parent_label}/unreadable"), "unreadable"); + Self::add_placeholder_leaf( + builder, + &format!("{parent_label}/unreadable"), + "unreadable", + ); return; } }; @@ -1211,13 +1273,21 @@ impl<'a> EditorTabViewer<'a> { kotlin_path.display(), err ); - Self::add_placeholder_leaf(builder, &format!("{source_label}/unreadable"), "unreadable"); + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/unreadable"), + "unreadable", + ); return true; } }; if entries.is_empty() { - Self::add_placeholder_leaf(builder, &format!("{source_label}/no_kotlin_files"), "no kotlin files"); + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/no_kotlin_files"), + "no kotlin files", + ); return true; } @@ -1258,7 +1328,11 @@ impl<'a> EditorTabViewer<'a> { dir_path.display(), err ); - Self::add_placeholder_leaf(builder, &format!("{full_path_str}/unreadable"), "unreadable"); + Self::add_placeholder_leaf( + builder, + &format!("{full_path_str}/unreadable"), + "unreadable", + ); return; } }; @@ -1392,7 +1466,10 @@ impl<'a> EditorTabViewer<'a> { } fn with_icon<'ui>(builder: NodeBuilder<'ui, u64>) -> NodeBuilder<'ui, u64> { - builder.icon(|ui| {egui_extras::install_image_loaders(ui.ctx()); Self::draw_asset_icon(ui)}) + builder.icon(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + Self::draw_asset_icon(ui) + }) } fn draw_asset_icon(ui: &mut egui::Ui) { @@ -279,7 +279,7 @@ impl Mouse for Editor { if let Some(window) = &self.window { window.set_cursor_visible(false); if let Err(e) = window.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| { - log_once::warn_once!("Using cursor grab fallback: CursorGrabMode::Locked"); + log_once::warn_once!("Using cursor grab fallback: CursorGrabMode::Confined"); window.set_cursor_grab(CursorGrabMode::Confined) }) { log_once::error_once!("Unable to grab mouse: {}", e); @@ -40,8 +40,7 @@ use eucalyptus_core::{ scripting::{BuildStatus, ScriptManager, ScriptTarget}, states, states::{ - Camera3D, EditorTab, Light, ModelProperties, PROJECT, SCENES, Script, - WorldLoadingStatus, + Camera3D, EditorTab, Light, ModelProperties, PROJECT, SCENES, Script, WorldLoadingStatus, }, success, success_without_console, utils::ViewportMode, @@ -77,7 +76,7 @@ pub struct Editor { pub light_manager: LightManager, pub color: Color, - pub active_camera: Arc<Mutex<Option<hecs::Entity>>>, + pub active_camera: Arc<Mutex<Option<Entity>>>, pub is_viewport_focused: bool, // is_cursor_locked: bool, @@ -1106,14 +1105,14 @@ impl Editor { ui.add_space(12.0); - ui.label("Made with love by 4tkbytes ♥️"); + ui.label("Made with love by tirbofish ♥️"); ui.add_space(12.0); ui.horizontal(|ui| { ui.label("Check out the repository at"); - if ui.label("https://github.com/4tkbytes/dropbear").clicked() { - let _ = open::that("https://github.com/4tkbytes/dropbear"); + if ui.label("https://github.com/tirbofish/dropbear").clicked() { + let _ = open::that("https://github.com/tirbofish/dropbear"); } }); @@ -10,9 +10,9 @@ use dropbear_engine::{ model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; +use eucalyptus_core::hierarchy::EntityTransformExt; use eucalyptus_core::logging; use eucalyptus_core::states::{Label, WorldLoadingStatus}; -use eucalyptus_core::hierarchy::{EntityTransformExt}; use eucalyptus_core::window::poll; use log; use parking_lot::Mutex; @@ -297,7 +297,10 @@ impl Scene for Editor { } for (entity, final_transform) in updates { - if let Ok(mut q) = self.world.query_one::<(&mut LightComponent, &mut Light)>(entity) { + if let Ok(mut q) = self + .world + .query_one::<(&mut LightComponent, &mut Light)>(entity) + { if let Some((light_component, light)) = q.get() { light.update(light_component, &final_transform); } @@ -1,5 +1,5 @@ // #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -// note to self: when it becomes release, remember to readd this back +// note to self: when it becomes release, remember to re-add this back use clap::{Arg, Command}; use dropbear_engine::future::FutureQueue; @@ -115,14 +115,12 @@ async fn main() -> anyhow::Result<()> { ), ) .subcommand( - Command::new("read") - .about("Reads a .eupak file") - .arg( - Arg::new("eupak_file") - .help("Path to the .eupak data file") - .value_name("EUPAK_FILE") - .required(true) - ), + Command::new("read").about("Reads a .eupak file").arg( + Arg::new("eupak_file") + .help("Path to the .eupak data file") + .value_name("EUPAK_FILE") + .required(true), + ), ) .get_matches(); @@ -144,7 +142,10 @@ async fn main() -> anyhow::Result<()> { Some(("read", sub_matches)) => { let eupak = match sub_matches.get_one::<String>("eupak_file") { Some(path) => PathBuf::from(path), - None => {log::error!("Eupak file returned none"); std::process::exit(1)}, + None => { + log::error!("Eupak file returned none"); + std::process::exit(1) + } }; build::read(eupak)?; @@ -157,7 +157,7 @@ impl MainMenu { } "gradle" => { log::debug!("Cloning gradle template from GitHub"); - let url = "https://github.com/4tkbytes/eucalyptus-gradle-template"; + let url = "https://github.com/tirbofish/eucalyptus-gradle-template"; fs::create_dir_all(path) .context("Failed to create project directory")?; @@ -495,10 +495,7 @@ impl SignalController for Editor { "typeid of CameraComponent: {:?}", TypeId::of::<CameraComponent>() ); - log::info!( - "typeid of Script: {:?}", - TypeId::of::<Script>() - ); + log::info!("typeid of Script: {:?}", TypeId::of::<Script>()); log::info!("typeid of EngineLight: {:?}", TypeId::of::<EngineLight>()); log::info!( "typeid of LightComponent: {:?}", @@ -633,8 +630,9 @@ impl SignalController for Editor { .await?; let model = loaded_model.make_mut(); - model.path = - ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + model.path = ResourceReference::from_euca_uri( + "euca://internal/dropbear/models/cube", + )?; loaded_model.refresh_registry(); @@ -665,8 +663,9 @@ impl SignalController for Editor { light_comp.clone(), transform, Some("New Light"), - ).await; - + ) + .await; + let light_config = Light { label: "New Light".to_string(), transform, @@ -675,13 +674,21 @@ impl SignalController for Editor { entity_id: None, }; - Ok::<(LightComponent, EngineLight, Light, Transform), anyhow::Error>((light_comp, engine_light, light_config, transform)) + Ok::<(LightComponent, EngineLight, Light, Transform), anyhow::Error>(( + light_comp, + engine_light, + light_config, + transform, + )) }; let handle = graphics.future_queue.push(Box::pin(future)); self.pending_components.push((*entity, handle)); success!("Queued Light addition for entity {:?}", entity); } else { - warn!("Unknown component type for AddComponent signal: {}", component_name); + warn!( + "Unknown component type for AddComponent signal: {}", + component_name + ); } self.signal = Signal::None; Ok(()) @@ -727,7 +734,7 @@ impl SignalController for Editor { ) } }; - + let handle = graphics.future_queue.push(Box::pin(future)); self.pending_components.push((*entity, handle)); success!("Queued model load for entity {:?} from '{}'", entity, uri); @@ -10,7 +10,9 @@ 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}; -use eucalyptus_core::states::{Light as LightConfig, ModelProperties, Script, SerializedMeshRenderer}; +use eucalyptus_core::states::{ + Light as LightConfig, ModelProperties, Script, SerializedMeshRenderer, +}; use eucalyptus_core::utils::ResolveReference; use eucalyptus_core::{fatal, success}; use hecs::EntityBuilder; @@ -144,10 +146,23 @@ impl PendingSpawnController for Editor { } } Err(result) => { - if let Ok(r) = result.downcast::<anyhow::Result<(LightComponent, Light, LightConfig, Transform)>>() { + if let Ok(r) = result.downcast::<anyhow::Result<( + LightComponent, + Light, + LightConfig, + Transform, + )>>() { match Arc::try_unwrap(r) { - Ok(Ok((light_comp, engine_light, light_config, transform))) => { - let _ = self.world.insert(*entity, (light_comp, engine_light, light_config, transform)); + Ok(Ok(( + light_comp, + engine_light, + light_config, + transform, + ))) => { + let _ = self.world.insert( + *entity, + (light_comp, engine_light, light_config, transform), + ); success!("Added Light to entity {:?}", entity); completed_components.push(index); } @@ -228,7 +243,8 @@ async fn load_renderer_from_serialized( .await?; let model = loaded_model.make_mut(); - model.path = ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; + model.path = + ResourceReference::from_euca_uri("euca://internal/dropbear/models/cube")?; loaded_model.refresh_registry(); @@ -1,5 +1,5 @@ /** - * dropbear-engine native header definitions. Created by 4tkbytes as part of the dropbear project. + * dropbear-engine native header definitions. Created by tirbofish as part of the dropbear project. * * Primarily used for Kotlin/Native, however nothing is stopping you from implementing it to your own language. * Exports are located at `eucalyptus_core::scripting::native::exports`. @@ -19,8 +19,8 @@ dependencies { } gradlePlugin { - website.set("https://github.com/4tkbytes/dropbear") - vcsUrl.set("https://github.com/4tkbytes/dropbear") + website.set("https://github.com/tirbofish/dropbear") + vcsUrl.set("https://github.com/tirbofish/dropbear") plugins { create("magnaCartaPlugin") { @@ -40,7 +40,7 @@ publishing { pom { name.set("magna-carta") description.set("Gradle plugin for manifest generation for the Dropbear engine") - url.set("https://4tkbytes.github.io/dropbear/") + url.set("https://tirbofish.github.io/dropbear/") licenses { license { @@ -50,15 +50,15 @@ publishing { } developers { developer { - id.set("4tkbytes") - name.set("4tkbytes") - email.set("4tkbytes@pm.me") + id.set("tirbofish") + name.set("tirbofish") + email.set("tirbofish@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") + connection.set("scm:git:git://github.com/tirbofish/dropbear.git") + developerConnection.set("scm:git:ssh://github.com/tirbofish/dropbear.git") + url.set("https://github.com/tirbofish/dropbear") } } } @@ -30,23 +30,23 @@ abstract class DownloadMagnaCartaToolTask: Download() { val (fileName, url) = when { os.isLinux && arch == "amd64" -> arrayOf( "magna-carta-linux-x64", - "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-linux-x64", + "https://github.com/tirbofish/dropbear/releases/download/${toolVersion.get()}/magna-carta-linux-x64", ) os.isMacOsX && arch == "aarch64" -> arrayOf( "magna-carta-macos-arm64", - "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-macos-arm64", + "https://github.com/tirbofish/dropbear/releases/download/${toolVersion.get()}/magna-carta-macos-arm64", ) os.isMacOsX && (arch == "x86_64" || arch == "amd64") -> arrayOf( "magna-carta-macos-x64", - "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-macos-x64", + "https://github.com/tirbofish/dropbear/releases/download/${toolVersion.get()}/magna-carta-macos-x64", ) os.isWindows && arch == "aarch64" -> arrayOf( "magna-carta-windows-arm64.exe", - "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-windows-arm64.exe", + "https://github.com/tirbofish/dropbear/releases/download/${toolVersion.get()}/magna-carta-windows-arm64.exe", ) os.isWindows && (arch == "x86_64" || arch == "amd64") -> arrayOf( "magna-carta-windows-x64.exe", - "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-windows-x64.exe", + "https://github.com/tirbofish/dropbear/releases/download/${toolVersion.get()}/magna-carta-windows-x64.exe", ) else -> throw GradleException("Unsupported OS/arch: $os / $arch") } @@ -11,6 +11,14 @@ dropbear-engine = { path = "../dropbear-engine" } eucalyptus-core = { path = "../eucalyptus-core" } anyhow.workspace = true -log = "0.4.27" +log.workspace = true app_dirs2.workspace = true tokio.workspace = true +chrono.workspace = true +env_logger.workspace = true +colored.workspace = true +parking_lot.workspace = true +bincode.workspace = true +hecs.workspace = true +bytemuck.workspace = true +log-once.workspace = true @@ -0,0 +1,78 @@ +use crate::scene::RuntimeScene; +use dropbear_engine::gilrs::{Button, GamepadId}; +use dropbear_engine::input::{Controller, Keyboard, Mouse}; +use dropbear_engine::winit::dpi::PhysicalPosition; +use dropbear_engine::winit::event::MouseButton; +use dropbear_engine::winit::event_loop::ActiveEventLoop; +use dropbear_engine::winit::keyboard::KeyCode; + +impl Keyboard for RuntimeScene { + fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { + self.input_state.pressed_keys.insert(key); + } + + fn key_up(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { + self.input_state.pressed_keys.remove(&key); + } +} + +impl Mouse for RuntimeScene { + fn mouse_move(&mut self, position: PhysicalPosition<f64>, delta: Option<(f64, f64)>) { + let delta = if delta.is_none() { + if let Some(last_pos) = self.input_state.last_mouse_pos { + Some((last_pos.0 - position.x, last_pos.1 - position.y)) + } else { + None + } + } else { + delta + }; + + self.input_state.mouse_delta = delta; + self.input_state.mouse_pos = (position.x, position.y); + self.input_state.last_mouse_pos = Some(<(f64, f64)>::from(position)); + } + + fn mouse_down(&mut self, button: MouseButton) { + self.input_state.mouse_button.insert(button); + } + + fn mouse_up(&mut self, button: MouseButton) { + self.input_state.mouse_button.remove(&button); + } +} + +impl Controller for RuntimeScene { + fn button_down(&mut self, button: Button, id: GamepadId) { + self.input_state + .pressed_buttons + .entry(id) + .or_default() + .insert(button); + } + + fn button_up(&mut self, button: Button, id: GamepadId) { + if let Some(buttons) = self.input_state.pressed_buttons.get_mut(&id) { + buttons.remove(&button); + } + } + + fn left_stick_changed(&mut self, x: f32, y: f32, id: GamepadId) { + self.input_state.left_stick_position.insert(id, (x, y)); + } + + fn right_stick_changed(&mut self, x: f32, y: f32, id: GamepadId) { + self.input_state.right_stick_position.insert(id, (x, y)); + } + + fn on_connect(&mut self, id: GamepadId) { + self.input_state.connected_gamepads.insert(id); + } + + fn on_disconnect(&mut self, id: GamepadId) { + self.input_state.connected_gamepads.remove(&id); + self.input_state.pressed_buttons.remove(&id); + self.input_state.left_stick_position.remove(&id); + self.input_state.right_stick_position.remove(&id); + } +} @@ -1,4 +1,191 @@ +mod input; +mod scene; + +use crate::scene::RuntimeScene; +use app_dirs2::AppInfo; +use dropbear_engine::future::FutureQueue; +use dropbear_engine::{MutableWindowConfiguration, WindowConfiguration, WindowedModes}; +use eucalyptus_core::runtime::RuntimeProjectConfig; +use eucalyptus_core::states::ConfigFile; +use parking_lot::RwLock; +use std::env::current_exe; +use std::fs; +use std::rc::Rc; +use std::sync::Arc; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { + #[cfg(not(target_os = "android"))] + { + use chrono::offset::Local; + use colored::Colorize; + use env_logger::Builder; + use log::LevelFilter; + use parking_lot::Mutex; + 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 = Local::now().format("%Y-%m-%d_%H-%M-%S"); + let log_filename = format!("{}.{}.log", "redback-runtime", 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 = Mutex::new(file); + + Builder::new() + .format(move |buf, record| { + use std::io::Write; + + let ts = 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(Some("dropbear_engine"), LevelFilter::Trace) + .filter( + Some("redback-runtime".replace('-', "_").as_str()), + LevelFilter::Debug, + ) + .filter(Some("eucalyptus_core"), LevelFilter::Debug) + .filter(Some("dropbear_traits"), LevelFilter::Debug) + .init(); + log::info!("Initialised logger"); + } + + dropbear_engine::panic::set_hook(); + log::debug!("Set panic hook"); + + let window_config_file = current_exe()? + .parent() + .ok_or(anyhow::anyhow!( + "Unable to get parent of current executable" + ))? + .join("config.eucfg"); + + let value = fs::read(&window_config_file); + + let config = match value { + Ok(val) => { + let (config, _): (ConfigFile, usize) = + bincode::decode_from_slice(val.as_slice(), bincode::config::standard())?; + config + } + Err(e) => { + log::warn!("Unable to read config: {}", e); + log::warn!("Creating new config file to overwrite old one"); + let window_configuration = MutableWindowConfiguration { + max_fps: u32::MAX, + windowed_mode: WindowedModes::Maximised, + }; + let cfg = ConfigFile { + jvm_args: None, + window_configuration, + }; + let vec = bincode::encode_to_vec(cfg.clone(), bincode::config::standard())?; + if let Err(e) = fs::write(&window_config_file, vec) { + log::warn!("Unable to write, still running game: {}", e); + } + cfg + } + }; + + let scene_config = fs::read( + current_exe()? + .parent() + .ok_or(anyhow::anyhow!( + "Unable to locate parent folder for current executable" + ))? + .join("data.eupak"), + )?; + let (scene_config, _): (RuntimeProjectConfig, usize) = + bincode::decode_from_slice(scene_config.as_slice(), bincode::config::standard())?; + + let runtime_scene = Rc::new(RwLock::new(RuntimeScene::new(scene_config.clone())?)); + let future_queue = Arc::new(FutureQueue::new()); + + let name = current_exe()? + .file_stem() + .ok_or(anyhow::anyhow!("Unable to locate file name of current exe"))? + .to_str() + .ok_or(anyhow::anyhow!("Unable to convert file name to string"))? + .to_string(); + + let authors = scene_config.authors.developer.clone(); + let project_name = scene_config.project_name.clone(); + + let win_cfg = WindowConfiguration { + title: name, + window_config: config.window_configuration, + app_info: AppInfo { + name: Box::leak(project_name.into_boxed_str()), + author: Box::leak(authors.into_boxed_str()), + }, + }; + + dropbear_engine::run_app!( + win_cfg, + Some(future_queue), + |mut scene_mgr, mut input_mgr| { + dropbear_engine::scene::add_scene_with_input( + &mut scene_mgr, + &mut input_mgr, + runtime_scene, + "runtime_scene", + ); + + scene_mgr.switch("runtime_scene"); + + (scene_mgr, input_mgr) + } + ) + .await?; + Ok(()) } @@ -0,0 +1,645 @@ +use std::collections::HashMap; +use std::env; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use dropbear_engine::future::FutureHandle; +use dropbear_engine::graphics::{InstanceRaw, RenderContext}; +use dropbear_engine::lighting::{Light, LightComponent, LightManager}; +use dropbear_engine::model::{DrawLight, DrawModel, MODEL_CACHE, ModelId}; +use dropbear_engine::scene::{Scene, SceneCommand}; +use dropbear_engine::shader::{self, Shader}; +use dropbear_engine::wgpu::util::DeviceExt; +use dropbear_engine::wgpu::{self, Color, RenderPipeline}; +use dropbear_engine::winit::event_loop::ActiveEventLoop; +use dropbear_engine::winit::window::Window; +use eucalyptus_core::camera::CameraComponent; +use eucalyptus_core::hierarchy::EntityTransformExt; +use eucalyptus_core::input::InputState; +use eucalyptus_core::ptr::{GraphicsPtr, InputStatePtr, WorldPtr}; +use eucalyptus_core::runtime::RuntimeProjectConfig; +use eucalyptus_core::scene::SceneConfig; +use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; +use eucalyptus_core::states::Script; +use eucalyptus_core::traits::registry::ComponentRegistry; +use eucalyptus_core::window::{self, GRAPHICS_COMMAND}; +use hecs::{Entity, World}; +use parking_lot::Mutex; +use tokio::sync::oneshot; +use tokio::sync::oneshot::error::TryRecvError; + +/// The scene that the redback-runtime uses. +pub(crate) struct RuntimeScene { + #[allow(dead_code)] + project_config: RuntimeProjectConfig, + scenes: HashMap<String, SceneConfig>, + initial_scene: String, + + pub world: Box<World>, + pub input_state: Box<InputState>, + pub active_camera: Arc<Mutex<Option<Entity>>>, + render_pipeline: Option<RenderPipeline>, + light_manager: LightManager, + component_registry: Arc<ComponentRegistry>, + script_manager: ScriptManager, + script_target: Option<ScriptTarget>, + scripts_ready: bool, + scene_command: SceneCommand, + + current_scene: Option<String>, + pending_scene: Option<PendingSceneLoad>, + pub window: Option<Arc<Window>>, +} + +struct PendingSceneLoad { + name: String, + receiver: oneshot::Receiver<anyhow::Result<LoadedScene>>, + handle: FutureHandle, +} + +struct LoadedScene { + world: World, + active_camera: Entity, +} + +impl RuntimeScene { + /// Creates a new instance of [`RuntimeScene`] + pub fn new(project_config: RuntimeProjectConfig) -> anyhow::Result<Self> { + // checks for any deadlocks in another thread. + std::thread::spawn(move || { + loop { + std::thread::sleep(Duration::from_secs(1)); + let deadlocks = parking_lot::deadlock::check_deadlock(); + if deadlocks.is_empty() { + continue; + } + + for (i, threads) in deadlocks.iter().enumerate() { + log::error!("Deadlock #{}", i); + for t in threads { + log::error!("Thread Id {:#?}", t.thread_id()); + log::error!("{:#?}", t.backtrace()); + } + } + panic!( + "Fatal: {} deadlocks detected, unable to continue on normal process", + deadlocks.len() + ); + } + }); + + let initial_scene = project_config.initial_scene.clone(); + + let scenes = project_config + .scenes + .iter() + .map(|scene| (scene.scene_name.clone(), scene.clone())) + .collect::<HashMap<_, _>>(); + + let result = Self { + project_config: project_config.clone(), + scenes, + initial_scene, + world: Box::new(World::new()), + input_state: Box::new(InputState::new()), + active_camera: Arc::new(Mutex::new(None)), + render_pipeline: None, + light_manager: LightManager::new(), + current_scene: None, + component_registry: Arc::new(ComponentRegistry::new()), + script_manager: ScriptManager::new()?, + script_target: None, + scripts_ready: false, + scene_command: Default::default(), + pending_scene: None, + window: None, + }; + + Ok(result) + } + + fn start_scene_load( + &mut self, + scene_name: &str, + graphics: &mut RenderContext, + ) -> anyhow::Result<()> { + if self.scenes.is_empty() { + anyhow::bail!("No scenes available in this runtime build"); + } + + if let Some(pending) = self.pending_scene.take() { + graphics.shared.future_queue.cancel(&pending.handle); + } + + self.world.clear(); + self.render_pipeline = None; + self.light_manager = LightManager::new(); + self.scripts_ready = false; + self.script_target = None; + *self.active_camera.lock() = None; + + let scene = self + .scenes + .get(scene_name) + .cloned() + .ok_or_else(|| anyhow::anyhow!("Scene '{}' does not exist", scene_name))?; + + let graphics_shared = graphics.shared.clone(); + let component_registry = self.component_registry.clone(); + let (tx, rx) = oneshot::channel(); + + let handle = graphics.shared.future_queue.push(async move { + let mut temp_world = World::new(); + let result = scene + .load_into_world( + &mut temp_world, + graphics_shared, + Some(component_registry.as_ref()), + None, + ) + .await + .map(|active_camera| LoadedScene { + world: temp_world, + active_camera, + }); + + if tx.send(result).is_err() { + log::warn!("Scene load result receiver dropped before completion"); + } + }); + + self.pending_scene = Some(PendingSceneLoad { + name: scene_name.to_string(), + receiver: rx, + handle, + }); + + Ok(()) + } + + fn poll_pending_scene(&mut self, graphics: &mut RenderContext) { + let Some(mut pending) = self.pending_scene.take() else { + return; + }; + + match pending.receiver.try_recv() { + Ok(result) => { + let _ = graphics + .shared + .future_queue + .exchange_owned_as::<()>(&pending.handle); + + match result { + Ok(loaded) => { + self.on_scene_loaded(loaded, pending.name, graphics); + } + Err(err) => { + log::error!("Failed to load scene: {err:?}"); + self.scene_command = SceneCommand::Quit; + } + } + } + Err(TryRecvError::Empty) => { + 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; + } + } + } + + fn on_scene_loaded( + &mut self, + loaded: LoadedScene, + scene_name: String, + graphics: &mut RenderContext, + ) { + self.world = Box::new(loaded.world); + *self.active_camera.lock() = Some(loaded.active_camera); + self.current_scene = Some(scene_name); + self.pending_scene = None; + self.window = Some(graphics.shared.window.clone()); + self.input_state.window = Some(graphics.shared.window.clone()); + + if let Err(err) = self.prepare_render_resources(graphics) { + log::error!("Failed to prepare render pipeline: {err:?}"); + } + + if let Err(err) = self.initialise_scripts() { + log::warn!("Unable to initialise scripts: {err:?}"); + } + } + + fn prepare_render_resources(&mut self, graphics: &mut RenderContext) -> anyhow::Result<()> { + self.light_manager + .create_light_array_resources(graphics.shared.clone()); + + let active_camera = self + .active_camera + .lock() + .context("Active camera was not set")?; + + let mut query = self + .world + .query_one::<&Camera>(active_camera) + .map_err(|_| anyhow::anyhow!("Unable to query active camera"))?; + + let camera = query + .get() + .ok_or_else(|| anyhow::anyhow!("Camera component missing on active entity"))?; + + let shader = Shader::new( + graphics.shared.clone(), + shader::shader_wesl::SHADER_SHADER, + Some("runtime_default"), + ); + + let pipeline = graphics.create_render_pipline( + &shader, + vec![ + graphics.shared.texture_bind_layout.as_ref(), + camera.layout(), + self.light_manager.layout(), + ], + Some("Runtime Scene Pipeline"), + ); + + self.render_pipeline = Some(pipeline); + + self.light_manager.create_render_pipeline( + graphics.shared.clone(), + shader::shader_wesl::LIGHT_SHADER, + camera, + Some("Runtime Light Pipeline"), + ); + + Ok(()) + } + + fn initialise_scripts(&mut self) -> anyhow::Result<()> { + let mut tag_database: HashMap<String, Vec<Entity>> = HashMap::new(); + + for (entity, script) in self.world.query::<&Script>().iter() { + for tag in &script.tags { + tag_database.entry(tag.clone()).or_default().push(entity); + } + } + + if tag_database.is_empty() { + self.scripts_ready = false; + return Ok(()); + } + + let Some(target) = self.detect_script_target()? else { + log::warn!( + "Script components detected but no script artifact found next to the runtime" + ); + self.scripts_ready = false; + return Ok(()); + }; + + self.script_manager + .init_script(tag_database, target.clone())?; + + let world_ptr = self.world.as_mut() as WorldPtr; + let input_ptr = self.input_state.as_mut() as InputStatePtr; + let graphics_ptr = GRAPHICS_COMMAND.0.as_ref() as GraphicsPtr; + + self.script_manager + .load_script(world_ptr, input_ptr, graphics_ptr)?; + + self.script_target = Some(target); + self.scripts_ready = true; + + Ok(()) + } + + fn detect_script_target(&self) -> anyhow::Result<Option<ScriptTarget>> { + if let Ok(path) = env::var("DROPBEAR_SCRIPT_PATH") { + let path = PathBuf::from(path); + if Self::is_native_library(&path) { + return Ok(Some(ScriptTarget::Native { library_path: path })); + } + if Self::is_jvm_artifact(&path) { + return Ok(Some(ScriptTarget::JVM { library_path: path })); + } + } + + let exe_dir = env::current_exe() + .context("Unable to locate runtime executable path")? + .parent() + .ok_or_else(|| anyhow::anyhow!("Executable has no parent directory"))? + .to_path_buf(); + + let search_dirs = [exe_dir.join("libs"), exe_dir.clone()]; + let mut native_candidates = Vec::new(); + let mut jar_candidates = Vec::new(); + + for dir in search_dirs { + if !dir.exists() { + continue; + } + if let Ok(entries) = std::fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_file() { + continue; + } + if Self::is_native_library(&path) { + native_candidates.push(path); + } else if Self::is_jvm_artifact(&path) { + jar_candidates.push(path); + } + } + } + } + + if let Some(native) = Self::pick_preferred(native_candidates) { + return Ok(Some(ScriptTarget::Native { + library_path: native, + })); + } + + if let Some(jar) = Self::pick_preferred(jar_candidates) { + return Ok(Some(ScriptTarget::JVM { library_path: jar })); + } + + Ok(None) + } + + fn is_native_library(path: &PathBuf) -> bool { + let ext = path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + + match env::consts::OS { + "windows" => ext == "dll", + "macos" => ext == "dylib", + _ => ext == "so", + } + } + + fn is_jvm_artifact(path: &PathBuf) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("jar")) + .unwrap_or(false) + } + + fn pick_preferred(mut candidates: Vec<PathBuf>) -> Option<PathBuf> { + if candidates.is_empty() { + return None; + } + + candidates.sort_by(|a, b| { + let priority_a = !Self::is_preferred_name(a); + let priority_b = !Self::is_preferred_name(b); + priority_a.cmp(&priority_b).then_with(|| { + b.metadata() + .and_then(|m| m.modified()) + .ok() + .cmp(&a.metadata().and_then(|m| m.modified()).ok()) + }) + }); + + candidates.into_iter().next() + } + + fn is_preferred_name(path: &PathBuf) -> bool { + path.file_stem() + .and_then(|stem| stem.to_str()) + .map(|name| { + let lower = name.to_ascii_lowercase(); + lower.contains("dropbear") || lower.contains("native") + }) + .unwrap_or(false) + } + + fn update_cameras(&mut self, graphics: &mut RenderContext) { + let (width, height) = graphics.frame.screen_size; + if height <= 0.0 { + return; + } + let aspect = f64::from(width / height); + + for (_, (camera, component)) in self + .world + .query::<(&mut Camera, &mut CameraComponent)>() + .iter() + { + camera.aspect = aspect; + component.update(camera); + camera.update(graphics.shared.clone()); + } + } + + fn update_render_transforms(&mut self) { + for (_, (renderer, transform)) in + self.world.query::<(&mut MeshRenderer, &Transform)>().iter() + { + renderer.update(transform); + } + + let mut propagated = Vec::new(); + for (entity, entity_transform) in self.world.query::<&EntityTransform>().iter() { + let final_transform = entity_transform.propagate(&self.world, entity); + propagated.push((entity, final_transform)); + } + + for (entity, final_transform) in propagated { + if let Ok(mut query) = self.world.query_one::<&mut MeshRenderer>(entity) { + if let Some(renderer) = query.get() { + renderer.update(&final_transform); + } + } + + if let Ok(mut query) = self + .world + .query_one::<(&mut LightComponent, &mut Light)>(entity) + { + if let Some((component, light)) = query.get() { + light.update(component, &final_transform); + } + } + } + } + + fn update_lights(&mut self, graphics: &mut RenderContext) { + self.light_manager + .update(graphics.shared.clone(), &self.world); + } +} + +impl Scene for RuntimeScene { + fn load(&mut self, graphics: &mut RenderContext) { + self.window = Some(graphics.shared.window.clone()); + self.input_state.window = Some(graphics.shared.window.clone()); + + let target_scene = self + .current_scene + .clone() + .or_else(|| Some(self.initial_scene.clone())) + .or_else(|| self.scenes.keys().next().cloned()); + + if let Some(scene_name) = target_scene { + if let Err(err) = self.start_scene_load(&scene_name, graphics) { + log::error!("Failed to start scene load: {err:?}"); + self.scene_command = SceneCommand::Quit; + } + } else { + log::error!("Unable to determine a scene to load"); + self.scene_command = SceneCommand::Quit; + } + } + + fn update(&mut self, dt: f32, graphics: &mut RenderContext) { + self.window = Some(graphics.shared.window.clone()); + self.input_state.window = Some(graphics.shared.window.clone()); + + self.poll_pending_scene(graphics); + + if self.render_pipeline.is_none() { + return; + } + + window::poll(graphics.shared.window.clone()); + + if self.scripts_ready { + let world_ptr = self.world.as_mut() as WorldPtr; + if let Err(err) = unsafe { + self.script_manager + .update_script(world_ptr, &self.input_state, dt) + } { + log::error!("Script runtime error: {err:?}"); + } + } + + self.update_cameras(graphics); + self.update_render_transforms(); + self.update_lights(graphics); + } + + fn render(&mut self, graphics: &mut RenderContext) { + let Some(pipeline) = &self.render_pipeline else { + return; + }; + + let color = Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }; + + let Some(active_camera) = self.active_camera.lock().clone() else { + log::warn!("No active camera available for rendering"); + return; + }; + + let camera = match self + .world + .query_one::<&Camera>(active_camera) + .and_then(|mut q| Ok(q.get().cloned())) + { + Ok(Some(camera)) => camera, + _ => { + log::warn!("Unable to fetch camera component for active camera entity"); + return; + } + }; + + self.window = Some(graphics.shared.window.clone()); + + let lights = self.world.query::<(&Light, &LightComponent)>().iter().map(|(_, (light, component))| (light.clone(), component.clone())).collect::<Vec<_>>(); + + { + let mut render_pass = graphics.clear_colour(color); + if let Some(light_pipeline) = &self.light_manager.pipeline { + render_pass.set_pipeline(light_pipeline); + for (light, component) in &lights + { + if !component.enabled { + continue; + } + if let Some(instance_buffer) = &light.instance_buffer { + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + } + render_pass.draw_light_model( + light.model(), + camera.bind_group(), + light.bind_group(), + ); + } + } + } + + let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); + + for (_, renderer) in self.world.query::<&MeshRenderer>().iter() { + model_batches + .entry(renderer.model_id()) + .or_default() + .push(renderer.instance.to_raw()); + } + + for (model_id, instances) in model_batches { + let model = { + let cache = MODEL_CACHE.lock(); + cache.values().find(|model| model.id == model_id).cloned() + }; + + let Some(model) = model else { + log::warn!("Unable to locate model with id {:?}", model_id); + continue; + }; + + let instance_buffer = + graphics + .shared + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Runtime Instance Buffer"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX, + }); + + let mut render_pass = graphics.continue_pass(); + render_pass.set_pipeline(pipeline); + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.draw_model_instanced( + &model, + 0..instances.len() as u32, + camera.bind_group(), + self.light_manager.bind_group(), + ); + } + } + + fn exit(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + self.scene_command = SceneCommand::None; + if let Some(window) = &self.window { + window.set_cursor_visible(true); + } + self.world.clear(); + self.render_pipeline = None; + self.scripts_ready = false; + self.script_target = None; + self.pending_scene = None; + } + + fn run_command(&mut self) -> SceneCommand { + std::mem::replace(&mut self.scene_command, SceneCommand::None) + } +}