diff options
Diffstat (limited to 'server/editor/src/main.rs')
-rw-r--r-- | server/editor/src/main.rs | 275 |
1 files changed, 275 insertions, 0 deletions
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(()) + } +} |