diff --git a/frontend/app/src/game.rs b/frontend/app/src/game.rs index 9339beee..8dfe776b 100644 --- a/frontend/app/src/game.rs +++ b/frontend/app/src/game.rs @@ -349,10 +349,14 @@ impl Component for Game { if matches!(self.overlay, Some(Overlay::Compiling)) { self.overlay = None; } + + // TODO: Smarter cache eviction policy if self.compilation_cache.len() > 10 { self.compilation_cache.clear(); } let mut teams_with_errors = vec![]; + + // Display compiler errors or cache compilation results for (team, result) in results.iter().enumerate() { match result { Ok(code) => { @@ -375,6 +379,7 @@ impl Component for Game { .cloned() .collect(); if errors.is_empty() { + // If no errors, start running simulation services::send_telemetry(Telemetry::StartScenario { scenario_name: context.props().scenario.clone(), code: code_to_string(&self.player_team().running_source_code), @@ -382,6 +387,7 @@ impl Component for Game { self.run(context, execution_mode); self.focus_simulation(); } else { + // Populate compiler output with compilation errors and focus compiler output tab self.compiler_errors = Some(errors.join("\n")); self.focus_editor(teams_with_errors[0]); js::golden_layout::select_tab("compiler_output"); @@ -1050,11 +1056,13 @@ impl Game { .link() .callback(move |results| Msg::CompileFinished(results, execution_mode)); + /// Sends code to the compiler service, returns a compiled WASM binary async fn compile(text: String) -> Result { if text.trim().is_empty() { return Ok(Code::None); } + // Compilation time will be logged let start_time = instant::Instant::now(); let url = format!("{}/compile", services::compiler_url()); @@ -1091,10 +1099,12 @@ impl Game { } else if team.running_source_code == team.initial_source_code && team.initial_compiled_code != Code::None { + // Avoid recompilation if using the initial source code team.initial_compiled_code.clone() } else if let Some(compiled_code) = self.compilation_cache.get(&team.running_source_code) { + // Avoid recompilation if current code has already been compilde and cached compiled_code.clone() } else { team.running_source_code.clone() @@ -1104,6 +1114,8 @@ impl Game { wasm_bindgen_futures::spawn_local(async move { let mut results = vec![]; + + // All uncompiled code is compiled, and the callback defined above is called on completion for source_code in source_codes { let result = match source_code { Code::Rust(text) => compile(text).await, @@ -1116,15 +1128,21 @@ impl Game { }); } + /// Kicks off simulation using running compiled code from each team + /// NOTE: Assumes no compiler errors pub fn run(&mut self, context: &Context, execution_mode: ExecutionMode) { self.compiler_errors = None; + // Collect compiled code from each team let codes: Vec<_> = self .teams .iter() .map(|x| x.running_compiled_code.clone()) .collect(); let rand_seed = rand::thread_rng().gen(); + + // If replaying, reuse previous seed if it exists + // instead of using a newly generated seed let seed = match execution_mode { ExecutionMode::Initial | ExecutionMode::Run => { self.configured_seed(context).unwrap_or(rand_seed) @@ -1134,6 +1152,8 @@ impl Game { .unwrap_or(self.previous_seed.unwrap_or(rand_seed)), }; let start_paused = matches!(execution_mode, ExecutionMode::Replay { paused: true }); + + // Cache seed for replays self.previous_seed = Some(seed); self.execution_mode = execution_mode; diff --git a/frontend/simulation_worker/src/lib.rs b/frontend/simulation_worker/src/lib.rs index e605a322..637629a2 100644 --- a/frontend/simulation_worker/src/lib.rs +++ b/frontend/simulation_worker/src/lib.rs @@ -55,6 +55,7 @@ impl yew_agent::Worker for SimAgent { nonce, } => { self.sim = Some(Simulation::new(&scenario_name, seed, &codes)); + // Snapshot of the starting state of the simulation let snapshot = self.sim().snapshot(nonce); self.errored = !snapshot.errors.is_empty(); self.link.respond(who, Response::Snapshot { snapshot }); diff --git a/shared/compiler/src/lib.rs b/shared/compiler/src/lib.rs index 706275a9..ea601bc5 100644 --- a/shared/compiler/src/lib.rs +++ b/shared/compiler/src/lib.rs @@ -95,6 +95,7 @@ impl Compiler { tmp_path.join("ai/src/lib.rs"), include_bytes!("../../ai/src/lib.rs"), )?; + // This is the file that exposes the global `tick` function referened in `shared/simulator/src/vm/mod.rs` std::fs::write( tmp_path.join("ai/src/tick.rs"), include_bytes!("../../ai/src/tick.rs"), @@ -103,6 +104,8 @@ impl Compiler { let disallowed_environment_variables = ["RUSTC_WORKSPACE_WRAPPER", "RUSTC_WRAPPER"]; + // TODO: If `cargo` crate was imported, we could call this directly instead of going through + // the command line match std::process::Command::new("cargo") .args([ "build", diff --git a/shared/simulator/src/bullet.rs b/shared/simulator/src/bullet.rs index 72290fc7..7998e162 100644 --- a/shared/simulator/src/bullet.rs +++ b/shared/simulator/src/bullet.rs @@ -85,23 +85,30 @@ pub fn destroy(sim: &mut Simulation, handle: BulletHandle) { ); } +/// Process movement and collisions for each bullet in the simulation +/// +/// NOTE: As a performance optimization, colliders are only added to +/// bullets after a collision has been detected since most bullets +/// spend most of their lifetime far away from any ships they could +/// hit. pub fn tick(sim: &mut Simulation) { let dt = PHYSICS_TICK_LENGTH; + // Indices for ships let (indices_by_team, coarse_grids_by_team) = build_indices(sim, dt); let mut stack = Vec::new(); let shape = rapier2d_f64::geometry::Ball { radius: 1.0 }; let bullets: Vec = sim.bullets.iter().cloned().collect(); for handle in bullets { - let team = { - let data = data_mut(sim, handle); - data.ttl -= dt as f32; - if data.ttl <= 0.0 { - destroy(sim, handle); - continue; - } - data.team - }; + let data = data_mut(sim, handle); + // If a bullet's lifetime has been exceeded, destroy it and move on to the next bullet + data.ttl -= dt as f32; + if data.ttl <= 0.0 { + destroy(sim, handle); + continue; + } + + let team = data.team; let has_collider; let coarse_grid_hit; let mut needs_collider = false; @@ -110,6 +117,7 @@ pub fn tick(sim: &mut Simulation) { let body = sim.bodies.get_mut(RigidBodyHandle(handle.index())).unwrap(); has_collider = !body.colliders().is_empty(); + // If bullet is outside world, destroy it let position = *body.translation(); if position.x < -world_size / 2.0 || position.x > world_size / 2.0 @@ -120,6 +128,7 @@ pub fn tick(sim: &mut Simulation) { continue; } + // If bullet maybe hits a ship belonging to another team coarse_grid_hit = coarse_grids_by_team .iter() .any(|(other_team, grid)| *other_team != team && grid.lookup(position)); @@ -129,8 +138,10 @@ pub fn tick(sim: &mut Simulation) { &body.predict_position_using_velocity_and_forces(dt), ); + // Check index for hit for (other_team, index) in indices_by_team.iter() { if team != *other_team { + // The bullet needs a collider if there was a hit needs_collider = needs_collider || index .query_iter_with_stack( @@ -142,6 +153,8 @@ pub fn tick(sim: &mut Simulation) { ) .next() .is_some(); + // TODO: Could we break here if the index query returns something + // rather than continuing to check if a bullet has hit multiple teams? } } } @@ -153,6 +166,7 @@ pub fn tick(sim: &mut Simulation) { remove_collider(sim, handle); } + // Debugging helper if COLOR_COLLIDERS { if needs_collider { data_mut(sim, handle).color = 0x00ff00ff; @@ -165,6 +179,9 @@ pub fn tick(sim: &mut Simulation) { } } +/// Builds ship position indices for fast lookups +/// +/// Returns (indicies_by_team, coarse_grids_by_team) fn build_indices( sim: &Simulation, dt: f64, @@ -261,10 +278,13 @@ impl CoarseGrid { pub fn insert(&mut self, mut aabb: Aabb) { aabb.mins -= vector![Self::CELL_SIZE, Self::CELL_SIZE]; aabb.maxs += vector![Self::CELL_SIZE, Self::CELL_SIZE]; + + // If the aabb intersects with the world, work with the intersection. if let Some(aabb) = aabb.intersection(&Aabb::from_half_extents( point![0.0, 0.0], vector![MAX_WORLD_SIZE / 2.0, MAX_WORLD_SIZE / 2.0], )) { + // Set the cells occupied by the aabb to true let w = ((aabb.maxs.x - aabb.mins.x) * Self::RECIP_CELL_SIZE).ceil() as i32; let h = ((aabb.maxs.y - aabb.mins.y) * Self::RECIP_CELL_SIZE).ceil() as i32; let min_index = Self::to_cell(aabb.mins.coords); diff --git a/shared/simulator/src/debug.rs b/shared/simulator/src/debug.rs index c0dad47e..be53ef93 100644 --- a/shared/simulator/src/debug.rs +++ b/shared/simulator/src/debug.rs @@ -12,6 +12,7 @@ pub struct Line { pub color: Vector4, } +/// Display gun orientation, acceleration vector, and radar radius for each ship pub fn emit_ship(sim: &mut Simulation, handle: ShipHandle) { let mut lines = Vec::with_capacity(2 + sim.ship(handle).data().guns.len()); let body = sim.ship(handle).body(); diff --git a/shared/simulator/src/radio.rs b/shared/simulator/src/radio.rs index 30813480..30ed15df 100644 --- a/shared/simulator/src/radio.rs +++ b/shared/simulator/src/radio.rs @@ -56,6 +56,7 @@ pub fn tick(sim: &mut Simulation) { let mut receivers: BTreeMap> = BTreeMap::new(); let mut senders: BTreeMap> = BTreeMap::new(); + // Create a cache of radio senders and recievers for each channel for handle in handle_snapshot.iter().cloned() { let ship = sim.ship(handle); let ship_data = ship.data(); @@ -82,6 +83,8 @@ pub fn tick(sim: &mut Simulation) { } for channel in 0..NUM_CHANNELS { + // For each reciever, look for the sender on the same channel with the highest + // signal strength, and give that message to the reciever for rx in receivers.get(&channel).unwrap_or(&Vec::new()) { let mut best_msg = None; let mut best_rssi = rx.min_rssi; @@ -100,6 +103,7 @@ pub fn tick(sim: &mut Simulation) { } } + // Reset sent messages for handle in handle_snapshot.iter().cloned() { for radio in sim.ship_mut(handle).data_mut().radios.iter_mut() { radio.sent = None; @@ -107,6 +111,10 @@ pub fn tick(sim: &mut Simulation) { } } +/// Computes signal strength between a sender and reciever based on +/// their distance from each other and the sender's power. +/// +/// Reference: https://en.wikipedia.org/wiki/Received_signal_strength_indicator fn compute_rssi(sender: &RadioSender, receiver: &RadioReceiver) -> f64 { let r_sq = nalgebra::distance_squared(&sender.position, &receiver.position); sender.power * receiver.rx_cross_section / (TAU * r_sq) diff --git a/shared/simulator/src/ship.rs b/shared/simulator/src/ship.rs index 265f17e9..c38fa6e1 100644 --- a/shared/simulator/src/ship.rs +++ b/shared/simulator/src/ship.rs @@ -766,6 +766,7 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { let team = ship_data.team; let gun = { let gun = &mut ship_data.guns[index as usize]; + // Exit if gun is still reloading if gun.reload_ticks_remaining > 0 { return; } @@ -932,13 +933,10 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { } pub fn tick(&mut self) { - // Weapons. + // Weapons + // Handle reload timers { - let ship_data = self - .simulation - .ship_data - .get_mut(self.handle.index()) - .unwrap(); + let ship_data = self.data_mut(); for gun in ship_data.guns.iter_mut() { if gun.reload_ticks_remaining > 0 { gun.reload_ticks_remaining -= 1; @@ -952,7 +950,8 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { } } - // Acceleration. + // Movement + // Apply boosts, consume fuel { let mut acceleration = self.data().acceleration; if self.readonly().is_ability_active(Ability::Boost) { @@ -976,7 +975,7 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { self.data_mut().acceleration = vector![0.0, 0.0]; } - // Torque. + // Torque { let inertia_sqrt = 1.0 / self @@ -991,6 +990,7 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { } // TTL + // Destroy ship if it exceeds TTL { if let Some(ttl) = self.data_mut().ttl { self.data_mut().ttl = Some(ttl - 1); @@ -1000,7 +1000,8 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { } } - // Special abilities. + // Special abilities + // Handle reload and remaining time { for ship_ability in self.data_mut().abilities.iter_mut() { ship_ability.active_time_remaining = @@ -1010,7 +1011,8 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { } } - // Destruction. + // Destruction + // If a ship has been destroyed, remove it from the simulation if self.data().destroyed { if let Some(team_ctrl) = self.simulation.get_team_controller(self.data().team) { team_ctrl.borrow_mut().remove_ship(self.handle); @@ -1022,7 +1024,7 @@ impl<'a: 'b, 'b> ShipAccessorMut<'a> { &mut self.simulation.colliders, &mut self.simulation.impulse_joints, &mut self.simulation.multibody_joints, - /*remove_attached_colliders=*/ true, + true, ); self.simulation .ship_data diff --git a/shared/simulator/src/simulation.rs b/shared/simulator/src/simulation.rs index dace5c0d..b7a5b091 100644 --- a/shared/simulator/src/simulation.rs +++ b/shared/simulator/src/simulation.rs @@ -70,6 +70,8 @@ impl Simulation { let mut scenario = scenario::load(scenario_name); log::debug!("seed {seed}"); + + // Create channel to communicate collision events let (contact_send, contact_recv) = crossbeam::channel::unbounded(); let mut sim = Box::new(Simulation { scenario: None, @@ -112,6 +114,7 @@ impl Simulation { collision::add_walls(&mut sim); + // Configure the simulation according to the scenario scenario.init(&mut sim, seed); sim.scenario = Some(scenario); @@ -204,6 +207,7 @@ impl Simulation { radar::tick(self); self.timing.radar += radar_timer.elapsed(); + // Transmit radio messages let radio_timer = Timer::new(); radio::tick(self); self.timing.radio += radio_timer.elapsed(); @@ -216,6 +220,12 @@ impl Simulation { .collect(); teams.sort_by_key(|(k, _)| *k); + // Ship tick processing happens in two steps. + // + // First within the team controller the user's ship tick function + // is called for each ship, which loads state updates into shared memory. + // The updates are read from shared memory and applied to the + // simulator. for (_, team_controller) in teams.iter() { team_controller.borrow_mut().tick(self); } @@ -223,6 +233,9 @@ impl Simulation { let ship_timer = Timer::new(); let handle_snapshot: Vec = self.ships.iter().cloned().collect(); + + // Second, some upkeep is performed for each ship, including things + // like moving reload timings forward. for handle in handle_snapshot { self.ship_mut(handle).tick(); if self.ships.contains(handle) { @@ -321,6 +334,9 @@ impl Simulation { s.finish() } + /// Generate a snapshot based on the current state of the simulation + /// + /// Collects the statuses of bullets and ships pub fn snapshot(&self, nonce: u32) -> Snapshot { let mut snapshot = Snapshot { nonce, @@ -382,6 +398,7 @@ impl Simulation { self.team_controllers.get_mut(&team).map(|x| x.clone()) } + /// Provide's every ship in the team with environment information pub fn update_environment(&mut self, team: i32, mut environment: BTreeMap) { environment.insert( "SCENARIO_NAME".to_string(), diff --git a/shared/simulator/src/vm/limiter.rs b/shared/simulator/src/vm/limiter.rs index ddcf9df1..4b62c759 100644 --- a/shared/simulator/src/vm/limiter.rs +++ b/shared/simulator/src/vm/limiter.rs @@ -1,6 +1,7 @@ // Based on https://github.com/scrtlabs/SecretNetwork/blob/621d3899babc4741ef1ba596152c097677d246db/cosmwasm/enclaves/shared/contract-engine/src/wasm3/gas.rs use walrus::{ir::*, FunctionBuilder, GlobalId, InitExpr, LocalFunction, ValType}; +/// Alters wasm to insert gas functions pub fn rewrite(wasm: &[u8]) -> Result, super::Error> { let mut module = match walrus::Module::from_buffer(wasm) { Ok(m) => m, diff --git a/shared/simulator/src/vm/mod.rs b/shared/simulator/src/vm/mod.rs index 9c2e60dd..48c8e286 100644 --- a/shared/simulator/src/vm/mod.rs +++ b/shared/simulator/src/vm/mod.rs @@ -184,6 +184,7 @@ impl TeamController { } fn tick_ship(&mut self, sim: &mut Simulation, handle: ShipHandle) -> Result<(), Error> { + // If ship code crashed if let Some(msg) = sim.ship(handle).data().crash_message.as_ref() { sim.emit_debug_text(handle, format!("Crashed: {}", msg.clone())); let mut rng = new_rng(sim.tick()); @@ -195,6 +196,8 @@ impl TeamController { let v = sim.ship(handle).body().linvel() + rot.transform_vector(&vector![speed, 0.0]); let offset = v * rng.gen_range(0.0..PHYSICS_TICK_LENGTH); + + // Display ship exploding animation sim.events.particles.push(Particle { position: p + offset, velocity: v, @@ -229,7 +232,7 @@ impl TeamController { slice.write_slice(&state.state).expect("system state write"); } - // Run user's ship tick + // Run user's ship tick function let result = vm.tick_ship.call(vm.store_mut().deref_mut(), &[]); if let Err(e) = result { // If gas has run out, throw an error @@ -252,6 +255,8 @@ impl TeamController { ) { let null_pos = vec.iter().position(|&x| x == 0).unwrap_or(vec.len()); let msg = String::from_utf8_lossy(&vec[0..null_pos]).to_string(); + + // Assume gas error if error message is missing if msg.is_empty() { return Err(Error { msg: "Ship exceeded maximum number of instructions".to_string(), @@ -273,6 +278,7 @@ impl TeamController { ); } + // Read shared memory after ship tick, and apply and actions to the simulation { let store = vm.store(); let memory_view = vm.memory.view(store.deref()); @@ -280,6 +286,7 @@ impl TeamController { let slice = ptr .slice(&memory_view, SystemState::Size as u32) .expect("system state read"); + slice .read_slice(&mut state.state) .expect("system state read"); @@ -337,6 +344,7 @@ impl TeamController { Ok(()) } + /// Writes `environment` to each ship's memory pub fn update_environment(&mut self, environment: &Environment) -> Result<(), Error> { self.environment = environment.clone(); for (_, ship_controller) in self.ship_controllers.iter_mut() { @@ -371,6 +379,8 @@ impl WasmVm { let module = match code { Code::Wasm(wasm) => { let wasm = wasm_submemory::rewrite(wasm, SUBMEMORY_SIZE)?; + + // Add gas tracking and functions let wasm = limiter::rewrite(&wasm)?; translate_error(Module::new(&store, wasm))? } @@ -404,6 +414,9 @@ impl WasmVm { .i32() .unwrap() as u32; + // The compiler service creates a file that includes this global `tick` function, which calls the + // user's `ship.tick()` function. You can find that file in `shared/ai/src/tick.rs`, + // and find the compile step in `shard/compiler/src/lib.rs` let tick_ship = translate_error(instance.exports.get_function("tick"))?.clone(); let reset_gas = translate_error(instance.exports.get_function("reset_gas"))?.clone(); let get_gas = translate_error(instance.exports.get_function("get_gas"))?.typed(&store)?; @@ -461,6 +474,9 @@ impl WasmVm { Some(src_slice.to_vec()) } + /// Write environment as a string to the VM's memory + /// + /// Throws error if environment size exceeds `MAX_ENVIRONMENT_SIZE` fn update_environment(&self, ptr: WasmPtr, environment: &Environment) -> Result<(), Error> { let environment_string = environment .iter() @@ -550,6 +566,7 @@ impl LocalSystemState { } } +/// Set ship memory based on the state of the ship in the simulator fn generate_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut LocalSystemState) { state.set( SystemState::Class, @@ -657,7 +674,10 @@ fn generate_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut L } } +/// Draws ship state from memory, applies it to the simulator, +/// and then resets memory to default values fn apply_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut LocalSystemState) { + // Set ship acceleration sim.ship_mut(handle).accelerate(Vec2::new( state.get(SystemState::AccelerateX), state.get(SystemState::AccelerateY), @@ -665,9 +685,11 @@ fn apply_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut Loca state.set(SystemState::AccelerateX, 0.0); state.set(SystemState::AccelerateY, 0.0); + // Set torque sim.ship_mut(handle).torque(state.get(SystemState::Torque)); state.set(SystemState::Torque, 0.0); + // Fire guns for (i, (aim, fire)) in [ (SystemState::Aim0, SystemState::Fire0), (SystemState::Aim1, SystemState::Fire1), @@ -684,6 +706,7 @@ fn apply_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut Loca } } + // Set radar positions for (idx, radar) in sim .ship_mut(handle) .data_mut() @@ -699,6 +722,7 @@ fn apply_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut Loca radar.set_ecm_mode(translate_ecm_mode(state.get(idxs.ecm_mode))); } + // Activate abilities let active_abilities = ActiveAbilities(state.get_u64(SystemState::ActivateAbility)); for &ability in oort_api::ABILITIES { let current = sim.ship(handle).is_ability_active(ability); @@ -712,11 +736,13 @@ fn apply_system_state(sim: &mut Simulation, handle: ShipHandle, state: &mut Loca } } + // Explode if state.get(SystemState::Explode) > 0.0 { sim.ship_mut(handle).explode(); state.set(SystemState::Explode, 0.0); } + // Set radio channels and send messages for (i, radio) in sim .ship_mut(handle) .data_mut()