diff options
-rw-r--r-- | Cargo.lock | 27 | ||||
-rw-r--r-- | protocol.md | 24 | ||||
-rw-r--r-- | server/Cargo.toml | 1 | ||||
-rw-r--r-- | server/protocol/Cargo.toml | 2 | ||||
-rw-r--r-- | server/protocol/src/lib.rs | 50 | ||||
-rw-r--r-- | server/src/main.rs | 92 | ||||
-rw-r--r-- | test-client/protocol.ts | 2 |
7 files changed, 147 insertions, 51 deletions
@@ -150,6 +150,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + +[[package]] name = "bindgen" version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -561,6 +580,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" name = "hurrycurry-protocol" version = "0.1.0" dependencies = [ + "bincode", "glam", "serde", ] @@ -589,6 +609,7 @@ name = "hurrycurry-server" version = "0.2.0" dependencies = [ "anyhow", + "bincode", "clap", "env_logger", "fake", @@ -1365,6 +1386,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + +[[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/protocol.md b/protocol.md index 28626200..956663ba 100644 --- a/protocol.md +++ b/protocol.md @@ -20,9 +20,10 @@ The protocol schema is defined in [`protocol.ts`](./test-client/protocol.ts) -1. Connect to the server via ~~TCP (on port 27031) or~~ WebSocket (on - port 27032) and send/receive json on individual lines / text messages. -2. Wait for `init` packet. +1. Connect to the server via WebSocket (on port 27032 for plain HTTP or 443 with + SSL) and send/receive json in WebSocket "Text" messages. The binary protocol + uses "Binary" messages and is optional for servers and clients. +2. Wait for `init` packet and check version compatibiliy (see below). 3. Send the join packet with your username. 4. The server will send the current game state: - `data` once for setting important look-up tables @@ -42,6 +43,23 @@ Collisions are handled by the clients. Whenever to players collide the player with the greater PlayerID is responsible for updating their own momentum and sending a packet to update that of the other player. +## Binary Protocol + +Servers might also support the binary protocol. It uses +[Bincode](https://github.com/bincode-org/bincode) to encode packets. If a server +advertises bincode support with the `init` packet, you are free to use the +binary protocol. By default the server will send JSON. For every packet you +send, you can choose between bincode and JSON. After a client sent the first +Bincode packet, the server might start to reply using Bincode aswell. Bincoded +packets are sent with WebSocket "Binary" messages. + +## Protocol Versioning + +The `init` packet sends minor and major version numbers of the protocol is use +by the server. These are to be interpreted according to +[SemVer](https://semver.org/) for the JSON protocol. The binary protocol can not +be used if either minor or major version differs. + ## Movement Movement is handled mostly client-side. Therefore it is implemented three times: diff --git a/server/Cargo.toml b/server/Cargo.toml index da084855..39b9ea1c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -19,5 +19,6 @@ shlex = "1.3.0" clap = { version = "4.5.8", features = ["derive"] } fake = "2.9.2" pollster = "0.3.0" +bincode = "2.0.0-rc.3" hurrycurry-protocol = { path = "protocol" } diff --git a/server/protocol/Cargo.toml b/server/protocol/Cargo.toml index 8fc63121..6c51c86d 100644 --- a/server/protocol/Cargo.toml +++ b/server/protocol/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] serde = { version = "1.0.204", features = ["derive"] } glam = { version = "0.28.0", features = ["serde"] } - +bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] } diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index 94bebf05..895af376 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -15,6 +15,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +use bincode::{ + config::{standard, Configuration, Limit, LittleEndian, Varint}, + Decode, Encode, +}; use glam::{IVec2, Vec2}; use serde::{Deserialize, Serialize}; use std::{ @@ -26,31 +30,44 @@ pub use glam; pub const VERSION: (u32, u32) = (1, 0); -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub const BINCODE_CONFIG: Configuration<LittleEndian, Varint, Limit<4096>> = + standard().with_limit(); + +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(transparent)] pub struct PlayerID(pub i64); -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(transparent)] pub struct ItemIndex(pub usize); -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(transparent)] pub struct TileIndex(pub usize); -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(transparent)] pub struct RecipeIndex(pub usize); -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(transparent)] pub struct DemandIndex(pub usize); -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct MapMetadata { name: String, players: usize, difficulty: i32, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Default)] #[rustfmt::skip] pub struct ClientGamedata { pub item_names: Vec<String>, @@ -61,7 +78,7 @@ pub struct ClientGamedata { pub maps: HashMap<String, MapMetadata>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] #[serde(rename_all = "snake_case", tag = "type")] pub enum PacketS { Join { @@ -70,15 +87,18 @@ pub enum PacketS { }, Leave, Position { + #[bincode(with_serde)] pos: Vec2, rot: f32, boosting: bool, }, Interact { + #[bincode(with_serde)] pos: Option<IVec2>, }, Collide { player: PlayerID, + #[bincode(with_serde)] force: Vec2, }, Communicate { @@ -87,6 +107,7 @@ pub enum PacketS { }, #[serde(skip)] + #[bincode(skip)] /// For internal use only ReplaceHand { item: Option<ItemIndex>, @@ -97,7 +118,7 @@ pub enum PacketS { }, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum Message { Text(String), @@ -105,12 +126,13 @@ pub enum Message { Effect(String), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] #[serde(rename_all = "snake_case", tag = "type")] pub enum PacketC { Version { minor: u32, major: u32, + supports_bincode: bool, }, Init { id: PlayerID, @@ -120,6 +142,7 @@ pub enum PacketC { }, AddPlayer { id: PlayerID, + #[bincode(with_serde)] position: Vec2, character: i32, name: String, @@ -129,6 +152,7 @@ pub enum PacketC { }, Position { player: PlayerID, + #[bincode(with_serde)] pos: Vec2, rot: f32, boosting: bool, @@ -147,12 +171,14 @@ pub enum PacketC { warn: bool, }, UpdateMap { + #[bincode(with_serde)] tile: IVec2, kind: Option<TileIndex>, neighbors: [Option<TileIndex>; 4], }, Collide { player: PlayerID, + #[bincode(with_serde)] force: Vec2, }, Communicate { @@ -181,10 +207,10 @@ pub enum PacketC { ReplayStart, } -#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Copy, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum ItemLocation { - Tile(IVec2), + Tile(#[bincode(with_serde)] IVec2), Player(PlayerID), } diff --git a/server/src/main.rs b/server/src/main.rs index 90d090d8..6f73851a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -18,10 +18,19 @@ use anyhow::{anyhow, Result}; use clap::Parser; use futures_util::{SinkExt, StreamExt}; -use hurrycurry_protocol::{PacketC, PacketS, PlayerID, VERSION}; +use hurrycurry_protocol::{PacketC, PacketS, PlayerID, BINCODE_CONFIG, VERSION}; use hurrycurry_server::{data::DATA_DIR, state::State}; use log::{debug, info, trace, warn, LevelFilter}; -use std::{path::PathBuf, process::exit, str::FromStr, sync::Arc, time::Duration}; +use std::{ + path::PathBuf, + process::exit, + str::FromStr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; use tokio::{ net::TcpListener, spawn, @@ -121,9 +130,14 @@ async fn run() -> anyhow::Result<()> { PacketC::Version { major: VERSION.0, minor: VERSION.1, + supports_bincode: true, }, ); init.insert(1, PacketC::Init { id }); + + let supports_binary = Arc::new(AtomicBool::new(false)); + let supports_binary2 = supports_binary.clone(); + spawn(async move { for p in init { if let Err(e) = write @@ -154,52 +168,62 @@ async fn run() -> anyhow::Result<()> { info!("client outbound sender dropped. closing connection"); break; }; - if let Err(e) = write - .send(tokio_tungstenite::tungstenite::Message::Text( - serde_json::to_string(&packet).unwrap(), - )) - .await - { + let message = if supports_binary.load(Ordering::Relaxed) { + Message::Binary(bincode::encode_to_vec(&packet, BINCODE_CONFIG).unwrap()) + } else { + Message::Text(serde_json::to_string(&packet).unwrap()) + }; + if let Err(e) = write.send(message).await { warn!("ws error: {e}"); break; } } }); + spawn(async move { info!("{id:?} joined"); while let Some(Ok(message)) = read.next().await { - match message { - Message::Text(line) => { - let packet = match serde_json::from_str(&line) { - Ok(p) => p, - Err(e) => { - warn!("invalid packet: {e}"); - break; - } - }; - if matches!( - packet, - PacketS::Position { .. } | PacketS::ReplayTick { .. } - ) { - trace!("<- {id:?} {packet:?}"); - } else { - debug!("<- {id:?} {packet:?}"); + let packet = match message { + Message::Text(line) => match serde_json::from_str(&line) { + Ok(p) => p, + Err(e) => { + warn!("invalid json packet: {e}"); + break; } - let packet_out = match state.write().await.packet_in(id, packet).await { - Ok(packets) => packets, + }, + Message::Binary(packet) => { + supports_binary2.store(true, Ordering::Relaxed); + match bincode::decode_from_slice::<PacketS, _>(&packet, BINCODE_CONFIG) { + Ok((p, _size)) => p, Err(e) => { - warn!("client error: {e}"); - vec![PacketC::Error { - message: format!("{e}"), - }] + warn!("invalid binary packet: {e}"); + break; } - }; - for packet in packet_out { - let _ = error_tx.send(packet).await; } } Message::Close(_) => break, - _ => (), + _ => continue, + }; + + if matches!( + packet, + PacketS::Position { .. } | PacketS::ReplayTick { .. } + ) { + trace!("<- {id:?} {packet:?}"); + } else { + debug!("<- {id:?} {packet:?}"); + } + let packet_out = match state.write().await.packet_in(id, packet).await { + Ok(packets) => packets, + Err(e) => { + warn!("client error: {e}"); + vec![PacketC::Error { + message: format!("{e}"), + }] + } + }; + for packet in packet_out { + let _ = error_tx.send(packet).await; } } info!("{id:?} left"); diff --git a/test-client/protocol.ts b/test-client/protocol.ts index 2cd58f75..f8241854 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -44,7 +44,7 @@ export type PacketS = | { type: "collide", player: PlayerID, force: Vec2 } // Apply force to another player as a result of a collision export type PacketC = - { type: "version", minor: number, major: number } // Sent once after connecting to ensure you client is compatible + { type: "version", minor: number, major: number, supports_bincode?: boolean } // Sent once after connecting to ensure you client is compatible | { type: "init", id: PlayerID } // You just connected. This is your id for this session. | { type: "data", data: Gamedata } // Game data was changed | { type: "add_player", id: PlayerID, name: string, position: Vec2, character: number } // Somebody else joined (or was already in the game) |