aboutsummaryrefslogtreecommitdiff
path: root/server/editor/src/main.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-12-25 21:37:01 +0100
committermetamuffin <metamuffin@disroot.org>2024-12-25 21:37:01 +0100
commit31507963a05dffb3327b62dd54629420061d6c4d (patch)
tree5f245a2abc769b5eeb30d43f1ec69d033a825a60 /server/editor/src/main.rs
parentcc6b50debb9d5b740adbe6f803755413c972659a (diff)
downloadhurrycurry-31507963a05dffb3327b62dd54629420061d6c4d.tar
hurrycurry-31507963a05dffb3327b62dd54629420061d6c4d.tar.bz2
hurrycurry-31507963a05dffb3327b62dd54629420061d6c4d.tar.zst
draft map editor
Diffstat (limited to 'server/editor/src/main.rs')
-rw-r--r--server/editor/src/main.rs275
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(())
+ }
+}