diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 3bf64dd3..0e760f2b 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -27,4 +27,5 @@ jobs: - name: Check dependencies uses: EmbarkStudios/cargo-deny-action@v2 with: + arguments: --all-features --workspace command-arguments: -D warnings diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5aaa683c..ec66e4d9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,10 +57,12 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Clippy - run: cargo clippy --all-features --benches --tests -- -D warnings + run: cargo clippy --workspace --examples --all-features --benches --tests -- -D warnings - name: Rustdoc - run: cargo rustdoc --all-features -- -D warnings + run: | + cargo rustdoc --all-features -- -D warnings + cargo rustdoc -p bevy_replicon_example_backend -- -D warnings doctest: name: Doctest @@ -76,7 +78,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Test doc - run: cargo test --all-features --doc + run: cargo test --workspace --all-features --doc feature-combinations: name: Feature combinations @@ -121,6 +123,11 @@ jobs: - name: Test run: cargo tarpaulin --all-features --engine llvm --out lcov --exclude-files benches/* + # Can't collect coverage from the example backend, + # it crashes tarpaulin due to TCP usage. + - name: Test backend + run: cargo test -p bevy_replicon_example_backend + - name: Upload code coverage results if: github.actor != 'dependabot[bot]' uses: actions/upload-artifact@v4 diff --git a/Cargo.toml b/Cargo.toml index b41effea..375d981f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,19 +24,26 @@ include = ["/benches", "/src", "/tests", "/LICENSE*"] rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true +[workspace] +members = ["bevy_replicon_example_backend"] + +[workspace.dependencies] +bevy = { version = "0.15", default-features = false } +serde = "1.0" + [dependencies] -bevy = { version = "0.15", default-features = false, features = ["serialize"] } +bevy = { workspace = true, features = ["serialize"] } thiserror = "2.0" typeid = "1.0" bytes = "1.5" bincode = "1.3" -serde = "1.0" +serde.workspace = true integer-encoding = "4.0" ordered-multimap = "0.7" bitflags = "2.6" [dev-dependencies] -bevy = { version = "0.15", default-features = false, features = [ +bevy = { workspace = true, features = [ "serialize", "bevy_asset", "bevy_scene", diff --git a/README.md b/README.md index de11d643..2d3f8123 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ See also [What kind of networking should X game use?](https://github.com/bevyeng Check out the [quick start guide](https://docs.rs/bevy_replicon). -For examples navigate to [messaging backends](#messaging-backends) repositories because you will need I/O in order to run them. +For examples navigate to the [`bevy_replicon_example_backend`](bevy_replicon_example_backend) (because you need I/O in order to run them). Have any questions? Feel free to ask in the dedicated [`bevy_replicon` channel](https://discord.com/channels/691052431525675048/1090432346907492443) in Bevy's Discord server. diff --git a/bevy_replicon_example_backend/Cargo.toml b/bevy_replicon_example_backend/Cargo.toml new file mode 100644 index 00000000..63d6ef37 --- /dev/null +++ b/bevy_replicon_example_backend/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "bevy_replicon_example_backend" +version = "0.1.0" +description = "A simple transport intended only for examples" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +bevy.workspace = true +bevy_replicon = { path = "..", default-features = false } + +[dev-dependencies] +bevy = { workspace = true, features = [ + "bevy_text", + "bevy_ui", + "bevy_gizmos", + "bevy_state", + "bevy_window", + "x11", + "default_font", +] } +serde.workspace = true +clap = { version = "4.1", features = ["derive"] } + +[features] +default = ["client", "server"] +server = ["bevy_replicon/server"] +client = ["bevy_replicon/client"] diff --git a/bevy_replicon_example_backend/README.md b/bevy_replicon_example_backend/README.md new file mode 100644 index 00000000..40fca6a4 --- /dev/null +++ b/bevy_replicon_example_backend/README.md @@ -0,0 +1,12 @@ +# Bevy Replicon Example Backend + +A simple TCP backend for running examples, testing backend API and serving as a reference for backend implementation. + +> [!WARNING] +> DO NOT USE this in a real project. Instead, choose a proper backend from [Messaging backends](../README.md#messaging-backends). + +To run an [example](examples) use the following command: + +```bash +cargo run -p bevy_replicon_example_backend --example +``` diff --git a/bevy_replicon_example_backend/assets/LICENSE b/bevy_replicon_example_backend/assets/LICENSE new file mode 100644 index 00000000..d952d62c --- /dev/null +++ b/bevy_replicon_example_backend/assets/LICENSE @@ -0,0 +1,92 @@ +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/bevy_replicon_example_backend/assets/NotoEmoji-Regular.ttf b/bevy_replicon_example_backend/assets/NotoEmoji-Regular.ttf new file mode 100644 index 00000000..19b7badf Binary files /dev/null and b/bevy_replicon_example_backend/assets/NotoEmoji-Regular.ttf differ diff --git a/bevy_replicon_example_backend/examples/simple_box.rs b/bevy_replicon_example_backend/examples/simple_box.rs new file mode 100644 index 00000000..2cfbb162 --- /dev/null +++ b/bevy_replicon_example_backend/examples/simple_box.rs @@ -0,0 +1,206 @@ +//! A simple demo to showcase how player could send inputs to move a box and server replicates position back. +//! Also demonstrates the single-player and how sever also could be a player. + +use std::io; + +use bevy::{ + color::palettes::css::GREEN, + prelude::*, + winit::{UpdateMode::Continuous, WinitSettings}, +}; +use bevy_replicon::prelude::*; +use bevy_replicon_example_backend::{ExampleClient, ExampleServer, RepliconExampleBackendPlugins}; +use clap::Parser; +use serde::{Deserialize, Serialize}; + +fn main() { + App::new() + .init_resource::() // Parse CLI before creating window. + // Makes the server/client update continuously even while unfocused. + .insert_resource(WinitSettings { + focused_mode: Continuous, + unfocused_mode: Continuous, + }) + .add_plugins(( + DefaultPlugins, + RepliconPlugins, + RepliconExampleBackendPlugins, + SimpleBoxPlugin, + )) + .run(); +} + +struct SimpleBoxPlugin; + +impl Plugin for SimpleBoxPlugin { + fn build(&self, app: &mut App) { + app.replicate::() + .replicate::() + .add_client_trigger::(ChannelKind::Ordered) + .add_observer(spawn_clients) + .add_observer(despawn_clients) + .add_observer(apply_movement) + .add_systems(Startup, (read_cli.map(Result::unwrap), spawn_camera)) + .add_systems(Update, (read_input, draw_boxes)); + } +} + +fn read_cli(mut commands: Commands, cli: Res) -> io::Result<()> { + match *cli { + Cli::SinglePlayer => { + info!("starting single-player game"); + commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into()))); + } + Cli::Server { port } => { + info!("starting server at port {port}"); + let server = ExampleServer::new(port)?; + commands.insert_resource(server); + commands.spawn(( + Text::new("Server"), + TextFont { + font_size: 30.0, + ..Default::default() + }, + TextColor::WHITE, + )); + commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into()))); + } + Cli::Client { port } => { + info!("connecting to port {port}"); + let client = ExampleClient::new(port)?; + let client_id = client.id()?; + commands.insert_resource(client); + commands.spawn(( + Text(format!("Client: {client_id:?}")), + TextFont { + font_size: 30.0, + ..default() + }, + TextColor::WHITE, + )); + } + } + + Ok(()) +} + +fn spawn_camera(mut commands: Commands) { + commands.spawn(Camera2d); +} + +/// Spawns a new box whenever a client connects. +fn spawn_clients(trigger: Trigger, mut commands: Commands) { + // Generate pseudo random color from client id. + let r = ((trigger.client_id.get() % 23) as f32) / 23.0; + let g = ((trigger.client_id.get() % 27) as f32) / 27.0; + let b = ((trigger.client_id.get() % 39) as f32) / 39.0; + info!("spawning box for `{:?}`", trigger.client_id); + commands.spawn((BoxPlayer(trigger.client_id), BoxColor(Color::srgb(r, g, b)))); +} + +/// Despawns a box whenever a client disconnects. +fn despawn_clients( + trigger: Trigger, + mut commands: Commands, + boxes: Query<(Entity, &BoxPlayer)>, +) { + let (entity, _) = boxes + .iter() + .find(|(_, &player)| *player == trigger.client_id) + .expect("all clients should have entities"); + commands.entity(entity).despawn(); +} + +/// Reads player inputs and sends [`MoveDirection`] events. +fn read_input(mut commands: Commands, input: Res>) { + let mut direction = Vec2::ZERO; + if input.pressed(KeyCode::KeyW) { + direction.y += 1.0; + } + if input.pressed(KeyCode::KeyA) { + direction.x -= 1.0; + } + if input.pressed(KeyCode::KeyS) { + direction.y -= 1.0; + } + if input.pressed(KeyCode::KeyD) { + direction.x += 1.0; + } + + if direction != Vec2::ZERO { + commands.client_trigger(MoveBox(direction.normalize_or_zero())); + } +} + +/// Mutates [`BoxPosition`] based on [`MoveBox`] events. +/// +/// Fast-paced games usually you don't want to wait until server send a position back because of the latency. +/// But this example just demonstrates simple replication concept. +fn apply_movement( + trigger: Trigger>, + time: Res