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::(&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, walkable: HashSet, out: Vec, 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(()) } }