use crate::{ GameState, config::NetConfig, protocol::{ ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity, }, tb_entities::{Movable, Platform, PlatformTarget}, }; use avian3d::prelude::{ Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderOf, ColliderTransform, CollisionEventsEnabled, CollisionLayers, Sensor, }; use bevy::{ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance}; use bevy_replicon::{ client::{ClientSystems, confirm_history::ConfirmHistory}, prelude::{ClientState, ClientTriggerExt, RepliconChannels}, }; use bevy_replicon_renet::{ RenetChannelsExt, renet::{ConnectionConfig, RenetClient}, }; use bevy_steamworks::Client; use bevy_trenchbroom::geometry::Brushes; pub mod aim; pub mod audio; pub mod backpack; pub mod control; pub mod debug; pub mod enemy; pub mod heal_effect; pub mod player; mod settings; pub mod setup; pub mod steam; pub mod ui; pub fn plugin(app: &mut App) { app.add_plugins(( aim::plugin, audio::plugin, backpack::plugin, control::plugin, debug::plugin, enemy::plugin, heal_effect::plugin, player::plugin, setup::plugin, steam::plugin, ui::plugin, settings::plugin, )); app.add_systems( OnEnter(GameState::Connecting), connect_to_server.run_if(|config: Res| config.is_client()), ); app.add_systems(Update, despawn_absent_map_entities); app.add_systems( PreUpdate, (migrate_remote_entities, ApplyDeferred) .chain() .after(ClientSystems::Receive), ); app.add_systems(OnEnter(ClientState::Connected), on_connected_state); app.add_systems(OnExit(ClientState::Connected), on_disconnect); } // // Client logic // fn on_connected_state(mut commands: Commands, mut game_state: ResMut>) { info!("sent entered playing signal"); commands.client_trigger(ClientEnteredPlaying); game_state.set(GameState::Playing); } fn on_disconnect() { info!("disconnected from the server"); } // // Renet // fn connect_to_server( mut commands: Commands, config: Res, channels: Res, steam_client: Option>, ) -> Result { let server_channels_config = channels.server_configs(); let client_channels_config = channels.client_configs(); let client = RenetClient::new(ConnectionConfig { server_channels_config, client_channels_config, ..Default::default() }); commands.insert_resource(client); if let NetConfig::SteamClient(host_steam_id) = &*config { let Some(steam_client) = steam_client else { return Err("Steam client not found".into()); }; info!("connecting to steam host: {host_steam_id:?}"); let transport = bevy_replicon_renet::steam::SteamClientTransport::new( (**steam_client).clone(), host_steam_id, )?; commands.insert_resource(transport); } else if let NetConfig::NetcodeClient(host_addr) = &*config { use std::time::SystemTime; info!("connecting to netcode host: {host_addr:?}"); let current_time = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let client_id = current_time.as_millis() as u64; let socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))?; let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure { client_id, protocol_id: 0, server_addr: *host_addr, user_data: None, }; let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new( current_time, authentication, socket, )?; commands.insert_resource(transport); } Ok(()) } #[allow(clippy::type_complexity)] fn migrate_remote_entities( query: Query<(Entity, &TbMapEntityId), (Added, With)>, children: Query<&Children>, mut commands: Commands, mut mapping: ResMut, ) { for (serverside, tb_id) in query.iter() { received_remote_map_entity(serverside, tb_id.id, &children, &mut mapping, &mut commands); } } fn received_remote_map_entity( serverside: Entity, tb_id: u64, children: &Query<&Children>, mapping: &mut TbMapEntityMapping, commands: &mut Commands, ) { let Some(clientside) = mapping.0.remove(&tb_id) else { warn!("received unknown MapEntity ID `{tb_id:?}`"); return; }; // cannot just use `take` directly with a bundle because then any missing component would cause // the entire bundle to fail move_component::(commands, clientside, serverside); move_component::<( Collider, ColliderAabb, ColliderDensity, ColliderMarker, CollisionLayers, )>(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); move_component::(commands, clientside, serverside); if let Ok(children) = children.get(clientside) { for child in children.iter() { commands.entity(child).insert(ChildOf(serverside)); } } commands.entity(clientside).despawn(); } fn move_component( commands: &mut Commands, from: Entity, to: Entity, ) { commands.queue(move |world: &mut World| { 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: MessageReader, mut map: ResMut, ) { for msg in messages.read() { // 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(); } }