aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock58
-rw-r--r--Cargo.toml1
-rw-r--r--protocol.md1
-rw-r--r--server/editor/Cargo.toml21
-rw-r--r--server/editor/src/main.rs275
-rw-r--r--test-client/protocol.ts3
6 files changed, 338 insertions, 21 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b3d1ec3e..54956984 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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 = [
diff --git a/Cargo.toml b/Cargo.toml
index cbe90e84..2e624ec8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"