diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | client/game.gd | 5 | ||||
-rw-r--r-- | client/multiplayer.gd | 2 | ||||
-rw-r--r-- | pixel-client/src/game.rs | 2 | ||||
-rw-r--r-- | protocol.md | 13 | ||||
-rw-r--r-- | server/protocol/src/lib.rs | 5 | ||||
-rw-r--r-- | server/protocol/src/movement.rs | 13 | ||||
-rw-r--r-- | server/protocol/src/registry.rs | 4 | ||||
-rw-r--r-- | server/registry/Cargo.toml | 3 | ||||
-rw-r--r-- | server/registry/src/lobby.rs | 164 | ||||
-rw-r--r-- | server/registry/src/main.rs | 25 |
11 files changed, 221 insertions, 16 deletions
@@ -1192,6 +1192,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tokio-tungstenite", ] [[package]] diff --git a/client/game.gd b/client/game.gd index 5618579d..4a4e3f1f 100644 --- a/client/game.gd +++ b/client/game.gd @@ -174,7 +174,7 @@ func handle_packet(p): players[p.location.player].set_item(null) "update_map": var neighbors: Array = p["neighbors"] - if p.kind != null: + if p.kind != null: if neighbors != null: neighbors = neighbors.map(func(x): return tile_names[x] if x != null else null) map.set_tile(p.tile, tile_names[p.kind], neighbors) else: map.clear_tile(p.tile) @@ -295,6 +295,9 @@ func handle_packet(p): popup_message.display_server_msg_positional(get_message_str(message), position_, false) "environment": $Environment.update(p.effects) + "redirect": + Global.server_url = p.uri[0] + get_parent().replace_menu("res://menu/game.tscn") "replay_start": is_replay = true diff --git a/client/multiplayer.gd b/client/multiplayer.gd index e9b2a4ec..38dc72f6 100644 --- a/client/multiplayer.gd +++ b/client/multiplayer.gd @@ -22,7 +22,7 @@ signal packet(packet: Dictionary) signal connection_closed() const VERSION_MAJOR: int = 7 -const VERSION_MINOR: int = 0 +const VERSION_MINOR: int = 1 var connected := false var socket := WebSocketPeer.new() diff --git a/pixel-client/src/game.rs b/pixel-client/src/game.rs index 15f4c726..ca5f773a 100644 --- a/pixel-client/src/game.rs +++ b/pixel-client/src/game.rs @@ -170,7 +170,7 @@ impl Game { if send_movement { self.network .queue_out - .push_back(player.movement.movement_packet(self.my_id)); + .push_back(player.movement.movement_packet_s(self.my_id)); } player.interact_target_anim.exp_to( diff --git a/protocol.md b/protocol.md index 35671e75..1abdfc36 100644 --- a/protocol.md +++ b/protocol.md @@ -19,9 +19,9 @@ The protocol schema is defined in [`protocol.ts`](./test-client/protocol.ts) -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. +1. Connect to the server via WebSocket and send/receive json in WebSocket "Text" + messages. The binary protocol uses "Binary" messages and is optional for + servers and clients. 2. Wait for `version` packet and check version compatibiliy (see below). 3. Send the join packet with your username. 4. The server will send the current game state: @@ -41,6 +41,13 @@ 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. +## Ports + +- 443: All uses with TLS +- 27032: Game Server Websocket +- 27033: Registry API HTTP +- 27034: Lobby Server Websocket + ## Binary Protocol Servers might also support the binary protocol. It uses diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index 56ddc7a8..b826edae 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -28,7 +28,7 @@ pub use glam; pub mod movement; pub mod registry; -pub const VERSION: (u32, u32) = (7, 0); +pub const VERSION: (u32, u32) = (7, 1); pub const BINCODE_CONFIG: Configuration<LittleEndian, Varint, Limit<4096>> = standard().with_limit(); @@ -247,6 +247,9 @@ pub enum PacketC { item: ItemIndex, success: bool, }, + Redirect { + uri: Vec<String>, + }, /// For use in replay sessions only ReplayStart, diff --git a/server/protocol/src/movement.rs b/server/protocol/src/movement.rs index 85accb31..a02ee8ee 100644 --- a/server/protocol/src/movement.rs +++ b/server/protocol/src/movement.rs @@ -17,7 +17,7 @@ */ use crate::{ glam::{IVec2, Vec2}, - PacketS, PlayerID, + PacketC, PacketS, PlayerID, }; use std::collections::HashSet; @@ -81,7 +81,7 @@ impl MovementBase { collide_player_tiles(self, map); } - pub fn movement_packet(&self, player: PlayerID) -> PacketS { + pub fn movement_packet_s(&self, player: PlayerID) -> PacketS { PacketS::Movement { pos: Some(self.position), boost: self.input_boost, @@ -89,6 +89,15 @@ impl MovementBase { player, } } + pub fn movement_packet_c(&self, player: PlayerID) -> PacketC { + PacketC::Movement { + rot: self.rotation, + pos: self.position, + boost: self.input_boost, + dir: self.input_direction, + player, + } + } pub fn collide(&mut self, other: &mut Self, dt: f32) { let diff = self.position - other.position; diff --git a/server/protocol/src/registry.rs b/server/protocol/src/registry.rs index 92f2c454..b5b83bd1 100644 --- a/server/protocol/src/registry.rs +++ b/server/protocol/src/registry.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] pub struct Entry { pub name: String, pub address: Vec<String>, @@ -9,7 +9,7 @@ pub struct Entry { pub version: (u32, u32), } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Submission { pub secret: u128, pub name: String, diff --git a/server/registry/Cargo.toml b/server/registry/Cargo.toml index 8acd3c4c..2eeebb30 100644 --- a/server/registry/Cargo.toml +++ b/server/registry/Cargo.toml @@ -12,6 +12,9 @@ tokio = { version = "1.39.2", features = ["full"] } serde_json = "1.0.128" markup = "0.15.0" serde = { version = "1.0.210", features = ["derive"] } +tokio-tungstenite = { version = "0.23.1", features = [ + "rustls-tls-native-roots", +] } hurrycurry-protocol = { path = "../protocol" } hurrycurry-client-lib = { path = "../client-lib" } diff --git a/server/registry/src/lobby.rs b/server/registry/src/lobby.rs new file mode 100644 index 00000000..f74dd48e --- /dev/null +++ b/server/registry/src/lobby.rs @@ -0,0 +1,164 @@ +use crate::Registry; +use anyhow::Result; +use hurrycurry_protocol::{ + glam::{ivec2, IVec2, Vec2}, + movement::MovementBase, + registry::Entry, + Gamedata, PacketC, PacketS, PlayerID, TileIndex, VERSION, +}; +use log::{error, info, warn}; +use rocket::futures::{SinkExt, StreamExt}; +use std::{ + collections::{HashMap, HashSet}, + net::SocketAddr, + sync::Arc, + time::Instant, +}; +use tokio::{ + net::{TcpListener, TcpStream}, + spawn, + sync::RwLock, +}; +use tokio_tungstenite::tungstenite::Message; + +pub(crate) async fn lobby_wrapper(registry: Arc<RwLock<Registry>>, saddr: SocketAddr) { + let Err(e) = lobby(registry, saddr).await; + error!("lobby crashed: {e}"); +} + +const TILES: &[(&str, bool)] = &[("grass", false), ("black-hole", false)]; + +async fn lobby(registry: Arc<RwLock<Registry>>, saddr: SocketAddr) -> Result<!> { + let ws_listener = TcpListener::bind(saddr).await?; + + loop { + let (sock, addr) = ws_listener.accept().await?; + let r = registry.clone(); + spawn(async move { + let entries = r.read().await.entries.clone(); + if let Err(e) = handle_conn(sock, addr, &entries).await { + warn!("client error: {e}"); + } + }); + } +} + +#[allow(unused_assignments)] +async fn handle_conn(sock: TcpStream, addr: SocketAddr, entries: &[Entry]) -> Result<()> { + let sock = tokio_tungstenite::accept_async(sock).await?; + info!("{addr} connected via websocket"); + + let mut tiles = HashMap::<IVec2, TileIndex>::new(); + for x in -5..5 + 5 * entries.len() as i32 { + for y in -5..5 { + tiles.insert(ivec2(x, y), TileIndex(0)); + } + } + let portal_location = |i: usize| ivec2(i as i32 * 5 + 5, 0); + for (i, _) in entries.iter().enumerate() { + tiles.insert(portal_location(i), TileIndex(1)); + } + + let mut out = Vec::new(); + out.push(PacketC::Version { + major: VERSION.0, + minor: VERSION.1, + supports_bincode: false, + }); + out.push(PacketC::Data { + data: Gamedata { + tile_collide: TILES.iter().map(|(_, c)| *c).collect(), + tile_interact: TILES.iter().map(|_| false).collect(), + tile_names: TILES.iter().map(|(s, _)| s.to_string()).collect(), + current_map: "registry".to_owned(), + ..Default::default() + }, + }); + let walkable = HashSet::from_iter(tiles.iter().filter(|(_, v)| !TILES[v.0].1).map(|(k, _)| *k)); + for (&tile, &kind) in &tiles { + out.push(PacketC::UpdateMap { + tile, + kind: Some(kind), + neighbors: [None, None, None, None], + }); + } + out.push(PacketC::SetIngame { + state: true, + lobby: false, // very ironic + }); + + let (mut write, mut read) = sock.split(); + + loop { + for p in out.drain(..) { + write + .send(tokio_tungstenite::tungstenite::Message::Text( + serde_json::to_string(&p).unwrap(), + )) + .await?; + } + + let Some(message) = read.next().await.transpose()? else { + break; + }; + let packet = match message { + Message::Text(line) => match serde_json::from_str::<PacketS>(&line) { + Ok(p) => p, + Err(e) => { + warn!("Invalid json packet: {e}"); + break; + } + }, + Message::Close(_) => break, + _ => continue, + }; + + let mut joined = false; + let mut redirected = false; + let mut movement = MovementBase::new(Vec2::ZERO); + let mut last_movement = Instant::now(); + match packet { + PacketS::Join { + character, name, .. + } if !joined => { + out.push(PacketC::Joined { id: PlayerID(0) }); + out.push(PacketC::AddPlayer { + id: PlayerID(0), + position: movement.position, + character, + name, + }); + joined = true; + } + PacketS::Leave { .. } if joined => { + out.push(PacketC::RemovePlayer { id: PlayerID(0) }); + joined = false; + } + PacketS::Movement { + player, + dir, + boost, + pos, + } => { + let dt = last_movement.elapsed(); + last_movement += dt; + movement.position = pos.unwrap_or(movement.position); + movement.input(dir, boost); + movement.update(&walkable, dt.as_secs_f32()); + out.push(movement.movement_packet_c(player)); + if !redirected { + for (i, e) in entries.iter().enumerate() { + if movement.position.distance(portal_location(i).as_vec2()) < 0.5 { + redirected = true; + out.push(PacketC::Redirect { + uri: e.address.clone(), + }); + } + } + } + } + _ => (), + } + } + Ok(()) +} diff --git a/server/registry/src/main.rs b/server/registry/src/main.rs index 670ac3a6..2e0b7656 100644 --- a/server/registry/src/main.rs +++ b/server/registry/src/main.rs @@ -15,12 +15,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +#![feature(never_type)] pub mod conn_test; pub mod list; +pub mod lobby; pub mod register; use hurrycurry_protocol::registry::Entry; use list::{generate_html_list, generate_json_list, r_list}; +use lobby::lobby_wrapper; use log::{error, info}; use register::r_register; use rocket::{get, routes, Config}; @@ -28,7 +31,7 @@ use std::{ cmp::Reverse, collections::HashMap, env::var, - net::{IpAddr, Ipv4Addr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, sync::Arc, time::{Duration, Instant}, @@ -40,19 +43,29 @@ const MAX_SERVERS: usize = 128; fn main() { env_logger::init_from_env("LOG"); + + let address = var("BIND_ADDR") + .map(|a| IpAddr::from_str(&a).unwrap()) + .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); + let http_port = var("PORT").map(|p| p.parse().unwrap()).unwrap_or(27033); + let lobby_port = var("LOBBY_PORT") + .map(|p| p.parse().unwrap()) + .unwrap_or(27034); + let lobby_addr = SocketAddr::new(address, lobby_port); + let registry = Arc::new(RwLock::new(Registry::default())); + tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async move { tokio::task::spawn(Registry::update_loop(registry.clone())); + tokio::task::spawn(lobby_wrapper(registry.clone(), lobby_addr)); rocket::build() .configure(Config { - address: var("BIND_ADDR") - .map(|a| IpAddr::from_str(&a).unwrap()) - .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)), - port: var("PORT").map(|p| p.parse().unwrap()).unwrap_or(27033), + address, + port: http_port, ..Default::default() }) .manage(registry) @@ -70,6 +83,7 @@ fn main() { struct Registry { json_response: Arc<str>, html_response: Arc<str>, + entries: Vec<Entry>, servers: HashMap<u128, InternalEntry>, } @@ -107,6 +121,7 @@ impl Registry { self.json_response = generate_json_list(&list)?; self.html_response = generate_html_list(&list)?; + self.entries = list; info!("done. {} servers registered", self.servers.len()); Ok(()) |