diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a014bf..b6130e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rename `client_event_registry` module into `registry`. +- Replace "event" with "message" and "trigger" with "event" in types, methods and modules according to the new naming in Bevy. +- Split `RemoteMessageRegistry::*_channel` methods for messages and events into dedicated methods for each type. + +### Removed + +- `trigger_*_targets`. Targets in Bevy now stored inside events. When you move entities to your events, dont't forget to use `register_*_event_mapped` in order to properly map them. ### Added diff --git a/Cargo.toml b/Cargo.toml index 45067840..01f7b90c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,9 +28,7 @@ all-features = true members = ["bevy_replicon_example_backend"] [dependencies] -bevy = { version = "0.16.0", default-features = false, features = [ - "bevy_state", -] } +bevy = { version = "0.17", default-features = false, features = ["bevy_state"] } log = "0.4" # Directly depend on `log` like other `no_std` Bevy crates, since `bevy_log` currently requires `std`. petgraph = { version = "0.8", default-features = false, features = [ "stable_graph", @@ -52,9 +50,9 @@ bytes = { version = "1.10", default-features = false, features = [ ] } [dev-dependencies] -bevy = { version = "0.16.0", default-features = false, features = [ +bevy = { version = "0.17", default-features = false, features = [ "bevy_log", - "bevy_sprite", + "bevy_sprite_render", "serialize", ] } test-log = "0.2" @@ -90,11 +88,11 @@ name = "mutations" required-features = ["client", "server"] [[test]] -name = "client_event" +name = "client_message" required-features = ["client", "server"] [[test]] -name = "client_trigger" +name = "client_event" required-features = ["client", "server"] [[test]] @@ -122,11 +120,11 @@ name = "scene" required-features = ["scene"] [[test]] -name = "server_event" +name = "server_message" required-features = ["client", "server"] [[test]] -name = "server_trigger" +name = "server_event" required-features = ["client", "server"] [[test]] diff --git a/bevy_replicon_example_backend/Cargo.toml b/bevy_replicon_example_backend/Cargo.toml index c22a7d92..2bb15ec9 100644 --- a/bevy_replicon_example_backend/Cargo.toml +++ b/bevy_replicon_example_backend/Cargo.toml @@ -21,20 +21,21 @@ license = "MIT OR Apache-2.0" include = ["/src", "../LICENSE*"] [dependencies] -bevy = { version = "0.16.0", default-features = false, features = ["bevy_log"] } +bevy = { version = "0.17", default-features = false, features = ["bevy_log"] } bevy_replicon = { path = "..", version = "0.35.0", default-features = false } fastrand = "2.3" [dev-dependencies] -bevy = { version = "0.16.0", default-features = false, features = [ +bevy = { version = "0.17", default-features = false, features = [ "bevy_gizmos", "bevy_text", "bevy_ui_picking_backend", - "bevy_ui", + "bevy_ui_render", "bevy_window", "default_font", "serialize", "x11", + "zstd_rust", ] } clap = { version = "4.1", features = ["derive"] } fastrand-contrib = "0.1" diff --git a/bevy_replicon_example_backend/examples/authoritative_rts.rs b/bevy_replicon_example_backend/examples/authoritative_rts.rs index 89865613..bbd37b73 100644 --- a/bevy_replicon_example_backend/examples/authoritative_rts.rs +++ b/bevy_replicon_example_backend/examples/authoritative_rts.rs @@ -16,6 +16,7 @@ use std::{ }; use bevy::{ + camera::primitives::Aabb, color::palettes::tailwind::{ BLUE_500, GREEN_500, LIME_500, ORANGE_500, PINK_500, PURPLE_500, RED_500, TEAL_500, YELLOW_500, @@ -23,7 +24,6 @@ use bevy::{ ecs::entity::MapEntities, platform::collections::HashMap, prelude::*, - render::primitives::Aabb, }; use bevy_replicon::prelude::*; use bevy_replicon_example_backend::{ExampleClient, ExampleServer, RepliconExampleBackendPlugins}; @@ -44,9 +44,9 @@ fn main() { .replicate::() .replicate::() .replicate_as::() - .add_client_trigger::(Channel::Unordered) - .add_client_trigger::(Channel::Unordered) - .add_mapped_client_trigger::(Channel::Unordered) + .add_client_event::(Channel::Unordered) + .add_client_event::(Channel::Unordered) + .add_mapped_client_event::(Channel::Unordered) .add_observer(apply_team_request) .add_observer(trigger_unit_spawn) .add_observer(apply_unit_spawn) @@ -131,29 +131,29 @@ fn trigger_team_request(mut commands: Commands, team: Res) { /// Assigns a team to a player. fn apply_team_request( - trigger: Trigger>, - mut events: EventWriter, + team_request: On>, + mut disconnects: MessageWriter, mut teams: ResMut, ) { - if let Some((client_id, team)) = teams.iter().find(|&(_, team)| *team == trigger.team) { + if let Some((client_id, team)) = teams.iter().find(|&(_, team)| *team == team_request.team) { error!( "`{}` requested team `{team:?}`, but it's already taken by `{client_id}`", - trigger.client_id, + team_request.client_id, ); - let client = trigger + let client = team_request .client_id .entity() .expect("server can't request an invalid team"); - events.write(DisconnectRequest { client }); + disconnects.write(DisconnectRequest { client }); return; } info!( "associating `{}` with team `{:?}`", - trigger.client_id, trigger.team + team_request.client_id, team_request.team ); - teams.insert(trigger.client_id, trigger.team); + teams.insert(team_request.client_id, team_request.team); } /// Requests spawning a unit at the click location. @@ -165,16 +165,16 @@ fn apply_team_request( /// This also makes the logic independent of camera position - even though in /// this demo the camera cannot move. fn trigger_unit_spawn( - trigger: Trigger>, + press: On>, mut commands: Commands, camera: Single<(&Camera, &GlobalTransform)>, ) -> Result<()> { - if trigger.button != PointerButton::Middle { + if press.button != PointerButton::Middle { return Ok(()); } let (camera, transform) = *camera; - let position = camera.viewport_to_world_2d(transform, trigger.pointer_location.position)?; + let position = camera.viewport_to_world_2d(transform, press.pointer_location.position)?; commands.client_trigger(UnitSpawn { position }); @@ -186,21 +186,21 @@ fn trigger_unit_spawn( /// Executed on server and singleplayer. /// The unit will be replicated back to clients. fn apply_unit_spawn( - trigger: Trigger>, + spawn: On>, mut commands: Commands, teams: Res, ) { - let Some(&team) = teams.get(&trigger.client_id) else { + let Some(&team) = teams.get(&spawn.client_id) else { error!( "`{}` attempted to spawn a unit but has no team", - trigger.client_id + spawn.client_id ); return; }; commands.spawn(( Unit { team }, - Transform::from_translation(trigger.position.extend(0.0)), + Transform::from_translation(spawn.position.extend(0.0)), )); } @@ -215,12 +215,12 @@ fn apply_unit_spawn( /// Works for initialization on both the server (when a unit /// is spawned) and the client (when the unit is replicated). fn init_unit( - trigger: Trigger, + insert: On, unit_mesh: Local, unit_materials: Local, mut units: Query<(&Unit, &mut Mesh2d, &mut MeshMaterial2d)>, ) { - let (unit, mut mesh, mut material) = units.get_mut(trigger.target()).unwrap(); + let (unit, mut mesh, mut material) = units.get_mut(insert.entity).unwrap(); **mesh = unit_mesh.0.clone(); **material = unit_materials.get(&unit.team).unwrap().clone(); } @@ -229,24 +229,22 @@ fn init_unit( /// /// The selection is local to the player and is not networked. fn select_units( - trigger: Trigger>, + drag: On>, mut commands: Commands, mut selection: ResMut, team: Res, camera: Single<(&Camera, &GlobalTransform)>, units: Query<(Entity, &Unit, &GlobalTransform, &Aabb, Has)>, ) -> Result<()> { - if trigger.button != PointerButton::Primary { + if drag.button != PointerButton::Primary { return Ok(()); } let (camera, transform) = *camera; - let origin = camera.viewport_to_world_2d( - transform, - trigger.pointer_location.position - trigger.distance, - )?; - let end = camera.viewport_to_world_2d(transform, trigger.pointer_location.position)?; + let origin = + camera.viewport_to_world_2d(transform, drag.pointer_location.position - drag.distance)?; + let end = camera.viewport_to_world_2d(transform, drag.pointer_location.position)?; selection.rect = Rect::from_corners(origin, end); selection.active = true; @@ -268,16 +266,16 @@ fn select_units( } /// Stops displaying the selection rectangle. -fn end_selection(_trigger: Trigger>, mut rect: ResMut) { +fn end_selection(_on: On>, mut rect: ResMut) { rect.active = false; } fn clear_selection( - trigger: Trigger>, + press: On>, mut commands: Commands, units: Query>, ) { - if trigger.button != PointerButton::Primary { + if press.button != PointerButton::Primary { return; } for unit in &units { @@ -287,19 +285,19 @@ fn clear_selection( /// Requests movement into a location for previously the selected units. fn trigger_units_move( - trigger: Trigger>, + press: On>, mut commands: Commands, camera: Single<(&Camera, &GlobalTransform)>, units: Populated>, ) -> Result<()> { - if trigger.button != PointerButton::Secondary { + if press.button != PointerButton::Secondary { return Ok(()); } let (camera, transform) = *camera; - let position = camera.viewport_to_world_2d(transform, trigger.pointer_location.position)?; + let position = camera.viewport_to_world_2d(transform, press.pointer_location.position)?; - commands.client_trigger(UnitsMove { + commands.client_trigger(MoveUnits { units: units.iter().collect(), position, }); @@ -314,7 +312,7 @@ const MOVE_SPACING: f32 = 30.0; /// Each unit receives a unique `Command::Move`, arranged in a grid formation /// centered on the requested position. The grid is oriented toward that position. fn apply_units_move( - trigger: Trigger>, + move_units: On>, teams: Res, mut slots: Local>, mut positions: Local>, @@ -323,26 +321,26 @@ fn apply_units_move( // Validate the received data since the client could be malicious. // For example, on the client side we skip empty selections, but a modified // client could bypass this and cause a division by zero on the server. - if trigger.units.is_empty() { - error!("`{}` attempted to move zero units", trigger.client_id); + if move_units.units.is_empty() { + error!("`{}` attempted to move zero units", move_units.client_id); return; } - let Some(&client_team) = teams.get(&trigger.client_id) else { + let Some(&client_team) = teams.get(&move_units.client_id) else { error!( "`{}` attempted to move units but has no team", - trigger.client_id + move_units.client_id ); return; }; positions.clear(); - positions.reserve(trigger.units.len()); - for (unit, transform, _) in units.iter_many(&trigger.units) { + positions.reserve(move_units.units.len()); + for (unit, transform, _) in units.iter_many(&move_units.units) { if unit.team != client_team { error!( "`{}` has team `{client_team:?}`, but tried to move unit with team `{:?}`", - trigger.client_id, unit.team + move_units.client_id, unit.team ); return; } @@ -350,7 +348,7 @@ fn apply_units_move( positions.push(transform.translation().truncate()); } - let units_count = trigger.units.len(); + let units_count = move_units.units.len(); let cols = (units_count as f32).sqrt().ceil() as usize; let rows = units_count.div_ceil(cols); let centering_offset = -Vec2::new(cols as f32 - 1.0, rows as f32 - 1.0) / 2.0 * MOVE_SPACING; @@ -358,7 +356,7 @@ fn apply_units_move( // Orientation basis to make grid facing from group centroid toward the click. let positions_sum = positions.iter().sum::(); let centroid = positions_sum / units_count as f32; - let forward = (trigger.position - centroid).normalize_or(Vec2::Y); + let forward = (move_units.position - centroid).normalize_or(Vec2::Y); let right = Vec2::new(forward.y, -forward.x); let rotation = Mat2::from_cols(right, forward); @@ -369,7 +367,7 @@ fn apply_units_move( let col = index % cols; let grid_position = centering_offset + Vec2::new(col as f32, row as f32) * MOVE_SPACING; - slots.push(trigger.position + rotation * grid_position); + slots.push(move_units.position + rotation * grid_position); } // Pick closest slot for each unit using @@ -385,7 +383,7 @@ fn apply_units_move( .collect(); let (_, unit_to_slot) = kuhn_munkres_min(&weights); - let mut iter = units.iter_many_mut(&trigger.units); + let mut iter = units.iter_many_mut(&move_units.units); for &slot_index in &unit_to_slot { let (.., mut command) = iter.fetch_next().unwrap(); *command = Command::Move(slots[slot_index]); @@ -649,21 +647,21 @@ impl FromWorld for UnitMaterials { } } -/// A trigger to join a specific team for a client. +/// Request to join a team. #[derive(Event, Serialize, Deserialize)] struct TeamRequest { team: Team, } -/// A trigger that spawns a unit at a location. +/// Request to spawn a unit at a location. #[derive(Event, Serialize, Deserialize)] struct UnitSpawn { position: Vec2, } -/// A trigger that orders units to move to a specified location. +/// Orders units to move to a specified location. #[derive(Event, Serialize, Deserialize, MapEntities, Clone)] -struct UnitsMove { +struct MoveUnits { #[entities] units: Vec, position: Vec2, diff --git a/bevy_replicon_example_backend/examples/deterministic_boids.rs b/bevy_replicon_example_backend/examples/deterministic_boids.rs index 9dde35c0..14de1052 100644 --- a/bevy_replicon_example_backend/examples/deterministic_boids.rs +++ b/bevy_replicon_example_backend/examples/deterministic_boids.rs @@ -132,12 +132,12 @@ fn spawn_boids(commands: &mut Commands) { /// is inserted. This way, visuals are applied automatically both when a boid is spawned /// manually on the server (or in singleplayer) and when it is replicated to a client. fn init_boid( - trigger: Trigger, + insert: On, boid_mesh: Local, mut materials: ResMut>, mut boids: Query<(&Boid, &mut Mesh2d, &mut MeshMaterial2d)>, ) { - let (boid, mut mesh, mut material) = boids.get_mut(trigger.target()).unwrap(); + let (boid, mut mesh, mut material) = boids.get_mut(insert.entity).unwrap(); // All boids share the same mesh, but each gets a unique material from its color. **mesh = boid_mesh.0.clone(); diff --git a/bevy_replicon_example_backend/examples/simple_button.rs b/bevy_replicon_example_backend/examples/simple_button.rs index d3a4fb4e..6c32d017 100644 --- a/bevy_replicon_example_backend/examples/simple_button.rs +++ b/bevy_replicon_example_backend/examples/simple_button.rs @@ -4,7 +4,7 @@ use std::net::{IpAddr, Ipv4Addr}; -use bevy::prelude::*; +use bevy::{ecs::entity::MapEntities, prelude::*}; use bevy_replicon::prelude::*; use bevy_replicon_example_backend::{ExampleClient, ExampleServer, RepliconExampleBackendPlugins}; use clap::Parser; @@ -21,7 +21,7 @@ fn main() { .replicate::() .replicate::() .replicate_filtered::>() // Replicate parent only for `ToggleButton`. - .add_client_trigger::(Channel::Unordered) + .add_mapped_client_event::(Channel::Unordered) .add_observer(init_toggle_button) .add_observer(trigger_remote_toggle) .add_observer(apply_remote_toggle) @@ -64,8 +64,8 @@ fn setup(mut commands: Commands, cli: Res) -> Result<()> { } /// Since we can't include hierarchy into required components, initialize it on insertion. -fn init_toggle_button(trigger: Trigger, mut commands: Commands) { - commands.entity(trigger.target()).with_child(( +fn init_toggle_button(add: On, mut commands: Commands) { + commands.entity(add.entity).with_child(( Text::default(), TextShadow::default(), TextFont { @@ -81,12 +81,14 @@ fn init_toggle_button(trigger: Trigger, mut commands: Comma /// Used on both the server and the clients. /// Triggering this on the server will emit [`FromClient`] with [`ClientId::Server`]. fn trigger_remote_toggle( - trigger: Trigger>, + click: On>, mut commands: Commands, buttons: Query<(), With>, ) { - if buttons.get(trigger.target()).is_ok() { - commands.client_trigger_targets(RemoteToggle, trigger.target()); + if buttons.get(click.entity).is_ok() { + commands.client_trigger(RemoteToggle { + entity: click.entity, + }); } } @@ -95,10 +97,10 @@ fn trigger_remote_toggle( /// Executed on server and singleplayer. /// The state will be replicated back to clients. fn apply_remote_toggle( - trigger: Trigger>, + toggle: On>, mut buttons: Query<&mut ToggleButton>, ) { - if let Ok(mut toggle) = buttons.get_mut(trigger.target()) { + if let Ok(mut toggle) = buttons.get_mut(toggle.entity) { **toggle = !**toggle } } @@ -200,11 +202,14 @@ struct UiRoot; align_items: AlignItems::Center, ..Default::default() }, - BorderColor(Color::WHITE), + BorderColor::all(Color::WHITE), BorderRadius::MAX, )] struct ToggleButton(bool); -/// A client trigger that toggles the buttons it targets. -#[derive(Event, Serialize, Deserialize)] -struct RemoteToggle; +/// Toggles the buttons it targets. +#[derive(EntityEvent, MapEntities, Serialize, Deserialize, Clone, Copy)] +struct RemoteToggle { + #[entities] + entity: Entity, +} diff --git a/bevy_replicon_example_backend/examples/tic_tac_toe.rs b/bevy_replicon_example_backend/examples/tic_tac_toe.rs index 9b295915..901cc496 100644 --- a/bevy_replicon_example_backend/examples/tic_tac_toe.rs +++ b/bevy_replicon_example_backend/examples/tic_tac_toe.rs @@ -22,7 +22,7 @@ fn main() { DefaultPlugins.build().set(WindowPlugin { primary_window: Some(Window { title: "Tic-Tac-Toe".into(), - resolution: (800.0, 600.0).into(), + resolution: (800, 600).into(), ..Default::default() }), ..Default::default() @@ -34,7 +34,7 @@ fn main() { .init_resource::() .init_resource::() .replicate::() - .add_client_trigger::(Channel::Ordered) + .add_client_event::(Channel::Ordered) .insert_resource(ClearColor(BACKGROUND_COLOR)) .add_observer(disconnect_by_client) .add_observer(init_client) @@ -214,7 +214,7 @@ fn setup_ui(mut commands: Commands, symbol_font: Res) { /// /// We don't just send mouse clicks to save traffic, they contain a lot of extra information. fn pick_cell( - trigger: Trigger>, + click: On>, mut commands: Commands, turn_symbol: Res, game_state: Res>, @@ -230,39 +230,39 @@ fn pick_cell( } let cell = cells - .get(trigger.target()) + .get(click.entity) .expect("cells should have assigned indices"); // We don't check if a cell can't be picked on client on purpose // just to demonstrate how server can receive invalid requests from a client. info!("picking cell {}", cell.index); - commands.client_trigger(CellPick { index: cell.index }); + commands.client_trigger(PickCell { index: cell.index }); } /// Handles cell pick events. /// /// Used only for single-player and server. fn apply_pick( - trigger: Trigger>, + pick: On>, mut commands: Commands, cells: Query<(Entity, &Cell), Without>, turn_symbol: Res, players: Query<&Symbol>, ) { // It's good to check the received data because client could be cheating. - if let ClientId::Client(client) = trigger.client_id { + if let ClientId::Client(client) = pick.client_id { let symbol = *players .get(client) .expect("all clients should have assigned symbols"); if symbol != **turn_symbol { - error!("`{client}` chose cell {} at wrong turn", trigger.index); + error!("`{client}` chose cell {} at wrong turn", pick.index); return; } } - let Some((entity, _)) = cells.iter().find(|(_, cell)| cell.index == trigger.index) else { + let Some((entity, _)) = cells.iter().find(|(_, cell)| cell.index == pick.index) else { error!( "`{}` has chosen occupied or invalid cell {}", - trigger.client_id, trigger.index + pick.client_id, pick.index ); return; }; @@ -272,18 +272,18 @@ fn apply_pick( /// Initializes spawned symbol on client after replication and on server / single-player right after the spawn. fn init_symbols( - trigger: Trigger, + add: On, mut commands: Commands, symbol_font: Res, mut cells: Query<(&mut BackgroundColor, &Symbol), With