Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Rename `RepliconClientStatus` to `ClientState` and `RepliconServerStatus` to `ServerState`. They are now regular Bevy states. As result, we now require `StatesPlugin` to be added. It's present by default in `DefaultPlugins`, but with `MinimalPlugins` you have to add it manually.
- Make custom entity ser/de compatible with `serde` attributes.
- All contexts now store `AppTypeRegistry` instead of `TypeRegistry`. To get `TypeRegistry`, call `AppTypeRegistry::read`.
- All events now use `ClientId` wrapper instead of `Entity`.
Expand All @@ -43,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `entity_serde::serialize_entity` and `entity_serde::deserialize_entity`. Use `postcard_utils::entity_to_extend_mut` and `postcard_utils::entity_from_buf` respectively; just swap the argument order.
- `SERVER`. Use `ClientId::Server` instead.
- `ReplicationMode::Periodic`. Use `PriorityMap` instead.
- All provided run conditions. Just use `in_state` or `OnEnter`/`OnExit` with `ServerState` and `ClientState` instead. `server_or_singleplayer` is just `in_state(ClientState::Disconnected)`.

## [0.34.4] - 2025-07-29

Expand Down
10 changes: 6 additions & 4 deletions benches/related_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ fn hierarchy_spawning(c: &mut Criterion) {
.sync_related_entities::<ChildOf>()
.finish();

let mut server = app.world_mut().resource_mut::<RepliconServer>();
server.set_running(true);
app.world_mut()
.resource_mut::<NextState<ServerState>>()
.set(ServerState::Running);

b.iter(|| spawn_then_despawn(&mut app));
});
Expand Down Expand Up @@ -73,8 +74,9 @@ fn hierarchy_changes(c: &mut Criterion) {

spawn_hierarchy(app.world_mut());

let mut server = app.world_mut().resource_mut::<RepliconServer>();
server.set_running(true);
app.world_mut()
.resource_mut::<NextState<ServerState>>()
.set(ServerState::Running);

b.iter(|| trigger_hierarchy_change(&mut app));
});
Expand Down
7 changes: 5 additions & 2 deletions bevy_replicon_example_backend/examples/authoritative_rts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ fn main() {
.add_observer(trigger_units_move)
.add_observer(apply_units_move)
.add_systems(Startup, setup)
.add_systems(FixedUpdate, move_units.run_if(server_or_singleplayer))
.add_systems(OnEnter(ClientState::Connected), trigger_team_request)
.add_systems(
FixedUpdate,
move_units.run_if(in_state(ClientState::Disconnected)),
)
.add_systems(
Update,
(
trigger_team_request.run_if(client_just_connected),
draw_selection.run_if(|r: Res<Selection>| r.active),
draw_selected,
),
Expand Down
18 changes: 8 additions & 10 deletions bevy_replicon_example_backend/examples/tic_tac_toe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,17 @@ fn main() {
.add_systems(OnEnter(GameState::Winner), show_winner_text)
.add_systems(OnEnter(GameState::Tie), show_tie_text)
.add_systems(OnEnter(GameState::Disconnected), stop_networking)
.add_systems(OnEnter(ClientState::Connected), client_start)
.add_systems(OnEnter(ClientState::Connecting), show_connecting_text)
.add_systems(OnExit(ClientState::Connected), disconnect_by_server)
.add_systems(OnEnter(ServerState::Running), show_waiting_client_text)
.add_systems(
Update,
(
show_connecting_text.run_if(resource_added::<ExampleClient>),
show_waiting_client_text.run_if(resource_added::<ExampleServer>),
client_start.run_if(client_just_connected),
(
disconnect_by_server.run_if(client_just_disconnected),
update_buttons_background.run_if(local_player_turn),
show_turn_symbol.run_if(resource_changed::<TurnSymbol>),
)
.run_if(in_state(GameState::InGame)),
),
update_buttons_background.run_if(local_player_turn),
show_turn_symbol.run_if(resource_changed::<TurnSymbol>),
)
.run_if(in_state(GameState::InGame)),
)
.run();
}
Expand Down
27 changes: 13 additions & 14 deletions bevy_replicon_example_backend/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,31 @@ impl Plugin for RepliconExampleClientPlugin {
app.add_systems(
PreUpdate,
(
(
receive_packets.run_if(resource_exists::<ExampleClient>),
// Run after since the resource might be removed after receiving packets.
set_disconnected.run_if(resource_removed::<ExampleClient>),
)
.chain(),
set_connected.run_if(resource_added::<ExampleClient>),
receive_packets.run_if(resource_exists::<ExampleClient>),
)
.chain()
.in_set(ClientSet::ReceivePackets),
)
.add_systems(
PostUpdate,
(
set_disconnected
.in_set(ClientSet::PrepareSend)
.run_if(resource_removed::<ExampleClient>),
send_packets
.in_set(ClientSet::SendPackets)
.run_if(resource_exists::<ExampleClient>),
),
send_packets
.run_if(resource_exists::<ExampleClient>)
.in_set(ClientSet::SendPackets),
);
}
}

fn set_disconnected(mut replicon_client: ResMut<RepliconClient>) {
replicon_client.set_status(RepliconClientStatus::Disconnected);
fn set_connected(mut state: ResMut<NextState<ClientState>>) {
state.set(ClientState::Connected);
}

fn set_connected(mut replicon_client: ResMut<RepliconClient>) {
replicon_client.set_status(RepliconClientStatus::Connected);
fn set_disconnected(mut state: ResMut<NextState<ClientState>>) {
state.set(ClientState::Disconnected);
}

fn receive_packets(
Expand Down
27 changes: 13 additions & 14 deletions bevy_replicon_example_backend/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,31 @@ impl Plugin for RepliconExampleServerPlugin {
app.add_systems(
PreUpdate,
(
(
receive_packets.run_if(resource_exists::<ExampleServer>),
// Run after since the resource might be removed after receiving packets.
set_stopped.run_if(resource_removed::<ExampleServer>),
)
.chain(),
set_running.run_if(resource_added::<ExampleServer>),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be before receive_packets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary, the packets can be passed into ServerMessages before we change the state.

receive_packets.run_if(resource_exists::<ExampleServer>),
)
.chain()
.in_set(ServerSet::ReceivePackets),
)
.add_systems(
PostUpdate,
(
set_stopped
.in_set(ServerSet::PrepareSend)
.run_if(resource_removed::<ExampleServer>),
send_packets
.in_set(ServerSet::SendPackets)
.run_if(resource_exists::<ExampleServer>),
),
send_packets
.run_if(resource_exists::<ExampleServer>)
.in_set(ServerSet::SendPackets),
);
}
}

fn set_stopped(mut server: ResMut<RepliconServer>) {
server.set_running(false);
fn set_running(mut state: ResMut<NextState<ServerState>>) {
state.set(ServerState::Running);
}

fn set_running(mut server: ResMut<RepliconServer>) {
server.set_running(true);
fn set_stopped(mut state: ResMut<NextState<ServerState>>) {
state.set(ServerState::Stopped);
}

fn receive_packets(
Expand Down
23 changes: 13 additions & 10 deletions bevy_replicon_example_backend/tests/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ fn connect_disconnect() {

setup(&mut server_app, &mut client_app).unwrap();

assert!(server_app.world().resource::<RepliconServer>().is_running());
let server_state = server_app.world().resource::<State<ServerState>>();
assert_eq!(*server_state, ServerState::Running);

let mut clients = server_app
.world_mut()
.query::<(&ConnectedClient, &AuthorizedClient)>();
assert_eq!(clients.iter(server_app.world()).len(), 1);

let replicon_client = client_app.world().resource::<RepliconClient>();
assert!(replicon_client.is_connected());
let client_state = client_app.world().resource::<State<ClientState>>();
assert_eq!(*client_state, ClientState::Connected);

let renet_client = client_app.world().resource::<ExampleClient>();
assert!(renet_client.is_connected());
Expand All @@ -45,8 +46,8 @@ fn connect_disconnect() {

assert_eq!(clients.iter(server_app.world()).len(), 0);

let replicon_client = client_app.world().resource::<RepliconClient>();
assert!(replicon_client.is_disconnected());
let client_state = client_app.world().resource::<State<ClientState>>();
assert_eq!(*client_state, ClientState::Disconnected);
}

#[test]
Expand Down Expand Up @@ -88,8 +89,8 @@ fn disconnect_request() {

assert_eq!(clients.iter(server_app.world()).len(), 0);

let client = client_app.world().resource::<RepliconClient>();
assert!(client.is_disconnected());
let client_state = client_app.world().resource::<State<ClientState>>();
assert_eq!(*client_state, ClientState::Disconnected);

let events = client_app.world().resource::<Events<TestEvent>>();
assert_eq!(events.len(), 1, "last event should be received");
Expand Down Expand Up @@ -134,10 +135,12 @@ fn server_stop() {

let mut clients = server_app.world_mut().query::<&ConnectedClient>();
assert_eq!(clients.iter(server_app.world()).len(), 0);
assert!(!server_app.world().resource::<RepliconServer>().is_running());

let client = client_app.world().resource::<RepliconClient>();
assert!(client.is_disconnected());
let server_state = server_app.world().resource::<State<ServerState>>();
assert_eq!(*server_state, ServerState::Stopped);

let client_state = client_app.world().resource::<State<ClientState>>();
assert_eq!(*client_state, ClientState::Disconnected);

let events = client_app.world().resource::<Events<TestEvent>>();
assert!(events.is_empty(), "event after stop shouldn't be received");
Expand Down
59 changes: 30 additions & 29 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,40 +48,45 @@ impl Plugin for ClientPlugin {
PreUpdate,
(
ClientSet::ReceivePackets,
(
ClientSet::ResetEvents.run_if(client_just_connected),
ClientSet::Reset.run_if(client_just_disconnected),
),
ClientSet::Receive,
ClientSet::Diagnostics,
)
.chain(),
)
.configure_sets(
PostUpdate,
OnEnter(ClientState::Connected),
(
ClientSet::PrepareSend,
ClientSet::Send,
ClientSet::SendPackets,
ClientSet::ResetEvents,
ClientSet::Receive,
ClientSet::Diagnostics,
)
.chain(),
)
.configure_sets(
PostUpdate,
(ClientSet::Send, ClientSet::SendPackets).chain(),
)
.add_systems(
PreUpdate,
receive_replication
.in_set(ClientSet::Receive)
.run_if(client_connected),
.run_if(in_state(ClientState::Connected)),
)
.add_systems(
OnEnter(ClientState::Connected),
receive_replication.in_set(ClientSet::Receive),
)
.add_systems(PreUpdate, reset.in_set(ClientSet::Reset));
.add_systems(
OnExit(ClientState::Connected),
reset.in_set(ClientSet::Reset),
);

let auth_method = *app.world().resource::<AuthMethod>();
debug!("using authorization method `{auth_method:?}`");
if auth_method == AuthMethod::ProtocolCheck {
app.add_observer(log_protocol_error).add_systems(
PreUpdate,
send_protocol_hash
.in_set(ClientSet::Receive)
.run_if(client_just_connected),
OnEnter(ClientState::Connected),
send_protocol_hash.in_set(ClientSet::SendHash),
);
}
}
Expand Down Expand Up @@ -165,12 +170,14 @@ pub(super) fn receive_replication(
}

fn reset(
mut client: ResMut<RepliconClient>,
mut update_tick: ResMut<ServerUpdateTick>,
mut entity_map: ResMut<ServerEntityMap>,
mut buffered_mutations: ResMut<BufferedMutations>,
mutate_ticks: Option<ResMut<ServerMutateTicks>>,
stats: Option<ResMut<ClientReplicationStats>>,
) {
client.clear();
*update_tick = Default::default();
entity_map.clear();
buffered_mutations.clear();
Expand Down Expand Up @@ -744,32 +751,26 @@ struct ReceiveParams<'a> {
/// Set with replication and event systems related to client.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub enum ClientSet {
/// Systems that receive packets from the messaging backend.
/// Systems that receive packets from the messaging backend and update [`ClientState`].
///
/// Used by messaging backend implementations.
///
/// Runs in [`PreUpdate`].
ReceivePackets,
/// Systems that receive data from [`RepliconClient`].
///
/// Used by `bevy_replicon`.
///
/// Runs in [`PreUpdate`].
/// Runs in [`PreUpdate`] and [`OnEnter`] for [`ClientState::Connected`] (to avoid 1 frame delay).
Receive,
/// Systems that populate Bevy's [`Diagnostics`](bevy::diagnostic::Diagnostics).
///
/// Used by `bevy_replicon`.
///
/// Runs in [`PreUpdate`].
/// Runs in [`PreUpdate`] and [`OnEnter`] for [`ClientState::Connected`] (to avoid 1 frame delay).
Diagnostics,
/// Systems that prepare for sending data to [`RepliconClient`].
/// System that sends [`ProtocolHash`].
///
/// Can be used by backends to add custom logic before sending data, such as transition to a disconnected or connecting state.
PrepareSend,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep this for renet2 to use.

Copy link
Contributor Author

@Shatur Shatur Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against, but how do you plan to use it? I ported bevy_renet, and PrepareSend wasn't needed because of the states (I used it previously in bevy_renet).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No necessary anymore. Just like with the server everything can be done in ClientSet::ReceivePackets. Here is how I did it in renet:
https://github.com/simgine/bevy_replicon_renet/blob/9b45a0bb84f4d47d47cc595b5cfbb6309fc139db/src/client.rs#L23-L24

/// Runs in [`OnEnter`] for [`ClientState::Connected`].
SendHash,
/// Systems that send data to [`RepliconClient`].
///
/// Used by `bevy_replicon`.
///
/// Runs in [`PostUpdate`].
Send,
/// Systems that send packets to the messaging backend.
Expand All @@ -780,13 +781,13 @@ pub enum ClientSet {
SendPackets,
/// Systems that reset queued server events.
///
/// Runs in [`PreUpdate`] immediately after the client connects to ensure client sessions have a fresh start.
///
/// This is a separate set from [`ClientSet::Reset`] to avoid sending events that were sent before the connection.
///
/// Runs in [`OnEnter`] for [`ClientState::Connected`].
ResetEvents,
/// Systems that reset the client.
///
/// Runs in [`PreUpdate`] when the client just disconnected.
/// Runs in [`OnExit`] for [`ClientState::Connected`].
Reset,
}

Expand Down
6 changes: 5 additions & 1 deletion src/client/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ impl Plugin for ClientDiagnosticsPlugin {
PreUpdate,
add_measurements
.in_set(ClientSet::Diagnostics)
.run_if(client_connected),
.run_if(in_state(ClientState::Connected)),
)
.add_systems(
OnEnter(ClientState::Connected),
add_measurements.in_set(ClientSet::Diagnostics),
)
Comment on lines 19 to 26
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very awkward to me. Adding states means there are systems in the main schedule and in state schedules, which is an increase in complexity not a decrease.

Copy link
Contributor Author

@Shatur Shatur Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran Replicon systems upon entering the schedule and within the schedule. This lets me process packets immediately after connection. It's not ideal, but I think it's worth it.

.register_diagnostic(
Diagnostic::new(RTT)
Expand Down
Loading