// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module populates the world with actors as defined in "defs.txt" extern crate regex; use bevy::prelude::*; use bevy_xpbd_3d::prelude::*; use bevy::math::DVec3; use crate::{actor, camera, chat, hud, nature, shading, skeleton, world}; use regex::Regex; use std::f32::consts::PI; use std::f64::consts::PI as PI64; pub struct CommandsPlugin; impl Plugin for CommandsPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, load_defs); app.add_systems(Update, spawn_entities); app.add_systems(PreUpdate, hide_colliders .run_if(any_with_component::)); app.add_event::(); } } #[derive(Component)] pub struct NeedsSceneColliderRemoved; #[derive(Event)] pub struct SpawnEvent(ParserState); #[derive(PartialEq, Clone)] enum DefClass { Actor, None, } #[derive(Clone)] struct ParserState { class: DefClass, // Generic fields name: Option, chat: String, // Actor fields id: String, pos: DVec3, relative_to: Option, model: Option, model_scale: f32, rotation: Quat, velocity: DVec3, angular_momentum: DVec3, pronoun: Option, is_sphere: bool, is_player: bool, is_lifeform: bool, is_alive: bool, is_suited: bool, is_vehicle: bool, is_clickable: bool, is_targeted_on_startup: bool, is_sun: bool, is_point_of_interest: bool, has_physics: bool, has_ring: bool, wants_maxrotation: Option, wants_maxvelocity: Option, collider_is_mesh: bool, collider_is_one_mesh_of_scene: bool, thrust_forward: f32, thrust_sideways: f32, thrust_back: f32, reaction_wheels: f32, warmup_seconds: f32, engine_type: actor::EngineType, oxygen: f32, density: f64, collider: Collider, camdistance: f32, suit_integrity: f32, light_brightness: f32, light_color: Option, ar_model: Option, show_only_in_map_at_distance: Option<(f64, String)>, } impl Default for ParserState { fn default() -> Self { let default_actor = actor::Actor::default(); let default_engine = actor::Engine::default(); Self { class: DefClass::None, name: None, chat: "".to_string(), id: "".to_string(), pos: DVec3::new(0.0, 0.0, 0.0), relative_to: None, model: None, model_scale: 1.0, rotation: Quat::IDENTITY, velocity: DVec3::splat(0.0), angular_momentum: DVec3::new(0.03, 0.3, 0.09), pronoun: None, is_sphere: false, is_player: false, is_lifeform: false, is_alive: false, is_suited: false, is_vehicle: false, is_clickable: true, is_targeted_on_startup: false, is_sun: false, is_point_of_interest: false, has_physics: true, has_ring: false, wants_maxrotation: None, wants_maxvelocity: None, collider_is_mesh: false, collider_is_one_mesh_of_scene: false, thrust_forward: default_engine.thrust_forward, thrust_sideways: default_engine.thrust_forward, thrust_back: default_engine.thrust_back, reaction_wheels: default_engine.reaction_wheels, warmup_seconds: default_engine.warmup_seconds, engine_type: default_engine.engine_type, oxygen: nature::OXY_D, density: 100.0, collider: Collider::sphere(1.0), camdistance: default_actor.camdistance, suit_integrity: 1.0, light_brightness: 0.0, light_color: None, ar_model: None, show_only_in_map_at_distance: None, } } } pub fn load_defs( mut ew_spawn: EventWriter, ) { let re1 = Regex::new(r"^\s*([a-z_-]+)\s+(.*)$").unwrap(); let re2 = Regex::new("\"([^\"]*)\"|(-?[0-9]+[0-9e-]*(?:\\.[0-9e-]+)?)|([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap(); let defs_string = include_str!("data/defs.txt"); let mut lines = defs_string.lines(); let mut state = ParserState::default(); let mut command; let mut parameters; let mut line_nr = -1; while let Some(line) = lines.next() { line_nr += 1; let caps = re1.captures(line); if caps.is_none() { if line.trim() != "" { error!("Syntax Error in definitions line {}: `{}`", line_nr, line); } continue; } if let Some(caps) = caps { command = caps.get(1).unwrap().as_str(); parameters = caps.get(2).unwrap().as_str(); } else { error!("Failed to read regex captures in line {}: `{}`", line_nr, line); continue; } let mut parts: Vec<&str> = Vec::new(); parts.push(command); for caps in re2.captures_iter(parameters) { if let Some(part) = caps.get(1) { parts.push(&part.as_str()); } if let Some(part) = caps.get(2) { parts.push(&part.as_str()); } if let Some(part) = caps.get(3) { parts.push(&part.as_str()); } } match parts.as_slice() { ["name", name] => { debug!("Registering name: {}", name); state.name = Some(name.to_string()); } // Parsing actors ["actor", x, y, z, model] => { ew_spawn.send(SpawnEvent(state)); state = ParserState::default(); state.class = DefClass::Actor; state.model = Some(model.to_string()); if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.pos = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse coordinates as floats in def: {line}"); state = ParserState::default(); continue; } } ["actor", x, y, z] => { ew_spawn.send(SpawnEvent(state)); state = ParserState::default(); state.class = DefClass::Actor; if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.pos = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse coordinates as floats in def: {line}"); state = ParserState::default(); continue; } } ["relativeto", id] => { state.relative_to = Some(id.to_string()); } ["orbit", radius_str, phase_str] => { if let (Ok(r), Ok(phase)) = (radius_str.parse::(), phase_str.parse::()) { let phase_deg = phase * PI64 * 2.0; state.pos = DVec3::new( state.pos.x + r * phase_deg.cos(), state.pos.y, state.pos.z + r * phase_deg.sin(), ); } else { error!("Can't parse float: {line}"); continue; } } ["sphere", "yes"] => { state.is_sphere = true; } ["id", id] => { state.id = id.to_string(); } ["alive", "yes"] => { state.is_alive = true; state.is_lifeform = true; state.is_suited = true; } ["vehicle", "yes"] => { state.is_vehicle = true; } ["clickable", "no"] => { state.is_clickable = false; } ["moon", "yes"] => { state.model_scale *= 3.0; } ["sun", "yes"] => { state.is_sun = true; state.model_scale *= 5.0; } ["ring", "yes"] => { state.has_ring = true; } ["pointofinterest", "yes"] => { state.is_point_of_interest = true; } ["oxygen", amount] => { if let Ok(amount) = amount.parse::() { state.is_lifeform = true; state.is_suited = true; state.oxygen = amount; } else { error!("Can't parse float: {line}"); continue; } } ["pronoun", pronoun] => { state.pronoun = Some(pronoun.to_string()); } ["chatid", chat] => { state.chat = chat.to_string(); } ["scale", scale] => { if let Ok(scale_float) = scale.parse::() { state.model_scale = scale_float; } else { error!("Can't parse float: {line}"); continue; } } ["rotationx", rotation_x] => { if let Ok(rotation_x_float) = rotation_x.parse::() { state.rotation *= Quat::from_rotation_x(PI * rotation_x_float); } else { error!("Can't parse float: {line}"); continue; } } ["rotationy", rotation_y] => { if let Ok(rotation_y_float) = rotation_y.parse::() { state.rotation *= Quat::from_rotation_y(PI * rotation_y_float); } else { error!("Can't parse float: {line}"); continue; } } ["rotationz", rotation_z] => { if let Ok(rotation_z_float) = rotation_z.parse::() { state.rotation *= Quat::from_rotation_z(PI * rotation_z_float); } else { error!("Can't parse float: {line}"); continue; } } ["velocity", x, y, z] => { if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.velocity = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse float: {line}"); continue; } } ["angularmomentum", x, y, z] => { if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.angular_momentum = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse float: {line}"); continue; } } ["thrust", forward, back, sideways, reaction_wheels, warmup_time] => { if let (Ok(forward_float), Ok(back_float), Ok(sideways_float), Ok(reaction_wheels_float), Ok(warmup_time_float)) = (forward.parse::(), back.parse::(), sideways.parse::(), reaction_wheels.parse::(), warmup_time.parse::()) { state.thrust_forward = forward_float; state.thrust_back = back_float; state.thrust_sideways = sideways_float; state.reaction_wheels = reaction_wheels_float; state.warmup_seconds = warmup_time_float; } } ["engine", "rocket"] => { state.engine_type = actor::EngineType::Rocket; } ["engine", "ion"] => { state.engine_type = actor::EngineType::Ion; } ["engine", "monopropellant"] => { state.engine_type = actor::EngineType::Monopropellant; } ["health", value] => { if let Ok(value_float) = value.parse::() { state.suit_integrity = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["density", value] => { if let Ok(value_float) = value.parse::() { state.density = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["physics", "off"] => { state.has_physics = false; } ["collider", "sphere", radius] => { if let Ok(radius_float) = radius.parse::() { state.collider = Collider::sphere(radius_float); } else { error!("Can't parse float: {line}"); continue; } } ["collider", "capsule", height, radius] => { if let (Ok(height_float), Ok(radius_float)) = (height.parse::(), radius.parse::()) { state.collider = Collider::capsule(height_float, radius_float); } else { error!("Can't parse float: {line}"); continue; } } ["collider", "mesh"] => { state.collider_is_mesh = true; } ["collider", "handcrafted"] => { state.collider_is_one_mesh_of_scene = true; } ["player", "yes"] => { state.is_player = true; state.is_alive = true; } ["camdistance", value] => { if let Ok(value_float) = value.parse::() { state.camdistance = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["light", color_hex, brightness] => { if let Ok(brightness_float) = brightness.parse::() { if let Ok(color) = Color::hex(color_hex) { state.light_color = Some(color); state.light_brightness = brightness_float; } else { error!("Can't parse hexadecimal color code: {line}"); continue; } } else { error!("Can't parse float: {line}"); continue; } } ["wants", "maxrotation", value] => { // NOTE: requires an engine to slow down velocity if let Ok(value_float) = value.parse::() { state.wants_maxrotation = Some(value_float); } else { error!("Can't parse float: {line}"); continue; } } ["wants", "maxvelocity", value] => { // NOTE: requires an engine to slow down velocity if let Ok(value_float) = value.parse::() { state.wants_maxvelocity = Some(value_float); } else { error!("Can't parse float: {line}"); continue; } } ["armodel", asset_name] => { state.ar_model = Some(asset_name.to_string()); } ["targeted", "yes"] => { state.is_targeted_on_startup = true; } ["only_in_map_at_dist", value, id] => { if let Ok(value_float) = value.parse::() { state.show_only_in_map_at_distance = Some((value_float, id.to_string())); } else { error!("Can't parse float: {line}"); continue; } } _ => { error!("No match for [{}]", parts.join(",")); } } } ew_spawn.send(SpawnEvent(state)); } fn spawn_entities( mut er_spawn: EventReader, mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, mut materials_jupiter: ResMut>, mut id2pos: ResMut, ) { for state_wrapper in er_spawn.read() { let state = &state_wrapper.0; if state.class == DefClass::Actor { let relative_pos = if let Some(id) = &state.relative_to { match id2pos.0.get(&id.to_string()) { Some(pos) => { state.pos + *pos } None => { error!("Specified `relativeto` command but could not find id `{id}`"); continue; } } } else { state.pos }; let actor_entity; { let mut actor = commands.spawn_empty(); actor.insert(actor::Actor { id: state.id.clone(), name: state.name.clone(), camdistance: state.camdistance, ..default() }); actor.insert(SleepingDisabled); actor.insert(world::DespawnOnPlayerDeath); actor.insert(actor::HitPoints::default()); actor.insert(Position::from(relative_pos)); actor.insert(Rotation::from(state.rotation)); if state.is_sphere { let sphere_texture_handle = if let Some(model) = &state.model { Some(asset_server.load(format!("textures/{}.jpg", model))) } else { None }; let sphere_handle = meshes.add(Sphere::new(1.0).mesh().uv(128, 128)); let sphere_material_handle = materials.add(StandardMaterial { base_color_texture: sphere_texture_handle, perceptual_roughness: 1.0, metallic: 0.0, ..default() }); actor.insert(PbrBundle { mesh: sphere_handle, material: sphere_material_handle, transform: Transform { scale: Vec3::splat(state.model_scale), ..default() }, ..default() }); } else if let Some(model) = &state.model { actor.insert(SpatialBundle { transform: Transform { scale: Vec3::splat(state.model_scale), ..default() }, ..default() }); skeleton::load(model.as_str(), &mut actor, &*asset_server); } // Physics Parameters if state.has_physics { actor.insert(RigidBody::Dynamic); actor.insert(LinearVelocity(state.velocity)); actor.insert(AngularVelocity(state.angular_momentum)); actor.insert(ColliderDensity(state.density)); if state.collider_is_mesh { actor.insert(MassPropertiesBundle::new_computed( &Collider::sphere(0.5 * state.model_scale as f64), state.density)); actor.insert(AsyncSceneCollider::new(Some( ComputedCollider::TriMesh //ComputedCollider::ConvexDecomposition(VHACDParameters::default()) ))); } else if state.collider_is_one_mesh_of_scene { actor.insert(MassPropertiesBundle::new_computed( &Collider::sphere(0.5 * state.model_scale as f64), state.density)); actor.insert(AsyncSceneCollider::new(None) .with_shape_for_name("Collider", ComputedCollider::TriMesh) .with_layers_for_name("Collider", CollisionLayers::ALL) //.with_density_for_name("Collider", state.density) ); actor.insert(NeedsSceneColliderRemoved); } else { actor.insert(state.collider.clone()); } } // TODO: angular velocity for objects without collisions, static objects // Optional Components if state.is_player { actor.insert(actor::Player); actor.insert(actor::PlayerCamera); } if state.is_sun { let (r, g, b) = nature::star_color_index_to_rgb(0.656); actor.insert(materials.add(StandardMaterial { base_color: Color::rgb(r, g, b) * 13.0, unlit: true, ..default() })); } if state.is_targeted_on_startup { actor.insert(hud::IsTargeted); } if let Some((mindist, id)) = &state.show_only_in_map_at_distance { actor.insert(camera::ShowOnlyInMap { min_distance: *mindist, distance_to_id: id.clone() }); } if state.is_player || state.is_vehicle { // used to apply mouse movement to actor rotation actor.insert(ExternalTorque::ZERO.with_persistence(false)); } if state.is_lifeform { actor.insert(actor::LifeForm::default()); actor.insert(actor::ExperiencesGForce::default()); actor.insert(actor::Suit { oxygen: state.oxygen, oxygen_max: nature::OXY_D, integrity: state.suit_integrity, ..default() }); } if state.is_clickable { actor.insert(hud::IsClickable { name: state.name.clone(), pronoun: state.pronoun.clone(), ..default() }); } if let Some(value) = state.wants_maxrotation { actor.insert(actor::WantsMaxRotation(value)); } if let Some(value) = state.wants_maxvelocity { actor.insert(actor::WantsMaxVelocity(value)); } if let Some(color) = state.light_color { actor.insert(PointLightBundle { point_light: PointLight { intensity: state.light_brightness, color, range: 100.0, radius: 100.0, ..default() }, ..default() }); } if !state.id.is_empty() { actor.insert(actor::Identifier(state.id.clone())); id2pos.0.insert(state.id.clone(), relative_pos); } if !state.chat.is_empty() { actor.insert(chat::Talker { actor_id: state.id.clone(), chat_name: state.chat.clone(), name: state.name.clone(), pronoun: state.pronoun.clone(), talking_speed: 1.0, }); } if state.is_vehicle { actor.insert(actor::Vehicle::default()); } if state.is_vehicle || state.is_suited || state.thrust_forward > 0.0 || state.thrust_sideways > 0.0 || state.thrust_back > 0.0 || state.reaction_wheels > 0.0 { actor.insert(actor::Engine { thrust_forward: state.thrust_forward, thrust_back: state.thrust_back, thrust_sideways: state.thrust_sideways, reaction_wheels: state.reaction_wheels, warmup_seconds: state.warmup_seconds, engine_type: state.engine_type, ..default() }); } if let Some(_) = state.ar_model { actor.insert(hud::AugmentedRealityOverlayBroadcaster); } actor_entity = actor.id(); } if let Some(ar_asset_name) = &state.ar_model { let mut entitycmd = commands.spawn(( hud::AugmentedRealityOverlay { owner: actor_entity, }, world::DespawnOnPlayerDeath, SpatialBundle { visibility: Visibility::Hidden, ..default() }, )); skeleton::load(ar_asset_name, &mut entitycmd, &*asset_server); } if state.is_point_of_interest { let mut entitycmd = commands.spawn(( hud::PointOfInterestMarker(actor_entity), world::DespawnOnPlayerDeath, hud::ToggleableHudElement, SpatialBundle { visibility: Visibility::Hidden, ..default() }, )); skeleton::load("point_of_interest", &mut entitycmd, &*asset_server); } if state.has_ring { commands.spawn(( world::DespawnOnPlayerDeath, MaterialMeshBundle { mesh: meshes.add(Mesh::from(Cylinder::new(nature::JUPITER_RING_RADIUS as f32, 1.0))), material: materials_jupiter.add(shading::JupitersRing { alpha_mode: AlphaMode::Blend, ring_radius: nature::JUPITER_RING_RADIUS as f32, jupiter_radius: nature::JUPITER_RADIUS as f32, }), transform: Transform::from_translation(relative_pos.as_vec3()), ..default() }, Position::new(relative_pos), Rotation::from(Quat::IDENTITY), //Rotation::from(Quat::from_rotation_x(-0.3f32.to_radians())), )); } } } } pub fn hide_colliders( //mut commands: Commands, mut q_mesh: Query<(&mut Visibility, &Name), With>>, //q_flag: Query>, ) { for (mut visibility, name) in &mut q_mesh { if name.as_str() == "Collider" { *visibility = Visibility::Hidden; } } // TODO: not quite done here yet... // for entity in &q_flag { // commands.entity(entity).remove::(); // } }