use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Instant;
use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Mut, Resource, World};
use vulkano::format::Format;
use vulkano::image::ImageUsage;
use vulkano::swapchain::Surface;
use vulkano::VulkanError;
use vulkano_util::context::VulkanoContext;
use vulkano_util::window::{VulkanoWindows, WindowDescriptor};
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::{DeviceEvent, DeviceId, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::PhysicalKey;
use winit::window::{CursorGrabMode, Fullscreen, Window, WindowId};
use crate::rendering::frame_pacer::{select_present_mode, FramePacer};
use crate::rendering::render_scale::ScaledTarget;
use crate::rendering::scene_renderer::SceneRenderer;
use crate::runtime::{
apply_gpu_state_samples, load_scene, load_scene_document,
record_gpu_state_hashes, route_gpu_physics_events, scene_document,
write_atomic, AppError, EventQueue, FrameTime, GpuEventRegistry,
GpuPhysicsClassWatches, GpuPhysicsEvent, GpuPhysicsEventsLost,
GpuPhysicsRule, GpuPhysicsWatch, HybridPhysicsPlugin, MeshRenderer, Name,
PhysicsBackendStatus, Plugin, RenderExtractPlugin, RenderSettings,
RenderWorld, RuntimeInput, SceneDocument, SceneLoadMode, ScheduleStage,
};
use crate::{App, AssetPlugin, AssetServer, Transform};
pub type GameResult<T = ()> = Result<T, Box<dyn Error>>;
#[derive(Clone, Debug)]
pub struct GpuBodySettings {
pub solver: crate::runtime::PhysicsSolver,
pub custom_shader: Option<String>,
pub rigid_body: crate::runtime::RigidBody,
pub collider: crate::runtime::Collider,
pub collision_layers: crate::runtime::CollisionLayers,
pub sync: crate::runtime::PhysicsSyncMode,
}
impl Default for GpuBodySettings {
fn default() -> Self {
Self {
solver: crate::runtime::PhysicsSolver::Full,
custom_shader: None,
rigid_body: crate::runtime::RigidBody::default(),
collider: crate::runtime::Collider::default(),
collision_layers: crate::runtime::CollisionLayers::default(),
sync: crate::runtime::PhysicsSyncMode::default(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CubeSpawn {
pub classes: crate::runtime::ObjectClasses,
}
impl CubeSpawn {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.classes.add(class);
self
}
}
#[derive(Clone, Debug, Default)]
pub struct SphereSpawn {
pub classes: crate::runtime::ObjectClasses,
pub subdivisions: u32,
}
impl SphereSpawn {
#[must_use]
pub fn new() -> Self {
Self {
subdivisions: 16,
..Self::default()
}
}
#[must_use]
pub fn subdivisions(mut self, value: u32) -> Self {
self.subdivisions = value.clamp(2, 128);
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.classes.add(class);
self
}
}
#[derive(Resource, Clone, Default)]
struct SphereMeshCache(
HashMap<u32, crate::assets::Handle<crate::assets::MeshAsset>>,
);
#[derive(Resource)]
struct StartingScene(SceneDocument);
#[derive(Resource)]
pub(crate) struct ProjectFolder(pub(crate) PathBuf);
#[derive(Clone)]
pub struct GameSnapshot {
scene: SceneDocument,
once: GameOnceState,
physics: Option<crate::runtime::PhysicsWorld>,
next_order: Option<crate::runtime::NextSpawnOrder>,
fluids: Vec<(uuid::Uuid, crate::runtime::Fluid)>,
objects:
Vec<(uuid::Uuid, Entity, Option<crate::runtime::SpawnOrder>, bool)>,
}
pub(crate) fn restart_scene(world: &mut World) -> bool {
let Some(start) = world.remove_resource::<StartingScene>() else {
return false;
};
let loaded = load_scene_document(world, &start.0, SceneLoadMode::Replace);
world.insert_resource(start);
if let Err(error) = loaded {
eprintln!("restart: the starting scene no longer loads: {error}");
return false;
}
forget_old_scene(world);
true
}
fn user_data_path(key: &str) -> std::io::Result<PathBuf> {
let relative = Path::new(key);
let inside = relative
.components()
.all(|part| matches!(part, std::path::Component::Normal(_)));
if !inside || key.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("data key `{key}` must be a relative path without `..`"),
));
}
Ok(crate::project::user_data_folder().join(relative))
}
#[derive(Resource, Clone, Default)]
struct GameOnceState(HashSet<String>);
#[derive(Resource, Default)]
struct WindowRequest {
fullscreen: bool,
size: Option<[u32; 2]>,
}
pub struct GameScene<'world> {
pub(crate) world: &'world mut World,
}
impl GameScene<'_> {
pub fn once(
&mut self,
key: impl Into<String>,
action: impl FnOnce(&mut GameScene<'_>),
) {
let key = key.into();
let should_run = self
.world
.get_resource_or_insert_with(GameOnceState::default)
.0
.insert(key);
if should_run {
action(self);
}
}
pub fn load_text(&self, path: &str) -> std::io::Result<String> {
let folder = self
.world
.get_resource::<ProjectFolder>()
.map(|folder| folder.0.join("assets"))
.unwrap_or_else(|| PathBuf::from("assets"));
std::fs::read_to_string(folder.join(path))
}
pub fn play_sound(
&mut self,
clip: &str,
volume: f32,
) -> crate::runtime::SoundId {
self.play_sound_with(
clip,
crate::runtime::Sound {
volume,
..Default::default()
},
)
}
pub fn play_sound_looped(
&mut self,
clip: &str,
volume: f32,
) -> crate::runtime::SoundId {
self.play_sound_with(
clip,
crate::runtime::Sound {
volume,
looped: true,
..Default::default()
},
)
}
pub fn play_sound_with(
&mut self,
clip: &str,
mut sound: crate::runtime::Sound,
) -> crate::runtime::SoundId {
if let Some(position) = sound.position {
if let Some((camera, _)) = self.active_camera() {
let matrix = world_matrix(self.world, camera);
let ear = matrix.transform_point(&nalgebra::Point3::origin());
let right = matrix
.transform_vector(&nalgebra::Vector3::x())
.normalize();
let offset = nalgebra::Point3::from(position) - ear;
let distance = offset.norm();
if distance > 1e-4 {
sound.pan = offset.dot(&right) / distance;
}
sound.volume *= (2.0 / distance).min(1.0);
}
}
let tick = self
.world
.get_resource::<FrameTime>()
.map_or(0, |time| time.fixed_tick);
self.audio().play(clip, &sound, tick)
}
pub fn set_sound_volume(
&mut self,
id: crate::runtime::SoundId,
volume: f32,
fade: f32,
) {
self.audio().set_volume(id, volume, fade);
}
pub fn set_bus_volume(&mut self, bus: &str, volume: f32, fade: f32) {
self.audio().set_bus_volume(bus, volume, fade);
}
pub fn stop_sound(&mut self, id: crate::runtime::SoundId) {
self.audio().stop(id);
}
pub fn stop_all_sounds(&mut self) {
self.audio().stop_all();
}
pub fn set_master_volume(&mut self, volume: f32) {
self.audio().set_master_volume(volume);
}
pub fn sounds_requested(&mut self) -> u64 {
self.audio().requested()
}
fn audio(&mut self) -> Mut<'_, crate::runtime::AudioQueue> {
self.world
.get_resource_or_insert_with(crate::runtime::AudioQueue::default)
}
pub fn restart(&mut self) -> bool {
restart_scene(self.world)
}
pub fn load_scene(
&mut self,
path: impl AsRef<Path>,
) -> Result<(), crate::runtime::SceneIoError> {
let path = match self.world.get_resource::<ProjectFolder>() {
Some(folder) => folder.0.join(path),
None => path.as_ref().to_path_buf(),
};
load_scene(self.world, &path, SceneLoadMode::Replace)?;
let start = scene_document(self.world, "start")?;
self.world.insert_resource(StartingScene(start));
forget_old_scene(self.world);
Ok(())
}
pub fn snapshot(
&mut self,
) -> Result<GameSnapshot, crate::runtime::SceneIoError> {
let scene = scene_document(self.world, "snapshot")?;
let mut query = self.world.query::<(
Entity,
&crate::runtime::SceneId,
Option<&crate::runtime::SpawnOrder>,
bevy_ecs::query::Has<crate::runtime::Sleeping>,
)>();
let objects = query
.iter(self.world)
.map(|(entity, id, order, asleep)| {
(id.0, entity, order.copied(), asleep)
})
.collect();
Ok(GameSnapshot {
scene,
once: self
.world
.get_resource::<GameOnceState>()
.cloned()
.unwrap_or_default(),
physics: self
.world
.get_resource::<crate::runtime::PhysicsWorld>()
.cloned(),
next_order: self
.world
.get_resource::<crate::runtime::NextSpawnOrder>()
.copied(),
fluids: crate::runtime::capture_fluids(self.world),
objects,
})
}
pub fn restore(
&mut self,
snapshot: &GameSnapshot,
) -> Result<(), crate::runtime::SceneIoError> {
load_scene_document(
self.world,
&snapshot.scene,
SceneLoadMode::Replace,
)?;
let mut query =
self.world.query::<(Entity, &crate::runtime::SceneId)>();
let now: HashMap<uuid::Uuid, Entity> = query
.iter(self.world)
.map(|(entity, id)| (id.0, entity))
.collect();
let mut renamed = HashMap::new();
for (id, old, order, asleep) in &snapshot.objects {
let Some(&entity) = now.get(id) else {
continue;
};
renamed.insert(*old, entity);
let mut entity = self.world.entity_mut(entity);
match order {
Some(order) => entity.insert(*order),
None => entity.remove::<crate::runtime::SpawnOrder>(),
};
if *asleep {
entity.insert(crate::runtime::Sleeping);
}
}
if let Some(next) = snapshot.next_order {
self.world.insert_resource(next);
}
if let Some(physics) = &snapshot.physics {
let mut physics = physics.clone();
physics.rename_bodies(&renamed);
self.world.insert_resource(physics);
}
crate::runtime::restore_fluids(self.world, &snapshot.fluids);
self.world.insert_resource(snapshot.once.clone());
self.world.remove_resource::<SceneNameIndex>();
Ok(())
}
#[cfg(feature = "ui")]
#[must_use]
pub fn ui(&self) -> egui::Context {
self.world
.resource::<crate::runtime::RuntimeUi>()
.context()
.clone()
}
pub fn spawn_cube(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &CubeSpawn,
) -> Entity {
let (mesh, material) = {
let assets = self.world.resource::<AssetServer>();
(assets.fallback_mesh, assets.fallback_material)
};
self.spawn_renderable(
name.into(),
transform,
mesh,
material,
template.classes.clone(),
)
}
pub fn spawn_cube_with_material(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &CubeSpawn,
material: crate::assets::Handle<crate::assets::MaterialAsset>,
) -> Entity {
let mesh = self.world.resource::<AssetServer>().fallback_mesh;
self.spawn_renderable(
name.into(),
transform,
mesh,
material,
template.classes.clone(),
)
}
pub fn create_material(
&mut self,
material: crate::assets::MaterialAsset,
) -> crate::assets::Handle<crate::assets::MaterialAsset> {
self.world
.resource_mut::<AssetServer>()
.materials
.insert(material)
}
pub fn set_background_color(&mut self, color: [f32; 4]) {
self.world.resource_mut::<RenderSettings>().background_color = color;
}
pub fn set_reflections(&mut self, enabled: bool) {
self.world.resource_mut::<RenderSettings>().reflections = enabled;
}
pub fn set_render_scale(&mut self, scale: f32) {
if scale.is_finite() {
let (low, high) =
crate::rendering::render_scale::RENDER_SCALE_RANGE;
self.world.resource_mut::<RenderSettings>().render_scale =
scale.clamp(low, high);
}
}
pub fn set_pixelated(&mut self, pixelated: bool) {
self.world.resource_mut::<RenderSettings>().pixelated = pixelated;
}
#[must_use]
pub fn render_scale(&self) -> f32 {
self.world.resource::<RenderSettings>().render_scale
}
pub fn set_vsync(&mut self, enabled: bool) {
self.world.resource_mut::<RenderSettings>().vsync = enabled;
}
pub fn set_max_fps(&mut self, fps: Option<u32>) {
let mut settings = self.world.resource_mut::<RenderSettings>();
settings.limit_fps = fps.is_some();
if let Some(fps) = fps {
settings.max_fps = fps.max(1);
}
}
pub fn set_fullscreen(&mut self, fullscreen: bool) {
self.world
.get_resource_or_insert_with(WindowRequest::default)
.fullscreen = fullscreen;
}
#[must_use]
pub fn fullscreen(&self) -> bool {
self.world
.get_resource::<WindowRequest>()
.is_some_and(|request| request.fullscreen)
}
pub fn set_window_size(&mut self, size: [u32; 2]) {
self.world
.get_resource_or_insert_with(WindowRequest::default)
.size = Some(size.map(|side| side.max(1)));
}
pub fn spawn_sphere(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &SphereSpawn,
) -> Entity {
let subdivisions = template.subdivisions;
let mesh = self
.world
.get_resource::<SphereMeshCache>()
.and_then(|cache| cache.0.get(&subdivisions).copied())
.unwrap_or_else(|| {
let mesh =
self.world.resource_mut::<AssetServer>().meshes.insert(
crate::assets::procedural_sphere_mesh(subdivisions),
);
self.world
.get_resource_or_insert_with(SphereMeshCache::default)
.0
.insert(subdivisions, mesh);
mesh
});
self.spawn_renderable(
name.into(),
transform,
mesh,
self.world.resource::<AssetServer>().fallback_material,
template.classes.clone(),
)
}
pub fn spawn_sphere_with_material(
&mut self,
name: impl Into<String>,
transform: Transform,
template: &SphereSpawn,
material: crate::assets::Handle<crate::assets::MaterialAsset>,
) -> Entity {
let subdivisions = template.subdivisions;
let mesh = self
.world
.get_resource::<SphereMeshCache>()
.and_then(|cache| cache.0.get(&subdivisions).copied())
.unwrap_or_else(|| {
let mesh =
self.world.resource_mut::<AssetServer>().meshes.insert(
crate::assets::procedural_sphere_mesh(subdivisions),
);
self.world
.get_resource_or_insert_with(SphereMeshCache::default)
.0
.insert(subdivisions, mesh);
mesh
});
self.spawn_renderable(
name.into(),
transform,
mesh,
material,
template.classes.clone(),
)
}
fn spawn_renderable(
&mut self,
name: String,
transform: Transform,
mesh: crate::assets::Handle<crate::assets::MeshAsset>,
material: crate::assets::Handle<crate::assets::MaterialAsset>,
classes: crate::runtime::ObjectClasses,
) -> Entity {
if find_named_entity(self.world, &name).is_some() {
panic!("scene object `{name}` already exists");
}
let order = crate::runtime::next_spawn_order(self.world);
let entity = self
.world
.spawn((
crate::runtime::SceneId::new(),
order,
Name(name.clone()),
transform,
crate::runtime::MeshRenderer {
mesh,
material,
cast_shadows: true,
receive_shadows: true,
},
crate::runtime::Visibility::default(),
))
.id();
if !classes.names.is_empty() {
self.world.entity_mut(entity).insert(classes);
}
self.world
.resource_mut::<SceneNameIndex>()
.entities
.insert(name, entity);
entity
}
pub fn apply_gpu_physics_to_class(
&mut self,
class: &str,
settings: &GpuBodySettings,
) -> usize {
let entities = {
let mut query = self
.world
.query::<(Entity, &crate::runtime::ObjectClasses)>();
query
.iter(self.world)
.filter_map(|(entity, classes)| {
classes.contains(class).then_some(entity)
})
.collect::<Vec<_>>()
};
for entity in &entities {
self.world.entity_mut(*entity).insert((
crate::runtime::PhysicsBody {
simulation: crate::runtime::SimulationClass::Gpu,
solver: settings.solver,
custom_shader: settings.custom_shader.clone(),
},
settings.rigid_body,
settings.collider,
settings.collision_layers,
settings.sync,
));
}
entities.len()
}
fn edit_body(
&mut self,
name: &str,
edit: impl FnOnce(&mut crate::runtime::RigidBody),
) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let Some(mut rigid_body) =
self.world.get_mut::<crate::runtime::RigidBody>(entity)
else {
return false;
};
edit(&mut rigid_body);
self.world
.entity_mut(entity)
.remove::<crate::runtime::Sleeping>();
true
}
pub fn set_linear_velocity(
&mut self,
name: &str,
velocity: [f32; 3],
) -> bool {
self.edit_body(name, |body| body.linear_velocity = velocity)
}
#[must_use]
pub fn linear_velocity(&mut self, name: &str) -> Option<[f32; 3]> {
let entity = find_named_entity(self.world, name)?;
self.world
.get::<crate::runtime::RigidBody>(entity)
.map(|body| body.linear_velocity)
}
pub fn set_angular_velocity(
&mut self,
name: &str,
velocity: [f32; 3],
) -> bool {
self.edit_body(name, |body| body.angular_velocity = velocity)
}
#[must_use]
pub fn angular_velocity(&mut self, name: &str) -> Option<[f32; 3]> {
let entity = find_named_entity(self.world, name)?;
self.world
.get::<crate::runtime::RigidBody>(entity)
.map(|body| body.angular_velocity)
}
pub fn reset_body(&mut self, name: &str) -> bool {
let reset = self.edit_body(name, |body| {
body.linear_velocity = [0.0; 3];
body.angular_velocity = [0.0; 3];
});
if let (true, Some(entity)) =
(reset, find_named_entity(self.world, name))
{
if let Some(mut physics) = self
.world
.get_resource_mut::<crate::runtime::PhysicsWorld>()
{
physics.forget_body(entity);
}
}
reset
}
pub fn set_body_kind(
&mut self,
name: &str,
kind: crate::runtime::RigidBodyKind,
) -> bool {
self.edit_body(name, |body| {
if body.kind != kind
&& kind != crate::runtime::RigidBodyKind::Dynamic
{
body.linear_velocity = [0.0; 3];
body.angular_velocity = [0.0; 3];
}
body.kind = kind;
})
}
#[must_use]
pub fn initial(&self, name: &str) -> Option<InitialState> {
use crate::runtime::SceneMaterial;
let start = self.world.get_resource::<StartingScene>()?;
let entity = start
.0
.entities
.iter()
.find(|entity| entity.name.as_deref() == Some(name))?;
let material =
entity
.mesh_renderer
.as_ref()
.and_then(|renderer| match &renderer.material {
SceneMaterial::Inline(material) => Some(material),
SceneMaterial::BuiltinError => None,
});
Some(InitialState {
transform: entity
.transform
.map(Transform::from)
.unwrap_or_default(),
color: material.map(|material| material.base_color),
emissive: material.map(|material| material.emissive),
body_kind: entity.rigid_body.map(|body| body.kind),
visible: entity.visible.unwrap_or(true),
})
}
pub fn set_look(&mut self, name: &str, yaw: f32, pitch: f32) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let Some(mut player) = self
.world
.get_mut::<crate::runtime::PlayerController>(entity)
else {
return false;
};
player.yaw = yaw;
player.pitch = pitch;
true
}
pub fn object(&mut self, name: &str) -> GameObject<'_> {
if self.try_object(name).is_none() {
panic!(
"scene object `{name}` does not exist or has no Transform{}",
nearest_names_hint(self.world, name)
);
}
self.try_object(name).expect("checked above")
}
pub fn world(&mut self) -> &mut World {
self.world
}
pub fn counter(
&mut self,
name: &str,
) -> Option<Mut<'_, crate::runtime::Counter>> {
let mut counters = self.world.query::<(
&mut crate::runtime::Counter,
Option<&crate::runtime::SceneId>,
)>();
crate::runtime::find_counter(counters.iter_mut(self.world), name)
}
#[must_use]
pub fn counter_value(&self, name: &str) -> i32 {
self.read_counter(name, |counter| counter.value)
.unwrap_or_default()
}
fn read_counter<T>(
&self,
name: &str,
read: impl FnOnce(&crate::runtime::Counter) -> T,
) -> Option<T> {
let found = self
.world
.try_query::<(
&crate::runtime::Counter,
Option<&crate::runtime::SceneId>,
)>()
.and_then(|mut counters| {
crate::runtime::find_counter(counters.iter(self.world), name)
.map(read)
});
if found.is_none() {
warn_missing_counter(name);
}
found
}
pub fn add_to_counter(&mut self, name: &str, amount: i32) -> i32 {
let mut counter = self.counter_or_create(name);
counter.value += amount;
counter.value
}
pub fn set_counter(&mut self, name: &str, value: i32) {
self.counter_or_create(name).value = value;
}
fn counter_or_create(
&mut self,
name: &str,
) -> Mut<'_, crate::runtime::Counter> {
if self.counter(name).is_none() {
let order = crate::runtime::next_spawn_order(self.world);
self.world.spawn((
Name(name.to_owned()),
crate::runtime::SceneId::new(),
order,
crate::runtime::Counter {
name: name.to_owned(),
value: 0,
target: None,
},
));
}
self.counter(name).expect("created above")
}
#[must_use]
pub fn random(&self, stream: u64) -> f32 {
let tick = self
.world
.get_resource::<FrameTime>()
.map_or(0, |time| time.fixed_tick);
let seed = self
.world
.get_resource::<crate::runtime::RandomSeed>()
.copied()
.unwrap_or_default();
seed.unit(tick, crate::runtime::RandomSeed::stream("game", stream))
}
#[must_use]
pub fn counter_complete(&self, name: &str) -> bool {
self.read_counter(name, crate::runtime::Counter::complete)
.unwrap_or_default()
}
#[must_use]
pub fn pressed(&self, action: &str) -> bool {
self.world
.resource::<crate::runtime::ActionMap>()
.just_pressed(self.world.resource::<RuntimeInput>(), action)
}
#[must_use]
pub fn press_tick(&self, action: &str) -> Option<f64> {
if !self.pressed(action) {
return None;
}
let actions = self.world.resource::<crate::runtime::ActionMap>();
actions
.press_tick(self.world.resource::<RuntimeInput>(), action)
.or_else(|| {
let time = self.world.get_resource::<FrameTime>()?;
Some(time.fixed_tick as f64)
})
}
#[must_use]
pub fn held(&self, action: &str) -> bool {
self.world
.resource::<crate::runtime::ActionMap>()
.held(self.world.resource::<RuntimeInput>(), action)
}
#[must_use]
pub fn cursor(&self) -> Option<[f32; 2]> {
self.world.resource::<RuntimeInput>().cursor_position()
}
#[must_use]
pub fn viewport_size(&self) -> [f32; 2] {
self.world.resource::<RuntimeInput>().viewport_size()
}
#[must_use]
pub fn keys_pressed(&self) -> Vec<String> {
let input = self.world.resource::<RuntimeInput>();
input
.keys_pressed()
.map(|key| format!("{key:?}"))
.chain(input.pads_pressed().map(|button| format!("Pad{button:?}")))
.collect()
}
#[must_use]
pub fn stick(&self, stick: crate::runtime::Stick) -> [f32; 2] {
self.world.resource::<RuntimeInput>().stick(stick)
}
#[must_use]
pub fn binding(&mut self, action: &str) -> Vec<String> {
let mut query = self.world.query::<&crate::runtime::InputAction>();
query
.iter(self.world)
.find(|found| found.action == action)
.map(|found| found.inputs.clone())
.unwrap_or_default()
}
pub fn rebind(
&mut self,
action: &str,
inputs: &[&str],
) -> Result<(), String> {
for input in inputs {
crate::runtime::parse_input(input)?;
}
let inputs: Vec<String> =
inputs.iter().map(|&input| input.to_owned()).collect();
let mut query = self.world.query::<&mut crate::runtime::InputAction>();
if let Some(mut found) = query
.iter_mut(self.world)
.find(|found| found.action == action)
{
found.inputs = inputs;
return Ok(());
}
self.world.spawn((
Name(format!("input {action}")),
crate::runtime::SceneId::new(),
crate::runtime::InputAction {
action: action.to_owned(),
inputs,
},
));
Ok(())
}
pub fn save_data(&self, key: &str, text: &str) -> std::io::Result<()> {
let path = user_data_path(key)?;
if let Some(folder) = path.parent() {
std::fs::create_dir_all(folder)?;
}
std::fs::write(path, text)
}
#[must_use]
pub fn load_data(&self, key: &str) -> Option<String> {
std::fs::read_to_string(user_data_path(key).ok()?).ok()
}
pub fn delete_data(&self, key: &str) -> bool {
user_data_path(key).is_ok_and(|path| std::fs::remove_file(path).is_ok())
}
pub fn quit(&mut self) {
self.world
.resource_mut::<crate::runtime::ExitState>()
.requested = true;
}
pub fn set_paused(&mut self, paused: bool) {
self.world
.resource_mut::<crate::runtime::TimeControl>()
.paused = paused;
}
#[must_use]
pub fn paused(&self) -> bool {
self.world.resource::<crate::runtime::TimeControl>().paused
}
#[must_use]
pub fn counters(&self) -> Vec<(String, i32)> {
let mut counters: Vec<_> = self
.world
.try_query::<&crate::runtime::Counter>()
.map(|mut query| {
query
.iter(self.world)
.map(|counter| (counter.name.clone(), counter.value))
.collect()
})
.unwrap_or_default();
counters.sort();
counters.dedup_by(|a, b| a.0 == b.0);
counters
}
#[must_use]
pub fn raycast(
&self,
origin: [f32; 3],
direction: [f32; 3],
max_distance: f32,
) -> Option<RayHit> {
self.raycast_skipping(origin, direction, max_distance, &[])
}
#[must_use]
pub fn raycast_skipping(
&self,
origin: [f32; 3],
direction: [f32; 3],
max_distance: f32,
skip_classes: &[&str],
) -> Option<RayHit> {
let keep = |entity| {
skip_classes.is_empty()
|| self
.world
.get::<crate::runtime::ObjectClasses>(entity)
.is_none_or(|classes| {
!skip_classes
.iter()
.any(|class| classes.contains(class))
})
};
let hit = self
.world
.get_resource::<crate::runtime::PhysicsWorld>()?
.raycast_where(origin, direction, max_distance, u32::MAX, keep)?;
Some(RayHit {
name: self
.world
.get::<Name>(hit.entity)
.map(|name| name.0.clone())
.unwrap_or_default(),
point: hit.point,
normal: hit.normal,
distance: hit.distance,
})
}
#[must_use]
pub fn aim(&mut self, max_distance: f32) -> Option<RayHit> {
let (origin, forward) = self.camera_ray()?;
self.raycast(origin, forward, max_distance)
}
#[must_use]
pub fn basis(&mut self, name: &str) -> Option<[[f32; 3]; 3]> {
let entity = find_named_entity(self.world, name)?;
let matrix = world_matrix(self.world, entity);
let axis = |v: nalgebra::Vector3<f32>| -> [f32; 3] {
matrix.transform_vector(&v).normalize().into()
};
Some([
axis(nalgebra::Vector3::x()),
axis(nalgebra::Vector3::y()),
axis(-nalgebra::Vector3::z()),
])
}
#[must_use]
pub fn camera_ray(&mut self) -> Option<([f32; 3], [f32; 3])> {
let (camera, _) = self.active_camera()?;
let matrix = world_matrix(self.world, camera);
let forward = matrix
.transform_vector(&-nalgebra::Vector3::z())
.normalize();
let origin = matrix.transform_point(&nalgebra::Point3::origin());
Some((origin.coords.into(), forward.into()))
}
#[must_use]
pub fn pointer_ray(&mut self) -> Option<([f32; 3], [f32; 3])> {
let input = self.world.resource::<RuntimeInput>();
let (cursor, size) = (input.cursor_position()?, input.viewport_size());
if size.contains(&0.0) {
return None;
}
let (entity, camera) = self.active_camera()?;
let transform = crate::runtime::GlobalTransform {
matrix: world_matrix(self.world, entity).into(),
};
let ray = crate::runtime::picking::scene_ray(
cursor, [0.0; 2], size, camera, transform,
)?;
Some((ray.origin.into(), ray.direction.into()))
}
pub fn set_active_camera(&mut self, name: &str) -> bool {
let Some(chosen) =
find_named_entity(self.world, name).filter(|entity| {
self.world.get::<crate::runtime::Camera>(*entity).is_some()
})
else {
return false;
};
let mut cameras =
self.world.query::<(Entity, &mut crate::runtime::Camera)>();
for (entity, mut camera) in cameras.iter_mut(self.world) {
let active = entity == chosen;
if camera.active != active {
camera.active = active;
}
}
true
}
pub fn set_camera(
&mut self,
name: &str,
active: bool,
viewport: Option<[f32; 4]>,
) -> bool {
let Some(mut camera) =
find_named_entity(self.world, name).and_then(|entity| {
self.world.get_mut::<crate::runtime::Camera>(entity)
})
else {
return false;
};
camera.active = active;
camera.viewport = viewport;
true
}
fn active_camera(&mut self) -> Option<(Entity, crate::runtime::Camera)> {
let mut cameras =
self.world.query::<(Entity, &crate::runtime::Camera)>();
cameras
.iter(self.world)
.filter(|(_, camera)| camera.active)
.max_by_key(|(entity, camera)| {
(
camera.viewport.is_none(),
camera.priority,
std::cmp::Reverse(entity.to_bits()),
)
})
.map(|(entity, camera)| (entity, *camera))
}
pub fn despawn(&mut self, name: &str) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let _ = rusting_core::hierarchy::clear_parent(self.world, entity);
despawn_tree(self.world, entity);
true
}
pub fn spawn_copy(
&mut self,
template: &str,
name: impl Into<String>,
position: [f32; 3],
) -> Option<Entity> {
let name = name.into();
let template = find_named_entity(self.world, template)?;
let copy = copy_tree(self.world, template, Some(&name));
if let Some(mut transform) = self.world.get_mut::<Transform>(copy) {
transform.position = position;
}
if let Some(parent) = self
.world
.get::<crate::runtime::Parent>(template)
.map(|p| p.0)
{
let _ =
rusting_core::hierarchy::set_parent(self.world, copy, parent);
}
Some(copy)
}
#[must_use]
pub fn name_of(&self, entity: Entity) -> Option<String> {
self.world.get::<Name>(entity).map(|name| name.0.clone())
}
#[must_use]
pub fn has_class(&self, entity: Entity, class: &str) -> bool {
self.world
.get::<crate::runtime::ObjectClasses>(entity)
.is_some_and(|classes| classes.contains(class))
}
#[must_use]
pub fn in_class(&mut self, class: &str) -> Vec<String> {
let mut query = self
.world
.query::<(&Name, &crate::runtime::ObjectClasses)>();
let mut names: Vec<_> = query
.iter(self.world)
.filter(|(_, classes)| classes.contains(class))
.map(|(name, _)| name.0.clone())
.collect();
names.sort();
names
}
#[must_use]
pub fn state_hash(&mut self, class: &str) -> u64 {
let mut hasher = crate::runtime::StateHasher::default();
for name in self.in_class(class) {
let Some(entity) = find_named_entity(self.world, &name) else {
continue;
};
std::fmt::Write::write_str(&mut hasher, &name).ok();
if let Some(transform) = self.world.get::<Transform>(entity) {
hasher.floats(&transform.position);
hasher.floats(&transform.rotation);
hasher.floats(&transform.scale);
}
if let Some(body) =
self.world.get::<crate::runtime::RigidBody>(entity)
{
hasher.floats(&body.linear_velocity);
hasher.floats(&body.angular_velocity);
}
}
hasher.finish()
}
pub fn reparent(&mut self, name: &str, parent: Option<&str>) -> bool {
let Some(child) = find_named_entity(self.world, name) else {
return false;
};
match parent {
Some(parent) => {
find_named_entity(self.world, parent).is_some_and(|parent| {
crate::runtime::hierarchy::set_parent(
self.world, child, parent,
)
.is_ok()
})
}
None => crate::runtime::hierarchy::clear_parent(self.world, child)
.is_ok(),
}
}
pub fn set_light(
&mut self,
name: &str,
color: Option<[f32; 3]>,
intensity: Option<f32>,
range: Option<f32>,
) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let mut entity = self.world.entity_mut(entity);
let apply = |light_color: &mut [f32; 3],
light_intensity: &mut f32,
light_range: &mut f32| {
*light_color = color.unwrap_or(*light_color);
*light_intensity = intensity.unwrap_or(*light_intensity);
*light_range = range.unwrap_or(*light_range);
};
if let Some(mut light) = entity.get_mut::<crate::runtime::PointLight>()
{
let light = &mut *light;
apply(&mut light.color, &mut light.intensity, &mut light.range);
true
} else if let Some(mut light) =
entity.get_mut::<crate::runtime::SpotLight>()
{
let light = &mut *light;
apply(&mut light.color, &mut light.intensity, &mut light.range);
true
} else {
false
}
}
pub fn set_hud(
&mut self,
name: &str,
edit: impl FnOnce(&mut crate::runtime::HudElement),
) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let Some(mut hud) =
self.world.get_mut::<crate::runtime::HudElement>(entity)
else {
return false;
};
edit(&mut hud);
true
}
pub fn set_visible(&mut self, name: &str, visible: bool) {
if let Some(entity) = find_named_entity(self.world, name) {
self.world
.entity_mut(entity)
.insert(crate::runtime::Visibility { visible });
}
}
#[must_use]
pub fn color(&mut self, name: &str) -> Option<[f32; 4]> {
let entity = find_named_entity(self.world, name)?;
let handle = self.world.get::<MeshRenderer>(entity)?.material;
let assets = self.world.get_resource::<AssetServer>()?;
Some(assets.materials.get(handle)?.base_color)
}
pub fn set_color(&mut self, name: &str, color: [f32; 4]) {
self.edit_material(name, |material| material.base_color = color);
}
pub fn set_emissive(&mut self, name: &str, emissive: [f32; 3]) {
self.edit_material(name, |material| material.emissive = emissive);
}
fn edit_material(
&mut self,
name: &str,
edit: impl FnOnce(&mut crate::assets::MaterialAsset),
) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
let Some(handle) = self
.world
.get::<MeshRenderer>(entity)
.map(|renderer| renderer.material)
else {
return;
};
let Some(mut assets) = self.world.get_resource_mut::<AssetServer>()
else {
return;
};
let Some(mut material) = assets.materials.get(handle).cloned() else {
return;
};
edit(&mut material);
let shared = assets.materials.iter().find_map(|(handle, existing)| {
(*existing == material).then_some(handle)
});
let handle =
shared.unwrap_or_else(|| assets.materials.insert(material));
if let Some(mut renderer) = self.world.get_mut::<MeshRenderer>(entity) {
renderer.material = handle;
}
}
#[must_use]
pub fn tile(&mut self, map: &str, position: [f32; 3]) -> Option<char> {
let entity = find_named_entity(self.world, map)?;
let origin = self.world.get::<Transform>(entity)?.position;
let map = self.world.get::<crate::runtime::TileMap>(entity)?;
let (column, row) =
map.cell_at([position[0] - origin[0], position[1] - origin[1]])?;
Some(map.cell(column, row))
}
pub fn set_tile(
&mut self,
map: &str,
position: [f32; 3],
character: char,
) -> bool {
let Some(entity) = find_named_entity(self.world, map) else {
return false;
};
let Some(origin) =
self.world.get::<Transform>(entity).map(|t| t.position)
else {
return false;
};
let Some(mut map) =
self.world.get_mut::<crate::runtime::TileMap>(entity)
else {
return false;
};
let local = [position[0] - origin[0], position[1] - origin[1]];
let Some((column, row)) = map.cell_at(local) else {
return false;
};
map.cell(column, row) != character
&& map.set_cell(column, row, character)
}
pub fn trigger(&mut self, name: &str) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut cue) =
self.world.get_mut::<crate::runtime::SoundCue>(entity)
{
cue.trigger();
}
if let Some(mut emitter) =
self.world.get_mut::<crate::runtime::BurstEmitter>(entity)
{
emitter.trigger();
}
self.particles(name, crate::runtime::ParticleCommand::Restart);
}
pub fn particles(
&mut self,
name: &str,
command: crate::runtime::ParticleCommand,
) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut emitter) = self
.world
.get_mut::<crate::runtime::ParticleEmitter>(entity)
{
emitter.command = command;
}
}
pub fn play_animation(&mut self, name: &str, clip: &str) {
self.animate(name, crate::runtime::AnimationCommand::Play(clip.into()));
}
pub fn crossfade(&mut self, name: &str, clip: &str, seconds: f32) {
self.animate(
name,
crate::runtime::AnimationCommand::Crossfade(clip.into(), seconds),
);
}
pub fn stop_animation(&mut self, name: &str) {
self.animate(name, crate::runtime::AnimationCommand::Stop);
}
pub fn set_animation_speed(&mut self, name: &str, speed: f32) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut animation) =
self.world.get_mut::<crate::runtime::Animation>(entity)
{
animation.speed = speed.max(0.0);
}
}
pub fn set_ragdoll(&mut self, name: &str, limp: bool) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut ragdoll) =
self.world.get_mut::<crate::runtime::Ragdoll>(entity)
{
ragdoll.command = Some(limp);
}
}
pub fn set_ragdoll_muscle(&mut self, name: &str, hz: f32) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut ragdoll) =
self.world.get_mut::<crate::runtime::Ragdoll>(entity)
{
ragdoll.muscle = hz.max(0.0);
}
}
#[must_use]
pub fn is_limp(&mut self, name: &str) -> bool {
find_named_entity(self.world, name)
.and_then(|entity| {
self.world.get::<crate::runtime::RagdollState>(entity)
})
.is_some_and(|state| {
state.phase == crate::runtime::RagdollPhase::Limp
})
}
pub fn set_animation_parameter(
&mut self,
name: &str,
parameter: &str,
value: f32,
) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut animation) =
self.world.get_mut::<crate::runtime::Animation>(entity)
{
animation.parameters.insert(parameter.into(), value);
}
}
fn animate(
&mut self,
name: &str,
command: crate::runtime::AnimationCommand,
) {
let Some(entity) = find_named_entity(self.world, name) else {
return;
};
if let Some(mut animation) =
self.world.get_mut::<crate::runtime::Animation>(entity)
{
animation.command = Some(command);
}
}
#[must_use]
pub fn is_playing(&mut self, name: &str, clip: &str) -> bool {
let Some(entity) = find_named_entity(self.world, name) else {
return false;
};
let (Some(animation), Some(player)) = (
self.world.get::<crate::runtime::Animation>(entity),
self.world.get::<crate::runtime::AnimationPlayer>(entity),
) else {
return false;
};
player.playing
&& player.current.is_some_and(|current| {
Some(current.clip) == animation.clip(clip)
})
}
pub fn take_root_motion(&mut self, name: &str) -> [f32; 3] {
find_named_entity(self.world, name)
.and_then(|entity| {
self.world
.get_mut::<crate::runtime::AnimationPlayer>(entity)
})
.map(|mut player| std::mem::take(&mut player.root_motion))
.unwrap_or_default()
}
#[must_use]
pub fn animation_events(&self) -> Vec<crate::runtime::AnimationEvent> {
self.world
.resource::<EventQueue<crate::runtime::AnimationEvent>>()
.iter()
.cloned()
.collect()
}
#[must_use]
pub fn touching(&mut self, name: &str) -> Vec<String> {
let Some(entity) = find_named_entity(self.world, name) else {
return Vec::new();
};
let Some(physics) =
self.world.get_resource::<crate::runtime::PhysicsWorld>()
else {
return Vec::new();
};
let mut others: Vec<Entity> = physics
.contacts()
.iter()
.filter_map(|contact| match (contact.a, contact.b) {
(a, other) | (other, a) if a == entity => Some(other),
_ => None,
})
.collect();
if let Some(player) =
self.world.get::<crate::runtime::PlayerController>(entity)
{
for other in player
.floor
.map(|(floor, _)| floor)
.into_iter()
.chain(player.wall)
{
if !others.contains(&other) {
others.push(other);
}
}
}
others
.into_iter()
.filter_map(|other| self.world.get::<Name>(other))
.map(|other| other.0.clone())
.collect()
}
#[must_use]
pub fn player(
&mut self,
name: &str,
) -> Option<crate::runtime::PlayerController> {
let entity = find_named_entity(self.world, name)?;
self.world
.get::<crate::runtime::PlayerController>(entity)
.copied()
}
pub fn try_object(&mut self, name: &str) -> Option<GameObject<'_>> {
let entity = find_named_entity(self.world, name)?;
self.world
.get_mut::<Transform>(entity)
.map(|transform| GameObject { entity, transform })
}
pub fn watch_gpu_object(&mut self, name: &str, rule: GpuPhysicsRule) {
let entity = find_named_entity(self.world, name).unwrap_or_else(|| {
panic!(
"scene object `{name}` does not exist{}",
nearest_names_hint(self.world, name)
)
});
if let Some(mut watch) = self.world.get_mut::<GpuPhysicsWatch>(entity) {
if !watch.rules.contains(&rule) {
watch.rules.push(rule);
}
} else {
self.world
.entity_mut(entity)
.insert(GpuPhysicsWatch { rules: vec![rule] });
}
}
pub fn watch_gpu_class(&mut self, class: &str, rule: GpuPhysicsRule) {
self.world
.resource_mut::<GpuPhysicsClassWatches>()
.add(class, rule);
}
pub fn set_gpu_condition_shaders(
&mut self,
shaders: Vec<crate::runtime::GpuConditionShader>,
) {
self.world
.insert_resource(crate::runtime::GpuConditionShaders(shaders));
}
#[must_use]
pub fn gpu_events(&self, name: &str) -> Vec<GpuPhysicsEvent> {
let Some(event_id) = self.world.resource::<GpuEventRegistry>().id(name)
else {
return Vec::new();
};
self.world
.resource::<EventQueue<GpuPhysicsEvent>>()
.iter()
.filter(|event| event.event_id == event_id)
.copied()
.collect()
}
#[must_use]
pub fn gpu_state(
&mut self,
name: &str,
) -> Option<crate::runtime::GpuStateMirror> {
let entity = find_named_entity(self.world, name)?;
self.world
.get::<crate::runtime::GpuStateMirror>(entity)
.copied()
}
}
pub struct GameObject<'world> {
entity: Entity,
transform: Mut<'world, Transform>,
}
impl GameObject<'_> {
#[must_use]
pub fn entity(&self) -> Entity {
self.entity
}
#[must_use]
pub fn position(&self) -> [f32; 3] {
self.transform.position
}
pub fn set_position(&mut self, position: [f32; 3]) -> &mut Self {
self.transform.position = position;
self
}
pub fn move_by(&mut self, offset: [f32; 3]) -> &mut Self {
for (position, offset) in self.transform.position.iter_mut().zip(offset)
{
*position += offset;
}
self
}
pub fn move_x(&mut self, distance: f32) -> &mut Self {
self.transform.position[0] += distance;
self
}
pub fn move_y(&mut self, distance: f32) -> &mut Self {
self.transform.position[1] += distance;
self
}
pub fn move_z(&mut self, distance: f32) -> &mut Self {
self.transform.position[2] += distance;
self
}
#[must_use]
pub fn rotation(&self) -> [f32; 3] {
self.transform.rotation
}
#[must_use]
pub fn scale(&self) -> [f32; 3] {
self.transform.scale
}
pub fn set_rotation(&mut self, rotation: [f32; 3]) -> &mut Self {
self.transform.rotation = rotation;
self
}
pub fn rotate_by(&mut self, rotation: [f32; 3]) -> &mut Self {
for (current, rotation) in
self.transform.rotation.iter_mut().zip(rotation)
{
*current += rotation;
}
self
}
pub fn rotate_x(&mut self, rotation: f32) -> &mut Self {
self.transform.rotation[0] += rotation;
self
}
pub fn rotate_y(&mut self, rotation: f32) -> &mut Self {
self.transform.rotation[1] += rotation;
self
}
pub fn rotate_z(&mut self, rotation: f32) -> &mut Self {
self.transform.rotation[2] += rotation;
self
}
pub fn set_scale(&mut self, scale: [f32; 3]) -> &mut Self {
self.transform.scale = scale;
self
}
}
static MISSING_COUNTERS: std::sync::Mutex<std::collections::BTreeSet<String>> =
std::sync::Mutex::new(std::collections::BTreeSet::new());
fn warn_missing_counter(name: &str) {
let mut missing = MISSING_COUNTERS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if missing.insert(name.to_owned()) {
eprintln!(
"warning: game code read counter `{name}`, which does not exist; \
it reads as 0. Add a `rusting.counter` named `{name}` to the \
scene, or create it with `set_counter` or `add_to_counter`."
);
}
}
#[derive(Resource, Clone, Default)]
struct SceneNameIndex {
entities: HashMap<String, Entity>,
initialized: bool,
}
fn ensure_scene_name_index(world: &mut World) {
if world
.get_resource::<SceneNameIndex>()
.is_some_and(|index| index.initialized)
{
return;
}
let entries = {
let mut query = world.query::<(Entity, &Name)>();
query
.iter(world)
.map(|(entity, name)| (name.0.clone(), entity))
.collect::<Vec<_>>()
};
let mut entities = HashMap::with_capacity(entries.len());
for (name, entity) in entries {
assert!(
entities.insert(name.clone(), entity).is_none(),
"scene object name `{name}` is not unique"
);
}
world.insert_resource(SceneNameIndex {
entities,
initialized: true,
});
}
fn nearest_names_hint(world: &mut World, name: &str) -> String {
let mut names = world.query::<&Name>();
let limit = (name.chars().count() / 2).max(2);
let mut close: Vec<(usize, &str)> = names
.iter(world)
.filter_map(|other| {
let distance = crate::scene_patch::edit_distance(
&name.to_lowercase(),
&other.0.to_lowercase(),
);
(distance <= limit).then_some((distance, other.0.as_str()))
})
.collect();
close.sort_unstable();
close.truncate(3);
if close.is_empty() {
return String::new();
}
let list: Vec<String> = close
.iter()
.map(|(_, other)| format!("`{other}`"))
.collect();
format!("; did you mean {}?", list.join(", "))
}
fn find_named_entity(world: &mut World, name: &str) -> Option<Entity> {
ensure_scene_name_index(world);
let indexed = world
.resource::<SceneNameIndex>()
.entities
.get(name)
.copied()
.filter(|entity| {
world
.get::<Name>(*entity)
.is_some_and(|current| current.0 == name)
});
if indexed.is_some() {
return indexed;
}
let mut names = world.query::<(Entity, &Name)>();
let entity = names
.iter(world)
.find(|(_, current)| current.0 == name)
.map(|(entity, _)| entity)?;
world
.resource_mut::<SceneNameIndex>()
.entities
.insert(name.to_owned(), entity);
Some(entity)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InitialState {
pub transform: Transform,
pub color: Option<[f32; 4]>,
pub emissive: Option<[f32; 3]>,
pub body_kind: Option<crate::runtime::RigidBodyKind>,
pub visible: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RayHit {
pub name: String,
pub point: [f32; 3],
pub normal: [f32; 3],
pub distance: f32,
}
fn world_matrix(world: &World, entity: Entity) -> nalgebra::Matrix4<f32> {
let mut matrix = nalgebra::Matrix4::identity();
let mut current = Some(entity);
while let Some(entity) = current {
if let Some(transform) = world.get::<Transform>(entity) {
matrix = nalgebra::Matrix4::from(transform.to_matrix()) * matrix;
}
current = world
.get::<crate::runtime::Parent>(entity)
.map(|parent| parent.0);
}
matrix
}
fn forget_old_scene(world: &mut World) {
world.remove_resource::<SceneNameIndex>();
world.remove_resource::<GameOnceState>();
if world.contains_resource::<crate::runtime::PhysicsWorld>() {
world.insert_resource(crate::runtime::PhysicsWorld::default());
}
}
fn copy_tree(world: &mut World, entity: Entity, name: Option<&str>) -> Entity {
if let Some(name) = name {
if find_named_entity(world, name).is_some() {
panic!("scene object `{name}` already exists");
}
}
let copy =
world
.entity_mut(entity)
.clone_and_spawn_with_opt_out(|builder| {
builder.deny::<(
crate::runtime::Parent,
crate::runtime::Children,
crate::runtime::SceneId,
crate::runtime::Sleeping,
Name,
)>();
});
let order = crate::runtime::next_spawn_order(world);
world
.entity_mut(copy)
.insert((crate::runtime::SceneId::new(), order));
if let Some(name) = name {
world.entity_mut(copy).insert(Name(name.to_owned()));
world
.resource_mut::<SceneNameIndex>()
.entities
.insert(name.to_owned(), copy);
}
let children = world
.get::<crate::runtime::Children>(entity)
.map(|children| children.0.clone())
.unwrap_or_default();
for child in children {
let child_name = world
.get::<Name>(child)
.zip(name)
.map(|(child, parent)| format!("{parent}/{}", child.0));
let child = copy_tree(world, child, child_name.as_deref());
let _ = rusting_core::hierarchy::set_parent(world, child, copy);
}
copy
}
fn despawn_tree(world: &mut World, entity: Entity) {
let children = world
.get::<crate::runtime::Children>(entity)
.map(|children| children.0.clone())
.unwrap_or_default();
for child in children {
despawn_tree(world, child);
}
world.despawn(entity);
}
pub type GameUpdate = for<'world> fn(&mut GameScene<'world>, &FrameTime);
#[derive(Resource, Clone, Copy)]
struct GameUpdateFunction(GameUpdate);
#[derive(Clone, Copy)]
struct SimpleGamePlugin {
update: GameUpdate,
}
impl Plugin for SimpleGamePlugin {
fn build(&self, app: &mut App) -> Result<(), AppError> {
app.insert_resource(GameUpdateFunction(self.update));
app.add_system(ScheduleStage::Update, run_simple_game_update);
app.register_snapshot_component::<GameUpdateFunction>()
.register_snapshot_component::<GameOnceState>()
.register_snapshot_component::<SceneNameIndex>()
.register_snapshot_component::<SphereMeshCache>();
Ok(())
}
}
fn run_simple_game_update(world: &mut World) {
let time = *world.resource::<FrameTime>();
let update = world.resource::<GameUpdateFunction>().0;
update(&mut GameScene { world }, &time);
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CodeReloadState {
scene: SceneDocument,
once: Vec<String>,
}
fn save_code_reload_state(
runtime: &mut App,
path: &Path,
) -> Result<(), Box<dyn Error>> {
let world = runtime.world_mut();
let scene = scene_document(world, "code reload")?;
let mut once = world
.get_resource::<GameOnceState>()
.map(|state| state.0.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();
once.sort();
write_atomic(path, &serde_json::to_vec(&CodeReloadState { scene, once })?)?;
Ok(())
}
fn restore_code_reload_state(
runtime: &mut App,
path: &Path,
) -> Result<(), Box<dyn Error>> {
let state: CodeReloadState = serde_json::from_slice(&std::fs::read(path)?)?;
let world = runtime.world_mut();
load_scene_document(world, &state.scene, SceneLoadMode::Replace)?;
world.insert_resource(GameOnceState(state.once.into_iter().collect()));
world.remove_resource::<SceneNameIndex>();
runtime.skip_startup();
Ok(())
}
fn load_project_runtime<P: Plugin>(
scene_path: &Path,
plugin: P,
) -> Result<App, Box<dyn Error>> {
let mut runtime = App::new();
runtime.add_plugin(AssetPlugin)?;
runtime.add_plugin(HybridPhysicsPlugin)?;
runtime.add_plugin(RenderExtractPlugin)?;
runtime.add_plugin(plugin)?;
load_scene(runtime.world_mut(), scene_path, SceneLoadMode::Replace)?;
let start = scene_document(runtime.world_mut(), "start")?;
runtime.insert_resource(StartingScene(start));
let folder = scene_path
.ancestors()
.skip(1)
.find(|folder| folder.join("project.json").is_file())
.or_else(|| scene_path.parent())
.unwrap_or(Path::new("."));
runtime.insert_resource(ProjectFolder(folder.to_path_buf()));
crate::runtime::check_determinism(runtime.world_mut())?;
Ok(runtime)
}
#[cfg(feature = "ui")]
struct RuntimeUiPainter {
input: egui_winit::State,
painter: crate::rendering::egui_painter::EguiPainter,
textures: egui::TexturesDelta,
primitives: Vec<egui::ClippedPrimitive>,
pixels_per_point: f32,
}
static SWAPCHAIN_IMAGES: AtomicU32 = AtomicU32::new(4);
static SWAPCHAIN_BLIT: AtomicBool = AtomicBool::new(false);
fn probe_swapchain_images(
event_loop: &ActiveEventLoop,
vulkan: &VulkanoContext,
) {
let Ok(window) = event_loop
.create_window(Window::default_attributes().with_visible(false))
else {
return;
};
let Ok(surface) =
Surface::from_window(vulkan.instance().clone(), Arc::new(window))
else {
return;
};
let Ok(capabilities) = vulkan
.device()
.physical_device()
.surface_capabilities(&surface, Default::default())
else {
return;
};
let wanted = capabilities.max_image_count.map_or(4, |max| max.min(4));
SWAPCHAIN_IMAGES.store(wanted, Ordering::Relaxed);
SWAPCHAIN_BLIT.store(
capabilities
.supported_usage_flags
.contains(ImageUsage::TRANSFER_DST),
Ordering::Relaxed,
);
}
struct WindowRunner {
title: String,
vulkan: VulkanoContext,
windows: VulkanoWindows,
scene_renderer: Option<SceneRenderer>,
frame_pacer: FramePacer,
applied_vsync: Option<bool>,
applied_cursor_capture: bool,
applied_fullscreen: bool,
scaled: Option<ScaledTarget>,
#[cfg(feature = "ui")]
ui: Option<RuntimeUiPainter>,
}
impl WindowRunner {
fn new(title: String) -> Self {
Self {
title,
vulkan: VulkanoContext::new(crate::rendering::vulkano_config()),
windows: VulkanoWindows::default(),
scene_renderer: None,
frame_pacer: FramePacer::default(),
applied_vsync: None,
applied_cursor_capture: false,
applied_fullscreen: false,
scaled: None,
#[cfg(feature = "ui")]
ui: None,
}
}
fn resumed(&mut self, event_loop: &ActiveEventLoop, runtime: &mut App) {
if self.scene_renderer.is_some() {
return;
}
probe_swapchain_images(event_loop, &self.vulkan);
self.windows.create_window(
event_loop,
&self.vulkan,
&WindowDescriptor {
title: self.title.clone(),
width: 1440.0,
height: 900.0,
..WindowDescriptor::default()
},
|create_info| {
create_info.image_format = Format::B8G8R8A8_UNORM;
create_info.min_image_count = create_info
.min_image_count
.max(SWAPCHAIN_IMAGES.load(Ordering::Relaxed));
if SWAPCHAIN_BLIT.load(Ordering::Relaxed) {
create_info.image_usage |= ImageUsage::TRANSFER_DST;
}
},
);
let renderer = self.windows.get_primary_renderer_mut().unwrap();
let settings = runtime.world().resource::<RenderSettings>();
let mode = select_present_mode(
&renderer.graphics_queue(),
&renderer.surface(),
settings.vsync,
);
if std::env::var_os(PERF_ENV).is_some() {
eprintln!(
"[rusting] present mode {mode:?}, render scale blit {}",
SWAPCHAIN_BLIT.load(Ordering::Relaxed)
);
}
renderer.set_present_mode(mode);
self.applied_vsync = Some(settings.vsync);
let initial_size = renderer.window().inner_size();
runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_viewport_size([
initial_size.width as f32,
initial_size.height as f32,
]);
self.scene_renderer = Some(
SceneRenderer::new(
renderer.graphics_queue(),
self.vulkan.memory_allocator().clone(),
renderer.swapchain_format(),
renderer.swapchain_image_size(),
)
.expect("failed to create game scene renderer"),
);
self.scaled = SWAPCHAIN_BLIT
.load(Ordering::Relaxed)
.then(|| ScaledTarget::new(self.vulkan.memory_allocator().clone()));
#[cfg(feature = "ui")]
if let Some(runtime_ui) =
runtime.world().get_resource::<crate::runtime::RuntimeUi>()
{
let painter = crate::rendering::egui_painter::EguiPainter::new(
renderer.graphics_queue(),
self.vulkan.memory_allocator().clone(),
renderer.swapchain_format(),
)
.expect("failed to create runtime UI painter");
let context = runtime_ui.context();
let input = egui_winit::State::new(
context.clone(),
context.viewport_id(),
event_loop,
Some(renderer.window().scale_factor() as f32),
None,
Some(painter.max_texture_side()),
);
self.ui = Some(RuntimeUiPainter {
input,
painter,
textures: egui::TexturesDelta::default(),
primitives: Vec::new(),
pixels_per_point: 1.0,
});
}
}
fn ui_event(&mut self, window_id: WindowId, event: &WindowEvent) -> bool {
#[cfg(feature = "ui")]
if let (Some(ui), Some(renderer)) =
(self.ui.as_mut(), self.windows.get_renderer(window_id))
{
return ui.input.on_window_event(renderer.window(), event).consumed;
}
let _ = (window_id, event);
false
}
fn begin_ui_frame(&mut self, window_id: WindowId, runtime: &mut App) {
#[cfg(feature = "ui")]
if let (Some(ui), Some(renderer), Some(mut runtime_ui)) = (
self.ui.as_mut(),
self.windows.get_renderer(window_id),
runtime
.world_mut()
.get_resource_mut::<crate::runtime::RuntimeUi>(),
) {
runtime_ui.set_input(ui.input.take_egui_input(renderer.window()));
}
let _ = (window_id, runtime);
}
fn finish_ui_frame(&mut self, window_id: WindowId, runtime: &mut App) {
#[cfg(feature = "ui")]
if let (Some(ui), Some(renderer), Some(output)) = (
self.ui.as_mut(),
self.windows.get_renderer(window_id),
runtime
.world_mut()
.get_resource_mut::<crate::runtime::RuntimeUi>()
.and_then(|mut runtime_ui| runtime_ui.take_output()),
) {
ui.input.handle_platform_output(
renderer.window(),
output.platform_output,
);
ui.textures.append(output.textures_delta);
ui.pixels_per_point = output.pixels_per_point;
ui.primitives = ui
.input
.egui_ctx()
.tessellate(output.shapes, output.pixels_per_point);
}
let _ = (window_id, runtime);
}
fn request_next_frame(
&mut self,
event_loop: &ActiveEventLoop,
runtime: &App,
) {
if let Some(renderer) = self.windows.get_primary_renderer_mut() {
self.frame_pacer.request_next_frame(
event_loop,
renderer.window(),
runtime.world().resource::<RenderSettings>(),
);
}
}
fn apply_window_request(&mut self, world: &mut World) {
let Some(mut request) = world.get_resource_mut::<WindowRequest>()
else {
return;
};
let size = request.size.take();
let fullscreen = request.fullscreen;
let Some(renderer) = self.windows.get_primary_renderer() else {
return;
};
let window = renderer.window();
if self.applied_fullscreen != fullscreen {
window.set_fullscreen(
fullscreen.then_some(Fullscreen::Borderless(None)),
);
self.applied_fullscreen = fullscreen;
}
if let Some([width, height]) = size {
let _ = window.request_inner_size(PhysicalSize::new(width, height));
}
}
fn apply_cursor_capture(&mut self, captured: bool) {
if self.applied_cursor_capture == captured {
return;
}
let Some(renderer) = self.windows.get_primary_renderer() else {
return;
};
let window = renderer.window();
let grab = if captured {
window
.set_cursor_grab(CursorGrabMode::Locked)
.or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined))
} else {
window.set_cursor_grab(CursorGrabMode::None)
};
if let Err(error) = grab {
eprintln!("cursor capture failed: {error}");
}
window.set_cursor_visible(!captured);
self.applied_cursor_capture = captured;
}
fn resize(&mut self, window_id: WindowId) {
self.windows.get_renderer_mut(window_id).unwrap().resize();
}
fn render(
&mut self,
window_id: WindowId,
runtime: &App,
) -> Result<(), String> {
let renderer = self.windows.get_renderer_mut(window_id).unwrap();
let vsync = runtime.world().resource::<RenderSettings>().vsync;
if self.applied_vsync != Some(vsync) {
renderer.set_present_mode(select_present_mode(
&renderer.graphics_queue(),
&renderer.surface(),
vsync,
));
self.applied_vsync = Some(vsync);
}
let acquired = renderer.acquire(None, |_| {});
match acquired {
Ok(future) => {
let world = runtime.world();
let future = crate::rendering::render_scale::render_game(
self.scene_renderer.as_mut().unwrap(),
self.scaled.as_mut(),
future,
renderer.swapchain_image_view(),
world.resource::<RenderSettings>(),
world.resource::<RenderWorld>(),
world.resource::<AssetServer>(),
|_, future, _| Ok(future),
)
.map_err(|error| format!("scene rendering failed: {error}"))?;
#[cfg(feature = "ui")]
let future = match self.ui.as_mut() {
Some(ui) => {
let future = ui
.painter
.paint(
future,
renderer.swapchain_image_view(),
ui.pixels_per_point,
&ui.primitives,
&ui.textures,
)
.map_err(|error| {
format!("runtime UI painting failed: {error}")
})?;
ui.textures.clear();
future
}
None => future,
};
renderer.present(future, false);
Ok(())
}
Err(VulkanError::OutOfDate) => {
renderer.resize();
Ok(())
}
Err(error) => Err(format!("swapchain acquisition failed: {error}")),
}
}
}
struct ProjectApplication {
window: WindowRunner,
runtime: App,
previous_frame: Instant,
gamepads: crate::runtime::Gamepads,
code_reload: Option<(PathBuf, Arc<AtomicBool>)>,
perf: Option<(Instant, u32)>,
perf_spent: [std::time::Duration; 2],
audio: Option<crate::audio_output::AudioOutput>,
quit_at: Option<Instant>,
}
pub const PERF_ENV: &str = "RUSTING_PERF";
impl ProjectApplication {
fn new(title: String, mut runtime: App) -> Self {
runtime
.world_mut()
.resource_mut::<PhysicsBackendStatus>()
.gpu_dynamic_available = true;
Self {
window: WindowRunner::new(title),
runtime,
previous_frame: Instant::now(),
gamepads: crate::runtime::Gamepads::new(),
code_reload: None,
perf: std::env::var_os(PERF_ENV).map(|_| (Instant::now(), 0)),
perf_spent: [std::time::Duration::ZERO; 2],
audio: crate::audio_output::AudioOutput::open(),
quit_at: std::env::var(crate::project::QUIT_AFTER_MS_ENV)
.ok()
.and_then(|ms| ms.parse().ok())
.map(|ms| {
Instant::now() + std::time::Duration::from_millis(ms)
}),
}
}
fn event_tick(&self) -> f64 {
let world = self.runtime.world();
let control = world.resource::<crate::runtime::TimeControl>();
let step = control.fixed_delta.as_secs_f64();
let since = match control.paused {
true => 0.0,
false => {
(control.accumulator().as_secs_f64()
+ self.previous_frame.elapsed().as_secs_f64()
* control.time_scale.max(0.0))
/ step
}
};
world.resource::<FrameTime>().fixed_tick as f64 + since
}
fn play_audio(&mut self) {
let world = self.runtime.world_mut();
crate::runtime::route_sound_events(world);
let commands =
world.resource_mut::<crate::runtime::AudioQueue>().drain();
let Some(audio) = &mut self.audio else {
return;
};
let root = world
.get_resource::<ProjectFolder>()
.map(|folder| folder.0.join("assets"))
.unwrap_or_default();
let control = world.resource::<crate::runtime::TimeControl>();
let (step, paused) = (control.fixed_delta, control.paused);
let past = control.accumulator().as_secs_f64()
/ control.time_scale.max(1e-6)
+ self.previous_frame.elapsed().as_secs_f64();
let now = world.resource::<FrameTime>().fixed_tick;
let delay = |tick: u64| {
let ahead = (tick as f64 - now as f64 + 1.0) * step.as_secs_f64();
match paused {
true => std::time::Duration::ZERO,
false => std::time::Duration::from_secs_f64(
(ahead - past).clamp(0.0, 3600.0),
),
}
};
for command in commands {
audio.run(&root, command, delay);
}
}
fn report_perf(&mut self) {
let Some((since, frames)) = &mut self.perf else {
return;
};
*frames += 1;
let elapsed = since.elapsed();
if elapsed < std::time::Duration::from_secs(1) {
return;
}
let fps = f64::from(*frames) / elapsed.as_secs_f64();
let [update, render] =
std::mem::take(&mut self.perf_spent).map(|time| time / *frames);
(*since, *frames) = (Instant::now(), 0);
if let Some(window) = self.window.windows.get_primary_window() {
window.set_title(&format!(
"{} - {fps:.0} fps ({:.2} ms)",
self.window.title,
1000.0 / fps
));
}
let ms = |time: std::time::Duration| time.as_secs_f64() * 1000.0;
let mut cpu = *self
.runtime
.world()
.resource::<crate::runtime::CpuFrameTimings>();
let Some(renderer) = self.window.scene_renderer.as_mut() else {
return;
};
renderer.write_cpu_timings(&mut cpu);
let gpu = renderer.gpu_pass_times();
let counters = renderer.render_counters();
let mut line = format!(
"[rusting] perf {fps:.0} fps ({:.2} ms/frame) | update {:.2} render {:.2} ms | CPU physics {:.2} extract {:.2} prepare {:.2} record {:.2} ms | GPU {:.2} ms",
1000.0 / fps,
ms(update),
ms(render),
ms(cpu.physics),
ms(cpu.extraction),
ms(cpu.preparation),
ms(cpu.recording),
ms(gpu.total()),
);
for (pass, time) in &gpu.0 {
line += &format!(" | {} {:.2}", pass.label(), ms(*time));
}
line += &format!(
" | draws {} dispatches {} triangles {}",
counters.draws, counters.dispatches, counters.triangles
);
eprintln!("{line}");
}
}
impl ApplicationHandler for ProjectApplication {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.window.resumed(event_loop, &mut self.runtime);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
if self.window.windows.get_renderer(window_id).is_none() {
return;
}
if self.window.ui_event(window_id, &event)
&& match &event {
WindowEvent::KeyboardInput { event, .. } => {
event.state.is_pressed()
}
WindowEvent::MouseInput { state, .. } => state.is_pressed(),
_ => false,
}
{
return;
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
self.window.resize(window_id);
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_viewport_size([
size.width as f32,
size.height as f32,
]);
}
WindowEvent::ScaleFactorChanged { .. } => {
self.window.resize(window_id);
}
WindowEvent::Focused(false) => {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.release_all();
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
let tick = self.event_tick();
let mut input =
self.runtime.world_mut().resource_mut::<RuntimeInput>();
input.record_key(code, event.state.is_pressed());
if event.state.is_pressed() && !event.repeat {
input.record_press_tick(
crate::runtime::InputBinding::Key(code),
tick,
);
}
}
}
WindowEvent::MouseInput { state, button, .. } => {
let tick = self.event_tick();
let mut input =
self.runtime.world_mut().resource_mut::<RuntimeInput>();
if state.is_pressed() {
input.record_press_tick(
crate::runtime::InputBinding::Mouse(button),
tick,
);
}
input.record_mouse_button(button, state.is_pressed());
}
WindowEvent::CursorMoved { position, .. } => {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_cursor_position([
position.x as f32,
position.y as f32,
]);
}
WindowEvent::RedrawRequested => {
let scene_renderer =
self.window.scene_renderer.as_mut().unwrap();
let raw_events = scene_renderer.take_completed_physics_events();
let states = scene_renderer.take_completed_physics_states();
let hashes =
scene_renderer.take_completed_physics_state_hashes();
let lost = scene_renderer.take_physics_events_lost();
if lost > 0 {
self.runtime
.world_mut()
.resource_mut::<EventQueue<GpuPhysicsEventsLost>>()
.send(GpuPhysicsEventsLost { count: lost });
}
if !raw_events.is_empty() {
route_gpu_physics_events(
self.runtime.world_mut(),
&raw_events,
);
}
if !states.is_empty() {
apply_gpu_state_samples(self.runtime.world_mut(), &states);
}
record_gpu_state_hashes(self.runtime.world_mut(), &hashes);
let now = Instant::now();
let delta = now.saturating_duration_since(self.previous_frame);
self.previous_frame = now;
self.gamepads.poll(
&mut self
.runtime
.world_mut()
.resource_mut::<RuntimeInput>(),
);
self.window.begin_ui_frame(window_id, &mut self.runtime);
let update_start = Instant::now();
let updated = self.runtime.update(delta);
self.perf_spent[0] += update_start.elapsed();
if let Err(error) = updated {
eprintln!("runtime update failed: {error}");
event_loop.exit();
return;
}
self.window.finish_ui_frame(window_id, &mut self.runtime);
if self.runtime.exit_requested()
|| self.quit_at.is_some_and(|at| Instant::now() >= at)
{
event_loop.exit();
return;
}
self.play_audio();
if let Some(mut assets) =
self.runtime.world_mut().get_resource_mut::<AssetServer>()
{
for failure in assets.take_reload_failures() {
eprintln!("hot reload failed: {failure}");
}
}
let mut input =
self.runtime.world_mut().resource_mut::<RuntimeInput>();
input.clear_frame_edges();
let captured = input.cursor_captured();
self.window.apply_cursor_capture(captured);
self.window.apply_window_request(self.runtime.world_mut());
let render_start = Instant::now();
let rendered = self.window.render(window_id, &self.runtime);
self.perf_spent[1] += render_start.elapsed();
match rendered {
Ok(()) => {
announce_first_frame();
self.report_perf();
}
Err(error) => {
eprintln!("{error}");
event_loop.exit();
}
}
}
_ => {}
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: DeviceId,
event: DeviceEvent,
) {
if let DeviceEvent::MouseMotion { delta } = event {
self.runtime
.world_mut()
.resource_mut::<RuntimeInput>()
.record_mouse_motion([delta.0 as f32, delta.1 as f32]);
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
#[cfg(feature = "ui")]
{
self.window.ui = None;
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if let Some((path, requested)) = &self.code_reload {
if requested.load(Ordering::Relaxed) {
if let Err(error) =
save_code_reload_state(&mut self.runtime, path)
{
eprintln!(
"{} could not save the scene: {error}",
crate::project::CODE_RELOAD_CLEAN_MARKER
);
}
event_loop.exit();
return;
}
}
self.window.request_next_frame(event_loop, &self.runtime);
}
}
pub(crate) fn run_windowed(
title: String,
mut runtime: App,
) -> Result<(), Box<dyn Error>> {
let replay_out = std::env::var_os(crate::project::REPLAY_OUT_ENV);
if replay_out.is_some() {
runtime.start_recording();
}
let event_loop = EventLoop::new()?;
let mut application = ProjectApplication::new(title, runtime);
if let Some(path) = std::env::var_os(crate::project::CODE_RELOAD_STATE_ENV)
{
let requested = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&requested);
std::thread::spawn(move || {
for line in std::io::stdin().lines().map_while(Result::ok) {
if line.trim() == crate::project::CODE_RELOAD_SAVE_COMMAND {
flag.store(true, Ordering::Relaxed);
}
}
});
application.code_reload = Some((PathBuf::from(path), requested));
}
event_loop.run_app(&mut application)?;
if let (Some(path), Some(replay)) =
(replay_out, application.runtime.finish_recording())
{
std::fs::write(path, serde_json::to_vec(&replay)?)?;
}
Ok(())
}
pub use crate::project::HEADLESS_TICKS_ENV;
static GAME_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
fn announce_first_frame() {
static ANNOUNCED: std::sync::Once = std::sync::Once::new();
ANNOUNCED.call_once(|| {
let start = GAME_START.get_or_init(Instant::now);
eprintln!(
"{} {} ms",
crate::project::FIRST_FRAME_MARKER,
start.elapsed().as_millis()
);
});
}
pub fn run_project<P: Plugin>(
title: impl Into<String>,
scene_path: impl Into<PathBuf>,
plugin: P,
) -> Result<(), Box<dyn Error>> {
GAME_START.get_or_init(Instant::now);
if std::env::var_os(crate::debug_session::DEBUG_SESSION_ENV).is_some() {
let mut runtime = load_project_runtime(&scene_path.into(), plugin)?;
crate::debug_session::run_debug_session(
&mut runtime,
std::io::stdin().lock(),
std::io::stdout().lock(),
)?;
return Ok(());
}
if let Some(scenario) = std::env::var_os(crate::scenario::TEST_SCENARIO_ENV)
{
let report = std::env::var_os(crate::scenario::TEST_REPORT_ENV);
return run_project_scenario(
scene_path,
plugin,
PathBuf::from(scenario),
report.map(PathBuf::from),
);
}
if let Some(ticks) = std::env::var_os(HEADLESS_TICKS_ENV) {
let ticks = ticks
.to_str()
.and_then(|ticks| ticks.parse().ok())
.ok_or(format!("{HEADLESS_TICKS_ENV} must be a tick count"))?;
return run_project_headless(scene_path, plugin, ticks);
}
let mut runtime = load_project_runtime(&scene_path.into(), plugin)?;
if let Some(path) = std::env::var_os(crate::project::REPLAY_PLAY_ENV) {
let replay: crate::runtime::Replay =
serde_json::from_slice(&std::fs::read(path)?)?;
return match crate::runtime::play_replay(&mut runtime, &replay)? {
None => Ok(()),
Some(tick) => Err(format!(
"replay diverges from the recording at tick {tick}"
)
.into()),
};
}
if let Some(path) = std::env::var_os(crate::project::CODE_RELOAD_STATE_ENV)
.map(PathBuf::from)
.filter(|path| path.is_file())
{
match restore_code_reload_state(&mut runtime, &path) {
Ok(()) => eprintln!("{}", crate::project::CODE_RELOAD_KEPT_MARKER),
Err(error) => eprintln!(
"{} the saved scene does not fit the new code: {error}",
crate::project::CODE_RELOAD_CLEAN_MARKER
),
}
let _ = std::fs::remove_file(&path);
}
run_windowed(title.into(), runtime)
}
pub fn run_project_headless<P: Plugin>(
scene_path: impl Into<PathBuf>,
plugin: P,
ticks: u32,
) -> Result<(), Box<dyn Error>> {
let (mut runtime, report, elapsed) =
simulate_timed(scene_path.into(), plugin, ticks)?;
eprintln!(
"{} {:.3}",
crate::project::TICK_TIME_MARKER,
elapsed.as_secs_f64() * 1000.0 / f64::from(ticks.max(1))
);
if let Some(path) = std::env::var_os(crate::project::FINAL_SCENE_OUT_ENV) {
crate::runtime::save_scene(
runtime.world_mut(),
path,
format!("after tick {ticks}"),
)?;
}
if let Some(path) = std::env::var_os(crate::project::STATE_HASH_OUT_ENV) {
std::fs::write(path, serde_json::to_vec(&report)?)?;
}
Ok(())
}
pub fn simulate_project_headless<P: Plugin>(
scene_path: impl Into<PathBuf>,
plugin: P,
ticks: u32,
) -> Result<(App, crate::runtime::StateHashReport), Box<dyn Error>> {
let (runtime, report, _) =
simulate_timed(scene_path.into(), plugin, ticks)?;
Ok((runtime, report))
}
fn simulate_timed<P: Plugin>(
scene_path: PathBuf,
plugin: P,
ticks: u32,
) -> Result<
(App, crate::runtime::StateHashReport, std::time::Duration),
Box<dyn Error>,
> {
let mut runtime = load_project_runtime(&scene_path, plugin)?;
let world = runtime.world_mut();
let gpu_bodies = world
.query::<&crate::runtime::PhysicsBody>()
.iter(world)
.filter(|body| body.uses_gpu())
.count();
if gpu_bodies > 0 {
eprintln!(
"headless run: {gpu_bodies} GPU physics bodies stay still without a renderer"
);
}
let delta = runtime.world().resource::<FrameTime>().fixed_delta;
let mut hashes = Vec::with_capacity(ticks as usize);
let start = Instant::now();
for _ in 0..ticks {
runtime.update(delta)?;
announce_first_frame();
if runtime.exit_requested() {
break;
}
hashes.extend(
runtime
.world()
.get_resource::<crate::runtime::StateHashes>()
.and_then(|recorded| recorded.recent.back().copied()),
);
}
let elapsed = start.elapsed();
let entities =
crate::runtime::named_entity_state_hashes(runtime.world_mut());
let report = crate::runtime::StateHashReport {
ticks: hashes,
entities,
};
Ok((runtime, report, elapsed))
}
pub fn run_project_scenario<P: Plugin>(
scene_path: impl Into<PathBuf>,
plugin: P,
scenario_path: PathBuf,
report_path: Option<PathBuf>,
) -> Result<(), Box<dyn Error>> {
let mut scenario: crate::scenario::Scenario =
serde_json::from_str(&std::fs::read_to_string(&scenario_path)?)?;
scenario.update_golden =
std::env::var_os(crate::scenario::UPDATE_GOLDEN_ENV).is_some();
scenario.keep_going |=
std::env::var_os(crate::scenario::KEEP_GOING_ENV).is_some();
let mut runtime = load_project_runtime(&scene_path.into(), plugin)?;
let base = scenario_path.parent().unwrap_or(Path::new("."));
let report = crate::scenario::run_scenario(&mut runtime, &scenario, base);
let text = serde_json::to_string_pretty(&report)?;
match report_path {
Some(path) => std::fs::write(path, text)?,
None => println!("{text}"),
}
match report.first_failure {
None => Ok(()),
Some(failure) => Err(format!(
"scenario `{}` failed at tick {} step {}: {}",
report.name, failure.tick, failure.step, failure.message
)
.into()),
}
}
pub fn run_game(
scene_path: impl Into<PathBuf>,
update: GameUpdate,
) -> GameResult {
run_project(
"RustingEngine Game",
scene_path,
SimpleGamePlugin { update },
)
}
#[must_use]
pub fn resolve_game_scene_path(
relative: impl AsRef<std::path::Path>,
project_root: impl AsRef<std::path::Path>,
) -> PathBuf {
let relative = relative.as_ref();
if let Some(path) = std::env::var_os("RUSTING_SCENE_PATH") {
return PathBuf::from(path);
}
if let Ok(executable) = std::env::current_exe() {
if let Some(folder) = executable.parent() {
let packaged = folder.join(relative);
if packaged.is_file() {
return packaged;
}
}
}
project_root.as_ref().join(relative)
}
#[macro_export]
macro_rules! rusting_game {
($update:path) => {
$crate::rusting_game!("build/main.rscene.bin", $update);
};
($scene:literal, $update:path) => {
fn main() -> $crate::project_runner::GameResult {
let scene = $crate::project_runner::resolve_game_scene_path(
$scene,
env!("CARGO_MANIFEST_DIR"),
);
$crate::project_runner::run_game(scene, $update)
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headless_simulation_runs_exact_ticks_from_a_cooked_scene() {
let directory = std::env::temp_dir()
.join(format!("rusting-headless-{}", uuid::Uuid::new_v4()));
let source = directory.join("main.rscene");
let cooked = directory.join("main.rscene.bin");
let mut editor = App::new();
editor.add_plugin(AssetPlugin).unwrap();
editor.spawn((
Name("Ball".into()),
Transform::new([0.0, 5.0, 0.0]),
crate::runtime::PhysicsBody::default(),
crate::runtime::RigidBody::default(),
crate::runtime::Collider::default(),
));
crate::runtime::save_scene(editor.world_mut(), &source, "main")
.unwrap();
crate::runtime::cook_scene(&source, &cooked).unwrap();
fn idle(_: &mut GameScene<'_>, _: &FrameTime) {}
let (mut runtime, report) = simulate_project_headless(
&cooked,
SimpleGamePlugin { update: idle },
30,
)
.unwrap();
std::fs::remove_dir_all(&directory).unwrap();
assert_eq!(runtime.world().resource::<FrameTime>().fixed_tick, 30);
let hashes = &runtime
.world()
.resource::<crate::runtime::StateHashes>()
.recent;
assert_eq!(hashes.len(), 30);
assert!(hashes.iter().eq(&report.ticks));
assert_eq!(hashes.back().unwrap().0, 30);
let mut scene = GameScene {
world: runtime.world_mut(),
};
assert!(scene.object("Ball").position()[1] < 5.0);
}
#[derive(
bevy_ecs::component::Component,
Clone,
Default,
serde::Serialize,
serde::Deserialize,
)]
struct Health {
hp: i32,
}
crate::reflect! {
struct Health {
hp: i32,
}
}
#[derive(
bevy_ecs::component::Component,
Clone,
Default,
serde::Serialize,
serde::Deserialize,
)]
struct HealthText {
hp: String,
}
crate::reflect! {
struct HealthText {
hp: String,
}
}
struct TestGame {
update: GameUpdate,
changed_health: bool,
}
impl Plugin for TestGame {
fn build(&self, app: &mut App) -> Result<(), AppError> {
let registered = if self.changed_health {
app.register_scene_component::<HealthText>("test.health")
.map(drop)
} else {
app.register_scene_component::<Health>("test.health")
.map(drop)
};
registered.map_err(|error| AppError::PluginSetup {
plugin: "TestGame",
message: error.to_string(),
})?;
app.add_plugin(SimpleGamePlugin {
update: self.update,
})?;
app.add_system(ScheduleStage::Startup, |world: &mut World| {
world.spawn((
crate::runtime::SceneId::new(),
Name("Marker".into()),
));
});
Ok(())
}
}
fn count_named(world: &mut World, name: &str) -> usize {
let mut query = world.query::<&Name>();
query.iter(world).filter(|named| named.0 == name).count()
}
#[test]
fn code_reload_keeps_the_scene_and_runs_the_new_code() {
let directory = std::env::temp_dir()
.join(format!("rusting-code-reload-{}", uuid::Uuid::new_v4()));
let source = directory.join("main.rscene");
let cooked = directory.join("main.rscene.bin");
let state = directory.join("state.json");
let mut editor = App::new();
editor.add_plugin(AssetPlugin).unwrap();
editor.spawn((Name("Ball".into()), Transform::new([0.0, 0.0, 0.0])));
crate::runtime::save_scene(editor.world_mut(), &source, "main")
.unwrap();
crate::runtime::cook_scene(&source, &cooked).unwrap();
fn old_code(scene: &mut GameScene<'_>, _: &FrameTime) {
scene.once("setup", |scene| {
scene.spawn_cube(
"Coin",
Transform::default(),
&CubeSpawn::new(),
);
let ball = find_named_entity(scene.world, "Ball").unwrap();
scene.world.entity_mut(ball).insert(Health { hp: 10 });
});
scene.object("Ball").move_x(1.0);
let ball = find_named_entity(scene.world, "Ball").unwrap();
scene.world.get_mut::<Health>(ball).unwrap().hp -= 1;
}
fn new_code(scene: &mut GameScene<'_>, _: &FrameTime) {
scene.once("setup", |scene| {
scene.spawn_cube(
"Fresh",
Transform::default(),
&CubeSpawn::new(),
);
});
scene.object("Ball").move_y(1.0);
}
let frame = std::time::Duration::from_millis(16);
let mut old = load_project_runtime(
&cooked,
TestGame {
update: old_code,
changed_health: false,
},
)
.unwrap();
for _ in 0..3 {
old.update(frame).unwrap();
}
save_code_reload_state(&mut old, &state).unwrap();
let mut new = load_project_runtime(
&cooked,
TestGame {
update: new_code,
changed_health: false,
},
)
.unwrap();
restore_code_reload_state(&mut new, &state).unwrap();
for _ in 0..2 {
new.update(frame).unwrap();
}
let world = new.world_mut();
let ball = find_named_entity(world, "Ball").unwrap();
assert_eq!(
world.get::<Transform>(ball).unwrap().position,
[3.0, 2.0, 0.0]
);
assert_eq!(world.get::<Health>(ball).unwrap().hp, 7);
assert_eq!(count_named(world, "Coin"), 1);
assert_eq!(count_named(world, "Fresh"), 0);
assert_eq!(count_named(world, "Marker"), 1);
let mut changed = load_project_runtime(
&cooked,
TestGame {
update: new_code,
changed_health: true,
},
)
.unwrap();
let error = restore_code_reload_state(&mut changed, &state)
.unwrap_err()
.to_string();
assert!(error.contains("test.health"), "{error}");
changed.update(frame).unwrap();
let world = changed.world_mut();
let ball = find_named_entity(world, "Ball").unwrap();
assert_eq!(
world.get::<Transform>(ball).unwrap().position,
[0.0, 1.0, 0.0]
);
assert_eq!(count_named(world, "Coin"), 0);
assert_eq!(count_named(world, "Fresh"), 1);
assert_eq!(count_named(world, "Marker"), 1);
std::fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn press_tick_is_the_event_time_or_the_frame_tick() {
use crate::runtime::{ActionMap, InputBinding, KeyCode};
let mut world = World::new();
world.insert_resource(FrameTime {
fixed_tick: 122,
..FrameTime::default()
});
let mut actions = ActionMap::default();
actions.bind("mine", InputBinding::Key(KeyCode::Space));
actions.bind("menu", InputBinding::Key(KeyCode::Escape));
world.insert_resource(actions);
let mut input = RuntimeInput::default();
input.record_key(KeyCode::Space, true);
input.record_press_tick(InputBinding::Key(KeyCode::Space), 121.4);
input.record_press_tick(InputBinding::Key(KeyCode::Space), 121.9);
input.record_key(KeyCode::Escape, true);
world.insert_resource(input);
let scene = GameScene { world: &mut world };
assert_eq!(scene.press_tick("mine"), Some(121.4));
assert_eq!(scene.press_tick("menu"), Some(122.0));
assert_eq!(scene.press_tick("jump"), None);
}
#[test]
fn sound_requests_are_queued_counted_and_drained() {
let mut world = World::new();
let mut scene = GameScene { world: &mut world };
let id = scene.play_sound("sfx/hit.wav", 0.5);
scene.play_sound_looped("sfx/hum.ogg", 1.0);
scene.stop_sound(id);
scene.set_master_volume(0.2);
assert_eq!(scene.sounds_requested(), 2);
let commands =
world.resource_mut::<crate::runtime::AudioQueue>().drain();
assert_eq!(commands.len(), 4);
assert!(matches!(
&commands[1],
crate::runtime::AudioCommand::Play { looped: true, .. }
));
}
#[test]
fn restart_puts_the_starting_scene_back_and_reruns_setup() {
let directory = std::env::temp_dir()
.join(format!("rusting-restart-{}", uuid::Uuid::new_v4()));
let source = directory.join("main.rscene");
let cooked = directory.join("main.rscene.bin");
let mut editor = App::new();
editor.add_plugin(AssetPlugin).unwrap();
editor.spawn((Name("Ball".into()), Transform::new([0.0, 0.0, 0.0])));
editor.spawn((Name("Gate".into()), Transform::default()));
crate::runtime::save_scene(editor.world_mut(), &source, "main")
.unwrap();
crate::runtime::cook_scene(&source, &cooked).unwrap();
fn code(scene: &mut GameScene<'_>, _: &FrameTime) {
scene.once("setup", |scene| {
scene.spawn_cube(
"Coin",
Transform::default(),
&CubeSpawn::new(),
);
});
scene.despawn("Gate");
scene.object("Ball").move_x(1.0);
if scene.object("Ball").position()[0] >= 3.0 {
assert!(scene.restart());
}
}
let frame = std::time::Duration::from_millis(16);
let mut game = load_project_runtime(
&cooked,
TestGame {
update: code,
changed_health: false,
},
)
.unwrap();
for _ in 0..3 {
game.update(frame).unwrap();
}
let world = game.world_mut();
let ball = find_named_entity(world, "Ball").unwrap();
assert_eq!(world.get::<Transform>(ball).unwrap().position, [0.0; 3]);
assert_eq!(count_named(world, "Gate"), 1);
assert_eq!(count_named(world, "Coin"), 0);
game.update(frame).unwrap();
let world = game.world_mut();
assert_eq!(count_named(world, "Coin"), 1, "setup ran again");
assert_eq!(count_named(world, "Gate"), 0);
let ball = find_named_entity(world, "Ball").unwrap();
assert_eq!(world.get::<Transform>(ball).unwrap().position[0], 1.0);
let mut world = World::new();
assert!(!GameScene { world: &mut world }.restart());
std::fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn basis_forward_follows_the_rotation_order() {
let mut world = World::new();
let rotation = [0.3_f32, 1.0, 0.0];
world.spawn((
Name("Cam".into()),
Transform {
rotation,
..Transform::default()
},
));
let mut scene = GameScene { world: &mut world };
let [_, _, forward] = scene.basis("Cam").unwrap();
let (x, y) = (rotation[0], rotation[1]);
let expected = [-y.sin() * x.cos(), x.sin(), -y.cos() * x.cos()];
for (a, b) in forward.iter().zip(expected) {
assert!((a - b).abs() < 1e-5, "{forward:?} {expected:?}");
}
}
#[test]
fn reparent_and_set_light_move_and_dim_a_lantern() {
use crate::runtime::{Parent, PointLight};
let mut world = World::new();
let lantern = world
.spawn((
Name("Lantern".into()),
Transform::default(),
PointLight {
color: [1.0; 3],
intensity: 5.0,
range: 4.0,
},
))
.id();
let hand = world
.spawn((Name("Hand".into()), Transform::default()))
.id();
let mut scene = GameScene { world: &mut world };
assert!(scene.reparent("Lantern", Some("Hand")));
assert_eq!(
scene.world().get::<Parent>(lantern).map(|p| p.0),
Some(hand)
);
assert!(!scene.reparent("Hand", Some("Lantern")), "a loop");
assert!(scene.reparent("Lantern", None));
assert!(scene.world().get::<Parent>(lantern).is_none());
assert!(scene.set_light("Lantern", None, Some(0.0), Some(2.0)));
assert!(!scene.set_light("Hand", None, Some(1.0), None));
let light = scene.world().get::<PointLight>(lantern).unwrap();
assert_eq!(
(light.color, light.intensity, light.range),
([1.0; 3], 0.0, 2.0)
);
}
#[test]
fn video_settings_write_render_settings_and_the_window_request() {
let mut world = World::new();
world.insert_resource(RenderSettings::default());
let mut scene = GameScene { world: &mut world };
scene.set_render_scale(0.1);
assert_eq!(scene.render_scale(), 0.25);
scene.set_render_scale(f32::NAN);
assert_eq!(scene.render_scale(), 0.25);
scene.set_vsync(true);
scene.set_max_fps(Some(0));
assert!(!scene.fullscreen());
scene.set_fullscreen(true);
scene.set_window_size([1280, 0]);
assert!(scene.fullscreen());
let settings = world.resource::<RenderSettings>();
assert!(settings.vsync && settings.limit_fps);
assert_eq!(settings.max_fps, 1);
assert_eq!(world.resource::<WindowRequest>().size, Some([1280, 1]));
GameScene { world: &mut world }.set_max_fps(None);
assert!(!world.resource::<RenderSettings>().limit_fps);
}
#[test]
fn body_calls_stop_spin_and_reset_bodies_and_set_look_turns_the_player() {
use crate::runtime::{PlayerController, RigidBody, RigidBodyKind};
let mut world = World::new();
let crate_entity = world
.spawn((
Name("Crate".into()),
Transform::default(),
RigidBody {
kind: RigidBodyKind::Dynamic,
..RigidBody::default()
},
crate::runtime::Sleeping,
))
.id();
world.spawn((
Name("Player".into()),
Transform::default(),
PlayerController::default(),
));
let mut scene = GameScene { world: &mut world };
assert!(scene.set_angular_velocity("Crate", [0.0, 2.0, 0.0]));
assert_eq!(scene.angular_velocity("Crate"), Some([0.0, 2.0, 0.0]));
assert!(scene.set_linear_velocity("Crate", [1.0, 0.0, 0.0]));
assert!(scene.set_body_kind("Crate", RigidBodyKind::Kinematic));
assert_eq!(scene.linear_velocity("Crate"), Some([0.0; 3]));
assert_eq!(scene.angular_velocity("Crate"), Some([0.0; 3]));
assert!(scene.set_body_kind("Crate", RigidBodyKind::Dynamic));
assert!(scene.set_angular_velocity("Crate", [3.0, 0.0, 0.0]));
assert!(scene.set_linear_velocity("Crate", [0.0, 5.0, 0.0]));
assert!(scene.reset_body("Crate"));
assert_eq!(scene.linear_velocity("Crate"), Some([0.0; 3]));
assert_eq!(scene.angular_velocity("Crate"), Some([0.0; 3]));
assert!(!scene.reset_body("Nothing"));
assert!(scene
.world()
.get::<crate::runtime::Sleeping>(crate_entity)
.is_none());
assert!(scene.set_look("Player", 1.5, -0.25));
assert!(!scene.set_look("Crate", 0.0, 0.0), "no controller");
let mut players = scene.world().query::<&PlayerController>();
let player = players.single(scene.world()).unwrap();
assert_eq!((player.yaw, player.pitch), (1.5, -0.25));
}
#[test]
fn restart_repeats_a_physics_pile_exactly() {
use crate::runtime::{
Collider, ColliderShape, PhysicsBody, RigidBody, RigidBodyKind,
SimulationClass,
};
let directory = std::env::temp_dir()
.join(format!("rusting-restart-pile-{}", uuid::Uuid::new_v4()));
let source = directory.join("main.rscene");
let cooked = directory.join("main.rscene.bin");
let mut editor = App::new();
editor.add_plugin(AssetPlugin).unwrap();
let body = |kind| {
(
PhysicsBody {
simulation: SimulationClass::Cpu,
..PhysicsBody::default()
},
RigidBody {
kind,
..RigidBody::default()
},
Collider {
shape: ColliderShape::Box {
half_extents: [0.5; 3],
},
..Collider::default()
},
)
};
let mut floor = Transform::new([0.0, -0.5, 0.0]);
floor.scale = [20.0, 1.0, 20.0];
editor.spawn((Name("Floor".into()), floor, body(RigidBodyKind::Fixed)));
for index in 0..12 {
let offset = index as f32;
editor.spawn((
Name(format!("Box {index}")),
crate::runtime::ObjectClasses::new(["box"]),
Transform::new([
(offset * 0.37).sin() * 0.4,
0.6 + offset * 1.05,
(offset * 0.61).cos() * 0.4,
]),
body(RigidBodyKind::Dynamic),
));
}
crate::runtime::save_scene(editor.world_mut(), &source, "main")
.unwrap();
let mut json: serde_json::Value =
serde_json::from_slice(&std::fs::read(&source).unwrap()).unwrap();
json["entities"].as_array_mut().unwrap().reverse();
std::fs::write(&source, json.to_string()).unwrap();
crate::runtime::cook_scene(&source, &cooked).unwrap();
fn code(_: &mut GameScene<'_>, _: &FrameTime) {}
let mut game = load_project_runtime(
&cooked,
TestGame {
update: code,
changed_health: false,
},
)
.unwrap();
let frame = game.world().resource::<FrameTime>().fixed_delta;
let run = |game: &mut App| {
for _ in 0..90 {
game.update(frame).unwrap();
}
let world = game.world_mut();
let hash = GameScene { world: &mut *world }.state_hash("box");
let poses = (0..12)
.map(|index| {
let entity =
find_named_entity(world, &format!("Box {index}"))
.unwrap();
let transform = world.get::<Transform>(entity).unwrap();
(transform.position, transform.rotation)
})
.collect::<Vec<_>>();
(hash, poses)
};
let start = GameScene {
world: game.world_mut(),
}
.state_hash("box");
let first = run(&mut game);
let mut scene = GameScene {
world: game.world_mut(),
};
let initial = scene.initial("Box 11").unwrap();
assert_eq!(
initial.transform.position,
[
(11.0f32 * 0.37).sin() * 0.4,
0.6 + 11.0 * 1.05,
(11.0f32 * 0.61).cos() * 0.4
]
);
assert_eq!(initial.body_kind, Some(RigidBodyKind::Dynamic));
assert!(initial.visible);
assert_eq!(scene.initial("Nothing"), None);
assert!(scene.restart());
let second = run(&mut game);
assert_ne!(first.0, start, "the pile moved");
assert_eq!(first, second, "tolerance 0, and equal class hashes");
for _ in 0..20 {
game.update(frame).unwrap();
}
let snapshot = GameScene {
world: game.world_mut(),
}
.snapshot()
.unwrap();
let went_on = run(&mut game);
GameScene {
world: game.world_mut(),
}
.restore(&snapshot)
.unwrap();
assert_eq!(run(&mut game), went_on, "restored run matches");
std::fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn load_scene_switches_to_another_project_scene() {
let directory = std::env::temp_dir()
.join(format!("rusting-load-scene-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(directory.join("levels")).unwrap();
std::fs::write(directory.join("project.json"), "{}").unwrap();
let source = directory.join("main.rscene");
let cooked = directory.join("build/main.rscene.bin");
let mut editor = App::new();
editor.add_plugin(AssetPlugin).unwrap();
let ball = editor.spawn((Name("Ball".into()), Transform::default()));
crate::runtime::save_scene(editor.world_mut(), &source, "main")
.unwrap();
crate::runtime::cook_scene(&source, &cooked).unwrap();
editor.world_mut().despawn(ball);
editor.spawn((Name("Door".into()), Transform::default()));
crate::runtime::save_scene(
editor.world_mut(),
directory.join("levels/2.rscene"),
"level 2",
)
.unwrap();
fn code(scene: &mut GameScene<'_>, _: &FrameTime) {
if scene.try_object("Ball").is_some() {
assert!(scene.load_scene("levels/missing.rscene").is_err());
assert!(scene.try_object("Ball").is_some(), "scene kept");
scene.load_scene("levels/2.rscene").unwrap();
return;
}
scene.once("setup", |scene| {
scene.spawn_cube(
"Coin",
Transform::default(),
&CubeSpawn::new(),
);
});
scene.object("Door").move_x(1.0);
if scene.object("Door").position()[0] >= 2.0 {
assert!(scene.restart());
}
}
let frame = std::time::Duration::from_millis(16);
let mut game = load_project_runtime(
&cooked,
TestGame {
update: code,
changed_health: false,
},
)
.unwrap();
game.update(frame).unwrap();
let world = game.world_mut();
assert_eq!(count_named(world, "Ball"), 0);
assert_eq!(count_named(world, "Door"), 1);
game.update(frame).unwrap();
assert_eq!(count_named(game.world_mut(), "Coin"), 1);
game.update(frame).unwrap();
let world = game.world_mut();
assert_eq!(count_named(world, "Ball"), 0);
assert_eq!(count_named(world, "Coin"), 0);
let door = find_named_entity(world, "Door").unwrap();
assert_eq!(world.get::<Transform>(door).unwrap().position, [0.0; 3]);
std::fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn named_game_object_moves_without_an_ecs_query_in_game_code() {
let mut world = World::new();
world.spawn((Name("Orange Cube".into()), Transform::default()));
let mut scene = GameScene { world: &mut world };
scene
.object("Orange Cube")
.move_x(2.0)
.move_y(3.0)
.move_z(4.0);
assert_eq!(scene.object("Orange Cube").position(), [2.0, 3.0, 4.0]);
}
#[test]
fn despawned_name_can_be_spawned_again() {
let mut world = World::new();
world.insert_resource(AssetServer::default());
let mut scene = GameScene { world: &mut world };
let first =
scene.spawn_cube("Shot", Transform::default(), &CubeSpawn::new());
scene.world.despawn(first);
let second =
scene.spawn_cube("Shot", Transform::default(), &CubeSpawn::new());
assert_ne!(first, second);
}
#[test]
fn game_randomness_repeats_for_a_seed_and_varies_by_tick_and_stream() {
let draw = |seed: u64, tick: u64, stream: u64| {
let mut world = World::new();
world.insert_resource(crate::runtime::RandomSeed(seed));
world.insert_resource(FrameTime {
fixed_tick: tick,
..FrameTime::default()
});
GameScene { world: &mut world }.random(stream)
};
let value = draw(7, 3, 0);
assert!((0.0..1.0).contains(&value));
assert_eq!(value, draw(7, 3, 0));
assert_ne!(value, draw(8, 3, 0));
assert_ne!(value, draw(7, 4, 0));
assert_ne!(value, draw(7, 3, 1));
let mut world = World::new();
let _ = GameScene { world: &mut world }.random(0);
}
#[test]
fn game_code_reads_and_sets_counters_by_name() {
let mut world = World::new();
world.spawn(crate::runtime::Counter {
name: "gems".into(),
value: 2,
target: Some(5),
});
let mut scene = GameScene { world: &mut world };
scene.counter("gems").unwrap().value += 3;
assert!(scene.counter("gems").unwrap().complete());
assert!(scene.counter("coins").is_none());
assert_eq!(scene.add_to_counter("gems", -1), 4);
assert_eq!(scene.counter_value("gems"), 4);
assert!(!scene.counter_complete("gems"));
assert_eq!(scene.counter_value("coins"), 0);
assert!(!scene.counter_complete("coins"));
assert!(scene.counter("coins").is_none(), "reads create nothing");
assert_eq!(scene.add_to_counter("coins", 1), 1);
scene.set_counter("gems", 9);
assert_eq!(scene.counter_value("gems"), 9);
scene.set_counter("coins", 3);
assert_eq!(scene.counter_value("coins"), 3);
scene.set_counter("night", 2);
assert_eq!(scene.counter_value("night"), 2);
assert!(!scene.counter_complete("night"), "created with no target");
assert!(MISSING_COUNTERS.lock().unwrap().contains("coins"));
}
#[test]
fn game_code_recolors_one_object_without_touching_shared_materials() {
let mut app = crate::App::new();
app.add_plugin(crate::AssetPlugin).unwrap();
let world = app.world_mut();
let (mesh, material) = {
let assets = world.resource::<AssetServer>();
(assets.fallback_mesh, assets.fallback_material)
};
let renderer = MeshRenderer {
mesh,
material,
cast_shadows: true,
receive_shadows: true,
};
world.spawn((Name("A".into()), Transform::default(), renderer));
world.spawn((Name("B".into()), Transform::default(), renderer));
let count =
|world: &World| world.resource::<AssetServer>().materials.len();
let before = count(world);
let mut scene = GameScene { world };
let original = scene.color("A").unwrap();
scene.set_color("A", [1.0, 0.0, 0.0, 1.0]);
scene.set_emissive("A", [2.0, 0.0, 0.0]);
assert_eq!(scene.color("A"), Some([1.0, 0.0, 0.0, 1.0]));
assert_eq!(scene.color("B"), Some(original));
assert_eq!(scene.color("Nowhere"), None);
scene.set_color("B", [1.0, 0.0, 0.0, 1.0]);
scene.set_emissive("B", [2.0, 0.0, 0.0]);
let material = |scene: &mut GameScene<'_>, name| {
let entity = find_named_entity(scene.world, name).unwrap();
scene.world.get::<MeshRenderer>(entity).unwrap().material
};
assert_eq!(material(&mut scene, "A"), material(&mut scene, "B"));
assert_eq!(count(scene.world), before + 2);
}
#[test]
#[should_panic(expected = "did you mean `Crate`")]
fn a_missing_object_name_panic_lists_the_nearest_names() {
let mut world = World::new();
world.spawn((Name("Crate".into()), Transform::new([0.0; 3])));
world.spawn((Name("Ground".into()), Transform::new([0.0; 3])));
GameScene { world: &mut world }.object("crate_");
}
#[test]
fn game_code_reads_and_writes_tile_map_cells_by_world_position() {
let mut world = World::new();
world.spawn((
Name("Level".into()),
Transform::new([-2.0, 1.0, 0.0]),
crate::runtime::TileMap {
rows: vec!["#.o".into()],
..Default::default()
},
));
let mut scene = GameScene { world: &mut world };
assert_eq!(scene.tile("Level", [-1.5, 0.5, 3.0]), Some('#'));
assert_eq!(scene.tile("Level", [0.2, 0.9, 0.0]), Some('o'));
assert_eq!(scene.tile("Level", [5.0, -4.0, 0.0]), Some('.'));
assert_eq!(scene.tile("Level", [-2.5, 0.5, 0.0]), None);
assert_eq!(scene.tile("Nowhere", [0.0; 3]), None);
assert!(scene.set_tile("Level", [0.2, 0.9, 0.0], '*'));
assert!(!scene.set_tile("Level", [0.2, 0.9, 0.0], '*'));
assert!(scene.set_tile("Level", [-1.5, -0.5, 0.0], '#'));
assert!(!scene.set_tile("Level", [-2.5, 0.5, 0.0], '#'));
let map = world
.query::<&crate::runtime::TileMap>()
.single(&world)
.unwrap();
assert_eq!(map.rows, vec!["#.*", "#"]);
}
#[test]
fn aim_hits_what_the_camera_faces_and_despawn_removes_it() {
let mut app = App::new();
let world = app.world_mut();
let player = world
.spawn((Name("Player".into()), Transform::new([0.0, 1.0, 0.0])))
.id();
let camera = world
.spawn((
Name("Camera".into()),
Transform::new([0.0, 0.5, 0.0]).with_rotation(
0.0,
std::f32::consts::FRAC_PI_2,
0.0,
),
crate::runtime::Camera {
active: true,
..Default::default()
},
))
.id();
rusting_core::hierarchy::set_parent(world, camera, player).unwrap();
for (name, x) in [("Left", -5.0), ("Right", 5.0)] {
world.spawn((
Name(name.into()),
Transform::new([x, 1.5, 0.0]),
crate::runtime::PhysicsBody::default(),
crate::runtime::RigidBody {
kind: crate::runtime::RigidBodyKind::Fixed,
..Default::default()
},
crate::runtime::Collider::default(),
));
}
let step = app.world().resource::<FrameTime>().fixed_delta;
app.update(step).unwrap();
let mut scene = GameScene {
world: app.world_mut(),
};
let hit = scene.aim(20.0).expect("the camera faces Left");
assert_eq!(hit.name, "Left");
assert!((hit.point[0] + 4.5).abs() < 1e-3, "{hit:?}");
assert!(scene.despawn("Left"));
assert!(!scene.despawn("Left"));
assert!(scene.try_object("Left").is_none());
let back = scene
.world()
.spawn((
Name("Back Camera".into()),
Transform::new([0.0, 1.5, 0.0]).with_rotation(
0.0,
-std::f32::consts::FRAC_PI_2,
0.0,
),
crate::runtime::Camera::default(),
))
.id();
assert!(!scene.set_active_camera("Right"), "not a camera");
assert!(scene.set_active_camera("Back Camera"));
assert_eq!(scene.object("Back Camera").entity(), back);
assert!(
!scene
.world()
.get::<crate::runtime::Camera>(camera)
.unwrap()
.active
);
let hit = scene.aim(20.0).expect("the back camera faces Right");
assert_eq!(
scene.world().get::<Name>(back).unwrap().as_str(),
"Back Camera"
);
assert_eq!(hit.name, "Right");
}
#[test]
fn a_kinematic_template_copy_launches_along_the_camera_ray() {
let mut app = App::new();
let world = app.world_mut();
world.spawn((
Name("Camera".into()),
Transform::new([0.0, 2.0, 0.0]).with_rotation(
0.0,
std::f32::consts::FRAC_PI_2,
0.0,
),
crate::runtime::Camera {
active: true,
..Default::default()
},
));
world.spawn((
crate::runtime::SceneId::new(),
Name("Ball".into()),
Transform::new([0.0, -30.0, 0.0]),
crate::runtime::PhysicsBody::default(),
crate::runtime::RigidBody {
kind: crate::runtime::RigidBodyKind::Kinematic,
..Default::default()
},
crate::runtime::Collider {
shape: crate::runtime::ColliderShape::Sphere { radius: 0.2 },
..Default::default()
},
));
let step = app.world().resource::<FrameTime>().fixed_delta;
let mut scene = GameScene {
world: app.world_mut(),
};
let (origin, forward) = scene.camera_ray().unwrap();
assert!((origin[1] - 2.0).abs() < 1e-5, "{origin:?}");
assert!((forward[0] + 1.0).abs() < 1e-5, "{forward:?}");
scene.spawn_copy("Ball", "Ball 1", origin);
assert!(scene
.set_body_kind("Ball 1", crate::runtime::RigidBodyKind::Dynamic));
assert!(scene.set_linear_velocity("Ball 1", forward.map(|v| v * 10.0)));
assert_eq!(
scene.linear_velocity("Ball 1"),
Some(forward.map(|v| v * 10.0))
);
assert_eq!(scene.linear_velocity("Nothing"), None);
assert!(!scene
.set_body_kind("Nothing", crate::runtime::RigidBodyKind::Dynamic));
for _ in 0..30 {
app.update(step).unwrap();
}
let mut scene = GameScene {
world: app.world_mut(),
};
let ball = scene.object("Ball 1").position();
assert!(ball[0] < -4.0 && ball[1] < 2.0, "flew and fell: {ball:?}");
assert_eq!(scene.object("Ball").position(), [0.0, -30.0, 0.0]);
}
#[test]
fn pointer_ray_goes_through_the_cursor() {
let mut app = App::new();
app.world_mut().spawn((
Transform::new([0.0, 10.0, 0.0]).with_rotation(
-std::f32::consts::FRAC_PI_2,
0.0,
0.0,
),
crate::runtime::Camera {
active: true,
..Default::default()
},
));
let mut scene = GameScene {
world: app.world_mut(),
};
assert_eq!(scene.pointer_ray(), None, "no cursor yet");
let mut input = scene.world.resource_mut::<RuntimeInput>();
input.record_viewport_size([800.0, 600.0]);
input.record_cursor_position([400.0, 300.0]);
let (origin, down) = scene.pointer_ray().unwrap();
assert!((origin[0].abs() + origin[2].abs()) < 1e-4, "{origin:?}");
assert!((down[1] + 1.0).abs() < 1e-4, "{down:?}");
scene
.world
.resource_mut::<RuntimeInput>()
.record_cursor_position([800.0, 300.0]);
let (_, right) = scene.pointer_ray().unwrap();
assert!(right[0] > 0.1 && right[1] < 0.0, "{right:?}");
}
#[test]
#[should_panic(expected = "`Copy/Glow` already exists")]
fn spawn_copy_refuses_a_child_name_that_is_taken() {
let mut app = App::new();
let world = app.world_mut();
let template = world.spawn(Name("Ember".into())).id();
let glow = world.spawn(Name("Glow".into())).id();
rusting_core::hierarchy::set_parent(world, glow, template).unwrap();
world.spawn(Name("Copy/Glow".into()));
let mut scene = GameScene {
world: app.world_mut(),
};
scene.spawn_copy("Ember", "Copy", [0.0; 3]);
}
#[test]
fn spawn_copy_clones_the_template_tree_and_in_class_lists_it() {
let mut app = App::new();
let world = app.world_mut();
let arena = world
.spawn((Name("Arena".into()), Transform::default()))
.id();
let template = world
.spawn((
crate::runtime::SceneId::new(),
Name("Ember".into()),
Transform::new([0.0, -50.0, 0.0]),
crate::runtime::ObjectClasses {
names: vec!["ember".into()],
},
crate::runtime::PhysicsBody::default(),
crate::runtime::Collider::default(),
crate::runtime::Visibility { visible: false },
))
.id();
let glow = world
.spawn((Name("Glow".into()), Transform::new([0.0, 1.0, 0.0])))
.id();
rusting_core::hierarchy::set_parent(world, glow, template).unwrap();
rusting_core::hierarchy::set_parent(world, template, arena).unwrap();
let mut scene = GameScene {
world: app.world_mut(),
};
let copy = scene
.spawn_copy("Ember", "Ember 1", [3.0, 0.5, 0.0])
.unwrap();
scene.set_visible("Ember 1", true);
assert!(scene.spawn_copy("Missing", "Ember 2", [0.0; 3]).is_none());
assert_eq!(scene.in_class("ember"), ["Ember", "Ember 1"]);
assert_eq!(scene.name_of(copy).as_deref(), Some("Ember 1"));
assert!(scene.has_class(copy, "ember"));
assert!(!scene.has_class(glow, "ember"));
assert!(scene.binding("jump").is_empty());
scene.rebind("jump", &["Space", "PadSouth"]).unwrap();
assert_eq!(scene.binding("jump"), ["Space", "PadSouth"]);
assert_eq!(scene.object("Ember 1").position(), [3.0, 0.5, 0.0]);
assert_eq!(scene.object("Ember").position(), [0.0, -50.0, 0.0]);
let world = app.world();
assert!(world.get::<crate::runtime::Collider>(copy).is_some());
assert!(
world
.get::<crate::runtime::Visibility>(copy)
.unwrap()
.visible
);
assert!(
!world
.get::<crate::runtime::Visibility>(template)
.unwrap()
.visible
);
assert_ne!(
world.get::<crate::runtime::SceneId>(copy),
world.get::<crate::runtime::SceneId>(template)
);
assert_eq!(world.get::<crate::runtime::Parent>(copy).unwrap().0, arena);
assert_eq!(
world
.get::<crate::runtime::Children>(arena)
.unwrap()
.0
.len(),
2
);
let children = &world.get::<crate::runtime::Children>(copy).unwrap().0;
assert_eq!(children.len(), 1);
assert_ne!(children[0], glow);
assert_eq!(world.get::<Name>(children[0]).unwrap().0, "Ember 1/Glow");
assert_eq!(
world.get::<crate::runtime::Parent>(children[0]).unwrap().0,
copy
);
assert_eq!(
world.get::<crate::runtime::Children>(template).unwrap().0,
[glow]
);
let child = children[0];
let step = world.resource::<FrameTime>().fixed_delta;
app.update(step).unwrap();
let global = app
.world()
.get::<crate::runtime::GlobalTransform>(child)
.expect("the copied child gets a world transform");
assert_eq!(global.matrix[3][..3], [3.0, 1.5, 0.0]);
}
#[test]
fn scene_input_actions_bind_and_report_presses() {
let mut app = App::new();
app.world_mut().spawn(crate::runtime::InputAction {
action: "fire".into(),
inputs: vec!["KeyF".into(), "MouseLeft".into()],
});
app.update(std::time::Duration::from_millis(16)).unwrap();
app.world_mut()
.resource_mut::<RuntimeInput>()
.record_mouse_button(crate::runtime::MouseButton::Left, true);
let scene = GameScene {
world: app.world_mut(),
};
assert!(scene.pressed("fire") && scene.held("fire"));
assert!(!scene.pressed("jump"));
let bad = serde_json::from_str::<crate::runtime::InputAction>(
r#"{"action": "fire", "inputs": ["KeyQQ"]}"#,
);
assert!(bad
.unwrap_err()
.to_string()
.contains("unknown input `KeyQQ`"));
}
#[test]
fn optional_object_lookup_handles_missing_names() {
let mut world = World::new();
let mut scene = GameScene { world: &mut world };
assert!(scene.try_object("Missing").is_none());
}
#[test]
fn concise_gpu_watch_registration_is_idempotent() {
let mut app = App::new();
app.add_plugin(HybridPhysicsPlugin).unwrap();
let entity = app.spawn((
Name("Cube".into()),
Transform::default(),
crate::runtime::PhysicsBody {
simulation: crate::runtime::SimulationClass::Gpu,
..Default::default()
},
));
let rule = GpuPhysicsRule::new(
"cube_fell",
crate::runtime::GpuCondition::position_y().less_than(-100.0),
);
let mut scene = GameScene {
world: app.world_mut(),
};
scene.watch_gpu_object("Cube", rule.clone());
scene.watch_gpu_object("Cube", rule);
assert_eq!(
scene
.world
.get::<GpuPhysicsWatch>(entity)
.unwrap()
.rules
.len(),
1
);
}
#[test]
fn class_gpu_watch_registration_is_idempotent() {
let mut app = App::new();
app.add_plugin(HybridPhysicsPlugin).unwrap();
let rule = GpuPhysicsRule::new(
"body_fell",
crate::runtime::GpuCondition::position_y().less_than(-100.0),
);
let mut scene = GameScene {
world: app.world_mut(),
};
scene.watch_gpu_class("falling_cubes", rule.clone());
scene.watch_gpu_class("falling_cubes", rule);
assert_eq!(
scene.world.resource::<GpuPhysicsClassWatches>().classes
["falling_cubes"]
.len(),
1
);
}
#[test]
fn concise_api_spawns_ten_thousand_gpu_cubes_only_once() {
const BODY_COUNT: usize = 10_000;
let mut app = App::new();
app.add_plugin(AssetPlugin).unwrap();
app.add_plugin(HybridPhysicsPlugin).unwrap();
let mut scene = GameScene {
world: app.world_mut(),
};
scene.once("spawn_test_cubes", |scene| {
let cube = CubeSpawn::new().class("gravity").class("falling_cubes");
for index in 0..BODY_COUNT {
scene.spawn_cube(
format!("Physics Cube {index}"),
Transform::new([index as f32, 0.0, 0.0]),
&cube,
);
}
assert_eq!(
scene.apply_gpu_physics_to_class(
"gravity",
&GpuBodySettings::default(),
),
BODY_COUNT
);
});
scene.once("spawn_test_cubes", |_| {
panic!("a completed once block ran twice")
});
let mut query = scene.world.query::<(
&crate::runtime::ObjectClasses,
&crate::runtime::PhysicsBody,
)>();
assert_eq!(query.iter(scene.world).count(), BODY_COUNT);
assert!(query.iter(scene.world).all(|(classes, physics)| {
classes.contains("falling_cubes") && physics.uses_gpu()
}));
}
#[test]
fn sphere_spawning_reuses_mesh_for_matching_subdivisions() {
let mut app = App::new();
app.add_plugin(AssetPlugin).unwrap();
let mut scene = GameScene {
world: app.world_mut(),
};
let sphere = SphereSpawn::new().subdivisions(8).class("gravity");
let first = scene.spawn_sphere(
"Sphere A",
Transform::new([0.0, 1.0, 0.0]),
&sphere,
);
let second = scene.spawn_sphere(
"Sphere B",
Transform::new([0.0, 2.0, 0.0]),
&sphere,
);
let first_mesh = scene
.world
.get::<crate::runtime::MeshRenderer>(first)
.unwrap()
.mesh;
let second_mesh = scene
.world
.get::<crate::runtime::MeshRenderer>(second)
.unwrap()
.mesh;
assert_eq!(first_mesh, second_mesh);
let mesh = scene
.world
.resource::<AssetServer>()
.meshes
.get(first_mesh)
.unwrap();
assert!(!mesh.vertices.is_empty());
assert!(!mesh.indices.is_empty());
assert!(scene
.world
.get::<crate::runtime::ObjectClasses>(first)
.unwrap()
.contains("gravity"));
}
}