summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/editor/Cargo.toml4
-rw-r--r--server/editor/src/main.rs108
-rw-r--r--server/editor/src/save.rs69
3 files changed, 166 insertions, 15 deletions
diff --git a/server/editor/Cargo.toml b/server/editor/Cargo.toml
index c8f15b0a..4c0fa307 100644
--- a/server/editor/Cargo.toml
+++ b/server/editor/Cargo.toml
@@ -12,10 +12,12 @@ anyhow = "1.0.89"
tokio-tungstenite = { version = "0.24.0", features = [
"rustls-tls-native-roots",
] }
+serde = { version = "1.0.216", features = ["derive"] }
rustls = { version = "0.23.13", features = ["ring"] }
clap = { version = "4.5.23", features = ["derive"] }
futures-util = "0.3.31"
shlex = "1.3.0"
+serde_yml = "0.0.12"
hurrycurry-protocol = { path = "../protocol" }
-hurrycurry-client-lib = { path = "../client-lib" }
+hurrycurry-client-lib = { path = "../client-lib", features = ["sync-network"] }
diff --git a/server/editor/src/main.rs b/server/editor/src/main.rs
index 1ae4c81e..c5b6f61d 100644
--- a/server/editor/src/main.rs
+++ b/server/editor/src/main.rs
@@ -1,16 +1,23 @@
+pub mod save;
+
use anyhow::{Result, anyhow};
use clap::Parser;
use futures_util::{SinkExt, StreamExt};
+use hurrycurry_client_lib::network::sync::Network;
use hurrycurry_protocol::{
Gamedata, Hand, Message, PacketC, PacketS, PlayerClass, PlayerID, TileIndex, VERSION,
glam::{IVec2, Vec2, ivec2},
movement::MovementBase,
};
use log::{debug, info, warn};
+use save::export_state;
use std::{
collections::{HashMap, HashSet},
+ fs::File,
+ io::Write,
net::SocketAddr,
- time::Instant,
+ thread::{sleep, spawn},
+ time::{Duration, Instant},
};
use tokio::net::{TcpListener, TcpStream};
@@ -24,8 +31,9 @@ struct Args {
#[derive(Parser)]
#[clap(multicall = true)]
-enum Command {
+pub enum Command {
Play,
+ Save,
}
#[tokio::main]
@@ -41,15 +49,17 @@ async fn main() -> Result<()> {
}
}
-const TILES: &[&str] = &[
- "grass",
- "floor",
- "counter",
- "oven",
- "stove",
- "cuttingboard",
- "chair",
- "table",
+const TILES: &[(&str, char, u8)] = &[
+ ("grass", 'a', 1),
+ ("floor", 'b', 1),
+ ("counter", 'c', 0),
+ ("oven", 'd', 0),
+ ("stove", 'e', 0),
+ ("cuttingboard", 'f', 0),
+ ("chair", 'g', 1),
+ ("table", 'h', 0),
+ ("wall", 'h', 2),
+ ("counter-window", 'h', 0),
];
#[allow(unused_assignments)]
@@ -65,6 +75,8 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> {
movement: MovementBase::new(Vec2::ZERO),
last_movement: Instant::now(),
tile: TileIndex(0),
+ chef_spawn: ivec2(0, 0),
+ customer_spawn: ivec2(0, 0),
};
state.out.push(PacketC::Version {
@@ -76,7 +88,7 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> {
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(),
+ tile_names: TILES.iter().map(|(name, _, _)| name.to_string()).collect(),
current_map: "editor".to_owned(),
..Default::default()
},
@@ -128,7 +140,7 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> {
Ok(())
}
-struct State {
+pub struct State {
tiles: HashMap<IVec2, TileIndex>,
walkable: HashSet<IVec2>,
out: Vec<PacketC>,
@@ -136,6 +148,9 @@ struct State {
movement: MovementBase,
last_movement: Instant,
tile: TileIndex,
+
+ customer_spawn: IVec2,
+ chef_spawn: IVec2,
}
impl State {
@@ -222,6 +237,7 @@ impl State {
message: Some(Message::Text(t)),
..
} => {
+ let t = t.strip_prefix("/").unwrap_or(&t);
self.handle_command(
Command::try_parse_from(
shlex::split(&t)
@@ -239,7 +255,7 @@ impl State {
self.tile.0 += 1;
self.tile.0 %= TILES.len();
self.out.push(PacketC::ServerMessage {
- message: Message::Text(format!("tile brush: {}", TILES[self.tile.0])),
+ message: Message::Text(format!("tile brush: {}", TILES[self.tile.0].0)),
error: false,
});
}
@@ -262,14 +278,78 @@ impl State {
Ok(())
}
+ pub fn save(&mut self) -> Result<()> {
+ let e = export_state(&self);
+ File::create("data/maps/editor.yaml")?.write_all(e.as_bytes())?;
+ self.out.push(PacketC::ServerMessage {
+ message: Message::Text(format!("Map saved.")),
+ error: false,
+ });
+ Ok(())
+ }
+
pub fn handle_command(&mut self, command: Command) -> Result<()> {
match command {
Command::Play => {
+ self.save()?;
+ spawn(move || {
+ if let Err(e) = start_map_bot("ws://127.0.0.1:27032", "editor") {
+ warn!("editor bot: {e}")
+ }
+ });
self.out.push(PacketC::Redirect {
uri: vec!["ws://127.0.0.1:27032".to_string()],
});
}
+ Command::Save => {
+ self.save()?;
+ }
}
Ok(())
}
}
+
+fn start_map_bot(address: &str, mapname: &str) -> Result<()> {
+ let mut network = Network::connect(address)?;
+
+ network.queue_out.push_back(PacketS::Join {
+ name: "editor-bot".to_owned(),
+ character: 0,
+ class: PlayerClass::Bot,
+ id: None,
+ });
+
+ let mut timer = 10.;
+ loop {
+ let dt = 1. / 50.;
+
+ network.poll()?;
+
+ while let Some(packet) = network.queue_in.pop_front() {
+ match &packet {
+ PacketC::Joined { id } => {
+ network.queue_out.push_back(PacketS::Communicate {
+ player: *id,
+ message: Some(Message::Text(format!("/start {mapname}"))),
+ timeout: None,
+ pin: None,
+ });
+ }
+ PacketC::ServerMessage {
+ message,
+ error: true,
+ } => {
+ warn!("server error message: {message:?}");
+ }
+ _ => (),
+ }
+ }
+
+ timer -= dt;
+ if timer < 0. {
+ break;
+ }
+ sleep(Duration::from_secs_f32(dt));
+ }
+ Ok(())
+}
diff --git a/server/editor/src/save.rs b/server/editor/src/save.rs
new file mode 100644
index 00000000..a77cd609
--- /dev/null
+++ b/server/editor/src/save.rs
@@ -0,0 +1,69 @@
+use crate::{State, TILES};
+use hurrycurry_protocol::glam::{IVec2, ivec2};
+use serde::Serialize;
+use std::collections::{HashMap, HashSet};
+
+#[rustfmt::skip]
+#[derive(Debug, Clone, Serialize)]
+pub struct MapDecl {
+ map: Vec<String>,
+ tiles: HashMap<char, String>,
+ items: HashMap<char, String>,
+ collider: Vec<String>,
+ walkable: Vec<String>,
+ chef_spawn: char,
+ customer_spawn: char,
+ score_baseline: i64,
+}
+
+pub fn export_state(state: &State) -> String {
+ let mut cmin = IVec2::MAX;
+ let mut cmax = IVec2::MIN;
+ for pos in state.tiles.keys().copied() {
+ cmin = cmin.min(pos);
+ cmax = cmax.max(pos);
+ }
+
+ let mut map = Vec::new();
+ let mut tiles = HashMap::new();
+ let mut collider = HashSet::new();
+ let mut walkable = HashSet::new();
+ for y in cmin.y..=cmax.y {
+ let mut line = String::new();
+ for x in cmin.x..=cmax.x {
+ let p = ivec2(x, y);
+ line.push(if let Some(t) = state.tiles.get(&p) {
+ let c = if p == state.chef_spawn {
+ '~'
+ } else if p == state.customer_spawn {
+ '!'
+ } else {
+ TILES[t.0].1
+ };
+ if TILES[t.0].2 == 2 {
+ collider.insert(TILES[t.0].0.to_string());
+ }
+ if TILES[t.0].2 == 1 {
+ walkable.insert(TILES[t.0].0.to_string());
+ }
+ tiles.insert(c, TILES[t.0].0.to_string());
+ c
+ } else {
+ ' '
+ })
+ }
+ map.push(line);
+ }
+
+ let decl: MapDecl = MapDecl {
+ map,
+ tiles,
+ items: HashMap::new(),
+ collider: collider.into_iter().collect(),
+ walkable: walkable.into_iter().collect(),
+ chef_spawn: '~',
+ customer_spawn: '!',
+ score_baseline: 200,
+ };
+ serde_yml::to_string(&decl).unwrap()
+}