# HG changeset patch # User unc0rr # Date 1624484511 -7200 # Node ID 7472781493936f51eb22e79ee0e8767508e9889c # Parent b06b33cf0a8948c383a4e36dd08391a142894292 Extract network protocol into a separate crate diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/Cargo.toml Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,12 @@ +[package] +name = "hedgewars-network-protocol" +version = "0.1.0" +authors = ["Andrey Korotaev "] +edition = "2018" + +[dependencies] +nom = "6" +serde_derive = "1.0" +serde = "1.0" + +proptest = "1.0" \ No newline at end of file diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/src/lib.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/lib.rs Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,3 @@ +pub mod messages; +pub mod parser; +pub mod types; diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/src/messages.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/messages.rs Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,421 @@ +use crate::types::{GameCfg, ServerVar, TeamInfo, VoteType}; +use std::iter::once; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum HwProtocolMessage { + // common messages + Ping, + Pong, + Quit(Option), + Global(String), + Watch(u32), + ToggleServerRegisteredOnly, + SuperPower, + Info(String), + // anteroom messages + Nick(String), + Proto(u16), + Password(String, String), + Checker(u16, String, String), + // lobby messages + List, + Chat(String), + CreateRoom(String, Option), + JoinRoom(String, Option), + Follow(String), + Rnd(Vec), + Kick(String), + Ban(String, String, u32), + BanIp(String, String, u32), + BanNick(String, String, u32), + BanList, + Unban(String), + SetServerVar(ServerVar), + GetServerVar, + RestartServer, + Stats, + // room messages + Part(Option), + Cfg(GameCfg), + AddTeam(Box), + RemoveTeam(String), + SetHedgehogsNumber(String, u8), + SetTeamColor(String, u8), + ToggleReady, + StartGame, + EngineMessage(String), + RoundFinished, + ToggleRestrictJoin, + ToggleRestrictTeams, + ToggleRegisteredOnly, + RoomName(String), + Delegate(String), + TeamChat(String), + MaxTeams(u8), + Fix, + Unfix, + Greeting(Option), + CallVote(Option), + Vote(bool), + ForceVote(bool), + Save(String, String), + Delete(String), + SaveRoom(String), + LoadRoom(String), +} + +#[derive(Debug, Clone, Copy)] +pub enum ProtocolFlags { + InRoom, + RoomMaster, + Ready, + InGame, + Registered, + Admin, + Contributor, +} + +impl ProtocolFlags { + #[inline] + fn flag_char(&self) -> char { + match self { + ProtocolFlags::InRoom => 'i', + ProtocolFlags::RoomMaster => 'h', + ProtocolFlags::Ready => 'r', + ProtocolFlags::InGame => 'g', + ProtocolFlags::Registered => 'u', + ProtocolFlags::Admin => 'a', + ProtocolFlags::Contributor => 'c', + } + } + + #[inline] + fn format(prefix: char, flags: &[ProtocolFlags]) -> String { + once(prefix) + .chain(flags.iter().map(|f| f.flag_char())) + .collect() + } +} + +#[inline] +pub fn add_flags(flags: &[ProtocolFlags]) -> String { + ProtocolFlags::format('+', flags) +} + +#[inline] +pub fn remove_flags(flags: &[ProtocolFlags]) -> String { + ProtocolFlags::format('-', flags) +} + +#[derive(Debug)] +pub enum HwServerMessage { + Connected(u32), + Redirect(u16), + + Ping, + Pong, + Bye(String), + + Nick(String), + Proto(u16), + AskPassword(String), + ServerAuth(String), + LogonPassed, + + LobbyLeft(String, String), + LobbyJoined(Vec), + ChatMsg { nick: String, msg: String }, + ClientFlags(String, Vec), + Rooms(Vec), + RoomAdd(Vec), + RoomJoined(Vec), + RoomLeft(String, String), + RoomRemove(String), + RoomUpdated(String, Vec), + Joining(String), + TeamAdd(Vec), + TeamRemove(String), + TeamAccepted(String), + TeamColor(String, u8), + HedgehogsNumber(String, u8), + ConfigEntry(String, Vec), + Kicked, + RunGame, + ForwardEngineMessage(Vec), + RoundFinished, + ReplayStart, + + Info(Vec), + ServerMessage(String), + ServerVars(Vec), + Notice(String), + Warning(String), + Error(String), + Unreachable, + + //Deprecated messages + LegacyReady(bool, Vec), +} + +fn special_chat(nick: &str, msg: String) -> HwServerMessage { + HwServerMessage::ChatMsg { + nick: nick.to_string(), + msg, + } +} + +pub fn server_chat(msg: String) -> HwServerMessage { + special_chat("[server]", msg) +} + +pub fn global_chat(msg: String) -> HwServerMessage { + special_chat("(global notice)", msg) +} + +impl ServerVar { + pub fn to_protocol(&self) -> Vec { + use ServerVar::*; + match self { + MOTDNew(s) => vec!["MOTD_NEW".to_string(), s.clone()], + MOTDOld(s) => vec!["MOTD_OLD".to_string(), s.clone()], + LatestProto(n) => vec!["LATEST_PROTO".to_string(), n.to_string()], + } + } +} + +impl VoteType { + pub fn to_protocol(&self) -> Vec { + use VoteType::*; + match self { + Kick(nick) => vec!["KICK".to_string(), nick.clone()], + Map(None) => vec!["MAP".to_string()], + Map(Some(name)) => vec!["MAP".to_string(), name.clone()], + Pause => vec!["PAUSE".to_string()], + NewSeed => vec!["NEWSEED".to_string()], + HedgehogsPerTeam(count) => vec!["HEDGEHOGS".to_string(), count.to_string()], + } + } +} + +impl GameCfg { + pub fn to_protocol(&self) -> (String, Vec) { + use GameCfg::*; + match self { + FeatureSize(s) => ("FEATURE_SIZE".to_string(), vec![s.to_string()]), + MapType(t) => ("MAP".to_string(), vec![t.to_string()]), + MapGenerator(g) => ("MAPGEN".to_string(), vec![g.to_string()]), + MazeSize(s) => ("MAZE_SIZE".to_string(), vec![s.to_string()]), + Seed(s) => ("SEED".to_string(), vec![s.to_string()]), + Template(t) => ("TEMPLATE".to_string(), vec![t.to_string()]), + + Ammo(n, None) => ("AMMO".to_string(), vec![n.to_string()]), + Ammo(n, Some(s)) => ("AMMO".to_string(), vec![n.to_string(), s.to_string()]), + Scheme(n, s) if s.is_empty() => ("SCHEME".to_string(), vec![n.to_string()]), + Scheme(n, s) => ("SCHEME".to_string(), { + let mut v = vec![n.to_string()]; + v.extend(s.clone()); + v + }), + Script(s) => ("SCRIPT".to_string(), vec![s.to_string()]), + Theme(t) => ("THEME".to_string(), vec![t.to_string()]), + DrawnMap(m) => ("DRAWNMAP".to_string(), vec![m.to_string()]), + } + } + + pub fn to_server_msg(&self) -> HwServerMessage { + let (name, args) = self.to_protocol(); + HwServerMessage::ConfigEntry(name, args) + } +} + +impl TeamInfo { + pub fn to_protocol(&self) -> Vec { + let mut info = vec![ + self.name.clone(), + self.grave.clone(), + self.fort.clone(), + self.voice_pack.clone(), + self.flag.clone(), + self.owner.clone(), + self.difficulty.to_string(), + ]; + let hogs = self + .hedgehogs + .iter() + .flat_map(|h| once(h.name.clone()).chain(once(h.hat.clone()))); + info.extend(hogs); + info + } +} + +macro_rules! const_braces { + ($e: expr) => { + "{}\n" + }; +} + +macro_rules! msg { + [$($part: expr),*] => { + format!(concat!($(const_braces!($part)),*, "\n"), $($part),*); + }; +} + +impl HwProtocolMessage { + /** Converts the message to a raw `String`, which can be sent over the network. + * + * This is the inverse of the `message` parser. + */ + pub fn to_raw_protocol(&self) -> String { + use self::HwProtocolMessage::*; + match self { + Ping => msg!["PING"], + Pong => msg!["PONG"], + Quit(None) => msg!["QUIT"], + Quit(Some(msg)) => msg!["QUIT", msg], + Global(msg) => msg!["CMD", format!("GLOBAL {}", msg)], + Watch(name) => msg!["CMD", format!("WATCH {}", name)], + ToggleServerRegisteredOnly => msg!["CMD", "REGISTERED_ONLY"], + SuperPower => msg!["CMD", "SUPER_POWER"], + Info(info) => msg!["CMD", format!("INFO {}", info)], + Nick(nick) => msg!("NICK", nick), + Proto(version) => msg!["PROTO", version], + Password(p, s) => msg!["PASSWORD", p, s], + Checker(i, n, p) => msg!["CHECKER", i, n, p], + List => msg!["LIST"], + Chat(msg) => msg!["CHAT", msg], + CreateRoom(name, None) => msg!["CREATE_ROOM", name], + CreateRoom(name, Some(password)) => msg!["CREATE_ROOM", name, password], + JoinRoom(name, None) => msg!["JOIN_ROOM", name], + JoinRoom(name, Some(password)) => msg!["JOIN_ROOM", name, password], + Follow(name) => msg!["FOLLOW", name], + Rnd(args) => { + if args.is_empty() { + msg!["CMD", "RND"] + } else { + msg!["CMD", format!("RND {}", args.join(" "))] + } + } + Kick(name) => msg!["KICK", name], + Ban(name, reason, time) => msg!["BAN", name, reason, time], + BanIp(ip, reason, time) => msg!["BAN_IP", ip, reason, time], + BanNick(nick, reason, time) => msg!("BAN_NICK", nick, reason, time), + BanList => msg!["BANLIST"], + Unban(name) => msg!["UNBAN", name], + SetServerVar(var) => construct_message(&["SET_SERVER_VAR"], &var.to_protocol()), + GetServerVar => msg!["GET_SERVER_VAR"], + RestartServer => msg!["CMD", "RESTART_SERVER YES"], + Stats => msg!["CMD", "STATS"], + Part(None) => msg!["PART"], + Part(Some(msg)) => msg!["PART", msg], + Cfg(config) => { + let (name, args) = config.to_protocol(); + msg!["CFG", name, args.join("\n")] + } + AddTeam(info) => msg![ + "ADD_TEAM", + info.name, + info.color, + info.grave, + info.fort, + info.voice_pack, + info.flag, + info.difficulty, + &(info.hedgehogs.iter()) + .flat_map(|h| [&h.name[..], &h.hat[..]]) + .collect::>() + .join("\n") + ], + RemoveTeam(name) => msg!["REMOVE_TEAM", name], + SetHedgehogsNumber(team, number) => msg!["HH_NUM", team, number], + SetTeamColor(team, color) => msg!["TEAM_COLOR", team, color], + ToggleReady => msg!["TOGGLE_READY"], + StartGame => msg!["START_GAME"], + EngineMessage(msg) => msg!["EM", msg], + RoundFinished => msg!["ROUNDFINISHED"], + ToggleRestrictJoin => msg!["TOGGLE_RESTRICT_JOINS"], + ToggleRestrictTeams => msg!["TOGGLE_RESTRICT_TEAMS"], + ToggleRegisteredOnly => msg!["TOGGLE_REGISTERED_ONLY"], + RoomName(name) => msg!["ROOM_NAME", name], + Delegate(name) => msg!["CMD", format!("DELEGATE {}", name)], + TeamChat(msg) => msg!["TEAMCHAT", msg], + MaxTeams(count) => msg!["CMD", format!("MAXTEAMS {}", count)], + Fix => msg!["CMD", "FIX"], + Unfix => msg!["CMD", "UNFIX"], + Greeting(None) => msg!["CMD", "GREETING"], + Greeting(Some(msg)) => msg!["CMD", format!("GREETING {}", msg)], + CallVote(None) => msg!["CMD", "CALLVOTE"], + CallVote(Some(vote)) => { + msg!["CMD", format!("CALLVOTE {}", &vote.to_protocol().join(" "))] + } + Vote(msg) => msg!["CMD", format!("VOTE {}", if *msg { "YES" } else { "NO" })], + ForceVote(msg) => msg!["CMD", format!("FORCE {}", if *msg { "YES" } else { "NO" })], + Save(name, location) => msg!["CMD", format!("SAVE {} {}", name, location)], + Delete(name) => msg!["CMD", format!("DELETE {}", name)], + SaveRoom(name) => msg!["CMD", format!("SAVEROOM {}", name)], + LoadRoom(name) => msg!["CMD", format!("LOADROOM {}", name)], + } + } +} + +fn construct_message(header: &[&str], msg: &[String]) -> String { + let mut v: Vec<_> = header.iter().cloned().collect(); + v.extend(msg.iter().map(|s| &s[..])); + v.push("\n"); + v.join("\n") +} + +impl HwServerMessage { + pub fn to_raw_protocol(&self) -> String { + use self::HwServerMessage::*; + match self { + Ping => msg!["PING"], + Pong => msg!["PONG"], + Connected(protocol_version) => msg![ + "CONNECTED", + "Hedgewars server https://www.hedgewars.org/", + protocol_version + ], + Redirect(port) => msg!["REDIRECT", port], + Bye(msg) => msg!["BYE", msg], + Nick(nick) => msg!["NICK", nick], + Proto(proto) => msg!["PROTO", proto], + AskPassword(salt) => msg!["ASKPASSWORD", salt], + ServerAuth(hash) => msg!["SERVER_AUTH", hash], + LogonPassed => msg!["LOGONPASSED"], + LobbyLeft(nick, msg) => msg!["LOBBY:LEFT", nick, msg], + LobbyJoined(nicks) => construct_message(&["LOBBY:JOINED"], &nicks), + ClientFlags(flags, nicks) => construct_message(&["CLIENT_FLAGS", flags], &nicks), + Rooms(info) => construct_message(&["ROOMS"], &info), + RoomAdd(info) => construct_message(&["ROOM", "ADD"], &info), + RoomJoined(nicks) => construct_message(&["JOINED"], &nicks), + RoomLeft(nick, msg) => msg!["LEFT", nick, msg], + RoomRemove(name) => msg!["ROOM", "DEL", name], + RoomUpdated(name, info) => construct_message(&["ROOM", "UPD", name], &info), + Joining(name) => msg!["JOINING", name], + TeamAdd(info) => construct_message(&["ADD_TEAM"], &info), + TeamRemove(name) => msg!["REMOVE_TEAM", name], + TeamAccepted(name) => msg!["TEAM_ACCEPTED", name], + TeamColor(name, color) => msg!["TEAM_COLOR", name, color], + HedgehogsNumber(name, number) => msg!["HH_NUM", name, number], + ConfigEntry(name, values) => construct_message(&["CFG", name], &values), + Kicked => msg!["KICKED"], + RunGame => msg!["RUN_GAME"], + ForwardEngineMessage(em) => construct_message(&["EM"], &em), + RoundFinished => msg!["ROUND_FINISHED"], + ChatMsg { nick, msg } => msg!["CHAT", nick, msg], + Info(info) => construct_message(&["INFO"], &info), + ServerMessage(msg) => msg!["SERVER_MESSAGE", msg], + ServerVars(vars) => construct_message(&["SERVER_VARS"], &vars), + Notice(msg) => msg!["NOTICE", msg], + Warning(msg) => msg!["WARNING", msg], + Error(msg) => msg!["ERROR", msg], + ReplayStart => msg!["REPLAY_START"], + + LegacyReady(is_ready, nicks) => { + construct_message(&[if *is_ready { "READY" } else { "NOT_READY" }], &nicks) + } + + _ => msg!["ERROR", "UNIMPLEMENTED"], + } + } +} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/src/parser.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/parser.rs Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,505 @@ +/** The parsers for the chat and multiplayer protocol. The main parser is `message`. + * # Protocol + * All messages consist of `\n`-separated strings. The end of a message is + * indicated by a double newline - `\n\n`. + * + * For example, a nullary command like PING will be actually sent as `PING\n\n`. + * A unary command, such as `START_GAME nick` will be actually sent as `START_GAME\nnick\n\n`. + */ +use nom::{ + branch::alt, + bytes::complete::{tag, tag_no_case, take_until, take_while}, + character::complete::{newline, not_line_ending}, + combinator::{map, peek}, + error::{ErrorKind, ParseError}, + multi::separated_list0, + sequence::{delimited, pair, preceded, terminated, tuple}, + Err, IResult, +}; + +use std::{ + num::ParseIntError, + str, + str::{FromStr, Utf8Error}, +}; + +use crate::messages::{HwProtocolMessage, HwProtocolMessage::*}; +use crate::types::{GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType}; + +#[derive(Debug, PartialEq)] +pub struct HwProtocolError {} + +impl HwProtocolError { + pub fn new() -> Self { + HwProtocolError {} + } +} + +impl ParseError for HwProtocolError { + fn from_error_kind(_input: I, _kind: ErrorKind) -> Self { + HwProtocolError::new() + } + + fn append(_input: I, _kind: ErrorKind, _other: Self) -> Self { + HwProtocolError::new() + } +} + +impl From for HwProtocolError { + fn from(_: Utf8Error) -> Self { + HwProtocolError::new() + } +} + +impl From for HwProtocolError { + fn from(_: ParseIntError) -> Self { + HwProtocolError::new() + } +} + +pub type HwResult<'a, O> = IResult<&'a [u8], O, HwProtocolError>; + +fn end_of_message(input: &[u8]) -> HwResult<&[u8]> { + tag("\n\n")(input) +} + +fn convert_utf8(input: &[u8]) -> HwResult<&str> { + match str::from_utf8(input) { + Ok(str) => Ok((b"", str)), + Err(utf_err) => Result::Err(Err::Failure(utf_err.into())), + } +} + +fn convert_from_str(str: &str) -> HwResult +where + T: FromStr, +{ + match T::from_str(str) { + Ok(x) => Ok((b"", x)), + Err(format_err) => Result::Err(Err::Failure(format_err.into())), + } +} + +fn str_line(input: &[u8]) -> HwResult<&str> { + let (i, text) = not_line_ending(<&[u8]>::clone(&input))?; + if i != input { + Ok((i, convert_utf8(text)?.1)) + } else { + Err(Err::Error(HwProtocolError::new())) + } +} + +fn a_line(input: &[u8]) -> HwResult { + map(str_line, String::from)(input) +} + +fn cmd_arg(input: &[u8]) -> HwResult { + let delimiters = b" \n"; + let (i, str) = take_while(move |c| !delimiters.contains(&c))(<&[u8]>::clone(&input))?; + if i != input { + Ok((i, convert_utf8(str)?.1.to_string())) + } else { + Err(Err::Error(HwProtocolError::new())) + } +} + +fn u8_line(input: &[u8]) -> HwResult { + let (i, str) = str_line(input)?; + Ok((i, convert_from_str(str)?.1)) +} + +fn u16_line(input: &[u8]) -> HwResult { + let (i, str) = str_line(input)?; + Ok((i, convert_from_str(str)?.1)) +} + +fn u32_line(input: &[u8]) -> HwResult { + let (i, str) = str_line(input)?; + Ok((i, convert_from_str(str)?.1)) +} + +fn yes_no_line(input: &[u8]) -> HwResult { + alt(( + map(tag_no_case(b"YES"), |_| true), + map(tag_no_case(b"NO"), |_| false), + ))(input) +} + +fn opt_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option> { + alt(( + map(peek(end_of_message), |_| None), + map(preceded(tag("\n"), a_line), Some), + ))(input) +} + +fn spaces(input: &[u8]) -> HwResult<&[u8]> { + preceded(tag(" "), take_while(|c| c == b' '))(input) +} + +fn opt_space_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option> { + alt(( + map(peek(end_of_message), |_| None), + map(preceded(spaces, a_line), Some), + ))(input) +} + +fn hedgehog_array(input: &[u8]) -> HwResult<[HedgehogInfo; 8]> { + fn hedgehog_line(input: &[u8]) -> HwResult { + map( + tuple((terminated(a_line, newline), a_line)), + |(name, hat)| HedgehogInfo { name, hat }, + )(input) + } + + let (i, (h1, h2, h3, h4, h5, h6, h7, h8)) = tuple(( + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + hedgehog_line, + ))(input)?; + + Ok((i, [h1, h2, h3, h4, h5, h6, h7, h8])) +} + +fn voting(input: &[u8]) -> HwResult { + alt(( + map(tag_no_case("PAUSE"), |_| VoteType::Pause), + map(tag_no_case("NEWSEED"), |_| VoteType::NewSeed), + map( + preceded(pair(tag_no_case("KICK"), spaces), a_line), + VoteType::Kick, + ), + map( + preceded(pair(tag_no_case("HEDGEHOGS"), spaces), u8_line), + VoteType::HedgehogsPerTeam, + ), + map(preceded(tag_no_case("MAP"), opt_space_arg), VoteType::Map), + ))(input) +} + +fn no_arg_message(input: &[u8]) -> HwResult { + fn message<'a>( + name: &'a str, + msg: HwProtocolMessage, + ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { + move |i| map(tag(name), |_| msg.clone())(i) + } + + alt(( + message("PING", Ping), + message("PONG", Pong), + message("LIST", List), + message("BANLIST", BanList), + message("GET_SERVER_VAR", GetServerVar), + message("TOGGLE_READY", ToggleReady), + message("START_GAME", StartGame), + message("TOGGLE_RESTRICT_JOINS", ToggleRestrictJoin), + message("TOGGLE_RESTRICT_TEAMS", ToggleRestrictTeams), + message("TOGGLE_REGISTERED_ONLY", ToggleRegisteredOnly), + ))(input) +} + +fn single_arg_message(input: &[u8]) -> HwResult { + fn message<'a, T, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> + where + F: Fn(&[u8]) -> HwResult, + G: Fn(T) -> HwProtocolMessage, + { + map(preceded(tag(name), parser), constructor) + } + + alt(( + message("NICK\n", a_line, Nick), + message("INFO\n", a_line, Info), + message("CHAT\n", a_line, Chat), + message("PART", opt_arg, Part), + message("FOLLOW\n", a_line, Follow), + message("KICK\n", a_line, Kick), + message("UNBAN\n", a_line, Unban), + message("EM\n", a_line, EngineMessage), + message("TEAMCHAT\n", a_line, TeamChat), + message("ROOM_NAME\n", a_line, RoomName), + message("REMOVE_TEAM\n", a_line, RemoveTeam), + message("ROUNDFINISHED", opt_arg, |_| RoundFinished), + message("PROTO\n", u16_line, Proto), + message("QUIT", opt_arg, Quit), + ))(input) +} + +fn cmd_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { + fn cmd_no_arg<'a>( + name: &'a str, + msg: HwProtocolMessage, + ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { + move |i| map(tag_no_case(name), |_| msg.clone())(i) + } + + fn cmd_single_arg<'a, T, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> + where + F: Fn(&'a [u8]) -> HwResult<'a, T>, + G: Fn(T) -> HwProtocolMessage, + { + map( + preceded(pair(tag_no_case(name), spaces), parser), + constructor, + ) + } + + fn cmd_no_arg_message(input: &[u8]) -> HwResult { + alt(( + cmd_no_arg("STATS", Stats), + cmd_no_arg("FIX", Fix), + cmd_no_arg("UNFIX", Unfix), + cmd_no_arg("REGISTERED_ONLY", ToggleServerRegisteredOnly), + cmd_no_arg("SUPER_POWER", SuperPower), + ))(input) + } + + fn cmd_single_arg_message(input: &[u8]) -> HwResult { + alt(( + cmd_single_arg("RESTART_SERVER", |i| tag("YES")(i), |_| RestartServer), + cmd_single_arg("DELEGATE", a_line, Delegate), + cmd_single_arg("DELETE", a_line, Delete), + cmd_single_arg("SAVEROOM", a_line, SaveRoom), + cmd_single_arg("LOADROOM", a_line, LoadRoom), + cmd_single_arg("GLOBAL", a_line, Global), + cmd_single_arg("WATCH", u32_line, Watch), + cmd_single_arg("VOTE", yes_no_line, Vote), + cmd_single_arg("FORCE", yes_no_line, ForceVote), + cmd_single_arg("INFO", a_line, Info), + cmd_single_arg("MAXTEAMS", u8_line, MaxTeams), + cmd_single_arg("CALLVOTE", voting, |v| CallVote(Some(v))), + ))(input) + } + + preceded( + tag("CMD\n"), + alt(( + cmd_no_arg_message, + cmd_single_arg_message, + map(tag_no_case("CALLVOTE"), |_| CallVote(None)), + map(preceded(tag_no_case("GREETING"), opt_space_arg), Greeting), + map(preceded(tag_no_case("PART"), opt_space_arg), Part), + map(preceded(tag_no_case("QUIT"), opt_space_arg), Quit), + map( + preceded( + tag_no_case("SAVE"), + pair(preceded(spaces, cmd_arg), preceded(spaces, cmd_arg)), + ), + |(n, l)| Save(n, l), + ), + map( + preceded( + tag_no_case("RND"), + alt(( + map(peek(end_of_message), |_| vec![]), + preceded(spaces, separated_list0(spaces, cmd_arg)), + )), + ), + Rnd, + ), + )), + )(input) +} + +fn config_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { + fn cfg_single_arg<'a, T, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult<'a, GameCfg> + where + F: Fn(&[u8]) -> HwResult, + G: Fn(T) -> GameCfg, + { + map(preceded(pair(tag(name), newline), parser), constructor) + } + + let (i, cfg) = preceded( + tag("CFG\n"), + alt(( + cfg_single_arg("THEME", a_line, GameCfg::Theme), + cfg_single_arg("SCRIPT", a_line, GameCfg::Script), + cfg_single_arg("MAP", a_line, GameCfg::MapType), + cfg_single_arg("MAPGEN", u32_line, GameCfg::MapGenerator), + cfg_single_arg("MAZE_SIZE", u32_line, GameCfg::MazeSize), + cfg_single_arg("TEMPLATE", u32_line, GameCfg::Template), + cfg_single_arg("FEATURE_SIZE", u32_line, GameCfg::FeatureSize), + cfg_single_arg("SEED", a_line, GameCfg::Seed), + cfg_single_arg("DRAWNMAP", a_line, GameCfg::DrawnMap), + preceded(pair(tag("AMMO"), newline), |i| { + let (i, name) = a_line(i)?; + let (i, value) = opt_arg(i)?; + Ok((i, GameCfg::Ammo(name, value))) + }), + preceded( + pair(tag("SCHEME"), newline), + map( + pair( + a_line, + alt(( + map(peek(end_of_message), |_| None), + map(preceded(newline, separated_list0(newline, a_line)), Some), + )), + ), + |(name, values)| GameCfg::Scheme(name, values.unwrap_or_default()), + ), + ), + )), + )(input)?; + Ok((i, Cfg(cfg))) +} + +fn server_var_message(input: &[u8]) -> HwResult { + map( + preceded( + tag("SET_SERVER_VAR\n"), + alt(( + map(preceded(tag("MOTD_NEW\n"), a_line), ServerVar::MOTDNew), + map(preceded(tag("MOTD_OLD\n"), a_line), ServerVar::MOTDOld), + map( + preceded(tag("LATEST_PROTO\n"), u16_line), + ServerVar::LatestProto, + ), + )), + ), + SetServerVar, + )(input) +} + +fn complex_message(input: &[u8]) -> HwResult { + alt(( + preceded( + pair(tag("PASSWORD"), newline), + map(pair(terminated(a_line, newline), a_line), |(pass, salt)| { + Password(pass, salt) + }), + ), + preceded( + pair(tag("CHECKER"), newline), + map( + tuple(( + terminated(u16_line, newline), + terminated(a_line, newline), + a_line, + )), + |(protocol, name, pass)| Checker(protocol, name, pass), + ), + ), + preceded( + pair(tag("CREATE_ROOM"), newline), + map(pair(a_line, opt_arg), |(name, pass)| CreateRoom(name, pass)), + ), + preceded( + pair(tag("JOIN_ROOM"), newline), + map(pair(a_line, opt_arg), |(name, pass)| JoinRoom(name, pass)), + ), + preceded( + pair(tag("ADD_TEAM"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(u8_line, newline), + terminated(a_line, newline), + terminated(a_line, newline), + terminated(a_line, newline), + terminated(a_line, newline), + terminated(u8_line, newline), + hedgehog_array, + )), + |(name, color, grave, fort, voice_pack, flag, difficulty, hedgehogs)| { + AddTeam(Box::new(TeamInfo { + owner: String::new(), + name, + color, + grave, + fort, + voice_pack, + flag, + difficulty, + hedgehogs, + hedgehogs_number: 0, + })) + }, + ), + ), + preceded( + pair(tag("HH_NUM"), newline), + map( + pair(terminated(a_line, newline), u8_line), + |(name, count)| SetHedgehogsNumber(name, count), + ), + ), + preceded( + pair(tag("TEAM_COLOR"), newline), + map( + pair(terminated(a_line, newline), u8_line), + |(name, color)| SetTeamColor(name, color), + ), + ), + preceded( + pair(tag("BAN"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(a_line, newline), + u32_line, + )), + |(name, reason, time)| Ban(name, reason, time), + ), + ), + preceded( + pair(tag("BAN_IP"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(a_line, newline), + u32_line, + )), + |(ip, reason, time)| BanIp(ip, reason, time), + ), + ), + preceded( + pair(tag("BAN_NICK"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(a_line, newline), + u32_line, + )), + |(nick, reason, time)| BanNick(nick, reason, time), + ), + ), + ))(input) +} + +pub fn malformed_message(input: &[u8]) -> HwResult<()> { + map(terminated(take_until(&b"\n\n"[..]), end_of_message), |_| ())(input) +} + +pub fn message(input: &[u8]) -> HwResult { + delimited( + take_while(|c| c == b'\n'), + alt(( + no_arg_message, + single_arg_message, + cmd_message, + config_message, + server_var_message, + complex_message, + )), + end_of_message, + )(input) +} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/src/types.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/types.rs Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,358 @@ +use serde_derive::{Deserialize, Serialize}; + +pub const MAX_HEDGEHOGS_PER_TEAM: u8 = 8; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum ServerVar { + MOTDNew(String), + MOTDOld(String), + LatestProto(u16), +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum GameCfg { + FeatureSize(u32), + MapType(String), + MapGenerator(u32), + MazeSize(u32), + Seed(String), + Template(u32), + + Ammo(String, Option), + Scheme(String, Vec), + Script(String), + Theme(String), + DrawnMap(String), +} + +#[derive(PartialEq, Eq, Clone, Debug, Default)] +pub struct TeamInfo { + pub owner: String, + pub name: String, + pub color: u8, + pub grave: String, + pub fort: String, + pub voice_pack: String, + pub flag: String, + pub difficulty: u8, + pub hedgehogs_number: u8, + pub hedgehogs: [HedgehogInfo; MAX_HEDGEHOGS_PER_TEAM as usize], +} + +#[derive(PartialEq, Eq, Clone, Debug, Default)] +pub struct HedgehogInfo { + pub name: String, + pub hat: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Ammo { + pub name: String, + pub settings: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Scheme { + pub name: String, + pub settings: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct RoomConfig { + pub feature_size: u32, + pub map_type: String, + pub map_generator: u32, + pub maze_size: u32, + pub seed: String, + pub template: u32, + + pub ammo: Ammo, + pub scheme: Scheme, + pub script: String, + pub theme: String, + pub drawn_map: Option, +} + +impl RoomConfig { + pub fn new() -> RoomConfig { + RoomConfig { + feature_size: 12, + map_type: "+rnd+".to_string(), + map_generator: 0, + maze_size: 0, + seed: "seed".to_string(), + template: 0, + + ammo: Ammo { + name: "Default".to_string(), + settings: None, + }, + scheme: Scheme { + name: "Default".to_string(), + settings: Vec::new(), + }, + script: "Normal".to_string(), + theme: "\u{1f994}".to_string(), + drawn_map: None, + } + } + + pub fn set_config(&mut self, cfg: GameCfg) { + match cfg { + GameCfg::FeatureSize(s) => self.feature_size = s, + GameCfg::MapType(t) => self.map_type = t, + GameCfg::MapGenerator(g) => self.map_generator = g, + GameCfg::MazeSize(s) => self.maze_size = s, + GameCfg::Seed(s) => self.seed = s, + GameCfg::Template(t) => self.template = t, + + GameCfg::Ammo(n, s) => { + self.ammo = Ammo { + name: n, + settings: s, + } + } + GameCfg::Scheme(n, s) => { + self.scheme = Scheme { + name: n, + settings: s, + } + } + GameCfg::Script(s) => self.script = s, + GameCfg::Theme(t) => self.theme = t, + GameCfg::DrawnMap(m) => self.drawn_map = Some(m), + }; + } + + pub fn to_map_config(&self) -> Vec { + vec![ + self.feature_size.to_string(), + self.map_type.to_string(), + self.map_generator.to_string(), + self.maze_size.to_string(), + self.seed.to_string(), + self.template.to_string(), + ] + } + + pub fn to_game_config(&self) -> Vec { + use GameCfg::*; + let mut v = vec![ + Ammo(self.ammo.name.to_string(), self.ammo.settings.clone()), + Scheme(self.scheme.name.to_string(), self.scheme.settings.clone()), + Script(self.script.to_string()), + Theme(self.theme.to_string()), + ]; + if let Some(ref m) = self.drawn_map { + v.push(DrawnMap(m.to_string())) + } + v + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum VoteType { + Kick(String), + Map(Option), + Pause, + NewSeed, + HedgehogsPerTeam(u8), +} + +pub struct Vote { + pub is_pro: bool, + pub is_forced: bool, +} + +//#[cfg(test)] +#[macro_use] +pub mod testing { + use crate::types::ServerVar::*; + use crate::types::*; + use proptest::{ + arbitrary::{any, Arbitrary}, + strategy::{BoxedStrategy, Just, Strategy}, + }; + + // Due to inability to define From between Options + pub trait Into2: Sized { + fn into2(self) -> T; + } + impl Into2 for T { + fn into2(self) -> T { + self + } + } + impl Into2> for Vec { + fn into2(self) -> Vec { + self.into_iter().map(|x| x.0).collect() + } + } + impl Into2 for Ascii { + fn into2(self) -> String { + self.0 + } + } + impl Into2> for Option { + fn into2(self) -> Option { + self.map(|x| x.0) + } + } + + #[macro_export] + macro_rules! proto_msg_case { + ($val: ident()) => { + Just($val) + }; + ($val: ident($arg: ty)) => { + any::<$arg>().prop_map(|v| $val(v.into2())) + }; + ($val: ident($arg1: ty, $arg2: ty)) => { + any::<($arg1, $arg2)>().prop_map(|v| $val(v.0.into2(), v.1.into2())) + }; + ($val: ident($arg1: ty, $arg2: ty, $arg3: ty)) => { + any::<($arg1, $arg2, $arg3)>().prop_map(|v| $val(v.0.into2(), v.1.into2(), v.2.into2())) + }; + } + + #[macro_export] + macro_rules! proto_msg_match { + ($var: expr, def = $default: expr, $($num: expr => $constr: ident $res: tt),*) => ( + match $var { + $($num => (proto_msg_case!($constr $res)).boxed()),*, + _ => Just($default).boxed() + } + ) +} + + /// Wrapper type for generating non-empty strings + #[derive(Debug)] + pub struct Ascii(String); + + impl Arbitrary for Ascii { + type Parameters = ::Parameters; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + "[a-zA-Z0-9]+".prop_map(Ascii).boxed() + } + + type Strategy = BoxedStrategy; + } + + impl Arbitrary for GameCfg { + type Parameters = (); + + fn arbitrary_with(_args: ::Parameters) -> ::Strategy { + use crate::types::GameCfg::*; + (0..10) + .no_shrink() + .prop_flat_map(|i| { + proto_msg_match!(i, def = FeatureSize(0), + 0 => FeatureSize(u32), + 1 => MapType(Ascii), + 2 => MapGenerator(u32), + 3 => MazeSize(u32), + 4 => Seed(Ascii), + 5 => Template(u32), + 6 => Ammo(Ascii, Option), + 7 => Scheme(Ascii, Vec), + 8 => Script(Ascii), + 9 => Theme(Ascii), + 10 => DrawnMap(Ascii)) + }) + .boxed() + } + + type Strategy = BoxedStrategy; + } + + impl Arbitrary for TeamInfo { + type Parameters = (); + + fn arbitrary_with(_args: ::Parameters) -> ::Strategy { + ( + "[a-z]+", + 0u8..127u8, + "[a-z]+", + "[a-z]+", + "[a-z]+", + "[a-z]+", + 0u8..127u8, + ) + .prop_map(|(name, color, grave, fort, voice_pack, flag, difficulty)| { + fn hog(n: u8) -> HedgehogInfo { + HedgehogInfo { + name: format!("hog{}", n), + hat: format!("hat{}", n), + } + } + let hedgehogs = [ + hog(1), + hog(2), + hog(3), + hog(4), + hog(5), + hog(6), + hog(7), + hog(8), + ]; + TeamInfo { + owner: String::new(), + name, + color, + grave, + fort, + voice_pack, + flag, + difficulty, + hedgehogs, + hedgehogs_number: 0, + } + }) + .boxed() + } + + type Strategy = BoxedStrategy; + } + + impl Arbitrary for ServerVar { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (0..=2) + .no_shrink() + .prop_flat_map(|i| { + proto_msg_match!(i, def = ServerVar::LatestProto(0), + 0 => MOTDNew(Ascii), + 1 => MOTDOld(Ascii), + 2 => LatestProto(u16) + ) + }) + .boxed() + } + + type Strategy = BoxedStrategy; + } + + impl Arbitrary for VoteType { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use VoteType::*; + (0..=4) + .no_shrink() + .prop_flat_map(|i| { + proto_msg_match!(i, def = VoteType::Pause, + 0 => Kick(Ascii), + 1 => Map(Option), + 2 => Pause(), + 3 => NewSeed(), + 4 => HedgehogsPerTeam(u8) + ) + }) + .boxed() + } + + type Strategy = BoxedStrategy; + } +} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/tests/parser.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/tests/parser.rs Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,54 @@ +use hedgewars_network_protocol::{ + parser::message, + types::GameCfg, + {messages::HwProtocolMessage::*, parser::HwProtocolError}, +}; + +#[test] +fn parse_test() { + assert_eq!(message(b"PING\n\n"), Ok((&b""[..], Ping))); + assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame))); + assert_eq!( + message(b"NICK\nit's me\n\n"), + Ok((&b""[..], Nick("it's me".to_string()))) + ); + assert_eq!(message(b"PROTO\n51\n\n"), Ok((&b""[..], Proto(51)))); + assert_eq!( + message(b"QUIT\nbye-bye\n\n"), + Ok((&b""[..], Quit(Some("bye-bye".to_string())))) + ); + assert_eq!(message(b"QUIT\n\n"), Ok((&b""[..], Quit(None)))); + assert_eq!( + message(b"CMD\nwatch 49471\n\n"), + Ok((&b""[..], Watch(49471))) + ); + assert_eq!( + message(b"BAN\nme\nbad\n77\n\n"), + Ok((&b""[..], Ban("me".to_string(), "bad".to_string(), 77))) + ); + + assert_eq!(message(b"CMD\nPART\n\n"), Ok((&b""[..], Part(None)))); + assert_eq!( + message(b"CMD\nPART _msg_\n\n"), + Ok((&b""[..], Part(Some("_msg_".to_string())))) + ); + + assert_eq!(message(b"CMD\nRND\n\n"), Ok((&b""[..], Rnd(vec![])))); + assert_eq!( + message(b"CMD\nRND A B\n\n"), + Ok((&b""[..], Rnd(vec![String::from("A"), String::from("B")]))) + ); + + assert_eq!( + message(b"CFG\nSCHEME\na\nA\n\n"), + Ok(( + &b""[..], + Cfg(GameCfg::Scheme("a".to_string(), vec!["A".to_string()])) + )) + ); + + assert_eq!( + message(b"QUIT\n1\n2\n\n"), + Err(nom::Err::Error(HwProtocolError::new())) + ); +} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-network-protocol/tests/test.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/tests/test.rs Wed Jun 23 23:41:51 2021 +0200 @@ -0,0 +1,83 @@ +use proptest::{ + arbitrary::any, + proptest, + strategy::{BoxedStrategy, Just, Strategy}, +}; + +use hedgewars_network_protocol::messages::{HwProtocolMessage, HwProtocolMessage::*}; +use hedgewars_network_protocol::parser::message; +use hedgewars_network_protocol::types::{GameCfg, ServerVar, TeamInfo, VoteType}; + +use hedgewars_network_protocol::types::testing::*; +use hedgewars_network_protocol::{proto_msg_case, proto_msg_match}; + +pub fn gen_proto_msg() -> BoxedStrategy where { + let res = (0..=55).no_shrink().prop_flat_map(|i| { + proto_msg_match!(i, def = Ping, + 0 => Ping(), + 1 => Pong(), + 2 => Quit(Option), + 4 => Global(Ascii), + 5 => Watch(u32), + 6 => ToggleServerRegisteredOnly(), + 7 => SuperPower(), + 8 => Info(Ascii), + 9 => Nick(Ascii), + 10 => Proto(u16), + 11 => Password(Ascii, Ascii), + 12 => Checker(u16, Ascii, Ascii), + 13 => List(), + 14 => Chat(Ascii), + 15 => CreateRoom(Ascii, Option), + 16 => JoinRoom(Ascii, Option), + 17 => Follow(Ascii), + 18 => Rnd(Vec), + 19 => Kick(Ascii), + 20 => Ban(Ascii, Ascii, u32), + 21 => BanIp(Ascii, Ascii, u32), + 22 => BanNick(Ascii, Ascii, u32), + 23 => BanList(), + 24 => Unban(Ascii), + 25 => SetServerVar(ServerVar), + 26 => GetServerVar(), + 27 => RestartServer(), + 28 => Stats(), + 29 => Part(Option), + 30 => Cfg(GameCfg), + 31 => AddTeam(Box), + 32 => RemoveTeam(Ascii), + 33 => SetHedgehogsNumber(Ascii, u8), + 34 => SetTeamColor(Ascii, u8), + 35 => ToggleReady(), + 36 => StartGame(), + 37 => EngineMessage(Ascii), + 38 => RoundFinished(), + 39 => ToggleRestrictJoin(), + 40 => ToggleRestrictTeams(), + 41 => ToggleRegisteredOnly(), + 42 => RoomName(Ascii), + 43 => Delegate(Ascii), + 44 => TeamChat(Ascii), + 45 => MaxTeams(u8), + 46 => Fix(), + 47 => Unfix(), + 48 => Greeting(Option), + 49 => CallVote(Option), + 50 => Vote(bool), + 51 => ForceVote(bool), + 52 => Save(Ascii, Ascii), + 53 => Delete(Ascii), + 54 => SaveRoom(Ascii), + 55 => LoadRoom(Ascii) + ) + }); + res.boxed() +} + +proptest! { + #[test] + fn is_parser_composition_idempotent(ref msg in gen_proto_msg()) { + println!("!! Msg: {:?}, Bytes: {:?} !!", msg, msg.to_raw_protocol().as_bytes()); + assert_eq!(message(msg.to_raw_protocol().as_bytes()), Ok((&b""[..], msg.clone()))) + } +} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/Cargo.toml --- a/rust/hedgewars-server/Cargo.toml Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/Cargo.toml Wed Jun 23 23:41:51 2021 +0200 @@ -1,7 +1,7 @@ [package] edition = "2018" name = "hedgewars-server" -version = "0.0.1" +version = "0.9.0" authors = [ "Andrey Korotaev " ] [features] @@ -26,6 +26,7 @@ serde_derive = "1.0" openssl = { version = "0.10", optional = true } mysql = { version = "15.0", optional = true } +hedgewars-network-protocol = { path = "../hedgewars-network-protocol" } [dev-dependencies] proptest = "1.0" diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/core/room.rs --- a/rust/hedgewars-server/src/core/room.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/core/room.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,10 +1,11 @@ use super::{ client::HwClient, - types::{ - ClientId, GameCfg, GameCfg::*, RoomConfig, RoomId, TeamInfo, Voting, MAX_HEDGEHOGS_PER_TEAM, - }, + types::{ClientId, RoomId, Voting}, }; use bitflags::*; +use hedgewars_network_protocol::types::{ + GameCfg, GameCfg::*, RoomConfig, TeamInfo, MAX_HEDGEHOGS_PER_TEAM, +}; use serde::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize}; use serde_yaml; diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/core/server.rs --- a/rust/hedgewars-server/src/core/server.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/core/server.rs Wed Jun 23 23:41:51 2021 +0200 @@ -3,9 +3,10 @@ client::HwClient, indexslab::IndexSlab, room::HwRoom, - types::{ClientId, GameCfg, RoomId, ServerVar, TeamInfo, Vote, VoteType, Voting}, + types::{ClientId, RoomId, Voting}, }; use crate::utils; +use hedgewars_network_protocol::types::{GameCfg, ServerVar, TeamInfo, Vote, VoteType}; use bitflags::*; use log::*; diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/core/types.rs --- a/rust/hedgewars-server/src/core/types.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/core/types.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,158 +1,9 @@ +use hedgewars_network_protocol::types::{RoomConfig, TeamInfo, VoteType}; use serde_derive::{Deserialize, Serialize}; pub type ClientId = usize; pub type RoomId = usize; -pub const MAX_HEDGEHOGS_PER_TEAM: u8 = 8; - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum ServerVar { - MOTDNew(String), - MOTDOld(String), - LatestProto(u16), -} - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum GameCfg { - FeatureSize(u32), - MapType(String), - MapGenerator(u32), - MazeSize(u32), - Seed(String), - Template(u32), - - Ammo(String, Option), - Scheme(String, Vec), - Script(String), - Theme(String), - DrawnMap(String), -} - -#[derive(PartialEq, Eq, Clone, Debug, Default)] -pub struct TeamInfo { - pub owner: String, - pub name: String, - pub color: u8, - pub grave: String, - pub fort: String, - pub voice_pack: String, - pub flag: String, - pub difficulty: u8, - pub hedgehogs_number: u8, - pub hedgehogs: [HedgehogInfo; MAX_HEDGEHOGS_PER_TEAM as usize], -} - -#[derive(PartialEq, Eq, Clone, Debug, Default)] -pub struct HedgehogInfo { - pub name: String, - pub hat: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Ammo { - pub name: String, - pub settings: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Scheme { - pub name: String, - pub settings: Vec, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct RoomConfig { - pub feature_size: u32, - pub map_type: String, - pub map_generator: u32, - pub maze_size: u32, - pub seed: String, - pub template: u32, - - pub ammo: Ammo, - pub scheme: Scheme, - pub script: String, - pub theme: String, - pub drawn_map: Option, -} - -impl RoomConfig { - pub fn new() -> RoomConfig { - RoomConfig { - feature_size: 12, - map_type: "+rnd+".to_string(), - map_generator: 0, - maze_size: 0, - seed: "seed".to_string(), - template: 0, - - ammo: Ammo { - name: "Default".to_string(), - settings: None, - }, - scheme: Scheme { - name: "Default".to_string(), - settings: Vec::new(), - }, - script: "Normal".to_string(), - theme: "\u{1f994}".to_string(), - drawn_map: None, - } - } - - pub fn set_config(&mut self, cfg: GameCfg) { - match cfg { - GameCfg::FeatureSize(s) => self.feature_size = s, - GameCfg::MapType(t) => self.map_type = t, - GameCfg::MapGenerator(g) => self.map_generator = g, - GameCfg::MazeSize(s) => self.maze_size = s, - GameCfg::Seed(s) => self.seed = s, - GameCfg::Template(t) => self.template = t, - - GameCfg::Ammo(n, s) => { - self.ammo = Ammo { - name: n, - settings: s, - } - } - GameCfg::Scheme(n, s) => { - self.scheme = Scheme { - name: n, - settings: s, - } - } - GameCfg::Script(s) => self.script = s, - GameCfg::Theme(t) => self.theme = t, - GameCfg::DrawnMap(m) => self.drawn_map = Some(m), - }; - } - - pub fn to_map_config(&self) -> Vec { - vec![ - self.feature_size.to_string(), - self.map_type.to_string(), - self.map_generator.to_string(), - self.maze_size.to_string(), - self.seed.to_string(), - self.template.to_string(), - ] - } - - pub fn to_game_config(&self) -> Vec { - use GameCfg::*; - let mut v = vec![ - Ammo(self.ammo.name.to_string(), self.ammo.settings.clone()), - Scheme(self.scheme.name.to_string(), self.scheme.settings.clone()), - Script(self.script.to_string()), - Theme(self.theme.to_string()), - ]; - if let Some(ref m) = self.drawn_map { - v.push(DrawnMap(m.to_string())) - } - v - } -} - #[derive(Debug)] pub struct Replay { pub config: RoomConfig, @@ -160,20 +11,6 @@ pub message_log: Vec, } -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum VoteType { - Kick(String), - Map(Option), - Pause, - NewSeed, - HedgehogsPerTeam(u8), -} - -pub struct Vote { - pub is_pro: bool, - pub is_forced: bool, -} - #[derive(Clone, Debug)] pub struct Voting { pub ttl: u32, diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers.rs --- a/rust/hedgewars-server/src/handlers.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers.rs Wed Jun 23 23:41:51 2021 +0200 @@ -10,19 +10,24 @@ inanteroom::LoginResult, strings::*, }; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ anteroom::HwAnteroom, room::RoomSave, server::HwServer, - types::{ClientId, GameCfg, Replay, RoomId, TeamInfo}, + types::{ClientId, Replay, RoomId}, }, - protocol::messages::{ + utils, +}; +use hedgewars_network_protocol::{ + messages::{ global_chat, server_chat, HwProtocolMessage, HwProtocolMessage::EngineMessage, HwServerMessage, HwServerMessage::*, }, - utils, + types::{GameCfg, TeamInfo}, }; + use base64::encode; use log::*; use rand::{thread_rng, RngCore}; diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers/actions.rs --- a/rust/hedgewars-server/src/handlers/actions.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers/actions.rs Wed Jun 23 23:41:51 2021 +0200 @@ -4,11 +4,14 @@ room::HwRoom, room::{GameInfo, RoomFlags}, server::HwServer, - types::{ClientId, GameCfg, RoomId, VoteType}, + types::{ClientId, RoomId}, }, - protocol::messages::{server_chat, HwProtocolMessage, HwServerMessage, HwServerMessage::*}, utils::to_engine_msg, }; +use hedgewars_network_protocol::{ + messages::{server_chat, HwProtocolMessage, HwServerMessage, HwServerMessage::*}, + types::{GameCfg, VoteType}, +}; use rand::{distributions::Uniform, thread_rng, Rng}; use std::{io, io::Write, iter::once, mem::replace}; @@ -101,20 +104,28 @@ } } -impl HwServerMessage { - pub fn send(self, client_id: ClientId) -> PendingMessage { +pub trait ToPendingMessage { + fn send(self, client_id: ClientId) -> PendingMessage; + fn send_many(self, client_ids: Vec) -> PendingMessage; + fn send_self(self) -> PendingMessage; + fn send_all(self) -> PendingMessage; + fn send_to_destination(self, destination: Destination) -> PendingMessage; +} + +impl ToPendingMessage for HwServerMessage { + fn send(self, client_id: ClientId) -> PendingMessage { PendingMessage::send(self, client_id) } - pub fn send_many(self, client_ids: Vec) -> PendingMessage { + fn send_many(self, client_ids: Vec) -> PendingMessage { PendingMessage::send_many(self, client_ids) } - pub fn send_self(self) -> PendingMessage { + fn send_self(self) -> PendingMessage { PendingMessage::send_self(self) } - pub fn send_all(self) -> PendingMessage { + fn send_all(self) -> PendingMessage { PendingMessage::send_all(self) } - pub fn send_to_destination(self, destination: Destination) -> PendingMessage { + fn send_to_destination(self, destination: Destination) -> PendingMessage { PendingMessage { destination, message: self, diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers/checker.rs --- a/rust/hedgewars-server/src/handlers/checker.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers/checker.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,10 +1,8 @@ use log::*; use mio; -use crate::{ - core::{server::HwServer, types::ClientId}, - protocol::messages::HwProtocolMessage, -}; +use crate::core::{server::HwServer, types::ClientId}; +use hedgewars_network_protocol::messages::HwProtocolMessage; pub fn handle(_server: &mut HwServer, _client_id: ClientId, message: HwProtocolMessage) { match message { diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers/common.rs --- a/rust/hedgewars-server/src/handlers/common.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers/common.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,3 +1,8 @@ +use super::{ + actions::{Destination, DestinationGroup}, + Response, +}; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ client::HwClient, @@ -6,23 +11,19 @@ EndGameResult, HwRoomControl, HwServer, JoinRoomError, LeaveRoomResult, StartGameError, VoteError, VoteResult, }, - types::{ClientId, GameCfg, RoomId, TeamInfo, Vote, VoteType, MAX_HEDGEHOGS_PER_TEAM}, + types::{ClientId, RoomId}, }, - protocol::messages::{ + utils::to_engine_msg, +}; +use hedgewars_network_protocol::{ + messages::{ add_flags, remove_flags, server_chat, HwProtocolMessage::{self, Rnd}, HwServerMessage::{self, *}, ProtocolFlags as Flags, }, - utils::to_engine_msg, + types::{GameCfg, RoomConfig, TeamInfo, Vote, VoteType, MAX_HEDGEHOGS_PER_TEAM}, }; - -use super::{ - actions::{Destination, DestinationGroup}, - Response, -}; - -use crate::core::types::RoomConfig; use rand::{self, seq::SliceRandom, thread_rng, Rng}; use std::{iter::once, mem::replace}; @@ -644,7 +645,7 @@ mod tests { use super::*; use crate::handlers::actions::PendingMessage; - use crate::protocol::messages::HwServerMessage::ChatMsg; + use hedgewars_network_protocol::messages::HwServerMessage::ChatMsg; fn reply2string(r: HwServerMessage) -> String { match r { diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers/inanteroom.rs --- a/rust/hedgewars-server/src/handlers/inanteroom.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers/inanteroom.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,6 +1,7 @@ use mio; use super::strings::*; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ anteroom::{HwAnteroom, HwAnteroomClient}, @@ -8,10 +9,11 @@ server::HwServer, types::ClientId, }, - protocol::messages::{HwProtocolMessage, HwProtocolMessage::LoadRoom, HwServerMessage::*}, utils::is_name_illegal, }; - +use hedgewars_network_protocol::messages::{ + HwProtocolMessage, HwProtocolMessage::LoadRoom, HwServerMessage::*, +}; use log::*; #[cfg(feature = "official-server")] use openssl::sha::sha1; diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers/inlobby.rs --- a/rust/hedgewars-server/src/handlers/inlobby.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers/inlobby.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,15 +1,19 @@ use super::{common::rnd_reply, strings::*}; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ client::HwClient, server::{AccessError, CreateRoomError, HwServer, JoinRoomError}, - types::{ClientId, ServerVar}, + types::ClientId, }, - protocol::messages::{ + utils::is_name_illegal, +}; +use hedgewars_network_protocol::{ + messages::{ add_flags, remove_flags, server_chat, HwProtocolMessage, HwServerMessage::*, ProtocolFlags as Flags, }, - utils::is_name_illegal, + types::ServerVar, }; use log::*; use std::{collections::HashSet, convert::identity}; @@ -20,7 +24,7 @@ response: &mut super::Response, message: HwProtocolMessage, ) { - use crate::protocol::messages::HwProtocolMessage::*; + use hedgewars_network_protocol::messages::HwProtocolMessage::*; match message { CreateRoom(name, password) => match server.create_room(client_id, name, password) { diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/handlers/inroom.rs --- a/rust/hedgewars-server/src/handlers/inroom.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/handlers/inroom.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,6 +1,7 @@ use super::{common::rnd_reply, strings::*}; use crate::core::room::GameInfo; use crate::core::server::{AddTeamError, SetTeamCountError}; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ room::{HwRoom, RoomFlags, MAX_TEAMS_IN_ROOM}, @@ -8,16 +9,19 @@ ChangeMasterError, ChangeMasterResult, HwRoomControl, HwServer, LeaveRoomResult, ModifyTeamError, StartGameError, }, - types, - types::{ClientId, GameCfg, RoomId, VoteType, Voting, MAX_HEDGEHOGS_PER_TEAM}, - }, - protocol::messages::{ - add_flags, remove_flags, server_chat, HwProtocolMessage, HwServerMessage::*, - ProtocolFlags as Flags, + types::{ClientId, RoomId, Voting}, }, utils::{is_name_illegal, to_engine_msg}, }; use base64::{decode, encode}; +use hedgewars_network_protocol::{ + messages::{ + add_flags, remove_flags, server_chat, HwProtocolMessage, HwServerMessage::*, + ProtocolFlags as Flags, + }, + types, + types::{GameCfg, VoteType, MAX_HEDGEHOGS_PER_TEAM}, +}; use log::*; use std::{cmp::min, iter::once, mem::swap}; @@ -87,7 +91,7 @@ } fn room_message_flag(msg: &HwProtocolMessage) -> RoomFlags { - use crate::protocol::messages::HwProtocolMessage::*; + use hedgewars_network_protocol::messages::HwProtocolMessage::*; match msg { ToggleRestrictJoin => RoomFlags::RESTRICTED_JOIN, ToggleRestrictTeams => RoomFlags::RESTRICTED_TEAM_ADD, @@ -104,7 +108,7 @@ let (client, room) = room_control.get(); let (client_id, room_id) = (client.id, room.id); - use crate::protocol::messages::HwProtocolMessage::*; + use hedgewars_network_protocol::messages::HwProtocolMessage::*; match message { Part(msg) => { let msg = match msg { diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/protocol.rs --- a/rust/hedgewars-server/src/protocol.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/protocol.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,13 +1,11 @@ -use self::parser::message; +use hedgewars_network_protocol::{ + messages::HwProtocolMessage, + parser::{malformed_message, message}, +}; use log::*; use netbuf; use std::io::{Read, Result}; -pub mod messages; -mod parser; -#[cfg(test)] -pub mod test; - pub struct ProtocolDecoder { buf: netbuf::Buf, is_recovering: bool, @@ -22,7 +20,7 @@ } fn recover(&mut self) -> bool { - self.is_recovering = match parser::malformed_message(&self.buf[..]) { + self.is_recovering = match malformed_message(&self.buf[..]) { Ok((tail, ())) => { let length = tail.len(); self.buf.consume(self.buf.len() - length); @@ -44,11 +42,11 @@ Ok(count) } - pub fn extract_messages(&mut self) -> Vec { + pub fn extract_messages(&mut self) -> Vec { let mut messages = vec![]; if !self.is_recovering { while !self.buf.is_empty() { - match parser::message(&self.buf[..]) { + match message(&self.buf[..]) { Ok((tail, message)) => { messages.push(message); let length = tail.len(); diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/protocol/messages.rs --- a/rust/hedgewars-server/src/protocol/messages.rs Wed Jun 23 15:32:48 2021 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,425 +0,0 @@ -use crate::core::types::{GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType}; -use std::{convert::From, iter::once, ops}; - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum HwProtocolMessage { - // common messages - Ping, - Pong, - Quit(Option), - Global(String), - Watch(u32), - ToggleServerRegisteredOnly, - SuperPower, - Info(String), - // anteroom messages - Nick(String), - Proto(u16), - Password(String, String), - Checker(u16, String, String), - // lobby messages - List, - Chat(String), - CreateRoom(String, Option), - JoinRoom(String, Option), - Follow(String), - Rnd(Vec), - Kick(String), - Ban(String, String, u32), - BanIp(String, String, u32), - BanNick(String, String, u32), - BanList, - Unban(String), - SetServerVar(ServerVar), - GetServerVar, - RestartServer, - Stats, - // room messages - Part(Option), - Cfg(GameCfg), - AddTeam(Box), - RemoveTeam(String), - SetHedgehogsNumber(String, u8), - SetTeamColor(String, u8), - ToggleReady, - StartGame, - EngineMessage(String), - RoundFinished, - ToggleRestrictJoin, - ToggleRestrictTeams, - ToggleRegisteredOnly, - RoomName(String), - Delegate(String), - TeamChat(String), - MaxTeams(u8), - Fix, - Unfix, - Greeting(Option), - CallVote(Option), - Vote(bool), - ForceVote(bool), - Save(String, String), - Delete(String), - SaveRoom(String), - LoadRoom(String), -} - -#[derive(Debug, Clone, Copy)] -pub enum ProtocolFlags { - InRoom, - RoomMaster, - Ready, - InGame, - Registered, - Admin, - Contributor, -} - -impl ProtocolFlags { - #[inline] - fn flag_char(&self) -> char { - match self { - ProtocolFlags::InRoom => 'i', - ProtocolFlags::RoomMaster => 'h', - ProtocolFlags::Ready => 'r', - ProtocolFlags::InGame => 'g', - ProtocolFlags::Registered => 'u', - ProtocolFlags::Admin => 'a', - ProtocolFlags::Contributor => 'c', - } - } - - #[inline] - fn format(prefix: char, flags: &[ProtocolFlags]) -> String { - once(prefix) - .chain(flags.iter().map(|f| f.flag_char())) - .collect() - } -} - -#[inline] -pub fn add_flags(flags: &[ProtocolFlags]) -> String { - ProtocolFlags::format('+', flags) -} - -#[inline] -pub fn remove_flags(flags: &[ProtocolFlags]) -> String { - ProtocolFlags::format('-', flags) -} - -#[derive(Debug)] -pub enum HwServerMessage { - Connected(u32), - Redirect(u16), - - Ping, - Pong, - Bye(String), - - Nick(String), - Proto(u16), - AskPassword(String), - ServerAuth(String), - LogonPassed, - - LobbyLeft(String, String), - LobbyJoined(Vec), - ChatMsg { nick: String, msg: String }, - ClientFlags(String, Vec), - Rooms(Vec), - RoomAdd(Vec), - RoomJoined(Vec), - RoomLeft(String, String), - RoomRemove(String), - RoomUpdated(String, Vec), - Joining(String), - TeamAdd(Vec), - TeamRemove(String), - TeamAccepted(String), - TeamColor(String, u8), - HedgehogsNumber(String, u8), - ConfigEntry(String, Vec), - Kicked, - RunGame, - ForwardEngineMessage(Vec), - RoundFinished, - ReplayStart, - - Info(Vec), - ServerMessage(String), - ServerVars(Vec), - Notice(String), - Warning(String), - Error(String), - Unreachable, - - //Deprecated messages - LegacyReady(bool, Vec), -} - -fn special_chat(nick: &str, msg: String) -> HwServerMessage { - HwServerMessage::ChatMsg { - nick: nick.to_string(), - msg, - } -} - -pub fn server_chat(msg: String) -> HwServerMessage { - special_chat("[server]", msg) -} - -pub fn global_chat(msg: String) -> HwServerMessage { - special_chat("(global notice)", msg) -} - -impl ServerVar { - pub fn to_protocol(&self) -> Vec { - use ServerVar::*; - match self { - MOTDNew(s) => vec!["MOTD_NEW".to_string(), s.clone()], - MOTDOld(s) => vec!["MOTD_OLD".to_string(), s.clone()], - LatestProto(n) => vec!["LATEST_PROTO".to_string(), n.to_string()], - } - } -} - -impl VoteType { - pub fn to_protocol(&self) -> Vec { - use VoteType::*; - match self { - Kick(nick) => vec!["KICK".to_string(), nick.clone()], - Map(None) => vec!["MAP".to_string()], - Map(Some(name)) => vec!["MAP".to_string(), name.clone()], - Pause => vec!["PAUSE".to_string()], - NewSeed => vec!["NEWSEED".to_string()], - HedgehogsPerTeam(count) => vec!["HEDGEHOGS".to_string(), count.to_string()], - } - } -} - -impl GameCfg { - pub fn to_protocol(&self) -> (String, Vec) { - use GameCfg::*; - match self { - FeatureSize(s) => ("FEATURE_SIZE".to_string(), vec![s.to_string()]), - MapType(t) => ("MAP".to_string(), vec![t.to_string()]), - MapGenerator(g) => ("MAPGEN".to_string(), vec![g.to_string()]), - MazeSize(s) => ("MAZE_SIZE".to_string(), vec![s.to_string()]), - Seed(s) => ("SEED".to_string(), vec![s.to_string()]), - Template(t) => ("TEMPLATE".to_string(), vec![t.to_string()]), - - Ammo(n, None) => ("AMMO".to_string(), vec![n.to_string()]), - Ammo(n, Some(s)) => ("AMMO".to_string(), vec![n.to_string(), s.to_string()]), - Scheme(n, s) if s.is_empty() => ("SCHEME".to_string(), vec![n.to_string()]), - Scheme(n, s) => ("SCHEME".to_string(), { - let mut v = vec![n.to_string()]; - v.extend(s.clone()); - v - }), - Script(s) => ("SCRIPT".to_string(), vec![s.to_string()]), - Theme(t) => ("THEME".to_string(), vec![t.to_string()]), - DrawnMap(m) => ("DRAWNMAP".to_string(), vec![m.to_string()]), - } - } - - pub fn to_server_msg(&self) -> HwServerMessage { - use self::HwServerMessage::ConfigEntry; - let (name, args) = self.to_protocol(); - HwServerMessage::ConfigEntry(name, args) - } -} - -impl TeamInfo { - pub fn to_protocol(&self) -> Vec { - let mut info = vec![ - self.name.clone(), - self.grave.clone(), - self.fort.clone(), - self.voice_pack.clone(), - self.flag.clone(), - self.owner.clone(), - self.difficulty.to_string(), - ]; - let hogs = self - .hedgehogs - .iter() - .flat_map(|h| once(h.name.clone()).chain(once(h.hat.clone()))); - info.extend(hogs); - info - } -} - -macro_rules! const_braces { - ($e: expr) => { - "{}\n" - }; -} - -macro_rules! msg { - [$($part: expr),*] => { - format!(concat!($(const_braces!($part)),*, "\n"), $($part),*); - }; -} - -impl HwProtocolMessage { - /** Converts the message to a raw `String`, which can be sent over the network. - * - * This is the inverse of the `message` parser. - */ - #[cfg(test)] - pub(crate) fn to_raw_protocol(&self) -> String { - use self::HwProtocolMessage::*; - match self { - Ping => msg!["PING"], - Pong => msg!["PONG"], - Quit(None) => msg!["QUIT"], - Quit(Some(msg)) => msg!["QUIT", msg], - Global(msg) => msg!["CMD", format!("GLOBAL {}", msg)], - Watch(name) => msg!["CMD", format!("WATCH {}", name)], - ToggleServerRegisteredOnly => msg!["CMD", "REGISTERED_ONLY"], - SuperPower => msg!["CMD", "SUPER_POWER"], - Info(info) => msg!["CMD", format!("INFO {}", info)], - Nick(nick) => msg!("NICK", nick), - Proto(version) => msg!["PROTO", version], - Password(p, s) => msg!["PASSWORD", p, s], - Checker(i, n, p) => msg!["CHECKER", i, n, p], - List => msg!["LIST"], - Chat(msg) => msg!["CHAT", msg], - CreateRoom(name, None) => msg!["CREATE_ROOM", name], - CreateRoom(name, Some(password)) => msg!["CREATE_ROOM", name, password], - JoinRoom(name, None) => msg!["JOIN_ROOM", name], - JoinRoom(name, Some(password)) => msg!["JOIN_ROOM", name, password], - Follow(name) => msg!["FOLLOW", name], - Rnd(args) => { - if args.is_empty() { - msg!["CMD", "RND"] - } else { - msg!["CMD", format!("RND {}", args.join(" "))] - } - } - Kick(name) => msg!["KICK", name], - Ban(name, reason, time) => msg!["BAN", name, reason, time], - BanIp(ip, reason, time) => msg!["BAN_IP", ip, reason, time], - BanNick(nick, reason, time) => msg!("BAN_NICK", nick, reason, time), - BanList => msg!["BANLIST"], - Unban(name) => msg!["UNBAN", name], - SetServerVar(var) => construct_message(&["SET_SERVER_VAR"], &var.to_protocol()), - GetServerVar => msg!["GET_SERVER_VAR"], - RestartServer => msg!["CMD", "RESTART_SERVER YES"], - Stats => msg!["CMD", "STATS"], - Part(None) => msg!["PART"], - Part(Some(msg)) => msg!["PART", msg], - Cfg(config) => { - let (name, args) = config.to_protocol(); - msg!["CFG", name, args.join("\n")] - } - AddTeam(info) => msg![ - "ADD_TEAM", - info.name, - info.color, - info.grave, - info.fort, - info.voice_pack, - info.flag, - info.difficulty, - info.hedgehogs - .iter() - .flat_map(|h| [&h.name[..], &h.hat[..]]) - .collect::>() - .join("\n") - ], - RemoveTeam(name) => msg!["REMOVE_TEAM", name], - SetHedgehogsNumber(team, number) => msg!["HH_NUM", team, number], - SetTeamColor(team, color) => msg!["TEAM_COLOR", team, color], - ToggleReady => msg!["TOGGLE_READY"], - StartGame => msg!["START_GAME"], - EngineMessage(msg) => msg!["EM", msg], - RoundFinished => msg!["ROUNDFINISHED"], - ToggleRestrictJoin => msg!["TOGGLE_RESTRICT_JOINS"], - ToggleRestrictTeams => msg!["TOGGLE_RESTRICT_TEAMS"], - ToggleRegisteredOnly => msg!["TOGGLE_REGISTERED_ONLY"], - RoomName(name) => msg!["ROOM_NAME", name], - Delegate(name) => msg!["CMD", format!("DELEGATE {}", name)], - TeamChat(msg) => msg!["TEAMCHAT", msg], - MaxTeams(count) => msg!["CMD", format!("MAXTEAMS {}", count)], - Fix => msg!["CMD", "FIX"], - Unfix => msg!["CMD", "UNFIX"], - Greeting(None) => msg!["CMD", "GREETING"], - Greeting(Some(msg)) => msg!["CMD", format!("GREETING {}", msg)], - CallVote(None) => msg!["CMD", "CALLVOTE"], - CallVote(Some(vote)) => { - msg!["CMD", format!("CALLVOTE {}", &vote.to_protocol().join(" "))] - } - Vote(msg) => msg!["CMD", format!("VOTE {}", if *msg { "YES" } else { "NO" })], - ForceVote(msg) => msg!["CMD", format!("FORCE {}", if *msg { "YES" } else { "NO" })], - Save(name, location) => msg!["CMD", format!("SAVE {} {}", name, location)], - Delete(name) => msg!["CMD", format!("DELETE {}", name)], - SaveRoom(name) => msg!["CMD", format!("SAVEROOM {}", name)], - LoadRoom(name) => msg!["CMD", format!("LOADROOM {}", name)], - _ => panic!("Protocol message not yet implemented"), - } - } -} - -fn construct_message(header: &[&str], msg: &[String]) -> String { - let mut v: Vec<_> = header.iter().cloned().collect(); - v.extend(msg.iter().map(|s| &s[..])); - v.push("\n"); - v.join("\n") -} - -impl HwServerMessage { - pub fn to_raw_protocol(&self) -> String { - use self::HwServerMessage::*; - match self { - Ping => msg!["PING"], - Pong => msg!["PONG"], - Connected(protocol_version) => msg![ - "CONNECTED", - "Hedgewars server https://www.hedgewars.org/", - protocol_version - ], - Redirect(port) => msg!["REDIRECT", port], - Bye(msg) => msg!["BYE", msg], - Nick(nick) => msg!["NICK", nick], - Proto(proto) => msg!["PROTO", proto], - AskPassword(salt) => msg!["ASKPASSWORD", salt], - ServerAuth(hash) => msg!["SERVER_AUTH", hash], - LogonPassed => msg!["LOGONPASSED"], - LobbyLeft(nick, msg) => msg!["LOBBY:LEFT", nick, msg], - LobbyJoined(nicks) => construct_message(&["LOBBY:JOINED"], &nicks), - ClientFlags(flags, nicks) => construct_message(&["CLIENT_FLAGS", flags], &nicks), - Rooms(info) => construct_message(&["ROOMS"], &info), - RoomAdd(info) => construct_message(&["ROOM", "ADD"], &info), - RoomJoined(nicks) => construct_message(&["JOINED"], &nicks), - RoomLeft(nick, msg) => msg!["LEFT", nick, msg], - RoomRemove(name) => msg!["ROOM", "DEL", name], - RoomUpdated(name, info) => construct_message(&["ROOM", "UPD", name], &info), - Joining(name) => msg!["JOINING", name], - TeamAdd(info) => construct_message(&["ADD_TEAM"], &info), - TeamRemove(name) => msg!["REMOVE_TEAM", name], - TeamAccepted(name) => msg!["TEAM_ACCEPTED", name], - TeamColor(name, color) => msg!["TEAM_COLOR", name, color], - HedgehogsNumber(name, number) => msg!["HH_NUM", name, number], - ConfigEntry(name, values) => construct_message(&["CFG", name], &values), - Kicked => msg!["KICKED"], - RunGame => msg!["RUN_GAME"], - ForwardEngineMessage(em) => construct_message(&["EM"], &em), - RoundFinished => msg!["ROUND_FINISHED"], - ChatMsg { nick, msg } => msg!["CHAT", nick, msg], - Info(info) => construct_message(&["INFO"], &info), - ServerMessage(msg) => msg!["SERVER_MESSAGE", msg], - ServerVars(vars) => construct_message(&["SERVER_VARS"], &vars), - Notice(msg) => msg!["NOTICE", msg], - Warning(msg) => msg!["WARNING", msg], - Error(msg) => msg!["ERROR", msg], - ReplayStart => msg!["REPLAY_START"], - - LegacyReady(is_ready, nicks) => { - construct_message(&[if *is_ready { "READY" } else { "NOT_READY" }], &nicks) - } - - _ => msg!["ERROR", "UNIMPLEMENTED"], - } - } -} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/protocol/parser.rs --- a/rust/hedgewars-server/src/protocol/parser.rs Wed Jun 23 15:32:48 2021 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,576 +0,0 @@ -/** The parsers for the chat and multiplayer protocol. The main parser is `message`. - * # Protocol - * All messages consist of `\n`-separated strings. The end of a message is - * indicated by a double newline - `\n\n`. - * - * For example, a nullary command like PING will be actually sent as `PING\n\n`. - * A unary command, such as `START_GAME nick` will be actually sent as `START_GAME\nnick\n\n`. - */ -use nom::{ - branch::alt, - bytes::complete::{tag, tag_no_case, take_until, take_while}, - character::complete::{newline, not_line_ending}, - combinator::{map, peek}, - error::{ErrorKind, ParseError}, - multi::separated_list0, - sequence::{delimited, pair, preceded, terminated, tuple}, - Err, IResult, -}; - -use std::{ - num::ParseIntError, - ops::Range, - str, - str::{FromStr, Utf8Error}, -}; - -use super::messages::{HwProtocolMessage, HwProtocolMessage::*}; -use crate::core::types::{ - GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType, MAX_HEDGEHOGS_PER_TEAM, -}; - -#[derive(Debug, PartialEq)] -pub struct HwProtocolError {} - -impl HwProtocolError { - fn new() -> Self { - HwProtocolError {} - } -} - -impl ParseError for HwProtocolError { - fn from_error_kind(input: I, kind: ErrorKind) -> Self { - HwProtocolError::new() - } - - fn append(input: I, kind: ErrorKind, other: Self) -> Self { - HwProtocolError::new() - } -} - -impl From for HwProtocolError { - fn from(_: Utf8Error) -> Self { - HwProtocolError::new() - } -} - -impl From for HwProtocolError { - fn from(_: ParseIntError) -> Self { - HwProtocolError::new() - } -} - -pub type HwResult<'a, O> = IResult<&'a [u8], O, HwProtocolError>; - -fn end_of_message(input: &[u8]) -> HwResult<&[u8]> { - tag("\n\n")(input) -} - -fn convert_utf8(input: &[u8]) -> HwResult<&str> { - match str::from_utf8(input) { - Ok(str) => Ok((b"", str)), - Err(utf_err) => Result::Err(Err::Failure(utf_err.into())), - } -} - -fn convert_from_str(str: &str) -> HwResult -where - T: FromStr, -{ - match T::from_str(str) { - Ok(x) => Ok((b"", x)), - Err(format_err) => Result::Err(Err::Failure(format_err.into())), - } -} - -fn str_line(input: &[u8]) -> HwResult<&str> { - let (i, text) = not_line_ending(input.clone())?; - if i != input { - Ok((i, convert_utf8(text)?.1)) - } else { - Err(Err::Error(HwProtocolError::new())) - } -} - -fn a_line(input: &[u8]) -> HwResult { - map(str_line, String::from)(input) -} - -fn cmd_arg(input: &[u8]) -> HwResult { - let delimiters = b" \n"; - let (i, str) = take_while(move |c| !delimiters.contains(&c))(input.clone())?; - if i != input { - Ok((i, convert_utf8(str)?.1.to_string())) - } else { - Err(Err::Error(HwProtocolError::new())) - } -} - -fn u8_line(input: &[u8]) -> HwResult { - let (i, str) = str_line(input)?; - Ok((i, convert_from_str(str)?.1)) -} - -fn u16_line(input: &[u8]) -> HwResult { - let (i, str) = str_line(input)?; - Ok((i, convert_from_str(str)?.1)) -} - -fn u32_line(input: &[u8]) -> HwResult { - let (i, str) = str_line(input)?; - Ok((i, convert_from_str(str)?.1)) -} - -fn yes_no_line(input: &[u8]) -> HwResult { - alt(( - map(tag_no_case(b"YES"), |_| true), - map(tag_no_case(b"NO"), |_| false), - ))(input) -} - -fn opt_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option> { - alt(( - map(peek(end_of_message), |_| None), - map(preceded(tag("\n"), a_line), Some), - ))(input) -} - -fn spaces(input: &[u8]) -> HwResult<&[u8]> { - preceded(tag(" "), take_while(|c| c == b' '))(input) -} - -fn opt_space_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option> { - alt(( - map(peek(end_of_message), |_| None), - map(preceded(spaces, a_line), Some), - ))(input) -} - -fn hedgehog_array(input: &[u8]) -> HwResult<[HedgehogInfo; 8]> { - fn hedgehog_line(input: &[u8]) -> HwResult { - map( - tuple((terminated(a_line, newline), a_line)), - |(name, hat)| HedgehogInfo { name, hat }, - )(input) - } - - let (i, (h1, h2, h3, h4, h5, h6, h7, h8)) = tuple(( - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - hedgehog_line, - ))(input)?; - - Ok((i, [h1, h2, h3, h4, h5, h6, h7, h8])) -} - -fn voting(input: &[u8]) -> HwResult { - alt(( - map(tag_no_case("PAUSE"), |_| VoteType::Pause), - map(tag_no_case("NEWSEED"), |_| VoteType::NewSeed), - map( - preceded(pair(tag_no_case("KICK"), spaces), a_line), - VoteType::Kick, - ), - map( - preceded(pair(tag_no_case("HEDGEHOGS"), spaces), u8_line), - VoteType::HedgehogsPerTeam, - ), - map(preceded(tag_no_case("MAP"), opt_space_arg), VoteType::Map), - ))(input) -} - -fn no_arg_message(input: &[u8]) -> HwResult { - fn message<'a>( - name: &'a str, - msg: HwProtocolMessage, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { - move |i| map(tag(name), |_| msg.clone())(i) - } - - alt(( - message("PING", Ping), - message("PONG", Pong), - message("LIST", List), - message("BANLIST", BanList), - message("GET_SERVER_VAR", GetServerVar), - message("TOGGLE_READY", ToggleReady), - message("START_GAME", StartGame), - message("TOGGLE_RESTRICT_JOINS", ToggleRestrictJoin), - message("TOGGLE_RESTRICT_TEAMS", ToggleRestrictTeams), - message("TOGGLE_REGISTERED_ONLY", ToggleRegisteredOnly), - ))(input) -} - -fn single_arg_message(input: &[u8]) -> HwResult { - fn message<'a, T, F, G>( - name: &'a str, - parser: F, - constructor: G, - ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> - where - F: Fn(&[u8]) -> HwResult, - G: Fn(T) -> HwProtocolMessage, - { - map(preceded(tag(name), parser), constructor) - } - - alt(( - message("NICK\n", a_line, Nick), - message("INFO\n", a_line, Info), - message("CHAT\n", a_line, Chat), - message("PART", opt_arg, Part), - message("FOLLOW\n", a_line, Follow), - message("KICK\n", a_line, Kick), - message("UNBAN\n", a_line, Unban), - message("EM\n", a_line, EngineMessage), - message("TEAMCHAT\n", a_line, TeamChat), - message("ROOM_NAME\n", a_line, RoomName), - message("REMOVE_TEAM\n", a_line, RemoveTeam), - message("ROUNDFINISHED", opt_arg, |_| RoundFinished), - message("PROTO\n", u16_line, Proto), - message("QUIT", opt_arg, Quit), - ))(input) -} - -fn cmd_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { - fn cmd_no_arg<'a>( - name: &'a str, - msg: HwProtocolMessage, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { - move |i| map(tag_no_case(name), |_| msg.clone())(i) - } - - fn cmd_single_arg<'a, T, F, G>( - name: &'a str, - parser: F, - constructor: G, - ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> - where - F: Fn(&'a [u8]) -> HwResult<'a, T>, - G: Fn(T) -> HwProtocolMessage, - { - map( - preceded(pair(tag_no_case(name), spaces), parser), - constructor, - ) - } - - fn cmd_no_arg_message(input: &[u8]) -> HwResult { - alt(( - cmd_no_arg("STATS", Stats), - cmd_no_arg("FIX", Fix), - cmd_no_arg("UNFIX", Unfix), - cmd_no_arg("REGISTERED_ONLY", ToggleServerRegisteredOnly), - cmd_no_arg("SUPER_POWER", SuperPower), - ))(input) - } - - fn cmd_single_arg_message(input: &[u8]) -> HwResult { - alt(( - cmd_single_arg("RESTART_SERVER", |i| tag("YES")(i), |_| RestartServer), - cmd_single_arg("DELEGATE", a_line, Delegate), - cmd_single_arg("DELETE", a_line, Delete), - cmd_single_arg("SAVEROOM", a_line, SaveRoom), - cmd_single_arg("LOADROOM", a_line, LoadRoom), - cmd_single_arg("GLOBAL", a_line, Global), - cmd_single_arg("WATCH", u32_line, Watch), - cmd_single_arg("VOTE", yes_no_line, Vote), - cmd_single_arg("FORCE", yes_no_line, ForceVote), - cmd_single_arg("INFO", a_line, Info), - cmd_single_arg("MAXTEAMS", u8_line, MaxTeams), - cmd_single_arg("CALLVOTE", voting, |v| CallVote(Some(v))), - ))(input) - } - - preceded( - tag("CMD\n"), - alt(( - cmd_no_arg_message, - cmd_single_arg_message, - map(tag_no_case("CALLVOTE"), |_| CallVote(None)), - map(preceded(tag_no_case("GREETING"), opt_space_arg), Greeting), - map(preceded(tag_no_case("PART"), opt_space_arg), Part), - map(preceded(tag_no_case("QUIT"), opt_space_arg), Quit), - map( - preceded( - tag_no_case("SAVE"), - pair(preceded(spaces, cmd_arg), preceded(spaces, cmd_arg)), - ), - |(n, l)| Save(n, l), - ), - map( - preceded( - tag_no_case("RND"), - alt(( - map(peek(end_of_message), |_| vec![]), - preceded(spaces, separated_list0(spaces, cmd_arg)), - )), - ), - Rnd, - ), - )), - )(input) -} - -fn config_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { - fn cfg_single_arg<'a, T, F, G>( - name: &'a str, - parser: F, - constructor: G, - ) -> impl FnMut(&'a [u8]) -> HwResult<'a, GameCfg> - where - F: Fn(&[u8]) -> HwResult, - G: Fn(T) -> GameCfg, - { - map(preceded(pair(tag(name), newline), parser), constructor) - } - - let (i, cfg) = preceded( - tag("CFG\n"), - alt(( - cfg_single_arg("THEME", a_line, GameCfg::Theme), - cfg_single_arg("SCRIPT", a_line, GameCfg::Script), - cfg_single_arg("MAP", a_line, GameCfg::MapType), - cfg_single_arg("MAPGEN", u32_line, GameCfg::MapGenerator), - cfg_single_arg("MAZE_SIZE", u32_line, GameCfg::MazeSize), - cfg_single_arg("TEMPLATE", u32_line, GameCfg::Template), - cfg_single_arg("FEATURE_SIZE", u32_line, GameCfg::FeatureSize), - cfg_single_arg("SEED", a_line, GameCfg::Seed), - cfg_single_arg("DRAWNMAP", a_line, GameCfg::DrawnMap), - preceded(pair(tag("AMMO"), newline), |i| { - let (i, name) = a_line(i)?; - let (i, value) = opt_arg(i)?; - Ok((i, GameCfg::Ammo(name, value))) - }), - preceded( - pair(tag("SCHEME"), newline), - map( - pair( - a_line, - alt(( - map(peek(end_of_message), |_| None), - map(preceded(newline, separated_list0(newline, a_line)), Some), - )), - ), - |(name, values)| GameCfg::Scheme(name, values.unwrap_or_default()), - ), - ), - )), - )(input)?; - Ok((i, Cfg(cfg))) -} - -fn server_var_message(input: &[u8]) -> HwResult { - map( - preceded( - tag("SET_SERVER_VAR\n"), - alt(( - map(preceded(tag("MOTD_NEW\n"), a_line), ServerVar::MOTDNew), - map(preceded(tag("MOTD_OLD\n"), a_line), ServerVar::MOTDOld), - map( - preceded(tag("LATEST_PROTO\n"), u16_line), - ServerVar::LatestProto, - ), - )), - ), - SetServerVar, - )(input) -} - -fn complex_message(input: &[u8]) -> HwResult { - alt(( - preceded( - pair(tag("PASSWORD"), newline), - map(pair(terminated(a_line, newline), a_line), |(pass, salt)| { - Password(pass, salt) - }), - ), - preceded( - pair(tag("CHECKER"), newline), - map( - tuple(( - terminated(u16_line, newline), - terminated(a_line, newline), - a_line, - )), - |(protocol, name, pass)| Checker(protocol, name, pass), - ), - ), - preceded( - pair(tag("CREATE_ROOM"), newline), - map(pair(a_line, opt_arg), |(name, pass)| CreateRoom(name, pass)), - ), - preceded( - pair(tag("JOIN_ROOM"), newline), - map(pair(a_line, opt_arg), |(name, pass)| JoinRoom(name, pass)), - ), - preceded( - pair(tag("ADD_TEAM"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(u8_line, newline), - terminated(a_line, newline), - terminated(a_line, newline), - terminated(a_line, newline), - terminated(a_line, newline), - terminated(u8_line, newline), - hedgehog_array, - )), - |(name, color, grave, fort, voice_pack, flag, difficulty, hedgehogs)| { - AddTeam(Box::new(TeamInfo { - owner: String::new(), - name, - color, - grave, - fort, - voice_pack, - flag, - difficulty, - hedgehogs, - hedgehogs_number: 0, - })) - }, - ), - ), - preceded( - pair(tag("HH_NUM"), newline), - map( - pair(terminated(a_line, newline), u8_line), - |(name, count)| SetHedgehogsNumber(name, count), - ), - ), - preceded( - pair(tag("TEAM_COLOR"), newline), - map( - pair(terminated(a_line, newline), u8_line), - |(name, color)| SetTeamColor(name, color), - ), - ), - preceded( - pair(tag("BAN"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(a_line, newline), - u32_line, - )), - |(name, reason, time)| Ban(name, reason, time), - ), - ), - preceded( - pair(tag("BAN_IP"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(a_line, newline), - u32_line, - )), - |(ip, reason, time)| BanIp(ip, reason, time), - ), - ), - preceded( - pair(tag("BAN_NICK"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(a_line, newline), - u32_line, - )), - |(nick, reason, time)| BanNick(nick, reason, time), - ), - ), - ))(input) -} - -pub fn malformed_message(input: &[u8]) -> HwResult<()> { - map(terminated(take_until(&b"\n\n"[..]), end_of_message), |_| ())(input) -} - -pub fn message(input: &[u8]) -> HwResult { - delimited( - take_while(|c| c == b'\n'), - alt(( - no_arg_message, - single_arg_message, - cmd_message, - config_message, - server_var_message, - complex_message, - )), - end_of_message, - )(input) -} - -#[cfg(test)] -mod test { - use super::message; - use crate::{ - core::types::GameCfg, - protocol::{messages::HwProtocolMessage::*, parser::HwProtocolError, test::gen_proto_msg}, - }; - use proptest::{proptest, proptest_helper}; - - #[cfg(test)] - proptest! { - #[test] - fn is_parser_composition_idempotent(ref msg in gen_proto_msg()) { - println!("!! Msg: {:?}, Bytes: {:?} !!", msg, msg.to_raw_protocol().as_bytes()); - assert_eq!(message(msg.to_raw_protocol().as_bytes()), Ok((&b""[..], msg.clone()))) - } - } - - #[test] - fn parse_test() { - assert_eq!(message(b"PING\n\n"), Ok((&b""[..], Ping))); - assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame))); - assert_eq!( - message(b"NICK\nit's me\n\n"), - Ok((&b""[..], Nick("it's me".to_string()))) - ); - assert_eq!(message(b"PROTO\n51\n\n"), Ok((&b""[..], Proto(51)))); - assert_eq!( - message(b"QUIT\nbye-bye\n\n"), - Ok((&b""[..], Quit(Some("bye-bye".to_string())))) - ); - assert_eq!(message(b"QUIT\n\n"), Ok((&b""[..], Quit(None)))); - assert_eq!( - message(b"CMD\nwatch 49471\n\n"), - Ok((&b""[..], Watch(49471))) - ); - assert_eq!( - message(b"BAN\nme\nbad\n77\n\n"), - Ok((&b""[..], Ban("me".to_string(), "bad".to_string(), 77))) - ); - - assert_eq!(message(b"CMD\nPART\n\n"), Ok((&b""[..], Part(None)))); - assert_eq!( - message(b"CMD\nPART _msg_\n\n"), - Ok((&b""[..], Part(Some("_msg_".to_string())))) - ); - - assert_eq!(message(b"CMD\nRND\n\n"), Ok((&b""[..], Rnd(vec![])))); - assert_eq!( - message(b"CMD\nRND A B\n\n"), - Ok((&b""[..], Rnd(vec![String::from("A"), String::from("B")]))) - ); - - assert_eq!( - message(b"CFG\nSCHEME\na\nA\n\n"), - Ok(( - &b""[..], - Cfg(GameCfg::Scheme("a".to_string(), vec!["A".to_string()])) - )) - ); - - assert_eq!( - message(b"QUIT\n1\n2\n\n"), - Err(nom::Err::Error(HwProtocolError::new())) - ); - } -} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/protocol/test.rs --- a/rust/hedgewars-server/src/protocol/test.rs Wed Jun 23 15:32:48 2021 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,252 +0,0 @@ -use proptest::{ - arbitrary::{any, any_with, Arbitrary, StrategyFor}, - strategy::{BoxedStrategy, Just, Map, Strategy}, - test_runner::{Reason, TestRunner}, -}; - -use crate::core::types::{GameCfg, HedgehogInfo, ServerVar, ServerVar::*, TeamInfo, VoteType}; - -use super::messages::{HwProtocolMessage, HwProtocolMessage::*}; - -// Due to inability to define From between Options -trait Into2: Sized { - fn into2(self) -> T; -} -impl Into2 for T { - fn into2(self) -> T { - self - } -} -impl Into2> for Vec { - fn into2(self) -> Vec { - self.into_iter().map(|x| x.0).collect() - } -} -impl Into2 for Ascii { - fn into2(self) -> String { - self.0 - } -} -impl Into2> for Option { - fn into2(self) -> Option { - self.map(|x| x.0) - } -} - -macro_rules! proto_msg_case { - ($val: ident()) => { - Just($val) - }; - ($val: ident($arg: ty)) => { - any::<$arg>().prop_map(|v| $val(v.into2())) - }; - ($val: ident($arg1: ty, $arg2: ty)) => { - any::<($arg1, $arg2)>().prop_map(|v| $val(v.0.into2(), v.1.into2())) - }; - ($val: ident($arg1: ty, $arg2: ty, $arg3: ty)) => { - any::<($arg1, $arg2, $arg3)>().prop_map(|v| $val(v.0.into2(), v.1.into2(), v.2.into2())) - }; -} - -macro_rules! proto_msg_match { - ($var: expr, def = $default: expr, $($num: expr => $constr: ident $res: tt),*) => ( - match $var { - $($num => (proto_msg_case!($constr $res)).boxed()),*, - _ => Just($default).boxed() - } - ) -} - -/// Wrapper type for generating non-empty strings -#[derive(Debug)] -struct Ascii(String); - -impl Arbitrary for Ascii { - type Parameters = ::Parameters; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - "[a-zA-Z0-9]+".prop_map(Ascii).boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for GameCfg { - type Parameters = (); - - fn arbitrary_with(_args: ::Parameters) -> ::Strategy { - use crate::core::types::GameCfg::*; - (0..10) - .no_shrink() - .prop_flat_map(|i| { - proto_msg_match!(i, def = FeatureSize(0), - 0 => FeatureSize(u32), - 1 => MapType(Ascii), - 2 => MapGenerator(u32), - 3 => MazeSize(u32), - 4 => Seed(Ascii), - 5 => Template(u32), - 6 => Ammo(Ascii, Option), - 7 => Scheme(Ascii, Vec), - 8 => Script(Ascii), - 9 => Theme(Ascii), - 10 => DrawnMap(Ascii)) - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for TeamInfo { - type Parameters = (); - - fn arbitrary_with(_args: ::Parameters) -> ::Strategy { - ( - "[a-z]+", - 0u8..127u8, - "[a-z]+", - "[a-z]+", - "[a-z]+", - "[a-z]+", - 0u8..127u8, - ) - .prop_map(|(name, color, grave, fort, voice_pack, flag, difficulty)| { - fn hog(n: u8) -> HedgehogInfo { - HedgehogInfo { - name: format!("hog{}", n), - hat: format!("hat{}", n), - } - } - let hedgehogs = [ - hog(1), - hog(2), - hog(3), - hog(4), - hog(5), - hog(6), - hog(7), - hog(8), - ]; - TeamInfo { - owner: String::new(), - name, - color, - grave, - fort, - voice_pack, - flag, - difficulty, - hedgehogs, - hedgehogs_number: 0, - } - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for ServerVar { - type Parameters = (); - - fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { - (0..=2) - .no_shrink() - .prop_flat_map(|i| { - proto_msg_match!(i, def = ServerVar::LatestProto(0), - 0 => MOTDNew(Ascii), - 1 => MOTDOld(Ascii), - 2 => LatestProto(u16) - ) - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for VoteType { - type Parameters = (); - - fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { - use VoteType::*; - (0..=4) - .no_shrink() - .prop_flat_map(|i| { - proto_msg_match!(i, def = VoteType::Pause, - 0 => Kick(Ascii), - 1 => Map(Option), - 2 => Pause(), - 3 => NewSeed(), - 4 => HedgehogsPerTeam(u8) - ) - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -pub fn gen_proto_msg() -> BoxedStrategy where { - let res = (0..=55).no_shrink().prop_flat_map(|i| { - proto_msg_match!(i, def = Ping, - 0 => Ping(), - 1 => Pong(), - 2 => Quit(Option), - 4 => Global(Ascii), - 5 => Watch(u32), - 6 => ToggleServerRegisteredOnly(), - 7 => SuperPower(), - 8 => Info(Ascii), - 9 => Nick(Ascii), - 10 => Proto(u16), - 11 => Password(Ascii, Ascii), - 12 => Checker(u16, Ascii, Ascii), - 13 => List(), - 14 => Chat(Ascii), - 15 => CreateRoom(Ascii, Option), - 16 => JoinRoom(Ascii, Option), - 17 => Follow(Ascii), - 18 => Rnd(Vec), - 19 => Kick(Ascii), - 20 => Ban(Ascii, Ascii, u32), - 21 => BanIp(Ascii, Ascii, u32), - 22 => BanNick(Ascii, Ascii, u32), - 23 => BanList(), - 24 => Unban(Ascii), - 25 => SetServerVar(ServerVar), - 26 => GetServerVar(), - 27 => RestartServer(), - 28 => Stats(), - 29 => Part(Option), - 30 => Cfg(GameCfg), - 31 => AddTeam(Box), - 32 => RemoveTeam(Ascii), - 33 => SetHedgehogsNumber(Ascii, u8), - 34 => SetTeamColor(Ascii, u8), - 35 => ToggleReady(), - 36 => StartGame(), - 37 => EngineMessage(Ascii), - 38 => RoundFinished(), - 39 => ToggleRestrictJoin(), - 40 => ToggleRestrictTeams(), - 41 => ToggleRegisteredOnly(), - 42 => RoomName(Ascii), - 43 => Delegate(Ascii), - 44 => TeamChat(Ascii), - 45 => MaxTeams(u8), - 46 => Fix(), - 47 => Unfix(), - 48 => Greeting(Option), - 49 => CallVote(Option), - 50 => Vote(bool), - 51 => ForceVote(bool), - 52 => Save(Ascii, Ascii), - 53 => Delete(Ascii), - 54 => SaveRoom(Ascii), - 55 => LoadRoom(Ascii) - ) - }); - res.boxed() -} diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/server/demo.rs --- a/rust/hedgewars-server/src/server/demo.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/server/demo.rs Wed Jun 23 23:41:51 2021 +0200 @@ -1,6 +1,6 @@ -use crate::{ - core::types::{Ammo, GameCfg, HedgehogInfo, Replay, RoomConfig, Scheme, TeamInfo}, - server::haskell::HaskellValue, +use crate::{core::types::Replay, server::haskell::HaskellValue}; +use hedgewars_network_protocol::types::{ + Ammo, GameCfg, HedgehogInfo, RoomConfig, Scheme, TeamInfo, }; use std::{ collections::HashMap, diff -r b06b33cf0a89 -r 747278149393 rust/hedgewars-server/src/server/network.rs --- a/rust/hedgewars-server/src/server/network.rs Wed Jun 23 15:32:48 2021 -0400 +++ b/rust/hedgewars-server/src/server/network.rs Wed Jun 23 23:41:51 2021 +0200 @@ -27,9 +27,10 @@ }, handlers, handlers::{IoResult, IoTask, ServerState}, - protocol::{messages::HwServerMessage::Redirect, messages::*, ProtocolDecoder}, + protocol::ProtocolDecoder, utils, }; +use hedgewars_network_protocol::{messages::HwServerMessage::Redirect, messages::*}; #[cfg(feature = "official-server")] use super::io::{IoThread, RequestId};