summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-12-26 10:57:17 +0100
committermetamuffin <metamuffin@disroot.org>2024-12-26 10:57:17 +0100
commit35bb41ad0ef2dcb34f143c0cc066610c29bae455 (patch)
tree6547a2301e8c9909bf927ed3f324520989d476eb
parent6ca4b3377a08fa30a13835d59f9558419f7f5cd1 (diff)
downloadhurrycurry-35bb41ad0ef2dcb34f143c0cc066610c29bae455.tar
hurrycurry-35bb41ad0ef2dcb34f143c0cc066610c29bae455.tar.bz2
hurrycurry-35bb41ad0ef2dcb34f143c0cc066610c29bae455.tar.zst
editor load feature and return to editor command
-rw-r--r--locale/en.ini1
-rw-r--r--server/editor/src/main.rs196
-rw-r--r--server/editor/src/save.rs51
-rw-r--r--server/src/commands.rs35
-rw-r--r--server/src/server.rs2
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,
})
}
}