summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-12-25 23:13:12 +0100
committermetamuffin <metamuffin@disroot.org>2024-12-25 23:13:12 +0100
commit6ca4b3377a08fa30a13835d59f9558419f7f5cd1 (patch)
tree711c5fd11a86a2f228f2f14912357f46ad7d7b73
parent31507963a05dffb3327b62dd54629420061d6c4d (diff)
downloadhurrycurry-6ca4b3377a08fa30a13835d59f9558419f7f5cd1.tar
hurrycurry-6ca4b3377a08fa30a13835d59f9558419f7f5cd1.tar.bz2
hurrycurry-6ca4b3377a08fa30a13835d59f9558419f7f5cd1.tar.zst
editor save and play features
-rw-r--r--Cargo.lock18
-rw-r--r--server/editor/Cargo.toml4
-rw-r--r--server/editor/src/main.rs108
-rw-r--r--server/editor/src/save.rs69
4 files changed, 176 insertions, 23 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 54956984..bca1d4d5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1204,7 +1204,9 @@ dependencies = [
"hurrycurry-protocol",
"log",
"rustls",
+ "serde",
"serde_json",
+ "serde_yml",
"shlex",
"tokio",
"tokio-tungstenite",
@@ -2074,9 +2076,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
@@ -2722,18 +2724,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
@@ -2904,9 +2906,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
-version = "2.0.77"
+version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
+checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
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()
+}