Files
HEDZReloaded/crates/hedz_reloaded/src/client/mod.rs
2025-12-21 12:01:50 -05:00

226 lines
6.6 KiB
Rust

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<NetConfig>| 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<NextState<GameState>>) {
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<NetConfig>,
channels: Res<RepliconChannels>,
steam_client: Option<Res<Client>>,
) -> 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<TbMapEntityId>, With<ConfirmHistory>)>,
children: Query<&Children>,
mut commands: Commands,
mut mapping: ResMut<TbMapEntityMapping>,
) {
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::<Brushes>(commands, clientside, serverside);
move_component::<(
Collider,
ColliderAabb,
ColliderDensity,
ColliderMarker,
CollisionLayers,
)>(commands, clientside, serverside);
move_component::<ColliderOf>(commands, clientside, serverside);
move_component::<ColliderTransform>(commands, clientside, serverside);
move_component::<CollisionEventsEnabled>(commands, clientside, serverside);
move_component::<Movable>(commands, clientside, serverside);
move_component::<Platform>(commands, clientside, serverside);
move_component::<PlatformTarget>(commands, clientside, serverside);
move_component::<SceneInstance>(commands, clientside, serverside);
move_component::<SceneRoot>(commands, clientside, serverside);
move_component::<Sensor>(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<B: Bundle + BundleFromComponents>(
commands: &mut Commands,
from: Entity,
to: Entity,
) {
commands.queue(move |world: &mut World| {
let comp = world.entity_mut(from).take::<B>();
if let Some(comp) = comp {
world.entity_mut(to).insert(comp);
}
});
}
fn despawn_absent_map_entities(
mut commands: Commands,
mut messages: MessageReader<DespawnTbMapEntity>,
mut map: ResMut<TbMapEntityMapping>,
) {
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();
}
}