diff --git a/Cargo.lock b/Cargo.lock index 08f13d4..69fd6b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2911,6 +2911,7 @@ dependencies = [ "byteorder", "include-flate", "once_cell", + "radsort", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 2ce90b8..e4c80f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ serde_json = "1.0.96" walkdir = "2.3.3" thiserror = "1.0.50" ahash = "0.8.6" +radsort = "0.1.0" [dependencies.bevy] version = "0.12.0" diff --git a/src/assets.rs b/src/assets.rs index 994529c..3747a37 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -27,10 +27,26 @@ impl Plugin for AssetsLoaderPlugin { } } -fn load_assets(mut commands: Commands, assets: Res) { +fn load_assets(mut commands: Commands, assets: Res, mut meshes: ResMut>) { commands.insert_resource(BoostPickupGlows { small: assets.load("Pickup_Boost/StaticMesh3/BoostPad_Small_02_SM.pskx"), + small_hitbox: meshes.add( + shape::Cylinder { + radius: 144. / 2., + height: 165., + ..default() + } + .into(), + ), large: assets.load("Pickup_Boost/StaticMesh3/BoostPad_Large_Glow.pskx"), + large_hitbox: meshes.add( + shape::Cylinder { + radius: 208. / 2., + height: 168., + ..default() + } + .into(), + ), }); commands.insert_resource(BallAssets { @@ -52,7 +68,9 @@ pub struct BallAssets { #[derive(Resource)] pub struct BoostPickupGlows { pub small: Handle, + pub small_hitbox: Handle, pub large: Handle, + pub large_hitbox: Handle, } const BLOCK_MESHES: [&str; 8] = [ diff --git a/src/gui.rs b/src/gui.rs index 977dfde..75e4ea2 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,6 +1,7 @@ use crate::{ bytes::ToBytes, camera::{DaylightOffset, PrimaryCamera, Sun}, + morton::Morton, rocketsim::GameState, udp::Connection, }; @@ -73,6 +74,8 @@ impl Plugin for DebugOverlayPlugin { .insert_resource(UserBallState::default()) .insert_resource(EnableCarInfo::default()) .insert_resource(UserCarStates::default()) + .insert_resource(EnablePadInfo::default()) + .insert_resource(UserPadStates::default()) .insert_resource(PickingPluginsSettings { enable: true, enable_input: false, @@ -81,6 +84,7 @@ impl Plugin for DebugOverlayPlugin { }) .add_event::() .add_event::() + .add_event::() .add_systems( Update, ( @@ -97,8 +101,10 @@ impl Plugin for DebugOverlayPlugin { update_shadows, update_ball_info.run_if(resource_equals(EnableBallInfo(true))), update_car_info.run_if(|enable_menu: Res| !enable_menu.0.is_empty()), + update_boost_pad_info.run_if(|enable_menu: Res| !enable_menu.0.is_empty()), set_user_ball_state.run_if(on_event::()), set_user_car_state.run_if(on_event::()), + set_user_pad_state.run_if(on_event::()), ) .run_if(resource_equals(MenuFocused(true))), update_camera_state, @@ -141,6 +147,143 @@ impl EnableCarInfo { } } +#[derive(Resource, PartialEq, Eq)] +pub struct EnablePadInfo(AHashMap); + +impl Default for EnablePadInfo { + #[inline] + fn default() -> Self { + Self(AHashMap::with_capacity(48)) + } +} + +impl EnablePadInfo { + pub fn toggle(&mut self, id: u64) { + if let Some(enabled) = self.0.get_mut(&id) { + *enabled = !*enabled; + } else { + self.0.insert(id, true); + } + } +} + +#[derive(Default)] +struct UserPadState { + pub is_active: usize, + pub timer: String, +} + +#[derive(Resource)] +pub struct UserPadStates(AHashMap); + +impl Default for UserPadStates { + #[inline] + fn default() -> Self { + Self(AHashMap::with_capacity(48)) + } +} + +impl UserPadStates { + pub fn clear(&mut self) { + self.0.clear(); + } +} + +#[derive(Event)] +struct UserSetPadState(u64); + +fn update_boost_pad_info( + mut contexts: EguiContexts, + game_state: Res, + mut enable_menu: ResMut, + mut set_user_state: EventWriter, + mut user_pads: ResMut, +) { + const USER_BOOL_NAMES: [&str; 3] = ["", "True", "False"]; + + let ctx = contexts.ctx_mut(); + + let morton_generator = Morton::default(); + for (i, pad) in game_state.pads.iter().enumerate() { + let code = morton_generator.get_code(pad.position); + let Some(entry) = enable_menu.0.get_mut(&code) else { + continue; + }; + + if !*entry { + continue; + } + + let user_pad = user_pads.0.entry(code).or_default(); + + let title = format!("{}Boost pad {}", if pad.is_big { "(Large) " } else { "" }, i); + egui::Window::new(title).open(entry).show(ctx, |ui| { + ui.label(format!( + "Position: [{:.0}, {:.0}, {:.0}]", + pad.position.x, pad.position.y, pad.position.z + )); + + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label(format!("Is active: {}", pad.state.is_active)); + egui::ComboBox::from_id_source("Is active").width(60.).show_index( + ui, + &mut user_pad.is_active, + USER_BOOL_NAMES.len(), + |i| USER_BOOL_NAMES[i], + ); + }); + ui.vertical(|ui| { + ui.label(format!("Timer: {:.1}", pad.state.cooldown)); + ui.add(egui::TextEdit::singleline(&mut user_pad.timer).desired_width(60.)); + }); + }); + + if ui + .button(" Set all ") + .on_hover_text("Set all (defined) boost pad properties") + .clicked() + { + set_user_state.send(UserSetPadState(code)); + } + }); + } +} + +fn set_user_pad_state( + mut events: EventReader, + mut game_state: ResMut, + user_pads: Res, + socket: Res, +) { + let morton_generator = Morton::default(); + let mut sorted_pads = game_state + .pads + .iter() + .enumerate() + .map(|(i, pad)| (i, morton_generator.get_code(pad.position))) + .collect::>(); + radsort::sort_by_key(&mut sorted_pads, |(_, code)| *code); + + for event in events.read() { + let Some(user_pad) = user_pads.0.get(&event.0) else { + continue; + }; + + let Ok(index) = sorted_pads.binary_search_by_key(&event.0, |(_, code)| *code) else { + continue; + }; + let pad = &mut game_state.pads[sorted_pads[index].0]; + + set_bool_from_usize(&mut pad.state.is_active, user_pad.is_active); + set_f32_from_str(&mut pad.state.cooldown, &user_pad.timer); + } + + if let Err(e) = socket.0.send(&game_state.to_bytes()) { + error!("Failed to send boost pad information: {e}"); + } +} + #[derive(Default)] struct UserCarState { pub pos: [String; 3], @@ -154,7 +297,7 @@ struct UserCarState { } #[derive(Resource)] -struct UserCarStates(AHashMap); +pub struct UserCarStates(AHashMap); impl Default for UserCarStates { #[inline] @@ -163,6 +306,16 @@ impl Default for UserCarStates { } } +impl UserCarStates { + pub fn clear(&mut self) { + self.0.clear(); + } + + pub fn remove(&mut self, id: u32) { + self.0.remove(&id); + } +} + enum SetCarStateAmount { Pos, Vel, @@ -199,14 +352,14 @@ fn set_user_car_state( set_vec3_from_arr_str(&mut game_state.cars[car_index].state.ang_vel, &user_car.ang_vel) } SetCarStateAmount::Jumped => { - set_bool_from_usize(&mut game_state.cars[car_index].state.has_jumped, user_car.has_jumped) + set_half_bool_from_usize(&mut game_state.cars[car_index].state.has_jumped, user_car.has_jumped) } - SetCarStateAmount::DoubleJumped => set_bool_from_usize( + SetCarStateAmount::DoubleJumped => set_half_bool_from_usize( &mut game_state.cars[car_index].state.has_double_jumped, user_car.has_double_jumped, ), SetCarStateAmount::Flipped => { - set_bool_from_usize(&mut game_state.cars[car_index].state.has_flipped, user_car.has_flipped) + set_half_bool_from_usize(&mut game_state.cars[car_index].state.has_flipped, user_car.has_flipped) } SetCarStateAmount::Boost => set_f32_from_str(&mut game_state.cars[car_index].state.boost, &user_car.boost), SetCarStateAmount::DemoRespawnTimer => set_f32_from_str( @@ -217,12 +370,12 @@ fn set_user_car_state( set_vec3_from_arr_str(&mut game_state.cars[car_index].state.pos, &user_car.pos); set_vec3_from_arr_str(&mut game_state.cars[car_index].state.vel, &user_car.vel); set_vec3_from_arr_str(&mut game_state.cars[car_index].state.ang_vel, &user_car.ang_vel); - set_bool_from_usize(&mut game_state.cars[car_index].state.has_jumped, user_car.has_jumped); - set_bool_from_usize( + set_half_bool_from_usize(&mut game_state.cars[car_index].state.has_jumped, user_car.has_jumped); + set_half_bool_from_usize( &mut game_state.cars[car_index].state.has_double_jumped, user_car.has_double_jumped, ); - set_bool_from_usize(&mut game_state.cars[car_index].state.has_flipped, user_car.has_flipped); + set_half_bool_from_usize(&mut game_state.cars[car_index].state.has_flipped, user_car.has_flipped); set_f32_from_str(&mut game_state.cars[car_index].state.boost, &user_car.boost); set_f32_from_str( &mut game_state.cars[car_index].state.demo_respawn_timer, @@ -233,7 +386,7 @@ fn set_user_car_state( } if let Err(e) = socket.0.send(&game_state.to_bytes()) { - error!("Failed to send ball position: {e}"); + error!("Failed to send car information: {e}"); } } @@ -361,7 +514,7 @@ fn update_car_info( ui.label(""); if ui - .button("Set all") + .button(" Set all ") .on_hover_text("Set all (defined) car properties") .clicked() { @@ -495,7 +648,7 @@ fn update_ball_info( } }); if ui - .button("Set all") + .button(" Set all ") .on_hover_text("Set all (defined) ball properties") .clicked() { @@ -516,12 +669,18 @@ fn set_vec3_from_arr_str(vec: &mut Vec3A, arr: &[String; 3]) { set_f32_from_str(&mut vec.z, &arr[2]); } -fn set_bool_from_usize(b: &mut bool, i: usize) { +fn set_half_bool_from_usize(b: &mut bool, i: usize) { if i != 0 { *b = false; } } +fn set_bool_from_usize(b: &mut bool, i: usize) { + if i != 0 { + *b = i == 1; + } +} + fn set_user_ball_state( mut events: EventReader, mut game_state: ResMut, @@ -542,7 +701,7 @@ fn set_user_ball_state( } if let Err(e) = socket.0.send(&game_state.to_bytes()) { - error!("Failed to send ball position: {e}"); + error!("Failed to send ball information: {e}"); } } diff --git a/src/main.rs b/src/main.rs index 508ed0b..a539678 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod bytes; mod camera; mod gui; mod mesh; +mod morton; mod rocketsim; mod spectator; mod udp; diff --git a/src/mesh.rs b/src/mesh.rs index 23bf521..bcda18e 100644 --- a/src/mesh.rs +++ b/src/mesh.rs @@ -2,9 +2,9 @@ use crate::{ assets::*, bytes::ToBytes, camera::{HighlightedEntity, PrimaryCamera}, - gui::{EnableBallInfo, EnableCarInfo}, + gui::{EnableBallInfo, EnableCarInfo, EnablePadInfo, UserCarStates, UserPadStates}, rocketsim::{GameMode, GameState}, - udp::{Ball, Car, Connection, ToBevyVec, ToBevyVecFlat}, + udp::{Ball, BoostPadI, Car, Connection, ToBevyVec, ToBevyVecFlat}, LoadState, }; use bevy::{ @@ -40,6 +40,7 @@ impl Plugin for FieldLoaderPlugin { .add_event::() .add_event::() .add_event::() + .add_event::() .add_systems( Update, ( @@ -48,6 +49,7 @@ impl Plugin for FieldLoaderPlugin { load_extra_field.run_if(in_state(LoadState::FieldExtra)), handle_ball_clicked.run_if(on_event::()), handle_car_clicked.run_if(on_event::()), + handle_boost_pad_clicked.run_if(on_event::()), ( advance_stopwatch, ( @@ -230,6 +232,31 @@ fn handle_ball_clicked(mut events: EventReader, mut enable_ball_inf enable_ball_info.toggle(); } +#[derive(Event)] +pub struct BoostPadClicked(PointerButton, Entity); + +impl From>> for BoostPadClicked { + fn from(event: ListenerInput>) -> Self { + Self(event.button, event.target) + } +} + +fn handle_boost_pad_clicked( + mut events: EventReader, + mut enable_boost_pad_info: ResMut, + boost_pads: Query<&BoostPadI>, +) { + for event in events.read() { + if event.0 != PointerButton::Secondary { + continue; + } + + if let Ok(boost_pad) = boost_pads.get(event.1) { + enable_boost_pad_info.toggle(boost_pad.id()); + } + } +} + fn load_extra_field( mut commands: Commands, mut materials: ResMut>, @@ -397,10 +424,16 @@ fn despawn_old_field( mut commands: Commands, mut state: ResMut>, static_field_entities: Query>, + mut user_pads: ResMut, + mut user_cars: ResMut, ) { + user_pads.clear(); + user_cars.clear(); + static_field_entities.for_each(|entity| { commands.entity(entity).despawn(); }); + state.set(LoadState::Field); } diff --git a/src/morton.rs b/src/morton.rs new file mode 100644 index 0000000..ba332f2 --- /dev/null +++ b/src/morton.rs @@ -0,0 +1,53 @@ +use bevy::math::Vec3A; + +#[derive(Debug)] +pub struct Morton { + offset: Vec3A, + scale: Vec3A, +} + +impl Default for Morton { + fn default() -> Self { + Self::new(Vec3A::splat(-10_000.), Vec3A::splat(10_000.)) + } +} + +impl Morton { + #[must_use] + fn new(min: Vec3A, max: Vec3A) -> Self { + // 2 ^ 20 - 1 = 1048575 + let scale = 1_048_575. / (max - min); + + Self { offset: min, scale } + } + + /// Prepare a 21-bit unsigned int for inverweaving. + #[must_use] + pub fn expand3(a: u32) -> u64 { + let mut x = u64::from(a) & 0x001f_ffff; // we only look at the first 21 bits + + x = (x | x << 32) & 0x001f_0000_0000_ffff; + x = (x | x << 16) & 0x001f_0000_ff00_00ff; + x = (x | x << 8) & 0x100f_00f0_0f00_f00f; + x = (x | x << 4) & 0x10c3_0c30_c30c_30c3; + x = (x | x << 2) & 0x1249_2492_4924_9249; + + x + } + + /// Get an AABB's morton code. + #[must_use] + pub fn get_code(&self, point: Vec3A) -> u64 { + let u = (point - self.offset) * self.scale; + + debug_assert!(u.x >= 0.); + debug_assert!(u.y >= 0.); + debug_assert!(u.z >= 0.); + + // These should actually be 21 bits, but there's no u21 type and the final type is u64 (21 bits * 3 = 63 bits) + // Allowing these warnings is ok because: + // We have offset the values so they're all greater than 0 + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + (Self::expand3(u.x as u32) | Self::expand3(u.y as u32) << 1 | Self::expand3(u.z as u32) << 2) + } +} diff --git a/src/udp.rs b/src/udp.rs index cb39954..794376f 100644 --- a/src/udp.rs +++ b/src/udp.rs @@ -2,8 +2,9 @@ use crate::{ assets::{get_material, get_mesh_info, BoostPickupGlows}, bytes::{FromBytes, ToBytes}, camera::{BoostAmount, HighlightedEntity, PrimaryCamera, TimeDisplay, BOOST_INDICATOR_FONT_SIZE, BOOST_INDICATOR_POS}, - gui::{BallCam, ShowTime, UiScale}, - mesh::{CarClicked, ChangeCarPos, LargeBoostPadLocRots}, + gui::{BallCam, ShowTime, UiScale, UserCarStates}, + mesh::{BoostPadClicked, CarClicked, ChangeCarPos, LargeBoostPadLocRots}, + morton::Morton, rocketsim::{CarInfo, GameMode, GameState, Team}, LoadState, ServerPort, }; @@ -22,7 +23,14 @@ use std::{cmp::Ordering, f32::consts::PI, fs, net::UdpSocket, time::Duration}; use crate::camera::EntityName; #[derive(Component)] -struct BoostPadI; +pub struct BoostPadI(u64); + +impl BoostPadI { + #[inline] + pub const fn id(&self) -> u64 { + self.0 + } +} #[derive(Component)] pub struct Ball; @@ -189,7 +197,7 @@ fn spawn_car( .spawn(( Car(car_info.id), PbrBundle { - mesh: meshes.add(shape::Box::new(hitbox.x * 2., hitbox.y * 2., hitbox.z * 2.).into()), + mesh: meshes.add(shape::Box::new(hitbox.x * 2., hitbox.y * 3., hitbox.z * 2.).into()), material: materials.add(Color::NONE.into()), ..default() }, @@ -217,7 +225,7 @@ fn spawn_car( }); parent.spawn(( - MaterialMeshBundle { + PbrBundle { mesh: meshes.add(Mesh::from(shape::Cylinder { height: CAR_BOOST_LENGTH, radius: 10., @@ -412,11 +420,13 @@ fn update_car( mut meshes: ResMut>, mut materials: ResMut>, mut last_boost_states: Local>, + mut user_cars: ResMut, ) { match cars.iter().count().cmp(&state.cars.len()) { Ordering::Greater => { for (entity, car) in &car_entities { if !state.cars.iter().any(|car_info| car.0 == car_info.id) { + user_cars.remove(car.0); commands.entity(entity).despawn_recursive(); } } @@ -522,12 +532,14 @@ fn update_car( fn update_pads( state: Res, pads: Query<(Entity, &BoostPadI)>, - query: Query<&Handle, With>, + query: Query<(&Handle, &BoostPadI)>, pad_glows: Res, large_boost_pad_loc_rots: Res, mut materials: ResMut>, mut commands: Commands, ) { + let morton_generator = Morton::default(); + if pads.iter().count() != state.pads.len() && !large_boost_pad_loc_rots.rots.is_empty() { // The number of pads shouldn't change often // There's also not an easy way to determine @@ -536,11 +548,13 @@ fn update_pads( for (entity, _) in pads.iter() { commands.entity(entity).despawn_recursive(); } + let hitbox_material = materials.add(Color::NONE.into()); - for pad in &*state.pads { + for pad in state.pads.iter() { + let code = morton_generator.get_code(pad.position); let mut transform = Transform::from_translation(pad.position.to_bevy() - Vec3::Y * 70.); - let mesh = if pad.is_big { + let (visual_mesh, hitbox) = if pad.is_big { let rotation = large_boost_pad_loc_rots .locs .iter() @@ -554,7 +568,7 @@ fn update_pads( transform.translation.y += 5.2; } - pad_glows.large.clone() + (pad_glows.large.clone(), pad_glows.large_hitbox.clone()) } else { if state.game_mode == GameMode::Soccar { if transform.translation.z > 10. { @@ -602,41 +616,61 @@ fn update_pads( transform.translation.y += 5.7; } - pad_glows.small.clone() + (pad_glows.small.clone(), pad_glows.small_hitbox.clone()) }; - commands.spawn(( - BoostPadI, - PbrBundle { - mesh, - transform, - material: materials.add(StandardMaterial { - base_color: Color::rgba(0.9, 0.9, 0.1, 0.6), - alpha_mode: AlphaMode::Add, - double_sided: true, - cull_mode: None, + commands + .spawn(( + BoostPadI(code), + PbrBundle { + mesh: visual_mesh, + transform, + material: materials.add(StandardMaterial { + base_color: Color::rgba(0.9, 0.9, 0.1, 0.6), + alpha_mode: AlphaMode::Add, + double_sided: true, + cull_mode: None, + ..default() + }), ..default() - }), - ..default() - }, - #[cfg(debug_assertions)] - EntityName::from("generic_boost_pad"), - RaycastPickable, - On::>::target_insert(HighlightedEntity), - On::>::target_remove::(), - NotShadowCaster, - NotShadowReceiver, - )); + }, + #[cfg(debug_assertions)] + EntityName::from("generic_boost_pad"), + RaycastPickable, + On::>::target_insert(HighlightedEntity), + On::>::target_remove::(), + On::>::send_event::(), + NotShadowCaster, + NotShadowReceiver, + )) + .with_children(|parent| { + parent.spawn(PbrBundle { + mesh: hitbox, + material: hitbox_material.clone(), + ..default() + }); + }); } } - for (pad, handle) in state.pads.iter().zip(query.iter()) { - materials.get_mut(handle).unwrap().base_color.set_a(if pad.state.is_active { + let mut sorted_pads = state + .pads + .iter() + .enumerate() + .map(|(i, pad)| (i, morton_generator.get_code(pad.position))) + .collect::>(); + radsort::sort_by_key(&mut sorted_pads, |(_, code)| *code); + + for (handle, id) in query.iter() { + let index = sorted_pads.binary_search_by_key(&id.id(), |(_, code)| *code).unwrap(); + let alpha = if state.pads[sorted_pads[index].0].state.is_active { 0.6 } else { // make the glow on inactive pads dissapear 0.0 - }); + }; + + materials.get_mut(handle).unwrap().base_color.set_a(alpha); } }