Switch to replicon (#80)

This commit is contained in:
PROMETHIA-27
2025-12-08 19:22:17 -05:00
committed by GitHub
parent 7a5d2e6914
commit ff52258ad2
55 changed files with 1212 additions and 2951 deletions

View File

@@ -12,12 +12,13 @@ avian3d = { workspace = true }
bevy = { workspace = true, default-features = false }
bevy-steamworks = { workspace = true }
bevy_common_assets = { workspace = true }
bevy_replicon = { workspace = true, features = ["server"] }
bevy_replicon_renet = { workspace = true, features = ["server"] }
bevy_sprite3d = { workspace = true }
bevy_trenchbroom = { workspace = true }
bevy_trenchbroom_avian = { workspace = true }
clap = { version = "=4.5.47", features = ["derive"] }
clap = { workspace = true }
happy_feet = { workspace = true }
lightyear = { workspace = true }
rand = { workspace = true }
ron = { workspace = true }
serde = { workspace = true }

View File

@@ -1,5 +1,5 @@
use bevy::prelude::*;
use lightyear::prelude::input::native::ActionState;
use bevy_replicon::prelude::{ClientState, FromClient, SendMode, ServerTriggerExt, ToClients};
use shared::{
GameState,
backpack::{
@@ -7,7 +7,7 @@ use shared::{
backpack_ui::{BackpackUiState, HEAD_SLOTS},
},
control::ControlState,
protocol::PlaySound,
protocol::{ClientToController, PlaySound},
};
pub fn plugin(app: &mut App) {
@@ -15,23 +15,34 @@ pub fn plugin(app: &mut App) {
FixedUpdate,
sync_on_change.run_if(in_state(GameState::Playing)),
);
app.add_systems(FixedUpdate, swap_head_inputs);
app.add_systems(
FixedUpdate,
swap_head_inputs.run_if(in_state(ClientState::Disconnected)),
);
}
fn swap_head_inputs(
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
backpacks: Query<Ref<Backpack>>,
clients: ClientToController,
mut inputs: MessageReader<FromClient<ControlState>>,
mut commands: Commands,
mut state: Single<&mut BackpackUiState>,
time: Res<Time>,
) {
for (controls, backpack) in player.iter() {
for controls in inputs.read() {
let player = clients.get_controller(controls.client_id);
let backpack = backpacks.get(player).unwrap();
if state.count == 0 {
return;
}
if controls.backpack_toggle {
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Backpack { open: state.open },
});
}
if !state.open {
@@ -52,7 +63,10 @@ fn swap_head_inputs(
}
if changed {
commands.trigger(PlaySound::Selection);
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Selection,
});
sync(&backpack, &mut state, time.elapsed_secs());
}
}

View File

@@ -3,7 +3,7 @@ use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
prelude::*,
};
use lightyear::prelude::{NetworkTarget, Replicate};
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use shared::{
global_observer,
head_drop::{HeadCollected, HeadDrop, HeadDropEnableTime, HeadDrops, SecretHeadMarker},
@@ -39,7 +39,10 @@ fn on_head_drop(
};
if drop.impulse {
commands.trigger(PlaySound::HeadDrop);
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::HeadDrop,
});
}
let mesh_addr = format!("{:?}", heads_db.head_stats(drop.head_id).ability).to_lowercase();
@@ -70,16 +73,18 @@ fn on_head_drop(
CollisionEventsEnabled,
HeadDrop { head_id },
HeadDropEnableTime(now + 1.2),
Replicated,
))
.observe(on_collect_head);
}
})),
Replicate::to_clients(NetworkTarget::All),
Replicated,
))
.with_child((
Billboard::All,
SquishAnimation(2.6),
GltfSceneRoot::HeadDrop(mesh_addr),
Replicated,
));
Ok(())
@@ -101,9 +106,15 @@ fn on_collect_head(
let is_secret = query_secret.contains(collectable);
if is_secret {
commands.trigger(PlaySound::SecretHeadCollect);
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::SecretHeadCollect,
});
} else {
commands.trigger(PlaySound::HeadCollect);
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::HeadCollect,
});
}
commands.entity(collider).trigger(|entity| HeadCollected {

View File

@@ -4,9 +4,7 @@ use bevy_common_assets::ron::RonAssetPlugin;
use bevy_sprite3d::Sprite3dPlugin;
use bevy_trenchbroom::prelude::*;
use bevy_trenchbroom_avian::AvianPhysicsBackend;
use lightyear::prelude::server::ServerPlugins;
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
use std::time::Duration;
mod backpack;
mod config;
@@ -74,15 +72,7 @@ fn main() {
..default()
}));
app.add_plugins(
PhysicsPlugins::default()
.build()
// FrameInterpolation handles interpolating Position and Rotation
.disable::<PhysicsInterpolationPlugin>(),
);
app.add_plugins(ServerPlugins {
tick_duration: Duration::from_secs_f32(1.0 / 60.0),
});
app.add_plugins(PhysicsPlugins::default());
app.add_plugins(Sprite3dPlugin);
app.add_plugins(TrenchBroomPlugins(
TrenchBroomConfig::new("hedz").icon(None),
@@ -112,9 +102,9 @@ fn main() {
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::steam::plugin);
app.add_plugins(shared::tb_entities::plugin);
app.add_plugins(shared::tick::plugin);
app.add_plugins(shared::utils::auto_rotate::plugin);
app.add_plugins(shared::utils::billboards::plugin);
app.add_plugins(shared::utils::explosions::plugin);
@@ -124,12 +114,16 @@ fn main() {
app.add_plugins(shared::utils::plugin);
app.add_plugins(shared::water::plugin);
// Networking
// The client/server plugin must go before the protocol, or else `ProtocolHasher` will not be available.
app.add_plugins(server::plugin);
app.add_plugins(shared::protocol::plugin);
app.add_plugins(backpack::plugin);
app.add_plugins(config::plugin);
app.add_plugins(head_drop::plugin);
app.add_plugins(platforms::plugin);
app.add_plugins(player::plugin);
app.add_plugins(server::plugin);
app.add_plugins(tb_entities::plugin);
app.add_plugins(utils::plugin);

View File

@@ -1,6 +1,5 @@
use bevy::prelude::*;
use bevy_trenchbroom::prelude::Target;
use lightyear::prelude::{NetworkTarget, PredictionTarget};
use shared::{
GameState,
platforms::ActivePlatform,
@@ -34,8 +33,6 @@ fn init(
target,
};
commands
.entity(e)
.insert((platform, PredictionTarget::to_clients(NetworkTarget::All)));
commands.entity(e).insert(platform);
}
}

View File

@@ -1,11 +1,11 @@
use bevy::prelude::*;
use lightyear::prelude::{input::native::ActionState, *};
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use shared::{
backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget},
cash::CashResource,
character::AnimatedCharacter,
control::{ControlState, controller_common::PlayerCharacterController},
control::{Inputs, controller_common::PlayerCharacterController},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
@@ -14,9 +14,7 @@ use shared::{
hitpoints::{Hitpoints, Kill},
npc::SpawnCharacter,
player::{Player, PlayerBodyMesh},
protocol::{
PlaySound, PlayerId, channels::UnorderedReliableChannel, events::ClientHeadChanged,
},
protocol::{PlaySound, PlayerId, events::ClientHeadChanged},
tb_entities::SpawnPoint,
};
@@ -26,7 +24,7 @@ pub fn plugin(app: &mut App) {
pub fn spawn(
mut commands: Commands,
owner: Entity,
_owner: Entity,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> Option<Entity> {
@@ -34,48 +32,53 @@ pub fn spawn(
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
let mut player = commands.spawn((
(
Name::from("player"),
Player,
ActiveHead(0),
ActiveHeads::new([
Some(HeadState::new(0, heads_db.as_ref())),
Some(HeadState::new(3, heads_db.as_ref())),
Some(HeadState::new(6, heads_db.as_ref())),
Some(HeadState::new(10, heads_db.as_ref())),
Some(HeadState::new(9, heads_db.as_ref())),
]),
Hitpoints::new(100),
CashResource::default(),
CameraTarget,
transform,
Visibility::default(),
PlayerCharacterController,
PlayerId { id: 0 },
),
ActionState::<ControlState>::default(),
Backpack::default(),
BackpackUiState::default(),
UiActiveHeads::default(),
Replicate::to_clients(NetworkTarget::All),
PredictionTarget::to_clients(NetworkTarget::All),
ControlledBy {
owner,
lifetime: Lifetime::SessionBased,
},
children![(
Name::new("player-rig"),
PlayerBodyMesh,
CameraArmRotation,
children![AnimatedCharacter::new(0)],
)],
));
player.observe(on_kill);
let id = commands
.spawn((
(
Name::from("player"),
Player,
ActiveHead(0),
ActiveHeads::new([
Some(HeadState::new(0, heads_db.as_ref())),
Some(HeadState::new(3, heads_db.as_ref())),
Some(HeadState::new(6, heads_db.as_ref())),
Some(HeadState::new(10, heads_db.as_ref())),
Some(HeadState::new(9, heads_db.as_ref())),
]),
Hitpoints::new(100),
CashResource::default(),
CameraTarget,
transform,
Visibility::default(),
PlayerCharacterController,
PlayerId { id: 0 },
),
Backpack::default(),
BackpackUiState::default(),
UiActiveHeads::default(),
Inputs::default(),
Replicated,
))
.with_children(|c| {
c.spawn((
Name::new("player-rig"),
PlayerBodyMesh,
CameraArmRotation,
Replicated,
))
.with_child((
Name::new("player-animated-character"),
AnimatedCharacter::new(0),
Replicated,
));
})
.observe(on_kill)
.id();
let id = player.id();
commands.trigger(PlaySound::Head("angry demonstrator".to_string()));
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Head("angry demonstrator".to_string()),
});
commands.trigger(SpawnCharacter(transform.translation));
Some(id)
@@ -103,7 +106,6 @@ fn on_update_head_mesh(
trigger: On<HeadChanged>,
mut commands: Commands,
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
mut sender: Single<&mut EventSender<ClientHeadChanged>>,
animated_characters: Query<&AnimatedCharacter>,
mut player: Single<&mut ActiveHead, With<Player>>,
) -> Result {
@@ -118,7 +120,10 @@ fn on_update_head_mesh(
.entity(animated_char)
.insert(AnimatedCharacter::new(trigger.0));
sender.trigger::<UnorderedReliableChannel>(ClientHeadChanged(trigger.0 as u64));
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: ClientHeadChanged(trigger.0 as u64),
});
Ok(())
}

View File

@@ -1,91 +1,58 @@
use crate::config::ServerConfig;
use bevy::{ecs::component::Components, prelude::*};
use lightyear::{
connection::client::PeerMetadata,
link::LinkConditioner,
use bevy::prelude::*;
use bevy_replicon::{
RepliconPlugins,
prelude::{
server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
*,
ClientId, ConnectedClient, FromClient, RepliconChannels, SendMode, ServerState,
ServerTriggerExt, ToClients,
},
server::AuthorizedClient,
};
use bevy_replicon_renet::{
RenetChannelsExt, RepliconRenetPlugins,
netcode::{NetcodeServerTransport, ServerAuthentication},
renet::{ConnectionConfig, RenetServer},
};
use shared::{
GameState, global_observer,
heads_database::HeadsDatabase,
protocol::{
ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
},
player::ClientPlayerId,
protocol::{ClientEnteredPlaying, PlayerId, SetGameTick, messages::AssignClientPlayer},
tb_entities::SpawnPoint,
tick::GameTick,
};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
net::{Ipv4Addr, UdpSocket},
time::SystemTime,
};
pub fn plugin(app: &mut App) {
app.add_systems(
OnEnter(GameState::Playing),
(start_server, setup_timeout_timer),
);
app.add_systems(
Update,
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
);
app.add_plugins((RepliconPlugins, RepliconRenetPlugins));
global_observer!(app, handle_new_client);
global_observer!(app, on_client_connected);
global_observer!(app, on_client_playing);
global_observer!(app, close_on_disconnect);
app.add_systems(OnEnter(GameState::Playing), setup_timeout_timer);
app.add_systems(OnEnter(ServerState::Running), notify_started);
app.add_systems(Update, run_timeout.run_if(in_state(GameState::Playing)));
app.add_systems(OnEnter(GameState::Playing), open_renet_server);
// Replicon
global_observer!(app, on_connected);
global_observer!(app, on_disconnected);
// Server logic
global_observer!(app, cancel_timeout);
}
#[derive(Component)]
struct ClientPlayerId(u8);
fn handle_new_client(
trigger: On<Add, Linked>,
mut commands: Commands,
id: Query<&PeerAddr>,
) -> Result {
let Ok(id) = id.get(trigger.event().entity) else {
return Ok(());
};
info!("Client connected on IP: {}", id.ip());
let conditioner = LinkConditioner::new(LinkConditionerConfig {
incoming_latency: Duration::from_millis(10),
incoming_jitter: Duration::from_millis(0),
incoming_loss: 0.0,
});
commands.entity(trigger.event().entity).insert((
ReplicationSender::default(),
Link::new(Some(conditioner)),
ClientPlayerId(0),
));
Ok(())
}
fn on_client_connected(
trigger: On<Add, ClientOf>,
mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
) -> Result {
// `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
// send the client player id until now.
let (id, mut sender) = assign_player.get_mut(trigger.event().entity)?;
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
Ok(())
global_observer!(app, on_client_playing);
}
fn on_client_playing(
trigger: On<RemoteEvent<ClientEnteredPlaying>>,
trigger: On<FromClient<ClientEnteredPlaying>>,
commands: Commands,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
peers: Res<PeerMetadata>,
) -> Result {
let Some(&client) = peers.mapping.get(&trigger.from) else {
info!("Client has entered playing gamestate");
let Some(client) = trigger.client_id.entity() else {
return Ok(());
};
@@ -94,51 +61,85 @@ fn on_client_playing(
Ok(())
}
fn close_on_disconnect(
_trigger: On<Remove, Connected>,
//
// Renet
//
fn open_renet_server(
mut commands: Commands,
channels: Res<RepliconChannels>,
) -> Result<(), BevyError> {
let server_channels_config = channels.server_configs();
let client_channels_config = channels.client_configs();
let server = RenetServer::new(ConnectionConfig {
server_channels_config,
client_channels_config,
..Default::default()
});
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let port = 31111;
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))?;
let server_config = bevy_replicon_renet::netcode::ServerConfig {
current_time,
max_clients: 1,
protocol_id: 0,
authentication: ServerAuthentication::Unsecure,
public_addresses: Default::default(),
};
let transport = NetcodeServerTransport::new(server_config, socket)?;
commands.insert_resource(server);
commands.insert_resource(transport);
info!("Hosting a server on port {port}");
Ok(())
}
//
// server logic
//
fn on_connected(
trigger: On<Add, AuthorizedClient>,
game_tick: Res<GameTick>,
mut commands: Commands,
mut assign_id: MessageWriter<ToClients<AssignClientPlayer>>,
) {
let client = trigger.event_target();
info!("{client} connected to server!");
let id = ClientPlayerId(PlayerId { id: 0 });
commands.entity(client).insert(id);
assign_id.write(ToClients {
mode: SendMode::Direct(ClientId::Client(trigger.entity)),
message: AssignClientPlayer(id.0),
});
commands.server_trigger(ToClients {
mode: SendMode::Direct(ClientId::Client(trigger.entity)),
message: SetGameTick(game_tick.0),
});
}
fn on_disconnected(
on: On<Remove, ConnectedClient>,
config: Res<ServerConfig>,
mut writer: MessageWriter<AppExit>,
) {
info!("client {} disconnected", on.entity);
if config.close_on_client_disconnect {
info!("client disconnected, exiting");
writer.write(AppExit::Success);
}
}
fn start_server(mut commands: Commands) -> Result {
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);
let conditioner = LinkConditioner::new(LinkConditionerConfig {
incoming_latency: Duration::from_millis(10),
incoming_jitter: Duration::from_millis(0),
incoming_loss: 0.0,
});
let mut commands = commands.spawn((
Name::from("Server"),
LocalAddr(server_addr),
ServerUdpIo::default(),
NetcodeServer::new(NetcodeConfig {
client_timeout_secs: 3,
..Default::default()
}),
Link::new(Some(conditioner)),
));
commands
.observe(|on: On<Add>, components: &Components| {
for &comp in on.trigger().components {
info!("Added {} to server", components.get_name(comp).unwrap());
}
})
.trigger(|entity| server::Start { entity });
Ok(())
}
fn notify_started(started: Query<&Started>, mut notified: Local<bool>) {
if !*notified && !started.is_empty() {
println!("hedz.server_started");
*notified = true;
}
fn notify_started() {
println!("hedz.server_started");
}
#[derive(Resource)]
@@ -161,7 +162,7 @@ fn run_timeout(
}
}
fn cancel_timeout(_trigger: On<Add, Connected>, mut timer: ResMut<TimeoutTimer>) {
fn cancel_timeout(_trigger: On<Add, ConnectedClient>, mut timer: ResMut<TimeoutTimer>) {
info!("client connected, cancelling timeout");
timer.0 = f32::INFINITY;
}

View File

@@ -1,8 +1,8 @@
use bevy::prelude::*;
use lightyear::prelude::{Connected, MessageSender};
use bevy_replicon::prelude::{ClientId, ConnectedClient, SendMode, ToClients};
use shared::{
GameState, global_observer,
protocol::{TbMapEntityId, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity},
protocol::{TbMapEntityId, messages::DespawnTbMapEntity},
};
pub fn plugin(app: &mut App) {
@@ -32,12 +32,14 @@ fn add_despawned_entities_to_cache(
pub struct DespawnedTbEntityCache(pub Vec<u64>);
fn send_new_client_despawned_cache(
trigger: On<Add, Connected>,
on: On<Add, ConnectedClient>,
cache: Res<DespawnedTbEntityCache>,
mut send: Query<&mut MessageSender<DespawnTbMapEntity>>,
mut send: MessageWriter<ToClients<DespawnTbMapEntity>>,
) {
let mut send = send.get_mut(trigger.event().entity).unwrap();
for &id in cache.0.iter() {
send.send::<UnorderedReliableChannel>(DespawnTbMapEntity(id));
send.write(ToClients {
mode: SendMode::Direct(ClientId::Client(on.entity)),
message: DespawnTbMapEntity(id),
});
}
}