// This plugin loads "defs.txt" and applies the therein contained commands extern crate regex; use bevy::prelude::*; use bevy_xpbd_3d::prelude::*; use bevy::math::DVec3; use crate::{actor, chat, hud, nature, world}; use regex::Regex; use std::f32::consts::PI; use std::f64::consts::PI as PI64; use std::collections::HashMap; 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_event::(); } } #[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, model: String, model_scale: f32, rotation: Quat, velocity: DVec3, angular_momentum: DVec3, pronoun: String, is_sphere: bool, is_player: bool, is_lifeform: bool, is_alive: bool, is_suited: bool, is_vehicle: bool, is_clickable: bool, has_physics: bool, wants_maxrotation: Option, wants_maxvelocity: Option, collider_is_mesh: 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, } 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), model: "".to_string(), model_scale: 1.0, rotation: Quat::IDENTITY, velocity: DVec3::splat(0.0), angular_momentum: DVec3::new(0.03, 0.3, 0.09), pronoun: "they/them".to_string(), is_sphere: false, is_player: false, is_lifeform: false, is_alive: false, is_suited: false, is_vehicle: false, is_clickable: true, has_physics: true, wants_maxrotation: None, wants_maxvelocity: None, collider_is_mesh: 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, } } } 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!("defs.txt"); let mut lines = defs_string.lines(); let mut state = ParserState::default(); let mut command; let mut parameters; let mut id2pos: HashMap = HashMap::new(); 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 = 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; } } ["relativeto", id] => { // NOTE: call this command before "id", otherwise actors that // set their position relative to this actor will get the wrong offset match id2pos.get(&id.to_string()) { Some(pos) => { state.pos += *pos; } None => { error!("Specified `relativeto` command but could not find id `{id}`"); continue; } } } ["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(); id2pos.insert(state.id.clone(), state.pos.clone()); } ["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; } ["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 = 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; } ["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()); } _ => { 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>, ) { for state_wrapper in er_spawn.read() { let state = &state_wrapper.0; if state.class == DefClass::Actor { 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(state.pos)); actor.insert(Rotation::from(state.rotation)); if state.is_sphere { let sphere_texture_handle: Handle = asset_server.load(format!("textures/{}.jpg", state.model)); let sphere_handle = meshes.add(Sphere::new(1.0).mesh().uv(128, 128)); let sphere_material_handle = materials.add(StandardMaterial { base_color_texture: Some(sphere_texture_handle.clone()), 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 { actor.insert(SceneBundle { transform: Transform { scale: Vec3::splat(state.model_scale), ..default() }, scene: asset_server.load(world::asset_name_to_path(state.model.as_str())), ..default() }); } // 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(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(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 { 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_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(), ..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.chat.is_empty() { actor.insert(chat::Talker { actor_id: state.id.clone(), chat_name: state.chat.clone(), name: state.name.clone(), pronoun: Some(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 { commands.spawn(( hud::AugmentedRealityOverlay { owner: actor_entity, }, world::DespawnOnPlayerDeath, SceneBundle { scene: asset_server.load(world::asset_name_to_path(ar_asset_name)), visibility: Visibility::Hidden, ..default() }, )); } } } }