diff options
-rw-r--r-- | Cargo.lock | 58 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | protocol.md | 1 | ||||
-rw-r--r-- | server/editor/Cargo.toml | 21 | ||||
-rw-r--r-- | server/editor/src/main.rs | 275 | ||||
-rw-r--r-- | test-client/protocol.ts | 3 |
6 files changed, 338 insertions, 21 deletions
@@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -461,9 +461,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -485,9 +485,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cmake" @@ -827,9 +827,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -837,9 +837,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -854,15 +854,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -871,21 +871,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1193,6 +1193,24 @@ dependencies = [ ] [[package]] +name = "hurrycurry-editor" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "futures-util", + "hurrycurry-client-lib", + "hurrycurry-protocol", + "log", + "rustls", + "serde_json", + "shlex", + "tokio", + "tokio-tungstenite", +] + +[[package]] name = "hurrycurry-protocol" version = "8.0.0" dependencies = [ @@ -7,6 +7,7 @@ members = [ "server/replaytool", "server/registry", "server/discover", + "server/editor", "pixel-client", "pixel-client/tools", "locale/tools", diff --git a/protocol.md b/protocol.md index 35a13076..d79c28c9 100644 --- a/protocol.md +++ b/protocol.md @@ -47,6 +47,7 @@ sending a packet to update that of the other player. - 27032: Game Server Websocket - 27033: Registry API HTTP / Local dicovery service - 27034: Lobby Server Websocket +- 27035: Map Editor Server Websocket ## Binary Protocol diff --git a/server/editor/Cargo.toml b/server/editor/Cargo.toml new file mode 100644 index 00000000..c8f15b0a --- /dev/null +++ b/server/editor/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "hurrycurry-editor" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1.40.0", features = ["full"] } +serde_json = "1.0.128" +log = "0.4.22" +env_logger = "0.11.5" +anyhow = "1.0.89" +tokio-tungstenite = { version = "0.24.0", features = [ + "rustls-tls-native-roots", +] } +rustls = { version = "0.23.13", features = ["ring"] } +clap = { version = "4.5.23", features = ["derive"] } +futures-util = "0.3.31" +shlex = "1.3.0" + +hurrycurry-protocol = { path = "../protocol" } +hurrycurry-client-lib = { path = "../client-lib" } diff --git a/server/editor/src/main.rs b/server/editor/src/main.rs new file mode 100644 index 00000000..1ae4c81e --- /dev/null +++ b/server/editor/src/main.rs @@ -0,0 +1,275 @@ +use anyhow::{Result, anyhow}; +use clap::Parser; +use futures_util::{SinkExt, StreamExt}; +use hurrycurry_protocol::{ + Gamedata, Hand, Message, PacketC, PacketS, PlayerClass, PlayerID, TileIndex, VERSION, + glam::{IVec2, Vec2, ivec2}, + movement::MovementBase, +}; +use log::{debug, info, warn}; +use std::{ + collections::{HashMap, HashSet}, + net::SocketAddr, + time::Instant, +}; +use tokio::net::{TcpListener, TcpStream}; + +#[derive(Parser)] +struct Args { + #[arg(short, long, default_value = "127.0.0.1")] + bind_addr: String, + #[arg(short, long, default_value = "27035")] + port: u16, +} + +#[derive(Parser)] +#[clap(multicall = true)] +enum Command { + Play, +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init_from_env("LOG"); + let args = Args::parse(); + let ws_listener = TcpListener::bind((args.bind_addr, args.port)).await?; + loop { + let (sock, addr) = ws_listener.accept().await?; + if let Err(e) = handle_conn(sock, addr).await { + warn!("client error: {e}"); + } + } +} + +const TILES: &[&str] = &[ + "grass", + "floor", + "counter", + "oven", + "stove", + "cuttingboard", + "chair", + "table", +]; + +#[allow(unused_assignments)] +async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> { + let sock = tokio_tungstenite::accept_async(sock).await?; + info!("{addr} connected via websocket"); + + let mut state = State { + out: Vec::new(), + tiles: HashMap::new(), + walkable: HashSet::new(), + joined: false, + movement: MovementBase::new(Vec2::ZERO), + last_movement: Instant::now(), + tile: TileIndex(0), + }; + + state.out.push(PacketC::Version { + major: VERSION.0, + minor: VERSION.1, + supports_bincode: false, + }); + state.out.push(PacketC::Data { + data: Gamedata { + tile_collide: TILES.iter().map(|_| false).collect(), + tile_interact: TILES.iter().map(|_| true).collect(), + tile_names: TILES.iter().map(|s| s.to_string()).collect(), + current_map: "editor".to_owned(), + ..Default::default() + }, + }); + state.out.push(PacketC::SetIngame { + state: true, + lobby: false, // very ironic + }); + + state.build_start_platform(); + + let (mut write, mut read) = sock.split(); + + loop { + for p in state.out.drain(..) { + debug!("-> {p:?}"); + 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 { + tokio_tungstenite::tungstenite::Message::Text(line) => { + match serde_json::from_str::<PacketS>(&line) { + Ok(p) => p, + Err(e) => { + warn!("Invalid json packet: {e}"); + break; + } + } + } + tokio_tungstenite::tungstenite::Message::Close(_) => break, + _ => continue, + }; + debug!("<- {packet:?}"); + + if let Err(e) = state.handle_packet(packet) { + state.out.push(PacketC::ServerMessage { + message: Message::Text(format!("{e}")), + error: true, + }); + } + } + Ok(()) +} + +struct State { + tiles: HashMap<IVec2, TileIndex>, + walkable: HashSet<IVec2>, + out: Vec<PacketC>, + joined: bool, + movement: MovementBase, + last_movement: Instant, + tile: TileIndex, +} + +impl State { + pub fn set_tile(&mut self, pos: IVec2, tile: TileIndex) { + self.tiles.insert(pos, tile); + self.walkable.insert(pos); + self.out.push(PacketC::UpdateMap { + tile: pos, + kind: Some(tile), + neighbors: [ + self.tiles.get(&(pos + IVec2::NEG_Y)).copied(), + self.tiles.get(&(pos + IVec2::NEG_X)).copied(), + self.tiles.get(&(pos + IVec2::Y)).copied(), + self.tiles.get(&(pos + IVec2::X)).copied(), + ], + }) + } + pub fn flush(&mut self) { + // send every existing tile once move because client does not remember + // TODO remove when client is fixed + for (tile, _) in &self.tiles { + self.out.push(PacketC::UpdateMap { + tile: *tile, + kind: None, + neighbors: [None; 4], + }) + } + for (tile, kind) in &self.tiles { + self.out.push(PacketC::UpdateMap { + tile: *tile, + kind: Some(*kind), + neighbors: [ + self.tiles.get(&(tile + IVec2::NEG_Y)).copied(), + self.tiles.get(&(tile + IVec2::NEG_X)).copied(), + self.tiles.get(&(tile + IVec2::Y)).copied(), + self.tiles.get(&(tile + IVec2::X)).copied(), + ], + }) + } + self.out.push(PacketC::FlushMap); + } + pub fn build_start_platform(&mut self) { + for x in 0..10 { + for y in 0..10 { + self.set_tile(ivec2(x, y), TileIndex(1)); + } + } + self.flush(); + } + + pub fn handle_packet(&mut self, packet: PacketS) -> Result<()> { + match packet { + PacketS::Join { + character, name, .. + } if !self.joined => { + self.out.push(PacketC::Joined { id: PlayerID(0) }); + self.out.push(PacketC::AddPlayer { + id: PlayerID(0), + position: self.movement.position, + class: PlayerClass::Chef, + character, + name, + }); + self.joined = true; + } + PacketS::Leave { .. } if self.joined => { + self.out.push(PacketC::RemovePlayer { id: PlayerID(0) }); + self.joined = false; + } + PacketS::Movement { + player, + dir, + boost, + pos, + } => { + let dt = self.last_movement.elapsed(); + self.last_movement += dt; + self.movement.position = pos.unwrap_or(self.movement.position); + self.movement.input(dir, boost); + self.movement.update(&self.walkable, dt.as_secs_f32()); + self.out.push(self.movement.movement_packet_c(player)); + } + PacketS::Communicate { + message: Some(Message::Text(t)), + .. + } => { + self.handle_command( + Command::try_parse_from( + shlex::split(&t) + .ok_or(anyhow!("invalid quoting"))? + .into_iter(), + ) + .map_err(|e| anyhow!("{e}"))?, + )?; + } + PacketS::Interact { + hand: Hand(1), + pos: Some(_), + .. + } => { + self.tile.0 += 1; + self.tile.0 %= TILES.len(); + self.out.push(PacketC::ServerMessage { + message: Message::Text(format!("tile brush: {}", TILES[self.tile.0])), + error: false, + }); + } + PacketS::Interact { + hand: Hand(0), + pos: Some(_), + .. + } => { + let bpos = self.movement.position.floor().as_ivec2(); + self.set_tile(bpos, self.tile); + for off in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { + if !self.tiles.contains_key(&(bpos + off)) { + self.set_tile(bpos + off, self.tile); + } + } + self.flush(); + } + _ => (), + } + Ok(()) + } + + pub fn handle_command(&mut self, command: Command) -> Result<()> { + match command { + Command::Play => { + self.out.push(PacketC::Redirect { + uri: vec!["ws://127.0.0.1:27032".to_string()], + }); + } + } + Ok(()) + } +} diff --git a/test-client/protocol.ts b/test-client/protocol.ts index 25d33cb5..787dfc15 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -19,6 +19,7 @@ export type Vec2 = [number, number] // x, y export type PlayerID = number // opaque number to identify players. export type ItemIndex = number // index used primarily for item_names in Gamedata export type TileIndex = number // index used primarily for tile_names in Gamedata +export type Hand = number export interface MapMetadata { name: string, @@ -97,7 +98,7 @@ export type Message = | { translation: { id: string, params: Message[] } } export type ItemLocation = - { player: PlayerID } + { player: [PlayerID, Hand] } | { tile: Vec2 } export type PlayerClass = "chef" | "bot" | "customer" |