diff options
author | metamuffin <metamuffin@disroot.org> | 2024-12-26 10:57:17 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-12-26 10:57:17 +0100 |
commit | 35bb41ad0ef2dcb34f143c0cc066610c29bae455 (patch) | |
tree | 6547a2301e8c9909bf927ed3f324520989d476eb | |
parent | 6ca4b3377a08fa30a13835d59f9558419f7f5cd1 (diff) | |
download | hurrycurry-35bb41ad0ef2dcb34f143c0cc066610c29bae455.tar hurrycurry-35bb41ad0ef2dcb34f143c0cc066610c29bae455.tar.bz2 hurrycurry-35bb41ad0ef2dcb34f143c0cc066610c29bae455.tar.zst |
editor load feature and return to editor command
-rw-r--r-- | locale/en.ini | 1 | ||||
-rw-r--r-- | server/editor/src/main.rs | 196 | ||||
-rw-r--r-- | server/editor/src/save.rs | 51 | ||||
-rw-r--r-- | server/src/commands.rs | 35 | ||||
-rw-r--r-- | server/src/server.rs | 2 |
5 files changed, 220 insertions, 65 deletions
diff --git a/locale/en.ini b/locale/en.ini index 041e79bc..74faa4f3 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -224,6 +224,7 @@ s.error.no_info=No information available. s.error.no_player=Player does not exist. s.error.no_tile=Tile does not exist. s.error.no_hand=Hand does not exist. +s.error.not_editor_session=Not within an editing session. s.error.packet_not_supported=Packet not supported in this session. s.error.packet_sender_invalid=Packet sent to a player that is not owned by this connection. s.error.quoting_invalid=Command quoting invalid diff --git a/server/editor/src/main.rs b/server/editor/src/main.rs index c5b6f61d..2d37859b 100644 --- a/server/editor/src/main.rs +++ b/server/editor/src/main.rs @@ -10,10 +10,10 @@ use hurrycurry_protocol::{ movement::MovementBase, }; use log::{debug, info, warn}; -use save::export_state; +use save::{export_state, import_state}; use std::{ collections::{HashMap, HashSet}, - fs::File, + fs::{File, read_to_string}, io::Write, net::SocketAddr, thread::{sleep, spawn}, @@ -33,7 +33,16 @@ struct Args { #[clap(multicall = true)] pub enum Command { Play, - Save, + Save { + name: Option<String>, + }, + Load { + name: Option<String>, + }, + Spawn { + #[arg(short, long)] + customer: bool, + }, } #[tokio::main] @@ -41,9 +50,10 @@ 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?; + let mut ms = None; loop { let (sock, addr) = ws_listener.accept().await?; - if let Err(e) = handle_conn(sock, addr).await { + if let Err(e) = handle_conn(sock, addr, &mut ms).await { warn!("client error: {e}"); } } @@ -58,12 +68,39 @@ const TILES: &[(&str, char, u8)] = &[ ("cuttingboard", 'f', 0), ("chair", 'g', 1), ("table", 'h', 0), - ("wall", 'h', 2), - ("counter-window", 'h', 0), + ("wall", 'i', 2), + ("tree", 'j', 2), + ("counter-window", 'k', 0), + ("tomato-crate", 'l', 0), + ("leek-crate", 'm', 0), + ("lettuce-crate", 'n', 0), + ("cheese-crate", 'o', 0), + ("potato-crate", 'p', 0), + ("chili-crate", 'q', 0), + ("fish-crate", 'r', 0), + ("steak-crate", 's', 0), + ("flour-crate", 't', 0), + ("coconut-crate", 'u', 0), + ("strawberry-crate", 'v', 0), + ("rice-crate", 'w', 0), + ("wall-window", 'x', 0), + ("freezer", 'y', 0), + ("trash", 'z', 0), + ("sink", 'A', 0), + ("street", 'B', 1), + ("conveyor", 'C', 0), + ("lamp", 'D', 1), + ("fence", 'E', 2), + ("door", 'F', 1), + ("path", 'G', 1), ]; #[allow(unused_assignments)] -async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> { +async fn handle_conn( + sock: TcpStream, + addr: SocketAddr, + mapname_save: &mut Option<String>, +) -> Result<()> { let sock = tokio_tungstenite::accept_async(sock).await?; info!("{addr} connected via websocket"); @@ -77,6 +114,8 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> { tile: TileIndex(0), chef_spawn: ivec2(0, 0), customer_spawn: ivec2(0, 0), + mapname: "editor".to_string(), + dirty_tiles: HashSet::new(), }; state.out.push(PacketC::Version { @@ -98,7 +137,13 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> { lobby: false, // very ironic }); - state.build_start_platform(); + if let Some(name) = mapname_save.clone() { + state.load(&name)?; + state.spawn(false); + } else { + state.build_start_platform(); + } + state.flush(); let (mut write, mut read) = sock.split(); @@ -136,6 +181,8 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr) -> Result<()> { error: true, }); } + state.flush(); + *mapname_save = Some(state.mapname.clone()); } Ok(()) } @@ -151,46 +198,59 @@ pub struct State { customer_spawn: IVec2, chef_spawn: IVec2, + + dirty_tiles: HashSet<IVec2>, + + mapname: String, } 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(), - ], - }) + self.dirty_tiles.insert(pos); + } + pub fn clear(&mut self) { + self.dirty_tiles.extend(self.tiles.keys()); + self.tiles.clear(); } 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(), - ], - }) + if !self.dirty_tiles.is_empty() { + for p in self.dirty_tiles.drain() { + self.out.push(PacketC::UpdateMap { + tile: p, + kind: self.tiles.get(&p).copied(), + neighbors: [ + self.tiles.get(&(p + IVec2::NEG_Y)).copied(), + self.tiles.get(&(p + IVec2::NEG_X)).copied(), + self.tiles.get(&(p + IVec2::Y)).copied(), + self.tiles.get(&(p + IVec2::X)).copied(), + ], + }) + } + // 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); } - self.out.push(PacketC::FlushMap); } pub fn build_start_platform(&mut self) { for x in 0..10 { @@ -198,7 +258,6 @@ impl State { self.set_tile(ivec2(x, y), TileIndex(1)); } } - self.flush(); } pub fn handle_packet(&mut self, packet: PacketS) -> Result<()> { @@ -277,23 +336,56 @@ impl State { } Ok(()) } + pub fn spawn(&mut self, c: bool) { + self.movement.position = if c { + self.customer_spawn + } else { + self.chef_spawn + } + .as_vec2(); + self.out.push(PacketC::Movement { + player: PlayerID(0), + pos: self.movement.position, + rot: 0., + dir: Vec2::X, + boost: false, + }); + self.out.push(PacketC::MovementSync { + player: PlayerID(0), + }); + } - pub fn save(&mut self) -> Result<()> { + pub fn save(&mut self, name: &str) -> Result<()> { let e = export_state(&self); - File::create("data/maps/editor.yaml")?.write_all(e.as_bytes())?; + File::create(format!("data/maps/{name}.yaml"))?.write_all(e.as_bytes())?; self.out.push(PacketC::ServerMessage { message: Message::Text(format!("Map saved.")), error: false, }); + self.mapname = name.to_owned(); + Ok(()) + } + pub fn load(&mut self, name: &str) -> Result<()> { + let e = read_to_string(format!("data/maps/{name}.yaml"))?; + self.clear(); + import_state(self, &e)?; + self.out.push(PacketC::ServerMessage { + message: Message::Text(format!("Map loaded.")), + error: false, + }); + self.mapname = name.to_owned(); Ok(()) } pub fn handle_command(&mut self, command: Command) -> Result<()> { match command { Command::Play => { - self.save()?; + self.save(&self.mapname.clone())?; + let mapname = self.mapname.clone(); spawn(move || { - if let Err(e) = start_map_bot("ws://127.0.0.1:27032", "editor") { + if let Err(e) = + start_map_bot("ws://127.0.0.1:27032", "ws://127.0.0.1:27035", &mapname) + { warn!("editor bot: {e}") } }); @@ -301,15 +393,21 @@ impl State { uri: vec!["ws://127.0.0.1:27032".to_string()], }); } - Command::Save => { - self.save()?; + Command::Save { name } => { + self.save(name.as_ref().unwrap_or(&self.mapname.clone()))?; + } + Command::Load { name } => { + self.load(name.as_ref().unwrap_or(&self.mapname.clone()))?; + } + Command::Spawn { customer } => { + self.spawn(customer); } } Ok(()) } } -fn start_map_bot(address: &str, mapname: &str) -> Result<()> { +fn start_map_bot(address: &str, own_addr: &str, mapname: &str) -> Result<()> { let mut network = Network::connect(address)?; network.queue_out.push_back(PacketS::Join { @@ -330,7 +428,9 @@ fn start_map_bot(address: &str, mapname: &str) -> Result<()> { PacketC::Joined { id } => { network.queue_out.push_back(PacketS::Communicate { player: *id, - message: Some(Message::Text(format!("/start {mapname}"))), + message: Some(Message::Text(format!( + "/set-editor-address {own_addr}\n/start {mapname}" + ))), timeout: None, pin: None, }); diff --git a/server/editor/src/save.rs b/server/editor/src/save.rs index a77cd609..c8ff350a 100644 --- a/server/editor/src/save.rs +++ b/server/editor/src/save.rs @@ -1,10 +1,17 @@ use crate::{State, TILES}; -use hurrycurry_protocol::glam::{IVec2, ivec2}; -use serde::Serialize; -use std::collections::{HashMap, HashSet}; +use anyhow::{Result, anyhow}; +use hurrycurry_protocol::{ + TileIndex, + glam::{IVec2, ivec2}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{HashMap, HashSet}, + hash::RandomState, +}; #[rustfmt::skip] -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MapDecl { map: Vec<String>, tiles: HashMap<char, String>, @@ -67,3 +74,39 @@ pub fn export_state(state: &State) -> String { }; serde_yml::to_string(&decl).unwrap() } + +pub fn import_state(state: &mut State, s: &str) -> Result<()> { + let decl: MapDecl = serde_yml::from_str(s)?; + + let name_to_tile = HashMap::<_, _, RandomState>::from_iter( + TILES + .iter() + .enumerate() + .map(|(i, (name, _, _))| ((*name).to_owned(), i)), + ); + + for (y, line) in decl.map.iter().enumerate() { + for (x, char) in line.chars().enumerate() { + let pos = ivec2(x as i32, y as i32); + if char == ' ' { + continue; + } + if char == decl.customer_spawn { + state.customer_spawn = pos; + } + if char == decl.chef_spawn { + state.chef_spawn = pos; + } + let tile = decl + .tiles + .get(&char) + .ok_or(anyhow!("char undefined {char:?}"))?; + let tile = name_to_tile + .get(tile) + .ok_or(anyhow!("unknown tile {tile:?}"))?; + state.set_tile(pos, TileIndex(*tile)); + } + } + + Ok(()) +} diff --git a/server/src/commands.rs b/server/src/commands.rs index 5daedda9..64fa50f6 100644 --- a/server/src/commands.rs +++ b/server/src/commands.rs @@ -65,35 +65,34 @@ enum Command { /// List all recipes and maps List, /// Send an effect - Effect { - name: String, - }, + Effect { name: String }, /// Send an item - Item { - name: String, - }, + Item { name: String }, /// Reload the resource index ReloadIndex, #[clap(alias = "summon", alias = "bot")] - CreateBot { - algo: String, - name: Option<String>, - }, + CreateBot { algo: String, name: Option<String> }, /// Reload the current map #[clap(alias = "r")] Reload, /// Shows the recipe book Book, + /// Start an interactive tutorial for some item #[clap(alias = "tutorial")] - StartTutorial { - item: String, - }, + StartTutorial { item: String }, + /// End the tutorial unfinished EndTutorial, #[clap(alias = "tr")] + /// Manually send a translated message TranslateMessage { message_id: String, arguments: Vec<String>, }, + /// Return to the map editor + #[clap(alias = "e", alias = "editor")] + Edit, + #[clap(hide = true)] + SetEditorAddress { url: String }, } #[derive(ValueEnum, Clone)] @@ -365,6 +364,16 @@ impl Server { timeout: None, }); } + Command::Edit => { + let addr = self + .editor_address + .clone() + .ok_or(tre!("s.error.not_editor_session"))?; + replies.push(PacketC::Redirect { uri: vec![addr] }); + } + Command::SetEditorAddress { url } => { + self.editor_address = Some(url); + } } Ok(()) } diff --git a/server/src/server.rs b/server/src/server.rs index 0889cd71..3da95a43 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -56,6 +56,7 @@ pub struct Server { pub packet_out: VecDeque<PacketC>, pub scoreboard: ScoreboardStore, pub gamedata_index: GamedataIndex, + pub editor_address: Option<String>, } pub trait GameServerExt { @@ -326,6 +327,7 @@ impl Server { scoreboard: ScoreboardStore::load() .await .context("Failed to load scoreboards")?, + editor_address: None, }) } } |