diff --git a/NOTES.md b/NOTES.md index 83abf14bb..d3f04bae9 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,3 +1,21 @@ +- Add a `Controlled` component to an entity to specify that the player is controlling the entity + - field (`controlled_by: NetworkTarget`) on the server `Replicate` + - it means that the `Controlled` component gets replicated to the client who has control of this entity. + - then the client can filter on `Controlled` to add `Prediction` behaviour, for example. And add Interpolation on non-controlled entities? + - server also creates an entity for each connected client. Each client entity has a component indicating the + list of entities under control of the client. `HasControl(EntityHashSet)` (with EntityMapping) + - if the player disconnects, we can despawn automatically all entities under their control. (this behavior can be made configurable in the future) + - Would `Controlled` be synced to the Predicted entity? Maybe? if we predict other players, it would be nice to know + which Predicted entity is under our control. + - client->server replication: + + - PROS: + - on the server, users can easily find the list of entities under control of a specific client + - on the client, users that receive an entity from the server can quickly check if they have control of it, without + having to compare client_ids. + + + - Transferring ownership to another client. - commands.transfer_ownership(entity, new_owner) - sends a message AuthorityTransfer to the new client who should replicate the entity diff --git a/README.md b/README.md index 144aaca52..7a3230451 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ app.add_plugins(InputPlugin::::default()); // components app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); // channels app.add_channel::(ChannelSettings { diff --git a/benches/message.rs b/benches/message.rs index fe9b1f0c3..fda16b9ee 100644 --- a/benches/message.rs +++ b/benches/message.rs @@ -11,9 +11,10 @@ use divan::{AllocProfiler, Bencher}; use lightyear::client::sync::SyncConfig; use lightyear::prelude::client::{InterpolationConfig, PredictionConfig}; use lightyear::prelude::{client, server, MessageRegistry, Tick, TickManager}; -use lightyear::prelude::{ClientId, NetworkTarget, SharedConfig, TickConfig}; +use lightyear::prelude::{ClientId, SharedConfig, TickConfig}; use lightyear::server::input::InputBuffers; use lightyear::shared::replication::components::Replicate; +use lightyear::shared::replication::network_target::NetworkTarget; use lightyear_benches::local_stepper::{LocalBevyStepper, Step as LocalStep}; use lightyear_benches::protocol::*; diff --git a/benches/spawn.rs b/benches/spawn.rs index d0b93bfcb..da42e1457 100644 --- a/benches/spawn.rs +++ b/benches/spawn.rs @@ -13,9 +13,10 @@ use lightyear::prelude::client::{ ClientConnection, InterpolationConfig, NetClient, PredictionConfig, }; use lightyear::prelude::{client, server, MessageRegistry, Tick, TickManager}; -use lightyear::prelude::{ClientId, NetworkTarget, SharedConfig, TickConfig}; +use lightyear::prelude::{ClientId, SharedConfig, TickConfig}; use lightyear::server::input::InputBuffers; use lightyear::shared::replication::components::Replicate; +use lightyear::shared::replication::network_target::NetworkTarget; use lightyear_benches::local_stepper::{LocalBevyStepper, Step as LocalStep}; use lightyear_benches::protocol::*; @@ -53,16 +54,7 @@ fn spawn_local(bencher: Bencher, n: usize) { ); stepper.init(); - let entities = vec![ - ( - Component1(0.0), - Replicate { - replication_target: NetworkTarget::All, - ..default() - }, - ); - n - ]; + let entities = vec![(Component1(0.0), Replicate::default()); n]; stepper.server_app.world.spawn_batch(entities); stepper @@ -109,16 +101,7 @@ fn spawn_multi_clients(bencher: Bencher, n: usize) { ); stepper.init(); - let entities = vec![ - ( - Component1(0.0), - Replicate { - replication_target: NetworkTarget::All, - ..default() - }, - ); - FIXED_NUM_ENTITIES - ]; + let entities = vec![(Component1(0.0), Replicate::default()); FIXED_NUM_ENTITIES]; stepper.server_app.world.spawn_batch(entities); stepper diff --git a/benches/src/local_stepper.rs b/benches/src/local_stepper.rs index 15e0ea0b3..290eccb6e 100644 --- a/benches/src/local_stepper.rs +++ b/benches/src/local_stepper.rs @@ -71,7 +71,7 @@ impl LocalBevyStepper { // channels to receive a message from/to server let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - let client_io = IoConfig::from_transport(TransportConfig::LocalChannel { + let client_io = client::IoConfig::from_transport(ClientTransport::LocalChannel { recv: from_server_recv, send: to_server_send, }); @@ -111,7 +111,7 @@ impl LocalBevyStepper { interpolation: interpolation_config.clone(), ..default() }; - client_app.add_plugins((ClientPlugin::new(config), ProtocolPlugin)); + client_app.add_plugins((ClientPlugins::new(config), ProtocolPlugin)); // Initialize Real time (needed only for the first TimeSystem run) client_app .world @@ -122,7 +122,7 @@ impl LocalBevyStepper { } // Setup server - let server_io = IoConfig::from_transport(TransportConfig::Channels { + let server_io = server::IoConfig::from_transport(ServerTransport::Channels { channels: client_params, }); @@ -151,7 +151,7 @@ impl LocalBevyStepper { }], ..default() }; - server_app.add_plugins((ServerPlugin::new(config), ProtocolPlugin)); + server_app.add_plugins((ServerPlugins::new(config), ProtocolPlugin)); // Initialize Real time (needed only for the first TimeSystem run) server_app diff --git a/benches/src/protocol.rs b/benches/src/protocol.rs index 1e7ded3ab..381ee26d9 100644 --- a/benches/src/protocol.rs +++ b/benches/src/protocol.rs @@ -3,6 +3,7 @@ use bevy::prelude::Component; use bevy::utils::default; use derive_more::{Add, Mul}; use lightyear::client::components::ComponentSyncMode; +use lightyear::client::prediction::plugin::add_prediction_systems; use serde::{Deserialize, Serialize}; use std::ops::Mul; @@ -55,13 +56,13 @@ impl Plugin for ProtocolPlugin { // inputs app.add_plugins(InputPlugin::::default()); // components - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Full); - app.add_linear_interpolation_fn::(); - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Simple); - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Simple); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once); // channels app.add_channel::(ChannelSettings { mode: ChannelMode::OrderedReliable(ReliableSettings::default()), diff --git a/book/src/concepts/advanced_replication/client_replication.md b/book/src/concepts/advanced_replication/client_replication.md index 2e20036c2..d637dc6a8 100644 --- a/book/src/concepts/advanced_replication/client_replication.md +++ b/book/src/concepts/advanced_replication/client_replication.md @@ -7,6 +7,24 @@ There are different possibilities. To replicate a client-entity to the server, it is exactly the same as for a server-entity. Just add the `Replicate` component to the entity and it will be replicated to the server. +```rust +fn handle_connection( + mut connection_event: EventReader, + mut commands: Commands, +) { + for event in connection_event.read() { + let local_client_id = event.client_id(); + commands.spawn(( + /* your other components here */ + Replicate { + replication_target: NetworkTarget::All, + interpolation_target: NetworkTarget::AllExcept(vec![local_client_id]), + ..default() + }, + )); + } +} +``` Note that `prediction_target` and `interpolation_target` will be unused as the server doesn't do any prediction or interpolation. diff --git a/book/src/concepts/advanced_replication/interest_management.md b/book/src/concepts/advanced_replication/interest_management.md index 246e63817..bc4a7ddf0 100644 --- a/book/src/concepts/advanced_replication/interest_management.md +++ b/book/src/concepts/advanced_replication/interest_management.md @@ -12,66 +12,84 @@ There are two main advantages: For example, in a RTS, you can avoid replicating units that are in fog-of-war. - ## Implementation -In lightyear, interest management is implemented with the concept of `Rooms`. +### VisibilityMode -An entity can join one or more rooms, and clients can similarly join one or more rooms. +The first step is to think about the `VisibilityMode` of your entities. It is defined on the `Replicate` component. -We then compute which entities should be replicated to which clients by looking at which rooms they are both in. - -To summarize: -- if a client is in a room but the entity is not (or vice-versa), we will not replicate that entity to that client -- if the client and entity are both in the same room, we will replicate that entity to that client -- if a client leaves a room that the entity is in (or an entity leaves a room that the client is in), we will despawn that entity for that client -- if a client joins a room that the entity is in (or an entity joins a room that the client is in), we will spawn that entity for that client +```rust,noplayground +#[derive(Default)] +pub enum VisibilityMode { + /// We will replicate this entity to all clients that are present in the [`NetworkTarget`] AND use visibility on top of that + InterestManagement, + /// We will replicate this entity to all clients that are present in the [`NetworkTarget`] + #[default] + All +} +``` +If `VisibilityMode::All`, you have a coarse way of doing interest management, which is to use the `replication_target` to +specify which clients will receive client updates. The `replication_target` is a `NetworkTarget` which is a list of clients +that we should replicate to. -Since it can be annoying to have always add your entities to the correct rooms, especially if you want to just replicate them to everyone. -We introduce several concepts to make this more convenient. +In some cases, you might want to use `VisibilityMode::InterestManagement`, which is a more fine-grained way of doing interest management. +This adds additional constraints on top of the `replication_target`, we will **never** send updates for a client that is not in the +`replication_target` of your entity. -#### NetworkTarget -```rust,noplayground -/// NetworkTarget indicated which clients should receive some message -pub enum NetworkTarget { - #[default] - /// Message sent to no client - None, - /// Message sent to all clients except for one - AllExcept(ClientId), - /// Message sent to all clients - All, - /// Message sent to only one client - Only(ClientId), -} -``` +### Interest management -NetworkTarget is used to indicate very roughly to which clients a given entity should be replicated. -Note that this is in addition of rooms. +If you set `VisibilityMode::InterestManagement`, we will add a `ReplicateVisibility` component to your entity, +which is a cached list of clients that should receive replication updates about this entity. -Even if an entity and a client are in the same room, the entity will not be replicated to the client if the NetworkTarget forbids it (for instance, it is not `All` or `Only(client_id)`) +There are several ways to update the visibility of an entity: +- you can either update the visibility directly with the `VisibilityManager` resource +- we also provide a more static way of updating the visibility with the concept of `Rooms` and the `RoomManager` resource. -However, if a `NetworkTarget` is `All`, that doesn't necessarily mean that the entity will be replicated to all clients; they still need to be in the same rooms. -There is a setting to change this behaviour, the `ReplicationMode`. +#### Immediate visibility update +You can simply directly update the visibility of an entity/client pair with the `VisibilityManager` resource. -#### ReplicationMode +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::server::*; -We also introduce: -```rust,noplayground -#[derive(Default)] -pub enum ReplicationMode { - /// Use rooms for replication - Room, - /// We will replicate this entity to clients using only the [`NetworkTarget`], without caring about rooms - #[default] - NetworkTarget +fn my_system( + mut visibility_manager: ResMut, +) { + // you can update the visibility like so + visibility_manager.gain_visibility(ClientId::Netcode(1), Entity::PLACEHOLDER); + visibility_manager.lose_visibility(ClientId::Netcode(2), Entity::PLACEHOLDER); } ``` -If the `ReplicationMode` is `Room`, then the `NetworkTarget` is a prerequisite for replication, but not sufficient. -i.e. the entity will be replicated if they are in the same room AND if the `NetworkTarget` allows it. +#### Rooms + +An entity can join one or more rooms, and clients can similarly join one or more rooms. + +We then compute which entities should be replicated to which clients by looking at which rooms they are both in. + +To summarize: +- if a client is in a room but the entity is not (or vice-versa), we will not replicate that entity to that client +- if the client and entity are both in the same room, we will replicate that entity to that client +- if a client leaves a room that the entity is in (or an entity leaves a room that the client is in), we will despawn that entity for that client +- if a client joins a room that the entity is in (or an entity joins a room that the client is in), we will spawn that entity for that client + +This can be useful for games where you have physical instances of rooms: +- a RPG where you can have different rooms (tavern, cave, city, etc.) +- a server could have multiple lobbies, and each lobby is in its own room +- a map could be divided into a grid of 2D squares, where each square is its own room -If the `ReplicationMode` is `NetworkTarget`, then we will only use the value of `replicate.replication_target` without checking rooms at all. \ No newline at end of file +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::server::*; + +fn room_system(mut manager: ResMut) { + // the entity will now be visible to the client + manager.add_client(ClientId::Netcode(0), RoomId(0)); + manager.add_entity(Entity::PLACEHOLDER, RoomId(0)); +} +``` \ No newline at end of file diff --git a/book/src/concepts/advanced_replication/replication_logic.md b/book/src/concepts/advanced_replication/replication_logic.md index 70b80e752..6b6d085e3 100644 --- a/book/src/concepts/advanced_replication/replication_logic.md +++ b/book/src/concepts/advanced_replication/replication_logic.md @@ -13,12 +13,12 @@ Those two are handled differently by the replication system. There are certain invariants/guarantees that we wish to maintain with replication. +**Rule #1a**: we would like a replicated entity to be in a consistent state compared to what it was on the server: at no point do we want a situation where +a given component is on tick T1 but another component of the same entity is on tick T2. The replicated entity should be equal to a version of the remote entity in the past. +Similarly, we would not want one component of an entity to be inserted later than other components. This could be disastrous because some other system could depend on both +components being present together! -Rule #1: we would like a replicated entity to be in a consistent state compared to what it was on the server: at no point do we want a situation where -a given component is on tick T1 but another component of the same entity is on tick T2. Similarly, we would not want one component of an entity to be inserted -later than other components. This could be disastrous because some other system could depend on both components being present together! - -Rule #2: we want to be able to extend this guarantee to multiple entities. +**Rule #2**: we want to be able to extend this guarantee to multiple entities. I will give two relevant examples: - client prediction: for client-prediction, we want to rollback if a receives server-state doesn't match with the predicted history. If we are running client-prediction for multiple entities that are not in the same tick, we could have situations where we need to rollback one entity starting from tick T1 @@ -41,25 +41,29 @@ will be equivalent to the state of the group on the server at a given previous t ## Entity Actions -For each [`ReplicationGroup`](crate::prelude::ReplicationGroup), Entity Actions are replicated in an `OrderedReliable` manner: -- we apply each action message *in order* +For each [`ReplicationGroup`](crate::prelude::ReplicationGroup), Entity Actions are replicated in an `OrderedReliable` manner. ### Send Whenever there are any actions for a given [`ReplicationGroup`](crate::prelude::ReplicationGroup), we send them as a single message AND we include any updates for this group as well. This is to guarantee consistency; if we sent them as 2 separate messages, the packet containing the updates could get lost and we would be in an inconsistent state. +Each message for a given [`ReplicationGroup`] is associated with a message id (a monotonically increasing number) that is used to order the messages on the client. + +### Receive + +On the receive side, we buffer the EntityActions that we receive, so that we can read them in order (message id 1, 2, 3, 4, etc.) +We keep track of the next message id that we should receive. + ## Entity Updates ### Send -We gather all updates since the most recent of: -- last time we sent some EntityActions for the Replication Group -- last time we got an ACK from the client that the EntityUpdates was received +We gather all updates since the last time we got an ACK from the client that the EntityUpdates was received The reason for this is: -- we could be gathering all the component changes since the last time we sent EntityActions, but then it could be wasteful if the last time we had any entity actions was a long time ago - and many components got updated since +- we could be gathering all the component changes since the last time we sent EntityActions, but then it could be wasteful +if the last time we had any entity actions was a long time ago and many components got updated since. - we could be gathering all the component changes since the last time we sent a message, but then we could have a situation where: - we send changes for C1 on tick 1 - we send changes for C2 on tick 2 @@ -68,11 +72,10 @@ The reason for this is: ### Receive - For each [`ReplicationGroup`](crate::prelude::ReplicationGroup), Entity Updates are replicated in a `SequencedUnreliable` manner. We have some additional constraints: - we only apply EntityUpdates if we have already applied all the EntityActions for the given [`ReplicationGroup`](crate::prelude::ReplicationGroup) that were sent when the Updates were sent. - for example we send A1, U2, A3, U4; we receive U4 first, but we only apply it if we have applied A3, as those are the latest EntityActions sent when U4 was sent -- if we received a more rencet updates that can be applied, we discard the older one (Sequencing) +- if we received a more recent update that can be applied, we discard the older one (Sequencing) - for example if we send A1, U2, U3 and we receive A1 then U3, we discard U2 because it is older than U3 \ No newline at end of file diff --git a/book/src/concepts/replication/replicate.md b/book/src/concepts/replication/replicate.md index 3d8d721cc..a11ec4445 100644 --- a/book/src/concepts/replication/replicate.md +++ b/book/src/concepts/replication/replicate.md @@ -1,23 +1,27 @@ # Replication -You can use the `Replicate` component to initiate replicating an entity from the local `World` to the remote `World`. +You can use the `Replicate` bundle to initiate replicating an entity from the local `World` to the remote `World`. + +It is composed of multiple smaller components that each control an aspect of replication: +- `ReplicationTarget` to decide who to replicate to +- `VisibilityMode` to enable interest management +- `ControlledBy` so the server can track which entity is owned by each client +- `ReplicationGroup` to know which entity updates should be sent together in the same message +- `ReplicateHierarchy` to control if the children of an entity should also be replicated +- `DisabledComponent` to disable replication for a specific component +- `ReplicateOnceComponent` to specify that some components should not replicate updates, only inserts/removals +- `OverrideTargetComponent` to override the replication target for a specific component By default, every component in the entity that is part of the `ComponentRegistry` will be replicated. Any changes in -those components -will be replicated. +those components will be replicated. However the entity state will always be 'consistent': the remote entity will always contain the exact same combination of components as the local entity, even if it's a bit delayed. -You can remove the `Replicate` component to pause the replication. This can be useful when you want to despawn the +You can remove the `ReplicationTarget` component to pause the replication. This can be useful when you want to despawn the entity on the server without replicating the despawn. (e.g. an entity can be despawned immediately on the server, but needs to remain alive on the client to play a dying animation) -There are a lot of additional fields on the `Replicate` component that let you control exactly how the replication -works. -For example, `per_component_metadata` lets you fine-tune the replication logic for each component (exclude a component -from being replicated, etc.) - You can find some of the other usages in the [advanced_replication](../advanced_replication/title.md) section. diff --git a/book/src/tutorial/basic_systems.md b/book/src/tutorial/basic_systems.md index 327963fee..8f5419a87 100644 --- a/book/src/tutorial/basic_systems.md +++ b/book/src/tutorial/basic_systems.md @@ -61,7 +61,7 @@ pub(crate) fn handle_connections( /// on the server, the `context()` method returns the `ClientId` of the client that connected let client_id = *connection.context(); - /// We add the `Replicate` component to start replicating the entity to clients + /// We add the `Replicate` bundle to start replicating the entity to clients /// By default, the entity will be replicated to all clients let replicate = Replicate::default(); let entity = commands.spawn((PlayerBundle::new(client_id, Vec2::ZERO), replicate)); @@ -71,14 +71,15 @@ pub(crate) fn handle_connections( } ``` -As you can see above, starting replicating an entity is very easy: you just need to add the `Replicate` component to the entity +As you can see above, starting replicating an entity is very easy: you just need to add the `Replicate` bundle to the entity and it will start getting replicated. (you can learn more in the [replicate](../concepts/replication/replicate.md) page) - -If you remove the `Replicate` component from an entity, any updates to that entity won't be replicated anymore. -(However the client entity won't get despawned) +The `Replicate` bundle is composed of several components that control the replication logic. For example `ReplicationTarget` +specifies which clients should receive the entity. +If you remove the `ReplicationTarget` component from an entity, any updates to that entity won't be replicated anymore. +(However the remote entity won't get despawned, it will just stop getting updated) ## Handle client inputs diff --git a/book/src/tutorial/setup.md b/book/src/tutorial/setup.md index 085273fe9..60ad268c1 100644 --- a/book/src/tutorial/setup.md +++ b/book/src/tutorial/setup.md @@ -94,17 +94,17 @@ pub struct ProtocolPlugin; impl Plugin for ProtocolPlugin{ fn build(&self, app: &mut App) { app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Full) - .add_interpolation::(ComponentSyncMode::Full) - .add_linear_interpolation_fn::(); + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); } } ``` diff --git a/examples/auth/Cargo.toml b/examples/auth/Cargo.toml index 40e4e7737..04aff3c3f 100644 --- a/examples/auth/Cargo.toml +++ b/examples/auth/Cargo.toml @@ -14,9 +14,9 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } lightyear = { path = "../../lightyear", features = [ "steam", "webtransport", @@ -31,10 +31,6 @@ bevy = { version = "0.13", features = ["bevy_core_pipeline"] } bevy_mod_picking = { version = "0.18.2", features = ["backend_bevy_ui"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" tokio = "1.37.0" diff --git a/examples/auth/assets/settings.ron b/examples/auth/assets/settings.ron index 5fe587bcc..f3a7913f2 100644 --- a/examples/auth/assets/settings.ron +++ b/examples/auth/assets/settings.ron @@ -1,59 +1,61 @@ -Settings( - client: ClientSettings( - inspector: true, - client_id: 0, - client_port: 0, // the OS will assign a random open port - server_addr: "127.0.0.1", - conditioner: Some(Conditioner( - latency_ms: 200, - jitter_ms: 20, - packet_loss: 0.05 - )), - // server_port: 5000, - // transport: WebTransport( - // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks - // the server will print the certificate digest on startup - // certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", - // ), - server_port: 5001, - transport: Udp, - // server_port: 5002, - // transport: WebSocket, - // server_port: 5003, - // transport: Steam( - // app_id: 480, - // ) - ), - server: ServerSettings( - headless: true, - inspector: false, - conditioner: Some(Conditioner( - latency_ms: 200, - jitter_ms: 20, - packet_loss: 0.05 - )), - transport: [ - WebTransport( - local_port: 5000 - ), - Udp( - local_port: 5001 - ), - WebSocket( - local_port: 5002 - ), - // Steam( - // app_id: 480, - // server_ip: "0.0.0.0", - // game_port: 5003, - // query_port: 27016, +MySettings( + netcode_auth_port: 5005, + common: Settings( + client: ClientSettings( + inspector: true, + client_id: 0, + client_port: 0, // the OS will assign a random open port + server_addr: "127.0.0.1", + conditioner: Some(Conditioner( + latency_ms: 200, + jitter_ms: 20, + packet_loss: 0.05 + )), + // server_port: 5000, + // transport: WebTransport( + // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks + // the server will print the certificate digest on startup + // certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", // ), - ], - netcode_auth_port: 5005, - ), - shared: SharedSettings( - protocol_id: 0, - private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - compression: None, + server_port: 5001, + transport: Udp, + // server_port: 5002, + // transport: WebSocket, + // server_port: 5003, + // transport: Steam( + // app_id: 480, + // ) + ), + server: ServerSettings( + headless: true, + inspector: false, + conditioner: Some(Conditioner( + latency_ms: 200, + jitter_ms: 20, + packet_loss: 0.05 + )), + transport: [ + WebTransport( + local_port: 5000 + ), + Udp( + local_port: 5001 + ), + WebSocket( + local_port: 5002 + ), + // Steam( + // app_id: 480, + // server_ip: "0.0.0.0", + // game_port: 5003, + // query_port: 27016, + // ), + ], + ), + shared: SharedSettings( + protocol_id: 0, + private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + compression: None, + ) ) -) +) \ No newline at end of file diff --git a/examples/auth/src/client.rs b/examples/auth/src/client.rs index a78614a9c..21b2149ae 100644 --- a/examples/auth/src/client.rs +++ b/examples/auth/src/client.rs @@ -6,10 +6,9 @@ //! predicted entity and the server entity) use async_compat::Compat; use std::io::Read; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::str::FromStr; -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::tasks::futures_lite::future; use bevy::tasks::{block_on, IoTaskPool, Task}; @@ -20,13 +19,11 @@ use bevy_mod_picking::prelude::{Click, On, Pointer}; use lightyear::connection::netcode::CONNECT_TOKEN_BYTES; use tokio::io::AsyncReadExt; -pub use lightyear::prelude::client::*; +use lightyear::prelude::client::*; use lightyear::prelude::*; -use crate::protocol::Direction; use crate::protocol::*; -use crate::shared::shared_config; -use crate::{shared, ClientTransports, SharedSettings}; +use crate::shared; pub struct ExampleClientPlugin { pub auth_backend_address: SocketAddr, diff --git a/examples/auth/src/main.rs b/examples/auth/src/main.rs index 453a9ca5b..97480eb04 100644 --- a/examples/auth/src/main.rs +++ b/examples/auth/src/main.rs @@ -11,268 +11,58 @@ #![allow(unused_variables)] #![allow(dead_code)] -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{InterpolationConfig, InterpolationDelay, NetConfig}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::config::Mode; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::Settings; +use serde::{Deserialize, Serialize}; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client, -} - -/// We parse the settings.ron file to read the settings, than create the apps and run them fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -/// This is the main function -/// The cli argument is used to determine if we are running as a client or a server (or listen-server) -/// Then we build the app and run it. -/// -/// To build a lightyear app you will need to add either the [`client::ClientPlugin`] or [`server::ServerPlugin`] -/// They can be created by providing a [`client::ClientConfig`] or [`server::ServerConfig`] struct, along with a -/// shared protocol which defines the messages (Messages, Components, Inputs) that can be sent between client and server. -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer => { - let client_net_config = NetConfig::Local { id: 1 }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - 1, - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); - } - Cli::Client => { - let net_config = get_client_net_config(&settings, 0); - let mut app = client_app(settings, net_config); - app.run(); - } - } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - ..default() + let settings = common::settings::settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings.common.clone(), cli); + // add the lightyear [`ClientPlugins`] and [`ServerPlugins`] + app.add_lightyear_plugin_groups(); + // add out plugins + let client_plugin = ExampleClientPlugin { + auth_backend_address: SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::UNSPECIFIED, + settings.netcode_auth_port, + )), }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin { - auth_backend_address: SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - settings.server.netcode_auth_port, - )), - }, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - ..default() + let server_plugin = ExampleServerPlugin { + protocol_id: settings.common.shared.protocol_id, + private_key: settings.common.shared.private_key, + game_server_addr: SocketAddr::V4(SocketAddrV4::new( + settings.common.client.server_addr, + settings.common.client.server_port, + )), + auth_backend_addr: SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::UNSPECIFIED, + settings.netcode_auth_port, + )), }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin { - protocol_id: settings.shared.protocol_id, - private_key: settings.shared.private_key, - game_server_addr: SocketAddr::V4(SocketAddrV4::new( - settings.client.server_addr, - settings.client.server_port, - )), - auth_backend_addr: SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::UNSPECIFIED, - settings.server.netcode_auth_port, - )), - }, - SharedPlugin, - )); - app + app.add_plugins(client_plugin, server_plugin, SharedPlugin); + // run the app + app.run(); } -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MySettings { + pub(crate) common: Settings, - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin { - protocol_id: settings.shared.protocol_id, - private_key: settings.shared.private_key, - game_server_addr: SocketAddr::V4(SocketAddrV4::new( - settings.client.server_addr, - settings.client.server_port, - )), - auth_backend_addr: SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::UNSPECIFIED, - settings.server.netcode_auth_port, - )), - }, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin { - auth_backend_address: SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - settings.server.netcode_auth_port, - )), - }, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + /// The server will listen on this port for incoming tcp authentication requests + /// and respond with a [`ConnectToken`](lightyear::prelude::ConnectToken) + pub(crate) netcode_auth_port: u16, } diff --git a/examples/auth/src/protocol.rs b/examples/auth/src/protocol.rs index ea8595fc0..6ab12a7af 100644 --- a/examples/auth/src/protocol.rs +++ b/examples/auth/src/protocol.rs @@ -17,122 +17,14 @@ use serde::{Deserialize, Serialize}; use lightyear::client::components::ComponentSyncMode; use lightyear::prelude::*; -// Player -#[derive(Bundle)] -pub(crate) struct PlayerBundle { - id: PlayerId, - position: PlayerPosition, - color: PlayerColor, -} - -impl PlayerBundle { - pub(crate) fn new(id: ClientId, position: Vec2) -> Self { - // Generate pseudo random color from client id. - let h = (((id.to_bits().wrapping_mul(30)) % 360) as f32) / 360.0; - let s = 0.8; - let l = 0.5; - let color = Color::hsl(h, s, l); - Self { - id: PlayerId(id), - position: PlayerPosition(position), - color: PlayerColor(color), - } - } -} - -// Components - -#[derive(Component, Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct PlayerId(ClientId); - -#[derive(Component, Serialize, Deserialize, Clone, Debug, PartialEq, Deref, DerefMut, Add)] -pub struct PlayerPosition(Vec2); - -impl Mul for &PlayerPosition { - type Output = PlayerPosition; - - fn mul(self, rhs: f32) -> Self::Output { - PlayerPosition(self.0 * rhs) - } -} - -#[derive(Component, Deserialize, Serialize, Clone, Debug, PartialEq)] -pub struct PlayerColor(pub(crate) Color); - -// Example of a component that contains an entity. -// This component, when replicated, needs to have the inner entity mapped from the Server world -// to the client World. -// You will need to derive the `MapEntities` trait for the component, and register -// app.add_map_entities() in your protocol -#[derive(Component, Deserialize, Serialize, Clone, Debug, PartialEq)] -pub struct PlayerParent(Entity); - -impl MapEntities for PlayerParent { - fn map_entities(&mut self, entity_mapper: &mut M) { - self.0 = entity_mapper.map_entity(self.0); - } -} - -// Channels - -#[derive(Channel)] -pub struct Channel1; - -// Messages - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct Message1(pub usize); - -// Inputs - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct Direction { - pub(crate) up: bool, - pub(crate) down: bool, - pub(crate) left: bool, - pub(crate) right: bool, -} - -impl Direction { - pub(crate) fn is_none(&self) -> bool { - !self.up && !self.down && !self.left && !self.right - } -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub enum Inputs { - Direction(Direction), - Delete, - Spawn, - None, -} - // Protocol pub(crate) struct ProtocolPlugin; impl Plugin for ProtocolPlugin { fn build(&self, app: &mut App) { // messages - app.add_message::(ChannelDirection::Bidirectional); // inputs - app.add_plugins(InputPlugin::::default()); // components - app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Full) - .add_interpolation::(ComponentSyncMode::Full) - .add_linear_interpolation_fn::(); - - app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); // channels - app.add_channel::(ChannelSettings { - mode: ChannelMode::OrderedReliable(ReliableSettings::default()), - ..default() - }); } } diff --git a/examples/auth/src/server.rs b/examples/auth/src/server.rs index 5fad2c558..ab2a3f6ea 100644 --- a/examples/auth/src/server.rs +++ b/examples/auth/src/server.rs @@ -8,22 +8,20 @@ //! Lightyear will handle the replication of entities automatically if you add a `Replicate` component to them. use anyhow::Context; use async_compat::Compat; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::sync::{Arc, RwLock}; -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::tasks::IoTaskPool; use bevy::utils::{Duration, HashSet}; use tokio::io::AsyncWriteExt; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::ClientId::Netcode; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::shared_config; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; pub struct ExampleServerPlugin { pub protocol_id: u64, @@ -84,13 +82,13 @@ fn handle_connect_events( mut disconnect_events: EventReader, ) { for event in connect_events.read() { - if let Netcode(client_id) = event.context() { - client_ids.0.write().unwrap().insert(*client_id); + if let Netcode(client_id) = event.client_id { + client_ids.0.write().unwrap().insert(client_id); } } for event in disconnect_events.read() { - if let Netcode(client_id) = event.context() { - client_ids.0.write().unwrap().remove(client_id); + if let Netcode(client_id) = event.client_id { + client_ids.0.write().unwrap().remove(&client_id); } } } diff --git a/examples/auth/src/settings.rs b/examples/auth/src/settings.rs deleted file mode 100644 index 077ea3234..000000000 --- a/examples/auth/src/settings.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::env::join_paths; -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: Duration::from_millis(self.latency_ms as u64), - incoming_jitter: Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, - - /// The server will listen on this port for incoming tcp authentication requests - /// and respond with a [`ConnectToken`](lightyear::prelude::ConnectToken) - pub(crate) netcode_auth_port: u16, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = if client_id == 0 { - Authentication::None - } else { - Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - } - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.clone().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/auth/src/shared.rs b/examples/auth/src/shared.rs index 978822858..4455d65e6 100644 --- a/examples/auth/src/shared.rs +++ b/examples/auth/src/shared.rs @@ -13,18 +13,7 @@ use lightyear::shared::config::Mode; use crate::protocol::*; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - server_send_interval: Duration::from_millis(40), - // server_send_interval: Duration::from_millis(100), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/examples/bullet_prespawn/Cargo.toml b/examples/bullet_prespawn/Cargo.toml index a8911aefd..df5002c5c 100644 --- a/examples/bullet_prespawn/Cargo.toml +++ b/examples/bullet_prespawn/Cargo.toml @@ -8,9 +8,9 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } bevy_screen_diagnostics = "0.5.0" leafwing-input-manager = "0.13" lightyear = { path = "../../lightyear", features = [ @@ -19,7 +19,6 @@ lightyear = { path = "../../lightyear", features = [ "leafwing", "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" @@ -27,10 +26,4 @@ tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -ron = "0.8.1" -crossbeam-channel = "0.5.11" diff --git a/examples/bullet_prespawn/assets/settings.ron b/examples/bullet_prespawn/assets/settings.ron index 08718860e..258391b62 100644 --- a/examples/bullet_prespawn/assets/settings.ron +++ b/examples/bullet_prespawn/assets/settings.ron @@ -1,60 +1,63 @@ -Settings( - client: ClientSettings( - inspector: true, - client_id: 0, - client_port: 0, // the OS will assign a random open port - server_addr: "127.0.0.1", - input_delay_ticks: 0, - correction_ticks_factor: 1.5, - conditioner: Some(Conditioner( - latency_ms: 150, - jitter_ms: 10, - packet_loss: 0.02 - )), - server_port: 5000, - transport: WebTransport( - // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks - // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", - ), - // server_port: 5001, - // transport: Udp, - // server_port: 5002, - // transport: WebSocket, - // server_port: 5003, - // transport: Steam( - // app_id: 480, - // ) - ), - server: ServerSettings( - headless: true, - inspector: false, - conditioner: Some(Conditioner( - latency_ms: 150, - jitter_ms: 10, - packet_loss: 0.02 - )), - transport: [ - WebTransport( - local_port: 5000 - ), - Udp( - local_port: 5001 - ), - WebSocket( - local_port: 5002 +MySettings( + input_delay_ticks: 0, + correction_ticks_factor: 1.5, + common: Settings( + client: ClientSettings( + inspector: true, + client_id: 0, + client_port: 0, // the OS will assign a random open port + server_addr: "127.0.0.1", + + conditioner: Some(Conditioner( + latency_ms: 150, + jitter_ms: 10, + packet_loss: 0.02 + )), + server_port: 5000, + transport: WebTransport( + // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks + // the server will print the certificate digest on startup + certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", ), - // Steam( + // server_port: 5001, + // transport: Udp, + // server_port: 5002, + // transport: WebSocket, + // server_port: 5003, + // transport: Steam( // app_id: 480, - // server_ip: "0.0.0.0", - // game_port: 5003, - // query_port: 27016, - // ), - ], - ), - shared: SharedSettings( - protocol_id: 0, - private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - compression: Zstd(level: 0), + // ) + ), + server: ServerSettings( + headless: true, + inspector: false, + conditioner: Some(Conditioner( + latency_ms: 150, + jitter_ms: 10, + packet_loss: 0.02 + )), + transport: [ + WebTransport( + local_port: 5000 + ), + Udp( + local_port: 5001 + ), + WebSocket( + local_port: 5002 + ), + // Steam( + // app_id: 480, + // server_ip: "0.0.0.0", + // game_port: 5003, + // query_port: 27016, + // ), + ], + ), + shared: SharedSettings( + protocol_id: 0, + private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + compression: None, + ) ) ) diff --git a/examples/bullet_prespawn/src/client.rs b/examples/bullet_prespawn/src/client.rs index 3a9979177..044c80583 100644 --- a/examples/bullet_prespawn/src/client.rs +++ b/examples/bullet_prespawn/src/client.rs @@ -1,8 +1,3 @@ -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; - -use bevy::app::PluginGroupBuilder; -use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings}; use bevy::prelude::*; use bevy::utils::Duration; use leafwing_input_manager::action_state::ActionData; @@ -13,12 +8,12 @@ use leafwing_input_manager::plugin::InputManagerSystem; use leafwing_input_manager::prelude::*; use lightyear::inputs::native::input_buffer::InputBuffer; -pub use lightyear::prelude::client::*; +use lightyear::prelude::client::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_player_movement}; -use crate::{shared, ClientTransports, SharedSettings}; +use crate::shared; +use crate::shared::{color_from_id, shared_player_movement}; pub struct ExampleClientPlugin; diff --git a/examples/bullet_prespawn/src/main.rs b/examples/bullet_prespawn/src/main.rs index 40263854a..74fdbd52e 100644 --- a/examples/bullet_prespawn/src/main.rs +++ b/examples/bullet_prespawn/src/main.rs @@ -1,269 +1,68 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -//! Run with -//! - `cargo run -- server` -//! - `cargo run -- client -c 1` -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{ - InterpolationConfig, InterpolationDelay, NetConfig, ReplicationConfig, -}; -use lightyear::prelude::{Mode, TransportConfig}; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::{settings, Settings}; +use lightyear::prelude::client::PredictionConfig; +use serde::{Deserialize, Serialize}; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -fn run(settings: Settings, cli: Cli) { - match cli { - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); + let settings = settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings.common, cli); + + // for this example, we will use input delay and a correction function + let prediction_config = PredictionConfig { + input_delay_ticks: settings.input_delay_ticks, + correction_ticks_factor: settings.correction_ticks_factor, + ..default() + }; + match &mut app { + Apps::Client { config, .. } => { + config.prediction = prediction_config; } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); + Apps::ListenServer { client_config, .. } => { + client_config.prediction = prediction_config; } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); + Apps::HostServer { client_config, .. } => { + client_config.prediction = prediction_config; } + _ => {} } + // add `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins(ExampleClientPlugin, ExampleServerPlugin, SharedPlugin); + // run the app + app.run(); } -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - replication: ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - replication: lightyear::server::replication::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - SharedPlugin, - )); - app -} - -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - replication: lightyear::server::replication::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - replication: ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MySettings { + pub common: Settings, + + /// By how many ticks an input press will be delayed? + /// This can be useful as a tradeoff between input delay and prediction accuracy. + /// If the input delay is greater than the RTT, then there won't ever be any mispredictions/rollbacks. + /// See [this article](https://www.snapnet.dev/docs/core-concepts/input-delay-vs-rollback/) for more information. + pub(crate) input_delay_ticks: u16, + + /// If visual correction is enabled, we don't instantly snapback to the corrected position + /// when we need to rollback. Instead we interpolated between the current position and the + /// corrected position. + /// This controls the duration of the interpolation; the higher it is, the longer the interpolation + /// will take + pub(crate) correction_ticks_factor: f32, } diff --git a/examples/bullet_prespawn/src/protocol.rs b/examples/bullet_prespawn/src/protocol.rs index cdd45b3fc..b020532be 100644 --- a/examples/bullet_prespawn/src/protocol.rs +++ b/examples/bullet_prespawn/src/protocol.rs @@ -40,11 +40,14 @@ impl PlayerBundle { transform: Transform::from_xyz(position.x, position.y, 0.0), color: ColorComponent(color), replicate: Replicate { + target: ReplicationTarget { + // For HostServer mode, remember to also set prediction/interpolation targets for other clients + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, // NOTE (important): all entities that are being predicted need to be part of the same replication-group // so that all their updates are sent as a single message and are consistent (on the same tick) - replication_group: ReplicationGroup::new_id(id.to_bits()), - // For HostServer mode, remember to also set prediction/interpolation targets for other clients - interpolation_target: NetworkTarget::AllExceptSingle(id), + group: ReplicationGroup::new_id(id.to_bits()), ..default() }, inputs: InputManagerBundle:: { @@ -131,22 +134,22 @@ impl Plugin for ProtocolPlugin { app.add_plugins(LeafwingInputPlugin::::default()); app.add_plugins(LeafwingInputPlugin::::default()); // components - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_interpolation_fn::(TransformLinearInterpolation::lerp); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_interpolation_fn(TransformLinearInterpolation::lerp); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); // channels app.add_channel::(ChannelSettings { mode: ChannelMode::OrderedReliable(ReliableSettings::default()), diff --git a/examples/bullet_prespawn/src/server.rs b/examples/bullet_prespawn/src/server.rs index 7a4df0d44..0d501f63e 100644 --- a/examples/bullet_prespawn/src/server.rs +++ b/examples/bullet_prespawn/src/server.rs @@ -1,19 +1,14 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; use leafwing_input_manager::prelude::*; use lightyear::client::prediction::Predicted; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::*; -use lightyear::server::config::PacketConfig; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_player_movement}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; +use crate::shared::{color_from_id, shared_player_movement}; // Plugin for server-specific logic pub struct ExampleServerPlugin; @@ -28,7 +23,6 @@ impl Plugin for ExampleServerPlugin { ); // the physics/FixedUpdates systems that consume inputs should be run in the `FixedUpdate` schedule // app.add_systems(FixedUpdate, player_movement); - app.add_systems(Update, handle_disconnections); } } @@ -50,22 +44,6 @@ pub(crate) fn init(mut commands: Commands) { ); } -/// Server disconnection system, delete all player entities upon disconnection -pub(crate) fn handle_disconnections( - mut disconnections: EventReader, - mut commands: Commands, - player_entities: Query<(Entity, &PlayerId)>, -) { - for disconnection in disconnections.read() { - let client_id = disconnection.context(); - for (entity, player_id) in player_entities.iter() { - if player_id.0 == *client_id { - commands.entity(entity).despawn(); - } - } - } -} - // // The client input only gets applied to predicted entities that we own // // This works because we only predict the user's controlled entity. // // If we were predicting more entities, we would have to only apply movement to the player owned one. @@ -90,25 +68,35 @@ pub(crate) fn replicate_players( let entity = event.entity(); if let Some(mut e) = commands.get_entity(entity) { - let mut replicate = Replicate { - // we want to replicate back to the original client, since they are using a pre-spawned entity - replication_target: NetworkTarget::All, - // NOTE: even with a pre-spawned Predicted entity, we need to specify who will run prediction - prediction_target: NetworkTarget::Single(*client_id), - // we want the other clients to apply interpolation for the player - interpolation_target: NetworkTarget::AllExceptSingle(*client_id), + let replicate = Replicate { + target: ReplicationTarget { + // we want to replicate back to the original client, since they are using a pre-spawned entity + replication: NetworkTarget::All, + // NOTE: even with a pre-spawned Predicted entity, we need to specify who will run prediction + prediction: NetworkTarget::Single(*client_id), + // we want the other clients to apply interpolation for the player + interpolation: NetworkTarget::AllExceptSingle(*client_id), + }, + // let the server know that this entity is controlled by client `client_id` + // - the client will have a Controlled component for this entity when it's replicated + // - when the client disconnects, this entity will be despawned + controlled_by: ControlledBy { + target: NetworkTarget::Single(*client_id), + }, // make sure that all predicted entities (i.e. all entities for a given client) are part of the same replication group - replication_group: ReplicationGroup::new_id(client_id.to_bits()), + group: ReplicationGroup::new_id(client_id.to_bits()), ..default() }; - // We don't want to replicate the ActionState to the original client, since they are updating it with - // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, - // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! - // We also don't need the inputs of the other clients, because we are not predicting them - replicate.add_target::>(NetworkTarget::None); - // The PrePredicted component must be replicated only to the original client - replicate.add_target::(NetworkTarget::Single(*client_id)); - e.insert(replicate); + e.insert(( + replicate, + // We don't want to replicate the ActionState to the original client, since they are updating it with + // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, + // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! + // We also don't need the inputs of the other clients, because we are not predicting them + OverrideTargetComponent::>::new(NetworkTarget::None), + // The PrePredicted component must be replicated only to the original client + OverrideTargetComponent::::new(NetworkTarget::Single(*client_id)), + )); } } } diff --git a/examples/bullet_prespawn/src/settings.rs b/examples/bullet_prespawn/src/settings.rs deleted file mode 100644 index 2d16c54cd..000000000 --- a/examples/bullet_prespawn/src/settings.rs +++ /dev/null @@ -1,320 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: bevy::utils::Duration::from_millis(self.latency_ms as u64), - incoming_jitter: bevy::utils::Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// By how many ticks an input press will be delayed? - /// This can be useful as a tradeoff between input delay and prediction accuracy. - /// If the input delay is greater than the RTT, then there won't ever be any mispredictions/rollbacks. - /// See [this article](https://www.snapnet.dev/docs/core-concepts/input-delay-vs-rollback/) for more information. - pub(crate) input_delay_ticks: u16, - - /// If visual correction is enabled, we don't instantly snapback to the corrected position - /// when we need to rollback. Instead we interpolated between the current position and the - /// corrected position. - /// This controls the duration of the interpolation; the higher it is, the longer the interpolation - /// will take - pub(crate) correction_ticks_factor: f32, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.to_string().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/bullet_prespawn/src/shared.rs b/examples/bullet_prespawn/src/shared.rs index e4909ee03..3702f2348 100644 --- a/examples/bullet_prespawn/src/shared.rs +++ b/examples/bullet_prespawn/src/shared.rs @@ -3,9 +3,7 @@ use bevy::prelude::*; use bevy::render::RenderPlugin; use bevy::utils::Duration; use bevy_screen_diagnostics::{Aggregate, ScreenDiagnostics, ScreenDiagnosticsPlugin}; -use leafwing_input_manager::orientation::Orientation; use leafwing_input_manager::prelude::ActionState; -use tracing::Level; use lightyear::client::prediction::plugin::is_in_rollback; use lightyear::prelude::client::*; @@ -15,23 +13,9 @@ use lightyear::transport::io::IoDiagnosticsPlugin; use crate::protocol::*; -const FRAME_HZ: f64 = 60.0; -const FIXED_TIMESTEP_HZ: f64 = 64.0; - const EPS: f32 = 0.0001; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - server_send_interval: Duration::from_secs_f64(1.0 / 32.0), - // server_send_interval: Duration::from_millis(500), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / FIXED_TIMESTEP_HZ), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { @@ -154,7 +138,7 @@ fn player_movement( tick_manager: Res, mut player_query: Query< (&mut Transform, &ActionState, &PlayerId), - Or<(With, With)>, + Or<(With, With)>, >, ) { for (transform, action_state, player_id) in player_query.iter_mut() { @@ -210,7 +194,7 @@ pub(crate) fn move_bullet( // move predicted bullets With, // move server entities - With, + With, // move prespawned bullets With, )>, @@ -245,7 +229,7 @@ pub(crate) fn shoot_bullet( &ColorComponent, &mut ActionState, ), - Or<(With, With)>, + Or<(With, With)>, >, ) { let tick = tick_manager.tick(); @@ -283,13 +267,15 @@ pub(crate) fn shoot_bullet( // unless you set the hash manually before PostUpdate to a value of your choice PreSpawnedPlayerObject::default(), Replicate { - replication_target: NetworkTarget::All, - // the bullet is predicted for the client who shot it - prediction_target: NetworkTarget::Single(id.0), - // the bullet is interpolated for other clients - interpolation_target: NetworkTarget::AllExceptSingle(id.0), + target: ReplicationTarget { + replication: NetworkTarget::All, + // the bullet is predicted for the client who shot it + prediction: NetworkTarget::Single(id.0), + // the bullet is interpolated for other clients + interpolation: NetworkTarget::AllExceptSingle(id.0), + }, // NOTE: all predicted entities need to have the same replication group - replication_group: ReplicationGroup::new_id(id.0.to_bits()), + group: ReplicationGroup::new_id(id.0.to_bits()), ..default() }, )); diff --git a/examples/client_replication/Cargo.toml b/examples/client_replication/Cargo.toml index 1b8ef313b..489994557 100644 --- a/examples/client_replication/Cargo.toml +++ b/examples/client_replication/Cargo.toml @@ -12,18 +12,16 @@ categories = ["game-development", "network-programming"] license = "MIT OR Apache-2.0" publish = false - [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } lightyear = { path = "../../lightyear", features = [ "webtransport", "websocket", "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" @@ -31,9 +29,4 @@ tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/client_replication/src/client.rs b/examples/client_replication/src/client.rs index 345fceb76..d359117ae 100644 --- a/examples/client_replication/src/client.rs +++ b/examples/client_replication/src/client.rs @@ -1,17 +1,11 @@ -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; - -pub use lightyear::prelude::client::*; +use lightyear::prelude::client::*; use lightyear::prelude::*; use crate::protocol::Direction; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; -use crate::{shared, ClientTransports, SharedSettings}; +use crate::shared::{color_from_id, shared_movement_behaviour}; pub struct ExampleClientPlugin; @@ -62,6 +56,7 @@ pub(crate) fn handle_connection( ..default() }, )); + info!("Spawning local cursor"); // spawn a local cursor which will be replicated to other clients, but remain client-authoritative. commands.spawn(CursorBundle::new( client_id, diff --git a/examples/client_replication/src/main.rs b/examples/client_replication/src/main.rs index 9f8b5e692..c5edb6e7d 100644 --- a/examples/client_replication/src/main.rs +++ b/examples/client_replication/src/main.rs @@ -1,270 +1,29 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -//! Run with -//! - `cargo run -- server` -//! - `cargo run -- client -c 1` -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{ - InterpolationConfig, InterpolationDelay, NetConfig, ReplicationConfig, -}; -use lightyear::prelude::{Mode, TransportConfig}; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::Settings; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); - } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); - } - } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - replication: ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - replication: lightyear::server::replication::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - SharedPlugin, - )); - app -} - -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - replication: lightyear::server::replication::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - replication: ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + let settings = common::settings::settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings, cli); + // add `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins(ExampleClientPlugin, ExampleServerPlugin, SharedPlugin); + // run the app + app.run(); } diff --git a/examples/client_replication/src/protocol.rs b/examples/client_replication/src/protocol.rs index bb0b4302c..a95551cbf 100644 --- a/examples/client_replication/src/protocol.rs +++ b/examples/client_replication/src/protocol.rs @@ -27,9 +27,11 @@ impl PlayerBundle { position: PlayerPosition(position), color: PlayerColor(color), replicate: Replicate { - // prediction_target: NetworkTarget::None, - prediction_target: NetworkTarget::Only(vec![id]), - interpolation_target: NetworkTarget::AllExcept(vec![id]), + target: ReplicationTarget { + prediction: NetworkTarget::Single(id), + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, ..default() }, } @@ -52,8 +54,10 @@ impl CursorBundle { position: CursorPosition(position), color: PlayerColor(color), replicate: Replicate { - replication_target: NetworkTarget::All, - interpolation_target: NetworkTarget::AllExcept(vec![id]), + target: ReplicationTarget { + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, ..default() }, } @@ -134,23 +138,23 @@ impl Plugin for ProtocolPlugin { // inputs app.add_plugins(InputPlugin::::default()); // components - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_linear_interpolation_fn::(); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_linear_interpolation_fn::(); + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); // channels app.add_channel::(ChannelSettings { mode: ChannelMode::OrderedReliable(ReliableSettings::default()), diff --git a/examples/client_replication/src/server.rs b/examples/client_replication/src/server.rs index 461dd3dc7..72a72a339 100644 --- a/examples/client_replication/src/server.rs +++ b/examples/client_replication/src/server.rs @@ -1,19 +1,15 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; use lightyear::client::components::Confirmed; use lightyear::client::interpolation::Interpolated; use lightyear::client::prediction::Predicted; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; +use crate::shared::{color_from_id, shared_movement_behaviour}; // Plugin for server-specific logic pub struct ExampleServerPlugin; @@ -28,7 +24,6 @@ impl Plugin for ExampleServerPlugin { ); // the physics/FixedUpdates systems that consume inputs should be run in this set app.add_systems(FixedUpdate, (movement, delete_player)); - app.add_systems(Update, handle_disconnections); } } @@ -50,22 +45,6 @@ pub(crate) fn init(mut commands: Commands) { ); } -/// Server disconnection system, delete all player entities upon disconnection -pub(crate) fn handle_disconnections( - mut disconnections: EventReader, - mut commands: Commands, - player_entities: Query<(Entity, &PlayerId)>, -) { - for disconnection in disconnections.read() { - let client_id = disconnection.context(); - for (entity, player_id) in player_entities.iter() { - if player_id.0 == *client_id { - commands.entity(entity).despawn(); - } - } - } -} - /// Read client inputs and move players pub(crate) fn movement( mut position_query: Query<(&mut PlayerPosition, &PlayerId)>, @@ -138,21 +117,24 @@ pub(crate) fn replicate_players( // for all cursors we have received, add a Replicate component so that we can start replicating it // to other clients if let Some(mut e) = commands.get_entity(entity) { - let mut replicate = Replicate { - // we want to replicate back to the original client, since they are using a pre-spawned entity - replication_target: NetworkTarget::All, - // NOTE: even with a pre-spawned Predicted entity, we need to specify who will run prediction - // NOTE: Be careful to not override the pre-spawned prediction! we do not need to enable prediction - // because there is a pre-spawned predicted entity - prediction_target: NetworkTarget::Only(vec![*client_id]), - // we want the other clients to apply interpolation for the player - interpolation_target: NetworkTarget::AllExcept(vec![*client_id]), + let replicate = Replicate { + target: ReplicationTarget { + // we want to replicate back to the original client, since they are using a pre-spawned entity + replication: NetworkTarget::All, + // NOTE: even with a pre-spawned Predicted entity, we need to specify who will run prediction + prediction: NetworkTarget::Only(vec![*client_id]), + // we want the other clients to apply interpolation for the player + interpolation: NetworkTarget::AllExceptSingle(*client_id), + ..default() + }, ..default() }; - // if we receive a pre-predicted entity, only send the prepredicted component back - // to the original client - replicate.add_target::(NetworkTarget::Single(*client_id)); - e.insert(replicate); + e.insert(( + replicate, + // if we receive a pre-predicted entity, only send the prepredicted component back + // to the original client + OverrideTargetComponent::::new(NetworkTarget::Single(*client_id)), + )); } } } @@ -170,10 +152,13 @@ pub(crate) fn replicate_cursors( // to other clients if let Some(mut e) = commands.get_entity(entity) { e.insert(Replicate { - // do not replicate back to the owning entity! - replication_target: NetworkTarget::AllExcept(vec![*client_id]), - // we want the other clients to apply interpolation for the cursor - interpolation_target: NetworkTarget::AllExcept(vec![*client_id]), + target: ReplicationTarget { + // do not replicate back to the client that owns the cursor! + replication: NetworkTarget::AllExceptSingle(*client_id), + // we want the other clients to apply interpolation for the cursor + interpolation: NetworkTarget::AllExceptSingle(*client_id), + ..default() + }, ..default() }); } diff --git a/examples/client_replication/src/settings.rs b/examples/client_replication/src/settings.rs deleted file mode 100644 index c64c8bdb6..000000000 --- a/examples/client_replication/src/settings.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: bevy::utils::Duration::from_millis(self.latency_ms as u64), - incoming_jitter: bevy::utils::Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.to_string().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/client_replication/src/shared.rs b/examples/client_replication/src/shared.rs index e193ee894..d27511050 100644 --- a/examples/client_replication/src/shared.rs +++ b/examples/client_replication/src/shared.rs @@ -7,18 +7,7 @@ use lightyear::prelude::*; use crate::protocol::*; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - server_send_interval: Duration::from_millis(40), - // server_send_interval: Duration::from_millis(100), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/examples/common/Cargo.toml b/examples/common/Cargo.toml new file mode 100644 index 000000000..7e19b26d2 --- /dev/null +++ b/examples/common/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2021" +description = "Common harness for the lightyear examples" + +[dependencies] +lightyear = { path = "../../lightyear", features = [ + "steam", + "webtransport", + "websocket", +] } + +# utils +anyhow = { version = "1.0.75", features = [] } +async-compat = "0.2.3" +cfg-if = "1.0.0" +clap = { version = "4.5.4", features = ["derive"] } +crossbeam-channel = "0.5.12" +serde = { version = "1.0.201", features = ["derive"] } + + +# bevy +bevy = { version = "0.13" } +bevy-inspector-egui = "0.24" diff --git a/examples/common/src/app.rs b/examples/common/src/app.rs new file mode 100644 index 000000000..8f42f781f --- /dev/null +++ b/examples/common/src/app.rs @@ -0,0 +1,348 @@ +//! This example showcases how to use Lightyear with Bevy, to easily get replication along with prediction/interpolation working. +//! +//! There is a lot of setup code, but it's mostly to have the examples work in all possible configurations of transport. +//! (all transports are supported, as well as running the example in listen-server or host-server mode) +//! +//! +//! Run with +//! - `cargo run -- server` +//! - `cargo run -- client -c 1` +#![allow(unused_imports)] +#![allow(unused_variables)] +#![allow(dead_code)] + +use std::net::SocketAddr; +use std::str::FromStr; + +use bevy::asset::ron; +use bevy::log::{Level, LogPlugin}; +use bevy::prelude::*; +use bevy::DefaultPlugins; +use bevy_inspector_egui::quick::WorldInspectorPlugin; +use clap::{Parser, ValueEnum}; +use lightyear::prelude::client::ClientConfig; +use lightyear::prelude::*; +use lightyear::prelude::{client, server}; +use lightyear::server::config::ServerConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; +use serde::{Deserialize, Serialize}; + +use crate::settings::*; +use crate::shared::shared_config; + +#[derive(Parser, PartialEq, Debug)] +pub enum Cli { + /// We have the client and the server running inside the same app. + /// The server will also act as a client. + #[cfg(not(target_family = "wasm"))] + HostServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// We will create two apps: a client app and a server app. + /// Data gets passed between the two via channels. + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server + Server, + /// The program will act as a client + Client { + #[arg(short, long, default_value = None)] + client_id: Option, + }, +} + +/// Pars the CLI arguments. +/// `clap` doesn't run in wasm; we simply run in Client mode with a random ClientId +pub fn cli() -> Cli { + cfg_if::cfg_if! { + if #[cfg(target_family = "wasm")] { + let client_id = rand::random::(); + Cli::Client { + client_id: Some(client_id) + } + } else { + Cli::parse() + } + } +} + +/// Apps that will be returned from the `build_apps` function +pub enum Apps { + /// A single app that contains only the ClientPlugins + Client { app: App, config: ClientConfig }, + /// A single app that contains only the ServerPlugins + Server { app: App, config: ServerConfig }, + /// Two apps (Client and Server) that will run in separate threads + ListenServer { + client_app: App, + client_config: ClientConfig, + server_app: App, + server_config: ServerConfig, + }, + /// A single app that contains both the Client and Server plugins + HostServer { + app: App, + client_config: ClientConfig, + server_config: ServerConfig, + }, +} + +impl Apps { + /// Add the [`ClientPlugins`] and [`ServerPlugins`] plugin groups to the app + pub fn add_lightyear_plugin_groups(&mut self) { + match self { + Apps::Client { app, config } => { + app.add_plugins(client::ClientPlugins { + config: config.clone(), + }); + } + Apps::Server { app, config } => { + app.add_plugins(server::ServerPlugins { + config: config.clone(), + }); + } + Apps::ListenServer { + client_app, + server_app, + client_config, + server_config, + } => { + client_app.add_plugins(client::ClientPlugins { + config: client_config.clone(), + }); + server_app.add_plugins(server::ServerPlugins { + config: server_config.clone(), + }); + } + Apps::HostServer { + app, + client_config, + server_config, + } => { + // TODO: currently we need ServerPlugins to run first, because it adds the + // SharedPlugins. not ideal + app.add_plugins(client::ClientPlugins { + config: client_config.clone(), + }); + app.add_plugins(server::ServerPlugins { + config: server_config.clone(), + }); + } + } + } + + /// Add the client, server, and shared plugins to the app + pub fn add_plugins( + &mut self, + client_plugin: impl Plugin, + server_plugin: impl Plugin, + shared_plugin: impl Plugin + Clone, + ) { + match self { + Apps::Client { app, .. } => { + app.add_plugins((client_plugin, shared_plugin)); + } + Apps::Server { app, .. } => { + app.add_plugins((server_plugin, shared_plugin)); + } + Apps::ListenServer { + client_app, + server_app, + .. + } => { + client_app.add_plugins((client_plugin, shared_plugin.clone())); + server_app.add_plugins((server_plugin, shared_plugin)); + } + Apps::HostServer { app, .. } => { + app.add_plugins((client_plugin, server_plugin, shared_plugin)); + } + } + } + + /// Start running the apps + pub fn run(self) { + match self { + Apps::Client { mut app, .. } => app.run(), + Apps::Server { mut app, .. } => app.run(), + Apps::ListenServer { + mut client_app, + mut server_app, + .. + } => { + std::thread::spawn(move || server_app.run()); + client_app.run(); + } + Apps::HostServer { mut app, .. } => { + app.run(); + } + } + } +} + +/// Build the bevy App. +pub fn build_app(settings: Settings, cli: Cli) -> Apps { + match cli { + // ListenServer using a single app + #[cfg(not(target_family = "wasm"))] + Cli::HostServer { client_id } => { + let client_net_config = client::NetConfig::Local { + id: client_id.unwrap_or(settings.client.client_id), + }; + let (app, client_config, server_config) = + combined_app(settings, vec![], client_net_config); + Apps::HostServer { + app, + client_config, + server_config, + } + } + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + // we will communicate between the client and server apps via channels + let transport_config = client::ClientTransport::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + let net_config = build_client_netcode_config( + client_id.unwrap_or(settings.client.client_id), + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + LOCAL_SOCKET, + settings.client.conditioner.as_ref(), + &settings.shared, + transport_config, + ); + let (client_app, client_config) = client_app(settings.clone(), net_config); + + // create server app + let extra_transport_configs = vec![server::ServerTransport::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let (server_app, server_config) = server_app(settings, extra_transport_configs); + Apps::ListenServer { + client_app, + client_config, + server_app, + server_config, + } + } + #[cfg(not(target_family = "wasm"))] + Cli::Server => { + let (app, config) = server_app(settings, vec![]); + Apps::Server { app, config } + } + Cli::Client { client_id } => { + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + // use the cli-provided client id if it exists, otherwise use the settings client id + let client_id = client_id.unwrap_or(settings.client.client_id); + let net_config = get_client_net_config(&settings, client_id); + let (app, config) = client_app(settings, net_config); + Apps::Client { app, config } + } + } +} + +/// Build the client app with the `ClientPlugins` added. +/// Takes in a `net_config` parameter so that we configure the network transport. +fn client_app(settings: Settings, net_config: client::NetConfig) -> (App, ClientConfig) { + let mut app = App::new(); + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_config = client::ClientConfig { + shared: shared_config(Mode::Separate), + net: net_config, + ..default() + }; + (app, client_config) +} + +/// Build the server app with the `ServerPlugins` added. +#[cfg(not(target_family = "wasm"))] +fn server_app( + settings: Settings, + extra_transport_configs: Vec, +) -> (App, ServerConfig) { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + // configure the network configuration + let mut net_configs = get_server_net_configs(&settings); + let extra_net_configs = extra_transport_configs.into_iter().map(|c| { + build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) + }); + net_configs.extend(extra_net_configs); + let server_config = ServerConfig { + shared: shared_config(Mode::Separate), + net: net_configs, + ..default() + }; + (app, server_config) +} + +/// An `App` that contains both the client and server plugins +#[cfg(not(target_family = "wasm"))] +fn combined_app( + settings: Settings, + extra_transport_configs: Vec, + client_net_config: client::NetConfig, +) -> (App, ClientConfig, ServerConfig) { + let mut app = App::new(); + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + + // server config + let mut net_configs = get_server_net_configs(&settings); + let extra_net_configs = extra_transport_configs.into_iter().map(|c| { + build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) + }); + net_configs.extend(extra_net_configs); + let server_config = server::ServerConfig { + shared: shared_config(Mode::HostServer), + net: net_configs, + ..default() + }; + + // client config + let client_config = client::ClientConfig { + shared: shared_config(Mode::HostServer), + net: client_net_config, + ..default() + }; + (app, client_config, server_config) +} diff --git a/examples/common/src/lib.rs b/examples/common/src/lib.rs new file mode 100644 index 000000000..3948b0e70 --- /dev/null +++ b/examples/common/src/lib.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod settings; +pub mod shared; diff --git a/examples/replication_groups/src/settings.rs b/examples/common/src/settings.rs similarity index 87% rename from examples/replication_groups/src/settings.rs rename to examples/common/src/settings.rs index 2e6334024..f4b353f5c 100644 --- a/examples/replication_groups/src/settings.rs +++ b/examples/common/src/settings.rs @@ -1,19 +1,26 @@ //! This module parses the settings.ron file and builds a lightyear configuration from it +#![allow(unused_variables)] use std::net::{Ipv4Addr, SocketAddr}; use async_compat::Compat; +use bevy::asset::ron; +use bevy::prelude::Resource; use bevy::tasks::IoTaskPool; use bevy::utils::Duration; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use lightyear::prelude::client::Authentication; #[cfg(not(target_family = "wasm"))] use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; +use lightyear::prelude::{CompressionConfig, LinkConditionerConfig}; -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; +use lightyear::prelude::{client, server}; + +/// We parse the settings.ron file to read the settings +pub fn settings(settings_str: &str) -> T { + ron::de::from_str::(settings_str).expect("Could not deserialize the settings file") +} #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -40,6 +47,7 @@ pub enum ServerTransports { WebSocket { local_port: u16, }, + #[cfg(not(target_family = "wasm"))] Steam { app_id: u32, server_ip: Ipv4Addr, @@ -80,7 +88,7 @@ pub struct ServerSettings { pub(crate) conditioner: Option, /// Which transport to use - pub(crate) transport: Vec, + pub transport: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -95,10 +103,10 @@ pub struct ClientSettings { pub(crate) client_port: u16, /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, + pub server_addr: Ipv4Addr, /// The port of the server - pub(crate) server_port: u16, + pub server_port: u16, /// Which transport to use pub(crate) transport: ClientTransports, @@ -110,16 +118,16 @@ pub struct ClientSettings { #[derive(Copy, Clone, Debug, Deserialize, Serialize)] pub struct SharedSettings { /// An id to identify the protocol version - pub(crate) protocol_id: u64, + pub protocol_id: u64, /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], + pub private_key: [u8; 32], /// compression options pub(crate) compression: CompressionConfig, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Resource, Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -129,7 +137,7 @@ pub struct Settings { pub fn build_server_netcode_config( conditioner: Option<&Conditioner>, shared: &SharedSettings, - transport_config: TransportConfig, + transport_config: server::ServerTransport, ) -> server::NetConfig { let conditioner = conditioner.map_or(None, |c| { Some(LinkConditionerConfig { @@ -141,7 +149,7 @@ pub fn build_server_netcode_config( let netcode_config = server::NetcodeConfig::default() .with_protocol_id(shared.protocol_id) .with_key(shared.private_key); - let io_config = IoConfig { + let io_config = server::IoConfig { transport: transport_config, conditioner, compression: shared.compression, @@ -164,7 +172,7 @@ pub fn get_server_net_configs(settings: &Settings) -> Vec { ServerTransports::Udp { local_port } => build_server_netcode_config( settings.server.conditioner.as_ref(), &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( + server::ServerTransport::UdpSocket(SocketAddr::new( Ipv4Addr::UNSPECIFIED.into(), *local_port, )), @@ -175,7 +183,7 @@ pub fn get_server_net_configs(settings: &Settings) -> Vec { let certificate = IoTaskPool::get() .scope(|s| { s.spawn(Compat::new(async { - Identity::load_pemfiles( + server::Identity::load_pemfiles( "../certificates/cert.pem", "../certificates/key.pem", ) @@ -190,7 +198,7 @@ pub fn get_server_net_configs(settings: &Settings) -> Vec { build_server_netcode_config( settings.server.conditioner.as_ref(), &settings.shared, - TransportConfig::WebTransportServer { + server::ServerTransport::WebTransportServer { server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), certificate, }, @@ -199,7 +207,7 @@ pub fn get_server_net_configs(settings: &Settings) -> Vec { ServerTransports::WebSocket { local_port } => build_server_netcode_config( settings.server.conditioner.as_ref(), &settings.shared, - TransportConfig::WebSocketServer { + server::ServerTransport::WebSocketServer { server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), }, ), @@ -233,7 +241,7 @@ pub fn build_client_netcode_config( server_addr: SocketAddr, conditioner: Option<&Conditioner>, shared: &SharedSettings, - transport_config: TransportConfig, + transport_config: client::ClientTransport, ) -> client::NetConfig { let conditioner = conditioner.map_or(None, |c| Some(c.build())); let auth = Authentication::Manual { @@ -243,7 +251,7 @@ pub fn build_client_netcode_config( protocol_id: shared.protocol_id, }; let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { + let io_config = client::IoConfig { transport: transport_config, conditioner, compression: shared.compression, @@ -270,14 +278,14 @@ pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::Net server_addr, settings.client.conditioner.as_ref(), &settings.shared, - TransportConfig::UdpSocket(client_addr), + client::ClientTransport::UdpSocket(client_addr), ), ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( client_id, server_addr, settings.client.conditioner.as_ref(), &settings.shared, - TransportConfig::WebTransportClient { + client::ClientTransport::WebTransportClient { client_addr, server_addr, #[cfg(target_family = "wasm")] @@ -289,7 +297,7 @@ pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::Net server_addr, settings.client.conditioner.as_ref(), &settings.shared, - TransportConfig::WebSocketClient { server_addr }, + client::ClientTransport::WebSocketClient { server_addr }, ), #[cfg(not(target_family = "wasm"))] ClientTransports::Steam { app_id } => client::NetConfig::Steam { diff --git a/examples/common/src/shared.rs b/examples/common/src/shared.rs new file mode 100644 index 000000000..17b301b17 --- /dev/null +++ b/examples/common/src/shared.rs @@ -0,0 +1,17 @@ +use lightyear::prelude::{Mode, SharedConfig, TickConfig}; +use std::time::Duration; + +pub const FIXED_TIMESTEP_HZ: f64 = 64.0; + +/// The [`SharedConfig`] must be shared between the `ClientConfig` and `ServerConfig` +pub fn shared_config(mode: Mode) -> SharedConfig { + SharedConfig { + client_send_interval: Duration::default(), + // send an update every 100ms + server_send_interval: Duration::from_millis(100), + tick: TickConfig { + tick_duration: Duration::from_secs_f64(1.0 / FIXED_TIMESTEP_HZ), + }, + mode, + } +} diff --git a/examples/interest_management/Cargo.toml b/examples/interest_management/Cargo.toml index fde0d9235..034aa80c1 100644 --- a/examples/interest_management/Cargo.toml +++ b/examples/interest_management/Cargo.toml @@ -14,9 +14,9 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } leafwing-input-manager = "0.13" lightyear = { path = "../../lightyear", features = [ "webtransport", @@ -24,17 +24,11 @@ lightyear = { path = "../../lightyear", features = [ "leafwing", "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } derive_more = { version = "0.99", features = ["add", "mul"] } -rand = "0.8.5" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" +rand = "0.8.1" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/interest_management/assets/settings.ron b/examples/interest_management/assets/settings.ron index 3aed79125..2d7f41269 100644 --- a/examples/interest_management/assets/settings.ron +++ b/examples/interest_management/assets/settings.ron @@ -27,11 +27,7 @@ Settings( server: ServerSettings( headless: true, inspector: false, - conditioner: Some(Conditioner( - latency_ms: 200, - jitter_ms: 20, - packet_loss: 0.05 - )), + conditioner: None, transport: [ WebTransport( local_port: 5000 diff --git a/examples/interest_management/src/main.rs b/examples/interest_management/src/main.rs index bccc35c09..c5edb6e7d 100644 --- a/examples/interest_management/src/main.rs +++ b/examples/interest_management/src/main.rs @@ -1,255 +1,29 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -//! Run with -//! - `cargo run -- server` -//! - `cargo run -- client -c 1` -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::utils::Duration; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{InterpolationConfig, InterpolationDelay, NetConfig}; -use lightyear::prelude::{Mode, TransportConfig}; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::Settings; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); - } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); - } - } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - interpolation: InterpolationConfig::default().with_delay( - InterpolationDelay::default() - .with_min_delay(Duration::from_millis(50)) - .with_send_interval_ratio(2.0), - ), - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - SharedPlugin, - )); - app -} - -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig::default().with_delay( - InterpolationDelay::default() - .with_min_delay(Duration::from_millis(50)) - .with_send_interval_ratio(2.0), - ), - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + let settings = common::settings::settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings, cli); + // add `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins(ExampleClientPlugin, ExampleServerPlugin, SharedPlugin); + // run the app + app.run(); } diff --git a/examples/interest_management/src/protocol.rs b/examples/interest_management/src/protocol.rs index c4ed88ade..325019b5e 100644 --- a/examples/interest_management/src/protocol.rs +++ b/examples/interest_management/src/protocol.rs @@ -13,7 +13,7 @@ use tracing::info; use lightyear::client::components::ComponentSyncMode; use lightyear::prelude::*; -use lightyear::shared::replication::components::ReplicationMode; +use lightyear::shared::replication::components::VisibilityMode; use UserAction; use crate::shared::color_from_id; @@ -26,31 +26,37 @@ pub(crate) struct PlayerBundle { color: PlayerColor, replicate: Replicate, action_state: ActionState, + action_state_override_target: OverrideTargetComponent>, } impl PlayerBundle { pub(crate) fn new(id: ClientId, position: Vec2) -> Self { let color = color_from_id(id); - let mut replicate = Replicate { - prediction_target: NetworkTarget::Only(vec![id]), - interpolation_target: NetworkTarget::AllExcept(vec![id]), + let replicate = Replicate { + target: ReplicationTarget { + prediction: NetworkTarget::Single(id), + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, + controlled_by: ControlledBy { + target: NetworkTarget::Single(id), + }, // use rooms for replication - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..default() }; - // We don't want to replicate the ActionState to the original client, since they are updating it with - // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, - // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! - replicate.add_target::>(NetworkTarget::AllExceptSingle(id)); - // // we don't want to replicate the ActionState from the server to client, because then the action-state - // // will keep getting replicated from confirmed to predicted and will interfere with our inputs - // replicate.disable_component::>(); Self { id: PlayerId(id), position: Position(position), color: PlayerColor(color), replicate, action_state: ActionState::default(), + // We don't want to replicate the ActionState to the original client, since they are updating it with + // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, + // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! + action_state_override_target: OverrideTargetComponent::new( + NetworkTarget::AllExceptSingle(id), + ), } } pub(crate) fn get_input_map() -> InputMap { @@ -124,22 +130,22 @@ impl Plugin for ProtocolPlugin { // inputs app.add_plugins(LeafwingInputPlugin::::default()); // components - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_linear_interpolation_fn::(); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); // channels app.add_channel::(ChannelSettings { mode: ChannelMode::OrderedReliable(ReliableSettings::default()), diff --git a/examples/interest_management/src/server.rs b/examples/interest_management/src/server.rs index eee67ba21..24bafb845 100644 --- a/examples/interest_management/src/server.rs +++ b/examples/interest_management/src/server.rs @@ -1,21 +1,18 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; +use bevy::utils::HashMap; use leafwing_input_manager::prelude::{ActionState, InputMap}; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; +use crate::shared::{color_from_id, shared_movement_behaviour}; const GRID_SIZE: f32 = 200.0; const NUM_CIRCLES: i32 = 10; -const INTEREST_RADIUS: f32 = 200.0; +const INTEREST_RADIUS: f32 = 150.0; // Special room for the player entities (so that all player entities always see each other) const PLAYER_ROOM: RoomId = RoomId(6000); @@ -31,7 +28,13 @@ impl Plugin for ExampleServerPlugin { app.add_systems(FixedUpdate, movement); app.add_systems( Update, - (handle_connections, interest_management, receive_message), + ( + handle_connections, + // we don't have to run interest management every tick, only every time + // the server is ready to send packets + interest_management.in_set(MainSet::Send), + receive_message, + ), ); } } @@ -67,7 +70,7 @@ pub(crate) fn init(mut commands: Commands) { CircleMarker, Replicate { // use rooms for replication - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..default() }, )); @@ -79,29 +82,17 @@ pub(crate) fn init(mut commands: Commands) { pub(crate) fn handle_connections( mut room_manager: ResMut, mut connections: EventReader, - mut disconnections: EventReader, - mut global: ResMut, mut commands: Commands, ) { for connection in connections.read() { - let client_id = *connection.context(); + let client_id = connection.client_id; let entity = commands.spawn(PlayerBundle::new(client_id, Vec2::ZERO)); - // Add a mapping from client id to entity id (so that when we receive an input from a client, - // we know which entity to move) - global.client_id_to_entity_id.insert(client_id, entity.id()); - // we will create a room for each client. To keep things simple, the room id will be the client id - let room_id = client_id.into(); - room_manager.room_mut(room_id).add_client(client_id); - room_manager.room_mut(PLAYER_ROOM).add_client(client_id); - // also add the player entity to that room (so that the client can always see their own player) - room_manager.room_mut(room_id).add_entity(entity.id()); - room_manager.room_mut(PLAYER_ROOM).add_entity(entity.id()); - } - for disconnection in disconnections.read() { - let client_id = disconnection.context(); - if let Some(entity) = global.client_id_to_entity_id.remove(client_id) { - commands.entity(entity).despawn(); - } + + // we can control the player visibility in a more static manner by using rooms + // we add all clients to a room, as well as all player entities + // this means that all clients will be able to see all player entities + room_manager.add_client(client_id, PLAYER_ROOM); + room_manager.add_entity(entity.id(), PLAYER_ROOM); } } @@ -111,27 +102,25 @@ pub(crate) fn receive_message(mut messages: EventReader>) } } -/// This is where we perform scope management: -/// - we will add/remove other entities from the player's room only if they are close +/// Here we perform more "immediate" interest management: we will make a circle visible to a client +/// depending on the distance to the client's entity pub(crate) fn interest_management( - mut room_manager: ResMut, - player_query: Query<(&PlayerId, Ref), (Without, With)>, - circle_query: Query<(Entity, &Position), (With, With)>, + mut visibility_manager: ResMut, + player_query: Query< + (&PlayerId, Ref), + (Without, With), + >, + circle_query: Query<(Entity, &Position), (With, With)>, ) { for (client_id, position) in player_query.iter() { if position.is_changed() { - let room_id = client_id.0.into(); - // let circles_in_room = server.room(room_id).entities(); - let mut room = room_manager.room_mut(room_id); + // in real game, you would have a spatial index (kd-tree) to only find entities within a certain radius for (circle_entity, circle_position) in circle_query.iter() { let distance = position.distance(**circle_position); if distance < INTEREST_RADIUS { - // add the circle to the player's room - room.add_entity(circle_entity) + visibility_manager.gain_visibility(client_id.0, circle_entity); } else { - // if circles_in_room.contains(&circle_entity) { - room.remove_entity(circle_entity); - // } + visibility_manager.lose_visibility(client_id.0, circle_entity); } } } diff --git a/examples/interest_management/src/settings.rs b/examples/interest_management/src/settings.rs deleted file mode 100644 index c64c8bdb6..000000000 --- a/examples/interest_management/src/settings.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: bevy::utils::Duration::from_millis(self.latency_ms as u64), - incoming_jitter: bevy::utils::Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.to_string().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/interest_management/src/shared.rs b/examples/interest_management/src/shared.rs index ebd1d90a0..ad2f2962b 100644 --- a/examples/interest_management/src/shared.rs +++ b/examples/interest_management/src/shared.rs @@ -10,20 +10,7 @@ use lightyear::prelude::*; use crate::protocol::*; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - // server_send_interval: Duration::default(), - server_send_interval: Duration::from_millis(40), - tick: TickConfig { - // right now, we NEED the tick_duration to be smaller than the send_interval - // (otherwise we can send multiple packets for the same tick at different frames) - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/examples/leafwing_inputs/Cargo.toml b/examples/leafwing_inputs/Cargo.toml index 6ec5ad129..eb4215204 100644 --- a/examples/leafwing_inputs/Cargo.toml +++ b/examples/leafwing_inputs/Cargo.toml @@ -8,9 +8,9 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } bevy_screen_diagnostics = "0.5.0" leafwing-input-manager = "0.13" bevy_xpbd_2d = { version = "0.4", features = ["serialize"] } @@ -21,7 +21,6 @@ lightyear = { path = "../../lightyear", features = [ "xpbd_2d", "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" @@ -29,9 +28,4 @@ tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/leafwing_inputs/assets/settings.ron b/examples/leafwing_inputs/assets/settings.ron index 458d70b81..69f08f1c1 100644 --- a/examples/leafwing_inputs/assets/settings.ron +++ b/examples/leafwing_inputs/assets/settings.ron @@ -1,57 +1,59 @@ -Settings( +MySettings( + input_delay_ticks: 0, + correction_ticks_factor: 1.5, + predict_all: true, + common: Settings( client: ClientSettings( - inspector: true, - client_id: 0, - client_port: 0, // the OS will assign a random open port - server_addr: "127.0.0.1", - input_delay_ticks: 0, - correction_ticks_factor: 1.5, - conditioner: Some(Conditioner( - latency_ms: 75, - jitter_ms: 10, - packet_loss: 0.02 - )), - server_port: 5000, - transport: WebTransport( - // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks - // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", - ), - // server_port: 5001, - // transport: Udp, - // server_port: 5002, - // transport: WebSocket, - // server_port: 5003, - // transport: Steam( - // app_id: 480, - // ) - ), - server: ServerSettings( - headless: true, - inspector: false, - predict_all: true, - conditioner: None, - transport: [ - WebTransport( - local_port: 5000 - ), - Udp( - local_port: 5001 - ), - WebSocket( - local_port: 5002 + inspector: true, + client_id: 0, + client_port: 0, // the OS will assign a random open port + server_addr: "127.0.0.1", + conditioner: Some(Conditioner( + latency_ms: 75, + jitter_ms: 10, + packet_loss: 0.02 + )), + server_port: 5000, + transport: WebTransport( + // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks + // the server will print the certificate digest on startup + certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", ), - // Steam( + // server_port: 5001, + // transport: Udp, + // server_port: 5002, + // transport: WebSocket, + // server_port: 5003, + // transport: Steam( // app_id: 480, - // server_ip: "0.0.0.0", - // game_port: 5003, - // query_port: 27016, - // ), - ], - ), - shared: SharedSettings( - protocol_id: 0, - private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - compression: None, + // ) + ), + server: ServerSettings( + headless: true, + inspector: false, + conditioner: None, + transport: [ + WebTransport( + local_port: 5000 + ), + Udp( + local_port: 5001 + ), + WebSocket( + local_port: 5002 + ), + // Steam( + // app_id: 480, + // server_ip: "0.0.0.0", + // game_port: 5003, + // query_port: 27016, + // ), + ], + ), + shared: SharedSettings( + protocol_id: 0, + private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + compression: None, + ) ) ) diff --git a/examples/leafwing_inputs/src/client.rs b/examples/leafwing_inputs/src/client.rs index e1eb95600..a83d6b387 100644 --- a/examples/leafwing_inputs/src/client.rs +++ b/examples/leafwing_inputs/src/client.rs @@ -1,21 +1,14 @@ -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; - use bevy::app::PluginGroupBuilder; -use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings}; use bevy::prelude::*; use bevy::utils::Duration; -use bevy_xpbd_2d::parry::shape::ShapeType::Ball; use bevy_xpbd_2d::prelude::*; use leafwing_input_manager::prelude::*; - -use lightyear::inputs::native::input_buffer::InputBuffer; -pub use lightyear::prelude::client::*; +use lightyear::prelude::client::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour, FixedSet}; -use crate::{shared, ClientTransports, SharedSettings}; +use crate::shared; +use crate::shared::{color_from_id, shared_movement_behaviour, FixedSet}; pub struct ExampleClientPlugin; diff --git a/examples/leafwing_inputs/src/main.rs b/examples/leafwing_inputs/src/main.rs index 1d65688de..db9568f63 100644 --- a/examples/leafwing_inputs/src/main.rs +++ b/examples/leafwing_inputs/src/main.rs @@ -1,286 +1,80 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -//! Run with -//! - `cargo run -- server` -//! - `cargo run -- client -c 1` -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{ - InterpolationConfig, InterpolationDelay, NetConfig, PredictionConfig, -}; -use lightyear::prelude::{Mode, TransportConfig}; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::{settings, Settings}; +use lightyear::prelude::client::PredictionConfig; +use serde::{Deserialize, Serialize}; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); + let settings = settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings.common, cli); + + // for this example, we will use input delay and a correction function + let prediction_config = PredictionConfig { + input_delay_ticks: settings.input_delay_ticks, + correction_ticks_factor: settings.correction_ticks_factor, + ..default() + }; + match &mut app { + Apps::Client { config, .. } => { + config.prediction = prediction_config; } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); + Apps::ListenServer { client_config, .. } => { + client_config.prediction = prediction_config; } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); + Apps::HostServer { client_config, .. } => { + client_config.prediction = prediction_config; } + _ => {} } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::default()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - prediction: PredictionConfig { - input_delay_ticks: settings.client.input_delay_ticks, - correction_ticks_factor: settings.client.correction_ticks_factor, - ..default() - }, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - replication: client::ReplicationConfig { - // enable send because we pre-spawn entities on the client - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), + // add `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins( ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - replication: lightyear::server::replication::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), ExampleServerPlugin { - predict_all: settings.server.predict_all, + predict_all: settings.predict_all, }, SharedPlugin, - )); - app + ); + // run the app + app.run(); } -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::default()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - replication: lightyear::server::replication::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin { - predict_all: settings.server.predict_all, - }, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - prediction: PredictionConfig { - input_delay_ticks: settings.client.input_delay_ticks, - correction_ticks_factor: settings.client.correction_ticks_factor, - ..default() - }, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - replication: client::ReplicationConfig { - // enable send because we pre-spawn entities on the client - enable_send: true, - enable_receive: true, - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MySettings { + pub common: Settings, + + /// If true, we will predict the client's entities, but also the ball and other clients' entities! + /// This is what is done by RocketLeague (see [video](https://www.youtube.com/watch?v=ueEmiDM94IE)) + /// + /// If false, we will predict the client's entities but simple interpolate everything else. + pub(crate) predict_all: bool, + + /// By how many ticks an input press will be delayed? + /// This can be useful as a tradeoff between input delay and prediction accuracy. + /// If the input delay is greater than the RTT, then there won't ever be any mispredictions/rollbacks. + /// See [this article](https://www.snapnet.dev/docs/core-concepts/input-delay-vs-rollback/) for more information. + pub(crate) input_delay_ticks: u16, + + /// If visual correction is enabled, we don't instantly snapback to the corrected position + /// when we need to rollback. Instead we interpolated between the current position and the + /// corrected position. + /// This controls the duration of the interpolation; the higher it is, the longer the interpolation + /// will take + pub(crate) correction_ticks_factor: f32, } diff --git a/examples/leafwing_inputs/src/protocol.rs b/examples/leafwing_inputs/src/protocol.rs index df6fb4eda..fa91881c2 100644 --- a/examples/leafwing_inputs/src/protocol.rs +++ b/examples/leafwing_inputs/src/protocol.rs @@ -42,13 +42,16 @@ impl PlayerBundle { position: Position(position), color: ColorComponent(color), replicate: Replicate { + target: ReplicationTarget { + // TODO: improve this! this should depend on the predict_all settings + // We still need to specify the interpolation/prediction target for this local entity + // in the case where we're running in HostServer mode + prediction: NetworkTarget::All, + ..default() + }, // NOTE (important): all entities that are being predicted need to be part of the same replication-group // so that all their updates are sent as a single message and are consistent (on the same tick) - replication_group: REPLICATION_GROUP, - // TODO: improve this! this should depend on the predict_all settings - // We still need to specify the interpolation/prediction target for this local entity - // in the case where we're running in HostServer mode - prediction_target: NetworkTarget::All, + group: REPLICATION_GROUP, ..default() }, physics: PhysicsBundle::player(), @@ -73,16 +76,19 @@ pub(crate) struct BallBundle { impl BallBundle { pub(crate) fn new(position: Vec2, color: Color, predicted: bool) -> Self { - let mut replicate = Replicate { - replication_target: NetworkTarget::All, - ..default() - }; + let mut replication_target = ReplicationTarget::default(); + let mut group = ReplicationGroup::default(); if predicted { - replicate.prediction_target = NetworkTarget::All; - replicate.replication_group = REPLICATION_GROUP; + replication_target.prediction = NetworkTarget::All; + group = REPLICATION_GROUP; } else { - replicate.interpolation_target = NetworkTarget::All; + replication_target.interpolation = NetworkTarget::All; } + let replicate = Replicate { + target: replication_target, + group, + ..default() + }; Self { position: Position(position), color: ColorComponent(color), @@ -165,37 +171,37 @@ impl Plugin for ProtocolPlugin { app.add_plugins(LeafwingInputPlugin::::default()); app.add_plugins(LeafwingInputPlugin::::default()); // components - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_interpolation_fn::(position::lerp); - app.add_correction_fn::(position::lerp); - - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_interpolation_fn::(rotation::lerp); - app.add_correction_fn::(rotation::lerp); + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_interpolation_fn(position::lerp) + .add_correction_fn(position::lerp); + + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_interpolation_fn(rotation::lerp) + .add_correction_fn(rotation::lerp); // NOTE: interpolation/correction is only needed for components that are visually displayed! // we still need prediction to be able to correctly predict the physics on the client - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full); - app.register_component::(ChannelDirection::Bidirectional); - app.add_prediction::(ComponentSyncMode::Full); + app.register_component::(ChannelDirection::Bidirectional) + .add_prediction(ComponentSyncMode::Full); // channels app.add_channel::(ChannelSettings { diff --git a/examples/leafwing_inputs/src/server.rs b/examples/leafwing_inputs/src/server.rs index 2c52676cc..046af474a 100644 --- a/examples/leafwing_inputs/src/server.rs +++ b/examples/leafwing_inputs/src/server.rs @@ -1,19 +1,15 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; +use bevy::utils::HashMap; use bevy_xpbd_2d::prelude::*; use leafwing_input_manager::prelude::*; - use lightyear::prelude::client::{Confirmed, Predicted}; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour, FixedSet}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; +use crate::shared::{color_from_id, shared_movement_behaviour, FixedSet}; // Plugin for server-specific logic pub struct ExampleServerPlugin { @@ -39,7 +35,6 @@ impl Plugin for ExampleServerPlugin { ); // the physics/FixedUpdates systems that consume inputs should be run in this set app.add_systems(FixedUpdate, movement.in_set(FixedSet::Main)); - app.add_systems(Update, handle_disconnections); } } @@ -73,22 +68,6 @@ fn init(mut commands: Commands, global: Res) { )); } -/// Server disconnection system, delete all player entities upon disconnection -pub(crate) fn handle_disconnections( - mut disconnections: EventReader, - mut commands: Commands, - player_entities: Query<(Entity, &PlayerId)>, -) { - for disconnection in disconnections.read() { - let client_id = disconnection.context(); - for (entity, player_id) in player_entities.iter() { - if player_id.0 == *client_id { - commands.entity(entity).despawn(); - } - } - } -} - /// Read client inputs and move players /// NOTE: this system can now be run in both client/server! pub(crate) fn movement( @@ -126,36 +105,38 @@ pub(crate) fn replicate_players( let entity = event.entity(); info!("received player spawn event: {:?}", event); - // for all cursors we have received, add a Replicate component so that we can start replicating it + // for all player entities we have received, add a Replicate component so that we can start replicating it // to other clients if let Some(mut e) = commands.get_entity(entity) { - let mut replicate = Replicate { - // we want to replicate back to the original client, since they are using a pre-predicted entity - replication_target: NetworkTarget::All, - // make sure that all entities that are predicted are part of the same replication group - replication_group: REPLICATION_GROUP, - ..default() - }; - // We don't want to replicate the ActionState to the original client, since they are updating it with - // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, - // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! - replicate.add_target::>(NetworkTarget::AllExceptSingle( - client_id, - )); - // if we receive a pre-predicted entity, only send the prepredicted component back - // to the original client - replicate.add_target::(NetworkTarget::Single(client_id)); + // we want to replicate back to the original client, since they are using a pre-predicted entity + let mut replication_target = ReplicationTarget::default(); + if global.predict_all { - replicate.prediction_target = NetworkTarget::All; - // // if we predict other players, we need to replicate their actions to all clients other than the original one - // // (the original client will apply the actions locally) - // replicate.disable_replicate_once::>(); + replication_target.prediction = NetworkTarget::All; } else { // we want the other clients to apply interpolation for the player - replicate.interpolation_target = NetworkTarget::AllExceptSingle(client_id); + replication_target.interpolation = NetworkTarget::AllExceptSingle(client_id); } + let replicate = Replicate { + target: replication_target, + controlled_by: ControlledBy { + target: NetworkTarget::Single(client_id), + }, + // make sure that all entities that are predicted are part of the same replication group + group: REPLICATION_GROUP, + ..default() + }; e.insert(( replicate, + // We don't want to replicate the ActionState to the original client, since they are updating it with + // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, + // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! + OverrideTargetComponent::>::new( + NetworkTarget::AllExceptSingle(client_id), + ), + // if we receive a pre-predicted entity, only send the prepredicted component back + // to the original client + OverrideTargetComponent::::new(NetworkTarget::Single(client_id)), // not all physics components are replicated over the network, so add them on the server as well PhysicsBundle::player(), )); diff --git a/examples/leafwing_inputs/src/settings.rs b/examples/leafwing_inputs/src/settings.rs deleted file mode 100644 index 4312d041c..000000000 --- a/examples/leafwing_inputs/src/settings.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: bevy::utils::Duration::from_millis(self.latency_ms as u64), - incoming_jitter: bevy::utils::Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// If true, we will predict the client's entities, but also the ball and other clients' entities! - /// This is what is done by RocketLeague (see [video](https://www.youtube.com/watch?v=ueEmiDM94IE)) - /// - /// If false, we will predict the client's entities but simple interpolate everything else. - pub(crate) predict_all: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// By how many ticks an input press will be delayed? - /// This can be useful as a tradeoff between input delay and prediction accuracy. - /// If the input delay is greater than the RTT, then there won't ever be any mispredictions/rollbacks. - /// See [this article](https://www.snapnet.dev/docs/core-concepts/input-delay-vs-rollback/) for more information. - pub(crate) input_delay_ticks: u16, - - /// If visual correction is enabled, we don't instantly snapback to the corrected position - /// when we need to rollback. Instead we interpolated between the current position and the - /// corrected position. - /// This controls the duration of the interpolation; the higher it is, the longer the interpolation - /// will take - pub(crate) correction_ticks_factor: f32, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.to_string().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/leafwing_inputs/src/shared.rs b/examples/leafwing_inputs/src/shared.rs index 1b33cc00b..f5439ae33 100644 --- a/examples/leafwing_inputs/src/shared.rs +++ b/examples/leafwing_inputs/src/shared.rs @@ -6,6 +6,7 @@ use bevy_screen_diagnostics::{Aggregate, ScreenDiagnostics, ScreenDiagnosticsPlu use bevy_xpbd_2d::parry::shape::Ball; use bevy_xpbd_2d::prelude::*; use bevy_xpbd_2d::{PhysicsSchedule, PhysicsStepSet}; +use common::shared::FIXED_TIMESTEP_HZ; use leafwing_input_manager::prelude::ActionState; use tracing::Level; @@ -15,24 +16,9 @@ use lightyear::prelude::*; use lightyear::transport::io::IoDiagnosticsPlugin; use crate::protocol::*; - -const FRAME_HZ: f64 = 60.0; -const FIXED_TIMESTEP_HZ: f64 = 64.0; const MAX_VELOCITY: f32 = 200.0; const WALL_SIZE: f32 = 350.0; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - // server_send_interval: Duration::from_secs_f64(1.0 / 32.0), - server_send_interval: Duration::from_millis(100), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / FIXED_TIMESTEP_HZ), - }, - mode, - } -} - #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum FixedSet { // main fixed update systems (handle inputs) @@ -41,6 +27,7 @@ pub enum FixedSet { Physics, } +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/examples/lobby/Cargo.toml b/examples/lobby/Cargo.toml index 0273b190f..20a0bbb8b 100644 --- a/examples/lobby/Cargo.toml +++ b/examples/lobby/Cargo.toml @@ -14,27 +14,23 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } +bevy_egui = "0.25.0" +egui_extras = "0.26.0" +leafwing-input-manager = "0.13" lightyear = { path = "../../lightyear", features = [ - "steam", "webtransport", "websocket", + "leafwing", + "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } -bevy_egui = "0.25.0" -egui_extras = "0.26.0" derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/lobby/assets/settings.ron b/examples/lobby/assets/settings.ron index 9a63a49bb..a31ee1699 100644 --- a/examples/lobby/assets/settings.ron +++ b/examples/lobby/assets/settings.ron @@ -23,15 +23,6 @@ Settings( // transport: Steam( // app_id: 480, // ) - host_server_port: 5005, - host_server_transport: Udp, - ), - host_server: HostServerSettings( - transport: [ - Udp( - local_port: 5005 - ), - ], ), server: ServerSettings( headless: true, @@ -64,4 +55,4 @@ Settings( private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), compression: None, ) -) +) \ No newline at end of file diff --git a/examples/lobby/src/client.rs b/examples/lobby/src/client.rs index f0f15c9e6..9a8944b53 100644 --- a/examples/lobby/src/client.rs +++ b/examples/lobby/src/client.rs @@ -14,7 +14,7 @@ use lightyear::prelude::server::ServerCommands; use lightyear::prelude::*; use crate::protocol::*; -use crate::settings::{get_client_net_config, Settings}; +use common::settings::{get_client_net_config, Settings}; pub struct ExampleClientPlugin { pub(crate) settings: Settings, @@ -103,6 +103,8 @@ fn on_disconnect( settings: Res, connection: Res, ) { + let existing_client_id = connection.id(); + for entity in entities.iter() { commands.entity(entity).despawn_recursive(); } @@ -112,7 +114,7 @@ fn on_disconnect( commands.stop_server(); // update the client config to connect to the lobby server - config.net = get_client_net_config(settings.as_ref()); + config.net = get_client_net_config(settings.as_ref(), existing_client_id.to_bits()); } mod game { @@ -209,6 +211,7 @@ mod lobby { use lightyear::server::config::ServerConfig; use crate::client::{lobby, AppState}; + use crate::HOST_SERVER_PORT; use super::*; @@ -453,7 +456,7 @@ mod lobby { Authentication::Manual { server_addr, .. } => { *server_addr = SocketAddr::new( settings.client.server_addr.into(), - settings.client.host_server_port, + HOST_SERVER_PORT, ); } _ => {} diff --git a/examples/lobby/src/main.rs b/examples/lobby/src/main.rs index 8aa03eaab..1ce1e7108 100644 --- a/examples/lobby/src/main.rs +++ b/examples/lobby/src/main.rs @@ -10,169 +10,80 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{InterpolationConfig, InterpolationDelay}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::config::Mode; -use lightyear::shared::log::add_log_layer; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::{Apps, Cli}; +use common::settings::{ServerTransports, Settings}; +use lightyear::prelude::{Deserialize, Serialize}; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client. We will also launch the ServerPlugin in the same app - /// so that a client can also act as host. - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} +pub const HOST_SERVER_PORT: u16 = 5050; -/// We parse the settings.ron file to read the settings, than create the apps and run them fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let mut cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} + let mut settings = common::settings::settings::(settings_str); -/// This is the main function -/// The cli argument is used to determine if we are running as a client or a server (or listen-server) -/// Then we build the app and run it. -/// -/// To build a lightyear app you will need to add either the [`client::ClientPlugin`] or [`server::ServerPlugin`] -/// They can be created by providing a [`client::ClientConfig`] or [`server::ServerConfig`] struct, along with a -/// shared protocol which defines the messages (Messages, Components, Inputs) that can be sent between client and server. -fn run(mut settings: Settings, cli: Cli) { + // in this example, every client will actually launch in host-server mode + // the reason is that we want every client to be able to be the 'host' of a lobby + // so every client needs to have the ServerPlugins included in the app match cli { - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); - } Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - if let Some(client_id) = client_id { - settings.client.client_id = client_id; - } - let net_config = get_client_net_config(&settings); - let mut app = combined_app(settings, net_config); - app.run(); + cli = Cli::HostServer { client_id }; + // when the client acts as host, we will use port UDP:5050 for the transport + settings.server.transport = vec![ServerTransports::Udp { + local_port: HOST_SERVER_PORT, + }]; + } + Cli::Server => {} + _ => { + panic!("This example only supports the modes Client and Server"); } } -} -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), + // build the bevy app (this adds common plugins such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them + let mut app = common::app::build_app(settings.clone(), cli); + // we do not modify the configurations of the plugins, so we can just build + // the `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins( + ExampleClientPlugin { settings }, ExampleServerPlugin, SharedPlugin, - )); - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - app + ); + + // run the app + app.run(); } -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app(settings: Settings, client_net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MySettings { + pub common: Settings, - // server plugin - let net_configs = get_host_server_net_configs(&settings); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); + /// If true, we will predict the client's entities, but also the ball and other clients' entities! + /// This is what is done by RocketLeague (see [video](https://www.youtube.com/watch?v=ueEmiDM94IE)) + /// + /// If false, we will predict the client's entities but simple interpolate everything else. + pub(crate) predict_all: bool, - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin { settings }, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + /// By how many ticks an input press will be delayed? + /// This can be useful as a tradeoff between input delay and prediction accuracy. + /// If the input delay is greater than the RTT, then there won't ever be any mispredictions/rollbacks. + /// See [this article](https://www.snapnet.dev/docs/core-concepts/input-delay-vs-rollback/) for more information. + pub(crate) input_delay_ticks: u16, + + /// If visual correction is enabled, we don't instantly snapback to the corrected position + /// when we need to rollback. Instead we interpolated between the current position and the + /// corrected position. + /// This controls the duration of the interpolation; the higher it is, the longer the interpolation + /// will take + pub(crate) correction_ticks_factor: f32, } diff --git a/examples/lobby/src/protocol.rs b/examples/lobby/src/protocol.rs index c6ed8f501..24aabc9c3 100644 --- a/examples/lobby/src/protocol.rs +++ b/examples/lobby/src/protocol.rs @@ -184,18 +184,18 @@ impl Plugin for ProtocolPlugin { // inputs app.add_plugins(InputPlugin::::default()); // components - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_linear_interpolation_fn::(); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); // resources app.register_resource::(ChannelDirection::ServerToClient); // channels diff --git a/examples/lobby/src/server.rs b/examples/lobby/src/server.rs index cb817324f..23a781d43 100644 --- a/examples/lobby/src/server.rs +++ b/examples/lobby/src/server.rs @@ -6,28 +6,21 @@ //! - read inputs from the clients and move the player entities accordingly //! //! Lightyear will handle the replication of entities automatically if you add a `Replicate` component to them. -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -use bevy::app::PluginGroupBuilder; -use bevy::ecs::system::RunSystemOnce; use bevy::prelude::*; use bevy::utils::Duration; +use bevy::utils::HashMap; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; +use crate::shared::shared_movement_behaviour; pub struct ExampleServerPlugin; impl Plugin for ExampleServerPlugin { fn build(&self, app: &mut App) { - app.insert_resource(Global { - client_id_to_entity_id: Default::default(), - }); app.insert_resource(Lobbies::default()); app.add_systems( Startup, @@ -77,23 +70,26 @@ fn start_dedicated_server(mut commands: Commands) { /// Spawn an entity for a given client fn spawn_player_entity( commands: &mut Commands, - mut global: Mut, client_id: ClientId, dedicated_server: bool, ) -> Entity { let replicate = Replicate { - prediction_target: NetworkTarget::Single(client_id), - interpolation_target: NetworkTarget::AllExceptSingle(client_id), - replication_mode: if dedicated_server { - ReplicationMode::Room + target: ReplicationTarget { + prediction: NetworkTarget::Single(client_id), + interpolation: NetworkTarget::AllExceptSingle(client_id), + ..default() + }, + controlled_by: ControlledBy { + target: NetworkTarget::Single(client_id), + }, + visibility: if dedicated_server { + VisibilityMode::InterestManagement } else { - ReplicationMode::NetworkTarget + VisibilityMode::All }, ..default() }; let entity = commands.spawn((PlayerBundle::new(client_id, Vec2::ZERO), replicate)); - // Add a mapping from client id to entity id - global.client_id_to_entity_id.insert(client_id, entity.id()); info!("Create entity {:?} for client {:?}", entity.id(), client_id); entity.id() } @@ -107,12 +103,10 @@ mod game { pub(crate) fn handle_connections( mut connections: EventReader, server: ResMut, - mut global: ResMut, mut commands: Commands, ) { for connection in connections.read() { - let client_id = *connection.context(); - spawn_player_entity(&mut commands, global.reborrow(), client_id, false); + spawn_player_entity(&mut commands, connection.client_id, false); } } @@ -120,30 +114,21 @@ mod game { pub(crate) fn handle_disconnections( mut disconnections: EventReader, server: ResMut, - mut global: ResMut, - mut commands: Commands, mut lobbies: Option>, ) { for disconnection in disconnections.read() { - let client_id = disconnection.context(); - if let Some(entity) = global.client_id_to_entity_id.remove(client_id) { - if let Some(mut entity) = commands.get_entity(entity) { - entity.despawn(); - } - } // NOTE: games hosted by players will disappear from the lobby list since the host // is not connected anymore if let Some(lobbies) = lobbies.as_mut() { - lobbies.remove_client(*client_id); + lobbies.remove_client(disconnection.client_id); } } } /// Read client inputs and move players pub(crate) fn movement( - mut position_query: Query<&mut PlayerPosition>, + mut position_query: Query<(&ControlledBy, &mut PlayerPosition)>, mut input_reader: EventReader>, - global: Res, tick_manager: Res, ) { for input in input_reader.read() { @@ -155,8 +140,10 @@ mod game { client_id, tick_manager.tick() ); - if let Some(player_entity) = global.client_id_to_entity_id.get(client_id) { - if let Ok(position) = position_query.get_mut(*player_entity) { + // NOTE: you can define a mapping from client_id to entity_id to avoid iterating through all + // entities here + for (controlled_by, position) in position_query.iter_mut() { + if controlled_by.targets(client_id) { shared_movement_behaviour(position, input); } } @@ -167,6 +154,7 @@ mod game { mod lobby { use lightyear::server::connection::ConnectionManager; + use lightyear::server::visibility::room::RoomManager; use super::*; @@ -178,7 +166,6 @@ mod lobby { mut lobbies: ResMut, mut room_manager: ResMut, mut commands: Commands, - mut global: ResMut, ) { for lobby_join in events.read() { let client_id = *lobby_join.context(); @@ -189,7 +176,7 @@ mod lobby { room_manager.add_client(client_id, RoomId(lobby_id as u64)); if lobby.in_game { // if the game has already started, we need to spawn the player entity - let entity = spawn_player_entity(&mut commands, global.reborrow(), client_id, true); + let entity = spawn_player_entity(&mut commands, client_id, true); room_manager.add_entity(entity, RoomId(lobby_id as u64)); } } @@ -223,7 +210,6 @@ mod lobby { mut lobbies: ResMut, mut room_manager: ResMut, mut commands: Commands, - mut global: ResMut, ) { for event in events.read() { let client_id = event.context(); @@ -243,8 +229,7 @@ mod lobby { if !lobby.players.contains(client_id) { lobby.players.push(*client_id); if host.is_none() { - let entity = - spawn_player_entity(&mut commands, global.reborrow(), *client_id, true); + let entity = spawn_player_entity(&mut commands, *client_id, true); room_manager.add_entity(entity, room_id); room_manager.add_client(*client_id, room_id); } @@ -261,8 +246,7 @@ mod lobby { // one of the players asked for the game to start for player in &lobby.players { error!("Spawning player {player:?} entity for game"); - let entity = - spawn_player_entity(&mut commands, global.reborrow(), *player, true); + let entity = spawn_player_entity(&mut commands, *player, true); room_manager.add_entity(entity, room_id); } } diff --git a/examples/lobby/src/shared.rs b/examples/lobby/src/shared.rs index 7c2197e37..02565d66c 100644 --- a/examples/lobby/src/shared.rs +++ b/examples/lobby/src/shared.rs @@ -14,17 +14,7 @@ use lightyear::shared::config::Mode; use crate::protocol::*; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - server_send_interval: Duration::from_millis(40), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/examples/priority/Cargo.toml b/examples/priority/Cargo.toml index 0f5a8c2cf..989014082 100644 --- a/examples/priority/Cargo.toml +++ b/examples/priority/Cargo.toml @@ -15,9 +15,9 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } bevy_screen_diagnostics = "0.5.0" leafwing-input-manager = "0.13" lightyear = { path = "../../lightyear", features = [ @@ -26,7 +26,6 @@ lightyear = { path = "../../lightyear", features = [ "leafwing", "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" @@ -34,9 +33,4 @@ tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/priority/src/main.rs b/examples/priority/src/main.rs index db1c6e268..0f992858f 100644 --- a/examples/priority/src/main.rs +++ b/examples/priority/src/main.rs @@ -1,263 +1,50 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -//! Run with -//! - `cargo run -- server` -//! - `cargo run -- client -c 1` -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{InterpolationConfig, InterpolationDelay, NetConfig}; -use lightyear::prelude::server::PacketConfig; -use lightyear::prelude::{Mode, TransportConfig}; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::Settings; +use lightyear::prelude::server::PacketConfig; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); + let settings = common::settings::settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings, cli); + + // for this example, we will put a bandwidth cap on the server-side + let packet_config = PacketConfig::default() + // by default there is no bandwidth limit so we need to enable it + .enable_bandwidth_cap() + // we can set the max bandwidth to 56 KB/s + .with_send_bandwidth_bytes_per_second_cap(1500); + match &mut app { + Apps::Server { config, .. } => { + config.packet = packet_config; } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); + Apps::ListenServer { server_config, .. } => { + server_config.packet = packet_config; } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); + Apps::HostServer { server_config, .. } => { + server_config.packet = packet_config; } + _ => {} } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - packet: PacketConfig::default() - // by default there is no bandwidth limit so we need to enable it - .enable_bandwidth_cap() - // we can set the max bandwidth to 56 KB/s - .with_send_bandwidth_bytes_per_second_cap(1500), - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - SharedPlugin, - )); - app -} - -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - packet: PacketConfig::default() - // by default there is no bandwidth limit so we need to enable it - .enable_bandwidth_cap() - // we can set the max bandwidth to 56 KB/s - .with_send_bandwidth_bytes_per_second_cap(1500), - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + // add the `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins(ExampleClientPlugin, ExampleServerPlugin, SharedPlugin); + // run the app + app.run(); } diff --git a/examples/priority/src/protocol.rs b/examples/priority/src/protocol.rs index ba3bfb6e9..d4ad6cefe 100644 --- a/examples/priority/src/protocol.rs +++ b/examples/priority/src/protocol.rs @@ -20,6 +20,7 @@ pub(crate) struct PlayerBundle { color: PlayerColor, replicate: Replicate, action_state: ActionState, + action_state_target_override: OverrideTargetComponent>, } impl PlayerBundle { @@ -30,21 +31,29 @@ impl PlayerBundle { let l = 0.5; let color = Color::hsl(h, s, l); - let mut replicate = Replicate { - prediction_target: NetworkTarget::Single(id), - interpolation_target: NetworkTarget::AllExceptSingle(id), + let replicate = Replicate { + target: ReplicationTarget { + prediction: NetworkTarget::Single(id), + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, + controlled_by: ControlledBy { + target: NetworkTarget::Single(id), + }, ..default() }; - // We don't want to replicate the ActionState to the original client, since they are updating it with - // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, - // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! - replicate.add_target::>(NetworkTarget::AllExceptSingle(id)); Self { id: PlayerId(id), position: Position(position), color: PlayerColor(color), replicate, action_state: ActionState::default(), + // We don't want to replicate the ActionState to the original client, since they are updating it with + // their own inputs (if you replicate it to the original client, it will be added on the Confirmed entity, + // which will keep syncing it to the Predicted entity because the ActionState gets updated every tick)! + action_state_target_override: OverrideTargetComponent::new( + NetworkTarget::AllExceptSingle(id), + ), } } pub(crate) fn get_input_map() -> InputMap { @@ -130,18 +139,18 @@ impl Plugin for ProtocolPlugin { // inputs app.add_plugins(LeafwingInputPlugin::::default()); // components - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Full); - app.add_interpolation::(ComponentSyncMode::Full); - app.add_linear_interpolation_fn::(); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); app.register_component::(ChannelDirection::ServerToClient); // channels diff --git a/examples/priority/src/server.rs b/examples/priority/src/server.rs index 6619fc4d6..e3b4bf47a 100644 --- a/examples/priority/src/server.rs +++ b/examples/priority/src/server.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use bevy::utils::HashMap; use std::ops::Deref; use bevy::prelude::*; @@ -61,8 +61,7 @@ pub(crate) fn init(mut commands: Commands) { // The priority can be sent when the entity is spawned; if multiple entities in the same group have // different priorities, the latest set priority will be used. // After the entity is spawned, you can update the priority using the ConnectionManager::upate_priority method. - replication_group: ReplicationGroup::default() - .set_priority(1.0 + y.abs() as f32), + group: ReplicationGroup::default().set_priority(1.0 + y.abs() as f32), ..default() }, )); @@ -73,22 +72,11 @@ pub(crate) fn init(mut commands: Commands) { /// Server connection system, create a player upon connection pub(crate) fn handle_connections( mut connections: EventReader, - mut disconnections: EventReader, - mut global: ResMut, mut commands: Commands, ) { for connection in connections.read() { - let client_id = *connection.context(); + let client_id = connection.client_id; let entity = commands.spawn(PlayerBundle::new(client_id, Vec2::splat(300.0))); - // Add a mapping from client id to entity id (so that when we receive an input from a client, - // we know which entity to move) - global.client_id_to_entity_id.insert(client_id, entity.id()); - } - for disconnection in disconnections.read() { - let client_id = disconnection.context(); - if let Some(entity) = global.client_id_to_entity_id.remove(client_id) { - commands.entity(entity).despawn(); - } } } diff --git a/examples/priority/src/settings.rs b/examples/priority/src/settings.rs deleted file mode 100644 index c64c8bdb6..000000000 --- a/examples/priority/src/settings.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: bevy::utils::Duration::from_millis(self.latency_ms as u64), - incoming_jitter: bevy::utils::Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.to_string().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/priority/src/shared.rs b/examples/priority/src/shared.rs index 237d323b8..520233053 100644 --- a/examples/priority/src/shared.rs +++ b/examples/priority/src/shared.rs @@ -15,19 +15,7 @@ use crate::protocol::*; const MOVE_SPEED: f32 = 10.0; const PROP_SIZE: f32 = 5.0; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - server_send_interval: Duration::from_millis(100), - tick: TickConfig { - // right now, we NEED the tick_duration to be smaller than the send_interval - // (otherwise we can send multiple packets for the same tick at different frames) - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/examples/replication_groups/Cargo.toml b/examples/replication_groups/Cargo.toml index cbed8b56b..140e43274 100644 --- a/examples/replication_groups/Cargo.toml +++ b/examples/replication_groups/Cargo.toml @@ -15,15 +15,14 @@ publish = false [features] metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] [dependencies] +common = { path = "../common" } lightyear = { path = "../../lightyear", features = [ "webtransport", "websocket", "steam", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" @@ -31,9 +30,4 @@ tracing-subscriber = "0.3.17" bevy = { version = "0.13", features = ["bevy_core_pipeline"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } -mock_instant = "0.4" metrics-exporter-prometheus = { version = "0.13.0", optional = true } -bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/replication_groups/src/client.rs b/examples/replication_groups/src/client.rs index 5a14f029b..bde78c40b 100644 --- a/examples/replication_groups/src/client.rs +++ b/examples/replication_groups/src/client.rs @@ -1,20 +1,11 @@ -use std::collections::VecDeque; -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; -use lightyear::client::interpolation::LinearInterpolator; -use lightyear::connection::netcode::NetcodeServer; -pub use lightyear::prelude::client::*; -use lightyear::prelude::*; - use crate::protocol::Direction; use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour, shared_tail_behaviour}; -use crate::{shared, ClientTransports, SharedSettings}; +use crate::shared::{shared_movement_behaviour, shared_tail_behaviour}; +use lightyear::prelude::client::*; +use lightyear::prelude::*; pub struct ExampleClientPlugin; @@ -123,7 +114,7 @@ pub(crate) fn handle_predicted_spawn( ) { for (entity, mut color) in predicted_heads.iter_mut() { color.0.set_s(0.3); - // add visual interpolation for the head position of the predited entity + // add visual interpolation for the head position of the predicted entity // so that the position gets updated smoothly every frame // (updating it only during FixedUpdate might cause visual artifacts, see: // https://cbournhonesque.github.io/lightyear/book/concepts/advanced_replication/visual_interpolation.html) diff --git a/examples/replication_groups/src/main.rs b/examples/replication_groups/src/main.rs index 3918c56e7..c0cd7b740 100644 --- a/examples/replication_groups/src/main.rs +++ b/examples/replication_groups/src/main.rs @@ -1,252 +1,29 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -//! Run with -//! - `cargo run -- server` -//! - `cargo run -- client -c 1` -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{InterpolationConfig, InterpolationDelay, NetConfig}; -use lightyear::prelude::{Mode, TransportConfig}; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::Settings; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); - } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); - } - } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - SharedPlugin, - )); - app -} - -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + let settings = common::settings::settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings, cli); + // add the `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins(ExampleClientPlugin, ExampleServerPlugin, SharedPlugin); + // run the app + app.run(); } diff --git a/examples/replication_groups/src/protocol.rs b/examples/replication_groups/src/protocol.rs index 6d0542700..68d68a31b 100644 --- a/examples/replication_groups/src/protocol.rs +++ b/examples/replication_groups/src/protocol.rs @@ -45,12 +45,16 @@ impl PlayerBundle { position: PlayerPosition(position), color: PlayerColor(color), replicate: Replicate { - // prediction_target: NetworkTarget::None, - prediction_target: NetworkTarget::Single(id), - // interpolation_target: NetworkTarget::None, - interpolation_target: NetworkTarget::AllExceptSingle(id), + target: ReplicationTarget { + prediction: NetworkTarget::Single(id), + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, + controlled_by: ControlledBy { + target: NetworkTarget::Single(id), + }, // the default is: the replication group id is a u64 value generated from the entity (`entity.to_bits()`) - replication_group: ReplicationGroup::default(), + group: ReplicationGroup::default(), ..default() }, } @@ -68,12 +72,16 @@ impl TailBundle { points: TailPoints(points), length: TailLength(length), replicate: Replicate { - // prediction_target: NetworkTarget::None, - prediction_target: NetworkTarget::Single(id), - // interpolation_target: NetworkTarget::None, - interpolation_target: NetworkTarget::AllExceptSingle(id), + target: ReplicationTarget { + prediction: NetworkTarget::Single(id), + interpolation: NetworkTarget::AllExceptSingle(id), + ..default() + }, + controlled_by: ControlledBy { + target: NetworkTarget::Single(id), + }, // replicate this entity within the same replication group as the parent - replication_group: ReplicationGroup::default().set_id(parent.to_bits()), + group: ReplicationGroup::default().set_id(parent.to_bits()), ..default() }, } @@ -286,34 +294,39 @@ impl Plugin for ProtocolPlugin { // inputs app.add_plugins(InputPlugin::::default()); // components - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Full); - app.add_custom_interpolation::(ComponentSyncMode::Full); - // we still register an interpolation function which will be used for visual interpolation - app.add_linear_interpolation_fn::(); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Full); - app.add_custom_interpolation::(ComponentSyncMode::Full); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Full) + // NOTE: notice that we use custom interpolation here, this means that we don't run + // the interpolation function for this component, so we need to implement our own interpolation system + // (we do this because our interpolation system queries multiple components at once) + .add_custom_interpolation(ComponentSyncMode::Full) + // we still register an interpolation function which will be used for visual interpolation + .add_linear_interpolation_fn(); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); + + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Full) + // NOTE: notice that we use custom interpolation here, this means that we don't run + // the interpolation function for this component, so we need to implement our own interpolation system + // (we do this because our interpolation system queries multiple components at once) + .add_custom_interpolation(ComponentSyncMode::Full); // we do not register an interpolation function because we will use a custom interpolation system - app.register_component::(ChannelDirection::ServerToClient); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); + app.register_component::(ChannelDirection::ServerToClient) + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); app.register_component::(ChannelDirection::ServerToClient) - .add_map_entities::(); - app.add_prediction::(ComponentSyncMode::Once); - app.add_interpolation::(ComponentSyncMode::Once); - // app.add_component_map_entities::(); + .add_map_entities() + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); // channels app.add_channel::(ChannelSettings { mode: ChannelMode::OrderedReliable(ReliableSettings::default()), diff --git a/examples/replication_groups/src/server.rs b/examples/replication_groups/src/server.rs index d93bd362c..9d9745e82 100644 --- a/examples/replication_groups/src/server.rs +++ b/examples/replication_groups/src/server.rs @@ -1,36 +1,26 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - -use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; +use bevy::utils::HashMap; -pub use lightyear::prelude::server::*; +use lightyear::prelude::server::*; use lightyear::prelude::*; use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour, shared_tail_behaviour}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; +use crate::shared::{shared_movement_behaviour, shared_tail_behaviour}; // Plugin for server-specific logic pub struct ExampleServerPlugin; impl Plugin for ExampleServerPlugin { fn build(&self, app: &mut App) { - app.init_resource::(); app.add_systems(Startup, init); - // the physics/FixedUpdates systems that consume inputs should be run in this set + // the simulation systems that can be rolled back must run in FixedUpdate app.add_systems(FixedUpdate, (movement, shared_tail_behaviour).chain()); app.add_systems(Update, handle_connections); - // app.add_systems(Update, debug_inputs); } } -#[derive(Resource, Default)] -pub(crate) struct Global { - pub client_id_to_entity_id: HashMap, -} - /// Start the server pub(crate) fn init(mut commands: Commands) { commands.start_server(); @@ -53,12 +43,10 @@ pub(crate) fn init(mut commands: Commands) { /// Server connection system, create a player upon connection pub(crate) fn handle_connections( mut connections: EventReader, - mut disconnections: EventReader, - mut global: ResMut, mut commands: Commands, ) { for connection in connections.read() { - let client_id = *connection.context(); + let client_id = connection.client_id; // Generate pseudo random color from client id. let h = (((client_id.to_bits().wrapping_mul(30)) % 360) as f32) / 360.0; let s = 0.8; @@ -76,26 +64,13 @@ pub(crate) fn handle_connections( tail_length, )) .id(); - // Add a mapping from client id to entity id - global - .client_id_to_entity_id - .insert(client_id, (player_entity, tail_entity)); - } - for disconnection in disconnections.read() { - let client_id = disconnection.context(); - if let Some((player_entity, tail_entity)) = global.client_id_to_entity_id.remove(client_id) - { - commands.entity(player_entity).despawn(); - commands.entity(tail_entity).despawn(); - } } } /// Read client inputs and move players pub(crate) fn movement( - mut position_query: Query<&mut PlayerPosition>, + mut position_query: Query<(&ControlledBy, &mut PlayerPosition)>, mut input_reader: EventReader>, - global: Res, tick_manager: Res, ) { for input in input_reader.read() { @@ -107,15 +82,13 @@ pub(crate) fn movement( client_id, tick_manager.tick() ); - if let Some((player_entity, _)) = global.client_id_to_entity_id.get(client_id) { - if let Ok(position) = position_query.get_mut(*player_entity) { - shared_movement_behaviour(position, input); + // NOTE: you can define a mapping from client_id to entity_id to avoid iterating through all + // entities here + for (controlled_by, position) in position_query.iter_mut() { + if controlled_by.targets(client_id) { + shared::shared_movement_behaviour(position, input); } } } } } - -// pub(crate) fn debug_inputs(server: Res) { -// info!(tick = ?server.tick(), inputs = ?server.get_input_buffer(1), "debug"); -// } diff --git a/examples/replication_groups/src/shared.rs b/examples/replication_groups/src/shared.rs index 1f376bd17..bd54f0aec 100644 --- a/examples/replication_groups/src/shared.rs +++ b/examples/replication_groups/src/shared.rs @@ -10,18 +10,7 @@ use lightyear::prelude::*; use crate::protocol::Direction; use crate::protocol::*; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - // server_send_interval: Duration::from_millis(40), - server_send_interval: Duration::from_millis(100), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { @@ -60,10 +49,10 @@ pub(crate) fn shared_movement_behaviour(mut position: Mut, input // Note: we only apply logic for the Predicted entity on the client (Interpolated is updated // during interpolation, and Confirmed is just replicated from Server) pub(crate) fn shared_tail_behaviour( - player_position: Query, Or<(With, With)>>, + player_position: Query, Or<(With, With)>>, mut tails: Query< (&mut TailPoints, &PlayerParent, &TailLength), - Or<(With, With)>, + Or<(With, With)>, >, ) { for (mut points, parent, length) in tails.iter_mut() { diff --git a/examples/simple_box/Cargo.toml b/examples/simple_box/Cargo.toml index 0961fa635..1afd81be9 100644 --- a/examples/simple_box/Cargo.toml +++ b/examples/simple_box/Cargo.toml @@ -16,12 +16,12 @@ publish = false metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] [dependencies] +common = { path = "../common" } lightyear = { path = "../../lightyear", features = [ "steam", "webtransport", "websocket", ] } -async-compat = "0.2.3" serde = { version = "1.0.188", features = ["derive"] } anyhow = { version = "1.0.75", features = [] } tracing = "0.1" @@ -30,8 +30,5 @@ bevy = { version = "0.13", features = ["bevy_core_pipeline"] } bevy_mod_picking = { version = "0.18.2", features = ["backend_bevy_ui"] } derive_more = { version = "0.99", features = ["add", "mul"] } rand = "0.8.1" -clap = { version = "4.4", features = ["derive"] } metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.24" -cfg-if = "1.0.0" -crossbeam-channel = "0.5.11" diff --git a/examples/simple_box/src/client.rs b/examples/simple_box/src/client.rs index 98b8896d5..72ba3191e 100644 --- a/examples/simple_box/src/client.rs +++ b/examples/simple_box/src/client.rs @@ -19,8 +19,7 @@ use lightyear::prelude::*; use crate::protocol::Direction; use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, ClientTransports, SharedSettings}; +use crate::shared; pub struct ExampleClientPlugin; @@ -129,7 +128,7 @@ fn player_movement( for input in input_reader.read() { if let Some(input) = input.input() { for position in position_query.iter_mut() { - shared_movement_behaviour(position, input); + shared::shared_movement_behaviour(position, input); } } } diff --git a/examples/simple_box/src/main.rs b/examples/simple_box/src/main.rs index 130c7ecab..c85c67e75 100644 --- a/examples/simple_box/src/main.rs +++ b/examples/simple_box/src/main.rs @@ -10,254 +10,29 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] - -use std::net::SocketAddr; -use std::str::FromStr; - -use bevy::asset::ron; -use bevy::log::{Level, LogPlugin}; -use bevy::prelude::*; -use bevy::DefaultPlugins; -use bevy_inspector_egui::quick::WorldInspectorPlugin; -use clap::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::{InterpolationConfig, InterpolationDelay, NetConfig}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::config::Mode; -use lightyear::shared::log::add_log_layer; -use lightyear::transport::LOCAL_SOCKET; - use crate::client::ExampleClientPlugin; use crate::server::ExampleServerPlugin; -use crate::settings::*; -use crate::shared::{shared_config, SharedPlugin}; +use crate::shared::SharedPlugin; +use bevy::prelude::*; +use common::app::Apps; +use common::settings::Settings; mod client; mod protocol; mod server; -mod settings; mod shared; -#[derive(Parser, PartialEq, Debug)] -enum Cli { - /// We have the client and the server running inside the same app. - /// The server will also act as a client. - #[cfg(not(target_family = "wasm"))] - HostServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// We will create two apps: a client app and a server app. - /// Data gets passed between the two via channels. - ListenServer { - #[arg(short, long, default_value = None)] - client_id: Option, - }, - #[cfg(not(target_family = "wasm"))] - /// Dedicated server - Server, - /// The program will act as a client - Client { - #[arg(short, long, default_value = None)] - client_id: Option, - }, -} - -/// We parse the settings.ron file to read the settings, than create the apps and run them fn main() { - cfg_if::cfg_if! { - if #[cfg(target_family = "wasm")] { - let client_id = rand::random::(); - let cli = Cli::Client { - client_id: Some(client_id) - }; - } else { - let cli = Cli::parse(); - } - } + let cli = common::app::cli(); let settings_str = include_str!("../assets/settings.ron"); - let settings = ron::de::from_str::(settings_str).unwrap(); - run(settings, cli); -} - -/// This is the main function -/// The cli argument is used to determine if we are running as a client or a server (or listen-server) -/// Then we build the app and run it. -/// -/// To build a lightyear app you will need to add either the [`client::ClientPlugin`] or [`server::ServerPlugin`] -/// They can be created by providing a [`client::ClientConfig`] or [`server::ServerConfig`] struct, along with a -/// shared protocol which defines the messages (Messages, Components, Inputs) that can be sent between client and server. -fn run(settings: Settings, cli: Cli) { - match cli { - // ListenServer using a single app - #[cfg(not(target_family = "wasm"))] - Cli::HostServer { client_id } => { - let client_net_config = NetConfig::Local { - id: client_id.unwrap_or(settings.client.client_id), - }; - let mut app = combined_app(settings, vec![], client_net_config); - app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::ListenServer { client_id } => { - // create client app - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - // we will communicate between the client and server apps via channels - let transport_config = TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }; - let net_config = build_client_netcode_config( - client_id.unwrap_or(settings.client.client_id), - // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server - LOCAL_SOCKET, - settings.client.conditioner.as_ref(), - &settings.shared, - transport_config, - ); - let mut client_app = client_app(settings.clone(), net_config); - - // create server app - let extra_transport_configs = vec![TransportConfig::Channels { - // even if we communicate via channels, we need to provide a socket address for the client - channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], - }]; - let mut server_app = server_app(settings, extra_transport_configs); - - // run both the client and server apps - std::thread::spawn(move || server_app.run()); - client_app.run(); - } - #[cfg(not(target_family = "wasm"))] - Cli::Server => { - let mut app = server_app(settings, vec![]); - app.run(); - } - Cli::Client { client_id } => { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - // use the cli-provided client id if it exists, otherwise use the settings client id - let client_id = client_id.unwrap_or(settings.client.client_id); - let net_config = get_client_net_config(&settings, client_id); - let mut app = client_app(settings, net_config); - app.run(); - } - } -} - -/// Build the client app -fn client_app(settings: Settings, net_config: client::NetConfig) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let client_config = client::ClientConfig { - shared: shared_config(Mode::Separate), - net: net_config, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - SharedPlugin, - )); - app -} - -/// Build the server app -#[cfg(not(target_family = "wasm"))] -fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { - let mut app = App::new(); - if !settings.server.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.server.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::Separate), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - SharedPlugin, - )); - app -} - -/// An app that contains both the client and server plugins -#[cfg(not(target_family = "wasm"))] -fn combined_app( - settings: Settings, - extra_transport_configs: Vec, - client_net_config: client::NetConfig, -) -> App { - let mut app = App::new(); - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=warn".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.client.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - - // server plugin - let mut net_configs = get_server_net_configs(&settings); - let extra_net_configs = extra_transport_configs.into_iter().map(|c| { - build_server_netcode_config(settings.server.conditioner.as_ref(), &settings.shared, c) - }); - net_configs.extend(extra_net_configs); - let server_config = server::ServerConfig { - shared: shared_config(Mode::HostServer), - net: net_configs, - ..default() - }; - app.add_plugins(( - server::ServerPlugin::new(server_config), - ExampleServerPlugin, - )); - - // client plugin - let client_config = client::ClientConfig { - shared: shared_config(Mode::HostServer), - net: client_net_config, - interpolation: InterpolationConfig { - delay: InterpolationDelay::default().with_send_interval_ratio(2.0), - ..default() - }, - ..default() - }; - app.add_plugins(( - client::ClientPlugin::new(client_config), - ExampleClientPlugin, - )); - // shared plugin - app.add_plugins(SharedPlugin); - app + let settings = common::settings::settings::(settings_str); + // build the bevy app (this adds common plugin such as the DefaultPlugins) + // and returns the `ClientConfig` and `ServerConfig` so that we can modify them if needed + let mut app = common::app::build_app(settings, cli); + // add the `ClientPlugins` and `ServerPlugins` plugin groups + app.add_lightyear_plugin_groups(); + // add our plugins + app.add_plugins(ExampleClientPlugin, ExampleServerPlugin, SharedPlugin); + // run the app + app.run(); } diff --git a/examples/simple_box/src/protocol.rs b/examples/simple_box/src/protocol.rs index ea8595fc0..f8dd0bcdf 100644 --- a/examples/simple_box/src/protocol.rs +++ b/examples/simple_box/src/protocol.rs @@ -118,17 +118,17 @@ impl Plugin for ProtocolPlugin { app.add_plugins(InputPlugin::::default()); // components app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Full) - .add_interpolation::(ComponentSyncMode::Full) - .add_linear_interpolation_fn::(); + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once) - .add_interpolation::(ComponentSyncMode::Once); + .add_prediction(ComponentSyncMode::Once) + .add_interpolation(ComponentSyncMode::Once); // channels app.add_channel::(ChannelSettings { mode: ChannelMode::OrderedReliable(ReliableSettings::default()), diff --git a/examples/simple_box/src/server.rs b/examples/simple_box/src/server.rs index 0ad903559..9a638fe72 100644 --- a/examples/simple_box/src/server.rs +++ b/examples/simple_box/src/server.rs @@ -6,27 +6,20 @@ //! - read inputs from the clients and move the player entities accordingly //! //! Lightyear will handle the replication of entities automatically if you add a `Replicate` component to them. -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; - use bevy::app::PluginGroupBuilder; use bevy::prelude::*; -use bevy::utils::Duration; - -pub use lightyear::prelude::server::*; +use bevy::utils::HashMap; +use lightyear::prelude::server::*; use lightyear::prelude::*; +use lightyear::shared::replication::components::ReplicationTarget; use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings}; +use crate::shared; pub struct ExampleServerPlugin; impl Plugin for ExampleServerPlugin { fn build(&self, app: &mut App) { - app.insert_resource(Global { - client_id_to_entity_id: Default::default(), - }); app.add_systems(Startup, (init, start_server)); // the physics/FixedUpdates systems that consume inputs should be run in this set app.add_systems(FixedUpdate, movement); @@ -34,11 +27,6 @@ impl Plugin for ExampleServerPlugin { } } -#[derive(Resource)] -pub(crate) struct Global { - pub client_id_to_entity_id: HashMap, -} - /// Start the server fn start_server(mut commands: Commands) { commands.start_server(); @@ -65,32 +53,50 @@ fn init(mut commands: Commands) { /// Server connection system, create a player upon connection pub(crate) fn handle_connections( mut connections: EventReader, - mut disconnections: EventReader, - mut global: ResMut, mut commands: Commands, ) { for connection in connections.read() { - let client_id = *connection.context(); + let client_id = connection.client_id; // server and client are running in the same app, no need to replicate to the local client let replicate = Replicate { - prediction_target: NetworkTarget::Single(client_id), - interpolation_target: NetworkTarget::AllExceptSingle(client_id), + target: ReplicationTarget { + prediction: NetworkTarget::Single(client_id), + interpolation: NetworkTarget::AllExceptSingle(client_id), + ..default() + }, + controlled_by: ControlledBy { + target: NetworkTarget::Single(client_id), + }, ..default() }; let entity = commands.spawn((PlayerBundle::new(client_id, Vec2::ZERO), replicate)); - // Add a mapping from client id to entity id - global.client_id_to_entity_id.insert(client_id, entity.id()); info!("Create entity {:?} for client {:?}", entity.id(), client_id); } +} + +/// Handle client disconnections: we want to despawn every entity that was controlled by that client. +/// +/// Lightyear creates one entity per client, which contains metadata associated with that client. +/// You can find that entity by calling `ConnectionManager::client_entity(client_id)`. +/// +/// That client entity contains the `ControlledEntities` component, which is a set of entities that are controlled by that client. +/// +/// By default, lightyear automatically despawns all the `ControlledEntities` when the client disconnects; +/// but in this example we will also do it manually to showcase how it can be done. +/// (however we don't actually run the system) +pub(crate) fn handle_disconnections( + mut commands: Commands, + mut disconnections: EventReader, + manager: Res, + client_query: Query<&ControlledEntities>, +) { for disconnection in disconnections.read() { - let client_id = disconnection.context(); - // TODO: handle this automatically in lightyear - // - provide a Owned component in lightyear that can specify that an entity is owned by a specific player? - // - maybe have the client-id to entity-mapping in the global metadata? - // - despawn automatically those entities when the client disconnects - if let Some(entity) = global.client_id_to_entity_id.remove(client_id) { - if let Some(mut entity) = commands.get_entity(entity) { - entity.despawn(); + debug!("Client {:?} disconnected", disconnection.client_id); + if let Ok(client_entity) = manager.client_entity(disconnection.client_id) { + if let Ok(controlled_entities) = client_query.get(client_entity) { + for entity in controlled_entities.iter() { + commands.entity(*entity).despawn(); + } } } } @@ -98,9 +104,8 @@ pub(crate) fn handle_connections( /// Read client inputs and move players pub(crate) fn movement( - mut position_query: Query<&mut PlayerPosition>, + mut position_query: Query<(&ControlledBy, &mut PlayerPosition)>, mut input_reader: EventReader>, - global: Res, tick_manager: Res, ) { for input in input_reader.read() { @@ -112,9 +117,11 @@ pub(crate) fn movement( client_id, tick_manager.tick() ); - if let Some(player_entity) = global.client_id_to_entity_id.get(client_id) { - if let Ok(position) = position_query.get_mut(*player_entity) { - shared_movement_behaviour(position, input); + // NOTE: you can define a mapping from client_id to entity_id to avoid iterating through all + // entities here + for (controlled_by, position) in position_query.iter_mut() { + if controlled_by.targets(client_id) { + shared::shared_movement_behaviour(position, input); } } } diff --git a/examples/simple_box/src/settings.rs b/examples/simple_box/src/settings.rs deleted file mode 100644 index 1419d3fca..000000000 --- a/examples/simple_box/src/settings.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! This module parses the settings.ron file and builds a lightyear configuration from it -use std::env::join_paths; -use std::net::{Ipv4Addr, SocketAddr}; - -use async_compat::Compat; -use bevy::tasks::IoTaskPool; -use bevy::utils::Duration; -use serde::{Deserialize, Serialize}; - -use lightyear::prelude::client::Authentication; -#[cfg(not(target_family = "wasm"))] -use lightyear::prelude::client::SteamConfig; -use lightyear::prelude::{CompressionConfig, IoConfig, LinkConditionerConfig, TransportConfig}; - -#[cfg(not(target_family = "wasm"))] -use crate::server::Identity; -use crate::{client, server}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ClientTransports { - #[cfg(not(target_family = "wasm"))] - Udp, - WebTransport { - certificate_digest: String, - }, - WebSocket, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum ServerTransports { - Udp { - local_port: u16, - }, - WebTransport { - local_port: u16, - }, - WebSocket { - local_port: u16, - }, - #[cfg(not(target_family = "wasm"))] - Steam { - app_id: u32, - server_ip: Ipv4Addr, - game_port: u16, - query_port: u16, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Conditioner { - /// One way latency in milliseconds - pub(crate) latency_ms: u16, - /// One way jitter in milliseconds - pub(crate) jitter_ms: u16, - /// Percentage of packet loss - pub(crate) packet_loss: f32, -} - -impl Conditioner { - pub fn build(&self) -> LinkConditionerConfig { - LinkConditionerConfig { - incoming_latency: Duration::from_millis(self.latency_ms as u64), - incoming_jitter: Duration::from_millis(self.jitter_ms as u64), - incoming_loss: self.packet_loss, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServerSettings { - /// If true, disable any rendering-related plugins - pub(crate) headless: bool, - - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, - - /// Which transport to use - pub(crate) transport: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ClientSettings { - /// If true, enable bevy_inspector_egui - pub(crate) inspector: bool, - - /// The client id - pub(crate) client_id: u64, - - /// The client port to listen on - pub(crate) client_port: u16, - - /// The ip address of the server - pub(crate) server_addr: Ipv4Addr, - - /// The port of the server - pub(crate) server_port: u16, - - /// Which transport to use - pub(crate) transport: ClientTransports, - - /// Possibly add a conditioner to simulate network conditions - pub(crate) conditioner: Option, -} - -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub struct SharedSettings { - /// An id to identify the protocol version - pub(crate) protocol_id: u64, - - /// a 32-byte array to authenticate via the Netcode.io protocol - pub(crate) private_key: [u8; 32], - - /// compression options - pub(crate) compression: CompressionConfig, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Settings { - pub server: ServerSettings, - pub client: ClientSettings, - pub shared: SharedSettings, -} - -pub fn build_server_netcode_config( - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> server::NetConfig { - let conditioner = conditioner.map_or(None, |c| { - Some(LinkConditionerConfig { - incoming_latency: Duration::from_millis(c.latency_ms as u64), - incoming_jitter: Duration::from_millis(c.jitter_ms as u64), - incoming_loss: c.packet_loss, - }) - }); - let netcode_config = server::NetcodeConfig::default() - .with_protocol_id(shared.protocol_id) - .with_key(shared.private_key); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - server::NetConfig::Netcode { - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a list of `NetConfig` that are used to configure how the lightyear server -/// listens for incoming client connections -#[cfg(not(target_family = "wasm"))] -pub fn get_server_net_configs(settings: &Settings) -> Vec { - settings - .server - .transport - .iter() - .map(|t| match t { - ServerTransports::Udp { local_port } => build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(SocketAddr::new( - Ipv4Addr::UNSPECIFIED.into(), - *local_port, - )), - ), - ServerTransports::WebTransport { local_port } => { - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let certificate = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - Identity::load_pemfiles( - "../certificates/cert.pem", - "../certificates/key.pem", - ) - .await - .unwrap() - })); - }) - .pop() - .unwrap(); - let digest = certificate.certificate_chain().as_slice()[0].hash(); - println!("Generated self-signed certificate with digest: {}", digest); - build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - certificate, - }, - ) - } - ServerTransports::WebSocket { local_port } => crate::build_server_netcode_config( - settings.server.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketServer { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), - }, - ), - ServerTransports::Steam { - app_id, - server_ip, - game_port, - query_port, - } => server::NetConfig::Steam { - config: server::SteamConfig { - app_id: *app_id, - server_ip: *server_ip, - game_port: *game_port, - query_port: *query_port, - max_clients: 16, - version: "1.0".to_string(), - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - }) - .collect() -} - -/// Build a netcode config for the client -pub fn build_client_netcode_config( - client_id: u64, - server_addr: SocketAddr, - conditioner: Option<&Conditioner>, - shared: &SharedSettings, - transport_config: TransportConfig, -) -> client::NetConfig { - let conditioner = conditioner.map_or(None, |c| Some(c.build())); - let auth = Authentication::Manual { - server_addr, - client_id, - private_key: shared.private_key, - protocol_id: shared.protocol_id, - }; - let netcode_config = client::NetcodeConfig::default(); - let io_config = IoConfig { - transport: transport_config, - conditioner, - compression: shared.compression, - }; - client::NetConfig::Netcode { - auth, - config: netcode_config, - io: io_config, - } -} - -/// Parse the settings into a `NetConfig` that is used to configure how the lightyear client -/// connects to the server -pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { - let server_addr = SocketAddr::new( - settings.client.server_addr.into(), - settings.client.server_port, - ); - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client.client_port); - match &settings.client.transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::UdpSocket(client_addr), - ), - ClientTransports::WebTransport { certificate_digest } => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest: certificate_digest.to_string().replace(":", ""), - }, - ), - ClientTransports::WebSocket => build_client_netcode_config( - client_id, - server_addr, - settings.client.conditioner.as_ref(), - &settings.shared, - TransportConfig::WebSocketClient { server_addr }, - ), - #[cfg(not(target_family = "wasm"))] - ClientTransports::Steam { app_id } => client::NetConfig::Steam { - config: SteamConfig { - server_addr, - app_id: *app_id, - }, - conditioner: settings - .server - .conditioner - .as_ref() - .map_or(None, |c| Some(c.build())), - }, - } -} diff --git a/examples/simple_box/src/shared.rs b/examples/simple_box/src/shared.rs index c2567f6ee..cc735ab7a 100644 --- a/examples/simple_box/src/shared.rs +++ b/examples/simple_box/src/shared.rs @@ -13,18 +13,7 @@ use lightyear::shared::config::Mode; use crate::protocol::*; -pub fn shared_config(mode: Mode) -> SharedConfig { - SharedConfig { - client_send_interval: Duration::default(), - server_send_interval: Duration::from_millis(40), - // server_send_interval: Duration::from_millis(100), - tick: TickConfig { - tick_duration: Duration::from_secs_f64(1.0 / 64.0), - }, - mode, - } -} - +#[derive(Clone)] pub struct SharedPlugin; impl Plugin for SharedPlugin { diff --git a/lightyear/Cargo.toml b/lightyear/Cargo.toml index 334d77397..d893f2313 100644 --- a/lightyear/Cargo.toml +++ b/lightyear/Cargo.toml @@ -18,7 +18,6 @@ metrics = [ "metrics-util", "metrics-tracing-context", "metrics-exporter-prometheus", - "dep:tokio", ] mock_time = ["dep:mock_instant"] webtransport = [ @@ -26,14 +25,12 @@ webtransport = [ "dep:xwt-core", "dep:xwt-web-sys", "dep:web-sys", - "dep:tokio", "dep:ring", "dep:wasm-bindgen-futures", ] leafwing = ["dep:leafwing-input-manager"] xpbd_2d = ["dep:bevy_xpbd_2d"] websocket = [ - "dep:tokio", "dep:tokio-tungstenite", "dep:futures-util", "dep:web-sys", @@ -119,7 +116,7 @@ futures-util = { version = "0.3.30", optional = true } tokio = { version = "1.36", features = [ "sync", "macros", -], default-features = false, optional = true } +], default-features = false } futures = "0.3.30" async-compat = "0.2.3" async-channel = "2.2.0" diff --git a/lightyear/src/channel/builder.rs b/lightyear/src/channel/builder.rs index 48d18e371..f6d0d7f78 100644 --- a/lightyear/src/channel/builder.rs +++ b/lightyear/src/channel/builder.rs @@ -27,7 +27,7 @@ pub struct ChannelContainer { pub(crate) sender: ChannelSender, } -/// A Channel is an abstraction for a way to send messages over the network +/// A `Channel` is an abstraction for a way to send messages over the network /// You can define the direction, ordering, reliability of the channel. /// /// # Example @@ -36,9 +36,10 @@ pub struct ChannelContainer { /// they can be lost (no reliability guarantee) and they can be sent in both directions. /// /// ```rust,ignore +/// #[derive(Channel)] /// struct MyChannel; /// -/// protocol.add_channel::(ChannelSettings { +/// app.add_channel::(ChannelSettings { /// mode: ChannelMode::UnorderedUnreliable, /// direction: ChannelDirection::Bidirectional, /// priority: 1.0, diff --git a/lightyear/src/client/config.rs b/lightyear/src/client/config.rs index 5f2f26cf5..b631caa0a 100644 --- a/lightyear/src/client/config.rs +++ b/lightyear/src/client/config.rs @@ -8,7 +8,6 @@ use nonzero_ext::nonzero; use crate::client::input::InputConfig; use crate::client::interpolation::plugin::InterpolationConfig; use crate::client::prediction::plugin::PredictionConfig; -use crate::client::replication::ReplicationConfig; use crate::client::sync::SyncConfig; use crate::connection::client::NetConfig; use crate::shared::config::{Mode, SharedConfig}; @@ -111,5 +110,4 @@ pub struct ClientConfig { pub sync: SyncConfig, pub prediction: PredictionConfig, pub interpolation: InterpolationConfig, - pub replication: ReplicationConfig, } diff --git a/lightyear/src/client/connection.rs b/lightyear/src/client/connection.rs index 37edbbeb1..b316355ed 100644 --- a/lightyear/src/client/connection.rs +++ b/lightyear/src/client/connection.rs @@ -20,7 +20,7 @@ use crate::inputs::native::input_buffer::InputBuffer; use crate::packet::message_manager::MessageManager; use crate::packet::packet::Packet; use crate::packet::packet_manager::{Payload, PACKET_BUFFER_CAPACITY}; -use crate::prelude::{Channel, ChannelKind, ClientId, Message, NetworkTarget, TargetEntity}; +use crate::prelude::{Channel, ChannelKind, ClientId, Message, ReplicationGroup, TargetEntity}; use crate::protocol::channel::ChannelRegistry; use crate::protocol::component::{ComponentNetId, ComponentRegistry}; use crate::protocol::message::MessageRegistry; @@ -36,12 +36,13 @@ use crate::shared::events::connection::ConnectionEvents; use crate::shared::message::MessageSend; use crate::shared::ping::manager::{PingConfig, PingManager}; use crate::shared::ping::message::{Ping, Pong, SyncMessage}; -use crate::shared::replication::components::{Replicate, ReplicationGroupId}; +use crate::shared::replication::components::{Replicate, ReplicationGroupId, ReplicationTarget}; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::replication::receive::ReplicationReceiver; use crate::shared::replication::send::ReplicationSender; -use crate::shared::replication::systems::DespawnMetadata; -use crate::shared::replication::ReplicationMessageData; +use crate::shared::replication::systems::ReplicateCache; use crate::shared::replication::{ReplicationMessage, ReplicationSend}; +use crate::shared::replication::{ReplicationMessageData, ReplicationPeer, ReplicationReceive}; use crate::shared::sets::{ClientMarker, ServerMarker}; use crate::shared::tick_manager::Tick; use crate::shared::tick_manager::TickManager; @@ -80,7 +81,7 @@ pub struct ConnectionManager { /// Stores some values that are needed to correctly replicate the despawning of Replicated entity. /// (when the entity is despawned, we don't have access to its components anymore, so we cache them here) - replicate_component_cache: EntityHashMap, + pub(crate) replicate_component_cache: EntityHashMap, /// Used to transfer raw bytes to a system that can convert the bytes to the actual type pub(crate) received_messages: HashMap>, @@ -464,91 +465,41 @@ impl MessageSend for ConnectionManager { } } -impl ReplicationSend for ConnectionManager { +impl ReplicationPeer for ConnectionManager { type Events = ConnectionEvents; type EventContext = (); type SetMarker = ClientMarker; +} +impl ReplicationReceive for ConnectionManager { fn events(&mut self) -> &mut Self::Events { &mut self.events } - fn writer(&mut self) -> &mut BitcodeWriter { - &mut self.writer - } - - fn component_registry(&self) -> &ComponentRegistry { - &self.component_registry + fn cleanup(&mut self, tick: Tick) { + self.replication_receiver.cleanup(tick); } +} - fn update_priority( - &mut self, - replication_group_id: ReplicationGroupId, - client_id: ClientId, - priority: f32, - ) -> Result<()> { - self.replication_sender - .update_base_priority(replication_group_id, priority); - Ok(()) +impl ReplicationSend for ConnectionManager { + fn writer(&mut self) -> &mut BitcodeWriter { + &mut self.writer } fn new_connected_clients(&self) -> Vec { vec![] } - fn prepare_entity_spawn( - &mut self, - entity: Entity, - replicate: &Replicate, - target: NetworkTarget, - system_current_tick: BevyTick, - ) -> Result<()> { - trace!(?entity, "Prepare entity spawn to server"); - let group_id = replicate.replication_group.group_id(Some(entity)); - let replication_sender = &mut self.replication_sender; - // update the collect changes tick - // (we can collect changes only since the last actions because all updates will wait for that action to be spawned) - // TODO: I don't think it's correct to update the change-tick since the latest action! - // replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); - match replicate.target_entity { - TargetEntity::Spawn => { - replication_sender.prepare_entity_spawn(entity, group_id); - } - TargetEntity::Preexisting(remote_entity) => { - replication_sender.prepare_entity_spawn_reuse(entity, group_id, remote_entity); - } - } - // also set the priority for the group when we spawn it - self.update_priority( - group_id, - // the client id argument is ignored on the client - ClientId::Local(0), - replicate.replication_group.priority(), - )?; - Ok(()) - } - fn prepare_entity_despawn( &mut self, entity: Entity, - replication_group_id: ReplicationGroupId, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()> { + let group_id = group.group_id(Some(entity)); // trace!(?entity, "Send entity despawn for tick {:?}", self.tick()); let replication_sender = &mut self.replication_sender; - // update the collect changes tick - // replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); - replication_sender.prepare_entity_despawn(entity, replication_group_id); - // Prediction/interpolation + replication_sender.prepare_entity_despawn(entity, group_id); Ok(()) } @@ -557,23 +508,18 @@ impl ReplicationSend for ConnectionManager { entity: Entity, kind: ComponentNetId, component: RawData, - replicate: &Replicate, + component_registry: &ComponentRegistry, + replication_target: &ReplicationTarget, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()> { - let group_id = replicate.replication_group.group_id(Some(entity)); + let group_id = group.group_id(Some(entity)); // debug!( // ?entity, // component = ?kind, // tick = ?self.tick_manager.tick(), // "Inserting single component" // ); - // update the collect changes tick - // self.replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); self.replication_sender .prepare_component_insert(entity, group_id, kind, component); Ok(()) @@ -583,17 +529,11 @@ impl ReplicationSend for ConnectionManager { &mut self, entity: Entity, component_kind: ComponentNetId, - replicate: &Replicate, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()> { - let group_id = replicate.replication_group.group_id(Some(entity)); + let group_id = group.group_id(Some(entity)); debug!(?entity, ?component_kind, "Sending RemoveComponent"); - // self.replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); self.replication_sender .prepare_component_remove(entity, group_id, component_kind); Ok(()) @@ -604,12 +544,12 @@ impl ReplicationSend for ConnectionManager { entity: Entity, kind: ComponentNetId, component: RawData, - replicate: &Replicate, + group: &ReplicationGroup, target: NetworkTarget, component_change_tick: BevyTick, system_current_tick: BevyTick, ) -> Result<()> { - let group_id = replicate.group_id(Some(entity)); + let group_id = group.group_id(Some(entity)); // TODO: should we have additional state tracking so that we know we are in the process of sending this entity to clients? let collect_changes_since_this_tick = self .replication_sender @@ -644,39 +584,11 @@ impl ReplicationSend for ConnectionManager { let _span = trace_span!("buffer_replication_messages").entered(); self.buffer_replication_messages(tick, bevy_tick) } - fn get_mut_replicate_despawn_cache(&mut self) -> &mut EntityHashMap { + fn get_mut_replicate_cache(&mut self) -> &mut EntityHashMap { &mut self.replicate_component_cache } fn cleanup(&mut self, tick: Tick) { debug!("Running replication clean"); - // if it's been enough time since we last any action for the group, we can set the last_action_tick to None - // (meaning that there's no need when we receive the update to check if we have already received a previous action) - for group_channel in self.replication_sender.group_channels.values_mut() { - debug!("Checking group channel: {:?}", group_channel); - if let Some(last_action_tick) = group_channel.last_action_tick { - if tick - last_action_tick > (i16::MAX / 2) { - debug!( - ?tick, - ?last_action_tick, - ?group_channel, - "Setting the last_action tick to None because there hasn't been any new actions in a while"); - group_channel.last_action_tick = None; - } - } - } - // if it's been enough time since we last had any update for the group, we update the latest_tick for the group - for group_channel in self.replication_receiver.group_channels.values_mut() { - debug!("Checking group channel: {:?}", group_channel); - if let Some(latest_tick) = group_channel.latest_tick { - if tick - latest_tick > (i16::MAX / 2) { - debug!( - ?tick, - ?latest_tick, - ?group_channel, - "Setting the latest_tick tick to tick because there hasn't been any new updates in a while"); - group_channel.latest_tick = Some(tick); - } - } - } + self.replication_sender.cleanup(tick); } } diff --git a/lightyear/src/client/events.rs b/lightyear/src/client/events.rs index 087ed4d8c..27b635cc6 100644 --- a/lightyear/src/client/events.rs +++ b/lightyear/src/client/events.rs @@ -31,6 +31,7 @@ impl Plugin for ClientEventsPlugin { app // EVENTS .add_event::() + .add_event::() // PLUGIN .add_plugins(EventsPlugin::::default()); } @@ -64,7 +65,9 @@ impl ConnectEvent { } /// Bevy [`Event`] emitted on the client on the frame where the connection is disconnected -pub type DisconnectEvent = crate::shared::events::components::DisconnectEvent<()>; +#[derive(Event, Default)] +pub struct DisconnectEvent; + /// Bevy [`Event`] emitted on the client to indicate the user input for the tick pub type InputEvent = crate::shared::events::components::InputEvent; /// Bevy [`Event`] emitted on the client when a EntitySpawn replication message is received diff --git a/lightyear/src/client/input.rs b/lightyear/src/client/input.rs index af1788298..5d0b60f45 100644 --- a/lightyear/src/client/input.rs +++ b/lightyear/src/client/input.rs @@ -1,24 +1,44 @@ -//! Module to generate client inputs +//! Module to handle client inputs //! -//! Client inputs are generated by the user and sent to the client. +//! Client inputs are generated by the user and sent to the server. //! They have to be handled separately from other messages, for several reasons: //! - the history of inputs might need to be saved on the client to perform rollback and client-prediction //! - we not only send the input for tick T, but we also include the inputs for the last N ticks before T. This redundancy helps ensure //! that the server isn't missing any client inputs even if a packet gets lost //! - we must provide [`SystemSet`]s so that the user can order their systems before and after the input handling //! -//! Inputs have to be defined as a single enum, which includes a variant for "no input" (for example, `None`). +//! ### Adding a new input type //! -//! ```no_run +//! An input type is an enum that implements the [`UserAction`] trait. +//! This trait is a marker trait that is used to tell Lightyear that this type can be used as an input. +//! In particular inputs must be `Serialize`, `Deserialize`, `Clone` and `PartialEq`. +//! +//! You can then add the input type by adding the [`InputPlugin`](crate::prelude::InputPlugin) to your app. +//! +//! ```rust +//! use bevy::prelude::*; +//! use lightyear::prelude::*; +//! +//! #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] //! pub enum MyInput { -//! Left, -//! Right, -//! Jump, -//! None, +//! Move { x: f32, y: f32 }, +//! Jump, +//! // we need a variant for "no input", to differentiate between "no input" and "missing input packet" +//! None, //! } +//! +//! let mut app = App::new(); +//! app.add_plugins(InputPlugin::::default()); //! ``` //! -//! You will also need to implement a system in the [`InputSystemSet::BufferInputs`] system set to add inputs to the input buffer every tick. +//! ### Sending inputs +//! +//! There are several steps to use the `InputPlugin`: +//! - (optional) read the inputs from an external signal (mouse click or keyboard press, for instance) +//! - to buffer inputs for each tick. This is done by calling [`add_input`](InputManager::add_input) in a system. +//! That system must run in the [`InputSystemSet::BufferInputs`] system set, in the `FixedPreUpdate` stage. +//! - handle inputs in your game logic in systems that run in the `FixedUpdate` schedule. These systems +//! will read the inputs using the [`InputEvent`] event. //! //! NOTE: I would advise to activate the `leafwing` feature to handle inputs via the `input_leafwing` module, instead. //! That module is more up-to-date and has more features. diff --git a/lightyear/src/client/input_leafwing.rs b/lightyear/src/client/input_leafwing.rs index 43dd5115a..7a80bbcdf 100644 --- a/lightyear/src/client/input_leafwing.rs +++ b/lightyear/src/client/input_leafwing.rs @@ -1,14 +1,17 @@ //! Module to handle inputs that are defined using the `leafwing_input_manager` crate //! -//! ## Creation +//! ### Adding leafwing inputs //! //! You first need to create Inputs that are defined using the [`leafwing_input_manager`](https://github.com/Leafwing-Studios/leafwing-input-manager) crate. //! (see the documentation of the crate for more information) //! In particular your inputs should implement the [`Actionlike`] trait. -//! You will also need to implement the `LeafwingUserAction` trait //! -//! ```no_run,ignore -//! # use lightyear::prelude::LeafwingUserAction; +//! ```rust +//! use bevy::prelude::*; +//! use lightyear::prelude::*; +//! use lightyear::prelude::client::*; +//! use leafwing_input_manager::Actionlike; +//! use serde::{Deserialize, Serialize}; //! #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect, Actionlike)] //! pub enum PlayerActions { //! Up, @@ -16,10 +19,14 @@ //! Left, //! Right, //! } -//! impl LeafwingUserAction for PlayerActions {} +//! +//! fn main() { +//! let mut app = App::new(); +//! app.add_plugins(LeafwingInputPlugin::::default()); +//! } //! ``` //! -//! ## Usage +//! ### Usage //! //! The networking of inputs is completely handled for you. You just need to add the `LeafwingInputPlugin` to your app. //! Make sure that all your systems that depend on user inputs are added to the [`FixedUpdate`] [`Schedule`]. @@ -963,7 +970,7 @@ mod tests { incoming_loss: 0.0, }; let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); + let prediction_config = PredictionConfig::default(); let interpolation_config = InterpolationConfig::default(); let mut stepper = BevyStepper::new( shared_config, diff --git a/lightyear/src/client/interpolation/visual_interpolation.rs b/lightyear/src/client/interpolation/visual_interpolation.rs index 4642b2b9b..c53b4cef2 100644 --- a/lightyear/src/client/interpolation/visual_interpolation.rs +++ b/lightyear/src/client/interpolation/visual_interpolation.rs @@ -23,7 +23,7 @@ //! - To enable VisualInterpolation on a given entity, you need to add the `VisualInterpolateStatus` component to it manually //! ```rust,no_run,ignore //! fn spawn_entity(mut commands: Commands) { -//! commands.spawn().insert(VisualInterpolateState::::default()); +//! commands.spawn().insert(VisualInterpolateStatus::::default()); //! } //! ``` @@ -35,7 +35,7 @@ use bevy::prelude::*; use crate::client::components::{ComponentSyncMode, SyncComponent, SyncMetadata}; -use crate::prelude::client::InterpolationSet; +use crate::prelude::client::{InterpolationSet, PredictionSet}; use crate::prelude::{ComponentRegistry, TickManager, TimeManager}; pub struct VisualInterpolationPlugin { @@ -52,11 +52,16 @@ impl Default for VisualInterpolationPlugin { impl Plugin for VisualInterpolationPlugin { fn build(&self, app: &mut App) { - // TODO: put the non-component specific stuff in a different plugin - // REFLECTION - app.register_type::(); // SETS - app.configure_sets(PreUpdate, InterpolationSet::RestoreVisualInterpolation); + app.configure_sets( + PreUpdate, + // make sure that we restore the actual component value before we perform a rollback check + ( + InterpolationSet::RestoreVisualInterpolation, + PredictionSet::CheckRollback, + ) + .chain(), + ); app.configure_sets( FixedPostUpdate, InterpolationSet::UpdateVisualInterpolationState, @@ -108,10 +113,6 @@ impl Default for VisualInterpolateStatus { } } -/// Marker component to indicate that this entity will be visually interpolated -#[derive(Component, Debug, Reflect)] -pub struct VisualInterpolateMarker; - // TODO: explore how we could allow this for non-marker components, user would need to specify the interpolation function? // (to avoid orphan rule) /// Currently we will only support components that are present in the protocol and have a SyncMetadata implementation @@ -205,7 +206,7 @@ mod tests { incoming_loss: 0.0, }; let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); + let prediction_config = PredictionConfig::default(); let interpolation_config = InterpolationConfig::default(); let mut stepper = BevyStepper::new( shared_config, diff --git a/lightyear/src/client/io/config.rs b/lightyear/src/client/io/config.rs new file mode 100644 index 000000000..fba460855 --- /dev/null +++ b/lightyear/src/client/io/config.rs @@ -0,0 +1,142 @@ +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportBuilderEnum}; +use crate::client::io::{Io, IoContext}; +use crate::prelude::CompressionConfig; +use crate::transport::config::SharedIoConfig; +use crate::transport::dummy::DummyIo; +use crate::transport::error::{Error, Result}; +use crate::transport::io::{BaseIo, IoStats}; +use crate::transport::local::LocalChannelBuilder; +#[cfg(feature = "zstd")] +use crate::transport::middleware::compression::zstd::compression::ZstdCompressor; +#[cfg(feature = "zstd")] +use crate::transport::middleware::compression::zstd::decompression::ZstdDecompressor; +use crate::transport::middleware::conditioner::LinkConditioner; +use crate::transport::middleware::{PacketReceiverWrapper, PacketSenderWrapper}; +#[cfg(not(target_family = "wasm"))] +use crate::transport::udp::UdpSocketBuilder; +#[cfg(feature = "websocket")] +use crate::transport::websocket::client::WebSocketClientSocketBuilder; +#[cfg(feature = "webtransport")] +use crate::transport::webtransport::client::WebTransportClientSocketBuilder; +use crate::transport::{BoxedReceiver, Transport, LOCAL_SOCKET}; +use bevy::prelude::TypePath; +use crossbeam_channel::{Receiver, Sender}; +use std::net::SocketAddr; + +/// Use this to configure the [`Transport`] that will be used to establish a connection with the +/// server. +#[derive(Clone, Debug, TypePath)] +pub enum ClientTransport { + /// Use a [`UdpSocket`](std::net::UdpSocket) + #[cfg(not(target_family = "wasm"))] + UdpSocket(SocketAddr), + /// Use [`WebTransport`](https://wicg.github.io/web-transport/) as a transport layer + #[cfg(feature = "webtransport")] + WebTransportClient { + client_addr: SocketAddr, + server_addr: SocketAddr, + /// On wasm, we need to provide a hash of the certificate to the browser + #[cfg(target_family = "wasm")] + certificate_digest: String, + }, + /// Use [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) as a transport + #[cfg(feature = "websocket")] + WebSocketClient { server_addr: SocketAddr }, + /// Use a crossbeam_channel as a transport. This is useful for testing. + /// This is mostly for clients. + LocalChannel { + recv: Receiver>, + send: Sender>, + }, + /// Dummy transport if the connection handles its own io (for example steam sockets) + Dummy, +} + +impl ClientTransport { + pub(super) fn build(self) -> ClientTransportBuilderEnum { + match self { + #[cfg(not(target_family = "wasm"))] + ClientTransport::UdpSocket(addr) => { + ClientTransportBuilderEnum::UdpSocket(UdpSocketBuilder { local_addr: addr }) + } + #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] + ClientTransport::WebTransportClient { + client_addr, + server_addr, + } => ClientTransportBuilderEnum::WebTransportClient(WebTransportClientSocketBuilder { + client_addr, + server_addr, + }), + #[cfg(all(feature = "webtransport", target_family = "wasm"))] + ClientTransport::WebTransportClient { + client_addr, + server_addr, + certificate_digest, + } => TransportBuilderEnum::WebTransportClient(WebTransportClientSocketBuilder { + client_addr, + server_addr, + certificate_digest, + }), + #[cfg(feature = "websocket")] + ClientTransport::WebSocketClient { server_addr } => { + ClientTransportBuilderEnum::WebSocketClient(WebSocketClientSocketBuilder { + server_addr, + }) + } + ClientTransport::LocalChannel { recv, send } => { + ClientTransportBuilderEnum::LocalChannel(LocalChannelBuilder { recv, send }) + } + ClientTransport::Dummy => ClientTransportBuilderEnum::Dummy(DummyIo), + } + } +} + +impl Default for ClientTransport { + #[cfg(not(target_family = "wasm"))] + fn default() -> Self { + ClientTransport::UdpSocket(LOCAL_SOCKET) + } + + #[cfg(target_family = "wasm")] + fn default() -> Self { + let (send, recv) = crossbeam_channel::unbounded(); + ClientTransport::LocalChannel { recv, send } + } +} + +impl SharedIoConfig { + pub fn connect(self) -> Result { + let (transport, state, io_rx, network_tx) = self.transport.build().connect()?; + let local_addr = transport.local_addr(); + #[allow(unused_mut)] + let (mut sender, receiver) = transport.split(); + #[allow(unused_mut)] + let mut receiver: BoxedReceiver = if let Some(conditioner_config) = self.conditioner { + let conditioner = LinkConditioner::new(conditioner_config); + Box::new(conditioner.wrap(receiver)) + } else { + Box::new(receiver) + }; + match self.compression { + CompressionConfig::None => {} + #[cfg(feature = "zstd")] + CompressionConfig::Zstd { level } => { + let compressor = ZstdCompressor::new(level); + sender = Box::new(compressor.wrap(sender)); + let decompressor = ZstdDecompressor::new(); + receiver = Box::new(decompressor.wrap(receiver)); + } + } + Ok(BaseIo { + local_addr, + sender, + receiver, + state, + stats: IoStats::default(), + context: IoContext { + event_sender: network_tx, + event_receiver: io_rx, + }, + }) + } +} diff --git a/lightyear/src/client/io/mod.rs b/lightyear/src/client/io/mod.rs new file mode 100644 index 000000000..e1101281b --- /dev/null +++ b/lightyear/src/client/io/mod.rs @@ -0,0 +1,45 @@ +//! Wrapper around a transport, that can perform additional transformations such as +//! bandwidth monitoring or compression +pub(crate) mod config; +pub(crate) mod transport; + +use crate::transport::error::{Error, Result}; +use crate::transport::io::{BaseIo, IoState}; +use async_channel::{Receiver, Sender}; +use bevy::prelude::{Deref, DerefMut, Real, Res, Resource, Time}; +use tokio::sync::mpsc; + +pub struct IoContext { + pub(crate) event_sender: Option, + pub(crate) event_receiver: Option, +} + +/// Client IO +pub type Io = BaseIo; + +impl Io { + pub fn close(&mut self) -> Result<()> { + self.state = IoState::Disconnected; + if let Some(event_sender) = self.context.event_sender.as_mut() { + event_sender + .send_blocking(ClientIoEvent::Disconnected( + std::io::Error::other("client requested disconnection").into(), + )) + .map_err(Error::from)?; + } + Ok(()) + } +} + +/// Events that will be sent from the io thread to the main thread +/// (so that we can update the netcode state when the io changes) +pub(crate) enum ClientIoEvent { + Connected, + Disconnected(Error), +} + +#[derive(Deref, DerefMut)] +pub(crate) struct ClientIoEventReceiver(pub(crate) Receiver); + +#[derive(Deref, DerefMut)] +pub(crate) struct ClientNetworkEventSender(pub(crate) Sender); diff --git a/lightyear/src/client/io/transport.rs b/lightyear/src/client/io/transport.rs new file mode 100644 index 000000000..42d5bc67c --- /dev/null +++ b/lightyear/src/client/io/transport.rs @@ -0,0 +1,56 @@ +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; +use crate::transport::dummy::DummyIo; +use crate::transport::error::Result; +use crate::transport::io::IoState; +use crate::transport::local::{LocalChannel, LocalChannelBuilder}; +#[cfg(not(target_family = "wasm"))] +use crate::transport::udp::{UdpSocket, UdpSocketBuilder}; +#[cfg(feature = "websocket")] +use crate::transport::websocket::client::{WebSocketClientSocket, WebSocketClientSocketBuilder}; +#[cfg(feature = "webtransport")] +use crate::transport::webtransport::client::{ + WebTransportClientSocket, WebTransportClientSocketBuilder, +}; +use enum_dispatch::enum_dispatch; + +/// Transport combines a PacketSender and a PacketReceiver +/// +/// This trait is used to abstract the raw transport layer that sends and receives packets. +/// There are multiple implementations of this trait, such as UdpSocket, WebSocket, WebTransport, etc. +#[enum_dispatch] +pub(crate) trait ClientTransportBuilder: Send + Sync { + /// Attempt to connect to the remote + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )>; +} + +#[enum_dispatch(ClientTransportBuilder)] +pub(crate) enum ClientTransportBuilderEnum { + #[cfg(not(target_family = "wasm"))] + UdpSocket(UdpSocketBuilder), + #[cfg(feature = "webtransport")] + WebTransportClient(WebTransportClientSocketBuilder), + #[cfg(feature = "websocket")] + WebSocketClient(WebSocketClientSocketBuilder), + LocalChannel(LocalChannelBuilder), + Dummy(DummyIo), +} + +#[allow(clippy::large_enum_variant)] +#[enum_dispatch(Transport)] +pub(crate) enum ClientTransportEnum { + #[cfg(not(target_family = "wasm"))] + UdpSocket(UdpSocket), + #[cfg(feature = "webtransport")] + WebTransportClient(WebTransportClientSocket), + #[cfg(feature = "websocket")] + WebSocketClient(WebSocketClientSocket), + LocalChannel(LocalChannel), + Dummy(DummyIo), +} diff --git a/lightyear/src/client/message.rs b/lightyear/src/client/message.rs index 19f20890b..3259d33e8 100644 --- a/lightyear/src/client/message.rs +++ b/lightyear/src/client/message.rs @@ -12,7 +12,7 @@ use crate::client::connection::ConnectionManager; use crate::client::events::MessageEvent; use crate::client::networking::is_connected; use crate::packet::message::SingleData; -use crate::prelude::{ChannelDirection, ChannelKind, MainSet, Message, NetworkTarget}; +use crate::prelude::{ChannelDirection, ChannelKind, Message}; use crate::protocol::message::{MessageKind, MessageRegistry}; use crate::protocol::registry::NetId; use crate::protocol::BitSerializable; @@ -20,12 +20,13 @@ use crate::serialize::reader::ReadBuffer; use crate::serialize::writer::WriteBuffer; use crate::serialize::RawData; use crate::shared::ping::message::{Ping, Pong, SyncMessage}; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::replication::{ReplicationMessage, ReplicationMessageData}; use crate::shared::sets::{ClientMarker, InternalMainSet}; // ClientMessages can include some extra Metadata #[derive(Encode, Decode, Clone, Debug)] -pub enum ClientMessage { +pub(crate) enum ClientMessage { #[bitcode_hint(frequency = 2)] // #[bitcode(with_serde)] Message(RawData, NetworkTarget), diff --git a/lightyear/src/client/mod.rs b/lightyear/src/client/mod.rs index 06c3c5732..52f46ccba 100644 --- a/lightyear/src/client/mod.rs +++ b/lightyear/src/client/mod.rs @@ -24,6 +24,7 @@ mod easings; #[cfg_attr(docsrs, doc(cfg(feature = "leafwing")))] #[cfg(feature = "leafwing")] pub mod input_leafwing; +pub(crate) mod io; pub(crate) mod message; pub(crate) mod networking; pub mod replication; diff --git a/lightyear/src/client/networking.rs b/lightyear/src/client/networking.rs index 85e2abca3..6ccc4ed00 100644 --- a/lightyear/src/client/networking.rs +++ b/lightyear/src/client/networking.rs @@ -13,14 +13,16 @@ use crate::client::config::ClientConfig; use crate::client::connection::ConnectionManager; use crate::client::events::{ConnectEvent, DisconnectEvent, EntityDespawnEvent, EntitySpawnEvent}; use crate::client::interpolation::Interpolated; +use crate::client::io::ClientIoEvent; use crate::client::prediction::Predicted; use crate::client::sync::SyncSet; use crate::connection::client::{ClientConnection, NetClient, NetConfig}; -use crate::connection::server::ServerConnections; +use crate::connection::server::{IoConfig, ServerConnections}; use crate::prelude::{ ChannelRegistry, MainSet, MessageRegistry, SharedConfig, TickManager, TimeManager, }; use crate::protocol::component::ComponentRegistry; +use crate::server::clients::ControlledEntities; use crate::server::networking::is_started; use crate::shared::config::Mode; use crate::shared::events::connection::{IterEntityDespawnEvent, IterEntitySpawnEvent}; @@ -36,8 +38,12 @@ pub(crate) struct ClientNetworkingPlugin; impl Plugin for ClientNetworkingPlugin { fn build(&self, app: &mut App) { app + // REFLECTION + .register_type::() // STATE .init_state::() + // RESOURCE + .init_resource::() // SYSTEM SETS .configure_sets( PreUpdate, @@ -76,7 +82,17 @@ impl Plugin for ClientNetworkingPlugin { // SYSTEMS .add_systems( PreUpdate, - receive.in_set(InternalMainSet::::Receive), + listen_io_state + // we are running the listen_io_state in a different set because it can impact the run_condition for the + // Receive system set + .before(InternalMainSet::::Receive) + .run_if(not( + SharedConfig::is_host_server_condition.or_else(is_disconnected) + )), + ) + .add_systems( + PreUpdate, + (listen_io_state, receive).in_set(InternalMainSet::::Receive), ) .add_systems( PostUpdate, @@ -96,16 +112,24 @@ impl Plugin for ClientNetworkingPlugin { // CONNECTING app.add_systems(OnEnter(NetworkingState::Connecting), connect); - app.add_systems( - PreUpdate, - handle_connection_failure.run_if(in_state(NetworkingState::Connecting)), - ); // CONNECTED - app.add_systems(OnEnter(NetworkingState::Connected), on_connect); + app.add_systems( + OnEnter(NetworkingState::Connected), + ( + on_connect, + on_connect_host_server.run_if(SharedConfig::is_host_server_condition), + ), + ); // DISCONNECTED - app.add_systems(OnEnter(NetworkingState::Disconnected), on_disconnect); + app.add_systems( + OnEnter(NetworkingState::Disconnected), + ( + on_disconnect, + on_disconnect_host_server.run_if(SharedConfig::is_host_server_condition), + ), + ); } } @@ -135,15 +159,19 @@ pub(crate) fn receive(world: &mut World) { // UPDATE: update client state, send keep-alives, receive packets from io, update connection sync state time_manager.update(delta); trace!(time = ?time_manager.current_time(), tick = ?tick_manager.tick(), "receive"); - let _ = netclient - .try_update(delta.as_secs_f64()) - .map_err(|e| { - error!("Error updating netcode: {}", e); - }); + + if netclient.state() != NetworkingState::Disconnected { + let _ = netclient + .try_update(delta.as_secs_f64()) + .map_err(|e| { + error!("Error updating netcode: {}", e); + }); + } if netclient.state() == NetworkingState::Connected { // we just connected, do a state transition if state.get() != &NetworkingState::Connected { + debug!("Setting the networking state to connected"); next_state.set(NetworkingState::Connected); } @@ -261,72 +289,75 @@ pub enum NetworkingState { Connected, } -/// If we are trying to connect but the client is disconnected; we failed to connect, -/// change the state back to Disconnected. -fn handle_connection_failure( +/// Listen to [`ClientIoEvent`]s and update the [`IoState`] and [`NetworkingState`] accordingly +fn listen_io_state( mut next_state: ResMut>, mut netclient: ResMut, ) { - // first check the status of the io - if netclient.io_mut().is_some_and(|io| match &mut io.state { - IoState::Connecting { - ref mut error_channel, - } => match error_channel.try_recv() { - Ok(Some(e)) => { - error!("Error starting the io: {}", e); - io.state = IoState::Disconnected; - true - } - Ok(None) => { - debug!("Io is connected!"); - io.state = IoState::Connected; - false - } - // we are still connecting the io, and there is no error yet - Err(TryRecvError::Empty) => { - debug!("we are still connecting the io, and there is no error yet"); - false - } - // we are still connecting the io, but the channel has been closed, this looks - // like an error - Err(TryRecvError::Closed) => { - error!("Io status channel has been closed when it shouldn't be"); - true + let mut disconnect = false; + if let Some(io) = netclient.io_mut() { + if let Some(receiver) = io.context.event_receiver.as_mut() { + match receiver.try_recv() { + Ok(ClientIoEvent::Connected) => { + debug!("Io is connected!"); + io.state = IoState::Connected; + } + Ok(ClientIoEvent::Disconnected(e)) => { + error!("Error from io: {}", e); + io.state = IoState::Disconnected; + disconnect = true; + } + Err(TryRecvError::Empty) => { + trace!("we are still connecting the io, and there is no error yet"); + } + Err(TryRecvError::Closed) => { + error!("Io status channel has been closed when it shouldn't be"); + disconnect = true; + } } - }, - _ => false, - }) { - info!("Setting the next state to disconnected because of io"); - next_state.set(NetworkingState::Disconnected); + } } - if netclient.state() == NetworkingState::Disconnected { - info!("Setting the next state to disconnected because of client connection error"); + if disconnect { + debug!("Going to NetworkingState::Disconnected because of io error."); next_state.set(NetworkingState::Disconnected); + let _ = netclient + .disconnect() + .inspect_err(|e| debug!("error disconnecting netclient: {e:?}")); } } +/// Holds metadata necessary when running in HostServer mode +#[derive(Resource, Default)] +struct HostServerMetadata { + /// entity for the client running as host-server + client_entity: Option, +} + /// System that runs when we enter the Connected state /// Updates the ConnectEvent events -fn on_connect( - mut connect_event_writer: EventWriter, - netcode: Res, - config: Res, - mut server_connect_event_writer: Option>>, -) { - info!( +fn on_connect(mut connect_event_writer: EventWriter, netcode: Res) { + debug!( "Running OnConnect schedule with client id: {:?}", netcode.id() ); connect_event_writer.send(ConnectEvent::new(netcode.id())); +} - // in host-server mode, we also want to send a connect event to the server - if config.shared.mode == Mode::HostServer { - info!("send connect event to server"); - server_connect_event_writer - .as_mut() - .unwrap() - .send(crate::server::events::ConnectEvent::new(netcode.id())); - } +/// Same as on-connect, but only runs if we are in host-server mode +fn on_connect_host_server( + mut commands: Commands, + netcode: Res, + mut metadata: ResMut, + mut server_connect_event_writer: ResMut>, +) { + // spawn an entity for the client + let client_entity = commands.spawn(ControlledEntities::default()).id(); + error!("send connect event to server"); + server_connect_event_writer.send(crate::server::events::ConnectEvent { + client_id: netcode.id(), + entity: client_entity, + }); + metadata.client_entity = Some(client_entity); } /// System that runs when we enter the Disconnected state @@ -335,10 +366,6 @@ fn on_disconnect( mut connection_manager: ResMut, mut disconnect_event_writer: EventWriter, mut netcode: ResMut, - config: Res, - mut server_disconnect_event_writer: Option< - ResMut>, - >, mut commands: Commands, received_entities: Query, With, With)>>, ) { @@ -356,19 +383,24 @@ fn on_disconnect( // no need to update the io state, because we will recreate a new `ClientConnection` // for the next connection attempt - disconnect_event_writer.send(DisconnectEvent::new(())); - - // in host-server mode, we also want to send a connect event to the server - if config.shared.mode == Mode::HostServer { - server_disconnect_event_writer - .as_mut() - .unwrap() - .send(crate::server::events::DisconnectEvent::new(netcode.id())); - } - + disconnect_event_writer.send(DisconnectEvent); // TODO: remove ClientConnection and ConnectionManager resources? } +fn on_disconnect_host_server( + netcode: Res, + mut metadata: ResMut, + mut server_disconnect_event_writer: ResMut>, +) { + let client_id = netcode.id(); + if let Some(client_entity) = std::mem::take(&mut metadata.client_entity) { + server_disconnect_event_writer.send(crate::server::events::DisconnectEvent { + client_id, + entity: client_entity, + }); + } +} + /// This run condition is provided to check if the client is connected. /// /// We check the status of the ClientConnection directly instead of using the `State` to avoid having a frame of delay @@ -387,11 +419,19 @@ pub(crate) fn is_connected(netclient: Option>) -> bool { /// We check the status of the ClientConnection directly instead of using the `State` to avoid having a frame of delay /// since the `StateTransition` schedule runs after `PreUpdate` pub(crate) fn is_disconnected(netclient: Option>) -> bool { - netclient.map_or(true, |c| { + netclient.as_ref().map_or(true, |c| { c.state() == NetworkingState::Disconnected || c.io() .map_or(true, |io| matches!(io.state, IoState::Disconnected)) }) + // error!("Is Disconnected: {res:?}"); + // if let Some(c) = netclient.as_ref() { + // error!("ClientConnection state: {:?}", c.state()); + // if let Some(io) = c.io() { + // error!("Io state: {:?}", io.state); + // } + // } + // res } /// This runs only when we enter the [`Connecting`](NetworkingState::Connecting) state. @@ -478,8 +518,10 @@ fn connect(world: &mut World) { // } pub trait ClientCommands { + /// Start the connection process fn connect_client(&mut self); + /// Disconnect the client fn disconnect_client(&mut self); } diff --git a/lightyear/src/client/plugin.rs b/lightyear/src/client/plugin.rs index 0c3386bfe..79bea3148 100644 --- a/lightyear/src/client/plugin.rs +++ b/lightyear/src/client/plugin.rs @@ -1,4 +1,14 @@ -//! Defines the client bevy plugin +//! Defines the [`ClientPlugins`] PluginGroup +//! +//! The client consists of multiple different plugins, each with their own responsibilities. These plugins +//! are grouped into the [`ClientPlugins`] plugin group, which allows you to easily configure and disable +//! any of the existing plugins. +//! +//! This means that users can simply disable existing functionality and replace it with specialized solutions, +//! while keeping the rest of the features intact. +//! +//! Most plugins are truly necessary for the server functionality to work properly, but some could be disabled. +use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use crate::client::diagnostics::ClientDiagnosticsPlugin; @@ -6,26 +16,64 @@ use crate::client::events::ClientEventsPlugin; use crate::client::interpolation::plugin::InterpolationPlugin; use crate::client::networking::ClientNetworkingPlugin; use crate::client::prediction::plugin::PredictionPlugin; -use crate::client::replication::ClientReplicationPlugin; +use crate::client::replication::{ + receive::ClientReplicationReceivePlugin, send::ClientReplicationSendPlugin, +}; +use crate::prelude::server::{ServerConfig, ServerPlugins}; use crate::shared::config::Mode; use crate::shared::plugin::SharedPlugin; use super::config::ClientConfig; -pub struct ClientPlugin { +/// A plugin group containing all the client plugins. +/// +/// By default, the following plugins will be added: +/// - [`SetupPlugin`]: Adds the [`ClientConfig`] resource and the [`SharedPlugin`] plugin. +/// - [`ClientEventsPlugin`]: Adds the client network event +/// - [`ClientNetworkingPlugin`]: Handles the network state (connecting/disconnecting the client, sending/receiving packets) +/// - [`ClientDiagnosticsPlugin`]: Computes diagnostics about the client connection. Can be disabled if you don't need it. +/// - [`ClientReplicationReceivePlugin`]: Handles the replication of entities and resources from server to client. This can be +/// disabled if you don't need server to client replication. +/// - [`ClientReplicationSendPlugin`]: Handles the replication of entities and resources from client to server. This can be +/// disabled if you don't need client to server replication. +/// - [`PredictionPlugin`]: Handles the client-prediction systems. This can be disabled if you don't need it. +/// - [`InterpolationPlugin`]: Handles the interpolation systems. This can be disabled if you don't need it. +pub struct ClientPlugins { pub config: ClientConfig, } -impl ClientPlugin { +impl ClientPlugins { pub fn new(config: ClientConfig) -> Self { Self { config } } } -// TODO: create this as PluginGroup so that users can easily disable sub plugins? +impl PluginGroup for ClientPlugins { + fn build(self) -> PluginGroupBuilder { + let builder = PluginGroupBuilder::start::(); + let tick_interval = self.config.shared.tick.tick_duration; + let interpolation_config = self.config.interpolation.clone(); + builder + .add(SetupPlugin { + config: self.config, + }) + .add(ClientEventsPlugin) + .add(ClientNetworkingPlugin) + .add(ClientDiagnosticsPlugin) + .add(ClientReplicationReceivePlugin { tick_interval }) + .add(ClientReplicationSendPlugin { tick_interval }) + .add(PredictionPlugin) + .add(InterpolationPlugin::new(interpolation_config)) + } +} + +struct SetupPlugin { + config: ClientConfig, +} + // TODO: override `ready` and `finish` to make sure that the transport/backend is connected // before the plugin is ready -impl Plugin for ClientPlugin { +impl Plugin for SetupPlugin { fn build(&self, app: &mut App) { app // RESOURCES // @@ -33,21 +81,13 @@ impl Plugin for ClientPlugin { // TODO: how do we make sure that SharedPlugin is only added once if we want to switch between // HostServer and Separate mode? - if self.config.shared.mode == Mode::Separate { + // if self.config.shared.mode == Mode::Separate { + if !app.is_plugin_added::() { app // PLUGINS .add_plugins(SharedPlugin { config: self.config.shared.clone(), }); } - - app - // PLUGINS // - .add_plugins(ClientNetworkingPlugin) - .add_plugins(ClientEventsPlugin) - .add_plugins(ClientDiagnosticsPlugin) - .add_plugins(ClientReplicationPlugin) - .add_plugins(PredictionPlugin) - .add_plugins(InterpolationPlugin::new(self.config.interpolation.clone())); } } diff --git a/lightyear/src/client/prediction/correction.rs b/lightyear/src/client/prediction/correction.rs index c066dbe77..f85fca467 100644 --- a/lightyear/src/client/prediction/correction.rs +++ b/lightyear/src/client/prediction/correction.rs @@ -60,6 +60,7 @@ pub struct InterpolatedCorrector; // } #[derive(Component, Debug)] +#[component(storage = "SparseSet")] pub struct Correction { /// This is what the original predicted value was before any correction was applied pub original_prediction: C, diff --git a/lightyear/src/client/prediction/despawn.rs b/lightyear/src/client/prediction/despawn.rs index 94de771c0..73e17171d 100644 --- a/lightyear/src/client/prediction/despawn.rs +++ b/lightyear/src/client/prediction/despawn.rs @@ -238,7 +238,7 @@ pub(crate) fn remove_despawn_marker( // incoming_loss: 0.05, // }; // let sync_config = SyncConfig::default().speedup_factor(1.0); -// let prediction_config = PredictionConfig::default().disable(false); +// let prediction_config = PredictionConfig::default(); // let interpolation_delay = Duration::from_millis(100); // let interpolation_config = InterpolationConfig::default().with_delay(InterpolationDelay { // min_delay: interpolation_delay, @@ -428,7 +428,7 @@ pub(crate) fn remove_despawn_marker( // incoming_loss: 0.05, // }; // let sync_config = SyncConfig::default().speedup_factor(1.0); -// let prediction_config = PredictionConfig::default().disable(false); +// let prediction_config = PredictionConfig::default(); // let interpolation_delay = Duration::from_millis(100); // let interpolation_config = InterpolationConfig::default().with_delay(InterpolationDelay { // min_delay: interpolation_delay, diff --git a/lightyear/src/client/prediction/plugin.rs b/lightyear/src/client/prediction/plugin.rs index 83c21cb4c..4ede47b4d 100644 --- a/lightyear/src/client/prediction/plugin.rs +++ b/lightyear/src/client/prediction/plugin.rs @@ -41,8 +41,6 @@ use super::spawn::spawn_predicted_entity; /// Configuration to specify how the prediction plugin should behave #[derive(Debug, Clone, Copy, Default, Reflect)] pub struct PredictionConfig { - /// If true, we completely disable the prediction plugin - pub disable: bool, /// If true, we always rollback whenever we receive a server update, instead of checking /// ff the confirmed state matches the predicted state history pub always_rollback: bool, @@ -59,11 +57,6 @@ pub struct PredictionConfig { } impl PredictionConfig { - pub fn disable(mut self, disable: bool) -> Self { - self.disable = disable; - self - } - pub fn always_rollback(mut self, always_rollback: bool) -> Self { self.always_rollback = always_rollback; self @@ -80,11 +73,6 @@ impl PredictionConfig { self.correction_ticks_factor = factor; self } - - /// [`Condition`] that returns `true` if the prediction plugin is disabled - pub(crate) fn is_disabled_condition(config: Option>) -> bool { - config.map_or(true, |config| config.prediction.disable) - } } /// Plugin that enables client-side prediction @@ -205,11 +193,8 @@ impl Plugin for PredictionPlugin { fn build(&self, app: &mut App) { // we only run prediction: // - if we're not in host-server mode - // - if the prediction plugin is not disabled // - after the client is synced - let should_prediction_run = - not(SharedConfig::is_host_server_condition - .or_else(PredictionConfig::is_disabled_condition)) + let should_prediction_run = not(SharedConfig::is_host_server_condition) .and_then(is_connected) .and_then(client_is_synced); diff --git a/lightyear/src/client/prediction/pre_prediction.rs b/lightyear/src/client/prediction/pre_prediction.rs index fef3df6e1..3da92894a 100644 --- a/lightyear/src/client/prediction/pre_prediction.rs +++ b/lightyear/src/client/prediction/pre_prediction.rs @@ -12,8 +12,9 @@ use crate::client::prediction::Predicted; use crate::client::sync::client_is_synced; use crate::connection::client::NetClient; use crate::prelude::client::{ClientConnection, PredictionSet}; -use crate::prelude::{NetworkTarget, ShouldBePredicted, Tick}; +use crate::prelude::{ShouldBePredicted, Tick}; use crate::shared::replication::components::{PrePredicted, Replicate}; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::sets::{ClientMarker, InternalReplicationSet}; #[derive(Default)] diff --git a/lightyear/src/client/prediction/rollback.rs b/lightyear/src/client/prediction/rollback.rs index c548e3fe2..37a5f10a3 100644 --- a/lightyear/src/client/prediction/rollback.rs +++ b/lightyear/src/client/prediction/rollback.rs @@ -102,6 +102,7 @@ impl Rollback { #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] pub(crate) fn check_rollback( + component_registry: Res, // TODO: have a way to only get the updates of entities that are predicted? tick_manager: Res, connection: Res, @@ -174,7 +175,9 @@ pub(crate) fn check_rollback( }), // confirm exist. rollback if history value is different Some(c) => history_value.map_or(true, |history_value| match history_value { - ComponentState::Updated(history_value) => history_value != *c, + ComponentState::Updated(history_value) => { + component_registry.should_rollback(&history_value, c) + } ComponentState::Removed => true, }), }; diff --git a/lightyear/src/client/replication.rs b/lightyear/src/client/replication.rs index 7313c722e..05649d029 100644 --- a/lightyear/src/client/replication.rs +++ b/lightyear/src/client/replication.rs @@ -1,45 +1,29 @@ +//! Client replication plugins use bevy::prelude::*; use bevy::utils::Duration; -use crate::client::config::ClientConfig; use crate::client::connection::ConnectionManager; use crate::client::networking::is_connected; use crate::client::sync::client_is_synced; -use crate::prelude::client::InterpolationDelay; use crate::prelude::SharedConfig; -use crate::shared::replication::plugin::ReplicationPlugin; +use crate::shared::replication::plugin::receive::ReplicationReceivePlugin; +use crate::shared::replication::plugin::send::ReplicationSendPlugin; use crate::shared::sets::{ClientMarker, InternalReplicationSet}; -#[derive(Clone, Debug, Reflect)] -pub struct ReplicationConfig { - /// Set to true to enable replicating this client's entities to the server - pub enable_send: bool, - /// Set to true to enable receiving replication updates from the server - pub enable_receive: bool, -} - -impl Default for ReplicationConfig { - fn default() -> Self { - Self { - enable_send: false, - enable_receive: true, - } +pub(crate) mod receive { + use super::*; + #[derive(Default)] + pub struct ClientReplicationReceivePlugin { + pub tick_interval: Duration, } -} - -#[derive(Default)] -pub struct ClientReplicationPlugin; -impl Plugin for ClientReplicationPlugin { - fn build(&self, app: &mut App) { - let config = app.world.resource::(); - app + impl Plugin for ClientReplicationReceivePlugin { + fn build(&self, app: &mut App) { // PLUGIN - .add_plugins(ReplicationPlugin::::new( - config.shared.tick.tick_duration, - config.replication.enable_send, - config.replication.enable_receive, - )) + app.add_plugins(ReplicationReceivePlugin::::new( + self.tick_interval, + )); + // TODO: currently we only support pre-spawned entities spawned during the FixedUpdate schedule // // SYSTEM SETS // .configure_sets( @@ -47,7 +31,8 @@ impl Plugin for ClientReplicationPlugin { // // on client, the client hash component is not replicated to the server, so there's no ordering constraint // ReplicationSet::SetPreSpawnedHash.in_set(ReplicationSet::All), // ) - .configure_sets( + + app.configure_sets( PostUpdate, // only replicate entities once client is synced // NOTE: we need is_synced, and not connected. Otherwise the ticks associated with the messages might be incorrect @@ -60,5 +45,106 @@ impl Plugin for ClientReplicationPlugin { .and_then(not(SharedConfig::is_host_server_condition)), ), ); + } + } +} + +pub(crate) mod send { + use super::*; + use crate::prelude::{ + ClientId, ComponentRegistry, ReplicationGroup, ShouldBePredicted, TargetEntity, + VisibilityMode, + }; + use crate::server::visibility::immediate::{ClientVisibility, ReplicateVisibility}; + use crate::shared::replication::components::{ + Controlled, ControlledBy, ReplicationTarget, ShouldBeInterpolated, + }; + use crate::shared::replication::network_target::NetworkTarget; + use crate::shared::replication::ReplicationSend; + + #[derive(Default)] + pub struct ClientReplicationSendPlugin { + pub tick_interval: Duration, + } + + impl Plugin for ClientReplicationSendPlugin { + fn build(&self, app: &mut App) { + app + // PLUGIN + .add_plugins(ReplicationSendPlugin::::new( + self.tick_interval, + )) + // SETS + .configure_sets( + PostUpdate, + // only replicate entities once client is synced + // NOTE: we need is_synced, and not connected. Otherwise the ticks associated with the messages might be incorrect + // and the message might be ignored by the server + // But then pre-predicted entities that are spawned right away will not be replicated? + // NOTE: we always need to add this condition if we don't enable replication, because + InternalReplicationSet::::All.run_if( + is_connected + .and_then(client_is_synced) + .and_then(not(SharedConfig::is_host_server_condition)), + ), + ) + // SYSTEMS + .add_systems( + PostUpdate, + send_entity_spawn + .in_set(InternalReplicationSet::::BufferEntityUpdates), + ); + } + } + + /// Send entity spawn replication messages to server when the ReplicationTarget component is added + /// Also handles: + /// - handles TargetEntity if it's a Preexisting entity + /// - setting the priority + pub(crate) fn send_entity_spawn( + query: Query< + ( + Entity, + Ref, + &ReplicationGroup, + Option<&TargetEntity>, + ), + Changed, + >, + mut sender: ResMut, + ) { + query + .iter() + .for_each(|(entity, replication_target, group, target_entity)| { + let mut target = replication_target.replication.clone(); + if !replication_target.is_added() { + if let Some(cached_replicate) = sender.replicate_component_cache.get(&entity) { + // do not re-send a spawn message to the server if we already have sent one + target.exclude(&cached_replicate.replication_target) + } + } + if target.is_empty() { + return; + } + trace!(?entity, "Prepare entity spawn to server"); + let group_id = group.group_id(Some(entity)); + if let Some(TargetEntity::Preexisting(remote_entity)) = target_entity { + sender.replication_sender.prepare_entity_spawn_reuse( + entity, + group_id, + *remote_entity, + ); + } else { + sender + .replication_sender + .prepare_entity_spawn(entity, group_id); + } + // TODO: should the priority be a component on the entity? but it should be shared between a group + // should a GroupChannel be a separate entity? + // also set the priority for the group when we spawn it + sender + .replication_sender + .update_base_priority(group_id, group.priority()); + }); } } diff --git a/lightyear/src/client/sync.rs b/lightyear/src/client/sync.rs index 69861d4b6..441d475bb 100644 --- a/lightyear/src/client/sync.rs +++ b/lightyear/src/client/sync.rs @@ -659,13 +659,7 @@ mod tests { let server_entity = stepper .server_app .world - .spawn(( - Component1(0.0), - Replicate { - replication_target: NetworkTarget::All, - ..default() - }, - )) + .spawn((Component1(0.0), Replicate::default())) .id(); // cross tick boundary diff --git a/lightyear/src/connection/client.rs b/lightyear/src/connection/client.rs index 1555a528d..81eeeeb73 100644 --- a/lightyear/src/connection/client.rs +++ b/lightyear/src/connection/client.rs @@ -8,6 +8,7 @@ use bevy::prelude::{NextState, Reflect, ResMut, Resource}; use enum_dispatch::enum_dispatch; use crate::client::config::NetcodeConfig; +use crate::client::io::Io; use crate::client::networking::NetworkingState; use crate::connection::id::ClientId; use crate::connection::netcode::ConnectToken; @@ -16,7 +17,9 @@ use crate::connection::netcode::ConnectToken; use crate::connection::steam::client::SteamConfig; use crate::packet::packet::Packet; -use crate::prelude::{generate_key, Io, IoConfig, Key, LinkConditionerConfig}; +use crate::prelude::client::ClientTransport; +use crate::prelude::{generate_key, Key, LinkConditionerConfig}; +use crate::transport::config::SharedIoConfig; // TODO: add diagnostics methods? #[enum_dispatch] @@ -69,6 +72,8 @@ pub struct ClientConnection { pub(crate) client: NetClientDispatch, } +pub type IoConfig = SharedIoConfig; + #[allow(clippy::large_enum_variant)] #[derive(Clone, Reflect)] #[reflect(from_reflect = false)] diff --git a/lightyear/src/connection/local/client.rs b/lightyear/src/connection/local/client.rs index 22618e4eb..a1812b7b7 100644 --- a/lightyear/src/connection/local/client.rs +++ b/lightyear/src/connection/local/client.rs @@ -1,7 +1,8 @@ +use crate::client::io::Io; use crate::client::networking::NetworkingState; use crate::connection::client::NetClient; use crate::packet::packet::Packet; -use crate::prelude::{ClientId, Io}; +use crate::prelude::ClientId; use crate::transport::LOCAL_SOCKET; use anyhow::Result; use std::net::SocketAddr; diff --git a/lightyear/src/connection/netcode/client.rs b/lightyear/src/connection/netcode/client.rs index 35bcc6b55..7934f5c1b 100644 --- a/lightyear/src/connection/netcode/client.rs +++ b/lightyear/src/connection/netcode/client.rs @@ -6,13 +6,13 @@ use anyhow::Context; use bevy::prelude::Resource; use tracing::{debug, error, info, trace, warn}; -use crate::connection::client::NetClient; +use crate::client::io::Io; +use crate::connection::client::{IoConfig, NetClient}; use crate::connection::id; use crate::prelude::client::NetworkingState; -use crate::prelude::IoConfig; use crate::serialize::bitcode::reader::BufferPool; use crate::serialize::reader::ReadBuffer; -use crate::transport::io::Io; +use crate::transport::io::IoState; use crate::transport::{PacketReceiver, PacketSender, Transport, LOCAL_SOCKET}; use super::{ @@ -170,9 +170,9 @@ pub enum ClientState { /// # use std::net::{Ipv4Addr, SocketAddr}; /// # use std::time::{Instant, Duration}; /// # use std::thread; -/// # use lightyear::prelude::{Io, IoConfig, TransportConfig}; +/// # use lightyear::prelude::client::{ClientTransport, IoConfig}; /// # let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0); -/// # let mut io = IoConfig::from_transport(TransportConfig::UdpSocket(addr)).connect().unwrap(); +/// # let mut io = IoConfig::from_transport(ClientTransport::UdpSocket(addr)).connect().unwrap(); /// # let mut server = NetcodeServer::new(0, [0; 32]).unwrap(); /// # let token_bytes = server.token(0, addr).generate().unwrap().try_into_bytes().unwrap(); /// let mut client = NetcodeClient::new(&token_bytes).unwrap(); @@ -566,12 +566,12 @@ impl NetcodeClient { /// # use bevy::utils::{Instant, Duration}; /// # use std::thread; /// # use lightyear::connection::netcode::NetcodeServer; - /// # use lightyear::prelude::{Io, IoConfig, TransportConfig}; + /// # use lightyear::prelude::client::{ClientTransport, IoConfig}; /// # let client_addr = SocketAddr::from(([127, 0, 0, 1], 40000)); /// # let server_addr = SocketAddr::from(([127, 0, 0, 1], 40001)); /// # let mut server = NetcodeServer::new(0, [0; 32]).unwrap(); /// # let token_bytes = server.token(0, server_addr).generate().unwrap().try_into_bytes().unwrap(); - /// # let mut io = IoConfig::from_transport(TransportConfig::UdpSocket(client_addr)).connect().unwrap(); + /// # let mut io = IoConfig::from_transport(ClientTransport::UdpSocket(client_addr)).connect().unwrap(); /// let mut client = NetcodeClient::new(&token_bytes).unwrap(); /// client.connect(); /// @@ -612,8 +612,10 @@ impl NetcodeClient { "client sending {} disconnect packets to server", self.cfg.num_disconnect_packets ); - for _ in 0..self.cfg.num_disconnect_packets { - self.send_packet(DisconnectPacket::create(), io)?; + if io.state == IoState::Connected { + for _ in 0..self.cfg.num_disconnect_packets { + self.send_packet(DisconnectPacket::create(), io)?; + } } self.reset(ClientState::Disconnected); Ok(()) @@ -667,6 +669,8 @@ impl NetClient for Client { // close and drop the io io.close().context("Could not close the io")?; std::mem::take(&mut self.io); + } else { + self.client.reset(ClientState::Disconnected); } Ok(()) } @@ -688,7 +692,7 @@ impl NetClient for Client { .context("io is not initialized, did you call connect?")?; self.client .try_update(delta_ms, io) - .inspect_err(|e| error!("error updating client: {:?}", e)) + .inspect_err(|e| error!("error updating netcode client: {:?}", e)) .context("could not update client") } diff --git a/lightyear/src/connection/netcode/mod.rs b/lightyear/src/connection/netcode/mod.rs index 2325789bb..96bac3203 100644 --- a/lightyear/src/connection/netcode/mod.rs +++ b/lightyear/src/connection/netcode/mod.rs @@ -44,34 +44,33 @@ * Optionally provide a [`ServerConfig`] - a struct that allows you to customize the server's behavior. ``` - use std::{thread, time::{Instant, Duration}, net::SocketAddr}; - use crate::lightyear::connection::netcode::{generate_key, NetcodeServer, MAX_PACKET_SIZE}; - - use lightyear::prelude::{IoConfig, TransportConfig}; - use crate::lightyear::transport::io::Io; - - // Create an io - let client_addr = SocketAddr::from(([127, 0, 0, 1], 40000)); - let mut io = IoConfig::from_transport(TransportConfig::UdpSocket(client_addr)).connect().unwrap(); - - // Create a server - let protocol_id = 0x11223344; - let private_key = generate_key(); // you can also provide your own key - let mut server = NetcodeServer::new(protocol_id, private_key).unwrap(); - - // Run the server at 60Hz - let start = Instant::now(); - let tick_rate = Duration::from_secs_f64(1.0 / 60.0); - loop { - let elapsed = start.elapsed().as_secs_f64(); - server.update(elapsed, &mut io); - while let Some((packet, from)) = server.recv() { - // ... - } - # break; - thread::sleep(tick_rate); - } - ``` +use std::{thread, time::{Instant, Duration}, net::SocketAddr}; +use crate::lightyear::connection::netcode::{generate_key, NetcodeServer, MAX_PACKET_SIZE}; +use lightyear::prelude::server::*; +use crate::lightyear::transport::io::BaseIo; + +// Create an io +let client_addr = SocketAddr::from(([127, 0, 0, 1], 40000)); +let mut io = IoConfig::from_transport(ServerTransport::UdpSocket(client_addr)).start().unwrap(); + +// Create a server +let protocol_id = 0x11223344; +let private_key = generate_key(); // you can also provide your own key +let mut server = NetcodeServer::new(protocol_id, private_key).unwrap(); + +// Run the server at 60Hz +let start = Instant::now(); +let tick_rate = Duration::from_secs_f64(1.0 / 60.0); +loop { + let elapsed = start.elapsed().as_secs_f64(); + server.update(elapsed, &mut io); + while let Some((packet, from)) = server.recv() { + // ... + } + # break; + thread::sleep(tick_rate); +} +``` ## Client @@ -86,13 +85,12 @@ ``` use std::{thread, time::{Instant, Duration}, net::SocketAddr}; -use lightyear::prelude::{IoConfig, TransportConfig}; +use lightyear::prelude::client::*; use crate::lightyear::connection::netcode::{generate_key, ConnectToken, NetcodeClient, MAX_PACKET_SIZE}; -use crate::lightyear::transport::io::Io; // Create an io let client_addr = SocketAddr::from(([127, 0, 0, 1], 40000)); -let mut io = IoConfig::from_transport(TransportConfig::UdpSocket(client_addr)).connect().unwrap(); +let mut io = IoConfig::from_transport(ClientTransport::UdpSocket(client_addr)).connect().unwrap(); // Generate a connection token for the client let protocol_id = 0x11223344; diff --git a/lightyear/src/connection/netcode/server.rs b/lightyear/src/connection/netcode/server.rs index e38d31f9f..839224c24 100644 --- a/lightyear/src/connection/netcode/server.rs +++ b/lightyear/src/connection/netcode/server.rs @@ -8,12 +8,12 @@ use tracing::{debug, error, trace}; use crate::connection::id; use crate::connection::netcode::token::TOKEN_EXPIRE_SEC; -use crate::connection::server::NetServer; -use crate::prelude::IoConfig; +use crate::connection::server::{IoConfig, NetServer}; use crate::serialize::bitcode::reader::BufferPool; use crate::serialize::reader::ReadBuffer; use crate::server::config::NetcodeConfig; -use crate::transport::io::Io; +use crate::server::io::{Io, ServerIoEvent, ServerNetworkEventSender}; +use crate::transport::io::BaseIo; use crate::transport::{PacketReceiver, PacketSender, Transport}; use super::{ @@ -216,7 +216,7 @@ impl ConnectionCache { } } -pub type Callback = Box; +pub type Callback = Box; /// Configuration for a server. /// @@ -234,7 +234,7 @@ pub type Callback = Box ServerConfig { /// See [`ServerConfig`] for an example. pub fn on_connect(mut self, cb: F) -> Self where - F: FnMut(ClientId, &mut Ctx) + Send + Sync + 'static, + F: FnMut(ClientId, SocketAddr, &mut Ctx) + Send + Sync + 'static, { self.on_connect = Some(Box::new(cb)); self @@ -332,7 +332,7 @@ impl ServerConfig { /// See [`ServerConfig`] for an example. pub fn on_disconnect(mut self, cb: F) -> Self where - F: FnMut(ClientId, &mut Ctx) + Send + Sync + 'static, + F: FnMut(ClientId, SocketAddr, &mut Ctx) + Send + Sync + 'static, { self.on_disconnect = Some(Box::new(cb)); self @@ -351,10 +351,10 @@ impl ServerConfig { /// # use std::net::{SocketAddr, Ipv4Addr}; /// # use bevy::utils::{Instant, Duration}; /// # use std::thread; -/// # use lightyear::prelude::{Io, IoConfig, TransportConfig}; -/// let mut io = IoConfig::from_transport(TransportConfig::UdpSocket( +/// # use lightyear::prelude::server::{IoConfig, ServerTransport}; +/// let mut io = IoConfig::from_transport(ServerTransport::UdpSocket( /// SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0) -/// )).connect().unwrap(); +/// )).start().unwrap(); /// let private_key = generate_key(); /// let protocol_id = 0x123456789ABCDEF0; /// let mut server = NetcodeServer::new(protocol_id, private_key).unwrap(); @@ -418,7 +418,7 @@ impl NetcodeServer { /// /// let private_key = generate_key(); /// let protocol_id = 0x123456789ABCDEF0; - /// let cfg = ServerConfig::with_context(42).on_connect(|idx, ctx| { + /// let cfg = ServerConfig::with_context(42).on_connect(|idx, _, ctx| { /// assert_eq!(ctx, &42); /// }); /// let server = NetcodeServer::with_config(protocol_id, private_key, cfg).unwrap(); @@ -447,14 +447,14 @@ impl NetcodeServer { | 1 << Packet::KEEP_ALIVE | 1 << Packet::PAYLOAD | 1 << Packet::DISCONNECT; - fn on_connect(&mut self, client_id: ClientId) { + fn on_connect(&mut self, client_id: ClientId, addr: SocketAddr) { if let Some(cb) = self.cfg.on_connect.as_mut() { - cb(client_id, &mut self.cfg.context) + cb(client_id, addr, &mut self.cfg.context) } } - fn on_disconnect(&mut self, client_id: ClientId) { + fn on_disconnect(&mut self, client_id: ClientId, addr: SocketAddr) { if let Some(cb) = self.cfg.on_disconnect.as_mut() { - cb(client_id, &mut self.cfg.context) + cb(client_id, addr, &mut self.cfg.context) } } fn touch_client(&mut self, client_id: Option) -> Result<()> { @@ -505,7 +505,7 @@ impl NetcodeServer { Packet::Disconnect(_) => { if let Some(idx) = client_id { debug!("server disconnected client {idx}"); - self.on_disconnect(idx); + self.on_disconnect(idx, addr); self.conn_cache.remove(idx); } Ok(()) @@ -686,7 +686,7 @@ impl NetcodeServer { id, challenge_token.client_id ); self.send_to_client(KeepAlivePacket::create(id), id, sender)?; - self.on_connect(id); + self.on_connect(id, from_addr); Ok(()) } fn check_for_timeouts(&mut self) { @@ -697,11 +697,12 @@ impl NetcodeServer { if !client.is_connected() { continue; } + let addr = client.addr; if client.timeout.is_positive() && client.last_receive_time + (client.timeout as f64) < self.time { debug!("server timed out client {id}"); - self.on_disconnect(id); + self.on_disconnect(id, addr); self.conn_cache.remove(id); } } @@ -826,14 +827,14 @@ impl NetcodeServer { /// # Example /// ``` /// # use crate::lightyear::connection::netcode::{NetcodeServer, ServerConfig, MAX_PACKET_SIZE}; - /// # use lightyear::prelude::{Io, IoConfig, TransportConfig}; /// # use std::net::{SocketAddr, Ipv4Addr}; /// # use bevy::utils::Instant; + /// # use lightyear::prelude::server::*; /// # let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 0)); /// # let protocol_id = 0x123456789ABCDEF0; /// # let private_key = [42u8; 32]; /// # let mut server = NetcodeServer::new(protocol_id, private_key).unwrap(); - /// # let mut io = IoConfig::from_transport(TransportConfig::UdpSocket(addr)).connect().unwrap(); + /// # let mut io = IoConfig::from_transport(ServerTransport::UdpSocket(addr)).start().unwrap(); /// # /// let start = Instant::now(); /// loop { @@ -919,6 +920,7 @@ impl NetcodeServer { self.token_sequence += 1; token_builder } + /// Disconnects a client. /// /// The server will send a number of redundant disconnect packets to the client, and then remove its connection info. @@ -929,11 +931,12 @@ impl NetcodeServer { if !conn.is_connected() { return Ok(()); } + let addr = conn.addr; debug!("server disconnecting client {client_id}"); for _ in 0..self.cfg.num_disconnect_packets { self.send_to_client(DisconnectPacket::create(), client_id, io)?; } - self.on_disconnect(client_id); + self.on_disconnect(client_id, addr); self.conn_cache.remove(client_id); Ok(()) } @@ -986,6 +989,7 @@ impl NetcodeServer { pub(crate) struct NetcodeServerContext { pub(crate) connections: Vec, pub(crate) disconnections: Vec, + sender: Option, } #[derive(Resource)] @@ -998,30 +1002,35 @@ pub struct Server { impl NetServer for Server { fn start(&mut self) -> anyhow::Result<()> { let io_config = self.io_config.clone(); - let io = io_config.connect().context("could not start io")?; + let io = io_config.start().context("could not start io")?; + self.server + .cfg + .context + .sender + .clone_from(&io.context.event_sender); self.io = Some(io); Ok(()) } fn stop(&mut self) -> anyhow::Result<()> { if let Some(mut io) = self.io.take() { - let mut connected_clients = self - .server - .connected_client_ids() - .map(id::ClientId::Netcode) - .collect::>(); + if let Some(sender) = &mut self.server.cfg.context.sender { + sender + .send_blocking(ServerIoEvent::ServerDisconnected( + crate::transport::error::Error::UserRequest, + )) + .context("Could not send 'ServerStopped' event to io")?; + } self.server.disconnect_all(&mut io)?; - self.server - .cfg - .context - .disconnections - .append(&mut connected_clients); + self.server.cfg.context.sender = None; // close and drop the io io.close().context("Could not close the io")?; } Ok(()) } + /// Disconnect a client from the server + /// (also adds the client_id to the list of newly disconnected clients) fn disconnect(&mut self, client_id: id::ClientId) -> anyhow::Result<()> { match client_id { id::ClientId::Netcode(id) => { @@ -1029,7 +1038,6 @@ impl NetServer for Server { self.server .disconnect(id, io) .context("Could not disconnect client")?; - self.server.cfg.context.disconnections.push(client_id); } Ok(()) } @@ -1082,24 +1090,35 @@ impl NetServer for Server { fn io(&self) -> Option<&Io> { self.io.as_ref() } + fn io_mut(&mut self) -> Option<&mut Io> { + self.io.as_mut() + } } impl Server { pub(crate) fn new(config: NetcodeConfig, io_config: IoConfig) -> Self { - let private_key = config.private_key.unwrap_or(generate_key()); // create context let context = NetcodeServerContext::default(); let mut cfg = ServerConfig::with_context(context) - .on_connect(|id, ctx| { + .on_connect(|id, addr, ctx| { ctx.connections.push(id::ClientId::Netcode(id)); }) - .on_disconnect(|id, ctx| { + .on_disconnect(|id, addr, ctx| { + // notify the io that a client got disconnected + if let Some(sender) = &mut ctx.sender { + debug!("Notify the io that client {id:?} got disconnected, so that we can stop the corresponding task"); + let _ = sender + .send_blocking(ServerIoEvent::ClientDisconnected(addr)) + .inspect_err(|e| { + error!("Error sending 'ClientDisconnected' event to io: {:?}", e) + }); + } ctx.disconnections.push(id::ClientId::Netcode(id)); }); cfg = cfg.keep_alive_send_rate(config.keep_alive_send_rate); cfg = cfg.num_disconnect_packets(config.num_disconnect_packets); cfg = cfg.client_timeout_secs(config.client_timeout_secs); - let server = NetcodeServer::with_config(config.protocol_id, private_key, cfg) + let server = NetcodeServer::with_config(config.protocol_id, config.private_key, cfg) .expect("Could not create server netcode"); Self { diff --git a/lightyear/src/connection/server.rs b/lightyear/src/connection/server.rs index 6fbd98998..fb55b737c 100644 --- a/lightyear/src/connection/server.rs +++ b/lightyear/src/connection/server.rs @@ -1,13 +1,18 @@ use anyhow::{anyhow, Result}; use bevy::prelude::Resource; use bevy::utils::HashMap; +use std::net::SocketAddr; use crate::connection::id::ClientId; #[cfg(all(feature = "steam", not(target_family = "wasm")))] use crate::connection::steam::server::SteamConfig; use crate::packet::packet::Packet; -use crate::prelude::{Io, IoConfig, LinkConditionerConfig}; +use crate::prelude::client::ClientTransport; +use crate::prelude::server::ServerTransport; +use crate::prelude::LinkConditionerConfig; use crate::server::config::NetcodeConfig; +use crate::server::io::Io; +use crate::transport::config::SharedIoConfig; pub trait NetServer: Send + Sync { /// Start the server @@ -41,6 +46,8 @@ pub trait NetServer: Send + Sync { fn new_disconnections(&self) -> Vec; fn io(&self) -> Option<&Io>; + + fn io_mut(&mut self) -> Option<&mut Io>; } /// A wrapper around a `Box` @@ -49,6 +56,8 @@ pub struct ServerConnection { server: Box, } +pub type IoConfig = SharedIoConfig; + /// Configuration for the server connection #[derive(Clone, Debug)] pub enum NetConfig { @@ -139,6 +148,10 @@ impl NetServer for ServerConnection { fn io(&self) -> Option<&Io> { self.server.io() } + + fn io_mut(&mut self) -> Option<&mut Io> { + self.server.io_mut() + } } type ServerConnectionIdx = usize; diff --git a/lightyear/src/connection/steam/client.rs b/lightyear/src/connection/steam/client.rs index 86b7fb455..be500a28a 100644 --- a/lightyear/src/connection/steam/client.rs +++ b/lightyear/src/connection/steam/client.rs @@ -2,7 +2,8 @@ use crate::client::networking::NetworkingState; use crate::connection::client::NetClient; use crate::connection::id::ClientId; use crate::packet::packet::Packet; -use crate::prelude::{Io, LinkConditionerConfig}; +use crate::prelude::client::Io; +use crate::prelude::LinkConditionerConfig; use crate::serialize::bitcode::reader::BufferPool; use crate::transport::LOCAL_SOCKET; use anyhow::{anyhow, Context, Result}; diff --git a/lightyear/src/connection/steam/server.rs b/lightyear/src/connection/steam/server.rs index 53e127510..45887d275 100644 --- a/lightyear/src/connection/steam/server.rs +++ b/lightyear/src/connection/steam/server.rs @@ -3,8 +3,10 @@ use crate::connection::id::ClientId; use crate::connection::netcode::MAX_PACKET_SIZE; use crate::connection::server::NetServer; use crate::packet::packet::Packet; -use crate::prelude::{Io, LinkConditionerConfig}; +use crate::prelude::LinkConditionerConfig; use crate::serialize::bitcode::reader::BufferPool; +use crate::server::io::Io; +use crate::transport::LOCAL_SOCKET; use anyhow::{anyhow, Context, Result}; use bevy::utils::HashMap; use std::collections::VecDeque; @@ -247,4 +249,8 @@ impl NetServer for Server { fn io(&self) -> Option<&Io> { None } + + fn io_mut(&mut self) -> Option<&mut Io> { + None + } } diff --git a/lightyear/src/inputs/mod.rs b/lightyear/src/inputs/mod.rs index cdb29b75d..3cb4d1d1b 100644 --- a/lightyear/src/inputs/mod.rs +++ b/lightyear/src/inputs/mod.rs @@ -1,4 +1,5 @@ //! Handles networking client inputs + // TODO: import this as inputs, check how xwt/party does it #[cfg(feature = "leafwing")] #[cfg_attr(docsrs, doc(cfg(feature = "leafwing")))] diff --git a/lightyear/src/inputs/native/mod.rs b/lightyear/src/inputs/native/mod.rs index ec70667f5..16f111012 100644 --- a/lightyear/src/inputs/native/mod.rs +++ b/lightyear/src/inputs/native/mod.rs @@ -1,5 +1,22 @@ /*! -Handles dealing with inputs (keyboard presses, mouse clicks) sent from a player (client) to server. +Handles inputs (keyboard presses, mouse clicks) sent from a player (client) to server. + +NOTE: You should use the `LeafwingInputPlugin` instead (requires the `leafwing` features), which +has mode features and is easier to use. + +Lightyear does the following things for you: +- buffers the inputs of a player for each tick +- makes sures that input are replayed correctly during rollback +- sends the inputs to the server in a compressed and reliable form + + +### Sending inputs + +There are several steps to use the `InputPlugin`: +- you need to buffer inputs for each tick. This is done by calling [`add_input`](crate::prelude::client::InputManager::add_input) in a system. +That system must run in the [`BufferI + + */ use std::fmt::Debug; diff --git a/lightyear/src/lib.rs b/lightyear/src/lib.rs index e108110a0..368ec1497 100644 --- a/lightyear/src/lib.rs +++ b/lightyear/src/lib.rs @@ -4,7 +4,159 @@ Lightyear is a networking library for Bevy. It is designed for server-authoritative multiplayer games; and aims to be both feature-complete and easy-to-use. You can find more information in the [book](https://cbournhonesque.github.io/lightyear/book/) or check out the [examples](https://github.com/cBournhonesque/lightyear/tree/main/examples)! -*/ + +## Getting started + +### Install the plugins + +`lightyear` provides two plugins groups: [`ServerPlugins`](prelude::server::ServerPlugins) and [`ClientPlugins`](prelude::client::ClientPlugins) that will handle the networking for you. + +```rust +use bevy::utils::Duration; +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::client::*; +use lightyear::prelude::server::*; + +fn run_client_app() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(ClientPlugins::new(ClientConfig::default())) + .run() +} + +fn run_server_app() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(ServerPlugins::new(ServerConfig::default())) + .run() +} +``` +In general, you will have to modify some parts of the [`ClientConfig`](prelude::client::ClientConfig) and [`ServerConfig`](prelude::server::ServerConfig) to fit your game. +Mostly the [`SharedConfig`], which must be the same on both the client and the server, and the `NetConfig` which defines +how the client and server will communicate. + +### Implement the protocol + +The [`Protocol`](protocol) is the set of types that can be sent over the network. +You will have to define your protocol in a shared module that is accessible to both the client and the server, +since the protocol must be shared between them. + +There are several steps: +- [Adding messages](MessageRegistry#adding-messages) +- [Adding components](ComponentRegistry#adding-components) +- [Adding channels](ChannelRegistry#adding-channels) +- [Adding leafwing inputs](client::input_leafwing#adding-leafwing-inputs) or [Adding inputs](client::input#adding-a-new-input-type) + +## Using lightyear + +Lightyear provides various commands and resources that can you can use to interact with the plugin. + +### Connecting/Disconnecting + +On the client, you can initiate the connection by using the [`connect_client`](prelude::client::ClientCommands::connect_client) Command. +You can also disconnect with the [`disconnect_client`](prelude::client::ClientCommands::disconnect_client) Command. + +On the server, you can start listening for connections by using the [`start_server`](prelude::server::ServerCommands::start_server) Command. +You can stop the server using the [`stop_server`](prelude::server::ServerCommands::stop_server) Command. + +While the client or server are disconnected, you can update the [`ClientConfig`](prelude::client::ClientConfig) and [`ServerConfig`](prelude::server::ServerConfig) resources, +and the new configuration will take effect on the next connection attempt. + +### Sending messages + +On both the [client](prelude::client::ConnectionManager) and the [server](prelude::server::ConnectionManager), you can send messages using the `ConnectionManager` resource. + +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::server::*; + +#[derive(Serialize, Deserialize)] +struct MyMessage; + +#[derive(Channel)] +struct MyChannel; + +fn send_message(mut connection_manager: ResMut) { + let _ = connection_manager.send_message_to_target::(&MyMessage, NetworkTarget::All); +} +``` + +### Receiving messages + +All network events are sent as Bevy events. +The full list is available [here](client::events) for the client, and [here](server::events) for the server. + +Since they are Bevy events, you can use the Bevy event system to react to them. +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::server::*; + +# #[derive(Serialize, Deserialize)] +# struct MyMessage; + +fn receive_message(mut message_reader: EventReader>) { + for message_event in message_reader.read() { + // the message itself + let message = message_event.message(); + // the client who sent the message + let client = message_event.context; + } +} +``` + +### Starting replication + +To replicate an entity from the local world to the remote world, you can just add the [`Replicate`] bundle to the entity. +The [`Replicate`] bundle contains many components to customize how the entity is replicated. + +You can remove the [`ReplicationTarget`] component to stop the replication. This will not despawn the entity on the remote world; it will simply +stop sending replication updates. + + +### Reacting to replication events + +Similarly to messages, you can react to replication events using Bevy's event system. +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::client::*; + +# #[derive(Component, Serialize, Deserialize)] +# struct MyComponent; + +fn component_inserted(mut events: EventReader>) { + for event in events.read() { + // the entity on which the component was inserted + let entity = event.entity(); + } +} +``` + +Lightyear also inserts the [`Replicated`] marker component on every entity that was spawned via replication, +so you can achieve the same result with: +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::client::*; + +# #[derive(Component, Serialize, Deserialize)] +# struct MyComponent; + +fn component_inserted(query: Query, Added)>) { + for entity in query.iter() { + println!("MyComponent was inserted via replication on {entity:?}"); + } +} +``` + +## Architecture + + + + */ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] @@ -23,6 +175,7 @@ pub(crate) mod _internal { /// Prelude containing commonly used types pub mod prelude { pub use lightyear_macros::Channel; + pub use serde::{Deserialize, Serialize}; pub use crate::channel::builder::TickBufferChannel; pub use crate::channel::builder::{ @@ -47,11 +200,13 @@ pub mod prelude { pub use crate::shared::ping::manager::PingConfig; pub use crate::shared::plugin::{NetworkIdentity, SharedPlugin}; pub use crate::shared::replication::components::{ - NetworkTarget, PrePredicted, Replicate, Replicated, ReplicationGroup, ReplicationMode, - ShouldBePredicted, TargetEntity, + ControlledBy, DisabledComponent, OverrideTargetComponent, PrePredicted, Replicate, + ReplicateHierarchy, ReplicateOnceComponent, Replicated, ReplicationGroup, + ReplicationTarget, ShouldBePredicted, TargetEntity, VisibilityMode, }; pub use crate::shared::replication::entity_map::RemoteEntityMap; pub use crate::shared::replication::hierarchy::ParentSync; + pub use crate::shared::replication::network_target::NetworkTarget; pub use crate::shared::replication::resources::{ ReplicateResourceExt, ReplicateResourceMetadata, StopReplicateResourceExt, }; @@ -59,8 +214,6 @@ pub mod prelude { pub use crate::shared::tick_manager::TickManager; pub use crate::shared::tick_manager::{Tick, TickConfig}; pub use crate::shared::time_manager::TimeManager; - pub use crate::transport::config::{IoConfig, TransportConfig}; - pub use crate::transport::io::Io; pub use crate::transport::middleware::compression::CompressionConfig; pub use crate::transport::middleware::conditioner::LinkConditionerConfig; @@ -84,18 +237,19 @@ pub mod prelude { pub use crate::client::interpolation::{ InterpolateStatus, Interpolated, VisualInterpolateStatus, VisualInterpolationPlugin, }; + pub use crate::client::io::config::ClientTransport; + pub use crate::client::io::Io; pub use crate::client::networking::{ClientCommands, NetworkingState}; - pub use crate::client::plugin::ClientPlugin; + pub use crate::client::plugin::ClientPlugins; pub use crate::client::prediction::correction::Correction; pub use crate::client::prediction::despawn::PredictionDespawnCommandsExt; pub use crate::client::prediction::plugin::is_in_rollback; pub use crate::client::prediction::plugin::{PredictionConfig, PredictionSet}; pub use crate::client::prediction::rollback::{Rollback, RollbackState}; pub use crate::client::prediction::Predicted; - pub use crate::client::replication::ReplicationConfig; pub use crate::client::sync::SyncConfig; pub use crate::connection::client::{ - Authentication, ClientConnection, NetClient, NetConfig, + Authentication, ClientConnection, IoConfig, NetClient, NetConfig, }; #[cfg(all(feature = "steam", not(target_family = "wasm")))] pub use crate::connection::steam::client::SteamConfig; @@ -105,25 +259,29 @@ pub mod prelude { pub use wtransport::tls::Identity; pub use crate::connection::server::{ - NetConfig, NetServer, ServerConnection, ServerConnections, + IoConfig, NetConfig, NetServer, ServerConnection, ServerConnections, }; #[cfg(all(feature = "steam", not(target_family = "wasm")))] pub use crate::connection::steam::server::SteamConfig; + pub use crate::server::clients::ControlledEntities; pub use crate::server::config::{NetcodeConfig, PacketConfig, ServerConfig}; pub use crate::server::connection::ConnectionManager; pub use crate::server::events::{ ComponentInsertEvent, ComponentRemoveEvent, ComponentUpdateEvent, ConnectEvent, DisconnectEvent, EntityDespawnEvent, EntitySpawnEvent, InputEvent, MessageEvent, }; + pub use crate::server::io::config::ServerTransport; + pub use crate::server::io::Io; pub use crate::server::networking::{NetworkingState, ServerCommands}; - pub use crate::server::plugin::ServerPlugin; - pub use crate::server::replication::{ - ReplicationConfig, ServerFilter, ServerReplicationSet, - }; - pub use crate::server::room::{RoomId, RoomManager, RoomMut, RoomRef}; + pub use crate::server::plugin::ServerPlugins; + pub use crate::server::replication::{send::ServerFilter, ServerReplicationSet}; + pub use crate::server::visibility::immediate::VisibilityManager; + pub use crate::server::visibility::room::{RoomId, RoomManager}; } } +use prelude::*; + pub mod channel; pub mod client; diff --git a/lightyear/src/packet/header.rs b/lightyear/src/packet/header.rs index 2f9f66876..0d92ae22c 100644 --- a/lightyear/src/packet/header.rs +++ b/lightyear/src/packet/header.rs @@ -36,7 +36,7 @@ impl PacketHeader { /// /// i is 0-indexed. So 0 represents the first bit of the bitfield (starting from the right) fn get_bitfield_bit(&self, i: u8) -> bool { - assert!(i < ACK_BITFIELD_SIZE); + debug_assert!(i < ACK_BITFIELD_SIZE); self.ack_bitfield & (1 << i) != 0 } diff --git a/lightyear/src/packet/packet_manager.rs b/lightyear/src/packet/packet_manager.rs index 296867d9b..366c9cf34 100644 --- a/lightyear/src/packet/packet_manager.rs +++ b/lightyear/src/packet/packet_manager.rs @@ -69,7 +69,7 @@ impl PacketBuilder { // TODO: we should actually call finish write to byte align! // TODO: CAREFUL, THIS COULD ALLOCATE A BIT MORE TO BYTE ALIGN? let payload = Payload::from(write_buffer.finish_write()); - assert!(payload.len() <= MAX_PACKET_SIZE, "packet = {:?}", packet); + debug_assert!(payload.len() <= MAX_PACKET_SIZE, "packet = {:?}", packet); Ok(payload) // packet.encode(&mut self.write_buffer)?; diff --git a/lightyear/src/protocol/channel.rs b/lightyear/src/protocol/channel.rs index 321a51ab6..67f90e85e 100644 --- a/lightyear/src/protocol/channel.rs +++ b/lightyear/src/protocol/channel.rs @@ -37,7 +37,31 @@ impl From for ChannelKind { } } -/// Registry to store metadata about the various [`Channel`] +/// Registry to store metadata about the various [`Channels`](Channel) to use to send messages. +/// +/// ### Adding channels +/// +/// You can add a new channel to the registry by calling the [`add_channel`](ChannelRegistry::add_channel) method. +/// +/// ```rust +/// use lightyear::prelude::*; +/// use bevy::prelude::*; +/// +/// #[derive(Channel)] +/// struct MyChannel; +/// +/// # fn main() { +/// # let mut app = App::new(); +/// # app.init_resource::(); +/// app.add_channel::(ChannelSettings { +/// mode: ChannelMode::UnorderedUnreliable, +/// direction: ChannelDirection::Bidirectional, +/// ..default() +/// }); +/// # } +/// ``` +/// +/// #[derive(Resource, Default, Clone, Debug, PartialEq, TypePath)] pub struct ChannelRegistry { // we only store the ChannelBuilder because we might want to create multiple instances of the same channel diff --git a/lightyear/src/protocol/component.rs b/lightyear/src/protocol/component.rs index 05e7a1c37..bfebad346 100644 --- a/lightyear/src/protocol/component.rs +++ b/lightyear/src/protocol/component.rs @@ -25,12 +25,12 @@ use crate::client::config::ClientConfig; use crate::client::interpolation::{add_interpolation_systems, add_prepare_interpolation_systems}; use crate::client::prediction::plugin::add_prediction_systems; use crate::prelude::client::SyncComponent; -use crate::prelude::server::{ServerConfig, ServerPlugin}; +use crate::prelude::server::{ServerConfig, ServerPlugins}; use crate::prelude::{ - client, server, ChannelDirection, Message, MessageRegistry, PreSpawnedPlayerObject, - RemoteEntityMap, ReplicateResourceMetadata, Tick, + client, server, AppMessageExt, ChannelDirection, Message, MessageRegistry, + PreSpawnedPlayerObject, RemoteEntityMap, ReplicateResourceMetadata, Tick, }; -use crate::protocol::message::{MessageKind, MessageType}; +use crate::protocol::message::{MessageKind, MessageRegistration, MessageType}; use crate::protocol::registry::{NetId, TypeKind, TypeMapper}; use crate::protocol::serialize::{ErasedSerializeFns, MapEntitiesFn, SerializeFns}; use crate::protocol::{BitSerializable, EventContext}; @@ -52,6 +52,93 @@ use crate::shared::sets::InternalMainSet; pub type ComponentNetId = NetId; +/// A [`Resource`] that will keep track of all the [`Components`](Component) that can be replicated. +/// +/// +/// ### Adding Components +/// +/// You register components by calling the [`register_component`](AppComponentExt::register_component) method directly on the App. +/// You can provide a [`ChannelDirection`] to specify if the component should be sent from the client to the server, from the server to the client, or both. +/// +/// A component needs to implement the `Serialize`, `Deserialize` and `PartialEq` traits. +/// +/// ```rust +/// use bevy::prelude::*; +/// use serde::{Deserialize, Serialize}; +/// use lightyear::prelude::*; +/// +/// #[derive(Component, PartialEq, Serialize, Deserialize)] +/// struct MyComponent; +/// +/// fn add_components(app: &mut App) { +/// app.register_component::(ChannelDirection::Bidirectional); +/// } +/// ``` +/// +/// ### Customizing Component behaviour +/// +/// There are some cases where you might want to define additional behaviour for a component. +/// +/// #### Entity Mapping +/// If the component contains [`Entities`](Entity), you need to specify how those entities +/// will be mapped from the remote world to the local world. +/// +/// Provided that your type implements [`MapEntities`], you can extend the protocol to support this behaviour, by +/// calling the [`add_map_entities`](ComponentRegistration::add_map_entities) method. +/// +/// #### Prediction +/// When client-prediction is enabled, we create two distinct entities on the client when the server replicates an entity: a Confirmed entity and a Predicted entity. +/// The Confirmed entity will just get updated when the client receives the server updates, while the Predicted entity will be updated by the client's prediction system. +/// +/// Components are not synced from the Confirmed entity to the Predicted entity by default, you have to enable this behaviour. +/// You can do this by calling the [`add_prediction`](ComponentRegistration::add_prediction) method. +/// You will have to provide a [`ComponentSyncMode`] that defines the behaviour of the prediction system. +/// +/// #### Correction +/// When client-prediction is enabled, there might be cases where there is a mismatch between the state of the Predicted entity +/// and the state of the Confirmed entity. In this case, we rollback by snapping the Predicted entity to the Confirmed entity and replaying the last few frames. +/// +/// However, rollbacks that do an instant update can be visually jarring, so we provide the option to smooth the rollback process over a few frames. +/// You can do this by calling the [`add_correction_fn`](ComponentRegistration::add_correction_fn) method. +/// +/// If your component implements the [`Linear`] trait, you can use the [`add_linear_correction_fn`](ComponentRegistration::add_linear_correction_fn) method, +/// which provides linear interpolation. +/// +/// #### Interpolation +/// Similarly to client-prediction, we create two distinct entities on the client when the server replicates an entity: a Confirmed entity and an Interpolated entity. +/// The Confirmed entity will just get updated when the client receives the server updates, while the Interpolated entity will be updated by the client's interpolation system, +/// which will interpolate between two Confirmed states. +/// +/// Components are not synced from the Confirmed entity to the Interpolated entity by default, you have to enable this behaviour. +/// You can do this by calling the [`add_interpolation`](ComponentRegistration::add_interpolation) method. +/// You will have to provide a [`ComponentSyncMode`] that defines the behaviour of the interpolation system. +/// +/// You will also need to provide an interpolation function that will be used to interpolate between two states. +/// If your component implements the [`Linear`] trait, you can use the [`add_linear_interpolation_fn`](ComponentRegistration::add_linear_interpolation_fn) method, +/// which means that we will interpolate using linear interpolation. +/// +/// You can also use your own interpolation function by using the [`add_interpolation_fn`](ComponentRegistration::add_interpolation_fn) method. +/// +/// ```rust +/// use bevy::prelude::*; +/// use lightyear::prelude::*; +/// use lightyear::prelude::client::*; +/// +/// #[derive(Component, Clone, PartialEq, Serialize, Deserialize)] +/// struct MyComponent(f32); +/// +/// fn my_lerp_fn(start: &MyComponent, other: &MyComponent, t: f32) -> MyComponent { +/// MyComponent(start.0 * (1.0 - t) + other.0 * t) +/// } +/// +/// +/// fn add_messages(app: &mut App) { +/// app.register_component::(ChannelDirection::ServerToClient) +/// .add_prediction(ComponentSyncMode::Full) +/// .add_interpolation(ComponentSyncMode::Full) +/// .add_interpolation_fn(my_lerp_fn); +/// } +/// ``` #[derive(Debug, Default, Clone, Resource, PartialEq, TypePath)] pub struct ComponentRegistry { replication_map: HashMap, @@ -71,6 +158,21 @@ pub struct ReplicationMetadata { pub struct PredictionMetadata { pub prediction_mode: ComponentSyncMode, pub correction: Option, + /// Function used to compare the confirmed component with the predicted component's history + /// to determine if a rollback is needed. Returns true if we should do a rollback. + /// Will default to a PartialEq::ne implementation, but can be overriden. + pub should_rollback: unsafe fn(), +} + +impl PredictionMetadata { + fn default_from(mode: ComponentSyncMode) -> Self { + let should_rollback: RollbackCheckFn = ::ne; + Self { + prediction_mode: mode, + correction: None, + should_rollback: unsafe { std::mem::transmute(should_rollback) }, + } + } } #[derive(Debug, Clone, PartialEq)] @@ -89,7 +191,13 @@ type RawWriteFn = fn( &mut ConnectionEvents, ) -> anyhow::Result<()>; -type LerpFn = fn(start: &C, other: &C, t: f32) -> C; +/// Function used to interpolate from one component state (`start`) to another (`other`) +/// t goes from 0.0 (`start`) to 1.0 (`other`) +pub type LerpFn = fn(start: &C, other: &C, t: f32) -> C; + +/// Function used to check if a rollback is needed, by comparing the server's value with the client's predicted value. +/// Defaults to PartialEq::eq +type RollbackCheckFn = fn(this: &C, that: &C) -> bool; pub trait Linear { fn lerp(start: &Self, other: &Self, t: f32) -> Self; @@ -141,7 +249,7 @@ impl ComponentRegistry { } } - pub(crate) fn register_component(&mut self) { + pub(crate) fn register_component(&mut self) { let component_kind = self.kind_map.add::(); self.serialize_fns_map .insert(component_kind, ErasedSerializeFns::new::()); @@ -169,28 +277,34 @@ impl ComponentRegistry { erased_fns.add_map_entities::(); } - pub(crate) fn set_prediction_mode(&mut self, mode: ComponentSyncMode) { + pub(crate) fn set_prediction_mode(&mut self, mode: ComponentSyncMode) { let kind = ComponentKind::of::(); + let default_equality_fn = ::eq; self.prediction_map .entry(kind) - .or_insert_with(|| PredictionMetadata { - prediction_mode: mode, - correction: None, - }); + .or_insert_with(|| PredictionMetadata::default_from::(mode)); } - pub(crate) fn set_linear_correction(&mut self) { + pub(crate) fn set_rollback_check( + &mut self, + rollback_check: RollbackCheckFn, + ) { + let kind = ComponentKind::of::(); + self.prediction_map + .entry(kind) + .or_insert_with(|| PredictionMetadata::default_from::(ComponentSyncMode::Full)) + .should_rollback = unsafe { std::mem::transmute(rollback_check) }; + } + + pub(crate) fn set_linear_correction(&mut self) { self.set_correction(::lerp); } - pub(crate) fn set_correction(&mut self, correction_fn: LerpFn) { + pub(crate) fn set_correction(&mut self, correction_fn: LerpFn) { let kind = ComponentKind::of::(); self.prediction_map .entry(kind) - .or_insert_with(|| PredictionMetadata { - prediction_mode: ComponentSyncMode::Full, - correction: None, - }) + .or_insert_with(|| PredictionMetadata::default_from::(ComponentSyncMode::Full)) .correction = Some(unsafe { std::mem::transmute(correction_fn) }); } @@ -298,6 +412,20 @@ impl ComponentRegistry { .context("the component is not part of the protocol") .map_or(false, |metadata| metadata.correction.is_some()) } + + /// Returns true if we should do a rollback + pub(crate) fn should_rollback(&self, this: &C, that: &C) -> bool { + let kind = ComponentKind::of::(); + let prediction_metadata = self + .prediction_map + .get(&kind) + .context("the component is not part of the protocol") + .unwrap(); + let rollback_check_fn: RollbackCheckFn = + unsafe { std::mem::transmute(prediction_metadata.should_rollback) }; + rollback_check_fn(this, that) + } + pub(crate) fn correct(&self, predicted: &C, corrected: &C, t: f32) -> C { let kind = ComponentKind::of::(); let prediction_metadata = self @@ -342,7 +470,7 @@ impl ComponentRegistry { (replication_metadata.write)(self, reader, net_id, entity_world_mut, entity_map, events) } - pub(crate) fn write( + pub(crate) fn write( &self, reader: &mut BitcodeReader, net_id: ComponentNetId, @@ -357,9 +485,11 @@ impl ComponentRegistry { let tick = Tick(0); // TODO: should we send the event based on on the message type (Insert/Update) or based on whether the component was actually inserted? if let Some(mut c) = entity_world_mut.get_mut::() { - events.push_update_component(entity, net_id, tick); - // TODO: use set_if_neq for PartialEq - *c = component; + // only apply the update if the component is different, to not trigger change detection + if c.as_ref() != &component { + events.push_update_component(entity, net_id, tick); + *c = component; + } } else { events.push_insert_component(entity, net_id, tick); entity_world_mut.insert(component); @@ -420,10 +550,10 @@ fn register_component_send(app: &mut App, direction: ChannelDirect pub trait AppComponentExt { /// Registers the component in the Registry /// This component can now be sent over the network. - fn register_component( + fn register_component( &mut self, direction: ChannelDirection, - ) -> ComponentRegistration<'_>; + ) -> ComponentRegistration<'_, C>; /// Enable prediction systems for this component. /// You can specify the prediction [`ComponentSyncMode`] @@ -435,6 +565,11 @@ pub trait AppComponentExt { /// Add a `Correction` behaviour to this component. fn add_correction_fn(&mut self, correction_fn: LerpFn); + /// Add a custom function to use for checking if a rollback is needed. + /// (By default we use the PartialEq::eq function, but you can use this to override the + /// equality check. For example, you might want to add a threshold for floating point numbers) + fn add_rollback_check(&mut self, rollback_check: RollbackCheckFn); + /// Register helper systems to perform interpolation for the component; but the user has to define the interpolation logic /// themselves (the interpolation_fn will not be used) fn add_custom_interpolation(&mut self, interpolation_mode: ComponentSyncMode); @@ -450,14 +585,18 @@ pub trait AppComponentExt { fn add_interpolation_fn(&mut self, interpolation_fn: LerpFn); } -pub struct ComponentRegistration<'a> { +pub struct ComponentRegistration<'a, C> { app: &'a mut App, + _phantom: std::marker::PhantomData, } -impl ComponentRegistration<'_> { +impl ComponentRegistration<'_, C> { /// Specify that the component contains entities which should be mapped from the remote world to the local world /// upon deserialization - pub fn add_map_entities(self) -> Self { + pub fn add_map_entities(self) -> Self + where + C: MapEntities + 'static, + { let mut registry = self.app.world.resource_mut::(); registry.add_map_entities::(); self @@ -465,68 +604,97 @@ impl ComponentRegistration<'_> { /// Enable prediction systems for this component. /// You can specify the prediction [`ComponentSyncMode`] - pub fn add_prediction(self, prediction_mode: ComponentSyncMode) -> Self { + pub fn add_prediction(self, prediction_mode: ComponentSyncMode) -> Self + where + C: SyncComponent, + { self.app.add_prediction::(prediction_mode); self } /// Add a `Correction` behaviour to this component by using a linear interpolation function. - pub fn add_linear_correction_fn(self) -> Self { + pub fn add_linear_correction_fn(self) -> Self + where + C: SyncComponent + Linear, + { self.app.add_linear_correction_fn::(); self } /// Add a `Correction` behaviour to this component. - pub fn add_correction_fn(self, correction_fn: LerpFn) -> Self { + pub fn add_correction_fn(self, correction_fn: LerpFn) -> Self + where + C: SyncComponent, + { self.app.add_correction_fn::(correction_fn); self } + /// Add a custom function to use for checking if a rollback is needed. + /// (By default we use the PartialEq::eq function, but you can use this to override the + /// equality check. For example, you might want to add a threshold for floating point numbers) + pub fn add_rollback_check(self, rollback_check: RollbackCheckFn) -> Self + where + C: SyncComponent, + { + self.app.add_rollback_check::(rollback_check); + self + } + /// Enable interpolation systems for this component. /// You can specify the interpolation [`ComponentSyncMode`] - pub fn add_interpolation( - self, - interpolation_mode: ComponentSyncMode, - ) -> Self { + pub fn add_interpolation(self, interpolation_mode: ComponentSyncMode) -> Self + where + C: SyncComponent, + { self.app.add_interpolation::(interpolation_mode); self } /// Register helper systems to perform interpolation for the component; but the user has to define the interpolation logic /// themselves (the interpolation_fn will not be used) - pub fn add_custom_interpolation( - self, - interpolation_mode: ComponentSyncMode, - ) -> Self { + pub fn add_custom_interpolation(self, interpolation_mode: ComponentSyncMode) -> Self + where + C: SyncComponent, + { self.app.add_custom_interpolation::(interpolation_mode); self } /// Add a `Interpolation` behaviour to this component by using a linear interpolation function. - pub fn add_linear_interpolation_fn(self) -> Self { + pub fn add_linear_interpolation_fn(self) -> Self + where + C: SyncComponent + Linear, + { self.app.add_linear_interpolation_fn::(); self } /// Add a `Interpolation` behaviour to this component. - pub fn add_interpolation_fn(self, interpolation_fn: LerpFn) -> Self { + pub fn add_interpolation_fn(self, interpolation_fn: LerpFn) -> Self + where + C: SyncComponent, + { self.app.add_interpolation_fn::(interpolation_fn); self } } impl AppComponentExt for App { - fn register_component( + fn register_component( &mut self, direction: ChannelDirection, - ) -> ComponentRegistration { + ) -> ComponentRegistration<'_, C> { let mut registry = self.world.resource_mut::(); if !registry.is_registered::() { registry.register_component::(); } debug!("register component {}", std::any::type_name::()); register_component_send::(self, direction); - ComponentRegistration { app: self } + ComponentRegistration { + app: self, + _phantom: std::marker::PhantomData, + } } fn add_prediction(&mut self, prediction_mode: ComponentSyncMode) { @@ -551,6 +719,11 @@ impl AppComponentExt for App { registry.set_correction::(correction_fn); } + fn add_rollback_check(&mut self, rollback_check: RollbackCheckFn) { + let mut registry = self.world.resource_mut::(); + registry.set_rollback_check::(rollback_check); + } + fn add_custom_interpolation( &mut self, interpolation_mode: ComponentSyncMode, diff --git a/lightyear/src/protocol/message.rs b/lightyear/src/protocol/message.rs index 28e2f1e35..c9274fc59 100644 --- a/lightyear/src/protocol/message.rs +++ b/lightyear/src/protocol/message.rs @@ -25,7 +25,7 @@ use crate::inputs::native::input_buffer::InputMessage; use crate::packet::message::Message; use crate::prelude::server::ServerConfig; use crate::prelude::{ChannelDirection, ChannelKind, MainSet}; -use crate::protocol::component::ComponentKind; +use crate::protocol::component::{ComponentKind, ComponentRegistration}; use crate::protocol::registry::{NetId, TypeKind, TypeMapper}; use crate::protocol::serialize::{ErasedSerializeFns, MapEntitiesFn}; use crate::protocol::{BitSerializable, EventContext}; @@ -49,6 +49,57 @@ pub(crate) enum MessageType { Normal, } +/// A [`Resource`] that will keep track of all the [`Message`]s that can be sent over the network. +/// A [`Message`] is any type that is serializable and deserializable. +/// +/// +/// ### Adding Messages +/// +/// You register messages by calling the [`add_message`](AppMessageExt::add_message) method directly on the App. +/// You can provide a [`ChannelDirection`] to specify if the message should be sent from the client to the server, from the server to the client, or both. +/// +/// ```rust +/// use bevy::prelude::*; +/// use serde::{Deserialize, Serialize}; +/// use lightyear::prelude::*; +/// +/// #[derive(Serialize, Deserialize)] +/// struct MyMessage; +/// +/// fn add_messages(app: &mut App) { +/// app.add_message::(ChannelDirection::Bidirectional); +/// } +/// ``` +/// +/// ### Customizing Message behaviour +/// +/// There are some cases where you might want to define additional behaviour for a message. +/// For example, if the message contains [`Entities`](bevy::prelude::Entity), you need to specify how those en +/// entities will be mapped from the remote world to the local world. +/// +/// Provided that your type implements [`MapEntities`], you can extend the protocol to support this behaviour, by +/// calling the [`add_map_entities`](MessageRegistration::add_map_entities) method. +/// +/// ```rust +/// use bevy::ecs::entity::{EntityMapper, MapEntities}; +/// use bevy::prelude::*; +/// use serde::{Deserialize, Serialize}; +/// use lightyear::prelude::*; +/// +/// #[derive(Serialize, Deserialize)] +/// struct MyMessage(Entity); +/// +/// impl MapEntities for MyMessage { +/// fn map_entities(&mut self, entity_map: &mut M) { +/// self.0 = entity_map.map_entity(self.0); +/// } +/// } +/// +/// fn add_messages(app: &mut App) { +/// app.add_message::(ChannelDirection::Bidirectional) +/// .add_map_entities(); +/// } +/// ``` #[derive(Debug, Default, Clone, Resource, PartialEq, TypePath)] pub struct MessageRegistry { typed_map: HashMap, @@ -136,14 +187,18 @@ fn register_resource_send(app: &mut App, direction: Chann } } -pub struct MessageRegistration<'a> { +pub struct MessageRegistration<'a, M> { app: &'a mut App, + _marker: std::marker::PhantomData, } -impl MessageRegistration<'_> { +impl MessageRegistration<'_, M> { /// Specify that the message contains entities which should be mapped from the remote world to the local world /// upon deserialization - pub fn add_map_entities(self) -> Self { + pub fn add_map_entities(self) -> Self + where + M: MapEntities + 'static, + { let mut registry = self.app.world.resource_mut::(); registry.add_map_entities::(); self @@ -154,7 +209,10 @@ impl MessageRegistration<'_> { pub trait AppMessageExt { /// Registers the message in the Registry /// This message can now be sent over the network. - fn add_message(&mut self, direction: ChannelDirection); + fn add_message( + &mut self, + direction: ChannelDirection, + ) -> MessageRegistration<'_, M>; /// Registers the resource in the Registry /// This resource can now be sent over the network. @@ -162,13 +220,20 @@ pub trait AppMessageExt { } impl AppMessageExt for App { - fn add_message(&mut self, direction: ChannelDirection) { + fn add_message( + &mut self, + direction: ChannelDirection, + ) -> MessageRegistration<'_, M> { let mut registry = self.world.resource_mut::(); if !registry.is_registered::() { registry.add_message::(MessageType::Normal); } debug!("register message {}", std::any::type_name::()); register_message_send::(self, direction); + MessageRegistration { + app: self, + _marker: std::marker::PhantomData, + } } /// Register a resource to be automatically replicated over the network diff --git a/lightyear/src/protocol/mod.rs b/lightyear/src/protocol/mod.rs index c1aeca023..ac598939e 100644 --- a/lightyear/src/protocol/mod.rs +++ b/lightyear/src/protocol/mod.rs @@ -1,11 +1,14 @@ -//! The Protocol is used to define all the types that can be sent over the network -//! -//! A protocol is composed of a few main parts: -//! - a [`MessageRegistry`](message::MessageRegistry) that contains the list of all the messages that can be sent over the network, along with how to serialize and deserialize them -//! - a [`ComponentRegistry`](component::ComponentRegistry) that contains the list of all the components that can be sent over the network, along with how to serialize and deserialize them. -//! You can also define additional behaviour for each component (such as how to run interpolation for them, etc.) -//! - a list of inputs that can be sent from client to server -//! - a [`ChannelRegistry`](channel::ChannelRegistry) that contains the list of channels that define how the data will be sent over the network (reliability, ordering, etc.) +/*! +The Protocol is used to define all the types that can be sent over the network + +A protocol is composed of a few main parts: +- a [`MessageRegistry`](message::MessageRegistry) that contains the list of all the messages that can be sent over the network, along with how to serialize and deserialize them +- a [`ComponentRegistry`](component::ComponentRegistry) that contains the list of all the components that can be sent over the network, along with how to serialize and deserialize them. +You can also define additional behaviour for each component (such as how to run interpolation for them, etc.) +- a list of inputs that can be sent from client to server +- a [`ChannelRegistry`](channel::ChannelRegistry) that contains the list of channels that define how the data will be sent over the network (reliability, ordering, etc.) + +*/ use anyhow::Context; diff --git a/lightyear/src/server/clients.rs b/lightyear/src/server/clients.rs new file mode 100644 index 000000000..d67ecf4f0 --- /dev/null +++ b/lightyear/src/server/clients.rs @@ -0,0 +1,111 @@ +//! The server spawns an entity per connected client to store metadata about them. +//! +//! This module contains components and systems to manage the metadata on client entities. +use crate::prelude::ClientId; +use crate::shared::sets::{InternalReplicationSet, ServerMarker}; +use bevy::ecs::entity::EntityHashSet; +use bevy::prelude::*; + +/// List of entities under the control of a client +#[derive(Component, Default, Debug, Deref, DerefMut)] +pub struct ControlledEntities(pub EntityHashSet); + +pub(crate) struct ClientsMetadataPlugin; + +mod systems { + use super::*; + use crate::prelude::Replicate; + use crate::server::clients::ControlledEntities; + use crate::server::connection::ConnectionManager; + use crate::server::events::DisconnectEvent; + use crate::shared::replication::components::ControlledBy; + use crate::shared::replication::network_target::NetworkTarget; + use tracing::{debug, error, trace}; + + // TODO: remove entity from ControlledBy when ControlledBy gets removed! (via observers)? + // TODO: remove entity in controlled by lists after the component gets updated + + pub(super) fn handle_controlled_by_update( + sender: Res, + query: Query<(Entity, &ControlledBy), Changed>, + mut client_query: Query<&mut ControlledEntities>, + ) { + let update_controlled_entities = + |entity: Entity, + client_id: ClientId, + client_query: &mut Query<&mut ControlledEntities>, + sender: &ConnectionManager| { + trace!( + "Adding entity {:?} to client {:?}'s controlled entities", + entity, + client_id + ); + if let Ok(client_entity) = sender.client_entity(client_id) { + if let Ok(mut controlled_entities) = client_query.get_mut(client_entity) { + // first check if it already contains, to not trigger change detection needlessly + if controlled_entities.contains(&entity) { + return; + } + controlled_entities.insert(entity); + } + } + }; + + for (entity, controlled_by) in query.iter() { + match &controlled_by.target { + NetworkTarget::None => {} + NetworkTarget::Single(client_id) => { + update_controlled_entities(entity, *client_id, &mut client_query, &sender); + } + NetworkTarget::Only(client_ids) => client_ids.iter().for_each(|client_id| { + update_controlled_entities(entity, *client_id, &mut client_query, &sender); + }), + _ => { + let client_ids: Vec = sender.connected_clients().collect(); + client_ids.iter().for_each(|client_id| { + update_controlled_entities(entity, *client_id, &mut client_query, &sender); + }); + } + } + } + } + + /// When a client disconnect, we despawn all the entities it controlled + pub(super) fn handle_client_disconnect( + mut commands: Commands, + client_query: Query<&ControlledEntities>, + mut events: EventReader, + ) { + for event in events.read() { + // despawn all the controlled entities for the disconnected client + if let Ok(controlled_entities) = client_query.get(event.entity) { + error!( + "Despawning all entities controlled by client {:?}", + event.client_id + ); + for entity in controlled_entities.iter() { + error!( + "Despawning entity {entity:?} controlled by client {:?}", + event.client_id + ); + commands.entity(*entity).despawn_recursive(); + } + } + // despawn the entity itself + commands.entity(event.entity).despawn_recursive(); + } + } +} + +impl Plugin for ClientsMetadataPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PostUpdate, + systems::handle_controlled_by_update + .in_set(InternalReplicationSet::::BeforeBuffer), + ); + // we handle this in the `Last` `SystemSet` to let the user handle the disconnect event + // however they want first, before the client entity gets despawned + app.add_systems(Last, systems::handle_client_disconnect); + } +} diff --git a/lightyear/src/server/config.rs b/lightyear/src/server/config.rs index 08d33daf8..8e4cfb42f 100644 --- a/lightyear/src/server/config.rs +++ b/lightyear/src/server/config.rs @@ -3,9 +3,8 @@ use bevy::prelude::Resource; use governor::Quota; use nonzero_ext::nonzero; -use crate::connection::netcode::Key; +use crate::connection::netcode::{Key, PRIVATE_KEY_BYTES}; use crate::connection::server::NetConfig; -use crate::server::replication::ReplicationConfig; use crate::shared::config::SharedConfig; use crate::shared::ping::manager::PingConfig; @@ -18,7 +17,7 @@ pub struct NetcodeConfig { /// The default is 3 seconds. A negative value means no timeout. pub client_timeout_secs: i32, pub protocol_id: u64, - pub private_key: Option, + pub private_key: Key, } impl Default for NetcodeConfig { @@ -28,7 +27,7 @@ impl Default for NetcodeConfig { keep_alive_send_rate: 1.0 / 10.0, client_timeout_secs: 3, protocol_id: 0, - private_key: None, + private_key: [0; PRIVATE_KEY_BYTES], } } } @@ -39,7 +38,7 @@ impl NetcodeConfig { self } pub fn with_key(mut self, key: Key) -> Self { - self.private_key = Some(key); + self.private_key = key; self } @@ -95,5 +94,4 @@ pub struct ServerConfig { pub net: Vec, pub packet: PacketConfig, pub ping: PingConfig, - pub replication: ReplicationConfig, } diff --git a/lightyear/src/server/connection.rs b/lightyear/src/server/connection.rs index 352652917..412c6ea15 100644 --- a/lightyear/src/server/connection.rs +++ b/lightyear/src/server/connection.rs @@ -19,8 +19,10 @@ use crate::inputs::native::input_buffer::InputBuffer; use crate::packet::message_manager::MessageManager; use crate::packet::packet::Packet; use crate::packet::packet_manager::{Payload, PACKET_BUFFER_CAPACITY}; +use crate::prelude::server::{DisconnectEvent, RoomId, RoomManager}; use crate::prelude::{ - Channel, ChannelKind, Message, Mode, PreSpawnedPlayerObject, ShouldBePredicted, TargetEntity, + Channel, ChannelKind, Message, Mode, PreSpawnedPlayerObject, ReplicationGroup, + ShouldBePredicted, TargetEntity, }; use crate::protocol::channel::ChannelRegistry; use crate::protocol::component::{ComponentNetId, ComponentRegistry}; @@ -33,20 +35,22 @@ use crate::serialize::reader::ReadBuffer; use crate::serialize::writer::WriteBuffer; use crate::serialize::RawData; use crate::server::config::PacketConfig; -use crate::server::events::ServerEvents; +use crate::server::events::{ConnectEvent, ServerEvents}; use crate::server::message::ServerMessage; use crate::shared::events::connection::ConnectionEvents; use crate::shared::message::MessageSend; use crate::shared::ping::manager::{PingConfig, PingManager}; use crate::shared::ping::message::{Ping, Pong, SyncMessage}; use crate::shared::replication::components::{ - NetworkTarget, Replicate, ReplicationGroupId, ShouldBeInterpolated, + Controlled, ControlledBy, Replicate, ReplicationGroupId, ReplicationTarget, + ShouldBeInterpolated, }; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::replication::receive::ReplicationReceiver; use crate::shared::replication::send::ReplicationSender; -use crate::shared::replication::systems::DespawnMetadata; -use crate::shared::replication::ReplicationMessageData; -use crate::shared::replication::{ReplicationMessage, ReplicationSend}; +use crate::shared::replication::systems::ReplicateCache; +use crate::shared::replication::{ReplicationMessage, ReplicationReceive, ReplicationSend}; +use crate::shared::replication::{ReplicationMessageData, ReplicationPeer}; use crate::shared::sets::ServerMarker; use crate::shared::tick_manager::Tick; use crate::shared::tick_manager::TickManager; @@ -57,7 +61,6 @@ type EntityHashMap = hashbrown::HashMap; #[derive(Resource)] pub struct ConnectionManager { pub(crate) connections: HashMap, - pub(crate) component_registry: ComponentRegistry, pub(crate) message_registry: MessageRegistry, channel_registry: ChannelRegistry, pub(crate) events: ServerEvents, @@ -65,7 +68,7 @@ pub struct ConnectionManager { // NOTE: we put this here because we only need one per world, not one per connection /// Stores some values that are needed to correctly replicate the despawning of Replicated entity. /// (when the entity is despawned, we don't have access to its components anymore, so we cache them here) - replicate_component_cache: EntityHashMap, + pub(crate) replicate_component_cache: EntityHashMap, // list of clients that connected since the last time we sent replication messages // (we want to keep track of them because we need to replicate the entire world state to them) @@ -78,7 +81,6 @@ pub struct ConnectionManager { impl ConnectionManager { pub(crate) fn new( - component_registry: ComponentRegistry, message_registry: MessageRegistry, channel_registry: ChannelRegistry, packet_config: PacketConfig, @@ -86,7 +88,6 @@ impl ConnectionManager { ) -> Self { Self { connections: HashMap::default(), - component_registry, message_registry, channel_registry, events: ServerEvents::new(), @@ -99,6 +100,65 @@ impl ConnectionManager { } } + /// Return the [`Entity`] associated with the given [`ClientId`] + pub fn client_entity(&self, client_id: ClientId) -> Result { + self.connection(client_id).map(|c| c.entity) + } + + /// Return the list of connected [`ClientId`]s + pub fn connected_clients(&self) -> impl Iterator + '_ { + self.connections.keys().copied() + } + + /// Queues up a message to be sent to all clients matching the specific [`NetworkTarget`] + pub fn send_message_to_target( + &mut self, + message: &M, + target: NetworkTarget, + ) -> Result<()> { + self.erased_send_message_to_target(message, ChannelKind::of::(), target) + } + + /// Send a message to all clients in a room + pub fn send_message_to_room( + &mut self, + message: &M, + room_id: RoomId, + room_manager: &RoomManager, + ) -> Result<()> { + let room = room_manager.get_room(room_id).context("room not found")?; + let target = NetworkTarget::Only(room.clients.iter().copied().collect()); + self.send_message_to_target::(message, target) + } + + /// Queues up a message to be sent to a client + pub fn send_message( + &mut self, + client_id: ClientId, + message: &M, + ) -> Result<()> { + self.send_message_to_target::(message, NetworkTarget::Only(vec![client_id])) + } + + /// Update the priority of a `ReplicationGroup` that is replicated to a given client + pub fn update_priority( + &mut self, + replication_group_id: ReplicationGroupId, + client_id: ClientId, + priority: f32, + ) -> Result<()> { + debug!( + ?client_id, + ?replication_group_id, + "Set priority to {:?}", + priority + ); + self.connection_mut(client_id)? + .replication_sender + .update_base_priority(replication_group_id, priority); + Ok(()) + } + /// Find the list of clients that should receive the replication message pub(crate) fn apply_replication( &mut self, @@ -158,18 +218,22 @@ impl ConnectionManager { } /// Add a new [`Connection`] to the list of connections with the given [`ClientId`] - pub(crate) fn add(&mut self, client_id: ClientId) { + pub(crate) fn add(&mut self, client_id: ClientId, client_entity: Entity) { if let Entry::Vacant(e) = self.connections.entry(client_id) { #[cfg(feature = "metrics")] metrics::gauge!("connected_clients").increment(1.0); info!("New connection from id: {}", client_id); let connection = Connection::new( + client_entity, &self.channel_registry, self.packet_config.clone(), self.ping_config.clone(), ); - self.events.push_connection(client_id); + self.events.add_connect_event(ConnectEvent { + client_id, + entity: client_entity, + }); self.new_clients.push(client_id); e.insert(connection); } else { @@ -177,13 +241,20 @@ impl ConnectionManager { } } - pub(crate) fn remove(&mut self, client_id: ClientId) { + /// Remove the connection associated with the given [`ClientId`], + /// and returns the [`Entity`] associated with the client + pub(crate) fn remove(&mut self, client_id: ClientId) -> Entity { #[cfg(feature = "metrics")] metrics::gauge!("connected_clients").decrement(1.0); info!("Client {} disconnected", client_id); - self.events.push_disconnection(client_id); + let entity = self + .client_entity(client_id) + .expect("client entity not found"); + self.events + .add_disconnect_event(DisconnectEvent { client_id, entity }); self.connections.remove(&client_id); + entity } pub(crate) fn buffer_message( @@ -194,21 +265,12 @@ impl ConnectionManager { ) -> Result<()> { self.connections .iter_mut() - .filter(|(id, _)| target.should_send_to(id)) + .filter(|(id, _)| target.targets(id)) // TODO: is it worth it to use Arc> or Bytes to have a free clone? // at some point the bytes will have to be copied into the final message, so maybe do it now? .try_for_each(|(_, c)| c.buffer_message(message.clone(), channel)) } - /// Queues up a message to be sent to all clients matching the specific [`NetworkTarget`] - pub fn send_message_to_target( - &mut self, - message: &M, - target: NetworkTarget, - ) -> Result<()> { - self.erased_send_message_to_target(message, ChannelKind::of::(), target) - } - pub(crate) fn erased_send_message_to_target( &mut self, message: &M, @@ -222,15 +284,6 @@ impl ConnectionManager { self.buffer_message(message_bytes, channel_kind, target) } - /// Queues up a message to be sent to a client - pub fn send_message( - &mut self, - client_id: ClientId, - message: &M, - ) -> Result<()> { - self.send_message_to_target::(message, NetworkTarget::Only(vec![client_id])) - } - /// Buffer all the replication messages to send. /// Keep track of the bevy Change Tick: when a message is acked, we know that we only have to send /// the updates since that Change Tick @@ -282,18 +335,18 @@ impl ConnectionManager { impl ConnectionManager { /// Helper function to prepare component insert for components for which we know the type - fn prepare_typed_component_insert( + pub(crate) fn prepare_typed_component_insert( &mut self, entity: Entity, group_id: ReplicationGroupId, client_id: ClientId, + component_registry: &ComponentRegistry, data: &C, ) -> Result<()> { - let net_id = self - .component_registry() + let net_id = component_registry .get_net_id::() .context(format!("{} is not registered", std::any::type_name::()))?; - let raw_data = self.component_registry.serialize(data, &mut self.writer)?; + let raw_data = component_registry.serialize(data, &mut self.writer)?; self.connection_mut(client_id)? .replication_sender .prepare_component_insert(entity, group_id, net_id, raw_data); @@ -303,7 +356,10 @@ impl ConnectionManager { /// Wrapper that handles the connection between the server and a client pub struct Connection { - pub message_manager: MessageManager, + /// We create one entity per connected client, so that users + /// can store metadata about the client using the ECS + entity: Entity, + pub(crate) message_manager: MessageManager, pub(crate) replication_sender: ReplicationSender, pub(crate) replication_receiver: ReplicationReceiver, pub(crate) events: ConnectionEvents, @@ -324,6 +380,7 @@ pub struct Connection { impl Connection { pub(crate) fn new( + entity: Entity, channel_registry: &ChannelRegistry, packet_config: PacketConfig, ping_config: PingConfig, @@ -344,6 +401,7 @@ impl Connection { ReplicationSender::new(update_acks_tracker, replication_update_send_receiver); let replication_receiver = ReplicationReceiver::new(); Self { + entity, message_manager, replication_sender, replication_receiver, @@ -626,99 +684,41 @@ impl MessageSend for ConnectionManager { } } -impl ReplicationSend for ConnectionManager { +impl ReplicationPeer for ConnectionManager { type Events = ServerEvents; type EventContext = ClientId; type SetMarker = ServerMarker; +} +impl ReplicationReceive for ConnectionManager { fn events(&mut self) -> &mut Self::Events { &mut self.events } - fn writer(&mut self) -> &mut BitcodeWriter { - &mut self.writer - } - - fn component_registry(&self) -> &ComponentRegistry { - &self.component_registry + fn cleanup(&mut self, tick: Tick) { + debug!("Running replication receive cleanup"); + for connection in self.connections.values_mut() { + connection.replication_receiver.cleanup(tick); + } } +} - fn update_priority( - &mut self, - replication_group_id: ReplicationGroupId, - client_id: ClientId, - priority: f32, - ) -> Result<()> { - debug!( - ?client_id, - ?replication_group_id, - "Set priority to {:?}", - priority - ); - let replication_sender = &mut self.connection_mut(client_id)?.replication_sender; - replication_sender.update_base_priority(replication_group_id, priority); - Ok(()) +impl ReplicationSend for ConnectionManager { + fn writer(&mut self) -> &mut BitcodeWriter { + &mut self.writer } fn new_connected_clients(&self) -> Vec { self.new_clients.clone() } - fn prepare_entity_spawn( - &mut self, - entity: Entity, - replicate: &Replicate, - target: NetworkTarget, - system_current_tick: BevyTick, - ) -> Result<()> { - trace!(?entity, "Prepare entity spawn to client"); - let group_id = replicate.replication_group.group_id(Some(entity)); - // TODO: should we have additional state tracking so that we know we are in the process of sending this entity to clients? - - self.apply_replication(target).try_for_each(|client_id| { - // if we need to do prediction/interpolation, send a marker component to indicate that to the client - if replicate.prediction_target.should_send_to(&client_id) { - // TODO: the serialized data is always the same; cache it somehow? - self.prepare_typed_component_insert( - entity, - group_id, - client_id, - &ShouldBePredicted, - )?; - } - if replicate.interpolation_target.should_send_to(&client_id) { - self.prepare_typed_component_insert( - entity, - group_id, - client_id, - &ShouldBeInterpolated, - )?; - } - let replication_sender = &mut self.connection_mut(client_id)?.replication_sender; - // update the collect changes tick - // replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); - if let TargetEntity::Preexisting(remote_entity) = replicate.target_entity { - replication_sender.prepare_entity_spawn_reuse(entity, group_id, remote_entity); - } else { - replication_sender.prepare_entity_spawn(entity, group_id); - } - // also set the priority for the group when we spawn it - self.update_priority(group_id, client_id, replicate.replication_group.priority())?; - Ok(()) - }) - } - fn prepare_entity_despawn( &mut self, entity: Entity, - replication_group_id: ReplicationGroupId, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()> { + let group_id = group.group_id(Some(entity)); self.apply_replication(target).try_for_each(|client_id| { // trace!( // ?entity, @@ -727,13 +727,9 @@ impl ReplicationSend for ConnectionManager { // self.tick_manager.tick() // ); let replication_sender = &mut self.connection_mut(client_id)?.replication_sender; - // update the collect changes tick - // replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); - replication_sender.prepare_entity_despawn(entity, replication_group_id); + self.connection_mut(client_id)? + .replication_sender + .prepare_entity_despawn(entity, group_id); Ok(()) }) } @@ -744,11 +740,12 @@ impl ReplicationSend for ConnectionManager { entity: Entity, kind: ComponentNetId, component: RawData, - replicate: &Replicate, + component_registry: &ComponentRegistry, + replication_target: &ReplicationTarget, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()> { - let group_id = replicate.replication_group.group_id(Some(entity)); + let group_id = group.group_id(Some(entity)); // TODO: think about this. this feels a bit clumsy // TODO: this might not be required anymore since we separated ShouldBePredicted from PrePredicted @@ -766,18 +763,15 @@ impl ReplicationSend for ConnectionManager { // same thing for PreSpawnedPlayerObject: that component should only be replicated to prediction_target let mut actual_target = target; - let should_be_predicted_kind = self - .component_registry() + let should_be_predicted_kind = component_registry .get_net_id::() .context("ShouldBePredicted is not registered")?; - let pre_spawned_player_object_kind = self - .component_registry() + let pre_spawned_player_object_kind = component_registry .get_net_id::() .context("PreSpawnedPlayerObject is not registered")?; if kind == should_be_predicted_kind || kind == pre_spawned_player_object_kind { - actual_target = replicate.prediction_target.clone(); + actual_target = replication_target.prediction.clone(); } - self.apply_replication(actual_target) .try_for_each(|client_id| { // trace!( @@ -793,12 +787,9 @@ impl ReplicationSend for ConnectionManager { // .entry(group) // .or_default() // .update_collect_changes_since_this_tick(system_current_tick); - replication_sender.prepare_component_insert( - entity, - group_id, - kind, - component.clone(), - ); + self.connection_mut(client_id)? + .replication_sender + .prepare_component_insert(entity, group_id, kind, component.clone()); Ok(()) }) } @@ -807,27 +798,22 @@ impl ReplicationSend for ConnectionManager { &mut self, entity: Entity, kind: ComponentNetId, - replicate: &Replicate, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()> { - let group_id = replicate.replication_group.group_id(Some(entity)); + let group_id = group.group_id(Some(entity)); debug!(?entity, ?kind, "Sending RemoveComponent"); self.apply_replication(target).try_for_each(|client_id| { - let replication_sender = &mut self.connection_mut(client_id)?.replication_sender; // TODO: I don't think it's actually correct to only correct the changes since that action. - // what if we do: - // - Frame 1: update is ACKED - // - Frame 2: update - // - Frame 3: action - // - Frame 4: send - // then we won't send the frame-2 update because we only collect changes since frame 3 - // replication_sender - // .group_channels - // .entry(group) - // .or_default() - // .update_collect_changes_since_this_tick(system_current_tick); - replication_sender.prepare_component_remove(entity, group_id, kind); + // what if we do: + // - Frame 1: update is ACKED + // - Frame 2: update + // - Frame 3: action + // - Frame 4: send + // then we won't send the frame-2 update because we only collect changes since frame 3 + self.connection_mut(client_id)? + .replication_sender + .prepare_component_remove(entity, group_id, kind); Ok(()) }) } @@ -837,7 +823,7 @@ impl ReplicationSend for ConnectionManager { entity: Entity, kind: ComponentNetId, component: RawData, - replicate: &Replicate, + group: &ReplicationGroup, target: NetworkTarget, component_change_tick: BevyTick, system_current_tick: BevyTick, @@ -850,7 +836,7 @@ impl ReplicationSend for ConnectionManager { "Prepare entity update" ); - let group_id = replicate.group_id(Some(entity)); + let group_id = group.group_id(Some(entity)); self.apply_replication(target).try_for_each(|client_id| { // TODO: should we have additional state tracking so that we know we are in the process of sending this entity to clients? let replication_sender = &mut self.connection_mut(client_id)?.replication_sender; @@ -894,44 +880,14 @@ impl ReplicationSend for ConnectionManager { self.buffer_replication_messages(tick, bevy_tick) } - fn get_mut_replicate_despawn_cache( - &mut self, - ) -> &mut bevy::ecs::entity::EntityHashMap { + fn get_mut_replicate_cache(&mut self) -> &mut bevy::ecs::entity::EntityHashMap { &mut self.replicate_component_cache } fn cleanup(&mut self, tick: Tick) { - debug!("Running replication clean"); + debug!("Running replication send cleanup"); for connection in self.connections.values_mut() { - // if it's been enough time since we last any action for the group, we can set the last_action_tick to None - // (meaning that there's no need when we receive the update to check if we have already received a previous action) - for group_channel in connection.replication_sender.group_channels.values_mut() { - debug!("Checking group channel: {:?}", group_channel); - if let Some(last_action_tick) = group_channel.last_action_tick { - if tick - last_action_tick > (i16::MAX / 2) { - debug!( - ?tick, - ?last_action_tick, - ?group_channel, - "Setting the last_action tick to None because there hasn't been any new actions in a while"); - group_channel.last_action_tick = None; - } - } - } - // if it's been enough time since we last had any update for the group, we update the latest_tick for the group - for group_channel in connection.replication_receiver.group_channels.values_mut() { - debug!("Checking group channel: {:?}", group_channel); - if let Some(latest_tick) = group_channel.latest_tick { - if tick - latest_tick > (i16::MAX / 2) { - debug!( - ?tick, - ?latest_tick, - ?group_channel, - "Setting the latest_tick tick to tick because there hasn't been any new updates in a while"); - group_channel.latest_tick = Some(tick); - } - } - } + connection.replication_sender.cleanup(tick); } } } diff --git a/lightyear/src/server/events.rs b/lightyear/src/server/events.rs index 123ca0f0a..26b392e86 100644 --- a/lightyear/src/server/events.rs +++ b/lightyear/src/server/events.rs @@ -19,13 +19,16 @@ use crate::shared::sets::{InternalMainSet, ServerMarker}; type EntityHashMap = hashbrown::HashMap; -/// Plugin that handles generating bevy [`Events`] related to networking and replication +/// Plugin that adds bevy [`Events`] related to networking and replication #[derive(Default)] pub struct ServerEventsPlugin; impl Plugin for ServerEventsPlugin { fn build(&self, app: &mut App) { app + // EVENTS + .add_event::() + .add_event::() // PLUGIN .add_plugins(EventsPlugin::::default()); } @@ -33,8 +36,8 @@ impl Plugin for ServerEventsPlugin { #[derive(Debug)] pub struct ServerEvents { - pub connections: Vec, - pub disconnections: Vec, + pub connections: Vec, + pub disconnections: Vec, pub events: HashMap, pub empty: bool, } @@ -95,31 +98,30 @@ impl ServerEvents { // } // TODO: should we consume connections? - pub fn iter_connections(&mut self) -> impl Iterator + '_ { - std::mem::take(&mut self.connections).into_iter() + pub fn iter_connections(&mut self) -> Vec { + std::mem::take(&mut self.connections) } pub fn has_connections(&self) -> bool { !self.connections.is_empty() } - pub fn iter_disconnections(&mut self) -> impl Iterator + '_ { - std::mem::take(&mut self.disconnections).into_iter() + pub fn iter_disconnections(&mut self) -> Vec { + std::mem::take(&mut self.disconnections) } pub fn has_disconnections(&self) -> bool { !self.disconnections.is_empty() } - pub(crate) fn push_connection(&mut self, client_id: ClientId) { - self.connections.push(client_id); - // self.events.remove(&client_id); + pub(crate) fn add_connect_event(&mut self, connect_event: ConnectEvent) { + self.connections.push(connect_event); self.empty = false; } - pub(crate) fn push_disconnection(&mut self, client_id: ClientId) { - self.disconnections.push(client_id); - self.events.remove(&client_id); + pub(crate) fn add_disconnect_event(&mut self, disconnect_event: DisconnectEvent) { + self.disconnections.push(disconnect_event); + self.events.remove(&disconnect_event.client_id); self.empty = false; } @@ -209,9 +211,19 @@ impl IterComponentInsertEvent for ServerEvents { } /// Bevy [`Event`] emitted on the server on the frame where a client is connected -pub type ConnectEvent = crate::shared::events::components::ConnectEvent; +#[derive(Event, Debug, Copy, Clone)] +pub struct ConnectEvent { + pub client_id: ClientId, + pub entity: Entity, +} + /// Bevy [`Event`] emitted on the server on the frame where a client is disconnected -pub type DisconnectEvent = crate::shared::events::components::DisconnectEvent; +#[derive(Event, Debug, Copy, Clone)] +pub struct DisconnectEvent { + pub client_id: ClientId, + pub entity: Entity, +} + /// Bevy [`Event`] emitted on the server on the frame where an input message from a client is received pub type InputEvent = crate::shared::events::components::InputEvent; /// Bevy [`Event`] emitted on the server on the frame where a EntitySpawn replication message is received diff --git a/lightyear/src/server/input.rs b/lightyear/src/server/input.rs index 598a3023e..99f7cb40f 100644 --- a/lightyear/src/server/input.rs +++ b/lightyear/src/server/input.rs @@ -9,14 +9,14 @@ use crate::inputs::native::input_buffer::InputBuffer; use crate::inputs::native::InputMessage; use crate::prelude::server::MessageEvent; use crate::prelude::{ - AppMessageExt, ChannelDirection, ClientId, Message, MessageRegistry, NetworkTarget, - TickManager, UserAction, + AppMessageExt, ChannelDirection, ClientId, Message, MessageRegistry, TickManager, UserAction, }; use crate::protocol::message::MessageKind; use crate::protocol::BitSerializable; use crate::server::connection::ConnectionManager; use crate::server::events::InputEvent; use crate::server::networking::is_started; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::sets::{InternalMainSet, ServerMarker}; // - ClientInputs: diff --git a/lightyear/src/server/input_leafwing.rs b/lightyear/src/server/input_leafwing.rs index c61a31663..fac5e060a 100644 --- a/lightyear/src/server/input_leafwing.rs +++ b/lightyear/src/server/input_leafwing.rs @@ -17,7 +17,7 @@ use crate::inputs::leafwing::input_buffer::{ use crate::inputs::leafwing::{InputMessage, LeafwingUserAction}; use crate::prelude::client::is_in_rollback; use crate::prelude::server::MessageEvent; -use crate::prelude::{client, MessageRegistry, Mode, NetworkTarget, SharedConfig, TickManager}; +use crate::prelude::{client, MessageRegistry, Mode, SharedConfig, TickManager}; use crate::protocol::message::MessageKind; use crate::protocol::registry::NetId; use crate::protocol::BitSerializable; @@ -25,6 +25,7 @@ use crate::server::config::ServerConfig; use crate::server::connection::ConnectionManager; use crate::server::networking::is_started; use crate::shared::replication::components::PrePredicted; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::sets::{InternalMainSet, ServerMarker}; pub struct LeafwingInputPlugin { @@ -271,7 +272,7 @@ mod tests { incoming_loss: 0.0, }; let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); + let prediction_config = PredictionConfig::default(); let interpolation_config = InterpolationConfig::default(); let mut stepper = BevyStepper::new( shared_config, diff --git a/lightyear/src/server/io/config.rs b/lightyear/src/server/io/config.rs new file mode 100644 index 000000000..1e742b2fb --- /dev/null +++ b/lightyear/src/server/io/config.rs @@ -0,0 +1,157 @@ +use super::*; +use crate::prelude::CompressionConfig; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportBuilderEnum}; +use crate::transport::channels::Channels; +use crate::transport::config::SharedIoConfig; +use crate::transport::dummy::DummyIo; +use crate::transport::io::IoStats; +#[cfg(feature = "zstd")] +use crate::transport::middleware::compression::zstd::compression::ZstdCompressor; +#[cfg(feature = "zstd")] +use crate::transport::middleware::compression::zstd::decompression::ZstdDecompressor; +use crate::transport::middleware::conditioner::LinkConditioner; +use crate::transport::middleware::{PacketReceiverWrapper, PacketSenderWrapper}; +#[cfg(not(target_family = "wasm"))] +use crate::transport::udp::UdpSocketBuilder; +#[cfg(all(feature = "websocket", not(target_family = "wasm")))] +use crate::transport::websocket::server::WebSocketServerSocketBuilder; +#[cfg(all(feature = "webtransport", not(target_family = "wasm")))] +use crate::transport::webtransport::server::WebTransportServerSocketBuilder; +use crate::transport::BoxedReceiver; +use crate::transport::Transport; +use bevy::prelude::TypePath; +use bevy::reflect::Reflect; +use std::net::IpAddr; +#[cfg(all(feature = "webtransport", not(target_family = "wasm")))] +use wtransport::Identity; + +#[derive(Debug, TypePath)] +pub enum ServerTransport { + /// Use a [`UdpSocket`](std::net::UdpSocket) + #[cfg(not(target_family = "wasm"))] + UdpSocket(SocketAddr), + /// Use [`WebTransport`](https://wicg.github.io/web-transport/) as a transport layer + #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] + WebTransportServer { + server_addr: SocketAddr, + /// Certificate that will be used for authentication + certificate: Identity, + }, + /// Use [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) as a transport + #[cfg(all(feature = "websocket", not(target_family = "wasm")))] + WebSocketServer { server_addr: SocketAddr }, + /// Use a crossbeam_channel as a transport. This is useful for testing. + /// This is server-only: each tuple corresponds to a different client. + Channels { + channels: Vec<( + SocketAddr, + crossbeam_channel::Receiver>, + Sender>, + )>, + }, + /// Dummy transport if the connection handles its own io (for example steam sockets) + Dummy, +} + +/// We provide a manual implementation because wtranport's `Identity` does not implement Clone +impl Clone for ServerTransport { + #[inline] + fn clone(&self) -> ServerTransport { + match self { + #[cfg(not(target_family = "wasm"))] + ServerTransport::UdpSocket(__self_0) => { + ServerTransport::UdpSocket(Clone::clone(__self_0)) + } + #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] + ServerTransport::WebTransportServer { + server_addr: __self_0, + certificate: __self_1, + } => ServerTransport::WebTransportServer { + server_addr: Clone::clone(__self_0), + certificate: __self_1.clone_identity(), + }, + #[cfg(all(feature = "websocket", not(target_family = "wasm")))] + ServerTransport::WebSocketServer { + server_addr: __self_0, + } => ServerTransport::WebSocketServer { + server_addr: Clone::clone(__self_0), + }, + ServerTransport::Channels { channels: __self_0 } => ServerTransport::Channels { + channels: Clone::clone(__self_0), + }, + ServerTransport::Dummy => ServerTransport::Dummy, + } + } +} + +impl ServerTransport { + fn build(self) -> ServerTransportBuilderEnum { + match self { + #[cfg(not(target_family = "wasm"))] + ServerTransport::UdpSocket(addr) => { + ServerTransportBuilderEnum::UdpSocket(UdpSocketBuilder { local_addr: addr }) + } + #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] + ServerTransport::WebTransportServer { + server_addr, + certificate, + } => ServerTransportBuilderEnum::WebTransportServer(WebTransportServerSocketBuilder { + server_addr, + certificate, + }), + #[cfg(all(feature = "websocket", not(target_family = "wasm")))] + ServerTransport::WebSocketServer { server_addr } => { + ServerTransportBuilderEnum::WebSocketServer(WebSocketServerSocketBuilder { + server_addr, + }) + } + ServerTransport::Channels { channels } => { + ServerTransportBuilderEnum::Channels(Channels::new(channels)) + } + ServerTransport::Dummy => ServerTransportBuilderEnum::Dummy(DummyIo), + } + } +} + +impl Default for ServerTransport { + fn default() -> Self { + ServerTransport::UdpSocket(SocketAddr::new(IpAddr::from([127, 0, 0, 1]), 0)) + } +} + +impl SharedIoConfig { + pub fn start(self) -> Result { + let (transport, state, io_rx, network_tx) = self.transport.build().start()?; + let local_addr = transport.local_addr(); + #[allow(unused_mut)] + let (mut sender, receiver) = transport.split(); + #[allow(unused_mut)] + let mut receiver: BoxedReceiver = if let Some(conditioner_config) = self.conditioner { + let conditioner = LinkConditioner::new(conditioner_config); + Box::new(conditioner.wrap(receiver)) + } else { + Box::new(receiver) + }; + match self.compression { + CompressionConfig::None => {} + #[cfg(feature = "zstd")] + CompressionConfig::Zstd { level } => { + let compressor = ZstdCompressor::new(level); + sender = Box::new(compressor.wrap(sender)); + let decompressor = ZstdDecompressor::new(); + receiver = Box::new(decompressor.wrap(receiver)); + } + } + Ok(BaseIo { + local_addr, + sender, + receiver, + state, + stats: IoStats::default(), + context: IoContext { + event_sender: network_tx, + event_receiver: io_rx, + }, + }) + } +} diff --git a/lightyear/src/server/io/mod.rs b/lightyear/src/server/io/mod.rs new file mode 100644 index 000000000..ec519a5c9 --- /dev/null +++ b/lightyear/src/server/io/mod.rs @@ -0,0 +1,47 @@ +//! Wrapper around a transport, that can perform additional transformations such as +//! bandwidth monitoring or compression +pub(crate) mod config; +pub(crate) mod transport; + +use crate::transport::error::{Error, Result}; +use crate::transport::io::{BaseIo, IoState}; +use async_channel::Receiver; +use bevy::prelude::{Deref, DerefMut}; +use crossbeam_channel::Sender; +use std::net::SocketAddr; + +pub struct IoContext { + pub(crate) event_sender: Option, + pub(crate) event_receiver: Option, +} + +/// Server IO +pub type Io = BaseIo; + +impl Io { + pub fn close(&mut self) -> Result<()> { + self.state = IoState::Disconnected; + if let Some(event_sender) = self.context.event_sender.as_mut() { + event_sender + .send_blocking(ServerIoEvent::ServerDisconnected( + std::io::Error::other("server requested disconnection").into(), + )) + .map_err(Error::from)?; + } + Ok(()) + } +} + +#[derive(Deref, DerefMut, Clone)] +pub(crate) struct ServerIoEventReceiver(pub(crate) async_channel::Receiver); + +/// Events that will be sent from the io thread to the main thread +pub(crate) enum ServerIoEvent { + ServerConnected, + ServerDisconnected(Error), + ClientDisconnected(SocketAddr), +} + +/// Events that will be sent from the main thread to the io thread +#[derive(Deref, DerefMut, Clone)] +pub(crate) struct ServerNetworkEventSender(pub(crate) async_channel::Sender); diff --git a/lightyear/src/server/io/transport.rs b/lightyear/src/server/io/transport.rs new file mode 100644 index 000000000..0417672c7 --- /dev/null +++ b/lightyear/src/server/io/transport.rs @@ -0,0 +1,53 @@ +use crate::server::io::{ServerIoEventReceiver, ServerNetworkEventSender}; +use crate::transport::channels::Channels; +use crate::transport::dummy::DummyIo; +use crate::transport::error::Result; +use crate::transport::io::IoState; +#[cfg(not(target_family = "wasm"))] +use crate::transport::udp::{UdpSocket, UdpSocketBuilder}; +#[cfg(all(feature = "websocket", not(target_family = "wasm")))] +use crate::transport::websocket::server::{WebSocketServerSocket, WebSocketServerSocketBuilder}; +#[cfg(all(feature = "webtransport", not(target_family = "wasm")))] +use crate::transport::webtransport::server::{ + WebTransportServerSocket, WebTransportServerSocketBuilder, +}; +use crate::transport::Transport; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub(crate) trait ServerTransportBuilder: Send + Sync { + /// Attempt to listen for incoming connections + fn start( + self, + ) -> Result<( + ServerTransportEnum, + IoState, + Option, + Option, + )>; +} + +#[enum_dispatch(ServerTransportBuilder)] +pub(crate) enum ServerTransportBuilderEnum { + #[cfg(not(target_family = "wasm"))] + UdpSocket(UdpSocketBuilder), + #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] + WebTransportServer(WebTransportServerSocketBuilder), + #[cfg(all(feature = "websocket", not(target_family = "wasm")))] + WebSocketServer(WebSocketServerSocketBuilder), + Channels(Channels), + Dummy(DummyIo), +} + +#[allow(clippy::large_enum_variant)] +#[enum_dispatch(Transport)] +pub(crate) enum ServerTransportEnum { + #[cfg(not(target_family = "wasm"))] + UdpSocket(UdpSocket), + #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] + WebTransportServer(WebTransportServerSocket), + #[cfg(all(feature = "websocket", not(target_family = "wasm")))] + WebSocketServer(WebSocketServerSocket), + Channels(Channels), + Dummy(DummyIo), +} diff --git a/lightyear/src/server/message.rs b/lightyear/src/server/message.rs index 7d8bf88dc..098d6a138 100644 --- a/lightyear/src/server/message.rs +++ b/lightyear/src/server/message.rs @@ -11,7 +11,7 @@ use bitcode::__private::Fixed; use bitcode::{Decode, Encode}; use crate::packet::message::SingleData; -use crate::prelude::{MainSet, Message, NetworkTarget}; +use crate::prelude::{MainSet, Message}; use crate::protocol::message::{MessageKind, MessageRegistry}; use crate::protocol::registry::NetId; use crate::protocol::BitSerializable; @@ -22,6 +22,7 @@ use crate::server::connection::ConnectionManager; use crate::server::events::MessageEvent; use crate::server::networking::is_started; use crate::shared::ping::message::{Ping, Pong, SyncMessage}; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::replication::{ReplicationMessage, ReplicationMessageData}; use crate::shared::sets::{InternalMainSet, ServerMarker}; diff --git a/lightyear/src/server/mod.rs b/lightyear/src/server/mod.rs index 66d98e4dc..8423de1ce 100644 --- a/lightyear/src/server/mod.rs +++ b/lightyear/src/server/mod.rs @@ -11,9 +11,9 @@ pub mod events; pub mod input; -pub mod plugin; +pub(crate) mod io; -pub mod room; +pub mod plugin; #[cfg_attr(docsrs, doc(cfg(feature = "leafwing")))] #[cfg(feature = "leafwing")] @@ -21,5 +21,7 @@ pub mod input_leafwing; pub(crate) mod message; pub(crate) mod prediction; +pub(crate) mod clients; pub(crate) mod networking; pub mod replication; +pub mod visibility; diff --git a/lightyear/src/server/networking.rs b/lightyear/src/server/networking.rs index f65104976..d8e0a0916 100644 --- a/lightyear/src/server/networking.rs +++ b/lightyear/src/server/networking.rs @@ -1,5 +1,6 @@ //! Defines the server bevy systems and run conditions use anyhow::{anyhow, Context}; +use async_channel::TryRecvError; use bevy::ecs::system::{RunSystemOnce, SystemChangeTick, SystemParam}; use bevy::prelude::*; use tracing::{debug, error, trace, trace_span}; @@ -7,13 +8,17 @@ use tracing::{debug, error, trace, trace_span}; use crate::client::config::ClientConfig; use crate::client::networking::is_disconnected; use crate::connection::client::{ClientConnection, NetClient}; -use crate::connection::server::{NetConfig, NetServer, ServerConnection, ServerConnections}; +use crate::connection::server::{ + IoConfig, NetConfig, NetServer, ServerConnection, ServerConnections, +}; use crate::prelude::{ChannelRegistry, MainSet, MessageRegistry, Mode, TickManager, TimeManager}; use crate::protocol::component::ComponentRegistry; +use crate::server::clients::ControlledEntities; use crate::server::config::ServerConfig; use crate::server::connection::ConnectionManager; use crate::server::events::{ConnectEvent, DisconnectEvent, EntityDespawnEvent, EntitySpawnEvent}; -use crate::server::room::RoomManager; +use crate::server::io::ServerIoEvent; +use crate::server::visibility::room::RoomManager; use crate::shared::events::connection::{IterEntityDespawnEvent, IterEntitySpawnEvent}; use crate::shared::replication::ReplicationSend; use crate::shared::sets::{InternalMainSet, ServerMarker}; @@ -32,6 +37,8 @@ pub(crate) struct ServerNetworkingPlugin; impl Plugin for ServerNetworkingPlugin { fn build(&self, app: &mut App) { app + // REFLECTION + .register_type::() // STATE .init_state::() // SYSTEM SETS @@ -88,8 +95,6 @@ pub(crate) fn receive(world: &mut World) { |world: &mut World, mut time_manager: Mut| { world.resource_scope( |world: &mut World, tick_manager: Mut| { - world.resource_scope( - |world: &mut World, mut room_manager: Mut| { let delta = world.resource::>().delta(); // UPDATE: update server state, send keep-alives, receive packets from io // update time manager @@ -100,18 +105,48 @@ pub(crate) fn receive(world: &mut World) { // reborrow trick to enable split borrows let netservers = &mut *netservers; for (server_idx, netserver) in netservers.servers.iter_mut().enumerate() { + // TODO: maybe run this before receive, like for clients? + // if the io task for any connection failed, disconnect the client in netcode + let mut to_disconnect = vec![]; + if let Some(io) = netserver.io_mut() { + if let Some(receiver) = &mut io.context.event_receiver { + match receiver.try_recv() { + Ok(event) => { + match event { + ServerIoEvent::ClientDisconnected(client_id) => { + error!("Disconnect client {client_id:?} because io task failed"); + to_disconnect.push(client_id); + } + ServerIoEvent::ServerDisconnected(e) => { + error!("Disconnect server because of io error: {:?}", e); + world.resource_mut::>().set(NetworkingState::Stopped); + } + _ => {} + } + } + Err(TryRecvError::Empty) => {} + Err(TryRecvError::Closed) => {} + } + } + } + let _ = netserver .try_update(delta.as_secs_f64()) .map_err(|e| error!("Error updating netcode server: {:?}", e)); for client_id in netserver.new_connections().iter().copied() { netservers.client_server_map.insert(client_id, server_idx); - connection_manager.add(client_id); + // spawn an entity for the client + let client_entity = world.spawn(ControlledEntities::default()).id(); + connection_manager.add(client_id, client_entity); } // handle disconnections for client_id in netserver.new_disconnections().iter().copied() { if netservers.client_server_map.remove(&client_id).is_some() { connection_manager.remove(client_id); - room_manager.client_disconnect(client_id); + // NOTE: we don't despawn the entity right away to let the user react to + // the disconnect event + // TODO: use observers/component_hooks to react automatically on the client despawn? + // world.despawn(client_entity); } else { error!("Client disconnected but could not map client_id to the corresponding netserver"); } @@ -159,22 +194,21 @@ pub(crate) fn receive(world: &mut World) { if connection_manager.events.has_connections() { let mut connect_event_writer = world.get_resource_mut::>().unwrap(); - for client_id in connection_manager.events.iter_connections() { - debug!("Client connected event: {}", client_id); - connect_event_writer.send(ConnectEvent::new(client_id)); + for connect_event in connection_manager.events.iter_connections() { + debug!("Client connected event: {}", connect_event.client_id); + connect_event_writer.send(connect_event); } } if connection_manager.events.has_disconnections() { let mut connect_event_writer = world.get_resource_mut::>().unwrap(); - for client_id in connection_manager.events.iter_disconnections() { - debug!("Client disconnected event: {}", client_id); - connect_event_writer.send(DisconnectEvent::new(client_id)); + for disconnect_event in connection_manager.events.iter_disconnections() { + debug!("Client disconnected event: {}", disconnect_event.client_id); + connect_event_writer.send(disconnect_event); } } } - }); }); }); }); @@ -266,7 +300,6 @@ fn rebuild_server_connections(world: &mut World) { // insert a new connection manager (to reset message numbers, ping manager, etc.) let connection_manager = ConnectionManager::new( - world.resource::().clone(), world.resource::().clone(), world.resource::().clone(), server_config.packet, diff --git a/lightyear/src/server/plugin.rs b/lightyear/src/server/plugin.rs index 8fcc62b01..bf8969cb0 100644 --- a/lightyear/src/server/plugin.rs +++ b/lightyear/src/server/plugin.rs @@ -1,38 +1,85 @@ -//! Defines the server bevy plugin +//! Defines the Server PluginGroup +//! +//! The Server consists of multiple different plugins, each with their own responsibilities. These plugins +//! are grouped into the [`ServerPlugins`] plugin group, which allows you to easily configure and disable +//! any of the existing plugins. +//! +//! This means that users can simply disable existing functionality and replace it with specialized solutions, +//! while keeping the rest of the features intact. +//! +//! Most plugins are truly necessary for the server functionality to work properly, but some could be disabled. +use crate::server::clients::ClientsMetadataPlugin; +use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use crate::server::events::ServerEventsPlugin; use crate::server::networking::ServerNetworkingPlugin; -use crate::server::replication::ServerReplicationPlugin; -use crate::server::room::RoomPlugin; +use crate::server::replication::{ + receive::ServerReplicationReceivePlugin, send::ServerReplicationSendPlugin, +}; +use crate::server::visibility::immediate::VisibilityPlugin; +use crate::server::visibility::room::RoomPlugin; use crate::shared::plugin::SharedPlugin; use super::config::ServerConfig; -pub struct ServerPlugin { - config: ServerConfig, +/// A plugin group containing all the server plugins. +/// +/// By default, the following plugins will be added: +/// - [`SetupPlugin`]: Adds the [`ServerConfig`] resource and the [`SharedPlugin`] plugin. +/// - [`ServerEventsPlugin`]: Adds the server network event +/// - [`ServerNetworkingPlugin`]: Handles the network state (starting/stopping the server, sending/receiving packets) +/// - [`VisibilityPlugin`]: Handles the visibility system. This can be disabled if you don't need fine-grained interest management. +/// - [`RoomPlugin`]: Handles the room system, which is an addition to the visibility system. This can be disabled if you don't need rooms. +/// - [`ServerReplicationReceivePlugin`]: Handles the replication of entities and resources from clients to the server. This can be +/// disabled if you don't need client to server replication. +/// - [`ServerReplicationSendPlugin`]: Handles the replication of entities and resources from the server to the client. This can be +/// disabled if you don't need server to client replication. +pub struct ServerPlugins { + pub config: ServerConfig, } -impl ServerPlugin { +impl ServerPlugins { pub fn new(config: ServerConfig) -> Self { Self { config } } } -impl Plugin for ServerPlugin { +impl PluginGroup for ServerPlugins { + fn build(self) -> PluginGroupBuilder { + let builder = PluginGroupBuilder::start::(); + let tick_interval = self.config.shared.tick.tick_duration; + builder + .add(SetupPlugin { + config: self.config, + }) + .add(ServerEventsPlugin) + .add(ServerNetworkingPlugin) + .add(VisibilityPlugin) + .add(RoomPlugin) + .add(ClientsMetadataPlugin) + .add(ServerReplicationReceivePlugin { tick_interval }) + .add(ServerReplicationSendPlugin { tick_interval }) + } +} + +/// A plugin that sets up the server by adding the [`ServerConfig`] resource and the [`SharedPlugin`] plugin. +struct SetupPlugin { + config: ServerConfig, +} + +impl Plugin for SetupPlugin { fn build(&self, app: &mut App) { app // RESOURCES // - .insert_resource(self.config.clone()) - // PLUGINS - // NOTE: SharedPlugin needs to be added after config - .add_plugins(SharedPlugin { + .insert_resource(self.config.clone()); + // PLUGINS + // NOTE: SharedPlugin needs to be added after config + if !app.is_plugin_added::() { + app.add_plugins(SharedPlugin { // TODO: move shared config out of server_config? config: self.config.shared.clone(), - }) - .add_plugins(ServerEventsPlugin) - .add_plugins(ServerNetworkingPlugin) - .add_plugins(RoomPlugin) - .add_plugins(ServerReplicationPlugin); + }); + } } } diff --git a/lightyear/src/server/replication.rs b/lightyear/src/server/replication.rs index 2a42032bc..d640b1825 100644 --- a/lightyear/src/server/replication.rs +++ b/lightyear/src/server/replication.rs @@ -1,5 +1,6 @@ use bevy::ecs::query::QueryFilter; use bevy::prelude::*; +use bevy::utils::Duration; use crate::client::components::Confirmed; use crate::client::interpolation::Interpolated; @@ -11,123 +12,283 @@ use crate::server::config::ServerConfig; use crate::server::connection::ConnectionManager; use crate::server::networking::is_started; use crate::server::prediction::compute_hash; -use crate::shared::replication::components::Replicate; -use crate::shared::replication::plugin::ReplicationPlugin; +use crate::shared::replication::plugin::receive::ReplicationReceivePlugin; +use crate::shared::replication::plugin::send::ReplicationSendPlugin; use crate::shared::sets::{InternalMainSet, InternalReplicationSet, ServerMarker}; -/// Configuration related to replicating the server's World to clients -#[derive(Clone, Debug)] -pub struct ReplicationConfig { - /// Set to true to disable replicating this server's entities to clients - pub enable_send: bool, - pub enable_receive: bool, +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum ServerReplicationSet { + // You can use this SystemSet to add Replicate components to entities received from clients (to rebroadcast them to other clients) + ClientReplication, } -impl Default for ReplicationConfig { - fn default() -> Self { - Self { - enable_send: true, - enable_receive: false, +pub(crate) mod receive { + use super::*; + + #[derive(Default)] + pub struct ServerReplicationReceivePlugin { + pub tick_interval: Duration, + } + + impl Plugin for ServerReplicationReceivePlugin { + fn build(&self, app: &mut App) { + app + // PLUGIN + .add_plugins(ReplicationReceivePlugin::::new( + self.tick_interval, + )) + // SETS + .configure_sets( + PreUpdate, + ServerReplicationSet::ClientReplication + .run_if(is_started) + .after(InternalMainSet::::EmitEvents), + ); } } } -#[derive(Default)] -pub struct ServerReplicationPlugin; +pub(crate) mod send { + use super::*; + use crate::prelude::{ + ComponentRegistry, ReplicationGroup, ShouldBePredicted, TargetEntity, VisibilityMode, + }; + use crate::server::visibility::immediate::{ClientVisibility, ReplicateVisibility}; + use crate::shared::replication::components::{ + Controlled, ControlledBy, ReplicationTarget, ShouldBeInterpolated, + }; + use crate::shared::replication::network_target::NetworkTarget; + use crate::shared::replication::ReplicationSend; + use bevy::ecs::system::SystemChangeTick; -#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub enum ServerReplicationSet { - /// You can use this SystemSet to add Replicate components to entities received from clients (to rebroadcast them to other clients) - ClientReplication, -} + #[derive(Default)] + pub struct ServerReplicationSendPlugin { + pub tick_interval: Duration, + } -impl Plugin for ServerReplicationPlugin { - fn build(&self, app: &mut App) { - let config = app.world.resource::(); - - app - // PLUGIN - .add_plugins(ReplicationPlugin::::new( - config.shared.tick.tick_duration, - config.replication.enable_send, - config.replication.enable_receive, - )) - // SYSTEM SETS - .configure_sets( - PreUpdate, - ServerReplicationSet::ClientReplication - .run_if(is_started) - .after(InternalMainSet::::EmitEvents), - ) - .configure_sets( - PostUpdate, - // on server: we need to set the hash value before replicating the component - InternalReplicationSet::::SetPreSpawnedHash - .before(InternalReplicationSet::::BufferComponentUpdates) - .in_set(InternalReplicationSet::::All), - ) - .configure_sets( + impl Plugin for ServerReplicationSendPlugin { + fn build(&self, app: &mut App) { + let config = app.world.resource::(); + + app + // PLUGIN + .add_plugins(ReplicationSendPlugin::::new( + self.tick_interval, + )) + // SYSTEM SETS + .configure_sets( + PostUpdate, + // on server: we need to set the hash value before replicating the component + InternalReplicationSet::::SetPreSpawnedHash + .before(InternalReplicationSet::::BufferComponentUpdates) + .in_set(InternalReplicationSet::::All), + ) + .configure_sets( + PostUpdate, + InternalReplicationSet::::All.run_if(is_started), + ) + // SYSTEMS + .add_systems( + PostUpdate, + compute_hash.in_set(InternalReplicationSet::::SetPreSpawnedHash), + ); + + // HOST-SERVER + app.add_systems( PostUpdate, - InternalReplicationSet::::All.run_if(is_started), - ) - // SYSTEMS - .add_systems( + // TODO: putting it here means we might miss entities that are spawned and despawned within the send_interval? bug or feature? + // be careful that newly_connected_client is cleared every send_interval, not every frame. + send_entity_spawn + .in_set(InternalReplicationSet::::BufferEntityUpdates), + ); + app.add_systems( PostUpdate, - compute_hash.in_set(InternalReplicationSet::::SetPreSpawnedHash), + add_prediction_interpolation_components + .after(InternalMainSet::::Send) + .run_if(SharedConfig::is_host_server_condition), ); - - // HOST-SERVER - app.add_systems( - PostUpdate, - add_prediction_interpolation_components - .after(InternalMainSet::::Send) - .run_if(SharedConfig::is_host_server_condition), - ); + } } -} -/// Filter to use to get all entities that are not client-side replicated entities -#[derive(QueryFilter)] -pub struct ServerFilter { - a: ( - Without, - Without, - Without, - ), -} + /// Filter to use to get all entities that are not client-side replicated entities + #[derive(QueryFilter)] + pub struct ServerFilter { + a: ( + Without, + Without, + Without, + ), + } -/// In HostServer mode, we will add the Predicted/Interpolated components to the server entities -/// So that client code can still query for them -fn add_prediction_interpolation_components( - mut commands: Commands, - query: Query<(Entity, Ref, Option<&PrePredicted>)>, - connection: Res, -) { - let local_client = connection.id(); - for (entity, replicate, pre_predicted) in query.iter() { - if (replicate.is_added() || replicate.is_changed()) - && replicate.replication_target.should_send_to(&local_client) - { - if pre_predicted.is_some_and(|pre_predicted| pre_predicted.client_entity.is_none()) { - // PrePredicted's client_entity is None if it's a pre-predicted entity that was spawned by the local client - // in that case, just remove it and add Predicted instead - commands - .entity(entity) - .insert(Predicted { + /// In HostServer mode, we will add the Predicted/Interpolated components to the server entities + /// So that client code can still query for them + fn add_prediction_interpolation_components( + mut commands: Commands, + query: Query<(Entity, Ref, Option<&PrePredicted>)>, + connection: Res, + ) { + let local_client = connection.id(); + for (entity, replication_target, pre_predicted) in query.iter() { + if (replication_target.is_changed()) + && replication_target.replication.targets(&local_client) + { + if pre_predicted.is_some_and(|pre_predicted| pre_predicted.client_entity.is_none()) + { + // PrePredicted's client_entity is None if it's a pre-predicted entity that was spawned by the local client + // in that case, just remove it and add Predicted instead + commands + .entity(entity) + .insert(Predicted { + confirmed_entity: Some(entity), + }) + .remove::(); + } + if replication_target.prediction.targets(&local_client) { + commands.entity(entity).insert(Predicted { confirmed_entity: Some(entity), - }) - .remove::(); - } - if replicate.prediction_target.should_send_to(&local_client) { - commands.entity(entity).insert(Predicted { - confirmed_entity: Some(entity), - }); - } - if replicate.interpolation_target.should_send_to(&local_client) { - commands.entity(entity).insert(Interpolated { - confirmed_entity: entity, - }); + }); + } + if replication_target.interpolation.targets(&local_client) { + commands.entity(entity).insert(Interpolated { + confirmed_entity: entity, + }); + } } } } + + /// Send entity spawn replication messages to clients + /// Also handles: + /// - newly_connected_clients should receive the entity spawn message even if the entity was not just spawned + /// - adds ControlledBy, ShouldBePredicted, ShouldBeInterpolated component + /// - handles TargetEntity if it's a Preexisting entity + pub(crate) fn send_entity_spawn( + component_registry: Res, + query: Query<( + Entity, + Ref, + &ReplicationGroup, + &ControlledBy, + Option<&TargetEntity>, + Option<&ReplicateVisibility>, + )>, + mut sender: ResMut, + ) { + // Replicate to already connected clients (replicate only new entities) + query.iter().for_each(|(entity, replication_target, group, controlled_by, target_entity, visibility )| { + let target = match visibility { + // for room mode, no need to handle newly-connected clients specially; they just need + // to be added to the correct room + Some(visibility) => { + visibility.clients_cache + .iter() + .filter_map(|(client_id, visibility)| { + if replication_target.replication.targets(client_id) { + match visibility { + ClientVisibility::Gained => { + trace!( + ?entity, + ?client_id, + "send entity spawn to client who just gained visibility" + ); + return Some(*client_id); + } + ClientVisibility::Lost => {} + ClientVisibility::Maintained => { + // only try to replicate if the replicate component was just added + if replication_target.is_added() { + trace!( + ?entity, + ?client_id, + "send entity spawn to client who maintained visibility" + ); + return Some(*client_id); + } + } + } + } + None + }).collect() + } + None => { + let mut target = NetworkTarget::None; + // only try to replicate if the replicate component was just added + if replication_target.is_added() { + trace!(?entity, "send entity spawn"); + target = replication_target.replication.clone(); + } else if replication_target.is_changed() { + target = replication_target.replication.clone(); + if let Some(cached_replicate) = sender.replicate_component_cache.get(&entity) { + // do not re-send a spawn message to the clients for which we already have + // replicated the entity + target.exclude(&cached_replicate.replication_target) + } + } + + // also replicate to the newly connected clients that match the target + let new_connected_clients = sender.new_connected_clients(); + if !new_connected_clients.is_empty() { + // replicate to the newly connected clients that match our target + let mut new_connected_target = NetworkTarget::Only(new_connected_clients); + new_connected_target.intersection(&replication_target.replication); + debug!(?entity, target = ?new_connected_target, "Replicate to newly connected clients"); + target.union(&new_connected_target); + } + target + } + }; + if target.is_empty() { + return; + } + + trace!(?entity, "Prepare entity spawn to client"); + let group_id = group.group_id(Some(entity)); + // TODO: should we have additional state tracking so that we know we are in the process of sending this entity to clients? + // (i.e. before we received an ack?) + let _ = sender.apply_replication(target).try_for_each(|client_id| { + // let the client know that this entity is controlled by them + if controlled_by.targets(&client_id) { + sender.prepare_typed_component_insert(entity, group_id, client_id, component_registry.as_ref(), &Controlled)?; + } + // if we need to do prediction/interpolation, send a marker component to indicate that to the client + if replication_target.prediction.targets(&client_id) { + // TODO: the serialized data is always the same; cache it somehow? + sender.prepare_typed_component_insert( + entity, + group_id, + client_id, + component_registry.as_ref(), + &ShouldBePredicted, + )?; + } + if replication_target.interpolation.targets(&client_id) { + sender.prepare_typed_component_insert( + entity, + group_id, + client_id, + component_registry.as_ref(), + &ShouldBeInterpolated, + )?; + } + + if let Some(TargetEntity::Preexisting(remote_entity)) = target_entity { + sender.connection_mut(client_id)?.replication_sender.prepare_entity_spawn_reuse( + entity, + group_id, + *remote_entity, + ); + } else { + sender.connection_mut(client_id)?.replication_sender + .prepare_entity_spawn(entity, group_id); + } + + // also set the priority for the group when we spawn it + sender.connection_mut(client_id)?.replication_sender.update_base_priority(group_id, group.priority()); + Ok(()) + }).inspect_err(|e: &anyhow::Error| { + error!("error sending entity spawn: {:?}", e); + }); + + }); + } } diff --git a/lightyear/src/server/visibility/immediate.rs b/lightyear/src/server/visibility/immediate.rs new file mode 100644 index 000000000..e05fe1f30 --- /dev/null +++ b/lightyear/src/server/visibility/immediate.rs @@ -0,0 +1,431 @@ +/*! Main visibility module, where you can immediately update the visibility of an entity for a given client + +# Visibility + +This module provides a [`VisibilityManager`] resource that allows you to update the visibility of entities in an immediate fashion. + +Visibilities are cached, so after you set an entity to `visible` for a client, it will remain visible +until you change the setting again. + +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::server::*; + +fn my_system( + mut visibility_manager: ResMut, +) { + // you can update the visibility like so + visibility_manager.gain_visibility(ClientId::Netcode(1), Entity::PLACEHOLDER); + visibility_manager.lose_visibility(ClientId::Netcode(2), Entity::PLACEHOLDER); +} +``` +*/ +use crate::prelude::server::ConnectionManager; +use crate::prelude::ClientId; +use crate::server::networking::is_started; +use crate::server::visibility::room::{RoomManager, RoomSystemSets}; +use crate::shared::sets::{InternalMainSet, InternalReplicationSet, ServerMarker}; +use bevy::ecs::entity::EntityHashSet; +use bevy::prelude::*; +use bevy::utils::HashMap; +use tracing::trace; + +/// Event related to [`Entities`](Entity) which are visible to a client +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +pub(crate) enum ClientVisibility { + /// the entity was not replicated to the client, but now is + Gained, + /// the entity was replicated to the client, but not anymore + Lost, + /// the entity was already replicated to the client, and still is + Maintained, +} + +#[derive(Component, Clone, Default, PartialEq, Debug, Reflect)] +pub(crate) struct ReplicateVisibility { + /// List of clients that the entity is currently replicated to. + /// Will be updated before the other replication systems + pub(crate) clients_cache: HashMap, +} + +#[derive(Debug, Default)] +struct VisibilityEvents { + gained: HashMap, + lost: HashMap, +} + +/// Resource that manages the visibility of entities for clients +/// +/// You can call the two functions +/// - [`gain_visibility`](VisibilityManager::gain_visibility) +/// - [`lose_visibility`](VisibilityManager::lose_visibility) +/// +/// to update the visibility of an entity for a given client. +#[derive(Resource, Debug, Default)] +pub struct VisibilityManager { + events: VisibilityEvents, +} + +impl VisibilityManager { + /// Gain visibility of an entity for a given client. + /// + /// The visibility status gets cached and will be maintained until is it changed. + pub fn gain_visibility(&mut self, client: ClientId, entity: Entity) -> &mut Self { + self.events.lost.entry(client).and_modify(|set| { + set.remove(&entity); + }); + self.events.gained.entry(client).or_default().insert(entity); + self + } + + /// Lost visibility of an entity for a given client + pub fn lose_visibility(&mut self, client: ClientId, entity: Entity) -> &mut Self { + self.events.gained.entry(client).and_modify(|set| { + set.remove(&entity); + }); + self.events.lost.entry(client).or_default().insert(entity); + self + } + + // NOTE: this might not be needed because we drain the event cache every Send update + // /// Remove all visibility events for a given client when they disconnect + // /// + // /// Called to release the memory associated with the client + // pub(crate) fn handle_client_disconnection(&mut self, client: ClientId) { + // self.events.gained.remove(&client); + // self.events.lost.remove(&client); + // } +} + +pub(super) mod systems { + use super::*; + use crate::prelude::server::DisconnectEvent; + use crate::prelude::VisibilityMode; + use crate::shared::replication::ReplicationSend; + use bevy::prelude::DetectChanges; + + // NOTE: this might not be needed because we drain the event cache every Send update + // /// Clear the internal room buffers when a client disconnects + // pub fn handle_client_disconnect( + // mut manager: ResMut, + // mut disconnect_events: EventReader, + // ) { + // for event in disconnect_events.read() { + // let client_id = event.context(); + // manager.handle_client_disconnection(*client_id); + // } + // } + + /// If VisibilityMode becomes InterestManagement, add ReplicateVisibility to the entity + /// If VisibilityMode becomes All, remove ReplicateVisibility from the entity + /// + /// Run this before the visibility systems and the replication buffer systems + /// so that the visibility cache can be updated before the replication systems + pub(in crate::server::visibility) fn add_replicate_visibility( + mut commands: Commands, + query: Query<(Entity, Ref, Option<&ReplicateVisibility>)>, + ) { + for (entity, visibility_mode, replicate_visibility) in query.iter() { + if visibility_mode.is_changed() { + match visibility_mode.as_ref() { + VisibilityMode::InterestManagement => { + // do not overwrite the visibility if it already exists + if replicate_visibility.is_none() { + debug!("Adding ReplicateVisibility component for entity {entity:?}"); + commands + .entity(entity) + .insert(ReplicateVisibility::default()); + } + } + VisibilityMode::All => { + commands.entity(entity).remove::(); + } + } + } + } + } + + /// System that updates the visibility cache of each Entity based on the visibility events. + pub fn update_visibility_from_events( + mut manager: ResMut, + mut visibility: Query<&mut ReplicateVisibility>, + ) { + if manager.events.gained.is_empty() && manager.events.lost.is_empty() { + return; + } + trace!("Visibility events: {:?}", manager.events); + for (client, mut entities) in manager.events.lost.drain() { + entities.drain().for_each(|entity| { + if let Ok(mut cache) = visibility.get_mut(entity) { + // Only lose visibility if the client was visible to the entity + // (to avoid multiple despawn messages) + if let Some(vis) = cache.clients_cache.get_mut(&client) { + trace!("lose visibility for entity {entity:?} and client {client:?}"); + *vis = ClientVisibility::Lost; + } + } + }); + } + for (client, mut entities) in manager.events.gained.drain() { + entities.drain().for_each(|entity| { + if let Ok(mut cache) = visibility.get_mut(entity) { + // if the entity was already visible (Visibility::Maintained), be careful to not set it to + // Visibility::Gained as it would trigger a spawn replication action + // + // we don't need to check if the entity was set to Lost in the same update, + // since calling gain_visibility removes the entity from the lost_visibility queue + cache + .clients_cache + .entry(client) + .or_insert(ClientVisibility::Gained); + } + }); + } + } + + /// After replication, update the Replication Cache: + /// - Visibility Gained becomes Visibility Maintained + /// - Visibility Lost gets removed from the cache + pub fn update_replicate_visibility(mut query: Query<(Entity, &mut ReplicateVisibility)>) { + for (entity, mut replicate) in query.iter_mut() { + replicate + .clients_cache + .retain(|client_id, visibility| match visibility { + ClientVisibility::Gained => { + trace!( + "Visibility for client {client_id:?} and entity {entity:?} goes from gained to maintained" + ); + *visibility = ClientVisibility::Maintained; + true + } + ClientVisibility::Lost => { + trace!("remove client {client_id:?} and entity {entity:?} from visibility cache"); + false + } + ClientVisibility::Maintained => true, + }); + // error!("replicate.clients_cache: {0:?}", replicate.clients_cache); + } + } + + /// Whenever the visibility of an entity changes, update the replication metadata cache + /// so that we can correctly replicate the despawn to the correct clients + pub(super) fn update_replication_cache( + mut sender: ResMut, + mut query: Query<(Entity, Ref, Option<&ReplicateVisibility>)>, + ) { + for (entity, visibility_mode, replicate_visibility) in query.iter_mut() { + match visibility_mode.as_ref() { + VisibilityMode::InterestManagement => { + if visibility_mode.is_changed() { + if let Some(cache) = sender.get_mut_replicate_cache().get_mut(&entity) { + cache.visibility_mode = VisibilityMode::InterestManagement; + if let Some(replicate_visibility) = replicate_visibility { + cache.replication_clients_cache = replicate_visibility + .clients_cache + .iter() + .filter_map(|(client, visibility)| { + if *visibility != ClientVisibility::Lost { + Some(*client) + } else { + None + } + }) + .collect(); + } else { + cache.replication_clients_cache.clear(); + } + } + } + } + VisibilityMode::All => { + if visibility_mode.is_changed() && !visibility_mode.is_added() { + if let Some(cache) = sender.get_mut_replicate_cache().get_mut(&entity) { + cache.visibility_mode = VisibilityMode::All; + cache.replication_clients_cache.clear(); + } + } + } + } + } + } +} + +/// System sets related to Rooms +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum VisibilitySet { + /// Update the visibility cache based on the visibility events + UpdateVisibility, + /// Perform bookkeeping for the visibility caches + VisibilityCleanup, +} + +/// Plugin that handles the visibility system +#[derive(Default)] +pub(crate) struct VisibilityPlugin; + +impl Plugin for VisibilityPlugin { + fn build(&self, app: &mut App) { + // RESOURCES + app.init_resource::(); + // SETS + app.configure_sets( + PostUpdate, + ( + ( + // update replication caches must happen before replication, but after we add ReplicateVisibility + InternalReplicationSet::::BeforeBuffer, + VisibilitySet::UpdateVisibility, + InternalReplicationSet::::Buffer, + VisibilitySet::VisibilityCleanup, + ) + .run_if(is_started) + .chain(), + // the room systems can run every send_interval + ( + VisibilitySet::UpdateVisibility, + VisibilitySet::VisibilityCleanup, + ) + .in_set(InternalMainSet::::Send), + ), + ); + // SYSTEMS + // NOTE: this might not be needed because we drain the event cache every Send update + // app.add_systems( + // PreUpdate, + // systems::handle_client_disconnect.after(InternalMainSet::::EmitEvents), + // ); + app.add_systems( + PostUpdate, + ( + systems::add_replicate_visibility + .in_set(InternalReplicationSet::::BeforeBuffer), + systems::update_visibility_from_events.in_set(VisibilitySet::UpdateVisibility), + systems::update_replication_cache + .in_set(InternalReplicationSet::::AfterBuffer), + systems::update_replicate_visibility.in_set(VisibilitySet::VisibilityCleanup), + ), + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy::ecs::system::RunSystemOnce; + + /// Multiple entities gain visibility for a given client + #[test] + fn test_multiple_visibility_gain() { + let mut app = App::new(); + app.world.init_resource::(); + let entity1 = app.world.spawn(ReplicateVisibility::default()).id(); + let entity2 = app.world.spawn(ReplicateVisibility::default()).id(); + let client = ClientId::Netcode(1); + + app.world + .resource_mut::() + .gain_visibility(client, entity1); + app.world + .resource_mut::() + .gain_visibility(client, entity2); + + assert_eq!( + app.world + .resource_mut::() + .events + .gained + .len(), + 1 + ); + assert_eq!( + app.world + .resource_mut::() + .events + .gained + .get(&client) + .unwrap() + .len(), + 2 + ); + app.world + .run_system_once(systems::update_visibility_from_events); + assert_eq!( + app.world + .resource_mut::() + .events + .gained + .len(), + 0 + ); + assert_eq!( + app.world + .entity(entity1) + .get::() + .unwrap() + .clients_cache + .get(&client) + .unwrap(), + &ClientVisibility::Gained + ); + assert_eq!( + app.world + .entity(entity2) + .get::() + .unwrap() + .clients_cache + .get(&client) + .unwrap(), + &ClientVisibility::Gained + ); + + // After we used the visibility events, check how they are updated for bookkeeping + // - Lost -> removed from cache + // - Gained -> Maintained + app.world + .resource_mut::() + .lose_visibility(client, entity1); + app.world + .run_system_once(systems::update_visibility_from_events); + assert_eq!( + app.world + .entity(entity1) + .get::() + .unwrap() + .clients_cache + .get(&client) + .unwrap(), + &ClientVisibility::Lost + ); + assert_eq!( + app.world + .entity(entity2) + .get::() + .unwrap() + .clients_cache + .get(&client) + .unwrap(), + &ClientVisibility::Gained + ); + app.world + .run_system_once(systems::update_replicate_visibility); + assert!(app + .world + .entity(entity1) + .get::() + .unwrap() + .clients_cache + .is_empty()); + assert_eq!( + app.world + .entity(entity2) + .get::() + .unwrap() + .clients_cache + .get(&client) + .unwrap(), + &ClientVisibility::Maintained + ); + } +} diff --git a/lightyear/src/server/visibility/mod.rs b/lightyear/src/server/visibility/mod.rs new file mode 100644 index 000000000..13ea05a4c --- /dev/null +++ b/lightyear/src/server/visibility/mod.rs @@ -0,0 +1,3 @@ +pub mod immediate; + +pub mod room; diff --git a/lightyear/src/server/room.rs b/lightyear/src/server/visibility/room.rs similarity index 70% rename from lightyear/src/server/room.rs rename to lightyear/src/server/visibility/room.rs index af70a2d6b..ce033a785 100644 --- a/lightyear/src/server/room.rs +++ b/lightyear/src/server/visibility/room.rs @@ -1,12 +1,47 @@ -//! # Room -//! -//! This module contains the room system, which is used to perform interest management. (being able to predict certain entities to certain clients only). -//! You can also find more information in the [book](https://cbournhonesque.github.io/lightyear/book/concepts/advanced_replication/interest_management.html). +/*! Room-based visibility module, where you can use semi-static rooms to manage visibility + +# Room + +Rooms are used to provide interest management in a semi-static way. +Entities and Clients can be added to multiple rooms. + +If an entity and a client are in the same room, then the entity will be visible to the client. +If an entity leaves a room that a client is in, or if a client leaves a room that an entity is in, +then the entity won't be visible anymore to the client. + +You can also find more information in the [book](https://cbournhonesque.github.io/lightyear/book/concepts/advanced_replication/interest_management.html). + +## Example + +This can be useful for games where you have physical instances of rooms: +- a RPG where you can have different rooms (tavern, cave, city, etc.) +- a server could have multiple lobbies, and each lobby is in its own room +- a map could be divided into a grid of 2D squares, where each square is its own room + +```rust +use bevy::prelude::*; +use lightyear::prelude::*; +use lightyear::prelude::server::*; + +fn room_system(mut manager: ResMut) { + // the entity will now be visible to the client + manager.add_client(ClientId::Netcode(0), RoomId(0)); + manager.add_entity(Entity::PLACEHOLDER, RoomId(0)); +} +``` + +## Implementation + +Under the hood, the [`RoomManager`] uses the same functions as in the immediate-mode [`VisibilityManager`], +it just caches the room metadata to keep track of the visibility of entities. + +*/ + use bevy::app::App; use bevy::ecs::entity::EntityHash; use bevy::prelude::{ - DetectChanges, Entity, IntoSystemConfigs, IntoSystemSetConfigs, Plugin, PostUpdate, Query, - RemovedComponents, Res, ResMut, Resource, SystemSet, + DetectChanges, Entity, IntoSystemConfigs, IntoSystemSetConfigs, Plugin, PostUpdate, PreUpdate, + Query, RemovedComponents, Res, ResMut, Resource, SystemSet, }; use bevy::reflect::Reflect; use bevy::utils::{HashMap, HashSet}; @@ -16,8 +51,10 @@ use tracing::{error, info, trace}; use crate::connection::id::ClientId; use crate::server::connection::ConnectionManager; + use crate::server::networking::is_started; -use crate::shared::replication::components::{DespawnTracker, Replicate, ReplicateVisibility}; +use crate::server::visibility::immediate::{VisibilityManager, VisibilitySet}; +use crate::shared::replication::components::{DespawnTracker, Replicate}; use crate::shared::replication::ReplicationSend; use crate::shared::sets::{InternalMainSet, InternalReplicationSet, ServerMarker}; use crate::shared::time_manager::is_server_ready_to_send; @@ -69,6 +106,12 @@ struct RoomEvents { entity_leave_room: EntityHashMap>, } +#[derive(Resource, Debug, Default)] +struct VisibilityEvents { + gained: HashMap, + lost: HashMap, +} + #[derive(Default, Debug)] struct RoomData { /// List of rooms that a client is in @@ -89,9 +132,9 @@ struct RoomData { #[derive(Debug, Default)] pub struct Room { /// list of clients that are in the room - clients: HashSet, + pub clients: HashSet, /// list of entities that are in the room - entities: EntityHashSet, + pub entities: EntityHashSet, } /// Manager responsible for handling rooms @@ -101,18 +144,6 @@ pub struct RoomManager { data: RoomData, } -impl RoomManager { - /// Returns a mutable reference to the room with the given id - pub fn room_mut(&mut self, id: RoomId) -> RoomMut { - RoomMut { id, manager: self } - } - - /// Returns a reference to the room with the given id - pub fn room(&self, id: RoomId) -> RoomRef { - RoomRef { id, manager: self } - } -} - /// Plugin used to handle interest managements via [`Room`]s #[derive(Default)] pub struct RoomPlugin; @@ -137,10 +168,9 @@ impl Plugin for RoomPlugin { PostUpdate, ( ( - // update replication caches must happen before replication, but after we add ReplicateVisibility - InternalReplicationSet::::HandleReplicateUpdate, + // the room events must be processed before the visibility events RoomSystemSets::UpdateReplicationCaches, - InternalReplicationSet::::Buffer, + VisibilitySet::UpdateVisibility, RoomSystemSets::RoomBookkeeping, ) .run_if(is_started) @@ -154,17 +184,16 @@ impl Plugin for RoomPlugin { ), ); // SYSTEMS + app.add_systems( + PreUpdate, + systems::handle_client_disconnect.after(InternalMainSet::::EmitEvents), + ); app.add_systems( PostUpdate, ( - ( - update_entity_replication_cache, - update_despawn_metadata_cache, - ) - .chain() + systems::buffer_room_visibility_events .in_set(RoomSystemSets::UpdateReplicationCaches), - (clear_entity_replication_cache, clean_entity_despawns) - .in_set(RoomSystemSets::RoomBookkeeping), + systems::clean_entity_despawns.in_set(RoomSystemSets::RoomBookkeeping), ), ); } @@ -172,7 +201,7 @@ impl Plugin for RoomPlugin { impl RoomManager { /// Remove the client from all the rooms it was in - pub(crate) fn client_disconnect(&mut self, client_id: ClientId) { + fn client_disconnect(&mut self, client_id: ClientId) { if let Some(rooms) = self.data.client_to_rooms.remove(&client_id) { for room_id in rooms { self.remove_client_internal(room_id, client_id); @@ -188,34 +217,35 @@ impl RoomManager { } } } + /// Add a client to the [`Room`] pub fn add_client(&mut self, client_id: ClientId, room_id: RoomId) { - self.room_mut(room_id).add_client(client_id) + self.add_client_internal(room_id, client_id) } /// Remove a client from the [`Room`] pub fn remove_client(&mut self, client_id: ClientId, room_id: RoomId) { - self.room_mut(room_id).remove_client(client_id) + self.remove_client_internal(room_id, client_id) } /// Add an entity to the [`Room`] pub fn add_entity(&mut self, entity: Entity, room_id: RoomId) { - self.room_mut(room_id).add_entity(entity) + self.add_entity_internal(room_id, entity) } /// Remove an entity from the [`Room`] pub fn remove_entity(&mut self, entity: Entity, room_id: RoomId) { - self.room_mut(room_id).remove_entity(entity) + self.remove_entity_internal(room_id, entity) } /// Returns true if the [`Room`] contains the [`ClientId`] pub fn has_client_id(&self, client_id: ClientId, room_id: RoomId) -> bool { - self.room(room_id).has_client_id(client_id) + self.has_client_internal(room_id, client_id) } /// Returns true if the [`Room`] contains the [`Entity`] pub fn has_entity(&self, entity: Entity, room_id: RoomId) -> bool { - self.room(room_id).has_entity(entity) + self.has_entity_internal(room_id, entity) } /// Get a room by its [`RoomId`] @@ -223,6 +253,13 @@ impl RoomManager { self.data.rooms.get(&room_id) } + /// Get a room by its [`RoomId`] + /// + /// Panics if the room does not exist. + pub fn room(&self, room_id: RoomId) -> &Room { + self.data.rooms.get(&room_id).unwrap() + } + fn add_client_internal(&mut self, room_id: RoomId, client_id: ClientId) { self.data .client_to_rooms @@ -282,76 +319,21 @@ impl RoomManager { .remove(&entity); self.events.entity_leave_room(room_id, entity); } -} - -/// Convenient wrapper to mutate a room -pub struct RoomMut<'s> { - pub(crate) id: RoomId, - pub(crate) manager: &'s mut RoomManager, -} -impl<'s> RoomMut<'s> { - fn new(manager: &'s mut RoomManager, id: RoomId) -> Self { - Self { id, manager } - } - - /// Add a client to the room - pub fn add_client(&mut self, client_id: ClientId) { - self.manager.add_client_internal(self.id, client_id) - } - - /// Remove a client from the room - pub fn remove_client(&mut self, client_id: ClientId) { - self.manager.remove_client_internal(self.id, client_id) - } - - /// Add an entity to the room - pub fn add_entity(&mut self, entity: Entity) { - self.manager.add_entity_internal(self.id, entity) - } - - /// Remove an entity from the room - pub fn remove_entity(&mut self, entity: Entity) { - self.manager.remove_entity_internal(self.id, entity) - } - - /// Returns true if the room contains the client - pub fn has_client_id(&self, client_id: ClientId) -> bool { - self.manager - .get_room(self.id) - .map_or_else(|| false, |room| room.clients.contains(&client_id)) - } - - /// Returns true if the room contains the entity - pub fn has_entity(&mut self, entity: Entity) -> bool { - self.manager - .get_room(self.id) + fn has_entity_internal(&self, room_id: RoomId, entity: Entity) -> bool { + self.data + .rooms + .get(&room_id) .map_or_else(|| false, |room| room.entities.contains(&entity)) } -} -/// Convenient wrapper to inspect a room -pub struct RoomRef<'s> { - pub(crate) id: RoomId, - pub(crate) manager: &'s RoomManager, -} - -impl<'s> RoomRef<'s> { - fn new(manager: &'s RoomManager, id: RoomId) -> Self { - Self { id, manager } - } - - pub fn has_client_id(&self, client_id: ClientId) -> bool { - self.manager - .get_room(self.id) + /// Returns true if the room contains the client + fn has_client_internal(&self, room_id: RoomId, client_id: ClientId) -> bool { + self.data + .rooms + .get(&room_id) .map_or_else(|| false, |room| room.clients.contains(&client_id)) } - - pub fn has_entity(&mut self, entity: Entity) -> bool { - self.manager - .get_room(self.id) - .map_or_else(|| false, |room| room.entities.contains(&entity)) - } } impl RoomEvents { @@ -361,6 +343,7 @@ impl RoomEvents { && self.entity_enter_room.is_empty() && self.entity_leave_room.is_empty() } + fn clear(&mut self) { self.client_enter_room.clear(); self.client_leave_room.clear(); @@ -444,160 +427,90 @@ impl RoomEvents { } } -// TODO: this should not be public? -/// Event related to [`Entities`](Entity) which are visible to a client -#[derive(Debug, PartialEq, Clone, Copy, Reflect)] -pub enum ClientVisibility { - /// the entity was not replicated to the client, but now is - Gained, - /// the entity was replicated to the client, but not anymore - Lost, - /// the entity was already replicated to the client, and still is - Maintained, -} - -// TODO: (perf) split this into 4 separate functions that access RoomManager in parallel? -// (we only use the ids in events, so we can read them in parallel) -/// Update each entities' replication-client-list based on the room events -/// Note that the rooms' entities/clients have already been updated at this point -fn update_entity_replication_cache( - mut room_manager: ResMut, - mut query: Query<&mut ReplicateVisibility>, -) { - if !room_manager.events.is_empty() { - trace!(?room_manager.events, "Room events"); - } - // enable split borrows by reborrowing Mut - let room_manager = &mut *room_manager; - - // NOTE: we handle leave room events before join room events so that if an entity leaves room 1 to join room 2 - // and the client is in both rooms, the entity does not get despawned - - // entity left room - for (entity, rooms) in room_manager.events.entity_leave_room.drain() { - // for each room left, update the entity's client visibility list if the client was in the room - rooms.into_iter().for_each(|room_id| { - let room = room_manager.data.rooms.get(&room_id).unwrap(); - room.clients.iter().for_each(|client_id| { - if let Ok(mut replicate) = query.get_mut(entity) { - if let Some(visibility) = replicate.clients_cache.get_mut(client_id) { - *visibility = ClientVisibility::Lost; - } - } - }); - }); +pub(super) mod systems { + use super::*; + use crate::server::events::DisconnectEvent; + use bevy::prelude::EventReader; + + /// Clear the internal room buffers when a client disconnects + pub fn handle_client_disconnect( + mut room_manager: ResMut, + mut disconnect_events: EventReader, + ) { + for event in disconnect_events.read() { + room_manager.client_disconnect(event.client_id); + } } - // entity joined room - for (entity, rooms) in room_manager.events.entity_enter_room.drain() { - // for each room joined, update the entity's client visibility list - rooms.into_iter().for_each(|room_id| { - let room = room_manager.data.rooms.get(&room_id).unwrap(); - room.clients.iter().for_each(|client_id| { - if let Ok(mut replicate) = query.get_mut(entity) { - replicate - .clients_cache - .entry(*client_id) - .and_modify(|vis| { - // if the visibility was lost above, then that means that the entity was visible - // for this client, so we just maintain it instead - if *vis == ClientVisibility::Lost { - *vis = ClientVisibility::Maintained - } - }) - // if the entity was not visible, the visibility is gained - .or_insert(ClientVisibility::Gained); - } + + // TODO: (perf) split this into 4 separate functions that access RoomManager in parallel? + // (we only use the ids in events, so we can read them in parallel) + /// Update each entities' replication-client-list based on the room events + /// Note that the rooms' entities/clients have already been updated at this point + pub fn buffer_room_visibility_events( + mut room_manager: ResMut, + mut visibility_manager: ResMut, + ) { + if !room_manager.events.is_empty() { + trace!(?room_manager.events, "Room events"); + } + // enable split borrows by reborrowing Mut + let room_manager = &mut *room_manager; + + // NOTE: we handle leave room events before join room events so that if an entity leaves room 1 to join room 2 + // and the client is in both rooms, the entity does not get despawned + + // entity left room + for (entity, rooms) in room_manager.events.entity_leave_room.drain() { + // for each room left, update the entity's client visibility list if the client was in the room + rooms.into_iter().for_each(|room_id| { + let room = room_manager.data.rooms.get(&room_id).unwrap(); + room.clients.iter().for_each(|client_id| { + trace!("entity {entity:?} left room {room:?}. Sending lost visibility to client {client_id:?}"); + visibility_manager.lose_visibility(*client_id, entity); + }); }); - }); - } - // client left room: update all the entities that are in that room - for (client_id, rooms) in room_manager.events.client_leave_room.drain() { - rooms.into_iter().for_each(|room_id| { - let room = room_manager.data.rooms.get(&room_id).unwrap(); - room.entities.iter().for_each(|entity| { - if let Ok(mut replicate) = query.get_mut(*entity) { - if let Some(visibility) = replicate.clients_cache.get_mut(&client_id) { - *visibility = ClientVisibility::Lost; - } - } + } + // entity joined room + for (entity, rooms) in room_manager.events.entity_enter_room.drain() { + // for each room joined, update the entity's client visibility list + rooms.into_iter().for_each(|room_id| { + let room = room_manager.data.rooms.get(&room_id).unwrap(); + room.clients.iter().for_each(|client_id| { + trace!("entity {entity:?} joined room {room:?}. Sending gained visibility to client {client_id:?}"); + visibility_manager.gain_visibility(*client_id, entity); + }); }); - }); - } - // client joined room: update all the entities that are in that room - for (client_id, rooms) in room_manager.events.client_enter_room.drain() { - rooms.into_iter().for_each(|room_id| { - let room = room_manager.data.rooms.get(&room_id).unwrap(); - room.entities.iter().for_each(|entity| { - if let Ok(mut replicate) = query.get_mut(*entity) { - replicate - .clients_cache - .entry(client_id) - .and_modify(|vis| { - // if the visibility was lost above, then that means that the entity was visible - // for this client, so we just maintain it instead - if *vis == ClientVisibility::Lost { - *vis = ClientVisibility::Maintained - } - }) - // if the entity was not visible, the visibility is gained - .or_insert(ClientVisibility::Gained); - } + } + // client left room: update all the entities that are in that room + for (client_id, rooms) in room_manager.events.client_leave_room.drain() { + rooms.into_iter().for_each(|room_id| { + let room = room_manager.data.rooms.get(&room_id).unwrap(); + room.entities.iter().for_each(|entity| { + trace!("client {client_id:?} left room {room:?}. Sending lost visibility to entity {entity:?}"); + visibility_manager.lose_visibility(client_id, *entity); + }); }); - }); - } -} - -/// Whenever the visibility of an entity changes, update the despawn metadata cache -/// so that we can correctly replicate the despawn to the correct clients -fn update_despawn_metadata_cache( - mut connection_manager: ResMut, - mut query: Query<(Entity, &mut ReplicateVisibility)>, -) { - for (entity, visibility) in query.iter_mut() { - if visibility.is_changed() { - if let Some(despawn_metadata) = connection_manager - .get_mut_replicate_despawn_cache() - .get_mut(&entity) - { - let new_cache = visibility.clients_cache.keys().copied().collect(); - despawn_metadata.replication_clients_cache = new_cache; - } } - } -} - -/// After replication, update the Replication Cache: -/// - Visibility Gained becomes Visibility Maintained -/// - Visibility Lost gets removed from the cache -fn clear_entity_replication_cache( - mut query: Query<&mut ReplicateVisibility>, - connection_manager: ResMut, -) { - for mut replicate in query.iter_mut() { - replicate - .clients_cache - .retain(|_, visibility| match visibility { - ClientVisibility::Gained => { - trace!("Visibility goes from gained to maintained"); - *visibility = ClientVisibility::Maintained; - true - } - ClientVisibility::Lost => { - trace!("remove client from room cache"); - false - } - ClientVisibility::Maintained => true, + // client joined room: update all the entities that are in that room + for (client_id, rooms) in room_manager.events.client_enter_room.drain() { + rooms.into_iter().for_each(|room_id| { + let room = room_manager.data.rooms.get(&room_id).unwrap(); + room.entities.iter().for_each(|entity| { + trace!("client {client_id:?} joined room {room:?}. Sending gained visibility to entity {entity:?}"); + visibility_manager.gain_visibility(client_id, *entity); + }); }); + } } -} -/// Clear out the room metadata for any entity that was ever replicated -fn clean_entity_despawns( - mut room_manager: ResMut, - mut despawned: RemovedComponents, -) { - for entity in despawned.read() { - room_manager.entity_despawn(entity); + /// Clear out the room metadata for any entity that was ever replicated + pub fn clean_entity_despawns( + mut room_manager: ResMut, + mut despawned: RemovedComponents, + ) { + for entity in despawned.read() { + room_manager.entity_despawn(entity); + } } } @@ -609,10 +522,16 @@ mod tests { use crate::prelude::client::*; use crate::prelude::*; - use crate::shared::replication::components::ReplicationMode; + use crate::server::visibility::immediate::systems::{ + add_replicate_visibility, update_visibility_from_events, + }; + use crate::server::visibility::immediate::{ClientVisibility, ReplicateVisibility}; + use crate::shared::replication::components::VisibilityMode; use crate::shared::replication::systems::handle_replicate_add; use crate::tests::stepper::{BevyStepper, Step}; + use super::systems::buffer_room_visibility_events; + use super::*; #[test] @@ -628,15 +547,14 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .add_client(client_id); + .add_client(client_id, room_id); // Spawn an entity on server let server_entity = stepper .server_app .world .spawn(Replicate { - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..Default::default() }) .id(); @@ -660,8 +578,7 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .add_entity(server_entity); + .add_entity(server_entity, room_id); assert!(stepper .server_app .world @@ -675,7 +592,18 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + assert!(stepper + .server_app + .world + .entity(server_entity) + .get::() + .is_some()); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); + assert_eq!( stepper .server_app @@ -730,8 +658,7 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .remove_entity(server_entity); + .remove_entity(server_entity, room_id); assert!(stepper .server_app .world @@ -744,7 +671,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -794,7 +725,7 @@ mod tests { .server_app .world .spawn(Replicate { - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..Default::default() }) .id(); @@ -806,8 +737,7 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .add_entity(server_entity); + .add_entity(server_entity, room_id); stepper.frame_step(); stepper.frame_step(); @@ -824,8 +754,7 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .add_client(client_id); + .add_client(client_id, room_id); assert!(stepper .server_app .world @@ -839,7 +768,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -894,8 +827,7 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .remove_client(client_id); + .remove_client(client_id, room_id); assert!(stepper .server_app .world @@ -908,7 +840,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -956,33 +892,35 @@ mod tests { .server_app .world .resource_mut::() - .room_mut(room_id) - .add_client(client_id); + .add_client(client_id, room_id); // Spawn an entity on server, in the same room let server_entity = stepper .server_app .world .spawn(Replicate { - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..Default::default() }) .id(); stepper .server_app .world - .run_system_once(handle_replicate_add::); + .run_system_once(add_replicate_visibility); stepper .server_app .world .resource_mut::() - .room_mut(room_id) - .add_entity(server_entity); + .add_entity(server_entity, room_id); // Run update replication cache once stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -1033,7 +971,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -1071,14 +1013,14 @@ mod tests { .server_app .world .spawn(Replicate { - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..Default::default() }) .id(); stepper .server_app .world - .run_system_once(handle_replicate_add::); + .run_system_once(add_replicate_visibility); stepper .server_app .world @@ -1088,7 +1030,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -1127,7 +1073,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -1160,14 +1110,14 @@ mod tests { .server_app .world .spawn(Replicate { - replication_mode: ReplicationMode::Room, + visibility: VisibilityMode::InterestManagement, ..Default::default() }) .id(); stepper .server_app .world - .run_system_once(handle_replicate_add::); + .run_system_once(add_replicate_visibility); stepper .server_app .world @@ -1182,7 +1132,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app @@ -1221,7 +1175,11 @@ mod tests { stepper .server_app .world - .run_system_once(update_entity_replication_cache); + .run_system_once(buffer_room_visibility_events); + stepper + .server_app + .world + .run_system_once(update_visibility_from_events); assert_eq!( stepper .server_app diff --git a/lightyear/src/shared/events/components.rs b/lightyear/src/shared/events/components.rs index 08aea278c..a44a154da 100644 --- a/lightyear/src/shared/events/components.rs +++ b/lightyear/src/shared/events/components.rs @@ -6,32 +6,6 @@ use bevy::prelude::{Component, Entity, Event}; use crate::packet::message::Message; -/// This event is emitted whenever a client connects to the server -#[derive(Event)] -pub struct ConnectEvent(Ctx); - -impl ConnectEvent { - pub fn new(context: Ctx) -> Self { - Self(context) - } - pub fn context(&self) -> &Ctx { - &self.0 - } -} - -/// This event is emitted whenever a client disconnects from the server -#[derive(Event)] -pub struct DisconnectEvent(Ctx); - -impl DisconnectEvent { - pub fn new(context: Ctx) -> Self { - Self(context) - } - pub fn context(&self) -> &Ctx { - &self.0 - } -} - /// This event is emitted whenever we receive a message from the remote #[derive(Event)] pub struct MessageEvent { diff --git a/lightyear/src/shared/events/mod.rs b/lightyear/src/shared/events/mod.rs index a9f26ea80..a98424742 100644 --- a/lightyear/src/shared/events/mod.rs +++ b/lightyear/src/shared/events/mod.rs @@ -1,6 +1,5 @@ //! This module defines bevy [`Events`](bevy::prelude::Events) related to networking events -pub(crate) mod connection; - pub mod components; +pub(crate) mod connection; pub mod plugin; pub mod systems; diff --git a/lightyear/src/shared/events/plugin.rs b/lightyear/src/shared/events/plugin.rs index bcfcfa804..95f72347d 100644 --- a/lightyear/src/shared/events/plugin.rs +++ b/lightyear/src/shared/events/plugin.rs @@ -3,11 +3,9 @@ use bevy::app::{App, PreUpdate}; use bevy::prelude::{IntoSystemConfigs, Plugin, PostUpdate}; -use crate::shared::events::components::{ - ConnectEvent, DisconnectEvent, EntityDespawnEvent, EntitySpawnEvent, -}; +use crate::shared::events::components::{EntityDespawnEvent, EntitySpawnEvent}; use crate::shared::events::systems::{clear_events, push_entity_events}; -use crate::shared::replication::ReplicationSend; +use crate::shared::replication::ReplicationReceive; use crate::shared::sets::{InternalMainSet, InternalReplicationSet}; pub struct EventsPlugin { @@ -22,12 +20,10 @@ impl Default for EventsPlugin { } } -impl Plugin for EventsPlugin { +impl Plugin for EventsPlugin { fn build(&self, app: &mut App) { // EVENTS - app.add_event::>() - .add_event::>() - .add_event::>() + app.add_event::>() .add_event::>(); // SYSTEMS app.add_systems( diff --git a/lightyear/src/shared/events/systems.rs b/lightyear/src/shared/events/systems.rs index ae7bfd613..89e6ed4a7 100644 --- a/lightyear/src/shared/events/systems.rs +++ b/lightyear/src/shared/events/systems.rs @@ -9,10 +9,10 @@ use crate::shared::events::connection::{ ClearEvents, IterComponentInsertEvent, IterComponentRemoveEvent, IterComponentUpdateEvent, IterEntityDespawnEvent, IterEntitySpawnEvent, }; -use crate::shared::replication::ReplicationSend; +use crate::shared::replication::ReplicationReceive; /// System that gathers the replication events received by the local host and sends them to bevy Events -pub(crate) fn push_component_events( +pub(crate) fn push_component_events( component_registry: Res, mut connection_manager: ResMut, mut component_insert_events: EventWriter>, @@ -40,7 +40,7 @@ pub(crate) fn push_component_events( } /// System that gathers the replication events received by the local host and sends them to bevy Events -pub(crate) fn push_entity_events( +pub(crate) fn push_entity_events( mut connection_manager: ResMut, mut entity_spawn_events: EventWriter>, mut entity_despawn_events: EventWriter>, @@ -59,6 +59,6 @@ pub(crate) fn push_entity_events( ); } -pub(crate) fn clear_events(mut connection_manager: ResMut) { +pub(crate) fn clear_events(mut connection_manager: ResMut) { connection_manager.events().clear() } diff --git a/lightyear/src/shared/message.rs b/lightyear/src/shared/message.rs index 53c3ab07c..388a7e5f5 100644 --- a/lightyear/src/shared/message.rs +++ b/lightyear/src/shared/message.rs @@ -1,5 +1,5 @@ -use crate::prelude::{Channel, ChannelKind, Message, NetworkTarget}; -use crate::protocol::EventContext; +use crate::prelude::{Channel, ChannelKind, Message}; +use crate::shared::replication::network_target::NetworkTarget; use bevy::prelude::Resource; use std::fmt::Debug; use std::hash::Hash; diff --git a/lightyear/src/shared/plugin.rs b/lightyear/src/shared/plugin.rs index 6c7f88b1e..9bfa5c8da 100644 --- a/lightyear/src/shared/plugin.rs +++ b/lightyear/src/shared/plugin.rs @@ -1,17 +1,17 @@ -//! Bevy [`bevy::prelude::Plugin`] used by both the server and the client +//! Bevy [`Plugin`] used by both the server and the client use crate::client::config::ClientConfig; use crate::connection::server::ServerConnections; use bevy::ecs::system::SystemParam; use bevy::prelude::*; use crate::prelude::{ - AppComponentExt, ChannelDirection, ChannelRegistry, ComponentRegistry, IoConfig, - LinkConditionerConfig, MessageRegistry, Mode, ParentSync, PingConfig, PrePredicted, - PreSpawnedPlayerObject, ShouldBePredicted, TickConfig, + AppComponentExt, ChannelDirection, ChannelRegistry, ComponentRegistry, LinkConditionerConfig, + MessageRegistry, Mode, ParentSync, PingConfig, PrePredicted, PreSpawnedPlayerObject, + ShouldBePredicted, TickConfig, }; use crate::server::config::ServerConfig; use crate::shared::config::SharedConfig; -use crate::shared::replication::components::ShouldBeInterpolated; +use crate::shared::replication::components::{Controlled, ShouldBeInterpolated}; use crate::shared::tick_manager::TickManagerPlugin; use crate::shared::time_manager::TimePlugin; use crate::transport::middleware::compression::CompressionConfig; @@ -73,8 +73,7 @@ impl Plugin for SharedPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() - .register_type::(); + .register_type::(); // RESOURCES // NOTE: this tick duration must be the same as any previous existing fixed timesteps @@ -108,7 +107,8 @@ impl Plugin for SharedPlugin { app.register_component::(ChannelDirection::ServerToClient); app.register_component::(ChannelDirection::ServerToClient); app.register_component::(ChannelDirection::Bidirectional) - .add_map_entities::(); + .add_map_entities(); + app.register_component::(ChannelDirection::Bidirectional); // check that the protocol was built correctly app.world.resource::().check(); } diff --git a/lightyear/src/shared/replication/commands.rs b/lightyear/src/shared/replication/commands.rs index 045a111f2..e671755ab 100644 --- a/lightyear/src/shared/replication/commands.rs +++ b/lightyear/src/shared/replication/commands.rs @@ -10,7 +10,7 @@ fn remove_replicate(entity: Entity, world: &mut World) { let mut sender = world.resource_mut::(); // remove the entity from the cache of entities that are being replicated // so that if it gets despawned, the despawn won't be replicated - sender.get_mut_replicate_despawn_cache().remove(&entity); + sender.get_mut_replicate_cache().remove(&entity); // remove the replicate component if let Some(mut entity) = world.get_entity_mut(entity) { entity.remove::(); @@ -58,7 +58,7 @@ mod tests { incoming_loss: 0.0, }; let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); + let prediction_config = PredictionConfig::default(); let interpolation_config = InterpolationConfig::default(); let mut stepper = BevyStepper::new( shared_config, diff --git a/lightyear/src/shared/replication/components.rs b/lightyear/src/shared/replication/components.rs index 9665f0daf..c6815b97e 100644 --- a/lightyear/src/shared/replication/components.rs +++ b/lightyear/src/shared/replication/components.rs @@ -1,7 +1,8 @@ //! Components used for replication use bevy::ecs::entity::MapEntities; use bevy::ecs::query::QueryFilter; -use bevy::prelude::{Component, Entity, EntityMapper, Or, Reflect, With}; +use bevy::ecs::system::SystemParam; +use bevy::prelude::{Bundle, Component, Entity, EntityMapper, Or, Query, Reflect, With}; use bevy::utils::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use tracing::trace; @@ -13,7 +14,8 @@ use crate::client::components::SyncComponent; use crate::connection::id::ClientId; use crate::prelude::ParentSync; use crate::protocol::component::{ComponentKind, ComponentNetId, ComponentRegistry}; -use crate::server::room::ClientVisibility; +use crate::server::visibility::immediate::{ClientVisibility, VisibilityManager}; +use crate::shared::replication::network_target::NetworkTarget; /// Marker component that indicates that the entity was spawned via replication /// (it is being replicated from a remote world) @@ -26,43 +28,109 @@ pub struct Replicated; #[component(storage = "SparseSet")] pub(crate) struct DespawnTracker; -/// Component that indicates that an entity should be replicated. Added to the entity when it is spawned -/// in the world that sends replication updates. -#[derive(Component, Clone, PartialEq, Debug, Reflect)] +/// Marker component to indicate that the entity is under the control of the local peer +#[derive(Component, Clone, Copy, PartialEq, Debug, Reflect, Serialize, Deserialize)] +#[component(storage = "SparseSet")] +pub struct Controlled; + +/// Bundle that indicates how an entity should be replicated. Add this to an entity to start replicating +/// it to remote peers. +/// +/// ```rust +/// use bevy::prelude::*; +/// use lightyear::prelude::*; +/// +/// let mut world = World::default(); +/// world.spawn(Replicate::default()); +/// ``` +/// +/// The bundle is composed of several components: +/// - [`ReplicationTarget`] to specify which clients should receive the entity +/// - [`ControlledBy`] to specify which client controls the entity +/// - [`VisibilityMode`] to specify if we should replicate the entity to all clients in the +/// replication target, or if we should apply interest management logic to determine which clients +/// - [`ReplicationGroup`] to group entities together for replication. Entities in the same group +/// will be sent together in the same message. +/// - [`ReplicateHierarchy`] to specify how the hierarchy of the entity should be replicated +/// +/// Some of the components can be updated at runtime even after the entity has been replicated. +/// For example you can update the [`ReplicationTarget`] to change which clients should receive the entity. +#[derive(Bundle, Clone, Default, PartialEq, Debug, Reflect)] pub struct Replicate { /// Which clients should this entity be replicated to - pub replication_target: NetworkTarget, - /// Which clients should predict this entity - pub prediction_target: NetworkTarget, - /// Which clients should interpolated this entity - pub interpolation_target: NetworkTarget, - /// How do we find the list of clients to replicate to? - pub replication_mode: ReplicationMode, + pub target: ReplicationTarget, + /// Which client(s) control this entity? + pub controlled_by: ControlledBy, + /// How do we control the visibility of the entity? + pub visibility: VisibilityMode, /// The replication group defines how entities are grouped (sent as a single message) for replication. - /// This should not be modified after the Replicate component is created + /// + /// After the entity is first replicated, the replication group of the entity should not be modified. + /// (but more entities can be added to the replication group) // TODO: currently, if the host removes Replicate, then the entity is not removed in the remote // it just keeps living but doesn't receive any updates. Should we make this configurable? - pub replication_group: ReplicationGroup, - /// If true, recursively add `Replicate` and `ParentSync` components to all children to make sure they are replicated - /// If false, you can still replicate hierarchies, but in a more fine-grained manner. You will have to add the `Replicate` - /// and `ParentSync` components to the children yourself - pub replicate_hierarchy: bool, + pub group: ReplicationGroup, + /// How should the hierarchy of the entity (parents/children) be replicated? + pub hierarchy: ReplicateHierarchy, +} + +/// Component that indicates which clients the entity should be replicated to. +#[derive(Component, Clone, Debug, PartialEq, Reflect)] +pub struct ReplicationTarget { + /// Which clients should this entity be replicated to + pub replication: NetworkTarget, + /// Which clients should predict this entity (unused for client to server replication) + pub prediction: NetworkTarget, + /// Which clients should interpolate this entity (unused for client to server replication) + pub interpolation: NetworkTarget, +} + +impl Default for ReplicationTarget { + fn default() -> Self { + Self { + replication: NetworkTarget::All, + prediction: NetworkTarget::None, + interpolation: NetworkTarget::None, + } + } +} - /// Defines the target entity for the replication. - /// In most cases, you will want to spawn a new entity on the target client - pub target_entity: TargetEntity, +/// Component storing metadata about which clients have control over the entity +/// +/// This is only used for server to client replication. +#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)] +pub struct ControlledBy { + /// Which client(s) control this entity? + pub target: NetworkTarget, +} - // TODO: could it be dangerous to use component kind here? (because the value could vary between rust versions) - // should be ok, because this is not networked - /// Lets you override the replication modalities for a specific component - #[reflect(ignore)] - pub per_component_metadata: HashMap, +impl ControlledBy { + /// Returns true if the entity is controlled by the specified client + pub fn targets(&self, client_id: &ClientId) -> bool { + self.target.targets(client_id) + } } -/// Defines the target entity for the replication -#[derive(Default, Clone, Debug, PartialEq, Reflect)] +/// Component to have more fine-grained control over the visibility of an entity +/// (which clients do we replicate this entity to?) +/// +/// This has no effect for client to server replication. +#[derive(Component, Clone, Debug, PartialEq, Reflect)] +pub struct Visibility { + /// Control if we do fine-grained or coarse-grained visibility + mode: VisibilityMode, + // TODO: should we store the visibility cache here if visibility_mode = InterestManagement? +} + +/// Defines the target entity for the replication. +/// +/// This can be used if you want to replicate this entity on an entity that already +/// exists in the remote world. +/// +/// This component is not part of the `Replicate` bundle as this is very infrequent. +#[derive(Component, Default, Clone, Copy, Debug, PartialEq, Reflect)] pub enum TargetEntity { - /// Spawn a new entity on the target client + /// Spawn a new entity on the remote peer #[default] Spawn, /// Instead of spawning a new entity, we will apply the replication updates @@ -70,129 +138,83 @@ pub enum TargetEntity { Preexisting(Entity), } -#[derive(Component, Clone, Default, PartialEq, Debug, Reflect)] -pub(crate) struct ReplicateVisibility { - /// List of clients that the entity is currently replicated to. - /// Will be updated before the other replication systems - pub(crate) clients_cache: HashMap, +/// Component that defines how the hierarchy of an entity (parent/children) should be replicated +#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)] +pub struct ReplicateHierarchy { + /// If true, recursively add `Replicate` and `ParentSync` components to all children to make sure they are replicated + /// If false, you can still replicate hierarchies, but in a more fine-grained manner. You will have to add the `Replicate` + /// and `ParentSync` components to the children yourself + pub recursive: bool, } -/// This lets you specify how to customize the replication behaviour for a given component -#[derive(Clone, Debug, PartialEq, Reflect)] -pub struct PerComponentReplicationMetadata { - /// If true, do not replicate the component. (By default, all components of this entity that are present in the - /// [`ComponentRegistry`] will be replicated. - disabled: bool, - /// If true, replicate only inserts/removals of the component, not the updates. - /// (i.e. the component will only get replicated once at spawn) - /// This is useful for components such as `ActionState`, which should only be replicated once - replicate_once: bool, - /// Custom replication target for this component. We will replicate to the intersection of - /// the entity's replication target and this target - target: NetworkTarget, -} -impl Default for PerComponentReplicationMetadata { +impl Default for ReplicateHierarchy { fn default() -> Self { - Self { - disabled: false, - replicate_once: false, - target: NetworkTarget::All, - } + Self { recursive: true } } } -impl Replicate { - pub(crate) fn group_id(&self, entity: Option) -> ReplicationGroupId { - self.replication_group.group_id(entity) - } +// TODO: should these be sparse set or not? +/// If this component is present, we won't replicate the component +/// +/// (By default, all components that are present in the [`ComponentRegistry`] will be replicated.) +#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)] +#[component(storage = "SparseSet")] +pub struct DisabledComponent { + _marker: std::marker::PhantomData, +} - /// Returns true if we don't want to replicate the component - pub fn is_disabled(&self) -> bool { - let kind = ComponentKind::of::(); - self.per_component_metadata - .get(&kind) - .is_some_and(|metadata| metadata.disabled) +impl Default for DisabledComponent { + fn default() -> Self { + Self { + _marker: Default::default(), + } } +} - /// If true, the component will be replicated only once, when the entity is spawned. - /// We do not replicate component updates - pub fn is_replicate_once(&self) -> bool { - let kind = ComponentKind::of::(); - self.per_component_metadata - .get(&kind) - .is_some_and(|metadata| metadata.replicate_once) - } +/// If this component is present, we will replicate only the inserts/removals of the component, +/// not the updates (i.e. the component will get only replicated once at entity spawn) +#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)] +#[component(storage = "SparseSet")] +pub struct ReplicateOnceComponent { + _marker: std::marker::PhantomData, +} - /// Replication target for this specific component - /// This will be the intersection of the provided `entity_target`, and the `target` of the component - /// if it exists - pub fn target(&self, mut entity_target: NetworkTarget) -> NetworkTarget { - let kind = ComponentKind::of::(); - match self.per_component_metadata.get(&kind) { - None => entity_target, - Some(metadata) => { - entity_target.intersection(metadata.target.clone()); - trace!(?kind, "final target: {:?}", entity_target); - entity_target - } +impl Default for ReplicateOnceComponent { + fn default() -> Self { + Self { + _marker: Default::default(), } } +} - /// Disable the replication of a component for this entity - pub fn disable_component(&mut self) { - let kind = ComponentKind::of::(); - self.per_component_metadata - .entry(kind) - .or_default() - .disabled = true; - } +// TODO: maybe have 3 fields: +// - target +// - override replication_target: bool (if true, we will completely override the replication target. If false, we do the intersection) +// - override visibility: bool (if true, we will completely override the visibility. If false, we do the intersection) +/// This component lets you override the replication target for a specific component +#[derive(Component, Clone, Debug, PartialEq, Reflect)] +pub struct OverrideTargetComponent { + pub target: NetworkTarget, + _marker: std::marker::PhantomData, +} - /// Enable the replication of a component for this entity - pub fn enable_component(&mut self) { - let kind = ComponentKind::of::(); - self.per_component_metadata - .entry(kind) - .or_default() - .disabled = false; - // if we are back at the default, remove the entry - if self.per_component_metadata.get(&kind).unwrap() - == &PerComponentReplicationMetadata::default() - { - self.per_component_metadata.remove(&kind); +impl OverrideTargetComponent { + pub fn new(target: NetworkTarget) -> Self { + Self { + target, + _marker: Default::default(), } } +} - pub fn enable_replicate_once(&mut self) { - let kind = ComponentKind::of::(); - self.per_component_metadata - .entry(kind) - .or_default() - .replicate_once = true; - } - - pub fn disable_replicate_once(&mut self) { - let kind = ComponentKind::of::(); - self.per_component_metadata - .entry(kind) - .or_default() - .replicate_once = false; - // if we are back at the default, remove the entry - if self.per_component_metadata.get(&kind).unwrap() - == &PerComponentReplicationMetadata::default() - { - self.per_component_metadata.remove(&kind); - } +impl Replicate { + pub(crate) fn group_id(&self, entity: Option) -> ReplicationGroupId { + self.group.group_id(entity) } - pub fn add_target(&mut self, target: NetworkTarget) { - let kind = ComponentKind::of::(); - self.per_component_metadata.entry(kind).or_default().target = target; - // if we are back at the default, remove the entry - if self.per_component_metadata.get(&kind).unwrap() - == &PerComponentReplicationMetadata::default() - { - self.per_component_metadata.remove(&kind); - } + /// Returns true if the entity is controlled by the specified client + pub fn is_controlled_by(&self, client_id: &ClientId) -> bool { + self.controlled_by.targets(client_id) } } @@ -208,7 +230,11 @@ pub enum ReplicationGroupIdBuilder { Group(u64), } -#[derive(Debug, Copy, Clone, PartialEq, Reflect)] +/// Component to specify the replication group of an entity +/// +/// If multiple entities are part of the same replication group, they will be sent together in the same message. +/// It is guaranteed that these entities will be updated at the same time on the remote world. +#[derive(Component, Debug, Copy, Clone, PartialEq, Reflect)] pub struct ReplicationGroup { id_builder: ReplicationGroupIdBuilder, /// the priority of the accumulation group @@ -280,200 +306,24 @@ impl ReplicationGroup { )] pub struct ReplicationGroupId(pub u64); -#[derive(Clone, Copy, Default, Debug, PartialEq, Reflect)] -pub enum ReplicationMode { - /// We will replicate this entity only to clients that are in the same room as the entity +#[derive(Component, Clone, Copy, Default, Debug, PartialEq, Reflect)] +pub enum VisibilityMode { + /// We will replicate this entity to the clients specified in the `replication_target`. + /// On top of that, we will apply interest management logic to determine which clients should receive the entity + /// + /// You can use [`gain_visibility`](VisibilityManager::gain_visibility) and [`lose_visibility`](VisibilityManager::lose_visibility) + /// to control the visibility of entities. + /// You can also use the [`RoomManager`](crate::prelude::server::RoomManager) + /// /// (the client still needs to be included in the [`NetworkTarget`], the room is simply an additional constraint) - Room, - /// We will replicate this entity to clients using only the [`NetworkTarget`], without caring about rooms - #[default] - NetworkTarget, -} - -impl Default for Replicate { - fn default() -> Self { - #[allow(unused_mut)] - let mut replicate = Self { - replication_target: NetworkTarget::All, - prediction_target: NetworkTarget::None, - interpolation_target: NetworkTarget::None, - replication_mode: ReplicationMode::default(), - replication_group: Default::default(), - replicate_hierarchy: true, - target_entity: Default::default(), - per_component_metadata: HashMap::default(), - }; - // TODO: what's the point in replicating them once since they don't change? - // or is it because they are removed and we don't want to replicate the removal? - // those metadata components should only be replicated once - replicate.enable_replicate_once::(); - replicate.enable_replicate_once::(); - // cfg_if! { - // // the ActionState components are replicated only once when the entity is spawned - // // then they get updated by the user inputs, not by replication! - // if #[cfg(feature = "leafwing")] { - // use leafwing_input_manager::prelude::ActionState; - // replicate.enable_replicate_once::>(); - // replicate.enable_replicate_once::>(); - // } - // } - replicate - } -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Reflect, Encode, Decode)] -/// NetworkTarget indicated which clients should receive some message -pub enum NetworkTarget { + InterestManagement, + /// We will replicate this entity to the client specified in the `replication_target`, without + /// running any additional interest management logic #[default] - /// Message sent to no client - None, - /// Message sent to all clients except one - AllExceptSingle(ClientId), - /// Message sent to all clients except for these - AllExcept(Vec), - /// Message sent to all clients All, - /// Message sent to only these - Only(Vec), - /// Message sent to only this one client - Single(ClientId), -} - -impl NetworkTarget { - /// Return true if we should replicate to the specified client - pub(crate) fn should_send_to(&self, client_id: &ClientId) -> bool { - match self { - NetworkTarget::All => true, - NetworkTarget::AllExceptSingle(single) => client_id != single, - NetworkTarget::AllExcept(client_ids) => !client_ids.contains(client_id), - NetworkTarget::Only(client_ids) => client_ids.contains(client_id), - NetworkTarget::Single(single) => client_id == single, - NetworkTarget::None => false, - } - } - - /// Compute the intersection of this target with another one (A ∩ B) - pub(crate) fn intersection(&mut self, target: NetworkTarget) { - match self { - NetworkTarget::All => { - *self = target; - } - NetworkTarget::AllExceptSingle(existing_client_id) => { - let mut a = NetworkTarget::AllExcept(vec![*existing_client_id]); - a.intersection(target); - *self = a; - } - NetworkTarget::AllExcept(existing_client_ids) => match target { - NetworkTarget::None => { - *self = NetworkTarget::None; - } - NetworkTarget::AllExceptSingle(target_client_id) => { - let mut new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); - new_excluded_ids.insert(target_client_id); - *existing_client_ids = Vec::from_iter(new_excluded_ids); - } - NetworkTarget::AllExcept(target_client_ids) => { - let mut new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); - target_client_ids.into_iter().for_each(|id| { - new_excluded_ids.insert(id); - }); - *existing_client_ids = Vec::from_iter(new_excluded_ids); - } - NetworkTarget::All => {} - NetworkTarget::Only(target_client_ids) => { - let mut new_included_ids = HashSet::from_iter(target_client_ids.clone()); - existing_client_ids.iter_mut().for_each(|id| { - new_included_ids.remove(id); - }); - *self = NetworkTarget::Only(Vec::from_iter(new_included_ids)); - } - NetworkTarget::Single(target_client_id) => { - if existing_client_ids.contains(&target_client_id) { - *self = NetworkTarget::None; - } else { - *self = NetworkTarget::Single(target_client_id); - } - } - }, - NetworkTarget::Only(existing_client_ids) => match target { - NetworkTarget::None => { - *self = NetworkTarget::None; - } - NetworkTarget::AllExceptSingle(target_client_id) => { - let mut new_included_ids = HashSet::from_iter(existing_client_ids.clone()); - new_included_ids.remove(&target_client_id); - *existing_client_ids = Vec::from_iter(new_included_ids); - } - NetworkTarget::AllExcept(target_client_ids) => { - let mut new_included_ids = HashSet::from_iter(existing_client_ids.clone()); - target_client_ids.into_iter().for_each(|id| { - new_included_ids.remove(&id); - }); - *existing_client_ids = Vec::from_iter(new_included_ids); - } - NetworkTarget::All => {} - NetworkTarget::Single(target_client_id) => { - if existing_client_ids.contains(&target_client_id) { - *self = NetworkTarget::Single(target_client_id); - } else { - *self = NetworkTarget::None; - } - } - NetworkTarget::Only(target_client_ids) => { - let new_included_ids = HashSet::from_iter(existing_client_ids.clone()); - let target_included_ids = HashSet::from_iter(target_client_ids.clone()); - let intersection = new_included_ids.intersection(&target_included_ids).cloned(); - *existing_client_ids = intersection.collect::>(); - } - }, - NetworkTarget::Single(existing_client_id) => { - let mut a = NetworkTarget::Only(vec![*existing_client_id]); - a.intersection(target); - *self = a; - } - NetworkTarget::None => {} - } - } - - /// Compute the difference of this target with another one (A - B) - pub(crate) fn exclude(&mut self, client_ids: Vec) { - match self { - NetworkTarget::All => { - *self = NetworkTarget::AllExcept(client_ids); - } - NetworkTarget::AllExceptSingle(existing_client_id) => { - let mut new_excluded_ids = HashSet::from_iter(client_ids.clone()); - new_excluded_ids.insert(*existing_client_id); - *self = NetworkTarget::AllExcept(Vec::from_iter(new_excluded_ids)); - } - NetworkTarget::AllExcept(existing_client_ids) => { - let mut new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); - client_ids.into_iter().for_each(|id| { - new_excluded_ids.insert(id); - }); - *existing_client_ids = Vec::from_iter(new_excluded_ids); - } - NetworkTarget::Only(existing_client_ids) => { - let mut new_ids = HashSet::from_iter(existing_client_ids.clone()); - client_ids.into_iter().for_each(|id| { - new_ids.remove(&id); - }); - if new_ids.is_empty() { - *self = NetworkTarget::None; - } else { - *existing_client_ids = Vec::from_iter(new_ids); - } - } - NetworkTarget::Single(client_id) => { - if client_ids.contains(client_id) { - *self = NetworkTarget::None; - } - } - NetworkTarget::None => {} - } - } } +/// Marker component that tells the client to spawn an Interpolated entity #[derive(Component, Serialize, Deserialize, Clone, Debug, PartialEq, Reflect)] #[component(storage = "SparseSet")] pub struct ShouldBeInterpolated; @@ -488,81 +338,7 @@ pub struct PrePredicted { pub(crate) client_entity: Option, } +/// Marker component that tells the client to spawn a Predicted entity #[derive(Component, Serialize, Deserialize, Clone, Debug, Default, PartialEq, Reflect)] #[component(storage = "SparseSet")] pub struct ShouldBePredicted; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_network_target() { - let client_0 = ClientId::Netcode(0); - let client_1 = ClientId::Netcode(1); - let client_2 = ClientId::Netcode(2); - let mut target = NetworkTarget::All; - assert!(target.should_send_to(&client_0)); - target.exclude(vec![client_1, client_2]); - assert_eq!(target, NetworkTarget::AllExcept(vec![client_1, client_2])); - - target = NetworkTarget::AllExcept(vec![client_0]); - assert!(!target.should_send_to(&client_0)); - assert!(target.should_send_to(&client_1)); - target.exclude(vec![client_0, client_1]); - assert!(matches!(target, NetworkTarget::AllExcept(_))); - - if let NetworkTarget::AllExcept(ids) = target { - assert!(ids.contains(&client_0)); - assert!(ids.contains(&client_1)); - } - - target = NetworkTarget::Only(vec![client_0]); - assert!(target.should_send_to(&client_0)); - assert!(!target.should_send_to(&client_1)); - target.exclude(vec![client_1]); - assert_eq!(target, NetworkTarget::Only(vec![client_0])); - target.exclude(vec![client_0, client_2]); - assert_eq!(target, NetworkTarget::None); - - target = NetworkTarget::None; - assert!(!target.should_send_to(&client_0)); - target.exclude(vec![client_1]); - assert_eq!(target, NetworkTarget::None); - } - - #[test] - fn test_intersection() { - let client_0 = ClientId::Netcode(0); - let client_1 = ClientId::Netcode(1); - let client_2 = ClientId::Netcode(2); - let mut target = NetworkTarget::All; - target.intersection(NetworkTarget::AllExcept(vec![client_1, client_2])); - assert_eq!(target, NetworkTarget::AllExcept(vec![client_1, client_2])); - - target = NetworkTarget::AllExcept(vec![client_0]); - target.intersection(NetworkTarget::AllExcept(vec![client_0, client_1])); - assert!(matches!(target, NetworkTarget::AllExcept(_))); - - if let NetworkTarget::AllExcept(ids) = target { - assert!(ids.contains(&client_0)); - assert!(ids.contains(&client_1)); - } - - target = NetworkTarget::AllExcept(vec![client_0, client_1]); - target.intersection(NetworkTarget::Only(vec![client_0, client_2])); - assert_eq!(target, NetworkTarget::Only(vec![client_2])); - - target = NetworkTarget::Only(vec![client_0, client_1]); - target.intersection(NetworkTarget::Only(vec![client_0, client_2])); - assert_eq!(target, NetworkTarget::Only(vec![client_0])); - - target = NetworkTarget::Only(vec![client_0, client_1]); - target.intersection(NetworkTarget::AllExcept(vec![client_0, client_2])); - assert_eq!(target, NetworkTarget::Only(vec![client_1])); - - target = NetworkTarget::None; - target.intersection(NetworkTarget::AllExcept(vec![client_0, client_2])); - assert_eq!(target, NetworkTarget::None); - } -} diff --git a/lightyear/src/shared/replication/entity_map.rs b/lightyear/src/shared/replication/entity_map.rs index fd6a702db..99b3da785 100644 --- a/lightyear/src/shared/replication/entity_map.rs +++ b/lightyear/src/shared/replication/entity_map.rs @@ -146,7 +146,7 @@ mod tests { incoming_loss: 0.0, }; let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); + let prediction_config = PredictionConfig::default(); let interpolation_config = InterpolationConfig::default(); let mut stepper = BevyStepper::new( shared_config, diff --git a/lightyear/src/shared/replication/hierarchy.rs b/lightyear/src/shared/replication/hierarchy.rs index 3b5071377..1752e5f81 100644 --- a/lightyear/src/shared/replication/hierarchy.rs +++ b/lightyear/src/shared/replication/hierarchy.rs @@ -3,9 +3,11 @@ use bevy::ecs::entity::MapEntities; use bevy::prelude::*; use serde::{Deserialize, Serialize}; -use crate::prelude::ReplicationGroup; -use crate::shared::replication::components::Replicate; -use crate::shared::replication::ReplicationSend; +use crate::prelude::{Replicated, ReplicationGroup, VisibilityMode}; +use crate::shared::replication::components::{ + ControlledBy, Replicate, ReplicateHierarchy, ReplicationTarget, +}; +use crate::shared::replication::{ReplicationPeer, ReplicationSend}; use crate::shared::sets::{InternalMainSet, InternalReplicationSet}; /// This component can be added to an entity to replicate the entity's hierarchy to the remote world. @@ -43,22 +45,44 @@ impl HierarchySendPlugin { fn propagate_replicate( mut commands: Commands, // query the root parent of the hierarchy - parent_query: Query<(Entity, Ref), (Without, With)>, + parent_query: Query< + ( + Entity, + Ref, + &ReplicationTarget, + &ControlledBy, + &VisibilityMode, + ), + (Without, With), + >, children_query: Query<&Children>, ) { - for (parent_entity, replicate) in parent_query.iter() { - // TODO: we only want to do this if the `replicate_hierarchy` field has changed, not other fields! - // maybe use a different component? - if replicate.is_changed() && replicate.replicate_hierarchy { + for ( + parent_entity, + replicate_hierarchy, + replication_target, + controlled_by, + visibility_mode, + ) in parent_query.iter() + { + if replicate_hierarchy.is_changed() && replicate_hierarchy.recursive { // iterate through all descendents of the entity for child in children_query.iter_descendants(parent_entity) { - let mut replicate = replicate.clone(); - // the entire hierarchy is replicated as a single group, that uses the parent's entity as the group id - replicate.replication_group = ReplicationGroup::new_id(parent_entity.to_bits()); + trace!("Propagate Replicate through hierarchy: adding Replicate on child: {child:?}"); + let replicate = Replicate { + target: replication_target.clone(), + controlled_by: controlled_by.clone(), + visibility: *visibility_mode, + // the entire hierarchy is replicated as a single group, that uses the parent's entity as the group id + group: ReplicationGroup::new_id(parent_entity.to_bits()), + hierarchy: ReplicateHierarchy { recursive: true }, + }; // no need to set the correct parent as it will be set later in the `update_parent_sync` system commands.entity(child).insert((replicate, ParentSync(None))); } } + // TODO: should we update the parent's replication group? we actually can't.. replication groups + // aren't supposed to be updated } } @@ -66,7 +90,9 @@ impl HierarchySendPlugin { /// (run this in post-update before replicating, to account for any hierarchy changed initiated by the user) /// /// This only runs on the sending side - fn update_parent_sync(mut query: Query<(Ref, &mut ParentSync), With>) { + fn update_parent_sync( + mut query: Query<(Ref, &mut ParentSync), With>, + ) { for (parent, mut parent_sync) in query.iter_mut() { if parent.is_changed() || parent_sync.is_added() { trace!( @@ -84,7 +110,7 @@ impl HierarchySendPlugin { /// This only runs on the sending side fn removal_system( mut removed_parents: RemovedComponents, - mut hierarchy: Query<&mut ParentSync, With>, + mut hierarchy: Query<&mut ParentSync, With>, ) { for entity in removed_parents.read() { if let Ok(mut parent_sync) = hierarchy.get_mut(entity) { @@ -122,7 +148,7 @@ impl Default for HierarchyReceivePlugin { } } -impl HierarchyReceivePlugin { +impl HierarchyReceivePlugin { /// Update parent/children hierarchy if parent_sync changed /// /// This only runs on the receiving side @@ -130,7 +156,7 @@ impl HierarchyReceivePlugin { mut commands: Commands, hierarchy: Query< (Entity, &ParentSync, Option<&Parent>), - (Changed, Without), + (Changed, Without), >, ) { for (entity, parent_sync, parent) in hierarchy.iter() { @@ -151,7 +177,7 @@ impl HierarchyReceivePlugin { } } -impl Plugin for HierarchyReceivePlugin { +impl Plugin for HierarchyReceivePlugin { fn build(&self, app: &mut App) { // REFLECTION app.register_type::(); @@ -173,6 +199,7 @@ mod tests { use bevy::prelude::{default, Entity, With}; use crate::prelude::{Replicate, ReplicationGroup}; + use crate::shared::replication::components::ReplicateHierarchy; use crate::shared::replication::hierarchy::ParentSync; use crate::tests::protocol::*; use crate::tests::stepper::{BevyStepper, Step}; @@ -200,10 +227,10 @@ mod tests { let (mut stepper, grandparent, parent, child) = setup_hierarchy(); let replicate = Replicate { - replicate_hierarchy: false, + hierarchy: ReplicateHierarchy { recursive: false }, // make sure that child and parent are replicated in the same group, so that both entities are spawned // before entity mapping is done - replication_group: ReplicationGroup::new_id(0), + group: ReplicationGroup::new_id(0), ..default() }; stepper @@ -277,6 +304,9 @@ mod tests { #[test] fn test_propagate_hierarchy() { + // tracing_subscriber::FmtSubscriber::builder() + // .with_max_level(tracing::Level::ERROR) + // .init(); let (mut stepper, grandparent, parent, child) = setup_hierarchy(); stepper @@ -335,23 +365,17 @@ mod tests { stepper .server_app .world - .entity_mut(client_parent) - .get::(), - Some(&Replicate { - replication_group: ReplicationGroup::new_id(grandparent.to_bits()), - ..Default::default() - }) + .entity_mut(parent) + .get::(), + Some(&ReplicationGroup::new_id(grandparent.to_bits())) ); assert_eq!( stepper .server_app .world - .entity_mut(client_child) - .get::(), - Some(&Replicate { - replication_group: ReplicationGroup::new_id(grandparent.to_bits()), - ..Default::default() - }) + .entity_mut(child) + .get::(), + Some(&ReplicationGroup::new_id(grandparent.to_bits())) ); } } diff --git a/lightyear/src/shared/replication/mod.rs b/lightyear/src/shared/replication/mod.rs index 3ae858751..19320cd24 100644 --- a/lightyear/src/shared/replication/mod.rs +++ b/lightyear/src/shared/replication/mod.rs @@ -11,11 +11,12 @@ use bevy::utils::HashSet; use serde::{Deserialize, Serialize}; use bitcode::{Decode, Encode}; +use network_target::NetworkTarget; use crate::channel::builder::Channel; use crate::connection::id::ClientId; use crate::packet::message::MessageId; -use crate::prelude::{NetworkTarget, Tick}; +use crate::prelude::{ReplicationGroup, Tick}; use crate::protocol::component::{ComponentNetId, ComponentRegistry}; use crate::protocol::registry::NetId; use crate::protocol::EventContext; @@ -26,14 +27,15 @@ use crate::shared::events::connection::{ ClearEvents, IterComponentInsertEvent, IterComponentRemoveEvent, IterComponentUpdateEvent, IterEntityDespawnEvent, IterEntitySpawnEvent, }; -use crate::shared::replication::components::{Replicate, ReplicationGroupId}; -use crate::shared::replication::systems::DespawnMetadata; +use crate::shared::replication::components::{ReplicationGroupId, ReplicationTarget}; +use crate::shared::replication::systems::ReplicateCache; pub mod components; mod commands; pub mod entity_map; pub(crate) mod hierarchy; +pub mod network_target; pub(crate) mod plugin; pub(crate) mod receive; pub(crate) mod resources; @@ -105,81 +107,70 @@ pub struct ReplicationMessage { pub(crate) data: ReplicationMessageData, } -#[doc(hidden)] -/// Trait for any service that can send replication messages to the remote. -/// (this trait is used to easily enable both client to server and server to client replication) -/// -/// The trait is made public because it is needed in the macros -pub(crate) trait ReplicationSend: Resource { +/// Trait for a service that participates in replication. +pub(crate) trait ReplicationPeer: Resource { type Events: IterComponentInsertEvent + IterComponentRemoveEvent + IterComponentUpdateEvent + IterEntitySpawnEvent + IterEntityDespawnEvent + ClearEvents; - /// Type of the context associated with the events emitted by this replication plugin + /// Type of the context associated with the events emitted/received by this replication peer type EventContext: EventContext; + /// Marker to identify the type of the ReplicationSet component /// This is mostly relevant in the unified mode, where a ReplicationSet can be added several times /// (in the client and the server replication plugins) type SetMarker: Debug + Hash + Send + Sync + Eq + Clone; +} +/// Trait for a service that receives replication messages. +pub(crate) trait ReplicationReceive: Resource + ReplicationPeer { + /// The received events buffer fn events(&mut self) -> &mut Self::Events; - fn writer(&mut self) -> &mut BitcodeWriter; - - fn component_registry(&self) -> &ComponentRegistry; + /// Do some regular cleanup on the internals of replication + /// - account for tick wrapping by resetting some internal ticks for each replication group + fn cleanup(&mut self, tick: Tick); +} - /// Set the priority for a given replication group, for a given client - /// This IS the client-facing API that users must use to update the priorities for a given client. - /// - /// If multiple entities in the group have different priorities, then the latest updated priority will take precedence - fn update_priority( - &mut self, - replication_group_id: ReplicationGroupId, - client_id: ClientId, - priority: f32, - ) -> Result<()>; +#[doc(hidden)] +/// Trait for any service that can send replication messages to the remote. +/// (this trait is used to easily enable both client to server and server to client replication) +/// +/// The trait is made public because it is needed in the macros +pub(crate) trait ReplicationSend: Resource + ReplicationPeer { + fn writer(&mut self) -> &mut BitcodeWriter; /// Return the list of clients that connected to the server since we last sent any replication messages /// (this is used to send the initial state of the world to new clients) fn new_connected_clients(&self) -> Vec; - fn prepare_entity_spawn( - &mut self, - entity: Entity, - replicate: &Replicate, - target: NetworkTarget, - system_current_tick: BevyTick, - ) -> Result<()>; - fn prepare_entity_despawn( &mut self, entity: Entity, - replication_group_id: ReplicationGroupId, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()>; + #[allow(clippy::too_many_arguments)] fn prepare_component_insert( &mut self, entity: Entity, kind: ComponentNetId, component: RawData, - replicate: &Replicate, + component_registry: &ComponentRegistry, + replication_target: &ReplicationTarget, + group: &ReplicationGroup, target: NetworkTarget, - // bevy_tick for the current system run (we send component updates since the most recent bevy_tick of - // last update ack OR last action sent) - system_current_tick: BevyTick, ) -> Result<()>; fn prepare_component_remove( &mut self, entity: Entity, component_kind: ComponentNetId, - replicate: &Replicate, + group: &ReplicationGroup, target: NetworkTarget, - system_current_tick: BevyTick, ) -> Result<()>; #[allow(clippy::too_many_arguments)] @@ -188,7 +179,7 @@ pub(crate) trait ReplicationSend: Resource { entity: Entity, kind: ComponentNetId, component: RawData, - replicate: &Replicate, + group: &ReplicationGroup, target: NetworkTarget, // bevy_tick when the component changes component_change_tick: BevyTick, @@ -206,96 +197,9 @@ pub(crate) trait ReplicationSend: Resource { /// But the receiving systems might expect both components to be present at the same time. fn buffer_replication_messages(&mut self, tick: Tick, bevy_tick: BevyTick) -> Result<()>; - fn get_mut_replicate_despawn_cache(&mut self) -> &mut EntityHashMap; + fn get_mut_replicate_cache(&mut self) -> &mut EntityHashMap; /// Do some regular cleanup on the internals of replication /// - account for tick wrapping by resetting some internal ticks for each replication group fn cleanup(&mut self, tick: Tick); } - -#[cfg(test)] -mod tests { - use bevy::utils::Duration; - - use crate::prelude::client::*; - use crate::prelude::*; - use crate::tests::protocol::*; - use crate::tests::stepper::{BevyStepper, Step}; - - // An entity gets replicated from server to client, - // then a component gets removed from that entity on server, - // that component should also removed on client as well. - #[test] - fn test_simple_component_remove() -> anyhow::Result<()> { - let frame_duration = Duration::from_millis(10); - let tick_duration = Duration::from_millis(10); - let shared_config = SharedConfig { - tick: TickConfig::new(tick_duration), - ..Default::default() - }; - let link_conditioner = LinkConditionerConfig { - incoming_latency: Duration::from_millis(0), - incoming_jitter: Duration::from_millis(0), - incoming_loss: 0.0, - }; - let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); - let interpolation_config = InterpolationConfig::default(); - let mut stepper = BevyStepper::new( - shared_config, - sync_config, - prediction_config, - interpolation_config, - link_conditioner, - frame_duration, - ); - stepper.init(); - - // Create an entity on server - let server_entity = stepper - .server_app - .world - .spawn((Component1(0.0), Replicate::default())) - .id(); - // we need to step twice because we run client before server - stepper.frame_step(); - stepper.frame_step(); - - // Check that the entity is replicated to client - let client_entity = *stepper - .client_app - .world - .resource::() - .replication_receiver - .remote_entity_map - .get_local(server_entity) - .unwrap(); - assert_eq!( - stepper - .client_app - .world - .entity(client_entity) - .get::() - .unwrap(), - &Component1(0.0) - ); - - // Remove the component on the server - stepper - .server_app - .world - .entity_mut(server_entity) - .remove::(); - stepper.frame_step(); - stepper.frame_step(); - - // Check that this removal was replicated - assert!(stepper - .client_app - .world - .entity(client_entity) - .get::() - .is_none()); - Ok(()) - } -} diff --git a/lightyear/src/shared/replication/network_target.rs b/lightyear/src/shared/replication/network_target.rs new file mode 100644 index 000000000..709333dc6 --- /dev/null +++ b/lightyear/src/shared/replication/network_target.rs @@ -0,0 +1,420 @@ +use crate::prelude::ClientId; +use bevy::prelude::Reflect; +use bevy::utils::HashSet; +use bitcode::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Reflect, Encode, Decode)] +/// NetworkTarget indicated which clients should receive some message +pub enum NetworkTarget { + #[default] + /// Message sent to no client + None, + /// Message sent to all clients except one + AllExceptSingle(ClientId), + /// Message sent to all clients except for these + AllExcept(Vec), + /// Message sent to all clients + All, + /// Message sent to only these + Only(Vec), + /// Message sent to only this one client + Single(ClientId), +} + +impl Extend for NetworkTarget { + fn extend>(&mut self, iter: T) { + self.union(&iter.into_iter().collect::()); + } +} + +impl FromIterator for NetworkTarget { + fn from_iter>(iter: T) -> Self { + let clients: Vec = iter.into_iter().collect(); + NetworkTarget::from(clients) + } +} + +impl From> for NetworkTarget { + fn from(value: Vec) -> Self { + match value.len() { + 0 => NetworkTarget::None, + 1 => NetworkTarget::Single(value[0]), + _ => NetworkTarget::Only(value), + } + } +} + +impl NetworkTarget { + /// Returns true if the target is empty + pub fn is_empty(&self) -> bool { + match self { + NetworkTarget::None => true, + NetworkTarget::Only(ids) => ids.is_empty(), + _ => false, + } + } + + pub fn from_exclude(client_ids: impl IntoIterator) -> Self { + let client_ids = client_ids.into_iter().collect::>(); + match client_ids.len() { + 0 => NetworkTarget::All, + 1 => NetworkTarget::AllExceptSingle(client_ids[0]), + _ => NetworkTarget::AllExcept(client_ids), + } + } + + /// Return true if we should replicate to the specified client + pub fn targets(&self, client_id: &ClientId) -> bool { + match self { + NetworkTarget::All => true, + NetworkTarget::AllExceptSingle(single) => client_id != single, + NetworkTarget::AllExcept(client_ids) => !client_ids.contains(client_id), + NetworkTarget::Only(client_ids) => client_ids.contains(client_id), + NetworkTarget::Single(single) => client_id == single, + NetworkTarget::None => false, + } + } + + /// Compute the intersection of this target with another one (A ∩ B) + pub(crate) fn intersection(&mut self, target: &NetworkTarget) { + match self { + NetworkTarget::All => { + *self = target.clone(); + } + // TODO: write the implementation by hand as an optimization! + NetworkTarget::AllExceptSingle(existing_client_id) => { + let mut a = NetworkTarget::AllExcept(vec![*existing_client_id]); + a.intersection(target); + *self = a; + } + NetworkTarget::AllExcept(existing_client_ids) => match target { + NetworkTarget::None => { + *self = NetworkTarget::None; + } + NetworkTarget::AllExceptSingle(target_client_id) => { + let mut new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); + new_excluded_ids.insert(*target_client_id); + *existing_client_ids = Vec::from_iter(new_excluded_ids); + } + NetworkTarget::AllExcept(target_client_ids) => { + let mut new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); + target_client_ids.iter().for_each(|id| { + new_excluded_ids.insert(*id); + }); + *existing_client_ids = Vec::from_iter(new_excluded_ids); + } + NetworkTarget::All => {} + NetworkTarget::Only(target_client_ids) => { + let mut new_included_ids = HashSet::from_iter(target_client_ids.clone()); + existing_client_ids.iter_mut().for_each(|id| { + new_included_ids.remove(id); + }); + *self = NetworkTarget::Only(Vec::from_iter(new_included_ids)); + } + NetworkTarget::Single(target_client_id) => { + if existing_client_ids.contains(target_client_id) { + *self = NetworkTarget::None; + } else { + *self = NetworkTarget::Single(*target_client_id); + } + } + }, + NetworkTarget::Only(existing_client_ids) => match target { + NetworkTarget::None => { + *self = NetworkTarget::None; + } + NetworkTarget::AllExceptSingle(target_client_id) => { + let mut new_included_ids = HashSet::from_iter(existing_client_ids.clone()); + new_included_ids.remove(target_client_id); + *self = NetworkTarget::from(Vec::from_iter(new_included_ids)); + } + NetworkTarget::AllExcept(target_client_ids) => { + let mut new_included_ids = HashSet::from_iter(existing_client_ids.clone()); + target_client_ids.iter().for_each(|id| { + new_included_ids.remove(id); + }); + *self = NetworkTarget::from(Vec::from_iter(new_included_ids)); + } + NetworkTarget::All => {} + NetworkTarget::Single(target_client_id) => { + if existing_client_ids.contains(target_client_id) { + *self = NetworkTarget::Single(*target_client_id); + } else { + *self = NetworkTarget::None; + } + } + NetworkTarget::Only(target_client_ids) => { + let new_included_ids = HashSet::from_iter(existing_client_ids.clone()); + let target_included_ids = HashSet::from_iter(target_client_ids.clone()); + let intersection = new_included_ids.intersection(&target_included_ids).cloned(); + *self = NetworkTarget::from(intersection.collect::>()); + } + }, + NetworkTarget::Single(existing_client_id) => { + if !target.targets(existing_client_id) { + *self = NetworkTarget::None; + } + } + NetworkTarget::None => {} + } + } + + /// Compute the union of this target with another one (A U B) + pub(crate) fn union(&mut self, target: &NetworkTarget) { + match self { + NetworkTarget::All => {} + NetworkTarget::AllExceptSingle(existing_client_id) => { + if target.targets(existing_client_id) { + *self = NetworkTarget::All; + } + } + NetworkTarget::AllExcept(existing_client_ids) => match target { + NetworkTarget::None => {} + NetworkTarget::AllExceptSingle(target_client_id) => { + if existing_client_ids.contains(target_client_id) { + *self = NetworkTarget::AllExceptSingle(*target_client_id); + } else { + *self = NetworkTarget::All; + } + } + NetworkTarget::AllExcept(target_client_ids) => { + let new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); + let target_excluded_ids = HashSet::from_iter(target_client_ids.clone()); + let intersection = new_excluded_ids + .intersection(&target_excluded_ids) + .copied() + .collect(); + *existing_client_ids = intersection; + } + NetworkTarget::All => { + *self = NetworkTarget::All; + } + NetworkTarget::Only(target_client_ids) => { + let mut new_excluded_ids = HashSet::from_iter(existing_client_ids.clone()); + target_client_ids.iter().for_each(|id| { + new_excluded_ids.remove(id); + }); + *self = NetworkTarget::from_exclude(new_excluded_ids) + } + NetworkTarget::Single(target_client_id) => { + existing_client_ids.retain(|id| id != target_client_id); + } + }, + NetworkTarget::Only(existing_client_ids) => match target { + NetworkTarget::None => {} + NetworkTarget::AllExceptSingle(target_client_id) => { + if existing_client_ids.contains(target_client_id) { + *self = NetworkTarget::All; + } else { + *self = NetworkTarget::AllExceptSingle(*target_client_id); + } + } + NetworkTarget::AllExcept(target_client_ids) => { + let mut target_excluded_ids = HashSet::from_iter(target_client_ids.clone()); + existing_client_ids.iter().for_each(|id| { + target_excluded_ids.remove(id); + }); + match target_excluded_ids.len() { + 0 => { + *self = NetworkTarget::All; + } + 1 => { + *self = NetworkTarget::AllExceptSingle( + *target_excluded_ids.iter().next().unwrap(), + ); + } + _ => { + *self = NetworkTarget::AllExcept(Vec::from_iter(target_excluded_ids)); + } + } + } + NetworkTarget::All => { + *self = NetworkTarget::All; + } + NetworkTarget::Single(target_client_id) => { + if !existing_client_ids.contains(target_client_id) { + existing_client_ids.push(*target_client_id); + } + } + NetworkTarget::Only(target_client_ids) => { + let new_included_ids = HashSet::from_iter(existing_client_ids.clone()); + let target_included_ids = HashSet::from_iter(target_client_ids.clone()); + let union = new_included_ids.union(&target_included_ids); + *existing_client_ids = union.into_iter().copied().collect::>(); + } + }, + NetworkTarget::Single(existing_client_id) => match target { + NetworkTarget::None => {} + NetworkTarget::AllExceptSingle(target_client_id) => { + if existing_client_id == target_client_id { + *self = NetworkTarget::All; + } else { + *self = NetworkTarget::AllExceptSingle(*target_client_id); + } + } + NetworkTarget::AllExcept(target_client_ids) => { + let mut new_excluded = target_client_ids.clone(); + new_excluded.retain(|id| id != existing_client_id); + *self = NetworkTarget::from_exclude(new_excluded); + } + NetworkTarget::All => { + *self = NetworkTarget::All; + } + NetworkTarget::Only(target_client_ids) => { + let mut new_targets = HashSet::from_iter(target_client_ids.clone()); + new_targets.insert(*existing_client_id); + *self = NetworkTarget::from(Vec::from_iter(new_targets)); + } + NetworkTarget::Single(target_client_id) => { + if existing_client_id != target_client_id { + *self = NetworkTarget::Only(vec![*existing_client_id, *target_client_id]); + } + } + }, + NetworkTarget::None => { + *self = target.clone(); + } + } + } + + /// Compute the inverse of this target (¬A) + pub(crate) fn inverse(&mut self) { + match self { + NetworkTarget::All => { + *self = NetworkTarget::None; + } + NetworkTarget::AllExceptSingle(client_id) => { + *self = NetworkTarget::Single(*client_id); + } + NetworkTarget::AllExcept(client_ids) => { + *self = NetworkTarget::Only(client_ids.clone()); + } + NetworkTarget::Only(client_ids) => { + *self = NetworkTarget::AllExcept(client_ids.clone()); + } + NetworkTarget::Single(client_id) => { + *self = NetworkTarget::AllExceptSingle(*client_id); + } + NetworkTarget::None => { + *self = NetworkTarget::All; + } + } + } + + /// Compute the difference of this target with another one (A - B) + pub(crate) fn exclude(&mut self, target: &NetworkTarget) { + let mut target = target.clone(); + target.inverse(); + self.intersection(&target); + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::ClientId; + use crate::shared::replication::network_target::NetworkTarget; + + #[test] + fn test_exclude() { + let client_0 = ClientId::Netcode(0); + let client_1 = ClientId::Netcode(1); + let client_2 = ClientId::Netcode(2); + let mut target = NetworkTarget::All; + assert!(target.targets(&client_0)); + target.exclude(&NetworkTarget::Only(vec![client_1, client_2])); + assert_eq!(target, NetworkTarget::AllExcept(vec![client_1, client_2])); + + target = NetworkTarget::AllExcept(vec![client_0]); + assert!(!target.targets(&client_0)); + assert!(target.targets(&client_1)); + target.exclude(&NetworkTarget::Only(vec![client_0, client_1])); + assert!(matches!(target, NetworkTarget::AllExcept(_))); + + if let NetworkTarget::AllExcept(ids) = target { + assert!(ids.contains(&client_0)); + assert!(ids.contains(&client_1)); + } + + target = NetworkTarget::Only(vec![client_0]); + assert!(target.targets(&client_0)); + assert!(!target.targets(&client_1)); + target.exclude(&NetworkTarget::Single(client_1)); + assert_eq!(target, NetworkTarget::Single(client_0)); + target.exclude(&NetworkTarget::Only(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::None); + + target = NetworkTarget::None; + assert!(!target.targets(&client_0)); + target.exclude(&NetworkTarget::Single(client_1)); + assert_eq!(target, NetworkTarget::None); + } + + #[test] + fn test_intersection() { + let client_0 = ClientId::Netcode(0); + let client_1 = ClientId::Netcode(1); + let client_2 = ClientId::Netcode(2); + let mut target = NetworkTarget::All; + target.intersection(&NetworkTarget::AllExcept(vec![client_1, client_2])); + assert_eq!(target, NetworkTarget::AllExcept(vec![client_1, client_2])); + + target = NetworkTarget::AllExcept(vec![client_0]); + target.intersection(&NetworkTarget::AllExcept(vec![client_0, client_1])); + assert!(matches!(target, NetworkTarget::AllExcept(_))); + + if let NetworkTarget::AllExcept(ids) = target { + assert!(ids.contains(&client_0)); + assert!(ids.contains(&client_1)); + } + + target = NetworkTarget::AllExcept(vec![client_0, client_1]); + target.intersection(&NetworkTarget::Only(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::Only(vec![client_2])); + + target = NetworkTarget::Only(vec![client_0, client_1]); + target.intersection(&NetworkTarget::Only(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::Single(client_0)); + + target = NetworkTarget::Only(vec![client_0, client_1]); + target.intersection(&NetworkTarget::AllExcept(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::Single(client_1)); + + target = NetworkTarget::None; + target.intersection(&NetworkTarget::AllExcept(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::None); + } + + #[test] + fn test_union() { + let client_0 = ClientId::Netcode(0); + let client_1 = ClientId::Netcode(1); + let client_2 = ClientId::Netcode(2); + let mut target = NetworkTarget::All; + target.union(&NetworkTarget::AllExcept(vec![client_1, client_2])); + assert_eq!(target, NetworkTarget::All); + + target = NetworkTarget::AllExcept(vec![client_0]); + target.union(&NetworkTarget::Only(vec![client_0, client_1])); + assert_eq!(target, NetworkTarget::All); + + target = NetworkTarget::AllExcept(vec![client_0, client_1]); + target.union(&NetworkTarget::Only(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::AllExceptSingle(client_1)); + + target = NetworkTarget::Only(vec![client_0, client_1]); + target.union(&NetworkTarget::Only(vec![client_0, client_2])); + assert!(matches!(target, NetworkTarget::Only(_))); + assert!(target.targets(&client_0)); + assert!(target.targets(&client_1)); + assert!(target.targets(&client_2)); + + target = NetworkTarget::Only(vec![client_0, client_1]); + target.union(&NetworkTarget::AllExcept(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::AllExceptSingle(client_2)); + + target = NetworkTarget::None; + target.union(&NetworkTarget::AllExcept(vec![client_0, client_2])); + assert_eq!(target, NetworkTarget::AllExcept(vec![client_0, client_2])); + } +} diff --git a/lightyear/src/shared/replication/plugin.rs b/lightyear/src/shared/replication/plugin.rs index e10f6293c..8ce0be614 100644 --- a/lightyear/src/shared/replication/plugin.rs +++ b/lightyear/src/shared/replication/plugin.rs @@ -1,73 +1,80 @@ -use bevy::prelude::*; -use bevy::time::common_conditions::on_timer; -use bevy::utils::Duration; - -use crate::prelude::{ - NetworkTarget, PrePredicted, RemoteEntityMap, ReplicationGroup, ReplicationMode, - ShouldBePredicted, -}; -use crate::shared::replication::components::{ - PerComponentReplicationMetadata, Replicate, ReplicationGroupId, ReplicationGroupIdBuilder, - ShouldBeInterpolated, TargetEntity, -}; -use crate::shared::replication::entity_map::{InterpolatedEntityMap, PredictedEntityMap}; +//! This module contains the `ReplicationReceivePlugin` and `ReplicationSendPlugin` plugins, which control +//! the replication of entities and resources. +//! use crate::shared::replication::hierarchy::{HierarchyReceivePlugin, HierarchySendPlugin}; use crate::shared::replication::resources::{ receive::ResourceReceivePlugin, send::ResourceSendPlugin, }; -use crate::shared::replication::systems::{add_replication_send_systems, cleanup}; -use crate::shared::replication::ReplicationSend; +use crate::shared::replication::systems; +use crate::shared::replication::{ReplicationReceive, ReplicationSend}; use crate::shared::sets::{InternalMainSet, InternalReplicationSet, MainSet}; +use bevy::prelude::*; +use bevy::time::common_conditions::on_timer; +use bevy::utils::Duration; -pub(crate) struct ReplicationPlugin { - tick_duration: Duration, - enable_send: bool, - enable_receive: bool, - _marker: std::marker::PhantomData, -} +pub(crate) mod receive { + use super::*; + pub(crate) struct ReplicationReceivePlugin { + clean_interval: Duration, + _marker: std::marker::PhantomData, + } -impl ReplicationPlugin { - pub(crate) fn new(tick_duration: Duration, enable_send: bool, enable_receive: bool) -> Self { - Self { - tick_duration, - enable_send, - enable_receive, - _marker: std::marker::PhantomData, + impl ReplicationReceivePlugin { + pub(crate) fn new(tick_interval: Duration) -> Self { + Self { + // TODO: find a better constant for the clean interval? + clean_interval: tick_interval * (i16::MAX as u32 / 3), + _marker: std::marker::PhantomData, + } + } + } + + impl Plugin for ReplicationReceivePlugin { + fn build(&self, app: &mut App) { + // PLUGINS + if !app.is_plugin_added::() { + app.add_plugins(shared::SharedPlugin); + } + app.add_plugins(HierarchyReceivePlugin::::default()) + .add_plugins(ResourceReceivePlugin::::default()); + + // SYSTEMS + app.add_systems( + Last, + systems::receive_cleanup::.run_if(on_timer(self.clean_interval)), + ); } } } -impl Plugin for ReplicationPlugin { - fn build(&self, app: &mut App) { - // TODO: have a better constant for clean_interval? - let clean_interval = self.tick_duration * (i16::MAX as u32 / 3); +pub(crate) mod send { + use super::*; + use crate::prelude::server::ServerReplicationSet; - // REFLECTION - app.register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::(); + pub(crate) struct ReplicationSendPlugin { + clean_interval: Duration, + _marker: std::marker::PhantomData, + } + impl ReplicationSendPlugin { + pub(crate) fn new(tick_interval: Duration) -> Self { + Self { + // TODO: find a better constant for the clean interval? + clean_interval: tick_interval * (i16::MAX as u32 / 3), + _marker: std::marker::PhantomData, + } + } + } - // TODO: should we put this back into enable_receive? - app.add_plugins(ResourceReceivePlugin::::default()); - app.add_plugins(ResourceSendPlugin::::default()); - // SYSTEM SETS // - if self.enable_receive { + impl Plugin for ReplicationSendPlugin { + fn build(&self, app: &mut App) { // PLUGINS - app.add_plugins(HierarchyReceivePlugin::::default()); - // app.add_plugins(ResourceReceivePlugin::::default()); - } - if self.enable_send { + if !app.is_plugin_added::() { + app.add_plugins(shared::SharedPlugin); + } + app.add_plugins(ResourceSendPlugin::::default()) + .add_plugins(HierarchySendPlugin::::default()); + + // SETS app.configure_sets( PostUpdate, ( @@ -75,15 +82,14 @@ impl Plugin for ReplicationPlugin { InternalMainSet::::Send.in_set(MainSet::Send), ), ); - // NOTE: it's ok to run the replication systems less frequently than every frame - // because bevy's change detection detects changes since the last time the system ran (not since the last frame) app.configure_sets( PostUpdate, ( ( - InternalReplicationSet::::HandleReplicateUpdate, + InternalReplicationSet::::BeforeBuffer, InternalReplicationSet::::BufferResourceUpdates, InternalReplicationSet::::Buffer, + InternalReplicationSet::::AfterBuffer, ) .in_set(InternalReplicationSet::::All), ( @@ -100,30 +106,88 @@ impl Plugin for ReplicationPlugin { // because Removed is cleared every frame? // NOTE: HandleReplicateUpdate should also run every frame? // NOTE: BufferDespawnsAndRemovals is not in MainSet::Send because we need to run them every frame + InternalReplicationSet::::AfterBuffer, ) .in_set(InternalMainSet::::Send), ( - InternalReplicationSet::::HandleReplicateUpdate, - InternalReplicationSet::::Buffer, + ( + ( + InternalReplicationSet::::BeforeBuffer, + InternalReplicationSet::::Buffer, + InternalReplicationSet::::AfterBuffer, + ) + .chain(), + InternalReplicationSet::::BufferResourceUpdates, + ), InternalMainSet::::SendPackets, ) .chain(), + ), + ); + // SYSTEMS + app.add_systems( + PreUpdate, + // we need to add despawn trackers immediately for entities for which we add replicate + // TODO: why? + systems::handle_replicate_add::.after(ServerReplicationSet::ClientReplication), + ); + app.add_systems( + PostUpdate, + ( + // NOTE: we need to run `send_entity_despawn` once per frame (and not once per send_interval) + // because the RemovedComponents Events are present only for 1 frame and we might miss them if we don't run this every frame + // It is ok to run it every frame because it creates at most one message per despawn + // NOTE: we make sure to update the replicate_cache before we make use of it in `send_entity_despawn` + (systems::handle_replicate_remove::,) + .in_set(InternalReplicationSet::::BeforeBuffer), + systems::send_entity_despawn:: + .in_set(InternalReplicationSet::::BufferDespawnsAndRemovals), ( - InternalReplicationSet::::BufferResourceUpdates, - InternalMainSet::::SendPackets, + systems::handle_replicate_add::, + systems::handle_replication_target_update::, ) - .chain(), + .in_set(InternalReplicationSet::::AfterBuffer), ), ); - // SYSTEMS - add_replication_send_systems::(app); - // PLUGINS - app.add_plugins(HierarchySendPlugin::::default()); - // app.add_plugins(ResourceSendPlugin::::default()); + app.add_systems( + Last, + systems::send_cleanup::.run_if(on_timer(self.clean_interval)), + ); } + } +} + +pub(crate) mod shared { + use crate::prelude::{ + PrePredicted, RemoteEntityMap, Replicate, ReplicationGroup, ShouldBePredicted, + TargetEntity, VisibilityMode, + }; + use crate::shared::replication::components::{ + ReplicationGroupId, ReplicationGroupIdBuilder, ShouldBeInterpolated, + }; + use crate::shared::replication::entity_map::{InterpolatedEntityMap, PredictedEntityMap}; + use crate::shared::replication::network_target::NetworkTarget; + use bevy::prelude::{App, Plugin}; - // TODO: split receive cleanup from send cleanup - // cleanup is for both receive and send - app.add_systems(Last, cleanup::.run_if(on_timer(clean_interval))); + pub(crate) struct SharedPlugin; + + impl Plugin for SharedPlugin { + fn build(&self, app: &mut App) { + // REFLECTION + app.register_type::() + .register_type::() + // .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::(); + } } } diff --git a/lightyear/src/shared/replication/receive.rs b/lightyear/src/shared/replication/receive.rs index def607251..1aa001e7c 100644 --- a/lightyear/src/shared/replication/receive.rs +++ b/lightyear/src/shared/replication/receive.rs @@ -168,6 +168,25 @@ impl ReplicationReceiver { .get(&remote_entity) .and_then(|group_id| self.group_channels.get(group_id)) } + + /// Do some internal bookkeeping: + /// - handle tick wrapping + pub(crate) fn cleanup(&mut self, tick: Tick) { + // if it's been enough time since we last had any update for the group, we update the latest_tick for the group + for group_channel in self.group_channels.values_mut() { + debug!("Checking group channel: {:?}", group_channel); + if let Some(latest_tick) = group_channel.latest_tick { + if tick - latest_tick > (i16::MAX / 2) { + debug!( + ?tick, + ?latest_tick, + ?group_channel, + "Setting the latest_tick tick to tick because there hasn't been any new updates in a while"); + group_channel.latest_tick = Some(tick); + } + } + } + } } /// We want: @@ -240,11 +259,12 @@ impl ReplicationReceiver { } SpawnAction::Reuse(local_entity) => { let local_entity = Entity::from_bits(local_entity); - if world.get_entity(local_entity).is_none() { + let Some(mut entity_mut) = world.get_entity_mut(local_entity) else { // TODO: ignore the entity in the next steps because it does not exist! error!("Received ReuseEntity({local_entity:?}) but the entity does not exist in the world"); continue; }; + entity_mut.insert(Replicated); // update the entity mapping self.remote_entity_map.insert(*remote_entity, local_entity); } diff --git a/lightyear/src/shared/replication/resources.rs b/lightyear/src/shared/replication/resources.rs index 0ecaa2568..f4fac71b5 100644 --- a/lightyear/src/shared/replication/resources.rs +++ b/lightyear/src/shared/replication/resources.rs @@ -7,19 +7,16 @@ use bevy::app::App; use bevy::ecs::entity::MapEntities; use bevy::ecs::system::Command; use bevy::prelude::{ - Commands, Component, DetectChanges, Entity, EntityMapper, IntoSystemConfigs, - IntoSystemSetConfigs, Plugin, PostUpdate, PreUpdate, Query, Ref, Res, ResMut, Resource, - SystemSet, With, World, + Commands, Component, DetectChanges, EntityMapper, IntoSystemConfigs, IntoSystemSetConfigs, + Plugin, PostUpdate, PreUpdate, Res, ResMut, Resource, SystemSet, }; +pub use command::{ReplicateResourceExt, StopReplicateResourceExt}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use tracing::error; - -pub use command::{ReplicateResourceExt, StopReplicateResourceExt}; -use crate::prelude::{ChannelKind, Message, NetworkTarget}; +use crate::prelude::{ChannelKind, Message}; use crate::protocol::BitSerializable; -use crate::shared::replication::components::Replicate; +use crate::shared::replication::network_target::NetworkTarget; use crate::shared::replication::ReplicationSend; use crate::shared::sets::{InternalMainSet, InternalReplicationSet}; @@ -173,7 +170,7 @@ pub(crate) mod send { ); let mut target = replication_resource.target.clone(); // no need to send a duplicate message to new clients - target.exclude(new_clients); + target.exclude(&NetworkTarget::Only(new_clients)); let _ = connection_manager.erased_send_message_to_target( resource.as_ref(), replication_resource.channel, @@ -194,6 +191,7 @@ pub(crate) mod receive { }; use crate::shared::message::MessageSend; use crate::shared::plugin::Identity; + use crate::shared::replication::ReplicationPeer; use bevy::prelude::{DetectChangesMut, EventReader, Events, RemovedComponents}; use tracing::{debug, trace}; @@ -211,7 +209,7 @@ pub(crate) mod receive { } } - impl Plugin for ResourceReceivePlugin { + impl Plugin for ResourceReceivePlugin { fn build(&self, app: &mut App) { app.configure_sets( PreUpdate, @@ -313,19 +311,13 @@ pub(crate) mod receive { #[cfg(test)] mod tests { - use bevy::ecs::system::RunSystemOnce; - use bevy::prelude::{apply_deferred, Commands, OnEnter, Resource}; - use serde::{Deserialize, Serialize}; - use std::marker::PhantomData; - use tracing::error; - - use crate::prelude::client::NetworkingState; - use crate::prelude::{AppComponentExt, NetworkTarget, Replicate}; + use crate::shared::replication::network_target::NetworkTarget; use crate::shared::replication::resources::ReplicateResourceExt; - use crate::tests::protocol::{Channel1, Component1, Resource1, Resource2}; + use crate::tests::protocol::{Channel1, Resource1, Resource2}; use crate::tests::stepper::{BevyStepper, Step}; + use bevy::prelude::Commands; - use super::{ReplicateResourceMetadata, StopReplicateResourceExt}; + use super::StopReplicateResourceExt; #[test] fn test_resource_replication_via_commands() { diff --git a/lightyear/src/shared/replication/send.rs b/lightyear/src/shared/replication/send.rs index 006d47875..ed0beb09c 100644 --- a/lightyear/src/shared/replication/send.rs +++ b/lightyear/src/shared/replication/send.rs @@ -18,7 +18,7 @@ use crate::protocol::component::ComponentNetId; use crate::protocol::registry::NetId; use crate::serialize::RawData; use crate::shared::replication::components::{Replicate, ReplicationGroupId}; -use crate::shared::replication::systems::DespawnMetadata; +use crate::shared::replication::systems::ReplicateCache; use super::{ EntityActionMessage, EntityActions, EntityUpdatesMessage, ReplicationMessageData, SpawnAction, @@ -128,6 +128,26 @@ impl ReplicationSender { } } } + + /// Do some internal bookkeeping: + /// - handle tick wrapping + pub(crate) fn cleanup(&mut self, tick: Tick) { + // if it's been enough time since we last any action for the group, we can set the last_action_tick to None + // (meaning that there's no need when we receive the update to check if we have already received a previous action) + for group_channel in self.group_channels.values_mut() { + debug!("Checking group channel: {:?}", group_channel); + if let Some(last_action_tick) = group_channel.last_action_tick { + if tick - last_action_tick > (i16::MAX / 2) { + debug!( + ?tick, + ?last_action_tick, + ?group_channel, + "Setting the last_action tick to None because there hasn't been any new actions in a while"); + group_channel.last_action_tick = None; + } + } + } + } } /// We want: diff --git a/lightyear/src/shared/replication/systems.rs b/lightyear/src/shared/replication/systems.rs index 47c5efe0b..e95c0c251 100644 --- a/lightyear/src/shared/replication/systems.rs +++ b/lightyear/src/shared/replication/systems.rs @@ -5,46 +5,51 @@ use std::ops::Deref; use bevy::ecs::entity::Entities; use bevy::ecs::system::SystemChangeTick; use bevy::prelude::{ - Added, App, Commands, Component, DetectChanges, Entity, IntoSystemConfigs, Mut, PostUpdate, - PreUpdate, Query, Ref, RemovedComponents, Res, ResMut, With, Without, + Added, App, Changed, Commands, Component, DetectChanges, Entity, Has, IntoSystemConfigs, Mut, + PostUpdate, PreUpdate, Query, Ref, RemovedComponents, Res, ResMut, With, Without, }; use tracing::{debug, error, info, trace, warn}; -use crate::client::replication::ClientReplicationPlugin; -use crate::prelude::{ClientId, NetworkTarget, ReplicationGroup, ShouldBePredicted, TickManager}; -use crate::protocol::component::ComponentRegistry; +use crate::prelude::{ClientId, ReplicationGroup, ShouldBePredicted, TargetEntity, TickManager}; +use crate::protocol::component::{ComponentNetId, ComponentRegistry}; +use crate::serialize::RawData; use crate::server::replication::ServerReplicationSet; -use crate::server::room::ClientVisibility; +use crate::server::visibility::immediate::{ClientVisibility, ReplicateVisibility}; use crate::shared::replication::components::{ - DespawnTracker, Replicate, ReplicateVisibility, ReplicationGroupId, ReplicationMode, + DespawnTracker, DisabledComponent, OverrideTargetComponent, Replicate, ReplicateOnceComponent, + ReplicationGroupId, ReplicationTarget, VisibilityMode, }; -use crate::shared::replication::ReplicationSend; +use crate::shared::replication::network_target::NetworkTarget; +use crate::shared::replication::{ReplicationReceive, ReplicationSend}; use crate::shared::sets::{InternalMainSet, InternalReplicationSet}; // TODO: replace this with observers -/// Metadata that holds Replicate-information (so that when the entity is despawned we know -/// how to replicate the despawn) -pub(crate) struct DespawnMetadata { - replication_target: NetworkTarget, - replication_group: ReplicationGroup, - replication_mode: ReplicationMode, +/// Metadata that holds Replicate-information from the previous send_interval's replication. +/// - when the entity gets despawned, we will use this to know how to replicate the despawn +/// - when the replicate metadata changes, we will use this to compute diffs +#[derive(PartialEq, Debug)] +pub(crate) struct ReplicateCache { + pub(crate) replication_target: NetworkTarget, + pub(crate) replication_group: ReplicationGroup, + pub(crate) visibility_mode: VisibilityMode, /// If mode = Room, the list of clients that could see the entity pub(crate) replication_clients_cache: Vec, } -/// For every entity that removes their Replicate component but are not despawned, remove the component +/// For every entity that removes their ReplicationTarget component but are not despawned, remove the component /// from our replicate cache (so that the entity's despawns are no longer replicated) -fn handle_replicate_remove( - mut commands: Commands, +pub(crate) fn handle_replicate_remove( + // mut commands: Commands, mut sender: ResMut, - mut query: RemovedComponents, + mut query: RemovedComponents, entity_check: &Entities, ) { for entity in query.read() { if entity_check.contains(entity) { debug!("handling replicate component remove (delete from cache)"); - sender.get_mut_replicate_despawn_cache().remove(&entity); - commands.entity(entity).remove::(); + sender.get_mut_replicate_cache().remove(&entity); + // TODO: should we also remove the replicate-visibility? or should we keep it? + // commands.entity(entity).remove::(); } } } @@ -57,206 +62,141 @@ fn handle_replicate_remove( pub(crate) fn handle_replicate_add( mut sender: ResMut, mut commands: Commands, - query: Query<(Entity, &Replicate), (Added, Without)>, + // We use `(With, Without)` as an optimization to + // only get the subset of entities that have had Replicate added + // (`Added` queries through each entity that has `Replicate`) + query: Query< + ( + Entity, + &ReplicationTarget, + &ReplicationGroup, + &VisibilityMode, + ), + (With, Without), + >, ) { - for (entity, replicate) in query.iter() { - debug!("Replicate component was added"); + for (entity, replication_target, group, visibility_mode) in query.iter() { + debug!("Replicate component was added for entity {entity:?}"); commands.entity(entity).insert(DespawnTracker); - let despawn_metadata = DespawnMetadata { - replication_target: replicate.replication_target.clone(), - replication_group: replicate.replication_group, - replication_mode: replicate.replication_mode, + let despawn_metadata = ReplicateCache { + replication_target: replication_target.replication.clone(), + replication_group: *group, + visibility_mode: *visibility_mode, replication_clients_cache: vec![], }; sender - .get_mut_replicate_despawn_cache() + .get_mut_replicate_cache() .insert(entity, despawn_metadata); - if replicate.replication_mode == ReplicationMode::Room { - commands - .entity(entity) - .insert(ReplicateVisibility::default()); + } +} + +/// Update the replication_target in the cache when the ReplicationTarget component changes +pub(crate) fn handle_replication_target_update( + mut sender: ResMut, + target_query: Query< + (Entity, Ref), + (Changed, With), + >, +) { + for (entity, replication_target) in target_query.iter() { + if replication_target.is_changed() && !replication_target.is_added() { + if let Some(replicate_cache) = sender.get_mut_replicate_cache().get_mut(&entity) { + replicate_cache.replication_target = replication_target.replication.clone(); + } } } } -fn send_entity_despawn( - query: Query<(Entity, &Replicate, Option<&ReplicateVisibility>)>, - system_bevy_ticks: SystemChangeTick, +// TODO: also send despawn if the target changed? +pub(crate) fn send_entity_despawn( + query: Query<( + Entity, + Ref, + &ReplicationGroup, + Option<&ReplicateVisibility>, + )>, // TODO: ideally we want to send despawns for entities that still had REPLICATE at the time of despawn // not just entities that had despawn tracker once mut despawn_removed: RemovedComponents, mut sender: ResMut, ) { - // Despawn entities for clients that lost visibility - query.iter().for_each(|(entity, replicate, visibility)| { - if matches!(replicate.replication_mode, ReplicationMode::Room) { - visibility - .unwrap() - .clients_cache - .iter() - .for_each(|(client_id, visibility)| { - if replicate.replication_target.should_send_to(client_id) - && matches!(visibility, ClientVisibility::Lost) - { - debug!("sending entity despawn for entity: {:?}", entity); - // TODO: don't unwrap but handle errors - let group_id = replicate.replication_group.group_id(Some(entity)); - let _ = sender - .prepare_entity_despawn( - entity, - group_id, - NetworkTarget::Only(vec![*client_id]), - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending entity despawn: {:?}", e); - }); - } - }); - } - }); + // Send entity-despawn for entities that still exist for clients that lost visibility + query + .iter() + .for_each(|(entity, replication_target, group, visibility)| { + let mut target: NetworkTarget = match visibility { + Some(visibility) => { + // send despawn for clients that lost visibility + visibility + .clients_cache + .iter() + .filter_map(|(client_id, visibility)| { + if replication_target.replication.targets(client_id) + && matches!(visibility, ClientVisibility::Lost) { + debug!( + "sending entity despawn for entity: {:?} because ClientVisibility::Lost", + entity + ); + return Some(*client_id); + } + None + }).collect() + } + None => { + NetworkTarget::None + } + }; + // if the replication target changed, find the clients that were removed in the new replication target + if replication_target.is_changed() && !replication_target.is_added() { + if let Some(cache) = sender.get_mut_replicate_cache().get_mut(&entity) { + let mut new_despawn = cache.replication_target.clone(); + new_despawn.exclude(&replication_target.replication); + target.union(&new_despawn); + } + } + if !target.is_empty() { + let _ = sender + .prepare_entity_despawn( + entity, + group, + target + ) + .inspect_err(|e| { + error!("error sending entity despawn: {:?}", e); + }); + } + }); - // TODO: check for banned replicate component? - // Despawn entities when the entity got despawned on local world + // Despawn entities when the entity gets despawned on local world for entity in despawn_removed.read() { - trace!("despawn tracker removed!"); + trace!("DespawnTracker component got removed, preparing entity despawn message!"); // TODO: we still don't want to replicate the despawn if the entity was not in the same room as the client! // only replicate the despawn if the entity still had a Replicate component - if let Some(despawn_metadata) = sender.get_mut_replicate_despawn_cache().remove(&entity) { + if let Some(replicate_cache) = sender.get_mut_replicate_cache().remove(&entity) { // TODO: DO NOT SEND ENTITY DESPAWN TO THE CLIENT WHO JUST DISCONNECTED! - let mut network_target = despawn_metadata.replication_target; + let mut network_target = replicate_cache.replication_target; // TODO: for this to work properly, we need the replicate stored in `sender.get_mut_replicate_component_cache()` // to be updated for every replication change! Wait for observers instead. // How did it work on the `main` branch? was there something else making it work? Maybe the // update replicate ran before - if despawn_metadata.replication_mode == ReplicationMode::Room { + if replicate_cache.visibility_mode == VisibilityMode::InterestManagement { // if the mode was room, only replicate the despawn to clients that were in the same room - network_target.intersection(NetworkTarget::Only( - despawn_metadata.replication_clients_cache, + network_target.intersection(&NetworkTarget::Only( + replicate_cache.replication_clients_cache, )); } trace!(?entity, ?network_target, "send entity despawn"); - let group_id = despawn_metadata.replication_group.group_id(Some(entity)); let _ = sender - .prepare_entity_despawn( - entity, - group_id, - network_target, - system_bevy_ticks.this_run(), - ) - // TODO: bubble up errors to user via ConnectionEvents - .map_err(|e| { + .prepare_entity_despawn(entity, &replicate_cache.replication_group, network_target) + // TODO: bubble up errors to user via ConnectionEvents? + .inspect_err(|e| { error!("error sending entity despawn: {:?}", e); }); } } } -fn send_entity_spawn( - system_bevy_ticks: SystemChangeTick, - component_registry: Res, - query: Query<(Entity, Ref, Option<&ReplicateVisibility>)>, - mut sender: ResMut, -) { - // Replicate to already connected clients (replicate only new entities) - query.iter().for_each(|(entity, replicate, visibility)| { - match replicate.replication_mode { - // for room mode, no need to handle newly-connected clients specially; they just need - // to be added to the correct room - ReplicationMode::Room => { - visibility.unwrap().clients_cache - .iter() - .for_each(|(client_id, visibility)| { - if replicate.replication_target.should_send_to(client_id) { - match visibility { - ClientVisibility::Gained => { - trace!( - ?entity, - ?client_id, - "send entity spawn to client who just gained visibility" - ); - let _ = sender - .prepare_entity_spawn( - entity, - &replicate, - NetworkTarget::Only(vec![*client_id]), - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending entity spawn: {:?}", e); - }); - } - ClientVisibility::Lost => {} - ClientVisibility::Maintained => { - // TODO: is this even reachable? - // only try to replicate if the replicate component was just added - if replicate.is_added() { - trace!( - ?entity, - ?client_id, - "send entity spawn to client who maintained visibility" - ); - let _ = sender - .prepare_entity_spawn( - entity, - replicate.deref(), - NetworkTarget::Only(vec![*client_id]), - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending entity spawn: {:?}", e); - }); - } - } - } - } - }); - } - ReplicationMode::NetworkTarget => { - let mut target = replicate.replication_target.clone(); - - let new_connected_clients = sender.new_connected_clients().clone(); - if !new_connected_clients.is_empty() { - // replicate to the newly connected clients that match our target - let mut new_connected_target = target.clone(); - new_connected_target - .intersection(NetworkTarget::Only(new_connected_clients.clone())); - debug!(?entity, target = ?new_connected_target, "Replicate to newly connected clients"); - // replicate all entities to newly connected clients - let _ = sender - .prepare_entity_spawn( - entity, - &replicate, - new_connected_target, - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending entity spawn: {:?}", e); - }); - // don't re-send to newly connection client - target.exclude(new_connected_clients.clone()); - } - - // only try to replicate if the replicate component was just added - if replicate.is_added() { - trace!(?entity, "send entity spawn"); - let _ = sender - .prepare_entity_spawn( - entity, - replicate.deref(), - target, - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending entity spawn: {:?}", e); - }); - } - } - } - }) -} - /// This system sends updates for all components that were added or changed /// Sends both ComponentInsert for newly added components /// and ComponentUpdates otherwise @@ -269,225 +209,205 @@ fn send_entity_spawn( /// NOTE: cannot use ConnectEvents because they are reset every frame pub(crate) fn send_component_update( registry: Res, - query: Query<(Entity, Ref, Ref, Option<&ReplicateVisibility>)>, + query: Query<( + Entity, + Ref, + Ref, + &ReplicationGroup, + Option<&ReplicateVisibility>, + Has>, + Has>, + Option<&OverrideTargetComponent>, + )>, system_bevy_ticks: SystemChangeTick, mut sender: ResMut, ) { let kind = registry.net_id::(); - query.iter().for_each(|(entity, component, replicate, visibility)| { - // do not replicate components that are disabled - if replicate.is_disabled::() { - return; - } - // will store (NetworkTarget, is_Insert). We use this to avoid serializing if there are no clients we need to replicate to - let mut replicate_args = vec![]; - match replicate.replication_mode { - ReplicationMode::Room => { - visibility.unwrap().clients_cache - .iter() - .for_each(|(client_id, visibility)| { - if replicate.replication_target.should_send_to(client_id) { - let target = replicate.target::(NetworkTarget::Only(vec![*client_id])); - match visibility { - // TODO: here we required the component to be clone because we send it to multiple clients. - // but maybe we can instead serialize it to Bytes early and then have the bytes be shared between clients? - // or just pass a reference? - ClientVisibility::Gained => { - replicate_args.push((target, true)); - } - ClientVisibility::Lost => {} - ClientVisibility::Maintained => { - // send a component_insert for components that were newly added - if component.is_added() { - replicate_args.push((target, true)); - } else { - // only update components that were not newly added - - // do not send updates for these components, only inserts/removes - if replicate.is_replicate_once::() { - // we can exit the function immediately because we know we don't want to replicate - // to any client - return; + query + .iter() + .for_each(|(entity, component, replication_target, group, visibility, disabled, replicate_once, override_target)| { + // do not replicate components that are disabled + if disabled { + return; + } + // use the overriden target if present + let target = override_target.map_or(&replication_target.replication, |override_target| &override_target.target); + let (insert_target, update_target): (NetworkTarget, NetworkTarget) = match visibility { + Some(visibility) => { + let mut insert_clients = vec![]; + let mut update_clients = vec![]; + visibility + .clients_cache + .iter() + .for_each(|(client_id, visibility)| { + if target.targets(client_id) { + match visibility { + ClientVisibility::Gained => { + insert_clients.push(*client_id); + } + ClientVisibility::Lost => {} + ClientVisibility::Maintained => { + // send a component_insert for components that were newly added + if component.is_added() { + insert_clients.push(*client_id); + } else { + // for components that were not newly added, only send as updates + if replicate_once { + // we can exit the function immediately because we know we don't want to replicate + // to any client + return; + } + update_clients.push(*client_id); } - replicate_args.push((target, true)); } } } - } - }) - } - ReplicationMode::NetworkTarget => { - let mut target = replicate.replication_target.clone(); - - let new_connected_clients = sender.new_connected_clients().clone(); - // replicate all components to newly connected clients - if !new_connected_clients.is_empty() { - // replicate to the newly connected clients that match our target - let mut new_connected_target = target.clone(); - new_connected_target - .intersection(NetworkTarget::Only(new_connected_clients.clone())); - replicate_args.push((replicate.target::(new_connected_target), true)); - // don't re-send to newly connection client - target.exclude(new_connected_clients.clone()); + }); + (NetworkTarget::from(insert_clients), NetworkTarget::from(update_clients)) } + None => { + let (mut insert_target, mut update_target) = + (NetworkTarget::None, NetworkTarget::None); - let target = replicate.target::(target); - // send a component_insert for components that were newly added - // or if replicate was newly added. - // TODO: ideally what we should be checking is: is the component newly added - // for the client we are sending to? - // Otherwise another solution would be to also insert the component on ComponentUpdate if it's missing - // Or should we just have ComponentInsert and ComponentUpdate be the same thing? Or we check - // on the receiver's entity world mut to know if we emit a ComponentInsert or a ComponentUpdate? - if component.is_added() || replicate.is_added() { - trace!("component is added"); - replicate_args.push((target, true)); - } else { - // do not send updates for these components, only inserts/removes - if replicate.is_replicate_once::() { - trace!(?entity, - "not replicating updates for {:?} because it is marked as replicate_once", - kind - ); - // we can exit the function immediately because we know we don't want to replicate - // to any client - return; + // send a component_insert for components that were newly added + // or if replicate was newly added. + // TODO: ideally what we should be checking is: is the component newly added + // for the client we are sending to? + // Otherwise another solution would be to also insert the component on ComponentUpdate if it's missing + // Or should we just have ComponentInsert and ComponentUpdate be the same thing? Or we check + // on the receiver's entity world mut to know if we emit a ComponentInsert or a ComponentUpdate? + if component.is_added() || replication_target.is_added() { + trace!("component is added or replication_target is added"); + insert_target.union(target); + } else { + // do not send updates for these components, only inserts/removes + if replicate_once { + trace!(?entity, + "not replicating updates for {:?} because it is marked as replicate_once", + kind + ); + return; + } + // otherwise send an update for all components that changed since the + // last update we have ack-ed + update_target.union(target); } - // otherwise send an update for all components that changed since the - // last update we have ack-ed - replicate_args.push((target, false)); - } - } - } - if !replicate_args.is_empty() { - // serialize component - let writer = sender.writer(); - let raw_data = registry.serialize(component.as_ref(), writer).expect("Could not serialize component"); + let new_connected_clients = sender.new_connected_clients(); + // replicate all components to newly connected clients + if !new_connected_clients.is_empty() { + // replicate to the newly connected clients that match our target + let mut new_connected_target = NetworkTarget::Only(new_connected_clients); + new_connected_target.intersection(target); + debug!(?entity, target = ?new_connected_target, "Replicate to newly connected clients"); + update_target.union(&new_connected_target); + } + (insert_target, update_target) + } + }; + if !insert_target.is_empty() || !update_target.is_empty() { + // serialize component + let writer = sender.writer(); + let raw_data = registry + .serialize(component.as_ref(), writer) + .expect("Could not serialize component"); - replicate_args.into_iter().for_each(|(target, is_insert)| { - if is_insert { + if !insert_target.is_empty() { let _ = sender .prepare_component_insert( entity, kind, // TODO: avoid the clone by using Arc? raw_data.clone(), - replicate.as_ref(), - target, - system_bevy_ticks.this_run(), + ®istry, + replication_target.as_ref(), + group, + insert_target ) - .map_err(|e| { + .inspect_err(|e| { error!("error sending component insert: {:?}", e); }); - } else { + } + if !update_target.is_empty() { let _ = sender .prepare_component_update( entity, kind, - raw_data.clone(), - replicate.as_ref(), - target, + raw_data, + group, + update_target, component.last_changed(), system_bevy_ticks.this_run(), ) - .map_err(|e| { + .inspect_err(|e| { error!("error sending component update: {:?}", e); }); } - }); - } - }); + } + }); } /// This system sends updates for all components that were removed pub(crate) fn send_component_removed( registry: Res, // only remove the component for entities that are being actively replicated - query: Query<(&Replicate, Option<&ReplicateVisibility>)>, - system_bevy_ticks: SystemChangeTick, + query: Query<( + &ReplicationTarget, + &ReplicationGroup, + Option<&ReplicateVisibility>, + Has>, + Option<&OverrideTargetComponent>, + )>, mut removed: RemovedComponents, mut sender: ResMut, ) { let kind = registry.net_id::(); removed.read().for_each(|entity| { - if let Ok((replicate, visibility)) = query.get(entity) { + if let Ok((replication_target, group, visibility, disabled, override_target)) = + query.get(entity) + { // do not replicate components that are disabled - if replicate.is_disabled::() { + if disabled { return; } - match replicate.replication_mode { - ReplicationMode::Room => { + // use the overriden target if present + let base_target = override_target + .map_or(&replication_target.replication, |override_target| { + &override_target.target + }); + let target = match visibility { + Some(visibility) => { visibility - .unwrap() .clients_cache .iter() - .for_each(|(client_id, visibility)| { - if replicate.replication_target.should_send_to(client_id) { + .filter_map(|(client_id, visibility)| { + if base_target.targets(client_id) { // TODO: maybe send no matter the vis? if matches!(visibility, ClientVisibility::Maintained) { - let _ = sender - .prepare_component_remove( - entity, - kind, - replicate, - replicate - .target::(NetworkTarget::Only(vec![*client_id])), - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending component remove: {:?}", e); - }); + // TODO: USE THE CUSTOM REPLICATE TARGET FOR THIS COMPONENT IF PRESENT! + return Some(*client_id); } - } + }; + None }) + .collect() } - ReplicationMode::NetworkTarget => { + None => { trace!("sending component remove!"); - let _ = sender - .prepare_component_remove( - entity, - kind, - replicate, - replicate.target::(replicate.replication_target.clone()), - system_bevy_ticks.this_run(), - ) - .map_err(|e| { - error!("error sending component remove: {:?}", e); - }); + // TODO: USE THE CUSTOM REPLICATE TARGET FOR THIS COMPONENT IF PRESENT! + base_target.clone() } + }; + if target.is_empty() { + return; } + let group_id = group.group_id(Some(entity)); + debug!(?entity, ?kind, "Sending RemoveComponent"); + let _ = sender.prepare_component_remove(entity, kind, group, target); } }) } -/// add replication systems that are shared between client and server -pub(crate) fn add_replication_send_systems(app: &mut App) { - // we need to add despawn trackers immediately for entities for which we add replicate - app.add_systems( - PreUpdate, - handle_replicate_add::.after(ServerReplicationSet::ClientReplication), - ); - app.add_systems( - PostUpdate, - ( - // TODO: try to move this to ReplicationSystems as well? entities are spawned only once - // so we can run the system every frame - // putting it here means we might miss entities that are spawned and depspawned within the send_interval? bug or feature? - send_entity_spawn:: - .in_set(InternalReplicationSet::::BufferEntityUpdates), - // NOTE: we need to run `send_entity_despawn` once per frame (and not once per send_interval) - // because the RemovedComponents Events are present only for 1 frame and we might miss them if we don't run this every frame - // It is ok to run it every frame because it creates at most one message per despawn - // NOTE: we make sure to update the replicate_cache before we make use of it in `send_entity_despawn` - (handle_replicate_add::, handle_replicate_remove::) - .in_set(InternalReplicationSet::::HandleReplicateUpdate), - send_entity_despawn:: - .in_set(InternalReplicationSet::::BufferDespawnsAndRemovals), - ), - ); -} - pub(crate) fn register_replicate_component_send(app: &mut App) { app.add_systems( PostUpdate, @@ -495,69 +415,1202 @@ pub(crate) fn register_replicate_component_send + send_component_removed:: .in_set(InternalReplicationSet::::BufferDespawnsAndRemovals), // NOTE: we run this system once every `send_interval` because we don't want to send too many Update messages // and use up all the bandwidth - crate::shared::replication::systems::send_component_update:: + send_component_update:: .in_set(InternalReplicationSet::::BufferComponentUpdates), ), ); } -pub(crate) fn cleanup(mut sender: ResMut, tick_manager: Res) { +/// Systems that runs internal clean-up on the ReplicationSender +/// (handle tick wrapping, etc.) +pub(crate) fn send_cleanup( + mut sender: ResMut, + tick_manager: Res, +) { let tick = tick_manager.tick(); sender.cleanup(tick); } +/// Systems that runs internal clean-up on the ReplicationReceiver +/// (handle tick wrapping, etc.) +pub(crate) fn receive_cleanup( + mut receiver: ResMut, + tick_manager: Res, +) { + let tick = tick_manager.tick(); + receiver.cleanup(tick); +} + #[cfg(test)] mod tests { - // TODO: how to check that no despawn message is sent? - // /// Check that when replicated entities in other rooms than the current client are despawned, - // /// the despawn is not sent to the client - // #[test] - // fn test_other_rooms_despawn() { - // let mut stepper = BevyStepper::default(); - // - // let server_entity = stepper - // .server_app - // .world - // .spawn(( - // Replicate { - // replication_mode: ReplicationMode::Room, - // ..default() - // }, - // Component1(0.0), - // )) - // .id(); - // let mut room_manager = stepper.server_app.world.resource_mut::(); - // room_manager.add_client(ClientId::Netcode(TEST_CLIENT_ID), RoomId(0)); - // room_manager.add_entity(server_entity, RoomId(0)); - // stepper.frame_step(); - // stepper.frame_step(); - // - // // check that the entity was replicated - // let client_entity = stepper - // .client_app - // .world - // .query_filtered::>() - // .single(&stepper.client_app.world); - // - // // update the room of the server entity to not be in the client's room anymore - // stepper - // .server_app - // .world - // .resource_mut::() - // .remove_entity(server_entity, RoomId(0)); - // stepper.frame_step(); - // stepper.frame_step(); - // - // // despawn the entity - // stepper.server_app.world.entity_mut(server_entity).despawn(); - // stepper.frame_step(); - // stepper.frame_step(); - // - // // the despawn shouldn't be replicated to the client, since it's in a different room - // assert!(stepper.client_app.world.get_entity(client_entity).is_some()); - // } + use super::*; + use crate::client::events::ComponentUpdateEvent; + use crate::prelude::client::Confirmed; + use crate::prelude::server::VisibilityManager; + use crate::prelude::{client, server, Replicated}; + use crate::shared::replication::components::{Controlled, ControlledBy}; + use crate::tests::multi_stepper::{MultiBevyStepper, TEST_CLIENT_ID_1, TEST_CLIENT_ID_2}; + use crate::tests::protocol::*; + use crate::tests::stepper::{BevyStepper, Step, TEST_CLIENT_ID}; + use bevy::prelude::{default, EventReader, Resource, Update}; + + // TODO: test entity spawn newly connected client + // TODO: test entity spawn replication target was updated + + #[test] + fn test_entity_spawn() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper.server_app.world.spawn_empty().id(); + stepper.frame_step(); + stepper.frame_step(); + // check that entity wasn't spawned + assert!(stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .is_none()); + + // add replicate + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Replicate { + target: ReplicationTarget { + replication: NetworkTarget::All, + prediction: NetworkTarget::All, + interpolation: NetworkTarget::All, + }, + controlled_by: ControlledBy { + target: NetworkTarget::All, + }, + ..default() + }); + + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was spawned + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + // check that prediction, interpolation, controlled was handled correctly + let confirmed = stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("Confirmed component missing"); + assert!(confirmed.predicted.is_some()); + assert!(confirmed.interpolated.is_some()); + assert!(stepper + .client_app + .world + .entity(client_entity) + .get::() + .is_some()); + } + + #[test] + fn test_entity_spawn_client_to_server() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server with visibility::All + let client_entity = stepper.client_app.world.spawn_empty().id(); + stepper.frame_step(); + stepper.frame_step(); + + // add replicate + stepper + .client_app + .world + .entity_mut(client_entity) + .insert(Replicate::default()); + // TODO: we need to run a couple frames because the server doesn't read the client's updates + // because they are from the future + stepper.frame_step(); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was spawned + stepper + .server_app + .world + .resource::() + .connection(ClientId::Netcode(TEST_CLIENT_ID)) + .expect("client connection missing") + .replication_receiver + .remote_entity_map + .get_local(client_entity) + .expect("entity was not replicated to server"); + } + + #[test] + fn test_entity_spawn_visibility() { + let mut stepper = MultiBevyStepper::default(); + + // spawn an entity on server with visibility::InterestManagement + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + visibility: VisibilityMode::InterestManagement, + ..default() + }) + .id(); + stepper.frame_step(); + stepper.frame_step(); + + // check that entity wasn't spawned + assert!(stepper + .client_app_1 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .is_none()); + // make entity visible + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID_1), server_entity); + stepper.frame_step(); + stepper.frame_step(); + + // check that entity was spawned + let client_entity = *stepper + .client_app_1 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + // check that the entity was not spawned on the other client + assert!(stepper + .client_app_2 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .is_none()); + } + + #[test] + fn test_entity_spawn_preexisting_target() { + let mut stepper = BevyStepper::default(); + + let client_entity = stepper.client_app.world.spawn_empty().id(); + stepper.frame_step(); + let server_entity = stepper + .server_app + .world + .spawn(( + Replicate::default(), + TargetEntity::Preexisting(client_entity), + )) + .id(); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was replicated on the client entity + assert_eq!( + stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .unwrap(), + &client_entity + ); + assert!(stepper + .client_app + .world + .get::(client_entity) + .is_some()); + assert_eq!(stepper.client_app.world.entities().len(), 1); + } + + /// Check that if we change the replication target on an entity that already has one + /// we spawn the entity for new clients + #[test] + fn test_entity_spawn_replication_target_update() { + let mut stepper = MultiBevyStepper::default(); + + // spawn an entity on server to client 1 + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + target: ReplicationTarget { + replication: NetworkTarget::Single(ClientId::Netcode(TEST_CLIENT_ID_1)), + ..default() + }, + ..default() + }) + .id(); + stepper.frame_step(); + stepper.frame_step(); + + let client_entity_1 = *stepper + .client_app_1 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client 1"); + + // update the replication target + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(ReplicationTarget { + replication: NetworkTarget::All, + ..default() + }); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity gets replicated to the other client + stepper + .client_app_2 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client 2"); + // TODO: check that client 1 did not receive another entity-spawn message + } + + #[test] + fn test_entity_despawn() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper.server_app.world.spawn(Replicate::default()).id(); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was spawned + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // despawn + stepper.server_app.world.despawn(server_entity); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was despawned + assert!(stepper.client_app.world.get_entity(client_entity).is_none()); + } + + /// Check that if interest management is used, a client losing visibility of an entity + /// will cause the server to send a despawn-entity message to the client + #[test] + fn test_entity_despawn_lose_visibility() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + visibility: VisibilityMode::InterestManagement, + ..default() + }) + .id(); + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID), server_entity); + + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was spawned + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // lose visibility + stepper + .server_app + .world + .resource_mut::() + .lose_visibility(ClientId::Netcode(TEST_CLIENT_ID), server_entity); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was despawned + assert!(stepper.client_app.world.get_entity(client_entity).is_none()); + } + + /// Test that if an entity with visibility is despawned, the despawn-message is not sent + /// to other clients who do not have visibility of the entity + #[test] + fn test_entity_despawn_non_visible() { + let mut stepper = MultiBevyStepper::default(); + + // spawn one entity replicated to each client + // they will share the same replication group id, so that each client's ReplicationReceiver + // can read the replication messages of the other client + let server_entity_1 = stepper + .server_app + .world + .spawn(Replicate { + visibility: VisibilityMode::InterestManagement, + group: ReplicationGroup::new_id(1), + ..default() + }) + .id(); + let server_entity_2 = stepper + .server_app + .world + .spawn(Replicate { + visibility: VisibilityMode::InterestManagement, + group: ReplicationGroup::new_id(1), + ..default() + }) + .id(); + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID_1), server_entity_1) + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID_2), server_entity_2); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was spawned on each client + let client_entity_1 = *stepper + .client_app_1 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity_1) + .expect("entity was not replicated to client 1"); + let client_entity_2 = *stepper + .client_app_2 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity_2) + .expect("entity was not replicated to client 2"); + + // update the entity_map on client 2 to re-use the same server entity as client 1 + // so that replication messages for server_entity_1 could also be read by client 2 + stepper + .client_app_2 + .world + .resource_mut::() + .replication_receiver + .remote_entity_map + .insert(server_entity_1, client_entity_2); + + // despawn the server_entity_1 + stepper.server_app.world.despawn(server_entity_1); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was despawned on client 1 + assert!(stepper + .client_app_1 + .world + .get_entity(client_entity_1) + .is_none()); + + // check that the entity still exists on client 2 + assert!(stepper + .client_app_2 + .world + .get_entity(client_entity_2) + .is_some()); + } + + /// Check that if we change the replication target on an entity that already has one + /// we despawn the entity for new clients + #[test] + fn test_entity_despawn_replication_target_update() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server to client 1 + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + target: ReplicationTarget { + replication: NetworkTarget::Single(ClientId::Netcode(TEST_CLIENT_ID)), + ..default() + }, + ..default() + }) + .id(); + stepper.frame_step(); + stepper.frame_step(); + + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // update the replication target + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(ReplicationTarget { + replication: NetworkTarget::None, + ..default() + }); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity was despawned + assert!(stepper.client_app.world.get_entity(client_entity).is_none()); + } + + #[test] + fn test_component_insert() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper.server_app.world.spawn(Replicate::default()).id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // add component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Component1(1.0)); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was replicated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + } + + #[test] + fn test_component_insert_visibility_maintained() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + visibility: VisibilityMode::InterestManagement, + ..default() + }) + .id(); + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID), server_entity); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // add component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Component1(1.0)); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was replicated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + } + + #[test] + fn test_component_insert_visibility_gained() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + visibility: VisibilityMode::InterestManagement, + ..default() + }) + .id(); + + stepper.frame_step(); + stepper.frame_step(); + + // add component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Component1(1.0)); + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID), server_entity); + stepper.frame_step(); + stepper.frame_step(); + + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + // check that the component was replicated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + } + + #[test] + fn test_component_insert_disabled() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper.server_app.world.spawn(Replicate::default()).id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // add component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert((Component1(1.0), DisabledComponent::::default())); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was not replicated + assert!(stepper + .client_app + .world + .entity(client_entity) + .get::() + .is_none()); + } + + #[test] + fn test_component_override_target() { + let mut stepper = MultiBevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn(( + Replicate::default(), + Component1(1.0), + OverrideTargetComponent::::new(NetworkTarget::Single( + ClientId::Netcode(TEST_CLIENT_ID_1), + )), + )) + .id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity_1 = *stepper + .client_app_1 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + let client_entity_2 = *stepper + .client_app_2 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // check that the component was replicated to client 1 only + assert_eq!( + stepper + .client_app_1 + .world + .entity(client_entity_1) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + assert!(stepper + .client_app_2 + .world + .entity(client_entity_2) + .get::() + .is_none()); + } + + /// Check that override target works even if the entity uses interest management + /// We still use visibility, but we use `override_target` instead of `replication_target` + #[test] + fn test_component_override_target_visibility() { + let mut stepper = MultiBevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn(( + Replicate { + // target is both + visibility: VisibilityMode::InterestManagement, + ..default() + }, + Component1(1.0), + // override target is only client 1 + OverrideTargetComponent::::new(NetworkTarget::Single( + ClientId::Netcode(TEST_CLIENT_ID_1), + )), + )) + .id(); + // entity is visible to both + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID_1), server_entity) + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID_2), server_entity); + stepper.frame_step(); + stepper.frame_step(); + let client_entity_1 = *stepper + .client_app_1 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + let client_entity_2 = *stepper + .client_app_2 + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // check that the component was replicated to client 1 only + assert_eq!( + stepper + .client_app_1 + .world + .entity(client_entity_1) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + assert!(stepper + .client_app_2 + .world + .entity(client_entity_2) + .get::() + .is_none()); + } + + #[test] + fn test_component_update() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn((Replicate::default(), Component1(1.0))) + .id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // add component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Component1(2.0)); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was replicated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(2.0) + ); + } + + /// Check that updates are not sent if the `ReplicationTarget` component gets removed. + /// Check that updates are resumed when the `ReplicationTarget` component gets re-added. + #[test] + fn test_component_update_replication_target_removed() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn((Replicate::default(), Component1(1.0))) + .id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // remove the replication_target component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Component1(2.0)) + .remove::(); + stepper.frame_step(); + stepper.frame_step(); + + // check that the entity still exists on the client, but that the component was not updated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + + // re-add the replication_target component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(ReplicationTarget::default()); + stepper.frame_step(); + stepper.frame_step(); + // check that the component gets updated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(2.0) + ); + } + + #[test] + fn test_component_update_disabled() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn((Replicate::default(), Component1(1.0))) + .id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + + // add component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert((Component1(2.0), DisabledComponent::::default())); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was not updated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + } + + #[test] + fn test_component_update_replicate_once() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn(( + Replicate::default(), + Component1(1.0), + ReplicateOnceComponent::::default(), + )) + .id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + // check that the component was replicated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + + // update component + stepper + .server_app + .world + .entity_mut(server_entity) + .insert(Component1(2.0)); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was not updated + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + } + + #[test] + fn test_component_remove() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper + .server_app + .world + .spawn((Replicate::default(), Component1(1.0))) + .id(); + stepper.frame_step(); + stepper.frame_step(); + let client_entity = *stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .expect("component missing"), + &Component1(1.0) + ); + + // remove component + stepper + .server_app + .world + .entity_mut(server_entity) + .remove::(); + stepper.frame_step(); + stepper.frame_step(); + + // check that the component was replicated + assert!(stepper + .client_app + .world + .entity(client_entity) + .get::() + .is_none()); + } + + #[test] + fn test_replication_target_add() { + let mut stepper = BevyStepper::default(); + + let server_entity = stepper.server_app.world.spawn(Replicate::default()).id(); + stepper.frame_step(); + + // check that a DespawnTracker was added + assert!(stepper + .server_app + .world + .entity(server_entity) + .get::() + .is_some()); + // check that a ReplicateCache was added + assert_eq!( + stepper + .server_app + .world + .resource::() + .replicate_component_cache + .get(&server_entity) + .expect("ReplicateCache missing"), + &ReplicateCache { + replication_target: NetworkTarget::All, + replication_group: ReplicationGroup::new_from_entity(), + visibility_mode: VisibilityMode::All, + replication_clients_cache: vec![], + } + ); + } + + /// Check that if we switch the visibility mode, the entity gets spawned + /// to the clients that now have visibility + #[test] + fn test_change_visibility_mode_spawn() { + let mut stepper = BevyStepper::default(); + + let server_entity = stepper + .server_app + .world + .spawn(Replicate { + target: ReplicationTarget { + replication: NetworkTarget::None, + ..default() + }, + ..default() + }) + .id(); + stepper.frame_step(); + stepper.frame_step(); + + // set visibility to interest management + stepper.server_app.world.entity_mut(server_entity).insert(( + VisibilityMode::InterestManagement, + ReplicationTarget { + replication: NetworkTarget::All, + ..default() + }, + )); + stepper + .server_app + .world + .resource_mut::() + .gain_visibility(ClientId::Netcode(TEST_CLIENT_ID), server_entity); + + stepper.frame_step(); + stepper.frame_step(); + stepper + .client_app + .world + .resource::() + .replication_receiver + .remote_entity_map + .get_local(server_entity) + .expect("entity was not replicated to client"); + } + + /// Check if we send an update with a component that is already equal to the component on the remote, + /// then we do not apply the update to the remote (to avoid triggering change detection) + #[test] + fn test_equal_update_does_not_trigger_change_detection() { + let mut stepper = BevyStepper::default(); + + stepper.client_app.add_systems( + Update, + |mut events: EventReader>| { + if let Some(event) = events.read().next() { + panic!( + "ComponentUpdateEvent received for entity: {:?}", + event.entity() + ); + } + }, + ); + + // spawn an entity on server + let server_entity = stepper.server_app.world.spawn(Component1(1.0)).id(); + // spawn an entity on the client with the component value + let client_entity = stepper.client_app.world.spawn(Component1(1.0)).id(); + + // add replication with a pre-existing target + stepper.server_app.world.entity_mut(server_entity).insert(( + Replicate::default(), + TargetEntity::Preexisting(client_entity), + )); + + // check that we did not receive an ComponentUpdateEvent because the component was already equal + // to the replicated value + stepper.frame_step(); + stepper.frame_step(); + } + + #[derive(Resource, Default)] + struct Counter(u32); + + /// Check if we send an update with a component that is not equal to the component on the remote, + /// then we apply the update to the remote (so we emit a ComponentUpdateEvent) + #[test] + fn test_not_equal_update_does_not_trigger_change_detection() { + let mut stepper = BevyStepper::default(); + + // spawn an entity on server + let server_entity = stepper.server_app.world.spawn(Component1(2.0)).id(); + // spawn an entity on the client with the component value + let client_entity = stepper.client_app.world.spawn(Component1(1.0)).id(); + + stepper.client_app.init_resource::(); + stepper.client_app.add_systems( + Update, + move |mut events: EventReader>, + mut counter: ResMut| { + for events in events.read() { + counter.0 += 1; + assert_eq!(events.entity(), client_entity); + } + }, + ); + + // add replication with a pre-existing target + stepper.server_app.world.entity_mut(server_entity).insert(( + Replicate::default(), + TargetEntity::Preexisting(client_entity), + )); + + // check that we did receive an ComponentUpdateEvent + stepper.frame_step(); + stepper.frame_step(); + assert_eq!( + stepper + .client_app + .world + .get_resource::() + .unwrap() + .0, + 1 + ); + } } diff --git a/lightyear/src/shared/sets.rs b/lightyear/src/shared/sets.rs index 66b18cb29..39248ab7c 100644 --- a/lightyear/src/shared/sets.rs +++ b/lightyear/src/shared/sets.rs @@ -19,7 +19,7 @@ pub(crate) enum InternalReplicationSet { /// (has a PreSpawnedPlayerObject component) SetPreSpawnedHash, /// System that handles the addition/removal of the `Replicate` component - HandleReplicateUpdate, + BeforeBuffer, /// Gathers entity despawns and component removals /// Needs to run once per frame instead of once per send_interval /// because they rely on bevy events that are cleared every frame @@ -33,6 +33,8 @@ pub(crate) enum InternalReplicationSet { /// All systems that buffer replication messages Buffer, + /// System that handles the update of an existing replication component + AfterBuffer, /// SystemSet that encompasses all send replication systems All, _Marker(std::marker::PhantomData), diff --git a/lightyear/src/tests/integration/multi_transport.rs b/lightyear/src/tests/integration/multi_transport.rs index 5c27ef5ac..e9dec0950 100644 --- a/lightyear/src/tests/integration/multi_transport.rs +++ b/lightyear/src/tests/integration/multi_transport.rs @@ -1,255 +1,11 @@ //! Tests related to the server using multiple transports at the same time to connect to clients -use bevy::core::TaskPoolThreadAssignmentPolicy; -use bevy::prelude::{default, App, PluginGroup, Real, TaskPoolOptions, TaskPoolPlugin, Time}; -use bevy::tasks::available_parallelism; -use bevy::time::TimeUpdateStrategy; -use bevy::utils::Duration; -use bevy::MinimalPlugins; - -use crate::connection::netcode::generate_key; -use crate::connection::server::{NetServer, ServerConnections}; -use crate::prelude::client::{ - Authentication, ClientConfig, ClientConnection, InterpolationConfig, NetClient, NetConfig, - PredictionConfig, SyncConfig, -}; -use crate::prelude::server::{NetcodeConfig, ServerConfig}; -use crate::prelude::*; -use crate::tests::protocol::*; +use crate::client::sync::SyncConfig; +use crate::prelude::client::{InterpolationConfig, PredictionConfig}; +use crate::prelude::{SharedConfig, TickConfig}; +use crate::tests::multi_stepper::MultiBevyStepper; use crate::tests::stepper::Step; -use crate::transport::LOCAL_SOCKET; - -pub struct MultiBevyStepper { - // first client will use local channels - pub client_app_1: App, - // second client will use udp - pub client_app_2: App, - pub server_app: App, - pub frame_duration: Duration, - /// fixed timestep duration - pub tick_duration: Duration, - pub current_time: bevy::utils::Instant, -} - -impl MultiBevyStepper { - pub fn new( - shared_config: SharedConfig, - sync_config: SyncConfig, - prediction_config: PredictionConfig, - interpolation_config: InterpolationConfig, - frame_duration: Duration, - ) -> Self { - let now = bevy::utils::Instant::now(); - - // both clients will use the same client id - let client_id = 0; - let server_addr = LOCAL_SOCKET; - - // Shared config - let protocol_id = 0; - let private_key = generate_key(); - let auth = Authentication::Manual { - server_addr, - protocol_id, - private_key, - client_id, - }; - - // client net config 1: use local channels - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - let client_io = IoConfig::from_transport(TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }); - let client_params = (LOCAL_SOCKET, to_server_recv, from_server_send); - let net_config_1 = NetConfig::Netcode { - auth: auth.clone(), - config: client::NetcodeConfig::default(), - io: client_io, - }; - - // TODO: maybe we don't need the server Channels transport and instead we can just have multiple - // concurrent LocalChannel connections? seems easier to reason about! - let server_io_1 = IoConfig::from_transport(TransportConfig::Channels { - channels: vec![client_params], - }); - - // client net config 2: use local channels - let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); - let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - let client_io = IoConfig::from_transport(TransportConfig::LocalChannel { - recv: from_server_recv, - send: to_server_send, - }); - let client_params = (LOCAL_SOCKET, to_server_recv, from_server_send); - let net_config_2 = NetConfig::Netcode { - auth, - config: client::NetcodeConfig::default(), - io: client_io, - }; - - let server_io_2 = IoConfig::from_transport(TransportConfig::Channels { - channels: vec![client_params], - }); - - // build server with two distinct transports - let mut server_app = App::new(); - server_app.add_plugins( - MinimalPlugins - .set(TaskPoolPlugin { - task_pool_options: TaskPoolOptions { - compute: TaskPoolThreadAssignmentPolicy { - min_threads: available_parallelism(), - max_threads: std::usize::MAX, - percent: 1.0, - }, - ..default() - }, - }) - .build(), - ); - let netcode_config = NetcodeConfig::default() - .with_protocol_id(protocol_id) - .with_key(private_key); - let config = ServerConfig { - shared: shared_config.clone(), - net: vec![ - server::NetConfig::Netcode { - config: netcode_config.clone(), - io: server_io_1, - }, - server::NetConfig::Netcode { - config: netcode_config, - io: server_io_2, - }, - ], - ..default() - }; - let plugin = server::ServerPlugin::new(config); - server_app.add_plugins((plugin, ProtocolPlugin)); - // Initialize Real time (needed only for the first TimeSystem run) - server_app - .world - .get_resource_mut::>() - .unwrap() - .update_with_instant(now); - - let build_client = |net_config: NetConfig| -> App { - let mut client_app = App::new(); - client_app.add_plugins( - MinimalPlugins - .set(TaskPoolPlugin { - task_pool_options: TaskPoolOptions { - compute: TaskPoolThreadAssignmentPolicy { - min_threads: available_parallelism(), - max_threads: std::usize::MAX, - percent: 1.0, - }, - ..default() - }, - }) - .build(), - ); - - let config = ClientConfig { - shared: shared_config.clone(), - net: net_config, - sync: sync_config.clone(), - prediction: prediction_config, - interpolation: interpolation_config.clone(), - ..default() - }; - let plugin = client::ClientPlugin::new(config); - client_app.add_plugins((plugin, ProtocolPlugin)); - // Initialize Real time (needed only for the first TimeSystem run) - client_app - .world - .get_resource_mut::>() - .unwrap() - .update_with_instant(now); - client_app - }; - - Self { - client_app_1: build_client(net_config_1), - client_app_2: build_client(net_config_2), - server_app, - frame_duration, - tick_duration: shared_config.tick.tick_duration, - current_time: now, - } - } - - pub fn init(&mut self) { - let _ = self - .server_app - .world - .resource_mut::() - .start(); - let _ = self - .client_app_1 - .world - .resource_mut::() - .connect(); - let _ = self - .client_app_2 - .world - .resource_mut::() - .connect(); - - // Advance the world to let the connection process complete - for _ in 0..100 { - if self - .client_app_1 - .world - .resource::() - .is_synced() - && self - .client_app_2 - .world - .resource::() - .is_synced() - { - return; - } - self.frame_step(); - } - } - - pub(crate) fn advance_time(&mut self, duration: Duration) { - self.current_time += duration; - self.client_app_1 - .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); - self.client_app_2 - .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); - self.server_app - .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); - mock_instant::MockClock::advance(duration); - } -} - -impl Step for MultiBevyStepper { - /// Advance the world by one frame duration - fn frame_step(&mut self) { - self.advance_time(self.frame_duration); - self.client_app_1.update(); - self.client_app_2.update(); - // sleep a bit to make sure that local io receives the packets - std::thread::sleep(Duration::from_millis(1)); - self.server_app.update(); - std::thread::sleep(Duration::from_millis(1)); - } - - fn tick_step(&mut self) { - self.advance_time(self.tick_duration); - self.client_app_1.update(); - self.client_app_2.update(); - // sleep a bit to make sure that local io receives the packets - std::thread::sleep(Duration::from_millis(1)); - self.server_app.update(); - std::thread::sleep(Duration::from_millis(1)); - } -} +use bevy::prelude::*; +use bevy::utils::Duration; #[test] fn test_multi_transport() { @@ -259,11 +15,6 @@ fn test_multi_transport() { tick: TickConfig::new(tick_duration), ..Default::default() }; - let link_conditioner = LinkConditionerConfig { - incoming_latency: Duration::from_millis(20), - incoming_jitter: Duration::from_millis(0), - incoming_loss: 0.0, - }; let mut stepper = MultiBevyStepper::new( shared_config, SyncConfig::default(), diff --git a/lightyear/src/tests/integration/tick_wrapping.rs b/lightyear/src/tests/integration/tick_wrapping.rs index 5c458f562..9412f27f0 100644 --- a/lightyear/src/tests/integration/tick_wrapping.rs +++ b/lightyear/src/tests/integration/tick_wrapping.rs @@ -68,13 +68,7 @@ fn test_sync_after_tick_wrap() { let server_entity = stepper .server_app .world - .spawn(( - Component1(0.0), - Replicate { - replication_target: NetworkTarget::All, - ..default() - }, - )) + .spawn((Component1(0.0), Replicate::default())) .id(); // advance 200 ticks to wrap ticks around u16::MAX @@ -159,13 +153,7 @@ fn test_sync_after_tick_half_wrap() { let server_entity = stepper .server_app .world - .spawn(( - Component1(0.0), - Replicate { - replication_target: NetworkTarget::All, - ..default() - }, - )) + .spawn((Component1(0.0), Replicate::default())) .id(); for i in 0..200 { diff --git a/lightyear/src/tests/mod.rs b/lightyear/src/tests/mod.rs index effda2535..c0b3b23c7 100644 --- a/lightyear/src/tests/mod.rs +++ b/lightyear/src/tests/mod.rs @@ -2,5 +2,6 @@ #![allow(unused_variables)] #![allow(dead_code)] mod integration; +pub(crate) mod multi_stepper; pub mod protocol; -pub mod stepper; +pub(crate) mod stepper; diff --git a/lightyear/src/tests/multi_stepper.rs b/lightyear/src/tests/multi_stepper.rs new file mode 100644 index 000000000..8edea066d --- /dev/null +++ b/lightyear/src/tests/multi_stepper.rs @@ -0,0 +1,284 @@ +//! Tests related to the server using multiple transports at the same time to connect to clients +use crate::client::networking::ClientCommands; +use bevy::core::TaskPoolThreadAssignmentPolicy; +use bevy::ecs::system::RunSystemOnce; +use bevy::prelude::{ + default, App, Commands, PluginGroup, Real, TaskPoolOptions, TaskPoolPlugin, Time, +}; +use bevy::tasks::available_parallelism; +use bevy::time::TimeUpdateStrategy; +use bevy::utils::Duration; +use bevy::MinimalPlugins; + +use crate::connection::netcode::generate_key; +use crate::connection::server::{NetServer, ServerConnections}; +use crate::prelude::client::{ + Authentication, ClientConfig, ClientConnection, ClientTransport, InterpolationConfig, + NetClient, NetConfig, PredictionConfig, SyncConfig, +}; +use crate::prelude::server::{NetcodeConfig, ServerCommands, ServerConfig, ServerTransport}; +use crate::prelude::*; +use crate::tests::protocol::*; +use crate::tests::stepper::{BevyStepper, Step}; +use crate::transport::LOCAL_SOCKET; + +pub(crate) const TEST_CLIENT_ID_1: u64 = 1; +pub(crate) const TEST_CLIENT_ID_2: u64 = 2; + +pub struct MultiBevyStepper { + // first client will use local channels + pub client_app_1: App, + // second client will use udp + pub client_app_2: App, + pub server_app: App, + pub frame_duration: Duration, + /// fixed timestep duration + pub tick_duration: Duration, + pub current_time: bevy::utils::Instant, +} + +impl Default for MultiBevyStepper { + fn default() -> Self { + let frame_duration = Duration::from_millis(10); + let tick_duration = Duration::from_millis(10); + let shared_config = SharedConfig { + tick: TickConfig::new(tick_duration), + ..Default::default() + }; + let sync_config = SyncConfig::default().speedup_factor(1.0); + let prediction_config = PredictionConfig::default(); + let interpolation_config = InterpolationConfig::default(); + let mut stepper = Self::new( + shared_config, + sync_config, + prediction_config, + interpolation_config, + frame_duration, + ); + stepper.init(); + stepper + } +} + +impl MultiBevyStepper { + pub fn new( + shared_config: SharedConfig, + sync_config: SyncConfig, + prediction_config: PredictionConfig, + interpolation_config: InterpolationConfig, + frame_duration: Duration, + ) -> Self { + let now = bevy::utils::Instant::now(); + + // both clients will use the same client id + let server_addr = LOCAL_SOCKET; + + // Shared config + let protocol_id = 0; + let private_key = generate_key(); + let auth_1 = Authentication::Manual { + server_addr, + protocol_id, + private_key, + client_id: TEST_CLIENT_ID_1, + }; + let auth_2 = Authentication::Manual { + server_addr, + protocol_id, + private_key, + client_id: TEST_CLIENT_ID_2, + }; + + // client net config 1: use local channels + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let client_io = client::IoConfig::from_transport(ClientTransport::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }); + let client_params = (LOCAL_SOCKET, to_server_recv, from_server_send); + let net_config_1 = NetConfig::Netcode { + auth: auth_1, + config: client::NetcodeConfig::default(), + io: client_io, + }; + + // TODO: maybe we don't need the server Channels transport and instead we can just have multiple + // concurrent LocalChannel connections? seems easier to reason about! + let server_io_1 = server::IoConfig::from_transport(ServerTransport::Channels { + channels: vec![client_params], + }); + + // client net config 2: use local channels + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let client_io = client::IoConfig::from_transport(ClientTransport::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }); + let client_params = (LOCAL_SOCKET, to_server_recv, from_server_send); + let net_config_2 = NetConfig::Netcode { + auth: auth_2, + config: client::NetcodeConfig::default(), + io: client_io, + }; + + let server_io_2 = server::IoConfig::from_transport(ServerTransport::Channels { + channels: vec![client_params], + }); + + // build server with two distinct transports + let mut server_app = App::new(); + server_app.add_plugins( + MinimalPlugins + .set(TaskPoolPlugin { + task_pool_options: TaskPoolOptions { + compute: TaskPoolThreadAssignmentPolicy { + min_threads: available_parallelism(), + max_threads: std::usize::MAX, + percent: 1.0, + }, + ..default() + }, + }) + .build(), + ); + let netcode_config = NetcodeConfig::default() + .with_protocol_id(protocol_id) + .with_key(private_key); + let config = ServerConfig { + shared: shared_config.clone(), + net: vec![ + server::NetConfig::Netcode { + config: netcode_config.clone(), + io: server_io_1, + }, + server::NetConfig::Netcode { + config: netcode_config, + io: server_io_2, + }, + ], + ..default() + }; + let plugin = server::ServerPlugins::new(config); + server_app.add_plugins((plugin, ProtocolPlugin)); + // Initialize Real time (needed only for the first TimeSystem run) + server_app + .world + .get_resource_mut::>() + .unwrap() + .update_with_instant(now); + + let build_client = |net_config: NetConfig| -> App { + let mut client_app = App::new(); + client_app.add_plugins( + MinimalPlugins + .set(TaskPoolPlugin { + task_pool_options: TaskPoolOptions { + compute: TaskPoolThreadAssignmentPolicy { + min_threads: available_parallelism(), + max_threads: std::usize::MAX, + percent: 1.0, + }, + ..default() + }, + }) + .build(), + ); + + let config = ClientConfig { + shared: shared_config.clone(), + net: net_config, + sync: sync_config.clone(), + prediction: prediction_config, + interpolation: interpolation_config.clone(), + ..default() + }; + let plugin = client::ClientPlugins::new(config); + client_app.add_plugins((plugin, ProtocolPlugin)); + // Initialize Real time (needed only for the first TimeSystem run) + client_app + .world + .get_resource_mut::>() + .unwrap() + .update_with_instant(now); + client_app + }; + + Self { + client_app_1: build_client(net_config_1), + client_app_2: build_client(net_config_2), + server_app, + frame_duration, + tick_duration: shared_config.tick.tick_duration, + current_time: now, + } + } + + pub fn init(&mut self) { + self.server_app.finish(); + self.server_app + .world + .run_system_once(|mut commands: Commands| commands.start_server()); + self.client_app_1.finish(); + self.client_app_1 + .world + .run_system_once(|mut commands: Commands| commands.connect_client()); + self.client_app_2.finish(); + self.client_app_2 + .world + .run_system_once(|mut commands: Commands| commands.connect_client()); + + // Advance the world to let the connection process complete + for _ in 0..100 { + if self + .client_app_1 + .world + .resource::() + .is_synced() + && self + .client_app_2 + .world + .resource::() + .is_synced() + { + return; + } + self.frame_step(); + } + } + + pub(crate) fn advance_time(&mut self, duration: Duration) { + self.current_time += duration; + self.client_app_1 + .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); + self.client_app_2 + .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); + self.server_app + .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); + mock_instant::MockClock::advance(duration); + } +} + +impl Step for MultiBevyStepper { + /// Advance the world by one frame duration + fn frame_step(&mut self) { + self.advance_time(self.frame_duration); + self.client_app_1.update(); + self.client_app_2.update(); + // sleep a bit to make sure that local io receives the packets + std::thread::sleep(Duration::from_millis(1)); + self.server_app.update(); + std::thread::sleep(Duration::from_millis(1)); + } + + fn tick_step(&mut self) { + self.advance_time(self.tick_duration); + self.client_app_1.update(); + self.client_app_2.update(); + // sleep a bit to make sure that local io receives the packets + std::thread::sleep(Duration::from_millis(1)); + self.server_app.update(); + std::thread::sleep(Duration::from_millis(1)); + } +} diff --git a/lightyear/src/tests/protocol.rs b/lightyear/src/tests/protocol.rs index 70ea99406..2787f4ca7 100644 --- a/lightyear/src/tests/protocol.rs +++ b/lightyear/src/tests/protocol.rs @@ -101,24 +101,24 @@ impl Plugin for ProtocolPlugin { app.add_plugins(InputPlugin::::default()); // components app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Full) - .add_interpolation::(ComponentSyncMode::Full) - .add_linear_interpolation_fn::(); + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Simple); + .add_prediction(ComponentSyncMode::Simple); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Once); + .add_prediction(ComponentSyncMode::Once); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Simple) - .add_map_entities::(); + .add_prediction(ComponentSyncMode::Simple) + .add_map_entities(); app.register_component::(ChannelDirection::ServerToClient) - .add_prediction::(ComponentSyncMode::Full) - .add_interpolation::(ComponentSyncMode::Full) - .add_linear_interpolation_fn::(); + .add_prediction(ComponentSyncMode::Full) + .add_interpolation(ComponentSyncMode::Full) + .add_linear_interpolation_fn(); // resources app.register_resource::(ChannelDirection::ServerToClient); diff --git a/lightyear/src/tests/stepper.rs b/lightyear/src/tests/stepper.rs index bb4d78861..1d401d564 100644 --- a/lightyear/src/tests/stepper.rs +++ b/lightyear/src/tests/stepper.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; use std::str::FromStr; -use crate::client::replication::ReplicationConfig; use bevy::ecs::system::RunSystemOnce; use bevy::prelude::{default, App, Commands, Mut, PluginGroup, Real, Time, World}; use bevy::time::TimeUpdateStrategy; @@ -10,11 +9,13 @@ use bevy::MinimalPlugins; use crate::connection::netcode::generate_key; use crate::prelude::client::{ - Authentication, ClientCommands, ClientConfig, InterpolationConfig, PredictionConfig, SyncConfig, + Authentication, ClientCommands, ClientConfig, ClientTransport, InterpolationConfig, + PredictionConfig, SyncConfig, }; -use crate::prelude::server::{NetcodeConfig, ServerCommands, ServerConfig}; +use crate::prelude::server::{NetcodeConfig, ServerCommands, ServerConfig, ServerTransport}; use crate::prelude::*; use crate::tests::protocol::*; +use crate::transport::LOCAL_SOCKET; pub const TEST_CLIENT_ID: u64 = 111; @@ -50,7 +51,7 @@ impl Default for BevyStepper { incoming_loss: 0.0, }; let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); + let prediction_config = PredictionConfig::default(); let interpolation_config = InterpolationConfig::default(); let mut stepper = Self::new( shared_config, @@ -76,22 +77,21 @@ impl BevyStepper { frame_duration: Duration, ) -> Self { // tracing_subscriber::FmtSubscriber::builder() - // // .with_span_events(FmtSpan::ENTER) // .with_max_level(tracing::Level::INFO) // .init(); // Use local channels instead of UDP for testing - let addr = SocketAddr::from_str("127.0.0.1:0").unwrap(); + let addr = LOCAL_SOCKET; // channels to receive a message from/to server let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); - let client_io = IoConfig::from_transport(TransportConfig::LocalChannel { + let client_io = client::IoConfig::from_transport(ClientTransport::LocalChannel { send: to_server_send, recv: from_server_recv, }) .with_conditioner(conditioner.clone()); - let server_io = IoConfig::from_transport(TransportConfig::Channels { + let server_io = server::IoConfig::from_transport(ServerTransport::Channels { channels: vec![(addr, to_server_recv, from_server_send)], }) .with_conditioner(conditioner.clone()); @@ -113,13 +113,9 @@ impl BevyStepper { shared: shared_config.clone(), net: vec![net_config], ping: PingConfig::default(), - replication: server::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, ..default() }; - let plugin = server::ServerPlugin::new(config); + let plugin = server::ServerPlugins::new(config); server_app.add_plugins((plugin, ProtocolPlugin)); // Setup client @@ -141,13 +137,9 @@ impl BevyStepper { sync: sync_config, prediction: prediction_config, interpolation: interpolation_config, - replication: client::ReplicationConfig { - enable_send: true, - enable_receive: true, - }, ..default() }; - let plugin = client::ClientPlugin::new(config); + let plugin = client::ClientPlugins::new(config); client_app.add_plugins((plugin, ProtocolPlugin)); // Initialize Real time (needed only for the first TimeSystem run) diff --git a/lightyear/src/transport/channels.rs b/lightyear/src/transport/channels.rs index 0e7c1b92c..21345adaf 100644 --- a/lightyear/src/transport/channels.rs +++ b/lightyear/src/transport/channels.rs @@ -7,10 +7,13 @@ use crossbeam_channel::{Receiver, Select, Sender}; use self_cell::self_cell; use tracing::info; +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::io::IoState; use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, LOCAL_SOCKET, + BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, LOCAL_SOCKET, }; use super::error::{Error, Result}; @@ -51,9 +54,21 @@ impl Channels { } } -impl TransportBuilder for Channels { - fn connect(self) -> Result<(TransportEnum, IoState)> { - Ok((TransportEnum::Channels(self), IoState::Connected)) +impl ServerTransportBuilder for Channels { + fn start( + self, + ) -> Result<( + ServerTransportEnum, + IoState, + Option, + Option, + )> { + Ok(( + ServerTransportEnum::Channels(self), + IoState::Connected, + None, + None, + )) } } @@ -62,8 +77,8 @@ impl Transport for Channels { LOCAL_SOCKET } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - (Box::new(self.sender), Box::new(self.receiver), None) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/config.rs b/lightyear/src/transport/config.rs index d6bd53150..342dfa1ef 100644 --- a/lightyear/src/transport/config.rs +++ b/lightyear/src/transport/config.rs @@ -1,228 +1,18 @@ -use bevy::prelude::Reflect; -use std::fmt::{Debug, Formatter}; -use std::net::{IpAddr, SocketAddr}; - -use crossbeam_channel::{Receiver, Sender}; - -#[cfg(all(feature = "webtransport", not(target_family = "wasm")))] -use { - crate::transport::webtransport::server::WebTransportServerSocketBuilder, - wtransport::tls::Identity, -}; - -use crate::prelude::Io; -use crate::transport::channels::Channels; -use crate::transport::dummy::DummyIo; -use crate::transport::error::Result; -use crate::transport::io::IoStats; -use crate::transport::local::LocalChannelBuilder; -#[cfg(feature = "zstd")] -use crate::transport::middleware::compression::zstd::{ - compression::ZstdCompressor, decompression::ZstdDecompressor, -}; use crate::transport::middleware::compression::CompressionConfig; -use crate::transport::middleware::conditioner::{LinkConditioner, LinkConditionerConfig}; -use crate::transport::middleware::{PacketReceiverWrapper, PacketSenderWrapper}; -#[cfg(not(target_family = "wasm"))] -use crate::transport::udp::UdpSocketBuilder; -#[cfg(feature = "websocket")] -use crate::transport::websocket::client::WebSocketClientSocketBuilder; -#[cfg(all(feature = "websocket", not(target_family = "wasm")))] -use crate::transport::websocket::server::WebSocketServerSocketBuilder; -#[cfg(feature = "webtransport")] -use crate::transport::webtransport::client::WebTransportClientSocketBuilder; -use crate::transport::{BoxedReceiver, Transport, TransportBuilder, TransportBuilderEnum}; - -/// Use this to configure the [`Transport`] that will be used to establish a connection with the -/// remote. -pub enum TransportConfig { - /// Use a [`UdpSocket`](std::net::UdpSocket) - #[cfg(not(target_family = "wasm"))] - UdpSocket(SocketAddr), - /// Use [`WebTransport`](https://wicg.github.io/web-transport/) as a transport layer - #[cfg(feature = "webtransport")] - WebTransportClient { - client_addr: SocketAddr, - server_addr: SocketAddr, - /// On wasm, we need to provide a hash of the certificate to the browser - #[cfg(target_family = "wasm")] - certificate_digest: String, - }, - /// Use [`WebTransport`](https://wicg.github.io/web-transport/) as a transport layer - #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] - WebTransportServer { - server_addr: SocketAddr, - /// Certificate that will be used for authentication - certificate: Identity, - }, - /// Use [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) as a transport - #[cfg(feature = "websocket")] - WebSocketClient { server_addr: SocketAddr }, - /// Use [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) as a transport - #[cfg(all(feature = "websocket", not(target_family = "wasm")))] - WebSocketServer { server_addr: SocketAddr }, - /// Use a crossbeam_channel as a transport. This is useful for testing. - /// This is server-only: each tuple corresponds to a different client. - Channels { - channels: Vec<(SocketAddr, Receiver>, Sender>)>, - }, - /// Use a crossbeam_channel as a transport. This is useful for testing. - /// This is mostly for clients. - LocalChannel { - recv: Receiver>, - send: Sender>, - }, - /// Dummy transport if the connection handles its own io (for example steam sockets) - Dummy, -} - -/// We provide a manual implementation because wtranport's `Identity` does not implement Clone -impl ::core::clone::Clone for TransportConfig { - #[inline] - fn clone(&self) -> TransportConfig { - match self { - #[cfg(not(target_family = "wasm"))] - TransportConfig::UdpSocket(__self_0) => { - TransportConfig::UdpSocket(::core::clone::Clone::clone(__self_0)) - } - #[cfg(feature = "webtransport")] - TransportConfig::WebTransportClient { - client_addr: __self_0, - server_addr: __self_1, - #[cfg(target_family = "wasm")] - certificate_digest: __self_2, - } => TransportConfig::WebTransportClient { - client_addr: ::core::clone::Clone::clone(__self_0), - server_addr: ::core::clone::Clone::clone(__self_1), - #[cfg(target_family = "wasm")] - certificate_digest: ::core::clone::Clone::clone(__self_2), - }, - #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] - TransportConfig::WebTransportServer { - server_addr: __self_0, - certificate: __self_1, - } => TransportConfig::WebTransportServer { - server_addr: ::core::clone::Clone::clone(__self_0), - certificate: __self_1.clone_identity(), - }, - #[cfg(feature = "websocket")] - TransportConfig::WebSocketClient { - server_addr: __self_0, - } => TransportConfig::WebSocketClient { - server_addr: ::core::clone::Clone::clone(__self_0), - }, - #[cfg(all(feature = "websocket", not(target_family = "wasm")))] - TransportConfig::WebSocketServer { - server_addr: __self_0, - } => TransportConfig::WebSocketServer { - server_addr: ::core::clone::Clone::clone(__self_0), - }, - TransportConfig::Channels { channels: __self_0 } => TransportConfig::Channels { - channels: ::core::clone::Clone::clone(__self_0), - }, - TransportConfig::LocalChannel { - recv: __self_0, - send: __self_1, - } => TransportConfig::LocalChannel { - recv: ::core::clone::Clone::clone(__self_0), - send: ::core::clone::Clone::clone(__self_1), - }, - TransportConfig::Dummy => TransportConfig::Dummy, - } - } -} - -impl TransportConfig { - fn build(self) -> TransportBuilderEnum { - match self { - #[cfg(not(target_family = "wasm"))] - TransportConfig::UdpSocket(addr) => { - TransportBuilderEnum::UdpSocket(UdpSocketBuilder { local_addr: addr }) - } - #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] - TransportConfig::WebTransportClient { - client_addr, - server_addr, - } => TransportBuilderEnum::WebTransportClient(WebTransportClientSocketBuilder { - client_addr, - server_addr, - }), - #[cfg(all(feature = "webtransport", target_family = "wasm"))] - TransportConfig::WebTransportClient { - client_addr, - server_addr, - certificate_digest, - } => TransportBuilderEnum::WebTransportClient(WebTransportClientSocketBuilder { - client_addr, - server_addr, - certificate_digest, - }), - #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] - TransportConfig::WebTransportServer { - server_addr, - certificate, - } => TransportBuilderEnum::WebTransportServer(WebTransportServerSocketBuilder { - server_addr, - certificate, - }), - #[cfg(feature = "websocket")] - TransportConfig::WebSocketClient { server_addr } => { - TransportBuilderEnum::WebSocketClient(WebSocketClientSocketBuilder { server_addr }) - } - #[cfg(all(feature = "websocket", not(target_family = "wasm")))] - TransportConfig::WebSocketServer { server_addr } => { - TransportBuilderEnum::WebSocketServer(WebSocketServerSocketBuilder { server_addr }) - } - TransportConfig::Channels { channels } => { - TransportBuilderEnum::Channels(Channels::new(channels)) - } - TransportConfig::LocalChannel { recv, send } => { - TransportBuilderEnum::LocalChannel(LocalChannelBuilder { recv, send }) - } - TransportConfig::Dummy => TransportBuilderEnum::Dummy(DummyIo), - } - } -} - -// TODO: derive Debug directly on TransportConfig once the new version of wtransport is out -impl Debug for TransportConfig { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} +use crate::transport::middleware::conditioner::LinkConditionerConfig; +use bevy::prelude::Reflect; -#[derive(Clone, Debug, Reflect)] +#[derive(Clone, Debug, Default, Reflect)] #[reflect(from_reflect = false)] -pub struct IoConfig { +pub struct SharedIoConfig { #[reflect(ignore)] - pub transport: TransportConfig, + pub transport: T, pub conditioner: Option, pub compression: CompressionConfig, } -impl Default for IoConfig { - #[cfg(not(target_family = "wasm"))] - fn default() -> Self { - Self { - transport: TransportConfig::UdpSocket(SocketAddr::new(IpAddr::from([127, 0, 0, 1]), 0)), - conditioner: None, - compression: CompressionConfig::default(), - } - } - - #[cfg(target_family = "wasm")] - fn default() -> Self { - let (send, recv) = crossbeam_channel::unbounded(); - Self { - transport: TransportConfig::LocalChannel { recv, send }, - conditioner: None, - compression: CompressionConfig::default(), - } - } -} - -impl IoConfig { - pub fn from_transport(transport: TransportConfig) -> Self { +impl SharedIoConfig { + pub fn from_transport(transport: T) -> Self { Self { transport, conditioner: None, @@ -238,36 +28,4 @@ impl IoConfig { self.compression = compression_config; self } - - pub fn connect(self) -> Result { - let (transport, state) = self.transport.build().connect()?; - let local_addr = transport.local_addr(); - #[allow(unused_mut)] - let (mut sender, receiver, close_fn) = transport.split(); - #[allow(unused_mut)] - let mut receiver: BoxedReceiver = if let Some(conditioner_config) = self.conditioner { - let conditioner = LinkConditioner::new(conditioner_config); - Box::new(conditioner.wrap(receiver)) - } else { - Box::new(receiver) - }; - match self.compression { - CompressionConfig::None => {} - #[cfg(feature = "zstd")] - CompressionConfig::Zstd { level } => { - let compressor = ZstdCompressor::new(level); - sender = Box::new(compressor.wrap(sender)); - let decompressor = ZstdDecompressor::new(); - receiver = Box::new(decompressor.wrap(receiver)); - } - } - Ok(Io { - local_addr, - sender, - receiver, - close_fn, - state, - stats: IoStats::default(), - }) - } } diff --git a/lightyear/src/transport/dummy.rs b/lightyear/src/transport/dummy.rs index 494a027ca..1d88df686 100644 --- a/lightyear/src/transport/dummy.rs +++ b/lightyear/src/transport/dummy.rs @@ -1,20 +1,53 @@ //! Dummy io for connections that provide their own way of sending and receiving raw bytes (for example steamworks). -use std::net::SocketAddr; - +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::io::IoState; +use crate::transport::udp::{UdpSocket, UdpSocketBuffer, UdpSocketBuilder}; use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, LOCAL_SOCKET, + BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, LOCAL_SOCKET, MTU, }; +use std::net::SocketAddr; use super::error::Result; #[derive(Clone, Copy, Debug, PartialEq)] pub struct DummyIo; -impl TransportBuilder for DummyIo { - fn connect(self) -> Result<(TransportEnum, IoState)> { - Ok((TransportEnum::Dummy(self), IoState::Connected)) +impl ClientTransportBuilder for DummyIo { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { + Ok(( + ClientTransportEnum::Dummy(self), + IoState::Connected, + None, + None, + )) + } +} + +impl ServerTransportBuilder for DummyIo { + fn start( + self, + ) -> Result<( + ServerTransportEnum, + IoState, + Option, + Option, + )> { + Ok(( + ServerTransportEnum::Dummy(self), + IoState::Connected, + None, + None, + )) } } @@ -23,8 +56,8 @@ impl Transport for DummyIo { LOCAL_SOCKET } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - (Box::new(self), Box::new(self), None) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self), Box::new(self)) } } diff --git a/lightyear/src/transport/error.rs b/lightyear/src/transport/error.rs index 548c71497..0e697ff57 100644 --- a/lightyear/src/transport/error.rs +++ b/lightyear/src/transport/error.rs @@ -12,4 +12,32 @@ pub enum Error { #[cfg(all(feature = "websocket", not(target_family = "wasm")))] #[error(transparent)] WebSocket(#[from] tokio_tungstenite::tungstenite::error::Error), + #[error("could not send message via channel: {0}")] + Channel(String), + #[error("requested by user")] + UserRequest, +} + +#[allow(unused_qualifications)] +impl ::core::convert::From> for Error { + #[allow(deprecated)] + fn from(source: async_channel::SendError) -> Self { + Error::Channel(source.to_string()) + } +} + +#[allow(unused_qualifications)] +impl ::core::convert::From> for Error { + #[allow(deprecated)] + fn from(source: crossbeam_channel::SendError) -> Self { + Error::Channel(source.to_string()) + } +} + +#[allow(unused_qualifications)] +impl ::core::convert::From> for Error { + #[allow(deprecated)] + fn from(source: tokio::sync::mpsc::error::SendError) -> Self { + Error::Channel(source.to_string()) + } } diff --git a/lightyear/src/transport/io.rs b/lightyear/src/transport/io.rs index 88e6310ed..f26001163 100644 --- a/lightyear/src/transport/io.rs +++ b/lightyear/src/transport/io.rs @@ -1,17 +1,16 @@ //! Wrapper around a transport, that can perform additional transformations such as //! bandwidth monitoring or compression +use async_channel::Receiver; use std::fmt::{Debug, Formatter}; use std::net::{IpAddr, SocketAddr}; use bevy::app::{App, Plugin}; use bevy::diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic}; -use bevy::prelude::{Real, Res, Resource, Time}; -use crossbeam_channel::{Receiver, Sender}; +use bevy::prelude::{Deref, DerefMut, Real, Res, Resource, Time}; #[cfg(feature = "metrics")] use metrics; use tracing::info; -use crate::transport::local::{LocalChannel, LocalChannelBuilder}; use crate::transport::middleware::conditioner::{ ConditionedPacketReceiver, LinkConditioner, LinkConditionerConfig, PacketLinkConditioner, }; @@ -19,25 +18,17 @@ use crate::transport::middleware::PacketReceiverWrapper; use crate::transport::{PacketReceiver, PacketSender, Transport}; use super::error::{Error, Result}; -use super::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, TransportBuilder, TransportBuilderEnum, LOCAL_SOCKET, -}; +use super::{BoxedReceiver, BoxedSender}; /// Connected io layer that can send/receive bytes #[derive(Resource)] -pub struct Io { +pub struct BaseIo { pub(crate) local_addr: SocketAddr, pub(crate) sender: BoxedSender, pub(crate) receiver: BoxedReceiver, - pub(crate) close_fn: Option, pub(crate) state: IoState, pub(crate) stats: IoStats, -} - -impl Default for Io { - fn default() -> Self { - panic!("Io::default() is not implemented. Please provide an io"); - } + pub(crate) context: T, } // TODO: add stats/compression to middleware @@ -49,7 +40,7 @@ pub struct IoStats { pub packets_received: usize, } -impl Io { +impl BaseIo { pub fn local_addr(&self) -> SocketAddr { self.local_addr } @@ -62,22 +53,15 @@ impl Io { pub fn stats(&self) -> &IoStats { &self.stats } - - pub fn close(&mut self) -> Result<()> { - if let Some(close_fn) = std::mem::take(&mut self.close_fn) { - close_fn()?; - } - Ok(()) - } } -impl Debug for Io { +impl Debug for BaseIo { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Io").finish() } } -impl PacketReceiver for Io { +impl PacketReceiver for BaseIo { fn recv(&mut self) -> Result> { // todo: bandwidth monitoring self.receiver.as_mut().recv().map(|x| { @@ -95,7 +79,7 @@ impl PacketReceiver for Io { } } -impl PacketSender for Io { +impl PacketSender for BaseIo { fn send(&mut self, payload: &[u8], address: &SocketAddr) -> Result<()> { // todo: bandwidth monitoring #[cfg(feature = "metrics")] @@ -171,11 +155,10 @@ impl Plugin for IoDiagnosticsPlugin { } } -/// Tracks the state of creating the Io +/// Tracks the state of the Io +#[derive(Debug, PartialEq)] pub(crate) enum IoState { - Connecting { - error_channel: async_channel::Receiver>, - }, + Connecting, Connected, Disconnected, } diff --git a/lightyear/src/transport/local.rs b/lightyear/src/transport/local.rs index d217914e7..5785398e9 100644 --- a/lightyear/src/transport/local.rs +++ b/lightyear/src/transport/local.rs @@ -4,10 +4,14 @@ use std::net::SocketAddr; use crossbeam_channel::{Receiver, Sender}; +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::io::IoState; +use crate::transport::udp::UdpSocketBuilder; use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, LOCAL_SOCKET, + BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, LOCAL_SOCKET, }; use super::error::{Error, Result}; @@ -18,17 +22,32 @@ pub(crate) struct LocalChannelBuilder { pub(crate) send: Sender>, } -impl TransportBuilder for LocalChannelBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl LocalChannelBuilder { + fn build(self) -> LocalChannel { + LocalChannel { + sender: LocalChannelSender { send: self.send }, + receiver: LocalChannelReceiver { + buffer: vec![], + recv: self.recv, + }, + } + } +} + +impl ClientTransportBuilder for LocalChannelBuilder { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { Ok(( - TransportEnum::LocalChannel(LocalChannel { - sender: LocalChannelSender { send: self.send }, - receiver: LocalChannelReceiver { - buffer: vec![], - recv: self.recv, - }, - }), + ClientTransportEnum::LocalChannel(self.build()), IoState::Connected, + None, + None, )) } } @@ -43,8 +62,8 @@ impl Transport for LocalChannel { LOCAL_SOCKET } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - (Box::new(self.sender), Box::new(self.receiver), None) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/middleware/compression/zstd.rs b/lightyear/src/transport/middleware/compression/zstd.rs index 8631b0013..53b135200 100644 --- a/lightyear/src/transport/middleware/compression/zstd.rs +++ b/lightyear/src/transport/middleware/compression/zstd.rs @@ -108,7 +108,7 @@ pub(crate) mod decompression { #[cfg(test)] mod tests { - use crate::prelude::{IoConfig, TransportConfig}; + use crate::prelude::{SharedIoConfig, TransportConfig}; use crate::transport::middleware::compression::CompressionConfig; use crate::transport::LOCAL_SOCKET; @@ -117,7 +117,7 @@ mod tests { let (send, recv) = crossbeam_channel::unbounded(); let config = TransportConfig::LocalChannel { send, recv }; - let io_config = IoConfig { + let io_config = SharedIoConfig { transport: config, conditioner: None, compression: CompressionConfig::Zstd { level: 0 }, diff --git a/lightyear/src/transport/mod.rs b/lightyear/src/transport/mod.rs index 9aa274159..611d14c64 100644 --- a/lightyear/src/transport/mod.rs +++ b/lightyear/src/transport/mod.rs @@ -6,6 +6,9 @@ use enum_dispatch::enum_dispatch; use error::Result; +// required import for enum dispatch to work +use crate::client::io::transport::ClientTransportEnum; +use crate::server::io::transport::ServerTransportEnum; use crate::transport::channels::Channels; use crate::transport::dummy::DummyIo; use crate::transport::io::IoState; @@ -63,76 +66,16 @@ pub(crate) const MTU: usize = 1472; pub(crate) type BoxedSender = Box; pub(crate) type BoxedReceiver = Box; -// pub(crate) trait CloseFn: Send + Sync {} -// impl Result<()> + Send + Sync> CloseFn for T {} -// pub(crate) type BoxedCloseFn = Box; -pub(crate) type BoxedCloseFn = Box Result<()>) + Send + Sync>; - -/// Transport combines a PacketSender and a PacketReceiver -/// -/// This trait is used to abstract the raw transport layer that sends and receives packets. -/// There are multiple implementations of this trait, such as UdpSocket, WebSocket, WebTransport, etc. -#[enum_dispatch] -pub(crate) trait TransportBuilder: Send + Sync { - /// Attempt to: - /// - connect to the remote (for clients) - /// - listen to incoming connections (for server) - fn connect(self) -> Result<(TransportEnum, IoState)>; - - // TODO maybe add a `async fn ready() -> bool` function? -} #[enum_dispatch] pub(crate) trait Transport { /// Return the local socket address for this transport fn local_addr(&self) -> SocketAddr; - /// Split the transport into a sender, receiver and close function + /// Split the transport into a sender, receiver. /// /// This is useful to have parallel mutable access to the sender and the retriever - fn split(self) -> (BoxedSender, BoxedReceiver, Option); -} - -#[enum_dispatch(TransportBuilder)] -pub(crate) enum TransportBuilderEnum { - #[cfg(not(target_family = "wasm"))] - UdpSocket(UdpSocketBuilder), - #[cfg(feature = "webtransport")] - WebTransportClient(WebTransportClientSocketBuilder), - #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] - WebTransportServer(WebTransportServerSocketBuilder), - #[cfg(feature = "websocket")] - WebSocketClient(WebSocketClientSocketBuilder), - #[cfg(all(feature = "websocket", not(target_family = "wasm")))] - WebSocketServer(WebSocketServerSocketBuilder), - Channels(Channels), - LocalChannel(LocalChannelBuilder), - Dummy(DummyIo), -} - -// impl Default for TransportBuilderEnum { -// fn default() -> Self { -// Self::Dummy(DummyIo) -// } -// } - -// TODO: maybe box large items? -#[allow(clippy::large_enum_variant)] -#[enum_dispatch(Transport)] -pub(crate) enum TransportEnum { - #[cfg(not(target_family = "wasm"))] - UdpSocket(UdpSocket), - #[cfg(feature = "webtransport")] - WebTransportClient(WebTransportClientSocket), - #[cfg(all(feature = "webtransport", not(target_family = "wasm")))] - WebTransportServer(WebTransportServerSocket), - #[cfg(feature = "websocket")] - WebSocketClient(WebSocketClientSocket), - #[cfg(all(feature = "websocket", not(target_family = "wasm")))] - WebSocketServer(WebSocketServerSocket), - Channels(Channels), - LocalChannel(LocalChannel), - Dummy(DummyIo), + fn split(self) -> (BoxedSender, BoxedReceiver); } /// Send data to a remote address diff --git a/lightyear/src/transport/udp.rs b/lightyear/src/transport/udp.rs index 3b0656d9d..fd35893d0 100644 --- a/lightyear/src/transport/udp.rs +++ b/lightyear/src/transport/udp.rs @@ -2,13 +2,14 @@ use std::net::SocketAddr; use std::sync::{Arc, Mutex}; -use anyhow::Context; - +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::io::IoState; -use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, MTU, -}; +use crate::transport::{BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, MTU}; +use anyhow::Context; +use async_channel::Receiver; use super::error::Result; @@ -16,8 +17,8 @@ pub struct UdpSocketBuilder { pub(crate) local_addr: SocketAddr, } -impl TransportBuilder for UdpSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl UdpSocketBuilder { + fn build(self) -> Result { let udp_socket = std::net::UdpSocket::bind(self.local_addr)?; let local_addr = udp_socket.local_addr()?; let socket = Arc::new(Mutex::new(udp_socket)); @@ -27,13 +28,46 @@ impl TransportBuilder for UdpSocketBuilder { buffer: [0; MTU], }; let receiver = sender.clone(); + Ok(UdpSocket { + local_addr, + sender, + receiver, + }) + } +} + +impl ClientTransportBuilder for UdpSocketBuilder { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { Ok(( - TransportEnum::UdpSocket(UdpSocket { - local_addr, - sender, - receiver, - }), + ClientTransportEnum::UdpSocket(self.build()?), IoState::Connected, + None, + None, + )) + } +} + +impl ServerTransportBuilder for UdpSocketBuilder { + fn start( + self, + ) -> Result<( + ServerTransportEnum, + IoState, + Option, + Option, + )> { + Ok(( + ServerTransportEnum::UdpSocket(self.build()?), + IoState::Connected, + None, + None, )) } } @@ -50,8 +84,8 @@ impl Transport for UdpSocket { self.local_addr } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - (Box::new(self.sender), Box::new(self.receiver), None) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } @@ -100,29 +134,31 @@ mod tests { use std::net::SocketAddr; use std::str::FromStr; + use crate::client::io::transport::ClientTransportBuilder; + use crate::server::io::transport::ServerTransportBuilder; use anyhow::Context; use bevy::utils::Duration; use crate::transport::middleware::conditioner::{LinkConditioner, LinkConditionerConfig}; use crate::transport::middleware::PacketReceiverWrapper; use crate::transport::udp::UdpSocketBuilder; - use crate::transport::{PacketReceiver, PacketSender, Transport, TransportBuilder}; + use crate::transport::{PacketReceiver, PacketSender, Transport}; #[test] fn test_udp_socket() -> Result<(), anyhow::Error> { // let the OS assign a port let local_addr = SocketAddr::from_str("127.0.0.1:0")?; - let (client_socket, _) = UdpSocketBuilder { local_addr } + let (client_socket, _, _, _) = UdpSocketBuilder { local_addr } .connect() .context("could not connect to socket")?; let client_addr = client_socket.local_addr(); - let (mut client_sender, _, _) = client_socket.split(); + let (mut client_sender, _) = client_socket.split(); - let (server_socket, _) = UdpSocketBuilder { local_addr } - .connect() + let (server_socket, _, _, _) = UdpSocketBuilder { local_addr } + .start() .context("could not connect to socket")?; let server_addr = server_socket.local_addr(); - let (_, mut server_receiver, _) = server_socket.split(); + let (_, mut server_receiver) = server_socket.split(); let msg = b"hello world"; client_sender.send(msg, &server_addr)?; @@ -145,17 +181,17 @@ mod tests { // let the OS assign a port let local_addr = SocketAddr::from_str("127.0.0.1:0")?; - let (client_socket, _) = UdpSocketBuilder { local_addr } + let (client_socket, _, _, _) = UdpSocketBuilder { local_addr } .connect() .context("could not connect to socket")?; let client_addr = client_socket.local_addr(); - let (mut client_sender, _, _) = client_socket.split(); + let (mut client_sender, _) = client_socket.split(); - let (server_socket, _) = UdpSocketBuilder { local_addr } - .connect() + let (server_socket, _, _, _) = UdpSocketBuilder { local_addr } + .start() .context("could not connect to socket")?; let server_addr = server_socket.local_addr(); - let (_, server_receiver, _) = server_socket.split(); + let (_, server_receiver) = server_socket.split(); let mut conditioned_server_receiver = LinkConditioner::new(LinkConditionerConfig { incoming_latency: Duration::from_millis(100), diff --git a/lightyear/src/transport/websocket/client_native.rs b/lightyear/src/transport/websocket/client_native.rs index 9a17aaa60..4251f2467 100644 --- a/lightyear/src/transport/websocket/client_native.rs +++ b/lightyear/src/transport/websocket/client_native.rs @@ -27,22 +27,30 @@ use tokio_tungstenite::{ use tracing::{debug, info, trace}; use tracing_log::log::error; +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEvent, ClientIoEventReceiver, ClientNetworkEventSender}; use crate::transport::error::{Error, Result}; use crate::transport::io::IoState; use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, LOCAL_SOCKET, MTU, + BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, LOCAL_SOCKET, MTU, }; pub(crate) struct WebSocketClientSocketBuilder { pub(crate) server_addr: SocketAddr, } -impl TransportBuilder for WebSocketClientSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl ClientTransportBuilder for WebSocketClientSocketBuilder { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { let (serverbound_tx, mut serverbound_rx) = unbounded_channel::(); let (clientbound_tx, clientbound_rx) = unbounded_channel::(); - let (close_tx, mut close_rx) = mpsc::channel(1); + let (close_tx, close_rx) = async_channel::bounded(1); // channels used to check the status of the io task let (status_tx, status_rx) = async_channel::bounded(1); @@ -64,11 +72,15 @@ impl TransportBuilder for WebSocketClientSocketBuilder { { Ok((ws_stream, _)) => ws_stream, Err(e) => { - status_tx.send(Some(e.into())).await.unwrap(); + status_tx + .send(ClientIoEvent::Disconnected(e.into())) + .await + .unwrap(); return; } }; info!("WebSocket handshake has been successfully completed"); + status_tx.send(ClientIoEvent::Connected).await.unwrap(); let (mut write, mut read) = ws_stream.split(); let send_handle = IoTaskPool::get().spawn(Compat::new(async move { @@ -97,22 +109,26 @@ impl TransportBuilder for WebSocketClientSocketBuilder { } })); // wait for a signal that the io should be closed - close_rx.recv().await; + let _ = close_rx.recv().await; + let _ = status_tx + .send(ClientIoEvent::Disconnected( + std::io::Error::other("websocket closed").into(), + )) + .await; info!("Close websocket connection"); send_handle.cancel().await; recv_handle.cancel().await; })) .detach(); Ok(( - TransportEnum::WebSocketClient(WebSocketClientSocket { + ClientTransportEnum::WebSocketClient(WebSocketClientSocket { local_addr: self.server_addr, sender, receiver, - close_sender: close_tx, }), - IoState::Connecting { - error_channel: status_rx, - }, + IoState::Connecting, + Some(ClientIoEventReceiver(status_rx)), + Some(ClientNetworkEventSender(close_tx)), )) } } @@ -121,7 +137,6 @@ pub struct WebSocketClientSocket { local_addr: SocketAddr, sender: WebSocketClientSocketSender, receiver: WebSocketClientSocketReceiver, - close_sender: mpsc::Sender<()>, } impl WebSocketClientSocket { @@ -150,17 +165,8 @@ impl Transport for WebSocketClientSocket { LOCAL_SOCKET } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - let close_fn = move || { - self.close_sender - .blocking_send(()) - .map_err(|e| Error::from(std::io::Error::other(format!("close error: {:?}", e)))) - }; - ( - Box::new(self.sender), - Box::new(self.receiver), - Some(Box::new(close_fn)), - ) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/websocket/client_wasm.rs b/lightyear/src/transport/websocket/client_wasm.rs index deaabe75a..7720d533c 100644 --- a/lightyear/src/transport/websocket/client_wasm.rs +++ b/lightyear/src/transport/websocket/client_wasm.rs @@ -17,22 +17,30 @@ use web_sys::{ BinaryType, CloseEvent, ErrorEvent, MessageEvent, WebSocket, }; +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; use crate::transport::error::{Error, Result}; use crate::transport::io::IoState; use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, LOCAL_SOCKET, MTU, + BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, LOCAL_SOCKET, MTU, }; pub(crate) struct WebSocketClientSocketBuilder { pub(crate) server_addr: SocketAddr, } -impl TransportBuilder for WebSocketClientSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl ClientTransportBuilder for WebSocketClientSocketBuilder { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { let (serverbound_tx, serverbound_rx) = unbounded_channel::>(); let (clientbound_tx, clientbound_rx) = unbounded_channel::>(); - let (close_tx, mut close_rx) = mpsc::channel(1); + let (close_tx, mut close_rx) = crossbeam_channel::bounded(1); let sender = WebSocketClientSocketSender { serverbound_tx }; @@ -107,12 +115,10 @@ impl TransportBuilder for WebSocketClientSocketBuilder { listen_close_signal_callback.forget(); Ok(( - TransportEnum::WebSocketClient(WebSocketClientSocket { - sender, - receiver, - close_sender: close_tx, - }), + ClientTransportEnum::WebSocketClient(WebSocketClientSocket { sender, receiver }), IoState::Connected, + None, + Some(ClientNetworkEventSender(close_tx)), )) } } @@ -120,7 +126,6 @@ impl TransportBuilder for WebSocketClientSocketBuilder { pub struct WebSocketClientSocket { sender: WebSocketClientSocketSender, receiver: WebSocketClientSocketReceiver, - close_sender: mpsc::Sender<()>, } impl Transport for WebSocketClientSocket { @@ -128,17 +133,8 @@ impl Transport for WebSocketClientSocket { LOCAL_SOCKET } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - let close_fn = move || { - self.close_sender - .blocking_send(()) - .map_err(|e| Error::from(std::io::Error::other(format!("close error: {:?}", e)))) - }; - ( - Box::new(self.sender), - Box::new(self.receiver), - Some(Box::new(close_fn)), - ) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/websocket/server.rs b/lightyear/src/transport/websocket/server.rs index b1024de7f..bd11c5346 100644 --- a/lightyear/src/transport/websocket/server.rs +++ b/lightyear/src/transport/websocket/server.rs @@ -17,26 +17,38 @@ use tokio::{ sync::mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver, UnboundedSender}, }; use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; -use tracing::{info, trace}; +use tracing::{debug, info, trace}; use tracing_log::log::error; +use wtransport::datagram::Datagram; +use wtransport::Connection; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEvent, ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::error::{Error, Result}; use crate::transport::io::IoState; -use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, MTU, -}; +use crate::transport::webtransport::server::WebTransportServerSocket; +use crate::transport::{BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, MTU}; pub(crate) struct WebSocketServerSocketBuilder { pub(crate) server_addr: SocketAddr, } -impl TransportBuilder for WebSocketServerSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl ServerTransportBuilder for WebSocketServerSocketBuilder { + fn start( + self, + ) -> Result<( + ServerTransportEnum, + IoState, + Option, + Option, + )> { let (serverbound_tx, serverbound_rx) = unbounded_channel::<(SocketAddr, Message)>(); let clientbound_tx_map = ClientBoundTxMap::new(Mutex::new(HashMap::new())); + // channels used to cancel the task + let (close_tx, close_rx) = async_channel::unbounded(); // channels used to check the status of the io task - let (status_tx, status_rx) = async_channel::bounded(1); + let (status_tx, status_rx) = async_channel::unbounded(); + let addr_to_task = Arc::new(Mutex::new(HashMap::new())); let sender = WebSocketServerSocketSender { server_addr: self.server_addr, @@ -53,77 +65,58 @@ impl TransportBuilder for WebSocketServerSocketBuilder { let listener = match TcpListener::bind(self.server_addr).await { Ok(l) => l, Err(e) => { - status_tx.send(Some(e.into())).await.unwrap(); + status_tx + .send(ServerIoEvent::ServerDisconnected(e.into())) + .await + .unwrap(); return; } }; info!("Starting server websocket task"); - while let Ok((stream, addr)) = listener.accept().await { - let clientbound_tx_map = clientbound_tx_map.clone(); - let serverbound_tx = serverbound_tx.clone(); - - let ws_stream = tokio_tungstenite::accept_async(stream) - .await - .expect("Error during the websocket handshake occurred"); - info!("New WebSocket connection: {}", addr); - - let (clientbound_tx, mut clientbound_rx) = unbounded_channel::(); - let (mut write, mut read) = ws_stream.split(); - - clientbound_tx_map - .lock() - .unwrap() - .insert(addr, clientbound_tx); - - let serverbound_tx = serverbound_tx.clone(); - - let clientbound_handle = IoTaskPool::get().spawn(async move { - while let Some(msg) = clientbound_rx.recv().await { - write - .send(msg) - .await - .map_err(|e| { - error!("Encountered error while sending websocket msg: {}", e); - }) - .unwrap(); - } - write.close().await.unwrap_or_else(|e| { - error!("Error closing websocket: {:?}", e); - }); - }); - let serverbound_handle = IoTaskPool::get().spawn(async move { - while let Some(msg) = read.next().await { - match msg { - Ok(msg) => { - serverbound_tx.send((addr, msg)).unwrap_or_else(|e| { - error!("receive websocket error: {:?}", e) - }); + status_tx + .send(ServerIoEvent::ServerConnected) + .await + .unwrap(); + + loop { + tokio::select! { + // event from netcode + Ok(event) = close_rx.recv() => { + match event { + ServerIoEvent::ServerDisconnected(e) => { + debug!("Stopping webtransport io task. Reason: {:?}", e); + drop(addr_to_task); + return; } - Err(e) => { - error!("receive websocket error: {:?}", e); + ServerIoEvent::ClientDisconnected(addr) => { + debug!("Stopping webtransport io task associated with address: {:?} because we received a disconnection signal from netcode", addr); + addr_to_task.lock().unwrap().remove(&addr); + clientbound_tx_map.lock().unwrap().remove(&addr); } + _ => {} } } - }); - - let _closed = - futures_lite::future::race(clientbound_handle, serverbound_handle).await; - - info!("Connection with {} closed", addr); - clientbound_tx_map.lock().unwrap().remove(&addr); - // dropping the task handles cancels them + Ok((stream, addr)) = listener.accept() => { + let clientbound_tx_map = clientbound_tx_map.clone(); + let serverbound_tx = serverbound_tx.clone(); + let task = IoTaskPool::get().spawn(Compat::new( + WebSocketServerSocket::handle_client(addr, stream, serverbound_tx, clientbound_tx_map, status_tx.clone()) + )); + addr_to_task.lock().unwrap().insert(addr, task); + } + } } })) .detach(); Ok(( - TransportEnum::WebSocketServer(WebSocketServerSocket { + ServerTransportEnum::WebSocketServer(WebSocketServerSocket { local_addr: self.server_addr, sender, receiver, }), - IoState::Connecting { - error_channel: status_rx, - }, + IoState::Connecting, + Some(ServerIoEventReceiver(status_rx)), + None, )) } } @@ -156,6 +149,70 @@ impl WebSocketServerSocket { }*/ } +impl WebSocketServerSocket { + async fn handle_client( + addr: SocketAddr, + stream: TcpStream, + serverbound_tx: UnboundedSender<(SocketAddr, Message)>, + clientbound_tx_map: Arc>>>, + status_tx: async_channel::Sender, + ) { + let Ok(ws_stream) = tokio_tungstenite::accept_async(stream) + .await + .inspect_err(|e| error!("An error occured during the websocket handshake: {e:?}")) + else { + return; + }; + info!("New WebSocket connection: {}", addr); + + let (clientbound_tx, mut clientbound_rx) = unbounded_channel::(); + let (mut write, mut read) = ws_stream.split(); + clientbound_tx_map + .lock() + .unwrap() + .insert(addr, clientbound_tx); + + let clientbound_handle = IoTaskPool::get().spawn(async move { + while let Some(msg) = clientbound_rx.recv().await { + write + .send(msg) + .await + .map_err(|e| { + error!("Encountered error while sending websocket msg: {}", e); + }) + .unwrap(); + } + write.close().await.unwrap_or_else(|e| { + error!("Error closing websocket: {:?}", e); + }); + }); + let serverbound_handle = IoTaskPool::get().spawn(async move { + while let Some(msg) = read.next().await { + match msg { + Ok(msg) => { + serverbound_tx + .send((addr, msg)) + .unwrap_or_else(|e| error!("receive websocket error: {:?}", e)); + } + Err(e) => { + error!("receive websocket error: {:?}", e); + } + } + } + }); + + let _closed = futures_lite::future::race(clientbound_handle, serverbound_handle).await; + + info!("Connection with {} closed", addr); + clientbound_tx_map.lock().unwrap().remove(&addr); + // notify netcode that the io task got disconnected + let _ = status_tx + .send(ServerIoEvent::ClientDisconnected(addr)) + .await; + // dropping the task handles cancels them + } +} + type ClientBoundTxMap = Arc>>>; impl Transport for WebSocketServerSocket { @@ -163,8 +220,8 @@ impl Transport for WebSocketServerSocket { self.local_addr } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - (Box::new(self.sender), Box::new(self.receiver), None) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/webtransport/client_native.rs b/lightyear/src/transport/webtransport/client_native.rs index 8894a91ea..69a0ff002 100644 --- a/lightyear/src/transport/webtransport/client_native.rs +++ b/lightyear/src/transport/webtransport/client_native.rs @@ -1,5 +1,6 @@ #![cfg(not(target_family = "wasm"))] //! WebTransport client implementation. +use async_channel::Receiver; use std::net::SocketAddr; use std::sync::Arc; @@ -10,28 +11,35 @@ use tokio::sync::mpsc::error::TryRecvError; use tracing::{debug, error, info, trace, warn}; use wtransport; use wtransport::datagram::Datagram; +use wtransport::error::{ConnectingError, ConnectionError}; use wtransport::ClientConfig; +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEvent, ClientIoEventReceiver, ClientNetworkEventSender}; use crate::transport::error::{Error, Result}; use crate::transport::io::IoState; -use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, MTU, -}; +use crate::transport::{BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, MTU}; pub(crate) struct WebTransportClientSocketBuilder { pub(crate) client_addr: SocketAddr, pub(crate) server_addr: SocketAddr, } -impl TransportBuilder for WebTransportClientSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl ClientTransportBuilder for WebTransportClientSocketBuilder { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { let (to_server_sender, mut to_server_receiver) = mpsc::unbounded_channel(); let (from_server_sender, from_server_receiver) = mpsc::unbounded_channel(); // channels used to cancel the task - let (close_tx, mut close_rx) = mpsc::channel(1); + let (close_tx, close_rx) = async_channel::bounded(1); // channels used to check the status of the io task - let (status_tx, status_rx) = async_channel::bounded(1); + let (event_tx, event_rx) = async_channel::bounded(1); IoTaskPool::get().spawn(Compat::new(async move { let config = ClientConfig::builder() @@ -48,7 +56,7 @@ impl TransportBuilder for WebTransportClientSocketBuilder { Ok(e) => {e} Err(e) => { error!("Error creating webtransport endpoint: {:?}", e); - let _ = status_tx.send(Some(e.into())).await; + let _ = event_tx.send(ClientIoEvent::Disconnected(e.into())).await; return } }; @@ -56,7 +64,7 @@ impl TransportBuilder for WebTransportClientSocketBuilder { tokio::select! { _ = close_rx.recv() => { info!("WebTransport connection closed. Reason: client requested disconnection."); - let _ = status_tx.send(Some(std::io::Error::other("received close signal").into())).await; + let _ = event_tx.send(ClientIoEvent::Disconnected(std::io::Error::other("received close signal").into())).await; return } connection = endpoint.connect(&server_url) => { @@ -64,12 +72,12 @@ impl TransportBuilder for WebTransportClientSocketBuilder { Ok(c) => {c} Err(e) => { error!("Error creating webtransport connection: {:?}", e); - let _ = status_tx.send(Some(std::io::Error::other(e).into())).await; + let _ = event_tx.send(ClientIoEvent::Disconnected(std::io::Error::other(e).into())).await; return } }; // signal that the io is connected - status_tx.send(None).await.unwrap(); + event_tx.send(ClientIoEvent::Connected).await.unwrap(); info!("Connected."); let connection = Arc::new(connection); @@ -90,7 +98,9 @@ impl TransportBuilder for WebTransportClientSocketBuilder { from_server_sender.send(data).unwrap(); } Err(e) => { + // all the ConnectionErrors are related to the connection being close, so we can close the task error!("receive_datagram connection error: {:?}", e); + return; } } } @@ -108,12 +118,23 @@ impl TransportBuilder for WebTransportClientSocketBuilder { })); // Wait for a close signal from the close channel, or for the quic connection to be closed tokio::select! { - reason = connection.closed() => {info!("WebTransport connection closed. Reason: {reason:?}")}, - _ = close_rx.recv() => {info!("WebTransport connection closed. Reason: client requested disconnection.");} + reason = connection.closed() => { + info!("WebTransport connection closed. Reason: {reason:?}. Shutting down webtransport tasks."); + event_tx.send(ClientIoEvent::Disconnected(Error::WebTransport(ConnectingError::ConnectionError(reason)))).await.unwrap(); + }, + _ = close_rx.recv() => { + info!("WebTransport connection closed. Reason: client requested disconnection. Shutting down webtransport tasks."); + } } // close the other tasks + + // NOTE: for some reason calling `cancel()` doesn't work (the task still keeps running indefinitely) + // instead we just drop the task handle + // drop(recv_handle); + // drop(send_handle); recv_handle.cancel().await; send_handle.cancel().await; + debug!("WebTransport tasks shut down."); } } })) @@ -126,15 +147,14 @@ impl TransportBuilder for WebTransportClientSocketBuilder { buffer: [0; MTU], }; Ok(( - TransportEnum::WebTransportClient(WebTransportClientSocket { + ClientTransportEnum::WebTransportClient(WebTransportClientSocket { local_addr: self.client_addr, sender, receiver, - close_sender: close_tx, }), - IoState::Connecting { - error_channel: status_rx, - }, + IoState::Connecting, + Some(ClientIoEventReceiver(event_rx)), + Some(ClientNetworkEventSender(close_tx)), )) } } @@ -144,7 +164,6 @@ pub struct WebTransportClientSocket { local_addr: SocketAddr, sender: WebTransportClientPacketSender, receiver: WebTransportClientPacketReceiver, - close_sender: mpsc::Sender<()>, } impl Transport for WebTransportClientSocket { @@ -152,17 +171,8 @@ impl Transport for WebTransportClientSocket { self.local_addr } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - let close_fn = move || { - self.close_sender - .blocking_send(()) - .map_err(|e| Error::from(std::io::Error::other(format!("close error: {:?}", e)))) - }; - ( - Box::new(self.sender), - Box::new(self.receiver), - Some(Box::new(close_fn)), - ) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/webtransport/client_wasm.rs b/lightyear/src/transport/webtransport/client_wasm.rs index eaee10e9f..24a9d2e57 100644 --- a/lightyear/src/transport/webtransport/client_wasm.rs +++ b/lightyear/src/transport/webtransport/client_wasm.rs @@ -11,12 +11,13 @@ use tokio::sync::mpsc::error::TryRecvError; use tracing::{debug, error, info, trace}; use xwt_core::prelude::*; +use crate::client::io::transport::{ClientTransportBuilder, ClientTransportEnum}; +use crate::client::io::{ClientIoEventReceiver, ClientNetworkEventSender}; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::error::{Error, Result}; use crate::transport::io::IoState; -use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, MTU, -}; +use crate::transport::{BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, MTU}; pub struct WebTransportClientSocketBuilder { pub(crate) client_addr: SocketAddr, @@ -24,13 +25,20 @@ pub struct WebTransportClientSocketBuilder { pub(crate) certificate_digest: String, } -impl TransportBuilder for WebTransportClientSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl ClientTransportBuilder for WebTransportClientSocketBuilder { + fn connect( + self, + ) -> Result<( + ClientTransportEnum, + IoState, + Option, + Option, + )> { // TODO: This can exhaust all available memory unless there is some other way to limit the amount of in-flight data in place let (to_server_sender, mut to_server_receiver) = mpsc::unbounded_channel(); let (from_server_sender, from_server_receiver) = mpsc::unbounded_channel(); // channels used to cancel the task - let (close_tx, mut close_rx) = mpsc::channel(1); + let (close_tx, mut close_rx) = crossbeam_channel::bounded(1); // channels used to check the status of the io task let (status_tx, status_rx) = async_channel::bounded(1); @@ -62,7 +70,7 @@ impl TransportBuilder for WebTransportClientSocketBuilder { Err(e) => { error!("Error creating webtransport connection: {:?}", e); status_tx - .send(Some( + .send(ClientIoEvent::Disconnected( std::io::Error::other("error creating webtransport connection").into(), )) .await @@ -75,7 +83,7 @@ impl TransportBuilder for WebTransportClientSocketBuilder { Err(e) => { error!("Error connecting to server: {:?}", e); status_tx - .send(Some( + .send(ClientIoEvent::Disconnected( std::io::Error::other( "error connecting webtransport endpoint to server", ) @@ -87,7 +95,7 @@ impl TransportBuilder for WebTransportClientSocketBuilder { } }; // signal that the io is connected - status_tx.send(None).await.unwrap(); + status_tx.send(ClientIoEvent::Connected).await.unwrap(); let connection = Rc::new(connection); send.send(connection.clone()).unwrap(); send2.send(connection.clone()).unwrap(); @@ -149,6 +157,7 @@ impl TransportBuilder for WebTransportClientSocketBuilder { tokio::select! { reason = wasm_bindgen_futures::JsFuture::from(connection.transport.closed()) => { info!("WebTransport connection closed. Reason: {reason:?}") + status_tx.send(ClientIoEvent::Disconnected(std::io::Error::other(format!("Error: {:?}", reason)).into())).await.unwrap(); }, _ = close_rx.recv() => { connection.transport.close(); @@ -164,15 +173,14 @@ impl TransportBuilder for WebTransportClientSocketBuilder { buffer: [0; MTU], }; Ok(( - TransportEnum::WebTransportClient(WebTransportClientSocket { + ClientTransportEnum::WebTransportClient(WebTransportClientSocket { local_addr: self.client_addr, sender, receiver, - close_sender: close_tx, }), - IoState::Connecting { - error_channel: status_rx, - }, + IoState::Connecting, + Some(ClientIoEventReceiver(status_rx)), + Some(ClientNetworkEventSender(close_tx)), )) } } @@ -182,7 +190,6 @@ pub struct WebTransportClientSocket { local_addr: SocketAddr, sender: WebTransportClientPacketSender, receiver: WebTransportClientPacketReceiver, - close_sender: mpsc::Sender<()>, } impl Transport for WebTransportClientSocket { @@ -190,17 +197,8 @@ impl Transport for WebTransportClientSocket { self.local_addr } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - let close_fn = move || { - self.close_sender - .blocking_send(()) - .map_err(|e| Error::from(std::io::Error::other(format!("close error: {:?}", e)))) - }; - ( - Box::new(self.sender), - Box::new(self.receiver), - Some(Box::new(close_fn)), - ) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } } diff --git a/lightyear/src/transport/webtransport/server.rs b/lightyear/src/transport/webtransport/server.rs index a821f95ed..43d1d9313 100644 --- a/lightyear/src/transport/webtransport/server.rs +++ b/lightyear/src/transport/webtransport/server.rs @@ -1,14 +1,14 @@ //! WebTransport client implementation. -use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use anyhow::Context; use async_compat::Compat; use bevy::tasks::{futures_lite, IoTaskPool}; -use tokio::sync::mpsc; +use bevy::utils::HashMap; use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::{mpsc, oneshot}; use tracing::{debug, error, info, trace}; use wtransport; use wtransport::datagram::Datagram; @@ -18,26 +18,35 @@ use wtransport::tls::Certificate; use wtransport::{Connection, Endpoint}; use wtransport::{Identity, ServerConfig}; +use crate::server::io::transport::{ServerTransportBuilder, ServerTransportEnum}; +use crate::server::io::{ServerIoEvent, ServerIoEventReceiver, ServerNetworkEventSender}; use crate::transport::error::{Error, Result}; use crate::transport::io::IoState; -use crate::transport::{ - BoxedCloseFn, BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, - TransportBuilder, TransportEnum, MTU, -}; +use crate::transport::{BoxedReceiver, BoxedSender, PacketReceiver, PacketSender, Transport, MTU}; pub(crate) struct WebTransportServerSocketBuilder { pub(crate) server_addr: SocketAddr, pub(crate) certificate: Identity, } -impl TransportBuilder for WebTransportServerSocketBuilder { - fn connect(self) -> Result<(TransportEnum, IoState)> { +impl ServerTransportBuilder for WebTransportServerSocketBuilder { + fn start( + self, + ) -> Result<( + ServerTransportEnum, + IoState, + Option, + Option, + )> { let (to_client_sender, to_client_receiver) = mpsc::unbounded_channel::<(Box<[u8]>, SocketAddr)>(); let (from_client_sender, from_client_receiver) = mpsc::unbounded_channel(); + // channels used to cancel the task + let (close_tx, close_rx) = async_channel::unbounded(); // channels used to check the status of the io task - let (status_tx, status_rx) = async_channel::bounded(1); + let (status_tx, status_rx) = async_channel::unbounded(); let to_client_senders = Arc::new(Mutex::new(HashMap::new())); + let addr_to_task = Arc::new(Mutex::new(HashMap::new())); let sender = WebTransportServerSocketSender { server_addr: self.server_addr, @@ -59,41 +68,76 @@ impl TransportBuilder for WebTransportServerSocketBuilder { let endpoint = match wtransport::Endpoint::server(config) { Ok(e) => e, Err(e) => { - status_tx.send(Some(e.into())).await.unwrap(); + status_tx + .send(ServerIoEvent::ServerDisconnected(e.into())) + .await + .unwrap(); return; } }; - info!("Starting server webtransport task"); + status_tx.send(ServerIoEvent::ServerConnected).await.unwrap(); loop { - // clone the channel for each client - let from_client_sender = from_client_sender.clone(); - let to_client_senders = to_client_senders.clone(); - - // new client connecting - let incoming_session = endpoint.accept().await; - - // TODO: when a client disconnects (i.e. the connection is closed), close the task here as well - IoTaskPool::get() - .spawn(Compat::new(WebTransportServerSocket::handle_client( - incoming_session, - from_client_sender, - to_client_senders, - ))) - .detach(); + tokio::select! { + // event from netcode + Ok(event) = close_rx.recv() => { + match event { + ServerIoEvent::ServerDisconnected(e) => { + debug!("Stopping webtransport io task. Reason: {:?}", e); + drop(addr_to_task); + return; + } + ServerIoEvent::ClientDisconnected(addr) => { + debug!("Stopping webtransport io task associated with address: {:?} because we received a disconnection signal from netcode", addr); + addr_to_task.lock().unwrap().remove(&addr); + } + _ => {} + } + } + // new client connecting + incoming_session = endpoint.accept() => { + let Ok(session_request) = incoming_session + .await + .inspect_err(|e| { + error!("failed to accept new client: {:?}", e); + }) else { + continue; + }; + let Ok(connection) = session_request + .accept() + .await + .inspect_err(|e| { + error!("failed to accept new client: {:?}", e); + }) else { + continue; + }; + let client_addr = connection.remote_address(); + let connection = Arc::new(connection); + let from_client_sender = from_client_sender.clone(); + let to_client_senders = to_client_senders.clone(); + let task = IoTaskPool::get() + .spawn(Compat::new(WebTransportServerSocket::handle_client( + connection, + from_client_sender, + to_client_senders, + status_tx.clone(), + ))); + addr_to_task.lock().unwrap().insert(client_addr, task); + } + } } })) .detach(); Ok(( - TransportEnum::WebTransportServer(WebTransportServerSocket { + ServerTransportEnum::WebTransportServer(WebTransportServerSocket { local_addr: self.server_addr, sender, receiver, }), - IoState::Connecting { - error_channel: status_rx, - }, + IoState::Connecting, + Some(ServerIoEventReceiver(status_rx)), + Some(ServerNetworkEventSender(close_tx)), )) } } @@ -107,27 +151,12 @@ pub struct WebTransportServerSocket { impl WebTransportServerSocket { pub async fn handle_client( - incoming_session: IncomingSession, + connection: Arc, from_client_sender: UnboundedSender<(Datagram, SocketAddr)>, to_client_channels: Arc>>>>, + status_tx: async_channel::Sender, ) { - let session_request = incoming_session - .await - .map_err(|e| { - error!("failed to accept new client: {:?}", e); - }) - .unwrap(); - - let connection = session_request - .accept() - .await - .map_err(|e| { - error!("failed to accept new client: {:?}", e); - }) - .unwrap(); - let connection = Arc::new(connection); let client_addr = connection.remote_address(); - info!( "Spawning new task to create connection with client: {}", client_addr @@ -182,10 +211,13 @@ impl WebTransportServerSocket { "Connection with {} closed. Reason: {:?}", client_addr, reason ); + // notify netcode that the io task got disconnected + let _ = status_tx + .send(ServerIoEvent::ClientDisconnected(client_addr)) + .await; to_client_channels.lock().unwrap().remove(&client_addr); debug!("Dropping tasks"); // the handles being dropped cancels the tasks - // TODO: need to disconnect the client in netcode } } @@ -194,8 +226,8 @@ impl Transport for WebTransportServerSocket { self.local_addr } - fn split(self) -> (BoxedSender, BoxedReceiver, Option) { - (Box::new(self.sender), Box::new(self.receiver), None) + fn split(self) -> (BoxedSender, BoxedReceiver) { + (Box::new(self.sender), Box::new(self.receiver)) } }