tirbofish/dropbear · diff
feature: rigidbodies as a component
Signature present but could not be verified.
Unverified
@@ -30,7 +30,7 @@ colored = "3.0" crossbeam-channel = "0.5" dropbear-engine = { path = "dropbear-engine" } egui = "0.33" -egui-toast = { git = "https://github.com/LazerSharkk/egui-toast" } +egui-toast = { version = "0.19" } egui-wgpu = { version = "0.33" } egui-winit = { version = "0.33" } egui_dnd = "0.14" @@ -40,7 +40,7 @@ env_logger = "0.11" futures = "0.3" gilrs = "0.11" git2 = { version = "0.20", features = ["vendored-openssl"] } -glam = { version = "0.30", features = ["serde"] } +glam = { version = "0.30", features = ["serde", "mint"] } hecs = { version = "0.10", features = ["serde"] } log = "0.4" log-once = "0.4" @@ -51,7 +51,7 @@ rfd = "0.15" ron = "0.11" serde = { version = "1.0", features = ["derive"] } spin_sleep = "1.3" -transform-gizmo-egui = { git = "https://github.com/kisya-games/transform-gizmo", branch = "egui-0.33" } +transform-gizmo-egui = { version = "0.8" } tokio = { version = "1", features = ["full"] } wgpu = "27" winit = { version = "0.30", features = [] } @@ -79,7 +79,7 @@ quote = "1.0" egui_ltreeview = { version = "0.6", features = ["doc"] } dyn-hash = "1.0" semver = { version = "1.0", features = ["serde"] } -rapier3d = { version = "0.31", features = [ "simd-stable" ] } +rapier3d = { version = "0.31", features = [ "simd-stable", "serde-serialize" ] } [workspace.dependencies.image] version = "0.25" @@ -87,7 +87,7 @@ default-features = false features = ["png"] [profile.dev] -opt-level = 1 +opt-level = 3 debug = true codegen-units = 16 incremental = true @@ -96,3 +96,4 @@ lto = false # makes the debug builds so much more faster [profile.dev.package."*"] opt-level = 3 +codegen-units = 1 @@ -10,14 +10,8 @@ use glam::{DMat4, DQuat, DVec3, Mat3}; use image::GenericImageView; use parking_lot::Mutex; use std::{fs, path::PathBuf, sync::Arc, time::Instant}; -use wgpu::{ - BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder, - CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, Queue, RenderPass, - RenderPassDepthStencilAttachment, RenderPipeline, Sampler, StencilState, SurfaceConfiguration, - TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, - VertexBufferLayout, - util::{BufferInitDescriptor, DeviceExt}, -}; +use wgpu::*; +use wgpu::util::*; use winit::window::Window; pub const NO_TEXTURE: &[u8] = include_bytes!("../../resources/textures/no-texture.png"); @@ -31,6 +25,8 @@ pub struct RenderContext<'a> { pub struct SharedGraphicsContext { pub device: Arc<Device>, pub queue: Arc<Queue>, + pub surface: Arc<Surface<'static>>, + pub surface_format: TextureFormat, pub instance: Arc<wgpu::Instance>, pub texture_bind_layout: Arc<BindGroupLayout>, pub window: Arc<Window>, @@ -112,6 +108,8 @@ impl<'a> RenderContext<'a> { diffuse_sampler, screen_size, texture_id: state.texture_id.clone(), + surface: state.surface.clone(), + surface_format: state.surface_format, }), frame: FrameGraphicsContext { encoder, @@ -38,10 +38,7 @@ use std::sync::OnceLock; use std::{fs, sync::Arc, time::{Duration, Instant}}; use std::collections::HashMap; use std::rc::Rc; -use wgpu::{ - BindGroupLayout, Device, ExperimentalFeatures, Instance, Queue, Surface, SurfaceConfiguration, - SurfaceError, TextureFormat, -}; +use wgpu::{BindGroupLayout, Device, ExperimentalFeatures, Instance, Queue, Surface, SurfaceConfiguration, SurfaceError, TextureFormat}; use winit::event::{DeviceEvent, DeviceId}; use winit::{ application::ApplicationHandler, @@ -66,7 +63,8 @@ pub struct State { pub window: Arc<Window>, pub instance: Arc<Instance>, - pub surface: Surface<'static>, + pub surface: Arc<Surface<'static>>, + pub surface_format: TextureFormat, pub device: Arc<Device>, pub queue: Arc<Queue>, pub config: SurfaceConfiguration, @@ -163,6 +161,7 @@ Hardware: .find(|f| f.is_srgb()) .copied() .unwrap_or(TextureFormat::Rgba8Unorm); + let config = SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, @@ -218,7 +217,8 @@ Hardware: .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear); let result = Self { - surface, + surface: Arc::new(surface), + surface_format, device: Arc::new(device), queue: Arc::new(queue), config, @@ -7,6 +7,45 @@ use hecs::{Entity, EntityBuilder, World}; use std::any::TypeId; use std::collections::HashMap; +// note: to anyone viewing this, yes i did use AI to generate the documentation for this module because +// sometimes i kept forgetting what function some did. mb + +/// Registry of conversions between ECS components (`hecs`) and [`SerializableComponent`] +/// values. +/// +/// # What this type does +/// +/// `ComponentRegistry` is an adapter layer around a `hecs::World` that lets you: +/// +/// - **Extract** one or more [`SerializableComponent`] values from an entity. +/// - **Deserialize** a [`SerializableComponent`] back into a `hecs` component +/// and insert it into an [`EntityBuilder`]. +/// - **Address component "kinds" by numeric IDs** (useful for editor UI, network +/// messages, prefab formats, etc.). +/// - **Provide default/factory construction** for editor "Add component" flows. +/// +/// # Numeric IDs and stability +/// +/// Component IDs are assigned lazily (on registration) starting from `1`. +/// +/// - IDs are **stable only for the lifetime of this registry instance**. +/// - IDs are **not guaranteed to be stable across runs** (or across different +/// registration orders), because they're assigned incrementally. +/// - Display / editor lists returned by [`iter_available_components`] will be in +/// **arbitrary order**, because `HashMap` iteration order is not deterministic. +/// +/// If you need cross-run stable identifiers (e.g., long-lived save files), you +/// should layer an explicit, user-assigned ID scheme on top. +/// +/// # Converters vs deserializers +/// +/// A **converter** looks at an entity and tries to produce a serializable value. +/// A **deserializer** takes a serializable value and inserts a real ECS component +/// into an [`EntityBuilder`]. +/// +/// For directly-serializable components, [`register`] wires up both. +/// For custom flows, [`register_converter`] and [`register_deserializer`] can be +/// used independently. pub struct ComponentRegistry { converters: HashMap<TypeId, Box<dyn ComponentConverter>>, deserializers: HashMap<TypeId, Box<dyn ComponentDeserializer>>, @@ -17,6 +56,9 @@ pub struct ComponentRegistry { } impl ComponentRegistry { + /// Creates an empty registry. + /// + /// No components can be extracted or deserialized until they are registered. pub fn new() -> Self { Self { converters: HashMap::new(), @@ -28,7 +70,17 @@ impl ComponentRegistry { } } - // Register a component that's already SerializableComponent + /// Registers a component type that is already a [`SerializableComponent`]. + /// + /// This is the common case: `T` is both a `hecs` component and a serializable + /// value. The registry will: + /// + /// - Assign a numeric ID to `T` (if it doesn't already have one). + /// - Register a direct converter (extract `T` from an entity and clone it). + /// - Register a direct deserializer (insert a cloned `T` into a builder). + /// + /// Note: numeric IDs are assigned based on registration order; see the type + /// docs for stability caveats. pub fn register<T>(&mut self) where T: SerializableComponent + hecs::Component + Clone + 'static, @@ -41,6 +93,12 @@ impl ComponentRegistry { .insert(type_id, Box::new(DirectDeserializer::<T>::new())); } + /// Registers `T` and also exposes it as an "available" component with a + /// default constructor. + /// + /// This is primarily meant for editor tooling: values registered via this + /// method will appear in [`iter_available_components`] and can be created via + /// [`create_default_component`]. pub fn register_with_default<T>(&mut self) where T: SerializableComponent + hecs::Component + Clone + Default + 'static, @@ -51,6 +109,11 @@ impl ComponentRegistry { .insert(id, Box::new(|| Box::new(T::default()))); } + /// Registers `T` for extraction/deserialization (if needed) and associates a + /// custom factory used to create new instances of `T`. + /// + /// Like [`register_with_default`], this is intended for editor tooling. + /// Use this when the best "empty" value can't be expressed as `Default`. pub fn register_factory<T, F>(&mut self, factory: F) where T: SerializableComponent + hecs::Component + Clone + 'static, @@ -64,6 +127,10 @@ impl ComponentRegistry { self.default_creators.insert(id, Box::new(factory)); } + /// Creates a new component instance using the default constructor/factory + /// registered for `component_id`. + /// + /// Returns `None` if no factory/default was registered for that ID. pub fn create_default_component( &self, component_id: u64, @@ -71,6 +138,18 @@ impl ComponentRegistry { self.default_creators.get(&component_id).map(|f| f()) } + /// Removes the (source) ECS component associated with the given numeric ID + /// from `entity`. + /// + /// The registry resolves `component_id` to a *serializable* type, then finds + /// the first registered converter whose output type matches and asks it to + /// remove the underlying ECS component. + /// + /// ## Notes / edge cases + /// + /// - If no ID mapping exists or no converter matches, this is a no-op. + /// - If multiple converters map to the same serializable type, only the first + /// match (in arbitrary `HashMap` order) will be used. pub fn remove_component_by_id(&self, world: &mut World, entity: Entity, component_id: u64) { if let Some(expected_type) = self.serializable_type_from_numeric(component_id) { // Find the converter that produces this serializable type @@ -88,6 +167,18 @@ impl ComponentRegistry { } } + /// Iterates the set of components that are considered "addable" via defaults + /// or factories. + /// + /// Yields pairs of `(numeric_id, type_name)`. + /// + /// The returned iterator only includes components for which: + /// + /// - a default/factory was registered (via [`register_with_default`] or + /// [`register_factory`]), and + /// - a deserializer exists to provide a human-friendly type name. + /// + /// Ordering is arbitrary. pub fn iter_available_components(&self) -> impl Iterator<Item = (u64, &str)> { self.default_creators.keys().filter_map(move |id| { let type_id = self.id_to_serializable.get(id)?; @@ -96,7 +187,17 @@ impl ComponentRegistry { }) } - // Register a custom converter for special cases + /// Registers a custom converter that extracts a serializable value `To` from + /// an ECS component `From`. + /// + /// Use this when the runtime ECS component isn't directly serializable, but + /// you can derive a serializable representation from it. + /// + /// The provided function receives `(world, entity, &From)` and may return + /// `None` to indicate "not present / not applicable". + /// + /// Note: this registers an ID for `To` (the serializable output type), not for + /// `From`. pub fn register_converter<From, To, F>(&mut self, converter_fn: F) where From: hecs::Component + 'static, @@ -110,6 +211,12 @@ impl ComponentRegistry { .insert(type_id, Box::new(CustomConverter::new(converter_fn))); } + /// Registers a custom deserializer that converts a serializable `From` into + /// a concrete ECS component `To`. + /// + /// This is the inverse of [`register_converter`]. Use it when `From` is the + /// type you store/transport, and `To` is the type you actually attach to an + /// entity. pub fn register_deserializer<From, To, F>(&mut self, converter_fn: F) where From: SerializableComponent + 'static, @@ -122,7 +229,11 @@ impl ComponentRegistry { .insert(type_id, Box::new(CustomDeserializer::new(converter_fn))); } - // Extract all serializable components from an entity + /// Extracts all registered serializable components from `entity`. + /// + /// This calls every registered converter and collects the values it returns. + /// + /// Ordering is arbitrary (depends on `HashMap` iteration order). pub fn extract_all_components( &self, world: &World, @@ -137,6 +248,10 @@ impl ComponentRegistry { return vec; } + /// Ensures a numeric ID exists for the given serializable `TypeId` and + /// returns it. + /// + /// IDs are assigned incrementally and wrap on overflow. `0` is never used. fn ensure_serializable_id(&mut self, type_id: TypeId) -> u64 { if let Some(id) = self.serializable_ids.get(&type_id) { *id @@ -149,14 +264,18 @@ impl ComponentRegistry { } } - /// Returns the numeric identifier that was assigned to the provided - /// [`SerializableComponent`] type when it was registered. + /// Returns the numeric identifier assigned to the dynamic component value. + /// + /// Returns `None` if the component's concrete type has not been registered. pub fn id_for_component(&self, component: &dyn SerializableComponent) -> Option<u64> { let type_id = component.as_any().type_id(); self.serializable_ids.get(&type_id).copied() } - /// Returns the numeric identifier for `T` if it has been registered. + /// Returns the numeric identifier assigned to the serializable type `T`. + /// + /// Returns `None` if `T` has not been registered (directly or as a converter + /// output). pub fn id_for_type<T>(&self) -> Option<u64> where T: SerializableComponent + 'static, @@ -164,12 +283,21 @@ impl ComponentRegistry { self.serializable_ids.get(&TypeId::of::<T>()).copied() } + /// Looks up the serializable `TypeId` associated with a numeric identifier. fn serializable_type_from_numeric(&self, component_id: u64) -> Option<TypeId> { self.id_to_serializable.get(&component_id).copied() } - /// Attempts to extract a specific component instance from an entity using - /// its registry-assigned numeric identifier. + /// Extracts a single serializable component from `entity` by numeric ID. + /// + /// Returns `None` if: + /// + /// - `component_id` is unknown, or + /// - none of the registered converters produce a value of that type for the + /// given entity. + /// + /// If multiple converters can produce the same serializable type, the first + /// match (in arbitrary order) wins. pub fn extract_component_by_numeric_id( &self, world: &World, @@ -189,8 +317,10 @@ impl ComponentRegistry { None } - /// Iterates every entity in the world and clones any components whose - /// numeric identifier matches `component_id`. + /// Finds every entity in `world` that has a component matching `component_id`. + /// + /// This is a convenience wrapper that iterates all entities and uses + /// [`extract_component_by_numeric_id`] to test each one. pub fn find_components_by_numeric_id( &self, world: &World, @@ -207,10 +337,14 @@ impl ComponentRegistry { matches } - /// Attempts to deserialize a [`SerializableComponent`] back into an - /// ECS component and insert it into the provided [`EntityBuilder`]. - /// Returns `Ok(true)` if the component was handled, `Ok(false)` if no - /// deserializer was registered, and `Err` if deserialization failed. + /// Deserializes a [`SerializableComponent`] into an ECS component and inserts + /// it into `builder`. + /// + /// Returns: + /// + /// - `Ok(true)` if a deserializer was found and insertion succeeded. + /// - `Ok(false)` if no deserializer is registered for this component type. + /// - `Err(_)` if a deserializer was found but it failed. pub fn deserialize_into_builder( &self, component: &dyn SerializableComponent, @@ -13,12 +13,19 @@ pub mod spawn; pub mod states; pub mod utils; pub mod command; +pub mod physics; pub use dropbear_macro as macros; pub use dropbear_traits as traits; pub use egui; pub use rapier3d; +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{EntityTransform, MeshRenderer}; +use dropbear_traits::registry::ComponentRegistry; +use crate::camera::CameraComponent; +use crate::physics::rigidbody::RigidBody; +use crate::states::{Camera3D, CustomProperties, Light, Script, SerializedMeshRenderer}; /// The appdata directory for storing any information. /// @@ -33,4 +40,62 @@ pub extern "C" fn get_rustc_version() -> *const u8 { let meta = rustc_version_runtime::version_meta(); let meta_string = format!("{:?}", meta); Box::leak(meta_string.into_boxed_str()).as_ptr() +} + +pub fn register_components( + component_registry: &mut ComponentRegistry, +) { + component_registry.register_with_default::<EntityTransform>(); + component_registry.register_with_default::<CustomProperties>(); + component_registry.register_with_default::<Light>(); + component_registry.register_with_default::<Script>(); + component_registry.register_with_default::<SerializedMeshRenderer>(); + component_registry.register_with_default::<Camera3D>(); + + component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>( + |_, _, renderer| { + Some(SerializedMeshRenderer { + handle: renderer.handle().path.clone(), + material_override: renderer.material_overrides().to_vec(), + }) + }, + ); + + component_registry.register_converter::<CameraComponent, Camera3D, _>( + |world, entity, component| { + let Ok(camera) = world.get::<&Camera>(entity) else { + log::debug!( + "Camera component without matching Camera found on entity {:?}", + entity + ); + return None; + }; + + Some(Camera3D::from_ecs_camera(&camera, component)) + }, + ); + + component_registry.register_with_default::<RigidBody>(); + + // // register plugin defined structs + // if let Err(e) = plugin_registry.load_plugins() { + // fatal!("Failed to load plugins: {}", e); + // return; + // } + // + // for p in plugin_registry.list_plugins() { + // log::info!("Plugin {} has been loaded", p.display_name); + // } + // + // log::info!("Total plugins added: {}", plugin_registry.plugins.len()); + // + // for plugin in plugin_registry.list_plugins() { + // if let Some(p) = plugin_registry.get_mut(&plugin.display_name) { + // p.register_component(component_registry); + // log::info!( + // "Components for plugin [{}] has been registered to component registry", + // plugin.display_name + // ); + // } + // } } @@ -0,0 +1,95 @@ +//! Components in the eucalyptus-editor and redback-runtime that relate to rapier3d based physics. + +use std::collections::HashMap; +use glam::{Vec3}; +use rapier3d::na::Vector3; +use rapier3d::prelude::*; +use serde::{Deserialize, Serialize}; +use crate::physics::rigidbody::RigidBodyMode; +use crate::states::Label; + +pub mod rigidbody; + +/// A serializable [rapier3d] state that shows all the different actions and types related +/// to physics rendering. +#[derive(Serialize, Deserialize, Clone)] +pub struct PhysicsState { + pub islands: IslandManager, + pub broad_phase: DefaultBroadPhase, + pub narrow_phase: NarrowPhase, + pub bodies: RigidBodySet, + pub colliders: ColliderSet, + pub impulse_joints: ImpulseJointSet, + pub multibody_joints: MultibodyJointSet, + pub ccd_solver: CCDSolver, + pub integration_parameters: IntegrationParameters, + pub gravity: Vec3, + + pub bodies_entity_map: HashMap<Label, RigidBodyHandle> +} + +impl PhysicsState { + pub fn new() -> Self { + Self { + islands: Default::default(), + broad_phase: Default::default(), + narrow_phase: Default::default(), + bodies: Default::default(), + colliders: Default::default(), + impulse_joints: Default::default(), + multibody_joints: Default::default(), + ccd_solver: Default::default(), + integration_parameters: Default::default(), + gravity: Vec3::new(0.0, -9.81, 0.0), + bodies_entity_map: Default::default(), + } + } + + pub fn step(&mut self, pipeline: &mut PhysicsPipeline, physics_hooks: (), event_handler: ()) { + pipeline.step( + &vector![self.gravity.x, self.gravity.y, self.gravity.z], + &self.integration_parameters, + &mut self.islands, + &mut self.broad_phase, + &mut self.narrow_phase, + &mut self.bodies, + &mut self.colliders, + &mut self.impulse_joints, + &mut self.multibody_joints, + &mut self.ccd_solver, + &physics_hooks, + &event_handler, + ); + } + + pub fn register_rigidbody(&mut self, rigid_body: &rigidbody::RigidBody) { + let mode = match rigid_body.mode { + RigidBodyMode::Dynamic => RigidBodyType::Dynamic, + RigidBodyMode::Fixed => RigidBodyType::Fixed, + RigidBodyMode::KinematicPosition => RigidBodyType::KinematicPositionBased, + RigidBodyMode::KinematicVelocity => RigidBodyType::KinematicVelocityBased, + }; + + let body = RigidBodyBuilder::new(mode) + .gravity_scale(rigid_body.gravity_scale) + .can_sleep(rigid_body.can_sleep) + .ccd_enabled(rigid_body.ccd_enabled) + .linvel(Vector3::from_column_slice(&rigid_body.linvel)) + .angvel(Vector3::from_column_slice(&rigid_body.angvel)) + .linear_damping(rigid_body.linear_damping) + .angular_damping(rigid_body.angular_damping) + .enabled_translations(rigid_body.lock_translation.x, rigid_body.lock_translation.y, rigid_body.lock_translation.z) + .enabled_rotations(rigid_body.lock_rotation.x, rigid_body.lock_rotation.y, rigid_body.lock_rotation.z) + .build(); + + let handle = self.bodies.insert(body); + + self.bodies_entity_map.insert(rigid_body.entity.clone(), handle); + } +} + +impl Default for PhysicsState { + fn default() -> Self { + Self::new() + } +} @@ -0,0 +1,115 @@ +use serde::{Deserialize, Serialize}; +use dropbear_macro::SerializableComponent; +use dropbear_traits::SerializableComponent; +use crate::states::Label; + +/// How this entity behaves in the physics simulation. +/// +/// This intentionally mirrors Rapier's rigid-body types, but stays engine-owned and serializable. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +pub enum RigidBodyMode { + /// A fully simulated body affected by forces and contacts. + Dynamic, + /// An immovable body. + Fixed, + /// A kinematic body controlled by setting its next position. + KinematicPosition, + /// A kinematic body controlled by setting its velocities. + KinematicVelocity, +} + +impl Default for RigidBodyMode { + fn default() -> Self { + Self::Dynamic + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)] +pub struct AxisLock { + pub x: bool, + pub y: bool, + pub z: bool, +} + +/// A serializable physics rigid-body component. +/// +/// Notes: +/// - This component should NOT store Rapier handles (`RigidBodyHandle`, `ColliderHandle`, ...). +/// Those are runtime-only and belong in a physics-world resource/system. +/// - The body's initial pose should typically come from your `EntityTransform`/`Transform`. +/// - Colliders/material (shape, friction, restitution, sensor, etc.) should usually be a separate +/// component (e.g. `Collider`). +#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)] +pub struct RigidBody { + /// The entity this component is attached to. + #[serde(default)] + pub entity: Label, + + /// Body type/mode. + #[serde(default)] + pub mode: RigidBodyMode, + + /// Scaling factor applied to gravity for this body. + #[serde(default = "RigidBody::default_gravity_scale")] + pub gravity_scale: f32, + + /// Whether this body is allowed to sleep. + #[serde(default = "RigidBody::default_can_sleep")] + pub can_sleep: bool, + + /// Whether continuous collision detection is enabled. + #[serde(default)] + pub ccd_enabled: bool, + + /// Initial linear velocity (m/s). + #[serde(default)] + pub linvel: [f32; 3], + + /// Initial angular velocity (rad/s). + #[serde(default)] + pub angvel: [f32; 3], + + /// Linear damping coefficient. + #[serde(default)] + pub linear_damping: f32, + + /// Angular damping coefficient. + #[serde(default)] + pub angular_damping: f32, + + /// Locks translation along specific axes. + #[serde(default)] + pub lock_translation: AxisLock, + + /// Locks rotation around specific axes. + #[serde(default)] + pub lock_rotation: AxisLock, +} + +impl Default for RigidBody { + fn default() -> Self { + Self { + entity: Label::default(), + mode: RigidBodyMode::default(), + gravity_scale: Self::default_gravity_scale(), + can_sleep: Self::default_can_sleep(), + ccd_enabled: false, + linvel: [0.0, 0.0, 0.0], + angvel: [0.0, 0.0, 0.0], + linear_damping: 0.0, + angular_damping: 0.0, + lock_translation: AxisLock::default(), + lock_rotation: AxisLock::default(), + } + } +} + +impl RigidBody { + const fn default_gravity_scale() -> f32 { + 1.0 + } + + const fn default_can_sleep() -> bool { + true + } +} @@ -53,7 +53,7 @@ impl Default for Authoring { /// /// 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, Clone)] +#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Clone)] pub struct RuntimeProjectConfig { /// The name of the project #[bincode(with_serde)] @@ -24,6 +24,8 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; use crossbeam_channel::Sender; +use crate::physics::PhysicsState; +use crate::physics::rigidbody::RigidBody; #[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct SceneEntity { @@ -83,7 +85,7 @@ impl SceneSettings { /// Specifies the configuration of a scene, such as its entities, hierarchies and any settings that /// may be necessary. -#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[derive(Default, Serialize, Deserialize, Clone)] pub struct SceneConfig { #[serde(default)] pub scene_name: String, @@ -95,6 +97,9 @@ pub struct SceneConfig { pub hierarchy_map: SceneHierarchy, #[serde(default)] + pub physics_state: PhysicsState, + + #[serde(default)] pub settings: SceneSettings, #[serde(skip)] @@ -109,6 +114,7 @@ impl SceneConfig { path: path.as_ref().to_path_buf(), entities: Vec::new(), hierarchy_map: SceneHierarchy::new(), + physics_state: PhysicsState::new(), settings: SceneSettings::new(), } } @@ -258,6 +264,8 @@ impl SceneConfig { builder.add(light_conf.transform); } else if let Some(script) = component.as_any().downcast_ref::<Script>() { builder.add(script.clone()); + } else if let Some(body) = component.as_any().downcast_ref::<RigidBody>() { + builder.add(body.clone()); } else if component.as_any().downcast_ref::<Parent>().is_some() { log::debug!( "Skipping Parent component for '{}' - will be rebuilt from hierarchy_map", @@ -312,7 +320,7 @@ impl SceneConfig { /// `is_play_mode` is used to specify if the viewport camera (debug camera) is to be used (`false`) /// or if the starting camera for the scene is too be used (`true`). pub async fn load_into_world( - &self, + &mut self, world: &mut hecs::World, graphics: Arc<SharedGraphicsContext>, registry: Option<&ComponentRegistry>, @@ -427,6 +435,20 @@ impl SceneConfig { } } + // adding to physics + if let Ok(mut q) = world.query_one::<(&Label, Option<&mut RigidBody>)>(entity) + && let Some((label, rigid)) = q.get() { + + // rigidbody + if let Some(body) = rigid { + body.entity = label.clone(); + + self.physics_state.register_rigidbody(body); + } + + // collider + } + if let Some(previous) = label_to_entity.insert(label_for_map.clone(), entity) { log::warn!( "Duplicate entity label '{}' detected; previous entity {:?} will be overwritten in hierarchy mapping", @@ -107,4 +107,57 @@ pub struct SceneLoadHandle { pub id: u64, /// The name of the planned scene. pub scene_name: String, +} + +#[derive(Clone)] +pub struct IsSceneLoaded { + pub requested_scene: String, + pub id: Option<u64>, + pub is_first_scene: bool, + pub scene_handle_requested: bool, + pub world_loaded: bool, + pub camera_received: bool, +} + +impl IsSceneLoaded { + pub fn new(requested_scene: String) -> Self { + Self { + requested_scene, + id: None, + is_first_scene: false, + scene_handle_requested: false, + world_loaded: false, + camera_received: false, + } + } + + pub fn new_with_id(requested_scene: String, id: u64) -> Self { + Self { + requested_scene, + id: Some(id), + is_first_scene: false, + scene_handle_requested: false, + world_loaded: false, + camera_received: false, + } + } + + pub fn new_first_time(requested_scene: String) -> Self { + Self { + requested_scene, + id: None, + is_first_scene: true, + scene_handle_requested: false, + world_loaded: false, + camera_received: false, + } + } + + pub fn is_everything_loaded(&self) -> bool { + self.scene_handle_requested && self.world_loaded && self.camera_received + } + + pub fn is_first_scene(&self) -> bool { + self.is_first_scene + } } @@ -436,23 +436,23 @@ impl ScriptManager { } } - fn destroy_tagged(&mut self, tag: &str) -> anyhow::Result<()> { - match self.script_target { - ScriptTarget::None => Ok(()), - ScriptTarget::JVM { .. } => { - if let Some(jvm) = &self.jvm { - let _ = jvm.unload_systems_for_tag(tag); - } - Ok(()) - } - ScriptTarget::Native { .. } => { - if let Some(library) = &mut self.library { - library.destroy_tagged(tag.to_string())?; - } - Ok(()) - } - } - } + // fn destroy_tagged(&mut self, tag: &str) -> anyhow::Result<()> { + // match self.script_target { + // ScriptTarget::None => Ok(()), + // ScriptTarget::JVM { .. } => { + // if let Some(jvm) = &self.jvm { + // let _ = jvm.unload_systems_for_tag(tag); + // } + // Ok(()) + // } + // ScriptTarget::Native { .. } => { + // if let Some(library) = &mut self.library { + // library.destroy_tagged(tag.to_string())?; + // } + // Ok(()) + // } + // } + // } fn destroy_in_scope_tagged(&mut self, tag: &str) -> anyhow::Result<()> { if !self.scripts_loaded { @@ -18,6 +18,7 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; +use hecs::World; /// A global "singleton" that contains the configuration of a project. pub static PROJECT: Lazy<RwLock<ProjectConfig>> = @@ -426,7 +427,7 @@ pub enum WorldLoadingStatus { Completed, } -#[derive(Clone, Debug, Serialize, Deserialize, bincode::Encode, bincode::Decode)] +#[derive(Clone, Serialize, Deserialize, bincode::Encode, bincode::Decode)] pub struct RuntimeData { #[bincode(with_serde)] pub project_config: ProjectConfig, @@ -477,6 +478,10 @@ impl Label { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + pub fn locate_entity(&self, world: &World) -> Option<hecs::Entity> { + world.query::<&Label>().iter().find_map(|(e, l)| if l == self { Some(e.clone()) } else { None }) + } } impl Display for Label { @@ -9,6 +9,7 @@ use semver::Version; use std::fs; use std::path::{Path, PathBuf}; use std::time::UNIX_EPOCH; +use ron::ser::PrettyConfig; use tokio::{fs as tokio_fs, process::Command, task}; use magna_carta::Target; @@ -109,12 +110,15 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> { /// Reads the contents of a data.eupak file into a pretty print format. /// -/// Returns the contents of the project config. +/// Returns the contents of the project config in a [`ron`] format 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); + + let str = ron::ser::to_string_pretty(&content, PrettyConfig::default())?; + + println!("{} contents: {:#?}", eupak.display(), str); Ok(content) } @@ -32,6 +32,7 @@ use indexmap::Equivalent; use log; use parking_lot::Mutex; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode, GizmoOrientation}; +use eucalyptus_core::physics::rigidbody::RigidBody; pub struct EditorTabViewer<'a> { pub view: egui::TextureId, @@ -664,6 +665,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.separator(); + // mesh renderer if let Ok(mut q) = world.query_one::<&mut MeshRenderer>(inspect_entity) && let Some(e) = q.get() { @@ -678,6 +680,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ); } + // entity transform if let Ok(mut q) = world.query_one::<&mut EntityTransform>(inspect_entity) && let Some(t) = q.get() { @@ -694,6 +697,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.separator(); } + // custom properties if let Ok(mut q) = world.query_one::<&mut CustomProperties>(inspect_entity) && let Some(props) = q.get() { @@ -710,6 +714,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.separator(); } + // camera if let Ok(mut q) = world .query_one::<(&mut Camera, &mut CameraComponent)>(inspect_entity) && let Some((camera, camera_component)) = q.get() @@ -774,6 +779,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.separator(); } + // light if let Ok(mut q) = world.query_one::<(&mut Light, &mut LightComponent, &mut Transform)>(inspect_entity) && let Some((light, comp, transform)) = q.get() { @@ -791,7 +797,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.separator(); } - if let Ok(mut q) = self.world.query_one::<&mut Script>(*entity) + // script + if let Ok(mut q) = world.query_one::<&mut Script>(*entity) && let Some(script) = q.get() { CollapsingHeader::new("Script").default_open(true).show(ui, |ui| { @@ -807,22 +814,24 @@ impl<'a> TabViewer for EditorTabViewer<'a> { ui.separator(); } - if let Some(t) = cfg.label_last_edit - && t.elapsed() >= Duration::from_millis(500) + // rigidbody + if let Ok(mut q) = world.query_one::<&mut RigidBody>(*entity) + && let Some(body) = q.get() { - if let Some(ent) = cfg.old_label_entity.take() - && let Some(orig) = cfg.label_original.take() - { - UndoableAction::push_to_undo( + CollapsingHeader::new("RigidBody").default_open(true).show(ui, |ui| { + body.inspect( + entity, + &mut cfg, + ui, self.undo_stack, - UndoableAction::Label(ent, orig), - ); - log::debug!( - "Pushed label change to undo stack after 500ms debounce period" + self.signal, + label.as_mut_string(), ); - } - cfg.label_last_edit = None; + }); + ui.separator(); } + + } } else { log_once::debug_once!("Unable to query entity inside resource inspector"); @@ -18,7 +18,7 @@ use dropbear_engine::shader::Shader; use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{RenderContext, SharedGraphicsContext}, lighting::LightManager, model::{MODEL_CACHE, ModelId}, scene::SceneCommand, DropbearWindowBuilder, WindowData}; use egui::{self, Context}; use egui_dock::{DockArea, DockState, NodeIndex, Style}; -use eucalyptus_core::APP_INFO; +use eucalyptus_core::{register_components, APP_INFO}; use eucalyptus_core::hierarchy::{Children, SceneHierarchy}; use eucalyptus_core::scene::{SceneConfig, SceneEntity}; use eucalyptus_core::states::{CustomProperties, Label, SerializedMeshRenderer}; @@ -31,7 +31,7 @@ use eucalyptus_core::{ scripting::{BuildStatus}, states, states::{ - Camera3D, EditorTab, Light, PROJECT, SCENES, Script, WorldLoadingStatus, + EditorTab, PROJECT, SCENES, Script, WorldLoadingStatus, }, success, utils::ViewportMode, @@ -45,7 +45,7 @@ use std::{ fs, path::PathBuf, sync::Arc, - time::{Duration, Instant}, + time::{Instant}, }; use std::rc::Rc; use tokio::sync::oneshot; @@ -161,67 +161,11 @@ impl Editor { eucalyptus_core::utils::start_deadlock_detector(); - let mut plugin_registry = PluginRegistry::new(); + let plugin_registry = PluginRegistry::new(); let mut component_registry = ComponentRegistry::new(); - fn register_components( - plugin_registry: &mut PluginRegistry, - component_registry: &mut ComponentRegistry, - ) { - component_registry.register_with_default::<EntityTransform>(); - component_registry.register_with_default::<CustomProperties>(); - component_registry.register_with_default::<Light>(); - component_registry.register_with_default::<Script>(); - component_registry.register_with_default::<SerializedMeshRenderer>(); - component_registry.register_with_default::<Camera3D>(); - - component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>( - |_, _, renderer| { - Some(SerializedMeshRenderer { - handle: renderer.handle().path.clone(), - material_override: renderer.material_overrides().to_vec(), - }) - }, - ); - - component_registry.register_converter::<CameraComponent, Camera3D, _>( - |world, entity, component| { - let Ok(camera) = world.get::<&Camera>(entity) else { - log::debug!( - "Camera component without matching Camera found on entity {:?}", - entity - ); - return None; - }; - - Some(Camera3D::from_ecs_camera(&camera, component)) - }, - ); - - // register plugin defined structs - if let Err(e) = plugin_registry.load_plugins() { - fatal!("Failed to load plugins: {}", e); - return; - } - - for p in plugin_registry.list_plugins() { - log::info!("Plugin {} has been loaded", p.display_name); - } - - log::info!("Total plugins added: {}", plugin_registry.plugins.len()); - - for plugin in plugin_registry.list_plugins() { - if let Some(p) = plugin_registry.get_mut(&plugin.display_name) { - p.register_component(component_registry); - log::info!( - "Components for plugin [{}] has been registered to component registry", - plugin.display_name - ); - } - } - } + register_components(/*&mut plugin_registry,*/ &mut component_registry); - register_components(&mut plugin_registry, &mut component_registry); let component_registry = Arc::new(component_registry); Ok(Self { @@ -532,7 +476,7 @@ impl Editor { }; { - if let Some(first_scene) = first_scene_opt { + if let Some(mut first_scene) = first_scene_opt { let cam = first_scene .load_into_world( world, @@ -663,7 +607,7 @@ impl Editor { } } - fn start_async_scene_load(&mut self, scene: SceneConfig, graphics: &mut RenderContext) { + fn start_async_scene_load(&mut self, mut scene: SceneConfig, graphics: &mut RenderContext) { self.cleanup_scene_resources(graphics); let (progress_sender, progress_receiver) = @@ -12,3 +12,4 @@ pub mod utils; pub mod runtime; pub mod about; pub mod process; +pub mod physics; @@ -0,0 +1 @@ +pub mod rigidbody; @@ -0,0 +1,103 @@ +use egui::{ComboBox, Ui}; +use hecs::Entity; +use eucalyptus_core::physics::rigidbody::{RigidBody, RigidBodyMode}; +use eucalyptus_core::states::Label; +use crate::editor::component::InspectableComponent; +use crate::editor::{Signal, StaticallyKept, UndoableAction}; + +impl InspectableComponent for RigidBody { + fn inspect( + &mut self, + _entity: &mut Entity, + _cfg: &mut StaticallyKept, + ui: &mut Ui, + _undo_stack: &mut Vec<UndoableAction>, + _signal: &mut Signal, + label: &mut String + ) { + ui.vertical(|ui| { + self.entity = Label::new(label.clone()); + + let mut selected = self.mode.clone(); + ComboBox::from_id_salt("rigidbody") + .selected_text(format!("{:?}", self.mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut selected, RigidBodyMode::Dynamic, "Dynamic"); + ui.selectable_value(&mut selected, RigidBodyMode::Fixed, "Fixed"); + ui.selectable_value(&mut selected, RigidBodyMode::KinematicPosition, "Kinematic Position"); + ui.selectable_value(&mut selected, RigidBodyMode::KinematicVelocity, "Kinematic Velocity"); + }); + + if selected != self.mode { + self.mode = selected; + } + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label("Gravity Scale:"); + ui.add(egui::DragValue::new(&mut self.gravity_scale) + .speed(0.1) + .range(0.0..=10.0)); + }); + + ui.checkbox(&mut self.can_sleep, "Can Sleep"); + + ui.checkbox(&mut self.ccd_enabled, "CCD Enabled"); + + ui.add_space(8.0); + + ui.label("Linear Velocity:"); + ui.horizontal(|ui| { + ui.label("X:"); + ui.add(egui::DragValue::new(&mut self.linvel[0]).speed(0.1)); + ui.label("Y:"); + ui.add(egui::DragValue::new(&mut self.linvel[1]).speed(0.1)); + ui.label("Z:"); + ui.add(egui::DragValue::new(&mut self.linvel[2]).speed(0.1)); + }); + + ui.label("Angular Velocity:"); + ui.horizontal(|ui| { + ui.label("X:"); + ui.add(egui::DragValue::new(&mut self.angvel[0]).speed(0.1)); + ui.label("Y:"); + ui.add(egui::DragValue::new(&mut self.angvel[1]).speed(0.1)); + ui.label("Z:"); + ui.add(egui::DragValue::new(&mut self.angvel[2]).speed(0.1)); + }); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label("Linear Damping:"); + ui.add(egui::DragValue::new(&mut self.linear_damping) + .speed(0.01) + .range(0.0..=10.0)); + }); + + ui.horizontal(|ui| { + ui.label("Angular Damping:"); + ui.add(egui::DragValue::new(&mut self.angular_damping) + .speed(0.01) + .range(0.0..=10.0)) + }); + + ui.add_space(8.0); + + ui.label("Lock Translation:"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.lock_translation.x, "X"); + ui.checkbox(&mut self.lock_translation.y, "Y"); + ui.checkbox(&mut self.lock_translation.z, "Z"); + }); + + ui.label("Lock Rotation:"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.lock_rotation.x, "X"); + ui.checkbox(&mut self.lock_rotation.y, "Y"); + ui.checkbox(&mut self.lock_rotation.z, "Z"); + }); + }); + } +} @@ -2,7 +2,7 @@ use dropbear_engine::graphics::RenderContext; use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand}; use winit::window::CursorGrabMode; use crate::runtime::PlayMode; -use crate::runtime::scene::IsSceneLoaded; +use eucalyptus_core::scene::loading::IsSceneLoaded; impl CommandBufferPoller for PlayMode { fn poll(&mut self, graphics: &mut RenderContext) { @@ -4,7 +4,7 @@ use std::sync::Arc; use crossbeam_channel::{unbounded, Receiver}; use futures::executor; use hecs::{Entity, World}; -use wgpu::RenderPipeline; +use wgpu::{RenderPipeline, SurfaceConfiguration}; use dropbear_engine::camera::Camera; use dropbear_engine::future::FutureHandle; use dropbear_engine::graphics::RenderContext; @@ -19,9 +19,12 @@ use eucalyptus_core::scene::loading::SCENE_LOADER; use eucalyptus_core::traits::registry::ComponentRegistry; use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr}; use eucalyptus_core::command::COMMAND_BUFFER; -use crate::runtime::scene::IsSceneLoaded; +use eucalyptus_core::scene::loading::IsSceneLoaded; use std::collections::HashMap; use std::path::PathBuf; +use winit::window::Fullscreen; +use eucalyptus_core::physics::PhysicsState; +use eucalyptus_core::rapier3d::prelude::*; mod scene; mod input; @@ -90,6 +93,10 @@ pub struct PlayMode { pending_camera: Option<Entity>, pub(crate) scripts_ready: bool, has_initial_resize_done: bool, + + // physics + physics_pipeline: PhysicsPipeline, + physics_state: PhysicsState, } impl PlayMode { @@ -103,7 +110,7 @@ impl PlayMode { current_scene: None, world_loading_progress: None, world_receiver: None, - component_registry: Arc::new(Default::default()), + component_registry: Arc::new(ComponentRegistry::new()), scene_loading_handle: None, scene_progress: None, pending_world: None, @@ -118,6 +125,8 @@ impl PlayMode { maintain_aspect_ratio: true, vsync: true, }, + physics_pipeline: Default::default(), + physics_state: PhysicsState::new(), }; log::debug!("Created new play mode instance"); @@ -237,7 +246,7 @@ impl PlayMode { } } - let scene_to_load = { + let mut scene_to_load = { let scenes = SCENES.read(); let scene = scenes.iter().find(|s| s.scene_name == scene_name).unwrap().clone(); scene @@ -288,7 +297,7 @@ impl PlayMode { self.scene_loading_handle = None; self.scene_progress = None; - let scene_to_load = { + let mut scene_to_load = { let scenes = SCENES.read(); scenes.iter() .find(|s| s.scene_name == scene_name) @@ -360,6 +369,58 @@ pub struct DisplaySettings { pub vsync: bool, } +impl DisplaySettings { + pub fn update(&mut self, graphics: &RenderContext) { + let window = graphics.shared.window.clone(); + + let is_maximized = window.is_maximized(); + let is_fullscreen = window.fullscreen().is_some(); + + self.window_mode = if is_fullscreen { + WindowMode::BorderlessFullscreen + } else if is_maximized { + WindowMode::Maximized + } else { + WindowMode::Windowed + }; + + 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 | WindowMode::BorderlessFullscreen => { + let monitor = window.current_monitor(); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); + window.set_maximized(false); + } + } + + if self.vsync { + let config = SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: graphics.shared.surface_format, + width: graphics.frame.screen_size.0 as u32, + height: graphics.frame.screen_size.1 as u32, + present_mode: if self.vsync { + wgpu::PresentMode::Fifo + } else { + wgpu::PresentMode::Immediate + }, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + graphics.shared.surface.configure(&graphics.shared.device, &config); + } + } +} + pub enum WindowMode { Windowed, Maximized, @@ -1,6 +1,6 @@ use std::collections::HashMap; use egui::{CentralPanel, MenuBar, TopBottomPanel}; -use hecs::{Entity}; +use hecs::Entity; use wgpu::Color; use wgpu::util::DeviceExt; use winit::event_loop::ActiveEventLoop; @@ -14,7 +14,7 @@ use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::command::CommandBufferPoller; use eucalyptus_core::hierarchy::EntityTransformExt; use eucalyptus_core::states::PROJECT; -use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult}; +use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER}; use crate::runtime::{PlayMode, WindowMode}; impl Scene for PlayMode { @@ -35,6 +35,17 @@ impl Scene for PlayMode { } } + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) { + // note to self: always update script before applying physics + if self.scripts_ready { + if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) { + panic!("Script physics update error: {}", e); + } + } + + self.physics_state.step(&mut self.physics_pipeline, (), ()); + } + fn update(&mut self, dt: f32, graphics: &mut RenderContext) { graphics.shared.future_queue.poll(); self.poll(graphics); @@ -74,7 +85,7 @@ impl Scene for PlayMode { log::debug!("Camera entity received: {:?}", cam); if let Some(ref mut progress) = self.scene_progress { progress.camera_received = true; - + if progress.world_loaded { if let Some(id) = progress.id { let mut loader = SCENE_LOADER.lock(); @@ -264,14 +275,6 @@ impl Scene for PlayMode { self.input_state.mouse_delta = None; } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) { - if self.scripts_ready { - if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) { - panic!("Script physics update error: {}", e); - } - } - } - fn render(&mut self, graphics: &mut RenderContext) { let Some(active_camera) = self.active_camera else { return; @@ -395,55 +398,3 @@ impl Scene for PlayMode { } } -#[derive(Clone)] -pub struct IsSceneLoaded { - pub(crate) requested_scene: String, - pub(crate) id: Option<u64>, - is_first_scene: bool, - pub(crate) scene_handle_requested: bool, - pub(crate) world_loaded: bool, - pub(crate) camera_received: bool, -} - -impl IsSceneLoaded { - pub fn new(requested_scene: String) -> Self { - Self { - requested_scene, - id: None, - is_first_scene: false, - scene_handle_requested: false, - world_loaded: false, - camera_received: false, - } - } - - pub fn new_with_id(requested_scene: String, id: u64) -> Self { - Self { - requested_scene, - id: Some(id), - is_first_scene: false, - scene_handle_requested: false, - world_loaded: false, - camera_received: false, - } - } - - pub fn new_first_time(requested_scene: String) -> Self { - Self { - requested_scene, - id: None, - is_first_scene: true, - scene_handle_requested: false, - world_loaded: false, - camera_received: false, - } - } - - pub fn is_everything_loaded(&self) -> bool { - self.scene_handle_requested && self.world_loaded && self.camera_received - } - - pub fn is_first_scene(&self) -> bool { - self.is_first_scene - } -} @@ -25,6 +25,9 @@ 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 = ["eucalyptus-core/runtime"] @@ -1,52 +1,57 @@ use dropbear_engine::graphics::RenderContext; -use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand, get_config}; +use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand}; use winit::window::CursorGrabMode; +use crate::scene::IsSceneLoaded; -use crate::scene::RuntimeScene; - -impl CommandBufferPoller for RuntimeScene { +impl CommandBufferPoller for Runtime { fn poll(&mut self, graphics: &mut RenderContext) { while let Ok(cmd) = COMMAND_BUFFER.1.try_recv() { log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd); match cmd { CommandBuffer::WindowCommand(w_cmd) => match w_cmd { - WindowCommand::WindowGrab(is_locked) => { - let mut cfg = get_config().write(); - if cfg.is_locked != is_locked { - if is_locked { - if let Err(e) = graphics.shared.window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked)) - { - log_once::warn_once!("Failed to grab cursor: {:?}", e); - } else { - log_once::info_once!("Grabbed cursor"); - cfg.is_locked = true; - } - } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) { - log_once::warn_once!("Failed to release cursor: {:?}", e); + WindowCommand::WindowGrab(lock) => { + if lock { + if let Err(e) = graphics.shared.window + .set_cursor_grab(CursorGrabMode::Confined) + .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked)) + { + log_once::warn_once!("Failed to grab cursor: {:?}", e); } else { - log_once::info_once!("Released cursor"); - cfg.is_locked = false; + log_once::info_once!("Grabbed cursor"); } + } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) { + log_once::warn_once!("Failed to release cursor: {:?}", e); + } else { + log_once::info_once!("Released cursor"); } } WindowCommand::HideCursor(should_hide) => { - let cfg = get_config().write(); - if cfg.is_hidden != should_hide { - if should_hide { - graphics.shared.window.set_cursor_visible(false); - } else { - graphics.shared.window.set_cursor_visible(true); - } + if should_hide { + graphics.shared.window.set_cursor_visible(false); + } else { + graphics.shared.window.set_cursor_visible(true); } } }, CommandBuffer::Quit => { self.scene_command = dropbear_engine::scene::SceneCommand::Quit; }, - CommandBuffer::SwitchScene(scene_name) => { - self.pending_scene_switch = Some(scene_name); + CommandBuffer::SwitchSceneImmediate(scene_name) => { + log::debug!("Immediate scene switch requested: {}", scene_name); + let scene_to_load = IsSceneLoaded::new(scene_name); + self.request_immediate_scene_load(graphics, scene_to_load); + } + CommandBuffer::LoadSceneAsync(handle) => { + log::debug!("Load scene async requested"); + let scene_to_load = IsSceneLoaded::new_with_id(handle.scene_name, handle.id); + self.request_async_scene_load(graphics, scene_to_load); + } + CommandBuffer::SwitchToAsync(handle) => { + if let Some(ref progress) = self.scene_progress { + if progress.requested_scene == handle.scene_name && progress.is_everything_loaded() { + self.switch_to(progress.clone(), graphics); + } + } } } } @@ -4,9 +4,8 @@ use dropbear_engine::entity::{MeshRenderer, Transform}; use dropbear_engine::lighting::{Light, LightComponent}; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::states::{Label, CustomProperties, Script}; -use crate::scene::RuntimeScene; -impl RuntimeScene { +impl Runtime { #[allow(dead_code)] pub fn display_all_entities(&self) { log::debug!("===================="); @@ -1,12 +1,12 @@ -use crate::scene::RuntimeScene; -use dropbear_engine::gilrs::{Button, GamepadId}; +use gilrs::{Button, GamepadId}; +use winit::dpi::PhysicalPosition; +use winit::event::MouseButton; +use winit::event_loop::ActiveEventLoop; +use winit::keyboard::KeyCode; 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; +use crate::Runtime; -impl Keyboard for RuntimeScene { +impl Keyboard for Runtime { fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { self.input_state.pressed_keys.insert(key); } @@ -16,11 +16,11 @@ impl Keyboard for RuntimeScene { } } -impl Mouse for RuntimeScene { +impl Mouse for Runtime { 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)) + Some((position.x - last_pos.0, position.y - last_pos.1)) } else { None } @@ -42,7 +42,7 @@ impl Mouse for RuntimeScene { } } -impl Controller for RuntimeScene { +impl Controller for Runtime { fn button_down(&mut self, button: Button, id: GamepadId) { self.input_state .pressed_buttons @@ -75,4 +75,4 @@ impl Controller for RuntimeScene { self.input_state.left_stick_position.remove(&id); self.input_state.right_stick_position.remove(&id); } -} +} @@ -0,0 +1,331 @@ +//! Allows you to a launch play mode as another window. + +use std::sync::Arc; +use crossbeam_channel::{unbounded, Receiver}; +use futures::executor; +use hecs::{Entity, World}; +use wgpu::RenderPipeline; +use dropbear_engine::camera::Camera; +use dropbear_engine::future::FutureHandle; +use dropbear_engine::graphics::RenderContext; +use dropbear_engine::lighting::LightManager; +use dropbear_engine::scene::SceneCommand; +use dropbear_engine::shader::Shader; +use eucalyptus_core::camera::CameraComponent; +use eucalyptus_core::input::InputState; +use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; +use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT}; +use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER}; +use eucalyptus_core::traits::registry::ComponentRegistry; +use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr}; +use eucalyptus_core::command::COMMAND_BUFFER; +use std::collections::HashMap; +use std::path::PathBuf; +use dropbear_engine::wgpu::RenderPipeline; + +mod input; +mod scene; +mod debug; +mod utils; +mod command; + +pub struct Runtime { + scene_command: SceneCommand, + input_state: InputState, + script_manager: ScriptManager, + world: Box<World>, + component_registry: Arc<ComponentRegistry>, + active_camera: Option<Entity>, + + // rendering + render_pipeline: Option<RenderPipeline>, + light_manager: LightManager, + + display_settings: DisplaySettings, + + initial_scene: Option<String>, + current_scene: Option<String>, + world_loading_progress: Option<Receiver<WorldLoadingStatus>>, + world_receiver: Option<tokio::sync::oneshot::Receiver<World>>, + scene_loading_handle: Option<FutureHandle>, + scene_progress: Option<IsSceneLoaded>, + pending_world: Option<Box<World>>, + pending_camera: Option<Entity>, + pub(crate) scripts_ready: bool, + has_initial_resize_done: bool, +} + +impl Runtime { + pub fn new(initial_scene: Option<String>) -> anyhow::Result<Self> { + let result = Self { + scene_command: SceneCommand::None, + input_state: InputState::new(), + script_manager: ScriptManager::new()?, + world: Box::new(World::new()), + initial_scene, + current_scene: None, + world_loading_progress: None, + world_receiver: None, + component_registry: Arc::new(Default::default()), + scene_loading_handle: None, + scene_progress: None, + pending_world: None, + pending_camera: None, + active_camera: None, + render_pipeline: None, + light_manager: Default::default(), + scripts_ready: false, + has_initial_resize_done: false, + display_settings: DisplaySettings { + window_mode: WindowMode::Windowed, + maintain_aspect_ratio: true, + vsync: true, + }, + }; + + log::debug!("Created new play mode instance"); + + Ok(result) + } + + pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) { + let shader = Shader::new( + graphics.shared.clone(), + dropbear_engine::shader::shader_wesl::SHADER_SHADER, + Some("viewport_shader"), + ); + + self.light_manager + .create_light_array_resources(graphics.shared.clone()); + + if let Some(active_camera) = self.active_camera { + if let Ok(mut q) = self + .world + .query_one::<(&Camera, &CameraComponent)>(active_camera) + { + if let Some((camera, _component)) = q.get() { + let pipeline = graphics.create_render_pipline( + &shader, + vec![ + &graphics.shared.texture_bind_layout.clone(), + camera.layout(), + self.light_manager.layout(), + ], + None, + ); + self.render_pipeline = Some(pipeline); + + self.light_manager.create_render_pipeline( + graphics.shared.clone(), + dropbear_engine::shader::shader_wesl::LIGHT_SHADER, + camera, + Some("Light Pipeline"), + ); + } else { + log_once::warn_once!( + "Unable to fetch the query result of camera: {:?}", + active_camera + ) + } + } else { + log_once::warn_once!( + "Unable to query camera, component for active camera: {:?}", + active_camera + ); + } + } else { + log_once::warn_once!("No active camera found"); + } + } + + fn reload_scripts_for_current_world(&mut self) { + let mut entity_tag_map: HashMap<String, Vec<Entity>> = HashMap::new(); + for (entity_id, script) in self.world.query::<&Script>().iter() { + for tag in &script.tags { + entity_tag_map.entry(tag.clone()).or_default().push(entity_id); + } + } + + let target = ScriptTarget::JVM { + library_path: find_jvm_library_path(), + }; + + self.scripts_ready = false; + + if let Err(e) = self + .script_manager + .init_script(None, entity_tag_map.clone(), target.clone()) + { + log::error!("Failed to initialise scripts: {}", e); + return; + } + + let world_ptr = self.world.as_mut() as WorldPtr; + let input_ptr = &mut self.input_state as InputStatePtr; + let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr; + + if let Err(e) = self + .script_manager + .load_script(world_ptr, input_ptr, graphics_ptr) + { + log::error!("Failed to load scripts: {}", e); + return; + } + + self.scripts_ready = true; + log::debug!("Scripts initialised successfully"); + } + + /// Requests an asynchronous scene load, returning immediately and loading the scene in the background. + pub fn request_async_scene_load(&mut self, graphics: &RenderContext, requested_scene: IsSceneLoaded) { + log::debug!("Requested async scene load: {}", requested_scene.requested_scene); + let scene_name = requested_scene.requested_scene.clone(); + self.scene_progress = Some(requested_scene); + + let (tx, rx) = unbounded::<WorldLoadingStatus>(); + let (world_tx, world_rx) = tokio::sync::oneshot::channel::<World>(); + + self.world_loading_progress = Some(rx); + self.world_receiver = Some(world_rx); + + + if let Some(ref progress) = self.scene_progress { + 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(); + } + } + } + } + + let scene_to_load = { + let scenes = SCENES.read(); + let scene = scenes.iter().find(|s| s.scene_name == scene_name).unwrap().clone(); + scene + }; + + let graphics_cloned = graphics.shared.clone(); + let component_registry = self.component_registry.clone(); + + let handle = graphics.shared.future_queue.push(async move { + let mut temp_world = World::new(); + let load_status = scene_to_load.load_into_world( + &mut temp_world, + graphics_cloned, + Some(&component_registry), + Some(tx), + true, + ).await; + match load_status { + Ok(v) => { + if world_tx.send(temp_world).is_err() { + log::warn!("Unable to send world: Receiver has been deallocated. This usually means a new scene load was requested before this one finished."); + }; + v + } + Err(e) => {panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e);} + } + }); + + log::debug!("Created future handle for scene loading: {:?}", handle); + + self.scene_loading_handle = Some(handle); + if let Some(ref mut progress) = self.scene_progress { + progress.scene_handle_requested = true; + } + } + + /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded. + pub fn request_immediate_scene_load(&mut self, graphics: &mut RenderContext, requested_scene: IsSceneLoaded) { + let scene_name = requested_scene.requested_scene.clone(); + log::debug!("Immediate scene load requested: {}", scene_name); + + self.world = Box::new(World::new()); + self.active_camera = None; + self.render_pipeline = None; + self.current_scene = None; + self.world_loading_progress = None; + self.world_receiver = None; + self.scene_loading_handle = None; + self.scene_progress = None; + + let scene_to_load = { + let scenes = SCENES.read(); + scenes.iter() + .find(|s| s.scene_name == scene_name) + .cloned() + .expect(&format!("Scene '{}' not found", scene_name)) + }; + + let graphics_cloned = graphics.shared.clone(); + let component_registry = self.component_registry.clone(); + + let (tx, _rx) = unbounded::<WorldLoadingStatus>(); + + let (loaded_world, camera_entity) = executor::block_on(async move { + let mut temp_world = World::new(); + let camera = scene_to_load.load_into_world( + &mut temp_world, + graphics_cloned, + Some(&component_registry), + Some(tx), + true, + ).await; + + match camera { + Ok(cam) => (temp_world, cam), + Err(e) => panic!("Failed to immediately load scene [{}]: {}", scene_to_load.scene_name, e), + } + }); + + self.world = Box::new(loaded_world); + self.active_camera = Some(camera_entity); + self.current_scene = Some(scene_name.clone()); + + let mut progress = requested_scene; + progress.scene_handle_requested = true; + progress.world_loaded = true; + progress.camera_received = true; + self.scene_progress = Some(progress); + + self.load_wgpu_nerdy_stuff(graphics); + + self.reload_scripts_for_current_world(); + + log::debug!("Scene '{}' loaded", scene_name); + } + + /// Switches to a new scene, clearing the current world and preparing to load the new scene. + pub fn switch_to(&mut self, scene_progress: IsSceneLoaded, graphics: &mut RenderContext) { + log::debug!("Switching to new scene requested: {}", scene_progress.requested_scene); + + if scene_progress.is_everything_loaded() { + if let Some(new_world) = self.pending_world.take() { + self.world = new_world; + } + if let Some(new_camera) = self.pending_camera.take() { + self.active_camera = Some(new_camera); + } + + self.load_wgpu_nerdy_stuff(graphics); + self.reload_scripts_for_current_world(); + + self.current_scene = Some(scene_progress.requested_scene.clone()); + } + } +} + +pub struct DisplaySettings { + pub window_mode: WindowMode, + pub maintain_aspect_ratio: bool, + pub vsync: bool, +} + +pub enum WindowMode { + Windowed, + Maximized, + Fullscreen, + BorderlessFullscreen, +} @@ -1,9 +1,3 @@ -mod input; -mod scene; -mod debug; -mod utils; -mod command; - use crate::scene::RuntimeScene; use app_dirs2::AppInfo; use dropbear_engine::future::FutureQueue; @@ -1,317 +1,110 @@ use std::collections::HashMap; -use std::env; -use std::path::PathBuf; -use std::sync::Arc; - +use hecs::{Entity}; +use wgpu::Color; +use wgpu::util::DeviceExt; +use winit::event_loop::ActiveEventLoop; 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::lighting::{Light, LightComponent}; +use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE}; 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 dropbear_engine::asset::ASSET_REGISTRY; +use dropbear_engine::wgpu; use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::egui::{self, CentralPanel, Frame, UiBuilder}; +use eucalyptus_core::command::CommandBufferPoller; +use eucalyptus_core::egui; +use eucalyptus_core::egui::{CentralPanel, MenuBar, TopBottomPanel}; use eucalyptus_core::hierarchy::EntityTransformExt; -use eucalyptus_core::input::InputState; -use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr}; -use eucalyptus_core::runtime::RuntimeProjectConfig; -use eucalyptus_core::scene::SceneConfig; -use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; -use eucalyptus_core::states::{Camera3D, Light as LightConfig, CustomProperties, Script, SerializedMeshRenderer}; -use eucalyptus_core::traits::registry::ComponentRegistry; -use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER}; -use hecs::{Entity, World}; -use parking_lot::Mutex; -use tokio::sync::oneshot; -use tokio::sync::oneshot::error::TryRecvError; -use crate::ConfigFile; - -/// The scene that the redback-runtime uses. -pub(crate) struct RuntimeScene { - #[allow(dead_code)] - project_config: RuntimeProjectConfig, - window_config: ConfigFile, - 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, - pub scene_command: SceneCommand, - - current_scene: Option<String>, - pub(crate) pending_scene_switch: Option<String>, - world_receiver: Option<oneshot::Receiver<World>>, - world_load_handle: Option<FutureHandle>, - pub window: Option<Arc<Window>>, -} - -impl RuntimeScene { - /// Creates a new instance of [`RuntimeScene`] - pub fn new(project_config: RuntimeProjectConfig, window_config: ConfigFile) -> anyhow::Result<Self> { - eucalyptus_core::utils::start_deadlock_detector(); - - 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 component_registry = Self::build_component_registry(); - let script_target = Self::detect_script_target(&project_config.project_name); - - let result = Self { - project_config: project_config.clone(), - window_config: window_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, - pending_scene_switch: None, - component_registry, - script_manager: ScriptManager::new()?, - script_target, - scripts_ready: false, - scene_command: Default::default(), - world_receiver: None, - world_load_handle: None, - window: None, - }; +use eucalyptus_core::states::PROJECT; +use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult, IsSceneLoaded}; +use crate::Runtime; - Ok(result) - } - - fn build_component_registry() -> Arc<ComponentRegistry> { - let mut component_registry = ComponentRegistry::new(); - component_registry.register_with_default::<EntityTransform>(); - component_registry.register_with_default::<CustomProperties>(); - component_registry.register_with_default::<LightConfig>(); - component_registry.register_with_default::<Script>(); - component_registry.register_with_default::<SerializedMeshRenderer>(); - - component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>( - |_, _, renderer| { - Some(SerializedMeshRenderer { - handle: renderer.handle().path.clone(), - material_override: renderer.material_overrides().to_vec(), - }) - }, - ); - - component_registry.register_converter::<CameraComponent, Camera3D, _>( - |world, entity, component| { - let Ok(camera) = world.get::<&Camera>(entity) else { - log::debug!( - "Camera component without matching Camera found on entity {:?}", - entity - ); - return None; - }; +impl Scene for Runtime { + fn load(&mut self, graphics: &mut RenderContext) { + if self.current_scene.is_none() { + let initial_scene = if let Some(s) = &self.initial_scene { + s.clone() + } else { + let proj = PROJECT.read(); + proj.runtime_settings.initial_scene.clone().expect("No initial scene set in project settings") + }; - Some(Camera3D::from_ecs_camera(&camera, component)) - }, - ); + log::debug!("Loading initial scene: {}", initial_scene); - Arc::new(component_registry) - } + let first_time = IsSceneLoaded::new_first_time(initial_scene); - fn detect_script_target(project_name: &str) -> Option<ScriptTarget> { - if let Ok(path) = env::var("REDBACK_SCRIPT_PATH") { - let candidate = PathBuf::from(path); - if candidate.exists() { - return if candidate - .extension() - .map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) - { - Some(ScriptTarget::JVM { - library_path: candidate, - }) - } else { - Some(ScriptTarget::Native { - library_path: candidate, - }) - }; - } + self.request_async_scene_load(graphics, first_time); } + } - if let Ok(mut exe_path) = env::current_exe() { - exe_path.pop(); - let project_root = exe_path; + fn update(&mut self, dt: f32, graphics: &mut RenderContext) { + graphics.shared.future_queue.poll(); + self.poll(graphics); - let preferred_jar = project_root.join(format!("{project_name}.jar")); - if preferred_jar.exists() { - return Some(ScriptTarget::JVM { - library_path: preferred_jar, - }); + if let Some(ref progress) = self.scene_progress { + if !progress.scene_handle_requested && self.world_receiver.is_none() && self.scene_loading_handle.is_none() { + log::debug!("Starting async load for scene: {}", progress.requested_scene); + let scene_to_load = IsSceneLoaded::new(progress.requested_scene.clone()); + self.request_async_scene_load(graphics, scene_to_load); } + } - if let Ok(entries) = std::fs::read_dir(&project_root) { - for entry in entries.flatten() { - let path = entry.path(); - if path - .extension() - .map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) - { - return Some(ScriptTarget::JVM { library_path: path }); + if let Some(mut receiver) = self.world_receiver.take() { + if let Ok(loaded_world) = receiver.try_recv() { + self.pending_world = Some(Box::new(loaded_world)); + log::debug!("World received"); + if let Some(ref mut progress) = self.scene_progress { + progress.world_loaded = true; + + if progress.camera_received { + if let Some(id) = progress.id { + let mut loader = SCENE_LOADER.lock(); + if let Some(entry) = loader.get_entry_mut(id) { + entry.result = SceneLoadResult::Success; + } + } } } + } else { + self.world_receiver = Some(receiver); } + } - let native_path = project_root.join(format!("{project_name}.dll")); - if native_path.exists() { - return Some(ScriptTarget::Native { - library_path: native_path, - }); - } - - #[cfg(not(target_os = "windows"))] - { - let so_path = project_root.join(format!("lib{project_name}.so")); - if so_path.exists() { - return Some(ScriptTarget::Native { - library_path: so_path, - }); + if let Some(handle) = self.scene_loading_handle.take() { + if let Some(cam) = graphics.shared.future_queue.exchange_owned_as::<Entity>(&handle) { + self.pending_camera = Some(cam); + log::debug!("Camera entity received: {:?}", cam); + if let Some(ref mut progress) = self.scene_progress { + progress.camera_received = true; + + if progress.world_loaded { + if let Some(id) = progress.id { + let mut loader = SCENE_LOADER.lock(); + if let Some(entry) = loader.get_entry_mut(id) { + entry.result = SceneLoadResult::Success; + } + } + } } + } else { + self.scene_loading_handle = Some(handle) } } - None - } - - fn start_initial_scene_load(&mut self, graphics: &mut RenderContext) -> anyhow::Result<()> { - let scene_name = if self.scenes.contains_key(&self.initial_scene) { - self.initial_scene.clone() - } else if let Some(first_scene) = self.scenes.values().next() { - log::warn!( - "Initial scene '{}' not found, falling back to '{}'", - self.initial_scene, - first_scene.scene_name - ); - first_scene.scene_name.clone() - } else { - anyhow::bail!("No scenes packaged with runtime config"); - }; - - self.queue_scene_load(&scene_name, graphics) - } - - fn queue_scene_load(&mut self, scene_name: &str, graphics: &mut RenderContext) -> anyhow::Result<()> { - self.cleanup_scene_resources(graphics); - - let scene = self - .scenes - .get(scene_name) - .cloned() - .ok_or_else(|| anyhow::anyhow!("Scene '{}' not found in runtime package", scene_name))?; - - // self.reset_state_for_scene_load(graphics); - - let (world_sender, world_receiver) = oneshot::channel(); - self.world_receiver = Some(world_receiver); - - let graphics_shared = graphics.shared.clone(); - let active_camera = self.active_camera.clone(); - let component_registry = self.component_registry.clone(); - let scene_name_owned = scene.scene_name.clone(); - - let handle = graphics.shared.future_queue.push(async move { - let mut temp_world = World::new(); - match scene - .load_into_world( - &mut temp_world, - graphics_shared.clone(), - Some(component_registry.as_ref()), - None, - true, - ) - .await - { - Ok(camera_entity) => { - let mut active = active_camera.lock(); - *active = Some(camera_entity); - log::info!("Loaded scene '{}'", scene_name_owned); - log::debug!("Checkpoint 2: Camera entity: {:?}", camera_entity); - } - Err(err) => { - panic!("Failed to load scene '{}': {}", scene_name_owned, err); + if let Some(ref progress) = self.scene_progress { + if progress.is_everything_loaded() { + if self.current_scene.as_ref() != Some(&progress.requested_scene) { + self.switch_to(progress.clone(), graphics); } } - - if world_sender.send(temp_world).is_err() { - panic!( - "Scene loader dropped before world delivery for '{}'", - scene_name_owned - ); - } - }); - - self.world_load_handle = Some(handle); - self.current_scene = Some(scene_name.to_string()); - - Ok(()) - } - - fn cleanup_scene_resources(&mut self, graphics: &mut RenderContext) { - if let Some(handle) = self.world_load_handle.take() { - graphics.shared.future_queue.cancel(&handle); - } - - self.world_receiver = None; - self.scripts_ready = false; - self.active_camera.lock().take(); - self.world.clear(); - - self.render_pipeline = None; - self.light_manager = LightManager::new(); - - { - let mut cache = MODEL_CACHE.lock(); - cache.clear(); } - // Drop cached asset registry entries so models/materials/meshes from the previous scene - // do not linger across scene loads. - ASSET_REGISTRY.clear_cached_assets(); - } - - fn poll_scene_loading(&mut self, graphics: &mut RenderContext) { - if let Some(mut receiver) = self.world_receiver.take() { - match receiver.try_recv() { - Ok(world) => { - self.world = Box::new(world); - self.initialise_rendering(graphics); - self.prepare_scripts(); - } - Err(TryRecvError::Empty) => { - self.world_receiver = Some(receiver); - } - Err(TryRecvError::Closed) => { - panic!("Scene loading task ended before delivering world"); - } + if self.scripts_ready { + if let Err(e) = self.script_manager.update_script(self.world.as_mut(), dt) { + panic!("Script update error: {}", e); } } - } - fn update_world_state(&mut self, graphics: &mut RenderContext) { { let mut query = self.world.query::<(&mut MeshRenderer, &Transform)>(); for (_entity, (renderer, transform)) in query.iter() { @@ -334,7 +127,6 @@ impl RuntimeScene { } { - // Update lights using their standalone Transform (not EntityTransform) let mut light_query = self.world.query::<(&mut LightComponent, &Transform, &mut Light)>(); for (_, (light_comp, transform, light)) in light_query.iter() { light.update(light_comp, transform); @@ -354,211 +146,85 @@ impl RuntimeScene { self.light_manager .update(graphics.shared.clone(), &self.world); - } - - fn initialise_rendering(&mut self, graphics: &mut RenderContext) { - if self.render_pipeline.is_some() { - return; - } - - let Some(active_camera) = *self.active_camera.lock() else { - return; - }; - - let camera = if let Ok(mut q) = self - .world - .query_one::<(&Camera, &CameraComponent)>(active_camera) - { - q.get().map(|(cam, _)| cam.clone()) - } else { - None - }; - - let Some(camera) = camera else { - return; - }; - - self.light_manager - .create_light_array_resources(graphics.shared.clone()); - - let shader = Shader::new( - graphics.shared.clone(), - shader::shader_wesl::SHADER_SHADER, - Some("runtime_viewport"), - ); - - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.shared.texture_bind_layout.clone(), - camera.layout(), - self.light_manager.layout(), - ], - None, - ); - self.render_pipeline = Some(pipeline); - - self.light_manager.create_render_pipeline( - graphics.shared.clone(), - shader::shader_wesl::LIGHT_SHADER, - &camera, - Some("light_pipeline"), - ); - - self.window = Some(graphics.shared.window.clone()); - } - - fn prepare_scripts(&mut self) { - self.scripts_ready = false; - let Some(target) = self.script_target.clone() else { - log::debug!("No script target detected; skipping script setup"); - return; - }; - - let mut entity_tag_map: HashMap<String, Vec<Entity>> = HashMap::new(); - for (entity_id, script) in self.world.query::<&Script>().iter() { - for tag in &script.tags { - entity_tag_map.entry(tag.clone()).or_default().push(entity_id); - } - } - - log::debug!("Awaiting for script library to be initialised"); - - if let Err(err) = self.script_manager.init_script( - self.window_config.jvm_args.clone(), - entity_tag_map.clone(), - target.clone(), - ) { - panic!("Failed to init script manager: {}", err); - } - - log::debug!("Loaded!"); - let world_ptr = self.world.as_mut() as WorldPtr; - let input_ptr = self.input_state.as_mut() as InputStatePtr; - let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr; - - if let Err(err) = self - .script_manager - .load_script(world_ptr, input_ptr, graphics_ptr) - { - panic!("Failed to load scripts: {}", err); - } - - self.scripts_ready = true; - } -} - -impl Scene for RuntimeScene { - fn load(&mut self, graphics: &mut RenderContext) { - self.window = Some(graphics.shared.window.clone()); - - if let Err(err) = self.start_initial_scene_load(graphics) { - panic!("Unable to load initial scene: {}", err); - } - } - - fn update(&mut self, dt: f32, graphics: &mut RenderContext) { - graphics.shared.future_queue.poll(); - - self.poll_scene_loading(graphics); - self.poll(graphics); - - if self.world_receiver.is_none() { - if let Some(scene_name) = self.pending_scene_switch.take() { - if let Err(err) = self.queue_scene_load(&scene_name, graphics) { - log::error!("Failed to switch scene contents to '{}': {}", scene_name, err); + CentralPanel::default().show(&graphics.shared.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| { + ui.label("Loading scene..."); + }); + return; } } - } - CentralPanel::default().frame(Frame::new()).show(&graphics.shared.get_egui_context(), |ui| { - if self.render_pipeline.is_none() { - ui.label("Loading scene..."); - } + let texture_id = *graphics.shared.texture_id; - if self.render_pipeline.is_none() { - return; - } + let available_size = ui.available_size(); + let available_rect = ui.available_rect_before_wrap(); - self.update_world_state(graphics); + if let Some(active_camera) = self.active_camera { + if let Ok(cam) = self.world.query_one_mut::<&mut Camera>(active_camera) { + if !self.has_initial_resize_done { + cam.aspect = (available_size.x / available_size.y) as f64; - let egui_ctx = graphics.shared.get_egui_context(); - let egui_wants_input = egui_ctx.wants_pointer_input() || egui_ctx.wants_keyboard_input(); - - if egui_wants_input { - self.input_state.pressed_keys.clear(); - self.input_state.mouse_button.clear(); - } - - 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) } { - panic!("Script update failed: {}", err); - } - } + self.has_initial_resize_done = true; + } - let texture_id = *graphics.shared.texture_id; - let available_size = ui.available_rect_before_wrap().size(); - - let is_fullscreen = self.window_config.windowed_mode.is_fullscreen(); - - let viewport_aspect = self.viewport_resolution.0 as f32 / self.viewport_resolution.1 as f32; - let available_aspect = available_size.x / available_size.y; - - let active_camera: Option<Entity> = *self.active_camera.lock(); - if let Some(cam_ent) = active_camera { - if let Ok(mut q) = self.world.query_one::<&mut Camera>(cam_ent) { - if let Some(camera) = q.get() { - if is_fullscreen { - camera.aspect = viewport_aspect as f64; - } else { - camera.aspect = available_aspect as f64; - } - camera.update_view_proj(); - camera.update(graphics.shared.clone()); + if !self.display_settings.maintain_aspect_ratio { + cam.aspect = (available_size.x / available_size.y) as f64; } - } - } - - let (display_width, display_height) = if is_fullscreen { - if available_aspect > viewport_aspect { - (available_size.x, available_size.x / viewport_aspect) + cam.update_view_proj(); + cam.update(graphics.shared.clone()); + + 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; + + let image_rect = egui::Rect::from_center_size( + egui::pos2(center_x, center_y), + egui::vec2(display_width, display_height), + ); + + ui.allocate_exact_size(available_size, egui::Sense::hover()); + + ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| { + ui.add(egui::Image::new(egui::load::SizedTexture { + id: texture_id, + size: egui::vec2(display_width, display_height), + })); + }); } else { - (available_size.y * viewport_aspect, available_size.y) + log::warn!("No such camera exists in the world"); } } else { - (available_size.x, available_size.y) - }; - - let rect = ui.available_rect_before_wrap(); - let x_offset = (available_size.x - display_width) / 2.0; - let y_offset = (available_size.y - display_height) / 2.0; - let image_rect = egui::Rect::from_min_size( - egui::pos2(rect.min.x + x_offset, rect.min.y + y_offset), - egui::vec2(display_width, display_height), - ); - - ui.scope_builder(UiBuilder::new().max_rect(image_rect), |ui| { - ui.add(egui::Image::new(egui::load::SizedTexture { - id: texture_id, - size: egui::vec2(display_width, display_height), - })); - }); - - self.input_state.window = self.window.clone(); - self.input_state.mouse_delta = None; + log::warn!("No active camera available"); + } }); + + self.input_state.mouse_delta = None; } - fn render(&mut self, graphics: &mut RenderContext) { - if self.render_pipeline.is_none() { - self.initialise_rendering(graphics); + fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) { + if self.scripts_ready { + if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) { + panic!("Script physics update error: {}", e); + } } + } - let Some(active_camera) = *self.active_camera.lock() else { + fn render(&mut self, graphics: &mut RenderContext) { + let Some(active_camera) = self.active_camera else { return; }; + log_once::debug_once!("Active camera found: {:?}", active_camera); let q = if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { query.get().cloned() @@ -569,18 +235,19 @@ impl Scene for RuntimeScene { let Some(camera) = q else { return; }; - - // camera.debug_camera_state(); - // println!("{:#?}", self.project_config); + log_once::debug_once!("Camera ready"); + log_once::debug_once!("Camera currently being viewed: {}", camera.label); let Some(pipeline) = &self.render_pipeline else { + log_once::warn_once!("Render pipeline not ready"); return; }; + log_once::debug_once!("Pipeline ready"); let clear_color = Color { - r: 0.05, - g: 0.07, - b: 0.10, + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, a: 1.0, }; @@ -603,6 +270,15 @@ impl Scene for RuntimeScene { }; { + let mut query = self.world.query::<(&mut LightComponent, &dropbear_engine::entity::Transform, &mut Light)>(); + for (_, (light_component, transform, light)) in query.iter() { + light.update(light_component, transform); + } + } + + self.light_manager.update(graphics.shared.clone(), &self.world); + + { let mut render_pass = graphics.clear_colour(clear_color); if let Some(light_pipeline) = &self.light_manager.pipeline { render_pass.set_pipeline(light_pipeline); @@ -660,21 +336,9 @@ impl Scene for RuntimeScene { } } - 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.world_receiver = None; - self.world_load_handle = None; - self.script_target = None; - } + fn exit(&mut self, _event_loop: &ActiveEventLoop) {} fn run_command(&mut self) -> SceneCommand { std::mem::replace(&mut self.scene_command, SceneCommand::None) } -} +}