diff --git a/Cargo.lock b/Cargo.lock index 76b09f2..7f01874 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7196,7 +7196,6 @@ version = "0.1.0" dependencies = [ "avian3d", "bevy", - "bevy-inspector-egui", "bevy-steamworks", "bevy-ui-gradients", "bevy_common_assets", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 19e5c39..58ab52a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,10 +1,24 @@ -use bevy::{prelude::*, utils::synccell::SyncCell}; +use avian3d::prelude::{ + Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderMassProperties, + CollisionEventsEnabled, CollisionLayers, Sensor, +}; +use bevy::{ + ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance, utils::synccell::SyncCell, +}; +use bevy_trenchbroom::geometry::Brushes; use lightyear::{ netcode::Key, prelude::{client::NetcodeConfig, input::native::InputMarker, *}, }; use nil::prelude::Mutex; -use shared::{GameState, control::ControlState, global_observer, player::Player}; +use shared::{ + GameState, + control::ControlState, + global_observer, + player::Player, + protocol::{DespawnTbMapEntity, TbMapEntityId, TbMapEntityMapping}, + tb_entities::{Platform, PlatformTarget}, +}; use std::{ env::current_exe, io::{BufRead, BufReader}, @@ -23,12 +37,14 @@ pub fn plugin(app: &mut App) { parse_local_server_stdout.run_if(resource_exists::), ); app.add_systems(Last, close_server_processes); + app.add_systems(FixedUpdate, despawn_absent_map_entities); global_observer!(app, on_connecting); global_observer!(app, on_connection_failed); global_observer!(app, on_connection_succeeded); global_observer!(app, temp_give_player_marker); global_observer!(app, connect_on_local_server_started); + global_observer!(app, received_remote_map_entity); } fn close_server_processes(mut app_exit: EventReader) { @@ -190,3 +206,74 @@ fn temp_give_player_marker(trigger: Trigger, mut commands: Comman .entity(trigger.target()) .insert(InputMarker::::default()); } + +fn received_remote_map_entity( + trigger: Trigger, + world: &mut World, + mut child_buffer: Local>, +) { + let serverside = trigger.target(); + + if world.get::(serverside).is_none() { + return; + } + + let id = *world.get::(serverside).unwrap(); + + let Some(clientside) = world.resource_mut::().0.remove(&id.id) else { + warn!("received unknown MapEntity ID `{id:?}`"); + return; + }; + + // cannot just use `take` directly with a bundle because then any missing component would cause + // the entire bundle to fail + move_component::(world, clientside, serverside); + move_component::<( + Collider, + ColliderAabb, + ColliderDensity, + ColliderMarker, + ColliderMassProperties, + CollisionLayers, + )>(world, clientside, serverside); + move_component::(world, clientside, serverside); + move_component::(world, clientside, serverside); + move_component::(world, clientside, serverside); + move_component::(world, clientside, serverside); + move_component::(world, clientside, serverside); + move_component::(world, clientside, serverside); + + if let Some(children) = world.get::(clientside) { + child_buffer.extend(children.iter()); + for child in child_buffer.drain(..) { + world.entity_mut(child).insert(ChildOf(serverside)); + } + } + + world.entity_mut(clientside).despawn(); +} + +fn move_component(world: &mut World, from: Entity, to: Entity) { + let comp = world.entity_mut(from).take::(); + if let Some(comp) = comp { + world.entity_mut(to).insert(comp); + } +} + +fn despawn_absent_map_entities( + mut commands: Commands, + mut messages: Query<&mut MessageReceiver>, + mut map: ResMut, +) { + for mut recv in messages.iter_mut() { + for msg in recv.receive() { + // the server may double-send DespawnTbMapEntity for a given ID, so ignore it if the entity + // was already despawned. + let Some(entity) = map.0.remove(&msg.0) else { + continue; + }; + + commands.entity(entity).despawn(); + } + } +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 227beeb..281aa36 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -5,12 +5,11 @@ edition = "2024" [features] default = ["shared/server"] -dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"] +dbg = ["avian3d/debug-plugin", "shared/dbg"] [dependencies] avian3d = { workspace = true } bevy = { workspace = true, default-features = false } -bevy-inspector-egui = { workspace = true, optional = true } bevy-steamworks = { workspace = true } bevy-ui-gradients = { workspace = true } bevy_common_assets = { workspace = true } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index c6eb985..1038f8f 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,4 +1,3 @@ -use crate::utils::{auto_rotate, explosions}; use avian3d::prelude::*; use bevy::{ app::plugin_group, @@ -11,14 +10,13 @@ use bevy_common_assets::ron::RonAssetPlugin; use bevy_sprite3d::Sprite3dPlugin; use bevy_trenchbroom::prelude::*; use bevy_ui_gradients::UiGradientsPlugin; -use heads_database::HeadDatabaseAsset; use lightyear::prelude::server::ServerPlugins; -use shared::*; +use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset}; use std::time::Duration; -use utils::{billboards, sprite_3d_animation, squish_animation, trail}; mod config; mod server; +mod tb_entities; plugin_group! { pub struct DefaultPlugins { @@ -105,10 +103,6 @@ fn main() { #[cfg(feature = "dbg")] { - app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin { - enable_multipass_for_primary_context: true, - }); - app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::new()); app.add_plugins(PhysicsDebugPlugin::default()); // app.add_plugins(bevy::pbr::wireframe::WireframePlugin) @@ -118,43 +112,45 @@ fn main() { // }); } - app.add_plugins(ai::plugin); - app.add_plugins(animation::plugin); - app.add_plugins(character::plugin); - app.add_plugins(cash::plugin); + app.add_plugins(shared::abilities::plugin); + app.add_plugins(shared::ai::plugin); + app.add_plugins(shared::aim::plugin); + app.add_plugins(shared::animation::plugin); + app.add_plugins(shared::backpack::plugin); + app.add_plugins(shared::camera::plugin); + app.add_plugins(shared::cash::plugin); + app.add_plugins(shared::cash_heal::plugin); + app.add_plugins(shared::character::plugin); + app.add_plugins(shared::control::plugin); + app.add_plugins(shared::cutscene::plugin); + app.add_plugins(shared::gates::plugin); + app.add_plugins(shared::head_drop::plugin); + app.add_plugins(shared::heads::plugin); + app.add_plugins(shared::heal_effect::plugin); + app.add_plugins(shared::hitpoints::plugin); + app.add_plugins(shared::keys::plugin); + app.add_plugins(shared::loading_assets::LoadingPlugin); + app.add_plugins(shared::loading_map::plugin); + app.add_plugins(shared::movables::plugin); + app.add_plugins(shared::npc::plugin); + app.add_plugins(shared::platforms::plugin); + app.add_plugins(shared::player::plugin); + app.add_plugins(shared::protocol::plugin); + app.add_plugins(shared::sounds::plugin); + app.add_plugins(shared::steam::plugin); + app.add_plugins(shared::tb_entities::plugin); + app.add_plugins(shared::utils::auto_rotate::plugin); + app.add_plugins(shared::utils::billboards::plugin); + app.add_plugins(shared::utils::explosions::plugin); + app.add_plugins(shared::utils::sprite_3d_animation::plugin); + app.add_plugins(shared::utils::squish_animation::plugin); + app.add_plugins(shared::utils::trail::plugin); + app.add_plugins(shared::utils::plugin); + app.add_plugins(shared::water::plugin); + app.add_plugins(config::plugin); - app.add_plugins(player::plugin); - app.add_plugins(gates::plugin); - app.add_plugins(platforms::plugin); - app.add_plugins(movables::plugin); - app.add_plugins(billboards::plugin); - app.add_plugins(aim::plugin); - app.add_plugins(protocol::plugin); app.add_plugins(server::plugin); - app.add_plugins(npc::plugin); - app.add_plugins(keys::plugin); - app.add_plugins(squish_animation::plugin); - app.add_plugins(cutscene::plugin); - app.add_plugins(control::plugin); - app.add_plugins(sounds::plugin); - app.add_plugins(camera::plugin); - app.add_plugins(backpack::plugin); - app.add_plugins(loading_assets::LoadingPlugin); - app.add_plugins(loading_map::plugin); - app.add_plugins(sprite_3d_animation::plugin); - app.add_plugins(abilities::plugin); - app.add_plugins(heads::plugin); - app.add_plugins(hitpoints::plugin); - app.add_plugins(cash_heal::plugin); - app.add_plugins(utils::plugin); - app.add_plugins(water::plugin); - app.add_plugins(head_drop::plugin); - app.add_plugins(trail::plugin); - app.add_plugins(auto_rotate::plugin); - app.add_plugins(heal_effect::plugin); app.add_plugins(tb_entities::plugin); - app.add_plugins(explosions::plugin); - app.add_plugins(steam::plugin); app.init_state::(); diff --git a/crates/server/src/tb_entities.rs b/crates/server/src/tb_entities.rs new file mode 100644 index 0000000..1e4dbe2 --- /dev/null +++ b/crates/server/src/tb_entities.rs @@ -0,0 +1,43 @@ +use bevy::prelude::*; +use lightyear::prelude::{ActionsChannel, Connected, MessageSender}; +use shared::{ + GameState, global_observer, + protocol::{DespawnTbMapEntity, TbMapEntityId}, +}; + +pub fn plugin(app: &mut App) { + app.register_type::(); + + app.init_resource::(); + + app.add_systems( + OnEnter(GameState::MapLoading), + |mut cache: ResMut| cache.0.clear(), + ); + + global_observer!(app, add_despawned_entities_to_cache); + global_observer!(app, send_new_client_despawned_cache); +} + +fn add_despawned_entities_to_cache( + trigger: Trigger, + id: Query<&TbMapEntityId>, + mut cache: ResMut, +) { + cache.0.push(id.get(trigger.target()).unwrap().id); +} + +#[derive(Default, Resource, Reflect)] +#[reflect(Resource)] +pub struct DespawnedTbEntityCache(pub Vec); + +fn send_new_client_despawned_cache( + trigger: Trigger, + cache: Res, + mut send: Query<&mut MessageSender>, +) { + let mut send = send.get_mut(trigger.target()).unwrap(); + for &id in cache.0.iter() { + send.send::(DespawnTbMapEntity(id)); + } +} diff --git a/crates/shared/src/aim/mod.rs b/crates/shared/src/aim/mod.rs index b012714..5b86102 100644 --- a/crates/shared/src/aim/mod.rs +++ b/crates/shared/src/aim/mod.rs @@ -79,14 +79,13 @@ fn update_player_aim( spatial_query: SpatialQuery, ) { for (player, state, mut aim_target, children) in player_aim.iter_mut() { - assert_eq!( - children.len(), - 1, - "expected player to have one direct child" - ); + let player_rot_child = children + .iter() + .find(|&child| player_rot.contains(child)) + .expect("expected child with PlayerBodyMesh"); let (player_pos, player_forward) = player_rot - .get(*children.first().unwrap()) + .get(player_rot_child) .map(|(t, global)| (global.translation(), t.forward())) .unwrap(); diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index c4e09f4..0f6ae9f 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -47,7 +47,7 @@ pub struct DebugVisuals { pub cam_follow: bool, } -#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)] +#[derive(States, Default, Clone, Copy, Eq, PartialEq, Debug, Hash)] pub enum GameState { #[default] AssetLoading, diff --git a/crates/shared/src/loading_map.rs b/crates/shared/src/loading_map.rs index 61ae37a..d64e2df 100644 --- a/crates/shared/src/loading_map.rs +++ b/crates/shared/src/loading_map.rs @@ -1,7 +1,11 @@ +#[cfg(feature = "server")] +use crate::protocol::TbMapEntityId; use crate::{GameState, physics_layers::GameLayer}; use avian3d::prelude::*; use bevy::prelude::*; use bevy_trenchbroom::physics::SceneCollidersReady; +#[cfg(feature = "server")] +use lightyear::prelude::{DisableReplicateHierarchy, NetworkTarget, Replicate}; pub fn plugin(app: &mut App) { app.add_systems(OnEnter(GameState::MapLoading), setup_scene); @@ -12,14 +16,33 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { .spawn(( CollisionLayers::new(LayerMask(GameLayer::Level.to_bits()), LayerMask::ALL), SceneRoot(asset_server.load("maps/map1.map#Scene")), + #[cfg(feature = "server")] + Replicate::to_clients(NetworkTarget::All), + #[cfg(feature = "server")] + DisableReplicateHierarchy, )) .observe( - |_t: Trigger, + |t: Trigger, + children: Query<&Children>, + #[cfg(feature = "server")] map_entities: Query<&TbMapEntityId>, + mut commands: Commands, #[cfg(any(feature = "client", feature = "server"))] mut next_game_state: ResMut< NextState, >| { info!("map loaded"); + for child in children.get(t.target()).unwrap() { + commands.entity(*child).remove::(); + + #[cfg(feature = "server")] + if map_entities.contains(*child) { + commands.entity(*child).insert(( + Replicate::to_clients(NetworkTarget::All), + DisableReplicateHierarchy, + )); + } + } + assert!(cfg!(feature = "client") ^ cfg!(feature = "server")); #[cfg(feature = "client")] diff --git a/crates/shared/src/platforms.rs b/crates/shared/src/platforms.rs index 4f30f79..0747df4 100644 --- a/crates/shared/src/platforms.rs +++ b/crates/shared/src/platforms.rs @@ -2,12 +2,15 @@ use crate::{ GameState, tb_entities::{Platform, PlatformTarget}, }; -use bevy::{math::ops::sin, prelude::*}; +#[cfg(feature = "server")] +use bevy::math::ops::sin; +use bevy::prelude::*; use bevy_trenchbroom::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Component, Reflect, Default, Debug)] +#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)] #[reflect(Component)] -struct ActivePlatform { +pub struct ActivePlatform { pub start: Vec3, pub target: Vec3, } @@ -15,6 +18,7 @@ struct ActivePlatform { pub fn plugin(app: &mut App) { app.register_type::(); app.add_systems(OnEnter(GameState::Playing), init); + #[cfg(feature = "server")] app.add_systems( FixedUpdate, move_active.run_if(in_state(GameState::Playing)), @@ -47,6 +51,7 @@ fn init( } } +#[cfg(feature = "server")] fn move_active(time: Res