# HG changeset patch # User nemo # Date 1544716267 18000 # Node ID 94f10f69fe76df6de61d9e0a8bc142c29111446b # Parent b33d1c694b1dd5e0f436758a0e137110bd7a3a70# Parent 1ffa8bfc5c58a8aecfe315076f2075d6f3e4ea14 merge in 0.9.25 fixes diff -r 1ffa8bfc5c58 -r 94f10f69fe76 CMakeLists.txt --- a/CMakeLists.txt Thu Dec 13 10:49:30 2018 -0500 +++ b/CMakeLists.txt Thu Dec 13 10:51:07 2018 -0500 @@ -85,7 +85,7 @@ set(CPACK_PACKAGE_VERSION_MAJOR 0) set(CPACK_PACKAGE_VERSION_MINOR 9) set(CPACK_PACKAGE_VERSION_PATCH 25) -set(HEDGEWARS_PROTO_VER 57) +set(HEDGEWARS_PROTO_VER 58) set(HEDGEWARS_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") include(${CMAKE_MODULE_PATH}/revinfo.cmake) diff -r 1ffa8bfc5c58 -r 94f10f69fe76 ChangeLog.txt --- a/ChangeLog.txt Thu Dec 13 10:49:30 2018 -0500 +++ b/ChangeLog.txt Thu Dec 13 10:51:07 2018 -0500 @@ -1,5 +1,21 @@ + features * bugfixes +=============== 1.0.0 (unreleased) ================= + + New chat command: “/help room” (shows room chat commands within the game) + + Colorize switching arrows, pointing arrow and target cross in clan color + + Longer delays between turns so its easier to see damage and messages + + Skip ammo menu animation when playing with turn time of 10s or less + * King Mode: Fix team sometimes not being killed properly if king drowned + * King Mode: Kill resurrected minions if king is not alive + * Fix poison damage not working in first round + * Hide most HUD elements in cinematic mode + * Don't show "F1", "F2", etc. in ammo menu if these aren't the actual slot keys + +Lua API: + + New call: SetTurnTimePaused(isPaused): Call with true to pause turn time, false to unpause + + New call: GetTurnTimePaused(): Returns true if turn time is paused due to Lua + + Params explode, poison in the SpawnFake*Crate functions now optional and default to false + ====================== 0.9.25 ====================== HIGHLIGHTS: + Complete overhaul of Continental supplies diff -r 1ffa8bfc5c58 -r 94f10f69fe76 QTfrontend/ui/page/pagenet.cpp --- a/QTfrontend/ui/page/pagenet.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/QTfrontend/ui/page/pagenet.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -47,6 +47,7 @@ BtnNetConnect = new QPushButton(ConnGroupBox); BtnNetConnect->setFont(*font14); BtnNetConnect->setText(QPushButton::tr("Connect")); + BtnNetConnect->setWhatsThis(tr("Connect to the selected server")); GBClayout->addWidget(BtnNetConnect, 2, 2); tvServersList = new QTableView(ConnGroupBox); @@ -56,11 +57,13 @@ BtnUpdateSList = new QPushButton(ConnGroupBox); BtnUpdateSList->setFont(*font14); BtnUpdateSList->setText(QPushButton::tr("Update")); + BtnUpdateSList->setWhatsThis(tr("Update the list of servers")); GBClayout->addWidget(BtnUpdateSList, 2, 0); BtnSpecifyServer = new QPushButton(ConnGroupBox); BtnSpecifyServer->setFont(*font14); - BtnSpecifyServer->setText(QPushButton::tr("Specify")); + BtnSpecifyServer->setText(QPushButton::tr("Specify address")); + BtnSpecifyServer->setWhatsThis(tr("Specify the address and port number of a known server and connect to it directly")); GBClayout->addWidget(BtnSpecifyServer, 2, 1); return pageLayout; @@ -71,6 +74,7 @@ QHBoxLayout * footerLayout = new QHBoxLayout(); BtnNetSvrStart = formattedButton(QPushButton::tr("Start server")); + BtnNetSvrStart->setWhatsThis(tr("Start private server")); BtnNetSvrStart->setMinimumSize(180, 50); QString serverPath = bindir->absolutePath() + "/hedgewars-server"; #ifdef Q_OS_WIN diff -r 1ffa8bfc5c58 -r 94f10f69fe76 QTfrontend/ui/page/pagesingleplayer.cpp --- a/QTfrontend/ui/page/pagesingleplayer.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/QTfrontend/ui/page/pagesingleplayer.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -48,7 +48,7 @@ BtnCampaignPage->setVisible(true); BtnTrainPage = addButton(":/res/Trainings.png", middleLine, 1, true); - BtnTrainPage->setWhatsThis(tr("Practice your skills in a range of training missions")); + BtnTrainPage->setWhatsThis(tr("Singleplayer missions: Learn how to play in the training, practice your skills in challenges or try to complete goals in scenarios.")); return vLayout; } diff -r 1ffa8bfc5c58 -r 94f10f69fe76 QTfrontend/ui/widget/about.cpp --- a/QTfrontend/ui/widget/about.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/QTfrontend/ui/widget/about.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -102,6 +102,10 @@ #if defined(__GNUC__) libinfo.append(QString(tr("GCC: %1")).arg(__VERSION__)); +#elif defined(WIN32_VCPKG) + libinfo.append(QString(tr("VC++: %1")).arg(_MSC_FULL_VER)); +#elif defined(__VERSION__) + libinfo.append(QString(tr("Unknown Compiler: %1")).arg(__VERSION__)); #else libinfo.append(QString(tr("Unknown Compiler"))); #endif diff -r 1ffa8bfc5c58 -r 94f10f69fe76 QTfrontend/ui/widget/mapContainer.cpp --- a/QTfrontend/ui/widget/mapContainer.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/QTfrontend/ui/widget/mapContainer.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -921,6 +921,7 @@ QString randomNoMapPrev = tr("Click to randomize the theme and seed"); QString mfsComplex = QString(tr("Adjust the complexity of the generated map")); QString mfsFortsDistance = QString(tr("Adjust the distance between forts")); + QString mfsDrawnMap = QString(tr("Scale size of the drawn map")); switch (type) { case MapModel::GeneratedMap: @@ -938,6 +939,7 @@ case MapModel::HandDrawnMap: mapPreview->setWhatsThis(tr("Click to edit")); btnRandomize->setWhatsThis(randomSeed); + mapFeatureSize->setWhatsThis(mfsDrawnMap); break; case MapModel::FortsMap: mapPreview->setWhatsThis(randomNoMapPrev); @@ -990,7 +992,7 @@ mapgen = MAPGEN_DRAWN; setMapInfo(MapModel::MapInfoDrawn); btnLoadMap->show(); - mapFeatureSize->hide(); + //mapFeatureSize->hide(); btnEditMap->show(); break; case MapModel::MissionMap: @@ -1075,14 +1077,15 @@ //if (qAbs(m_prevMapFeatureSize-m_mapFeatureSize) > 4) { m_prevMapFeatureSize = m_mapFeatureSize; - updatePreview(); + if(m_mapInfo.type!= MapModel::HandDrawnMap) + updatePreview(); } } // unused because I needed the space for the slider void HWMapContainer::updateThemeButtonSize() { - if (m_mapInfo.type != MapModel::StaticMap && m_mapInfo.type != MapModel::HandDrawnMap) + if (m_mapInfo.type != MapModel::StaticMap) { btnTheme->setIconSize(QSize(30, 30)); btnTheme->setFixedHeight(30); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer/Actions.hs --- a/gameServer/Actions.hs Thu Dec 13 10:49:30 2018 -0500 +++ b/gameServer/Actions.hs Thu Dec 13 10:51:07 2018 -0500 @@ -885,3 +885,13 @@ processAction CheckVotes = checkVotes >>= mapM_ processAction + +processAction (ShowRegisteredOnlyState chans) = do + si <- gets serverInfo + processAction $ AnswerClients chans + ["CHAT", nickServer, + if isRegisteredUsersOnly si then + loc "This server no longer allows unregistered players to join." + else + loc "This server now allows unregistered players to join." + ] diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer/CoreTypes.hs --- a/gameServer/CoreTypes.hs Thu Dec 13 10:49:30 2018 -0500 +++ b/gameServer/CoreTypes.hs Thu Dec 13 10:51:07 2018 -0500 @@ -103,6 +103,7 @@ | ReactCmd [B.ByteString] | CheckVotes | SetRandomSeed + | ShowRegisteredOnlyState [ClientChan] data Event = LobbyChatMessage diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer/HWProtoCore.hs --- a/gameServer/HWProtoCore.hs Thu Dec 13 10:49:30 2018 -0500 +++ b/gameServer/HWProtoCore.hs Thu Dec 13 10:51:07 2018 -0500 @@ -107,7 +107,7 @@ -- lobby-only commands h "STATS" _ = handleCmd_lobbyOnly ["STATS"] - h "RESTART_SERVER" "YES" = handleCmd_lobbyOnly ["RESTART_SERVER"] + h "RESTART_SERVER" p = handleCmd_lobbyOnly ["RESTART_SERVER", upperCase p] -- room and lobby commands h "QUIT" _ = handleCmd ["QUIT"] @@ -120,11 +120,11 @@ h "INFO" n | not $ B.null n = handleCmd ["INFO", n] h "HELP" _ = handleCmd ["HELP"] h "REGISTERED_ONLY" _ = serverAdminOnly $ do - cl <- thisClient + rnc <- liftM snd ask + let chans = map (sendChan . client rnc) $ allClients rnc return [ModifyServerInfo(\s -> s{isRegisteredUsersOnly = not $ isRegisteredUsersOnly s}) - -- TODO: Say whether 'registered only' state is on or off - , AnswerClients [sendChan cl] ["CHAT", nickServer, loc "'Registered only' state toggled."] + , ShowRegisteredOnlyState chans ] h "SUPER_POWER" _ = serverAdminOnly $ do cl <- thisClient diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer/HWProtoInRoomState.hs --- a/gameServer/HWProtoInRoomState.hs Thu Dec 13 10:49:30 2018 -0500 +++ b/gameServer/HWProtoInRoomState.hs Thu Dec 13 10:51:07 2018 -0500 @@ -452,7 +452,7 @@ handleCmd_inRoom ["CALLVOTE"] = do cl <- thisClient return [AnswerClients [sendChan cl] - ["CHAT", nickServer, loc "Available callvote commands: kick , map , pause, newseed, hedgehogs"] + ["CHAT", nickServer, loc "Available callvote commands: hedgehogs , pause, newseed, map , kick "] ] handleCmd_inRoom ["CALLVOTE", "KICK"] = do diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer/HWProtoLobbyState.hs --- a/gameServer/HWProtoLobbyState.hs Thu Dec 13 10:49:30 2018 -0500 +++ b/gameServer/HWProtoLobbyState.hs Thu Dec 13 10:51:07 2018 -0500 @@ -220,12 +220,18 @@ handleCmd_lobby ["CLEAR_ACCOUNTS_CACHE"] = serverAdminOnly $ return [ClearAccountsCache] +handleCmd_lobby ["RESTART_SERVER", "YES"] = serverAdminOnly $ + return [RestartServer] + handleCmd_lobby ["RESTART_SERVER"] = serverAdminOnly $ - return [RestartServer] + return [Warning $ loc "Please confirm server restart with '/restart_server yes'."] + +handleCmd_lobby ["RESTART_SERVER", _] = handleCmd_lobby ["RESTART_SERVER"] + handleCmd_lobby ["STATS"] = serverAdminOnly $ return [Stats] handleCmd_lobby (s:_) = return [ProtocolError $ "Incorrect command '" `B.append` s `B.append` "' (state: in lobby)"] -handleCmd_lobby [] = return [ProtocolError "Empty command (state: in lobby)"] \ No newline at end of file +handleCmd_lobby [] = return [ProtocolError "Empty command (state: in lobby)"] diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/Cargo.toml --- a/gameServer2/Cargo.toml Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -[package] -edition = "2018" -name = "hedgewars-server" -version = "0.0.1" -authors = [ "Andrey Korotaev " ] - -[features] -official-server = ["openssl"] -tls-connections = ["openssl"] -default = [] - -[dependencies] -rand = "0.5" -mio = "0.6" -slab = "0.4" -netbuf = "0.4" -nom = "4.1" -env_logger = "0.6" -log = "0.4" -base64 = "0.10" -bitflags = "1.0" -serde = "1.0" -serde_yaml = "0.8" -serde_derive = "1.0" -openssl = { version = "0.10", optional = true } - -[dev-dependencies] -proptest = "0.8" \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/main.rs --- a/gameServer2/src/main.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -#![allow(unused_imports)] -#![deny(bare_trait_objects)] - -//use std::io::*; -//use rand::Rng; -//use std::cmp::Ordering; -use mio::net::*; -use mio::*; -use log::*; - -mod utils; -mod server; -mod protocol; - -use crate::server::network::NetworkLayer; -use std::time::Duration; - -fn main() { - env_logger::init(); - - info!("Hedgewars game server, protocol {}", utils::PROTOCOL_VERSION); - - let address = "0.0.0.0:46631".parse().unwrap(); - let listener = TcpListener::bind(&address).unwrap(); - - let poll = Poll::new().unwrap(); - let mut hw_network = NetworkLayer::new(listener, 1024, 512); - hw_network.register_server(&poll).unwrap(); - - let mut events = Events::with_capacity(1024); - - loop { - let timeout = if hw_network.has_pending_operations() { - Some(Duration::from_millis(1)) - } else { - None - }; - poll.poll(&mut events, timeout).unwrap(); - - for event in events.iter() { - if event.readiness() & Ready::readable() == Ready::readable() { - match event.token() { - utils::SERVER => hw_network.accept_client(&poll).unwrap(), - Token(tok) => hw_network.client_readable(&poll, tok).unwrap(), - } - } - if event.readiness() & Ready::writable() == Ready::writable() { - match event.token() { - utils::SERVER => unreachable!(), - Token(tok) => hw_network.client_writable(&poll, tok).unwrap(), - } - } -// if event.kind().is_hup() || event.kind().is_error() { -// match event.token() { -// utils::SERVER => unreachable!(), -// Token(tok) => server.client_error(&poll, tok).unwrap(), -// } -// } - } - hw_network.on_idle(&poll).unwrap(); - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/protocol/messages.rs --- a/gameServer2/src/protocol/messages.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,316 +0,0 @@ -use crate::server::coretypes::{ - ServerVar, GameCfg, TeamInfo, - HedgehogInfo, VoteType -}; -use std::{ops, convert::From, iter::once}; - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum HWProtocolMessage { - // core - Ping, - Pong, - Quit(Option), - //Cmd(String, Vec), - Global(String), - Watch(String), - ToggleServerRegisteredOnly, - SuperPower, - Info(String), - // not entered state - Nick(String), - Proto(u16), - Password(String, String), - Checker(u16, String, String), - // lobby - 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, - // in room - 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(String), - CallVote(Option), - Vote(bool), - ForceVote(bool), - Save(String, String), - Delete(String), - SaveRoom(String), - LoadRoom(String), - Malformed, - Empty, -} - -#[derive(Debug)] -pub enum HWServerMessage { - Ping, - Pong, - Bye(String), - Nick(String), - Proto(u16), - ServerAuth(String), - 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), - TeamAdd(Vec), - TeamRemove(String), - TeamAccepted(String), - TeamColor(String, u8), - HedgehogsNumber(String, u8), - ConfigEntry(String, Vec), - Kicked, - RunGame, - ForwardEngineMessage(Vec), - RoundFinished, - - ServerMessage(String), - Notice(String), - Warning(String), - Error(String), - Connected(u32), - Unreachable, - - //Deprecated messages - LegacyReady(bool, Vec) -} - -pub fn server_chat(msg: String) -> HWServerMessage { - HWServerMessage::ChatMsg{ nick: "[server]".to_string(), msg } -} - -impl GameCfg { - pub fn to_protocol(&self) -> (String, Vec) { - use crate::server::coretypes::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().into_iter()); - 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) - } -} - -macro_rules! const_braces { - ($e: expr) => { "{}\n" } -} - -macro_rules! msg { - [$($part: expr),*] => { - format!(concat!($(const_braces!($part)),*, "\n"), $($part),*); - }; -} - -#[cfg(test)] -macro_rules! several { - [$part: expr] => { once($part) }; - [$part: expr, $($other: expr),*] => { once($part).chain(several![$($other),*]) }; -} - -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(ServerVar), ??? - 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| several![&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(msg) => msg!["CMD", format!("GREETING {}", msg)], - //CallVote(Option<(String, Option)>) =>, ?? - 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)], - Malformed => msg!["A", "QUICK", "BROWN", "HOG", "JUMPS", "OVER", "THE", "LAZY", "DOG"], - Empty => msg![""], - _ => 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], - Bye(msg) => msg!["BYE", msg], - Nick(nick) => msg!["NICK", nick], - Proto(proto) => msg!["PROTO", proto], - ServerAuth(hash) => msg!["SERVER_AUTH", hash], - 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), - 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], - ServerMessage(msg) => msg!["SERVER_MESSAGE", msg], - Notice(msg) => msg!["NOTICE", msg], - Warning(msg) => msg!["WARNING", msg], - Error(msg) => msg!["ERROR", msg], - - LegacyReady(is_ready, nicks) => - construct_message(&[if *is_ready {"READY"} else {"NOT_READY"}], &nicks), - - _ => msg!["ERROR", "UNIMPLEMENTED"], - } - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/protocol/mod.rs --- a/gameServer2/src/protocol/mod.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -use netbuf; -use std::{ - io::{Read, Result} -}; -use nom::{ - IResult, Err -}; - -pub mod messages; -#[cfg(test)] -pub mod test; -mod parser; - -pub struct ProtocolDecoder { - buf: netbuf::Buf, - consumed: usize, -} - -impl ProtocolDecoder { - pub fn new() -> ProtocolDecoder { - ProtocolDecoder { - buf: netbuf::Buf::new(), - consumed: 0, - } - } - - pub fn read_from(&mut self, stream: &mut R) -> Result { - self.buf.read_from(stream) - } - - pub fn extract_messages(&mut self) -> Vec { - let parse_result = parser::extract_messages(&self.buf[..]); - match parse_result { - Ok((tail, msgs)) => { - self.consumed = self.buf.len() - self.consumed - tail.len(); - msgs - }, - Err(Err::Incomplete(_)) => unreachable!(), - Err(Err::Error(_)) | Err(Err::Failure(_)) => unreachable!(), - } - } - - pub fn sweep(&mut self) { - self.buf.consume(self.consumed); - self.consumed = 0; - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/protocol/parser.rs --- a/gameServer2/src/protocol/parser.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,284 +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::*; - -use std::{ - str, str::FromStr, - ops::Range -}; -use super::{ - messages::{HWProtocolMessage, HWProtocolMessage::*} -}; -#[cfg(test)] -use { - super::test::gen_proto_msg, - proptest::{proptest, proptest_helper} -}; -use crate::server::coretypes::{ - HedgehogInfo, TeamInfo, GameCfg, VoteType, MAX_HEDGEHOGS_PER_TEAM -}; - -named!(end_of_message, tag!("\n\n")); -named!(str_line<&[u8], &str>, map_res!(not_line_ending, str::from_utf8)); -named!( a_line<&[u8], String>, map!(str_line, String::from)); -named!(cmd_arg<&[u8], String>, - map!(map_res!(take_until_either!(" \n"), str::from_utf8), String::from)); -named!( u8_line<&[u8], u8>, map_res!(str_line, FromStr::from_str)); -named!(u16_line<&[u8], u16>, map_res!(str_line, FromStr::from_str)); -named!(u32_line<&[u8], u32>, map_res!(str_line, FromStr::from_str)); -named!(yes_no_line<&[u8], bool>, alt!( - do_parse!(tag_no_case!("YES") >> (true)) - | do_parse!(tag_no_case!("NO") >> (false)))); -named!(opt_param<&[u8], Option >, alt!( - do_parse!(peek!(tag!("\n\n")) >> (None)) - | do_parse!(tag!("\n") >> s: str_line >> (Some(s.to_string()))))); -named!(spaces<&[u8], &[u8]>, preceded!(tag!(" "), eat_separator!(" "))); -named!(opt_space_param<&[u8], Option >, alt!( - do_parse!(peek!(tag!("\n\n")) >> (None)) - | do_parse!(spaces >> s: str_line >> (Some(s.to_string()))))); -named!(hog_line<&[u8], HedgehogInfo>, - do_parse!(name: str_line >> eol >> hat: str_line >> - (HedgehogInfo{name: name.to_string(), hat: hat.to_string()}))); -named!(_8_hogs<&[u8], [HedgehogInfo; MAX_HEDGEHOGS_PER_TEAM as usize]>, - do_parse!(h1: hog_line >> eol >> h2: hog_line >> eol >> - h3: hog_line >> eol >> h4: hog_line >> eol >> - h5: hog_line >> eol >> h6: hog_line >> eol >> - h7: hog_line >> eol >> h8: hog_line >> - ([h1, h2, h3, h4, h5, h6, h7, h8]))); -named!(voting<&[u8], VoteType>, alt!( - do_parse!(tag_no_case!("KICK") >> spaces >> n: a_line >> - (VoteType::Kick(n))) - | do_parse!(tag_no_case!("MAP") >> - n: opt!(preceded!(spaces, a_line)) >> - (VoteType::Map(n))) - | do_parse!(tag_no_case!("PAUSE") >> - (VoteType::Pause)) - | do_parse!(tag_no_case!("NEWSEED") >> - (VoteType::NewSeed)) - | do_parse!(tag_no_case!("HEDGEHOGS") >> spaces >> n: u8_line >> - (VoteType::HedgehogsPerTeam(n))))); - -/** Recognizes messages which do not take any parameters */ -named!(basic_message<&[u8], HWProtocolMessage>, alt!( - do_parse!(tag!("PING") >> (Ping)) - | do_parse!(tag!("PONG") >> (Pong)) - | do_parse!(tag!("LIST") >> (List)) - | do_parse!(tag!("BANLIST") >> (BanList)) - | do_parse!(tag!("GET_SERVER_VAR") >> (GetServerVar)) - | do_parse!(tag!("TOGGLE_READY") >> (ToggleReady)) - | do_parse!(tag!("START_GAME") >> (StartGame)) - | do_parse!(tag!("ROUNDFINISHED") >> _m: opt_param >> (RoundFinished)) - | do_parse!(tag!("TOGGLE_RESTRICT_JOINS") >> (ToggleRestrictJoin)) - | do_parse!(tag!("TOGGLE_RESTRICT_TEAMS") >> (ToggleRestrictTeams)) - | do_parse!(tag!("TOGGLE_REGISTERED_ONLY") >> (ToggleRegisteredOnly)) -)); - -/** Recognizes messages which take exactly one parameter */ -named!(one_param_message<&[u8], HWProtocolMessage>, alt!( - do_parse!(tag!("NICK") >> eol >> n: a_line >> (Nick(n))) - | do_parse!(tag!("INFO") >> eol >> n: a_line >> (Info(n))) - | do_parse!(tag!("CHAT") >> eol >> m: a_line >> (Chat(m))) - | do_parse!(tag!("PART") >> msg: opt_param >> (Part(msg))) - | do_parse!(tag!("FOLLOW") >> eol >> n: a_line >> (Follow(n))) - | do_parse!(tag!("KICK") >> eol >> n: a_line >> (Kick(n))) - | do_parse!(tag!("UNBAN") >> eol >> n: a_line >> (Unban(n))) - | do_parse!(tag!("EM") >> eol >> m: a_line >> (EngineMessage(m))) - | do_parse!(tag!("TEAMCHAT") >> eol >> m: a_line >> (TeamChat(m))) - | do_parse!(tag!("ROOM_NAME") >> eol >> n: a_line >> (RoomName(n))) - | do_parse!(tag!("REMOVE_TEAM") >> eol >> n: a_line >> (RemoveTeam(n))) - - | do_parse!(tag!("PROTO") >> eol >> d: u16_line >> (Proto(d))) - - | do_parse!(tag!("QUIT") >> msg: opt_param >> (Quit(msg))) -)); - -/** Recognizes messages preceded with CMD */ -named!(cmd_message<&[u8], HWProtocolMessage>, preceded!(tag!("CMD\n"), alt!( - do_parse!(tag_no_case!("STATS") >> (Stats)) - | do_parse!(tag_no_case!("FIX") >> (Fix)) - | do_parse!(tag_no_case!("UNFIX") >> (Unfix)) - | do_parse!(tag_no_case!("RESTART_SERVER") >> spaces >> tag!("YES") >> (RestartServer)) - | do_parse!(tag_no_case!("REGISTERED_ONLY") >> (ToggleServerRegisteredOnly)) - | do_parse!(tag_no_case!("SUPER_POWER") >> (SuperPower)) - | do_parse!(tag_no_case!("PART") >> m: opt_space_param >> (Part(m))) - | do_parse!(tag_no_case!("QUIT") >> m: opt_space_param >> (Quit(m))) - | do_parse!(tag_no_case!("DELEGATE") >> spaces >> n: a_line >> (Delegate(n))) - | do_parse!(tag_no_case!("SAVE") >> spaces >> n: cmd_arg >> spaces >> l: cmd_arg >> (Save(n, l))) - | do_parse!(tag_no_case!("DELETE") >> spaces >> n: a_line >> (Delete(n))) - | do_parse!(tag_no_case!("SAVEROOM") >> spaces >> r: a_line >> (SaveRoom(r))) - | do_parse!(tag_no_case!("LOADROOM") >> spaces >> r: a_line >> (LoadRoom(r))) - | do_parse!(tag_no_case!("GLOBAL") >> spaces >> m: a_line >> (Global(m))) - | do_parse!(tag_no_case!("WATCH") >> spaces >> i: a_line >> (Watch(i))) - | do_parse!(tag_no_case!("GREETING") >> spaces >> m: a_line >> (Greeting(m))) - | do_parse!(tag_no_case!("VOTE") >> spaces >> m: yes_no_line >> (Vote(m))) - | do_parse!(tag_no_case!("FORCE") >> spaces >> m: yes_no_line >> (ForceVote(m))) - | do_parse!(tag_no_case!("INFO") >> spaces >> n: a_line >> (Info(n))) - | do_parse!(tag_no_case!("MAXTEAMS") >> spaces >> n: u8_line >> (MaxTeams(n))) - | do_parse!(tag_no_case!("CALLVOTE") >> - v: opt!(preceded!(spaces, voting)) >> (CallVote(v))) - | do_parse!( - tag_no_case!("RND") >> alt!(spaces | peek!(end_of_message)) >> - v: str_line >> - (Rnd(v.split_whitespace().map(String::from).collect()))) -))); - -named!(complex_message<&[u8], HWProtocolMessage>, alt!( - do_parse!(tag!("PASSWORD") >> eol >> - p: a_line >> eol >> - s: a_line >> - (Password(p, s))) - | do_parse!(tag!("CHECKER") >> eol >> - i: u16_line >> eol >> - n: a_line >> eol >> - p: a_line >> - (Checker(i, n, p))) - | do_parse!(tag!("CREATE_ROOM") >> eol >> - n: a_line >> - p: opt_param >> - (CreateRoom(n, p))) - | do_parse!(tag!("JOIN_ROOM") >> eol >> - n: a_line >> - p: opt_param >> - (JoinRoom(n, p))) - | do_parse!(tag!("ADD_TEAM") >> eol >> - name: a_line >> eol >> - color: u8_line >> eol >> - grave: a_line >> eol >> - fort: a_line >> eol >> - voice_pack: a_line >> eol >> - flag: a_line >> eol >> - difficulty: u8_line >> eol >> - hedgehogs: _8_hogs >> - (AddTeam(Box::new(TeamInfo{ - name, color, grave, fort, - voice_pack, flag, difficulty, - hedgehogs, hedgehogs_number: 0 - })))) - | do_parse!(tag!("HH_NUM") >> eol >> - n: a_line >> eol >> - c: u8_line >> - (SetHedgehogsNumber(n, c))) - | do_parse!(tag!("TEAM_COLOR") >> eol >> - n: a_line >> eol >> - c: u8_line >> - (SetTeamColor(n, c))) - | do_parse!(tag!("BAN") >> eol >> - n: a_line >> eol >> - r: a_line >> eol >> - t: u32_line >> - (Ban(n, r, t))) - | do_parse!(tag!("BAN_IP") >> eol >> - n: a_line >> eol >> - r: a_line >> eol >> - t: u32_line >> - (BanIP(n, r, t))) - | do_parse!(tag!("BAN_NICK") >> eol >> - n: a_line >> eol >> - r: a_line >> eol >> - t: u32_line >> - (BanNick(n, r, t))) -)); - -named!(cfg_message<&[u8], HWProtocolMessage>, preceded!(tag!("CFG\n"), map!(alt!( - do_parse!(tag!("THEME") >> eol >> - name: a_line >> - (GameCfg::Theme(name))) - | do_parse!(tag!("SCRIPT") >> eol >> - name: a_line >> - (GameCfg::Script(name))) - | do_parse!(tag!("AMMO") >> eol >> - name: a_line >> - value: opt_param >> - (GameCfg::Ammo(name, value))) - | do_parse!(tag!("SCHEME") >> eol >> - name: a_line >> - values: opt!(preceded!(eol, separated_list!(eol, a_line))) >> - (GameCfg::Scheme(name, values.unwrap_or_default()))) - | do_parse!(tag!("FEATURE_SIZE") >> eol >> - value: u32_line >> - (GameCfg::FeatureSize(value))) - | do_parse!(tag!("MAP") >> eol >> - value: a_line >> - (GameCfg::MapType(value))) - | do_parse!(tag!("MAPGEN") >> eol >> - value: u32_line >> - (GameCfg::MapGenerator(value))) - | do_parse!(tag!("MAZE_SIZE") >> eol >> - value: u32_line >> - (GameCfg::MazeSize(value))) - | do_parse!(tag!("SEED") >> eol >> - value: a_line >> - (GameCfg::Seed(value))) - | do_parse!(tag!("TEMPLATE") >> eol >> - value: u32_line >> - (GameCfg::Template(value))) - | do_parse!(tag!("DRAWNMAP") >> eol >> - value: a_line >> - (GameCfg::DrawnMap(value))) -), Cfg))); - -named!(malformed_message<&[u8], HWProtocolMessage>, - do_parse!(separated_list!(eol, a_line) >> (Malformed))); - -named!(empty_message<&[u8], HWProtocolMessage>, - do_parse!(alt!(end_of_message | eol) >> (Empty))); - -named!(message<&[u8], HWProtocolMessage>, alt!(terminated!( - alt!( - basic_message - | one_param_message - | cmd_message - | complex_message - | cfg_message - ), end_of_message - ) - | terminated!(malformed_message, end_of_message) - | empty_message - ) -); - -named!(pub extract_messages<&[u8], Vec >, many0!(complete!(message))); - -#[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 demo\n\n"), Ok((&b""[..], Watch("demo".to_string())))); - 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!(extract_messages(b"QUIT\n1\n2\n\n"), Ok((&b""[..], vec![Malformed]))); - - assert_eq!(extract_messages(b"PING\n\nPING\n\nP"), Ok((&b"P"[..], vec![Ping, Ping]))); - assert_eq!(extract_messages(b"SING\n\nPING\n\n"), Ok((&b""[..], vec![Malformed, Ping]))); - assert_eq!(extract_messages(b"\n\n\n\nPING\n\n"), Ok((&b""[..], vec![Empty, Empty, Ping]))); - assert_eq!(extract_messages(b"\n\n\nPING\n\n"), Ok((&b""[..], vec![Empty, Empty, Ping]))); -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/protocol/test.rs --- a/gameServer2/src/protocol/test.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,168 +0,0 @@ -use proptest::{ - test_runner::{TestRunner, Reason}, - arbitrary::{any, any_with, Arbitrary, StrategyFor}, - strategy::{Strategy, BoxedStrategy, Just, Map} -}; - -use crate::server::coretypes::{GameCfg, TeamInfo, HedgehogInfo}; - -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::server::coretypes::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 { - name, color, grave, fort, - voice_pack, flag,difficulty, - hedgehogs, hedgehogs_number: 0 - } - }).boxed() - } - - type Strategy = BoxedStrategy; -} - -pub fn gen_proto_msg() -> BoxedStrategy where { - let res = (0..58).no_shrink().prop_flat_map(|i| { - proto_msg_match!(i, def = Malformed, - 0 => Ping(), - 1 => Pong(), - 2 => Quit(Option), - //3 => Cmd - 4 => Global(Ascii), - 5 => Watch(Ascii), - 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(Ascii), - //49 => CallVote(Option<(String, Option)>), - 50 => Vote(bool), - 51 => ForceVote(bool), - 52 => Save(Ascii, Ascii), - 53 => Delete(Ascii), - 54 => SaveRoom(Ascii), - 55 => LoadRoom(Ascii), - 56 => Malformed(), - 57 => Empty() - )}); - res.boxed() -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/actions.rs --- a/gameServer2/src/server/actions.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,631 +0,0 @@ -use std::{ - io, io::Write, - iter::once, - mem::replace -}; -use super::{ - server::HWServer, - room::{GameInfo, RoomFlags}, - client::HWClient, - coretypes::{ClientId, RoomId, GameCfg, VoteType}, - room::HWRoom, - handlers -}; -use crate::{ - protocol::messages::{ - HWProtocolMessage, - HWServerMessage, - HWServerMessage::*, - server_chat - }, - utils::to_engine_msg -}; -use rand::{thread_rng, Rng, distributions::Uniform}; - -pub enum Destination { - ToId(ClientId), - ToSelf, - ToAll { - room_id: Option, - protocol: Option, - skip_self: bool - } -} - -pub struct PendingMessage { - pub destination: Destination, - pub message: HWServerMessage -} - -impl PendingMessage { - pub fn send(message: HWServerMessage, client_id: ClientId) -> PendingMessage { - PendingMessage{ destination: Destination::ToId(client_id), message} - } - - pub fn send_self(message: HWServerMessage) -> PendingMessage { - PendingMessage{ destination: Destination::ToSelf, message } - } - - pub fn send_all(message: HWServerMessage) -> PendingMessage { - let destination = Destination::ToAll { - room_id: None, - protocol: None, - skip_self: false, - }; - PendingMessage{ destination, message } - } - - pub fn in_room(mut self, clients_room_id: RoomId) -> PendingMessage { - if let Destination::ToAll {ref mut room_id, ..} = self.destination { - *room_id = Some(clients_room_id) - } - self - } - - pub fn with_protocol(mut self, protocol_number: u16) -> PendingMessage { - if let Destination::ToAll {ref mut protocol, ..} = self.destination { - *protocol = Some(protocol_number) - } - self - } - - pub fn but_self(mut self) -> PendingMessage { - if let Destination::ToAll {ref mut skip_self, ..} = self.destination { - *skip_self = true - } - self - } - - pub fn action(self) -> Action { Send(self) } -} - -impl Into for PendingMessage { - fn into(self) -> Action { self.action() } -} - -impl HWServerMessage { - pub fn send(self, client_id: ClientId) -> PendingMessage { PendingMessage::send(self, client_id) } - pub fn send_self(self) -> PendingMessage { PendingMessage::send_self(self) } - pub fn send_all(self) -> PendingMessage { PendingMessage::send_all(self) } -} - -pub enum Action { - Send(PendingMessage), - RemoveClient, - ByeClient(String), - ReactProtocolMessage(HWProtocolMessage), - CheckRegistered, - JoinLobby, - AddRoom(String, Option), - RemoveRoom(RoomId), - MoveToRoom(RoomId), - MoveToLobby(String), - ChangeMaster(RoomId, Option), - RemoveTeam(String), - RemoveClientTeams, - SendRoomUpdate(Option), - StartRoomGame(RoomId), - SendTeamRemovalMessage(String), - FinishRoomGame(RoomId), - SendRoomData{to: ClientId, teams: bool, config: bool, flags: bool}, - AddVote{vote: bool, is_forced: bool}, - ApplyVoting(VoteType, RoomId), - Warn(String), - ProtocolError(String) -} - -use self::Action::*; - -pub fn run_action(server: &mut HWServer, client_id: usize, action: Action) { - match action { - Send(msg) => server.send(client_id, &msg.destination, msg.message), - ByeClient(msg) => { - let c = &server.clients[client_id]; - let nick = c.nick.clone(); - - if let Some(id) = c.room_id{ - if id != server.lobby_id { - server.react(client_id, vec![ - MoveToLobby(format!("quit: {}", msg.clone()))]); - } - } - - server.react(client_id, vec![ - LobbyLeft(nick, msg.clone()).send_all().action(), - Bye(msg).send_self().action(), - RemoveClient]); - }, - RemoveClient => { - server.removed_clients.push(client_id); - if server.clients.contains(client_id) { - server.clients.remove(client_id); - } - }, - ReactProtocolMessage(msg) => - handlers::handle(server, client_id, msg), - CheckRegistered => { - let client = &server.clients[client_id]; - if client.protocol_number > 0 && client.nick != "" { - let has_nick_clash = server.clients.iter().any( - |(id, c)| id != client_id && c.nick == client.nick); - - let actions = if !client.is_checker() && has_nick_clash { - if client.protocol_number < 38 { - vec![ByeClient("Nickname is already in use".to_string())] - } else { - server.clients[client_id].nick.clear(); - vec![Notice("NickAlreadyInUse".to_string()).send_self().action()] - } - } else { - vec![JoinLobby] - }; - server.react(client_id, actions); - } - }, - JoinLobby => { - server.clients[client_id].room_id = Some(server.lobby_id); - - let mut lobby_nicks = Vec::new(); - for (_, c) in server.clients.iter() { - if c.room_id.is_some() { - lobby_nicks.push(c.nick.clone()); - } - } - let joined_msg = LobbyJoined(lobby_nicks); - - let everyone_msg = LobbyJoined(vec![server.clients[client_id].nick.clone()]); - let flags_msg = ClientFlags( - "+i".to_string(), - server.clients.iter() - .filter(|(_, c)| c.room_id.is_some()) - .map(|(_, c)| c.nick.clone()) - .collect()); - let server_msg = ServerMessage("\u{1f994} is watching".to_string()); - let rooms_msg = Rooms(server.rooms.iter() - .filter(|(id, _)| *id != server.lobby_id) - .flat_map(|(_, r)| - r.info(r.master_id.map(|id| &server.clients[id]))) - .collect()); - server.react(client_id, vec![ - everyone_msg.send_all().but_self().action(), - joined_msg.send_self().action(), - flags_msg.send_self().action(), - server_msg.send_self().action(), - rooms_msg.send_self().action(), - ]); - }, - AddRoom(name, password) => { - let room_id = server.add_room();; - - let r = &mut server.rooms[room_id]; - let c = &mut server.clients[client_id]; - r.master_id = Some(c.id); - r.name = name; - r.password = password; - r.protocol_number = c.protocol_number; - - let actions = vec![ - RoomAdd(r.info(Some(&c))).send_all() - .with_protocol(r.protocol_number).action(), - MoveToRoom(room_id)]; - - server.react(client_id, actions); - }, - RemoveRoom(room_id) => { - let r = &mut server.rooms[room_id]; - let actions = vec![RoomRemove(r.name.clone()).send_all() - .with_protocol(r.protocol_number).action()]; - server.rooms.remove(room_id); - server.react(client_id, actions); - } - MoveToRoom(room_id) => { - let r = &mut server.rooms[room_id]; - let c = &mut server.clients[client_id]; - r.players_number += 1; - c.room_id = Some(room_id); - - let is_master = r.master_id == Some(c.id); - c.set_is_master(is_master); - c.set_is_ready(is_master); - c.set_is_joined_mid_game(false); - - if is_master { - r.ready_players_number += 1; - } - - let mut v = vec![ - RoomJoined(vec![c.nick.clone()]).send_all().in_room(room_id).action(), - ClientFlags("+i".to_string(), vec![c.nick.clone()]).send_all().action(), - SendRoomUpdate(None)]; - - if !r.greeting.is_empty() { - v.push(ChatMsg {nick: "[greeting]".to_string(), msg: r.greeting.clone()} - .send_self().action()); - } - - if !c.is_master() { - let team_names: Vec<_>; - if let Some(ref mut info) = r.game_info { - c.set_is_in_game(true); - c.set_is_joined_mid_game(true); - - { - let teams = info.client_teams(c.id); - c.teams_in_game = teams.clone().count() as u8; - c.clan = teams.clone().next().map(|t| t.color); - team_names = teams.map(|t| t.name.clone()).collect(); - } - - if !team_names.is_empty() { - info.left_teams.retain(|name| - !team_names.contains(&name)); - info.teams_in_game += team_names.len() as u8; - r.teams = info.teams_at_start.iter() - .filter(|(_, t)| !team_names.contains(&t.name)) - .cloned().collect(); - } - } else { - team_names = Vec::new(); - } - - v.push(SendRoomData{ to: client_id, teams: true, config: true, flags: true}); - - if let Some(ref info) = r.game_info { - v.push(RunGame.send_self().action()); - v.push(ClientFlags("+g".to_string(), vec![c.nick.clone()]) - .send_all().in_room(r.id).action()); - v.push(ForwardEngineMessage( - vec![to_engine_msg("e$spectate 1".bytes())]) - .send_self().action()); - v.push(ForwardEngineMessage(info.msg_log.clone()) - .send_self().action()); - - for name in &team_names { - v.push(ForwardEngineMessage( - vec![to_engine_msg(once(b'G').chain(name.bytes()))]) - .send_all().in_room(r.id).action()); - } - if info.is_paused { - v.push(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]) - .send_all().in_room(r.id).action()) - } - } - } - server.react(client_id, v); - } - SendRoomData {to, teams, config, flags} => { - let mut actions = Vec::new(); - let room_id = server.clients[client_id].room_id; - if let Some(r) = room_id.and_then(|id| server.rooms.get(id)) { - if config { - actions.push(ConfigEntry("FULLMAPCONFIG".to_string(), r.map_config()) - .send(to).action()); - for cfg in r.game_config() { - actions.push(cfg.to_server_msg().send(to).action()); - } - } - if teams { - let current_teams = match r.game_info { - Some(ref info) => &info.teams_at_start, - None => &r.teams - }; - for (owner_id, team) in current_teams.iter() { - actions.push(TeamAdd(HWRoom::team_info(&server.clients[*owner_id], &team)) - .send(to).action()); - actions.push(TeamColor(team.name.clone(), team.color) - .send(to).action()); - actions.push(HedgehogsNumber(team.name.clone(), team.hedgehogs_number) - .send(to).action()); - } - } - if flags { - if let Some(id) = r.master_id { - actions.push(ClientFlags("+h".to_string(), vec![server.clients[id].nick.clone()]) - .send(to).action()); - } - let nicks: Vec<_> = server.clients.iter() - .filter(|(_, c)| c.room_id == Some(r.id) && c.is_ready()) - .map(|(_, c)| c.nick.clone()).collect(); - if !nicks.is_empty() { - actions.push(ClientFlags("+r".to_string(), nicks) - .send(to).action()); - } - } - } - server.react(client_id, actions); - } - AddVote{vote, is_forced} => { - let mut actions = Vec::new(); - if let Some(r) = server.room(client_id) { - let mut result = None; - if let Some(ref mut voting) = r.voting { - if is_forced || voting.votes.iter().all(|(id, _)| client_id != *id) { - actions.push(server_chat("Your vote has been counted.".to_string()) - .send_self().action()); - voting.votes.push((client_id, vote)); - let i = voting.votes.iter(); - let pro = i.clone().filter(|(_, v)| *v).count(); - let contra = i.filter(|(_, v)| !*v).count(); - let success_quota = voting.voters.len() / 2 + 1; - if is_forced && vote || pro >= success_quota { - result = Some(true); - } else if is_forced && !vote || contra > voting.voters.len() - success_quota { - result = Some(false); - } - } else { - actions.push(server_chat("You already have voted.".to_string()) - .send_self().action()); - } - } else { - actions.push(server_chat("There's no voting going on.".to_string()) - .send_self().action()); - } - - if let Some(res) = result { - actions.push(server_chat("Voting closed.".to_string()) - .send_all().in_room(r.id).action()); - let voting = replace(&mut r.voting, None).unwrap(); - if res { - actions.push(ApplyVoting(voting.kind, r.id)); - } - } - } - - server.react(client_id, actions); - } - ApplyVoting(kind, room_id) => { - let mut actions = Vec::new(); - let mut id = client_id; - match kind { - VoteType::Kick(nick) => { - if let Some(c) = server.find_client(&nick) { - if c.room_id == Some(room_id) { - id = c.id; - actions.push(Kicked.send_self().action()); - actions.push(MoveToLobby("kicked".to_string())); - } - } - }, - VoteType::Map(None) => (), - VoteType::Map(Some(name)) => { - if let Some(location) = server.rooms[room_id].load_config(&name) { - actions.push(server_chat(location.to_string()) - .send_all().in_room(room_id).action()); - actions.push(SendRoomUpdate(None)); - for (_, c) in server.clients.iter() { - if c.room_id == Some(room_id) { - actions.push(SendRoomData{ - to: c.id, teams: false, - config: true, flags: false}) - } - } - } - }, - VoteType::Pause => { - if let Some(ref mut info) = server.rooms[room_id].game_info { - info.is_paused = !info.is_paused; - actions.push(server_chat("Pause toggled.".to_string()) - .send_all().in_room(room_id).action()); - actions.push(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]) - .send_all().in_room(room_id).action()); - } - }, - VoteType::NewSeed => { - let seed = thread_rng().gen_range(0, 1_000_000_000).to_string(); - let cfg = GameCfg::Seed(seed); - actions.push(cfg.to_server_msg().send_all().in_room(room_id).action()); - server.rooms[room_id].set_config(cfg); - }, - VoteType::HedgehogsPerTeam(number) => { - let r = &mut server.rooms[room_id]; - let nicks = r.set_hedgehogs_number(number); - actions.extend(nicks.into_iter().map(|n| - HedgehogsNumber(n, number).send_all().in_room(room_id).action() - )); - }, - } - server.react(id, actions); - } - MoveToLobby(msg) => { - let mut actions = Vec::new(); - let lobby_id = server.lobby_id; - if let (c, Some(r)) = server.client_and_room(client_id) { - r.players_number -= 1; - if c.is_ready() && r.ready_players_number > 0 { - r.ready_players_number -= 1; - } - if c.is_master() && (r.players_number > 0 || r.is_fixed()) { - actions.push(ChangeMaster(r.id, None)); - } - actions.push(ClientFlags("-i".to_string(), vec![c.nick.clone()]) - .send_all().action()); - } - server.react(client_id, actions); - actions = Vec::new(); - - if let (c, Some(r)) = server.client_and_room(client_id) { - c.room_id = Some(lobby_id); - if r.players_number == 0 && !r.is_fixed() { - actions.push(RemoveRoom(r.id)); - } else { - actions.push(RemoveClientTeams); - actions.push(RoomLeft(c.nick.clone(), msg) - .send_all().in_room(r.id).but_self().action()); - actions.push(SendRoomUpdate(Some(r.name.clone()))); - } - } - server.react(client_id, actions) - } - ChangeMaster(room_id, new_id) => { - let mut actions = Vec::new(); - let room_client_ids = server.room_clients(room_id); - let new_id = if server.room(client_id).map(|r| r.is_fixed()).unwrap_or(false) { - new_id - } else { - new_id.or_else(|| - room_client_ids.iter().find(|id| **id != client_id).cloned()) - }; - let new_nick = new_id.map(|id| server.clients[id].nick.clone()); - - if let (c, Some(r)) = server.client_and_room(client_id) { - match r.master_id { - Some(id) if id == c.id => { - c.set_is_master(false); - r.master_id = None; - actions.push(ClientFlags("-h".to_string(), vec![c.nick.clone()]) - .send_all().in_room(r.id).action()); - } - Some(_) => unreachable!(), - None => {} - } - r.master_id = new_id; - if !r.is_fixed() && c.protocol_number < 42 { - r.name.replace_range(.., new_nick.as_ref().map_or("[]", String::as_str)); - } - r.set_join_restriction(false); - r.set_team_add_restriction(false); - let is_fixed = r.is_fixed(); - r.set_unregistered_players_restriction(is_fixed); - if let Some(nick) = new_nick { - actions.push(ClientFlags("+h".to_string(), vec![nick]) - .send_all().in_room(r.id).action()); - } - } - if let Some(id) = new_id { - server.clients[id].set_is_master(true) - } - server.react(client_id, actions); - } - RemoveTeam(name) => { - let mut actions = Vec::new(); - if let (c, Some(r)) = server.client_and_room(client_id) { - r.remove_team(&name); - if let Some(ref mut info) = r.game_info { - info.left_teams.push(name.clone()); - } - actions.push(TeamRemove(name.clone()).send_all().in_room(r.id).action()); - actions.push(SendRoomUpdate(None)); - if r.game_info.is_some() && c.is_in_game() { - actions.push(SendTeamRemovalMessage(name)); - } - } - server.react(client_id, actions); - }, - RemoveClientTeams => { - if let (c, Some(r)) = server.client_and_room(client_id) { - let actions = r.client_teams(c.id).map(|t| RemoveTeam(t.name.clone())).collect(); - server.react(client_id, actions); - } - } - SendRoomUpdate(old_name) => { - if let (c, Some(r)) = server.client_and_room(client_id) { - let name = old_name.unwrap_or_else(|| r.name.clone()); - let actions = vec![RoomUpdated(name, r.info(Some(&c))) - .send_all().with_protocol(r.protocol_number).action()]; - server.react(client_id, actions); - } - }, - StartRoomGame(room_id) => { - let actions = { - let (room_clients, room_nicks): (Vec<_>, Vec<_>) = server.clients.iter() - .map(|(id, c)| (id, c.nick.clone())).unzip(); - let room = &mut server.rooms[room_id]; - - if !room.has_multiple_clans() { - vec![Warn("The game can't be started with less than two clans!".to_string())] - } else if room.protocol_number <= 43 && room.players_number != room.ready_players_number { - vec![Warn("Not all players are ready".to_string())] - } else if room.game_info.is_some() { - vec![Warn("The game is already in progress".to_string())] - } else { - room.start_round(); - for id in room_clients { - let c = &mut server.clients[id]; - c.set_is_in_game(false); - c.team_indices = room.client_team_indices(c.id); - } - vec![RunGame.send_all().in_room(room.id).action(), - SendRoomUpdate(None), - ClientFlags("+g".to_string(), room_nicks) - .send_all().in_room(room.id).action()] - } - }; - server.react(client_id, actions); - } - SendTeamRemovalMessage(team_name) => { - let mut actions = Vec::new(); - if let Some(r) = server.room(client_id) { - if let Some(ref mut info) = r.game_info { - let msg = once(b'F').chain(team_name.bytes()); - actions.push(ForwardEngineMessage(vec![to_engine_msg(msg)]). - send_all().in_room(r.id).but_self().action()); - info.teams_in_game -= 1; - if info.teams_in_game == 0 { - actions.push(FinishRoomGame(r.id)); - } - let remove_msg = to_engine_msg(once(b'F').chain(team_name.bytes())); - if let Some(m) = &info.sync_msg { - info.msg_log.push(m.clone()); - } - if info.sync_msg.is_some() { - info.sync_msg = None - } - info.msg_log.push(remove_msg.clone()); - actions.push(ForwardEngineMessage(vec![remove_msg]) - .send_all().in_room(r.id).but_self().action()); - } - } - server.react(client_id, actions); - } - FinishRoomGame(room_id) => { - let mut actions = Vec::new(); - - let r = &mut server.rooms[room_id]; - r.ready_players_number = 1; - actions.push(SendRoomUpdate(None)); - actions.push(RoundFinished.send_all().in_room(r.id).action()); - - if let Some(info) = replace(&mut r.game_info, None) { - for (_, c) in server.clients.iter() { - if c.room_id == Some(room_id) && c.is_joined_mid_game() { - actions.push(SendRoomData{ - to: c.id, teams: false, - config: true, flags: false}); - for name in &info.left_teams { - actions.push(TeamRemove(name.clone()) - .send(c.id).action()); - } - } - } - } - - let nicks: Vec<_> = server.clients.iter_mut() - .filter(|(_, c)| c.room_id == Some(room_id)) - .map(|(_, c)| { - c.set_is_ready(c.is_master()); - c.set_is_joined_mid_game(false); - c - }).filter_map(|c| if !c.is_master() { - Some(c.nick.clone()) - } else { - None - }).collect(); - - if !nicks.is_empty() { - let msg = if r.protocol_number < 38 { - LegacyReady(false, nicks) - } else { - ClientFlags("-r".to_string(), nicks) - }; - actions.push(msg.send_all().in_room(room_id).action()); - } - server.react(client_id, actions); - } - Warn(msg) => { - run_action(server, client_id, Warning(msg).send_self().action()); - } - ProtocolError(msg) => { - run_action(server, client_id, Error(msg).send_self().action()) - } - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/client.rs --- a/gameServer2/src/server/client.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,68 +0,0 @@ -use super::coretypes::ClientId; -use bitflags::*; - -bitflags!{ - pub struct ClientFlags: u8 { - const IS_ADMIN = 0b0000_0001; - const IS_MASTER = 0b0000_0010; - const IS_READY = 0b0000_0100; - const IS_IN_GAME = 0b0000_1000; - const IS_JOINED_MID_GAME = 0b0001_0000; - const IS_CHECKER = 0b0010_0000; - - const NONE = 0b0000_0000; - const DEFAULT = Self::NONE.bits; - } -} - -pub struct HWClient { - pub id: ClientId, - pub room_id: Option, - pub nick: String, - pub web_password: String, - pub server_salt: String, - pub protocol_number: u16, - pub flags: ClientFlags, - pub teams_in_game: u8, - pub team_indices: Vec, - pub clan: Option -} - -impl HWClient { - pub fn new(id: ClientId, salt: String) -> HWClient { - HWClient { - id, - room_id: None, - nick: String::new(), - web_password: String::new(), - server_salt: salt, - protocol_number: 0, - flags: ClientFlags::DEFAULT, - teams_in_game: 0, - team_indices: Vec::new(), - clan: None, - } - } - - fn contains(& self, mask: ClientFlags) -> bool { - self.flags.contains(mask) - } - - fn set(&mut self, mask: ClientFlags, value: bool) { - self.flags.set(mask, value); - } - - pub fn is_admin(&self)-> bool { self.contains(ClientFlags::IS_ADMIN) } - pub fn is_master(&self)-> bool { self.contains(ClientFlags::IS_MASTER) } - pub fn is_ready(&self)-> bool { self.contains(ClientFlags::IS_READY) } - pub fn is_in_game(&self)-> bool { self.contains(ClientFlags::IS_IN_GAME) } - pub fn is_joined_mid_game(&self)-> bool { self.contains(ClientFlags::IS_JOINED_MID_GAME) } - pub fn is_checker(&self)-> bool { self.contains(ClientFlags::IS_CHECKER) } - - pub fn set_is_admin(&mut self, value: bool) { self.set(ClientFlags::IS_ADMIN, value) } - pub fn set_is_master(&mut self, value: bool) { self.set(ClientFlags::IS_MASTER, value) } - pub fn set_is_ready(&mut self, value: bool) { self.set(ClientFlags::IS_READY, value) } - pub fn set_is_in_game(&mut self, value: bool) { self.set(ClientFlags::IS_IN_GAME, value) } - pub fn set_is_joined_mid_game(&mut self, value: bool) { self.set(ClientFlags::IS_JOINED_MID_GAME, value) } - pub fn set_is_checker(&mut self, value: bool) { self.set(ClientFlags::IS_CHECKER, value) } -} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/coretypes.rs --- a/gameServer2/src/server/coretypes.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -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(u32), -} - -#[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)] -pub struct TeamInfo { - 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)] -pub struct HedgehogInfo { - pub name: String, - pub hat: String, -} - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum VoteType { - Kick(String), - Map(Option), - Pause, - NewSeed, - HedgehogsPerTeam(u8) -} - -#[derive(Clone, Debug)] -pub struct Voting { - pub ttl: u32, - pub voters: Vec, - pub votes: Vec<(ClientId, bool)>, - pub kind: VoteType -} - -impl Voting { - pub fn new(kind: VoteType, voters: Vec) -> Voting { - Voting { - kind, voters, ttl: 2, - votes: Vec::new() - } - } -} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/handlers/checker.rs --- a/gameServer2/src/server/handlers/checker.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -use mio; -use log::*; - -use crate::{ - server::{ - server::HWServer, - coretypes::ClientId, - }, - protocol::messages::{ - HWProtocolMessage - }, -}; - -pub fn handle(server: & mut HWServer, client_id: ClientId, message: HWProtocolMessage) { - match message { - _ => warn!("Unknown command"), - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/handlers/common.rs --- a/gameServer2/src/server/handlers/common.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -use crate::{ - server::{actions::Action, server::HWServer}, - protocol::messages::{ - HWProtocolMessage::{self, Rnd}, HWServerMessage::{self, ChatMsg}, - } -}; -use rand::{self, Rng, thread_rng}; - -pub fn rnd_reply(options: &[String]) -> HWServerMessage { - let mut rng = thread_rng(); - let reply = if options.is_empty() { - (*rng.choose(&["heads", "tails"]).unwrap()).to_owned() - } else { - rng.choose(&options).unwrap().clone() - }; - - ChatMsg { - nick: "[random]".to_owned(), - msg: reply.clone(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::protocol::messages::HWServerMessage::ChatMsg; - use crate::server::actions::{ - Action::{self, Send}, PendingMessage, - }; - - fn reply2string(r: HWServerMessage) -> String { - match r { - ChatMsg { msg: p, .. } => String::from(p), - _ => panic!("expected a ChatMsg"), - } - } - - fn run_handle_test(opts: Vec) { - let opts2 = opts.clone(); - for opt in opts { - while reply2string(rnd_reply(&opts2)) != opt {} - } - } - - /// This test terminates almost surely. - #[test] - fn test_handle_rnd_empty() { - run_handle_test(vec![]) - } - - /// This test terminates almost surely. - #[test] - fn test_handle_rnd_nonempty() { - run_handle_test(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]) - } - - /// This test terminates almost surely (strong law of large numbers) - #[test] - fn test_distribution() { - let eps = 0.000001; - let lim = 0.5; - let opts = vec![0.to_string(), 1.to_string()]; - let mut ones = 0; - let mut tries = 0; - - while tries < 1000 || ((ones as f64 / tries as f64) - lim).abs() >= eps { - tries += 1; - if reply2string(rnd_reply(&opts)) == 1.to_string() { - ones += 1; - } - } - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/handlers/inroom.rs --- a/gameServer2/src/server/handlers/inroom.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,499 +0,0 @@ -use mio; - -use crate::{ - server::{ - coretypes::{ - ClientId, RoomId, Voting, VoteType, GameCfg, - MAX_HEDGEHOGS_PER_TEAM - }, - server::HWServer, - room::{HWRoom, RoomFlags}, - actions::{Action, Action::*} - }, - protocol::messages::{ - HWProtocolMessage, - HWServerMessage::*, - server_chat - }, - utils::is_name_illegal -}; -use std::{ - mem::swap, fs::{File, OpenOptions}, - io::{Read, Write, Result, Error, ErrorKind} -}; -use base64::{encode, decode}; -use super::common::rnd_reply; -use log::*; - -#[derive(Clone)] -struct ByMsg<'a> { - messages: &'a[u8] -} - -impl <'a> Iterator for ByMsg<'a> { - type Item = &'a[u8]; - - fn next(&mut self) -> Option<::Item> { - if let Some(size) = self.messages.get(0) { - let (msg, next) = self.messages.split_at(*size as usize + 1); - self.messages = next; - Some(msg) - } else { - None - } - } -} - -fn by_msg(source: &[u8]) -> ByMsg { - ByMsg {messages: source} -} - -const VALID_MESSAGES: &[u8] = - b"M#+LlRrUuDdZzAaSjJ,NpPwtgfhbc12345\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A"; -const NON_TIMED_MESSAGES: &[u8] = b"M#hb"; - -#[cfg(canhazslicepatterns)] -fn is_msg_valid(msg: &[u8], team_indices: &[u8]) -> bool { - match msg { - [size, typ, body..] => VALID_MESSAGES.contains(typ) - && match body { - [1...MAX_HEDGEHOGS_PER_TEAM, team, ..] if *typ == b'h' => - team_indices.contains(team), - _ => *typ != b'h' - }, - _ => false - } -} - -fn is_msg_valid(msg: &[u8], _team_indices: &[u8]) -> bool { - if let Some(typ) = msg.get(1) { - VALID_MESSAGES.contains(typ) - } else { - false - } -} - -fn is_msg_empty(msg: &[u8]) -> bool { - msg.get(1).filter(|t| **t == b'+').is_some() -} - -fn is_msg_timed(msg: &[u8]) -> bool { - msg.get(1).filter(|t| !NON_TIMED_MESSAGES.contains(t)).is_some() -} - -fn voting_description(kind: &VoteType) -> String { - format!("New voting started: {}", match kind { - VoteType::Kick(nick) => format!("kick {}", nick), - VoteType::Map(name) => format!("map {}", name.as_ref().unwrap()), - VoteType::Pause => "pause".to_string(), - VoteType::NewSeed => "new seed".to_string(), - VoteType::HedgehogsPerTeam(number) => format!("hedgehogs per team: {}", number) - }) -} - -fn room_message_flag(msg: &HWProtocolMessage) -> RoomFlags { - use crate::protocol::messages::HWProtocolMessage::*; - match msg { - ToggleRestrictJoin => RoomFlags::RESTRICTED_JOIN, - ToggleRestrictTeams => RoomFlags::RESTRICTED_TEAM_ADD, - ToggleRegisteredOnly => RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS, - _ => RoomFlags::empty() - } -} - -fn read_file(filename: &str) -> Result { - let mut reader = File::open(filename)?; - let mut result = String::new(); - reader.read_to_string(&mut result)?; - Ok(result) -} - -fn write_file(filename: &str, content: &str) -> Result<()> { - let mut writer = OpenOptions::new().create(true).write(true).open(filename)?; - writer.write_all(content.as_bytes()) -} - -pub fn handle(server: &mut HWServer, client_id: ClientId, room_id: RoomId, message: HWProtocolMessage) { - use crate::protocol::messages::HWProtocolMessage::*; - match message { - Part(None) => server.react(client_id, vec![ - MoveToLobby("part".to_string())]), - Part(Some(msg)) => server.react(client_id, vec![ - MoveToLobby(format!("part: {}", msg))]), - Chat(msg) => { - let actions = { - let c = &mut server.clients[client_id]; - let chat_msg = ChatMsg {nick: c.nick.clone(), msg}; - vec![chat_msg.send_all().in_room(room_id).but_self().action()] - }; - server.react(client_id, actions); - }, - Fix => { - if let (c, Some(r)) = server.client_and_room(client_id) { - if c.is_admin() { r.set_is_fixed(true) } - } - } - Unfix => { - if let (c, Some(r)) = server.client_and_room(client_id) { - if c.is_admin() { r.set_is_fixed(false) } - } - } - Greeting(text) => { - if let (c, Some(r)) = server.client_and_room(client_id) { - if c.is_admin() || c.is_master() && !r.is_fixed() { - r.greeting = text - } - } - } - RoomName(new_name) => { - let actions = - if is_name_illegal(&new_name) { - vec![Warn("Illegal room name! A room name must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] - } else if server.rooms[room_id].is_fixed() { - vec![Warn("Access denied.".to_string())] - } else if server.has_room(&new_name) { - vec![Warn("A room with the same name already exists.".to_string())] - } else { - let mut old_name = new_name.clone(); - swap(&mut server.rooms[room_id].name, &mut old_name); - vec![SendRoomUpdate(Some(old_name))] - }; - server.react(client_id, actions); - }, - ToggleReady => { - if let (c, Some(r)) = server.client_and_room(client_id) { - let flags = if c.is_ready() { - r.ready_players_number -= 1; - "-r" - } else { - r.ready_players_number += 1; - "+r" - }; - - let msg = if c.protocol_number < 38 { - LegacyReady(c.is_ready(), vec![c.nick.clone()]) - } else { - ClientFlags(flags.to_string(), vec![c.nick.clone()]) - }; - - let mut v = vec![msg.send_all().in_room(r.id).action()]; - - if r.is_fixed() && r.ready_players_number == r.players_number { - v.push(StartRoomGame(r.id)) - } - - c.set_is_ready(!c.is_ready()); - server.react(client_id, v); - } - } - AddTeam(info) => { - let mut actions = Vec::new(); - if let (c, Some(r)) = server.client_and_room(client_id) { - if r.teams.len() >= r.team_limit as usize { - actions.push(Warn("Too many teams!".to_string())) - } else if r.addable_hedgehogs() == 0 { - actions.push(Warn("Too many hedgehogs!".to_string())) - } else if r.find_team(|t| t.name == info.name) != None { - actions.push(Warn("There's already a team with same name in the list.".to_string())) - } else if r.game_info.is_some() { - actions.push(Warn("Joining not possible: Round is in progress.".to_string())) - } else if r.is_team_add_restricted() { - actions.push(Warn("This room currently does not allow adding new teams.".to_string())); - } else { - let team = r.add_team(c.id, *info, c.protocol_number < 42); - c.teams_in_game += 1; - c.clan = Some(team.color); - actions.push(TeamAccepted(team.name.clone()) - .send_self().action()); - actions.push(TeamAdd(HWRoom::team_info(&c, team)) - .send_all().in_room(room_id).but_self().action()); - actions.push(TeamColor(team.name.clone(), team.color) - .send_all().in_room(room_id).action()); - actions.push(HedgehogsNumber(team.name.clone(), team.hedgehogs_number) - .send_all().in_room(room_id).action()); - actions.push(SendRoomUpdate(None)); - } - } - server.react(client_id, actions); - }, - RemoveTeam(name) => { - let mut actions = Vec::new(); - if let (c, Some(r)) = server.client_and_room(client_id) { - match r.find_team_owner(&name) { - None => - actions.push(Warn("Error: The team you tried to remove does not exist.".to_string())), - Some((id, _)) if id != client_id => - actions.push(Warn("You can't remove a team you don't own.".to_string())), - Some((_, name)) => { - c.teams_in_game -= 1; - c.clan = r.find_team_color(c.id); - actions.push(Action::RemoveTeam(name.to_string())); - } - } - }; - server.react(client_id, actions); - }, - SetHedgehogsNumber(team_name, number) => { - if let (c, Some(r)) = server.client_and_room(client_id) { - let addable_hedgehogs = r.addable_hedgehogs(); - let actions = if let Some((_, team)) = r.find_team_and_owner_mut(|t| t.name == team_name) { - if !c.is_master() { - vec![ProtocolError("You're not the room master!".to_string())] - } else if number < 1 || number > MAX_HEDGEHOGS_PER_TEAM - || number > addable_hedgehogs + team.hedgehogs_number { - vec![HedgehogsNumber(team.name.clone(), team.hedgehogs_number) - .send_self().action()] - } else { - team.hedgehogs_number = number; - vec![HedgehogsNumber(team.name.clone(), number) - .send_all().in_room(room_id).but_self().action()] - } - } else { - vec![(Warn("No such team.".to_string()))] - }; - server.react(client_id, actions); - } - }, - SetTeamColor(team_name, color) => { - if let (c, Some(r)) = server.client_and_room(client_id) { - let mut owner_id = None; - let actions = if let Some((owner, team)) = r.find_team_and_owner_mut(|t| t.name == team_name) { - if !c.is_master() { - vec![ProtocolError("You're not the room master!".to_string())] - } else if false { - Vec::new() - } else { - owner_id = Some(owner); - team.color = color; - vec![TeamColor(team.name.clone(), color) - .send_all().in_room(room_id).but_self().action()] - } - } else { - vec![(Warn("No such team.".to_string()))] - }; - - if let Some(id) = owner_id { - server.clients[id].clan = Some(color); - } - - server.react(client_id, actions); - }; - }, - Cfg(cfg) => { - if let (c, Some(r)) = server.client_and_room(client_id) { - let actions = if r.is_fixed() { - vec![Warn("Access denied.".to_string())] - } else if !c.is_master() { - vec![ProtocolError("You're not the room master!".to_string())] - } else { - let cfg = match cfg { - GameCfg::Scheme(name, mut values) => { - if c.protocol_number == 49 && values.len() >= 2 { - let mut s = "X".repeat(50); - s.push_str(&values.pop().unwrap()); - values.push(s); - } - GameCfg::Scheme(name, values) - } - cfg => cfg - }; - - let v = vec![cfg.to_server_msg() - .send_all().in_room(r.id).but_self().action()]; - r.set_config(cfg); - v - }; - server.react(client_id, actions); - } - } - Save(name, location) => { - let actions = vec![server_chat(format!("Room config saved as {}", name)) - .send_all().in_room(room_id).action()]; - server.rooms[room_id].save_config(name, location); - server.react(client_id, actions); - } - SaveRoom(filename) => { - if server.clients[client_id].is_admin() { - let actions = match server.rooms[room_id].get_saves() { - Ok(text) => match write_file(&filename, &text) { - Ok(_) => vec![server_chat("Room configs saved successfully.".to_string()) - .send_self().action()], - Err(e) => { - warn!("Error while writing the config file \"{}\": {}", filename, e); - vec![Warn("Unable to save the room configs.".to_string())] - } - } - Err(e) => { - warn!("Error while serializing the room configs: {}", e); - vec![Warn("Unable to serialize the room configs.".to_string())] - } - }; - server.react(client_id, actions); - } - } - LoadRoom(filename) => { - if server.clients[client_id].is_admin() { - let actions = match read_file(&filename) { - Ok(text) => match server.rooms[room_id].set_saves(&text) { - Ok(_) => vec![server_chat("Room configs loaded successfully.".to_string()) - .send_self().action()], - Err(e) => { - warn!("Error while deserializing the room configs: {}", e); - vec![Warn("Unable to deserialize the room configs.".to_string())] - } - } - Err(e) => { - warn!("Error while reading the config file \"{}\": {}", filename, e); - vec![Warn("Unable to load the room configs.".to_string())] - } - }; - server.react(client_id, actions); - } - } - Delete(name) => { - let actions = if !server.rooms[room_id].delete_config(&name) { - vec![Warn(format!("Save doesn't exist: {}", name))] - } else { - vec![server_chat(format!("Room config {} has been deleted", name)) - .send_all().in_room(room_id).action()] - }; - server.react(client_id, actions); - } - CallVote(None) => { - server.react(client_id, vec![ - server_chat("Available callvote commands: kick , map , pause, newseed, hedgehogs ".to_string()) - .send_self().action()]) - } - CallVote(Some(kind)) => { - let is_in_game = server.rooms[room_id].game_info.is_some(); - let error = match &kind { - VoteType::Kick(nick) => { - if server.find_client(&nick).filter(|c| c.room_id == Some(room_id)).is_some() { - None - } else { - Some("/callvote kick: No such user!".to_string()) - } - }, - VoteType::Map(None) => { - let names: Vec<_> = server.rooms[room_id].saves.keys().cloned().collect(); - if names.is_empty() { - Some("/callvote map: No maps saved in this room!".to_string()) - } else { - Some(format!("Available maps: {}", names.join(", "))) - } - }, - VoteType::Map(Some(name)) => { - if server.rooms[room_id].saves.get(&name[..]).is_some() { - None - } else { - Some("/callvote map: No such map!".to_string()) - } - }, - VoteType::Pause => { - if is_in_game { - None - } else { - Some("/callvote pause: No game in progress!".to_string()) - } - }, - VoteType::NewSeed => { - None - }, - VoteType::HedgehogsPerTeam(number) => { - match number { - 1...MAX_HEDGEHOGS_PER_TEAM => None, - _ => Some("/callvote hedgehogs: Specify number from 1 to 8.".to_string()) - } - }, - }; - match error { - None => { - let msg = voting_description(&kind); - let voting = Voting::new(kind, server.room_clients(client_id)); - server.rooms[room_id].voting = Some(voting); - server.react(client_id, vec![ - server_chat(msg).send_all().in_room(room_id).action(), - AddVote{ vote: true, is_forced: false}]); - } - Some(msg) => { - server.react(client_id, vec![ - server_chat(msg).send_self().action()]) - } - } - } - Vote(vote) => { - server.react(client_id, vec![AddVote{ vote, is_forced: false }]); - } - ForceVote(vote) => { - let is_forced = server.clients[client_id].is_admin(); - server.react(client_id, vec![AddVote{ vote, is_forced }]); - } - ToggleRestrictJoin | ToggleRestrictTeams | ToggleRegisteredOnly => { - if server.clients[client_id].is_master() { - server.rooms[room_id].flags.toggle(room_message_flag(&message)); - } - server.react(client_id, vec![SendRoomUpdate(None)]); - } - StartGame => { - server.react(client_id, vec![StartRoomGame(room_id)]); - } - EngineMessage(em) => { - let mut actions = Vec::new(); - if let (c, Some(r)) = server.client_and_room(client_id) { - if c.teams_in_game > 0 { - let decoding = decode(&em[..]).unwrap(); - let messages = by_msg(&decoding); - let valid = messages.filter(|m| is_msg_valid(m, &c.team_indices)); - let non_empty = valid.clone().filter(|m| !is_msg_empty(m)); - let sync_msg = valid.clone().filter(|m| is_msg_timed(m)) - .last().map(|m| if is_msg_empty(m) {Some(encode(m))} else {None}); - - let em_response = encode(&valid.flat_map(|msg| msg).cloned().collect::>()); - if !em_response.is_empty() { - actions.push(ForwardEngineMessage(vec![em_response]) - .send_all().in_room(r.id).but_self().action()); - } - let em_log = encode(&non_empty.flat_map(|msg| msg).cloned().collect::>()); - if let Some(ref mut info) = r.game_info { - if !em_log.is_empty() { - info.msg_log.push(em_log); - } - if let Some(msg) = sync_msg { - info.sync_msg = msg; - } - } - } - } - server.react(client_id, actions) - } - RoundFinished => { - let mut actions = Vec::new(); - if let (c, Some(r)) = server.client_and_room(client_id) { - if c.is_in_game() { - c.set_is_in_game(false); - actions.push(ClientFlags("-g".to_string(), vec![c.nick.clone()]). - send_all().in_room(r.id).action()); - if r.game_info.is_some() { - for team in r.client_teams(c.id) { - actions.push(SendTeamRemovalMessage(team.name.clone())); - } - } - } - } - server.react(client_id, actions) - }, - Rnd(v) => { - let result = rnd_reply(&v); - let mut echo = vec!["/rnd".to_string()]; - echo.extend(v.into_iter()); - let chat_msg = ChatMsg { - nick: server.clients[client_id].nick.clone(), - msg: echo.join(" ") - }; - server.react(client_id, vec![ - chat_msg.send_all().in_room(room_id).action(), - result.send_all().in_room(room_id).action()]) - }, - _ => warn!("Unimplemented!") - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/handlers/lobby.rs --- a/gameServer2/src/server/handlers/lobby.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -use mio; - -use crate::{ - server::{ - server::HWServer, - coretypes::ClientId, - actions::{Action, Action::*} - }, - protocol::messages::{ - HWProtocolMessage, - HWServerMessage::* - }, - utils::is_name_illegal -}; -use super::common::rnd_reply; -use log::*; - -pub fn handle(server: &mut HWServer, client_id: ClientId, message: HWProtocolMessage) { - use crate::protocol::messages::HWProtocolMessage::*; - match message { - CreateRoom(name, password) => { - let actions = - if is_name_illegal(&name) { - vec![Warn("Illegal room name! A room name must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] - } else if server.has_room(&name) { - vec![Warn("A room with the same name already exists.".to_string())] - } else { - let flags_msg = ClientFlags( - "+hr".to_string(), - vec![server.clients[client_id].nick.clone()]); - vec![AddRoom(name, password), - flags_msg.send_self().action()] - }; - server.react(client_id, actions); - }, - Chat(msg) => { - let actions = vec![ChatMsg {nick: server.clients[client_id].nick.clone(), msg} - .send_all().in_room(server.lobby_id).but_self().action()]; - server.react(client_id, actions); - }, - JoinRoom(name, _password) => { - let room = server.rooms.iter().find(|(_, r)| r.name == name); - let room_id = room.map(|(_, r)| r.id); - let nicks = server.clients.iter() - .filter(|(_, c)| c.room_id == room_id) - .map(|(_, c)| c.nick.clone()) - .collect(); - let c = &mut server.clients[client_id]; - - let actions = if let Some((_, r)) = room { - if c.protocol_number != r.protocol_number { - vec![Warn("Room version incompatible to your Hedgewars version!".to_string())] - } else if r.is_join_restricted() { - vec![Warn("Access denied. This room currently doesn't allow joining.".to_string())] - } else if r.players_number == u8::max_value() { - vec![Warn("This room is already full".to_string())] - } else { - vec![MoveToRoom(r.id), - RoomJoined(nicks).send_self().action()] - } - } else { - vec![Warn("No such room.".to_string())] - }; - server.react(client_id, actions); - }, - Rnd(v) => { - server.react(client_id, vec![rnd_reply(&v).send_self().action()]); - }, - List => warn!("Deprecated LIST message received"), - _ => warn!("Incorrect command in lobby state"), - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/handlers/loggingin.rs --- a/gameServer2/src/server/handlers/loggingin.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -use mio; - -use crate::{ - server::{ - client::HWClient, - server::HWServer, - coretypes::ClientId, - actions::{Action, Action::*} - }, - protocol::messages::{ - HWProtocolMessage, HWServerMessage::* - }, - utils::is_name_illegal -}; -#[cfg(feature = "official-server")] -use openssl::sha::sha1; -use std::fmt::{Formatter, LowerHex}; -use log::*; - -#[derive(PartialEq)] -struct Sha1Digest([u8; 20]); - -impl LowerHex for Sha1Digest { - fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { - for byte in &self.0 { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - -#[cfg(feature = "official-server")] -fn get_hash(client: &HWClient, salt1: &str, salt2: &str) -> Sha1Digest { - let s = format!("{}{}{}{}{}", salt1, salt2, - client.web_password, client.protocol_number, "!hedgewars"); - Sha1Digest(sha1(s.as_bytes())) -} - -pub fn handle(server: & mut HWServer, client_id: ClientId, message: HWProtocolMessage) { - match message { - HWProtocolMessage::Nick(nick) => { - let client = &mut server.clients[client_id]; - debug!("{} {}", nick, is_name_illegal(&nick)); - let actions = if client.room_id != None { - unreachable!() - } - else if !client.nick.is_empty() { - vec![ProtocolError("Nickname already provided.".to_string())] - } - else if is_name_illegal(&nick) { - vec![ByeClient("Illegal nickname! Nicknames must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] - } - else { - client.nick = nick.clone(); - vec![Nick(nick).send_self().action(), - CheckRegistered] - }; - - server.react(client_id, actions); - } - HWProtocolMessage::Proto(proto) => { - let client = &mut server.clients[client_id]; - let actions = if client.protocol_number != 0 { - vec![ProtocolError("Protocol already known.".to_string())] - } - else if proto == 0 { - vec![ProtocolError("Bad number.".to_string())] - } - else { - client.protocol_number = proto; - vec![Proto(proto).send_self().action(), - CheckRegistered] - }; - server.react(client_id, actions); - } - #[cfg(feature = "official-server")] - HWProtocolMessage::Password(hash, salt) => { - let c = &server.clients[client_id]; - - let client_hash = get_hash(c, &salt, &c.server_salt); - let server_hash = get_hash(c, &c.server_salt, &salt); - let actions = if client_hash == server_hash { - vec![ServerAuth(format!("{:x}", server_hash)).send_self().action(), - JoinLobby] - } else { - vec![ByeClient("Authentication failed".to_string())] - }; - server.react(client_id, actions); - } - #[cfg(feature = "official-server")] - HWProtocolMessage::Checker(protocol, nick, password) => { - let c = &mut server.clients[client_id]; - c.nick = nick; - c.web_password = password; - c.set_is_checker(true); - } - _ => warn!("Incorrect command in logging-in state"), - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/handlers/mod.rs --- a/gameServer2/src/server/handlers/mod.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,44 +0,0 @@ -use mio; -use std::{io, io::Write}; - -use super::{ - server::HWServer, - actions::{Action, Action::*}, - coretypes::ClientId -}; -use crate::{ - protocol::messages::{ - HWProtocolMessage, - HWServerMessage::* - } -}; -use log::*; - -mod loggingin; -mod lobby; -mod inroom; -mod common; -mod checker; - -pub fn handle(server: &mut HWServer, client_id: ClientId, message: HWProtocolMessage) { - match message { - HWProtocolMessage::Ping => - server.react(client_id, vec![Pong.send_self().action()]), - HWProtocolMessage::Quit(Some(msg)) => - server.react(client_id, vec![ByeClient("User quit: ".to_string() + &msg)]), - HWProtocolMessage::Quit(None) => - server.react(client_id, vec![ByeClient("User quit".to_string())]), - HWProtocolMessage::Malformed => warn!("Malformed/unknown message"), - HWProtocolMessage::Empty => warn!("Empty message"), - _ => { - match server.clients[client_id].room_id { - None => - loggingin::handle(server, client_id, message), - Some(id) if id == server.lobby_id => - lobby::handle(server, client_id, message), - Some(id) => - inroom::handle(server, client_id, id, message) - } - }, - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/mod.rs --- a/gameServer2/src/server/mod.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -pub mod server; -pub mod client; -pub mod room; -pub mod network; -pub mod coretypes; -mod actions; -mod handlers; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/network.rs --- a/gameServer2/src/server/network.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,443 +0,0 @@ -extern crate slab; - -use std::{ - io, io::{Error, ErrorKind, Read, Write}, - net::{SocketAddr, IpAddr, Ipv4Addr}, - collections::HashSet, - mem::{swap, replace} -}; - -use mio::{ - net::{TcpStream, TcpListener}, - Poll, PollOpt, Ready, Token -}; -use netbuf; -use slab::Slab; -use log::*; - -use crate::{ - utils, - protocol::{ProtocolDecoder, messages::*} -}; -use super::{ - server::{HWServer}, - coretypes::ClientId -}; -#[cfg(feature = "tls-connections")] -use openssl::{ - ssl::{ - SslMethod, SslContext, Ssl, SslContextBuilder, - SslVerifyMode, SslFiletype, SslOptions, - SslStreamBuilder, HandshakeError, MidHandshakeSslStream, SslStream - }, - error::ErrorStack -}; - -const MAX_BYTES_PER_READ: usize = 2048; - -#[derive(Hash, Eq, PartialEq, Copy, Clone)] -pub enum NetworkClientState { - Idle, - NeedsWrite, - NeedsRead, - Closed, -} - -type NetworkResult = io::Result<(T, NetworkClientState)>; - -#[cfg(not(feature = "tls-connections"))] -pub enum ClientSocket { - Plain(TcpStream) -} - -#[cfg(feature = "tls-connections")] -pub enum ClientSocket { - SslHandshake(Option>), - SslStream(SslStream) -} - -impl ClientSocket { - fn inner(&self) -> &TcpStream { - #[cfg(not(feature = "tls-connections"))] - match self { - ClientSocket::Plain(stream) => stream, - } - - #[cfg(feature = "tls-connections")] - match self { - ClientSocket::SslHandshake(Some(builder)) => builder.get_ref(), - ClientSocket::SslHandshake(None) => unreachable!(), - ClientSocket::SslStream(ssl_stream) => ssl_stream.get_ref() - } - } -} - -pub struct NetworkClient { - id: ClientId, - socket: ClientSocket, - peer_addr: SocketAddr, - decoder: ProtocolDecoder, - buf_out: netbuf::Buf -} - -impl NetworkClient { - pub fn new(id: ClientId, socket: ClientSocket, peer_addr: SocketAddr) -> NetworkClient { - NetworkClient { - id, socket, peer_addr, - decoder: ProtocolDecoder::new(), - buf_out: netbuf::Buf::new() - } - } - - #[cfg(feature = "tls-connections")] - fn handshake_impl(&mut self, handshake: MidHandshakeSslStream) -> io::Result { - match handshake.handshake() { - Ok(stream) => { - self.socket = ClientSocket::SslStream(stream); - debug!("TLS handshake with {} ({}) completed", self.id, self.peer_addr); - Ok(NetworkClientState::Idle) - } - Err(HandshakeError::WouldBlock(new_handshake)) => { - self.socket = ClientSocket::SslHandshake(Some(new_handshake)); - Ok(NetworkClientState::Idle) - } - Err(HandshakeError::Failure(new_handshake)) => { - self.socket = ClientSocket::SslHandshake(Some(new_handshake)); - debug!("TLS handshake with {} ({}) failed", self.id, self.peer_addr); - Err(Error::new(ErrorKind::Other, "Connection failure")) - } - Err(HandshakeError::SetupFailure(_)) => unreachable!() - } - } - - fn read_impl(decoder: &mut ProtocolDecoder, source: &mut R, - id: ClientId, addr: &SocketAddr) -> NetworkResult> { - let mut bytes_read = 0; - let result = loop { - match decoder.read_from(source) { - Ok(bytes) => { - debug!("Client {}: read {} bytes", id, bytes); - bytes_read += bytes; - if bytes == 0 { - let result = if bytes_read == 0 { - info!("EOF for client {} ({})", id, addr); - (Vec::new(), NetworkClientState::Closed) - } else { - (decoder.extract_messages(), NetworkClientState::NeedsRead) - }; - break Ok(result); - } - else if bytes_read >= MAX_BYTES_PER_READ { - break Ok((decoder.extract_messages(), NetworkClientState::NeedsRead)) - } - } - Err(ref error) if error.kind() == ErrorKind::WouldBlock => { - let messages = if bytes_read == 0 { - Vec::new() - } else { - decoder.extract_messages() - }; - break Ok((messages, NetworkClientState::Idle)); - } - Err(error) => - break Err(error) - } - }; - decoder.sweep(); - result - } - - pub fn read(&mut self) -> NetworkResult> { - #[cfg(not(feature = "tls-connections"))] - match self.socket { - ClientSocket::Plain(ref mut stream) => - NetworkClient::read_impl(&mut self.decoder, stream, self.id, &self.peer_addr), - } - - #[cfg(feature = "tls-connections")] - match self.socket { - ClientSocket::SslHandshake(ref mut handshake_opt) => { - let handshake = std::mem::replace(handshake_opt, None).unwrap(); - Ok((Vec::new(), self.handshake_impl(handshake)?)) - }, - ClientSocket::SslStream(ref mut stream) => - NetworkClient::read_impl(&mut self.decoder, stream, self.id, &self.peer_addr) - } - } - - fn write_impl(buf_out: &mut netbuf::Buf, destination: &mut W) -> NetworkResult<()> { - let result = loop { - match buf_out.write_to(destination) { - Ok(bytes) if buf_out.is_empty() || bytes == 0 => - break Ok(((), NetworkClientState::Idle)), - Ok(_) => (), - Err(ref error) if error.kind() == ErrorKind::Interrupted - || error.kind() == ErrorKind::WouldBlock => { - break Ok(((), NetworkClientState::NeedsWrite)); - }, - Err(error) => - break Err(error) - } - }; - result - } - - pub fn write(&mut self) -> NetworkResult<()> { - let result = { - #[cfg(not(feature = "tls-connections"))] - match self.socket { - ClientSocket::Plain(ref mut stream) => - NetworkClient::write_impl(&mut self.buf_out, stream) - } - - #[cfg(feature = "tls-connections")] { - match self.socket { - ClientSocket::SslHandshake(ref mut handshake_opt) => { - let handshake = std::mem::replace(handshake_opt, None).unwrap(); - Ok(((), self.handshake_impl(handshake)?)) - } - ClientSocket::SslStream(ref mut stream) => - NetworkClient::write_impl(&mut self.buf_out, stream) - } - } - }; - - self.socket.inner().flush()?; - result - } - - pub fn send_raw_msg(&mut self, msg: &[u8]) { - self.buf_out.write_all(msg).unwrap(); - } - - pub fn send_string(&mut self, msg: &str) { - self.send_raw_msg(&msg.as_bytes()); - } - - pub fn send_msg(&mut self, msg: &HWServerMessage) { - self.send_string(&msg.to_raw_protocol()); - } -} - -#[cfg(feature = "tls-connections")] -struct ServerSsl { - context: SslContext -} - -pub struct NetworkLayer { - listener: TcpListener, - server: HWServer, - clients: Slab, - pending: HashSet<(ClientId, NetworkClientState)>, - pending_cache: Vec<(ClientId, NetworkClientState)>, - #[cfg(feature = "tls-connections")] - ssl: ServerSsl -} - -impl NetworkLayer { - pub fn new(listener: TcpListener, clients_limit: usize, rooms_limit: usize) -> NetworkLayer { - let server = HWServer::new(clients_limit, rooms_limit); - let clients = Slab::with_capacity(clients_limit); - let pending = HashSet::with_capacity(2 * clients_limit); - let pending_cache = Vec::with_capacity(2 * clients_limit); - - NetworkLayer { - listener, server, clients, pending, pending_cache, - #[cfg(feature = "tls-connections")] - ssl: NetworkLayer::create_ssl_context() - } - } - - #[cfg(feature = "tls-connections")] - fn create_ssl_context() -> ServerSsl { - let mut builder = SslContextBuilder::new(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - builder.set_read_ahead(true); - builder.set_certificate_file("ssl/cert.pem", SslFiletype::PEM).unwrap(); - builder.set_private_key_file("ssl/key.pem", SslFiletype::PEM).unwrap(); - builder.set_options(SslOptions::NO_COMPRESSION); - builder.set_cipher_list("DEFAULT:!LOW:!RC4:!EXP").unwrap(); - ServerSsl { context: builder.build() } - } - - pub fn register_server(&self, poll: &Poll) -> io::Result<()> { - poll.register(&self.listener, utils::SERVER, Ready::readable(), - PollOpt::edge()) - } - - fn deregister_client(&mut self, poll: &Poll, id: ClientId) { - let mut client_exists = false; - if let Some(ref client) = self.clients.get(id) { - poll.deregister(client.socket.inner()) - .expect("could not deregister socket"); - info!("client {} ({}) removed", client.id, client.peer_addr); - client_exists = true; - } - if client_exists { - self.clients.remove(id); - } - } - - fn register_client(&mut self, poll: &Poll, id: ClientId, client_socket: ClientSocket, addr: SocketAddr) { - poll.register(client_socket.inner(), Token(id), - Ready::readable() | Ready::writable(), - PollOpt::edge()) - .expect("could not register socket with event loop"); - - let entry = self.clients.vacant_entry(); - let client = NetworkClient::new(id, client_socket, addr); - info!("client {} ({}) added", client.id, client.peer_addr); - entry.insert(client); - } - - fn flush_server_messages(&mut self) { - debug!("{} pending server messages", self.server.output.len()); - for (clients, message) in self.server.output.drain(..) { - debug!("Message {:?} to {:?}", message, clients); - let msg_string = message.to_raw_protocol(); - for client_id in clients { - if let Some(client) = self.clients.get_mut(client_id) { - client.send_string(&msg_string); - self.pending.insert((client_id, NetworkClientState::NeedsWrite)); - } - } - } - } - - fn create_client_socket(&self, socket: TcpStream) -> io::Result { - #[cfg(not(feature = "tls-connections"))] { - Ok(ClientSocket::Plain(socket)) - } - - #[cfg(feature = "tls-connections")] { - let ssl = Ssl::new(&self.ssl.context).unwrap(); - let mut builder = SslStreamBuilder::new(ssl, socket); - builder.set_accept_state(); - match builder.handshake() { - Ok(stream) => - Ok(ClientSocket::SslStream(stream)), - Err(HandshakeError::WouldBlock(stream)) => - Ok(ClientSocket::SslHandshake(Some(stream))), - Err(e) => { - debug!("OpenSSL handshake failed: {}", e); - Err(Error::new(ErrorKind::Other, "Connection failure")) - } - } - } - } - - pub fn accept_client(&mut self, poll: &Poll) -> io::Result<()> { - let (client_socket, addr) = self.listener.accept()?; - info!("Connected: {}", addr); - - let client_id = self.server.add_client(); - self.register_client(poll, client_id, self.create_client_socket(client_socket)?, addr); - self.flush_server_messages(); - - Ok(()) - } - - fn operation_failed(&mut self, poll: &Poll, client_id: ClientId, error: &Error, msg: &str) -> io::Result<()> { - let addr = if let Some(ref mut client) = self.clients.get_mut(client_id) { - client.peer_addr - } else { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) - }; - debug!("{}({}): {}", msg, addr, error); - self.client_error(poll, client_id) - } - - pub fn client_readable(&mut self, poll: &Poll, - client_id: ClientId) -> io::Result<()> { - let messages = - if let Some(ref mut client) = self.clients.get_mut(client_id) { - client.read() - } else { - warn!("invalid readable client: {}", client_id); - Ok((Vec::new(), NetworkClientState::Idle)) - }; - - match messages { - Ok((messages, state)) => { - for message in messages { - self.server.handle_msg(client_id, message); - } - match state { - NetworkClientState::NeedsRead => { - self.pending.insert((client_id, state)); - }, - NetworkClientState::Closed => - self.client_error(&poll, client_id)?, - _ => {} - }; - } - Err(e) => self.operation_failed( - poll, client_id, &e, - "Error while reading from client socket")? - } - - self.flush_server_messages(); - - if !self.server.removed_clients.is_empty() { - let ids: Vec<_> = self.server.removed_clients.drain(..).collect(); - for client_id in ids { - self.deregister_client(poll, client_id); - } - } - - Ok(()) - } - - pub fn client_writable(&mut self, poll: &Poll, - client_id: ClientId) -> io::Result<()> { - let result = - if let Some(ref mut client) = self.clients.get_mut(client_id) { - client.write() - } else { - warn!("invalid writable client: {}", client_id); - Ok(((), NetworkClientState::Idle)) - }; - - match result { - Ok(((), state)) if state == NetworkClientState::NeedsWrite => { - self.pending.insert((client_id, state)); - }, - Ok(_) => {} - Err(e) => self.operation_failed( - poll, client_id, &e, - "Error while writing to client socket")? - } - - Ok(()) - } - - pub fn client_error(&mut self, poll: &Poll, - client_id: ClientId) -> io::Result<()> { - self.deregister_client(poll, client_id); - self.server.client_lost(client_id); - - Ok(()) - } - - pub fn has_pending_operations(&self) -> bool { - !self.pending.is_empty() - } - - pub fn on_idle(&mut self, poll: &Poll) -> io::Result<()> { - if self.has_pending_operations() { - let mut cache = replace(&mut self.pending_cache, Vec::new()); - cache.extend(self.pending.drain()); - for (id, state) in cache.drain(..) { - match state { - NetworkClientState::NeedsRead => - self.client_readable(poll, id)?, - NetworkClientState::NeedsWrite => - self.client_writable(poll, id)?, - _ => {} - } - } - swap(&mut cache, &mut self.pending_cache); - } - Ok(()) - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/room.rs --- a/gameServer2/src/server/room.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,391 +0,0 @@ -use std::{ - iter, collections::HashMap -}; -use crate::server::{ - coretypes::{ - ClientId, RoomId, TeamInfo, GameCfg, GameCfg::*, Voting, - MAX_HEDGEHOGS_PER_TEAM - }, - client::{HWClient} -}; -use bitflags::*; -use serde::{Serialize, Deserialize}; -use serde_derive::{Serialize, Deserialize}; -use serde_yaml; - -const MAX_TEAMS_IN_ROOM: u8 = 8; -const MAX_HEDGEHOGS_IN_ROOM: u8 = - MAX_HEDGEHOGS_PER_TEAM * MAX_HEDGEHOGS_PER_TEAM; - -#[derive(Clone, Serialize, Deserialize)] -struct Ammo { - name: String, - settings: Option -} - -#[derive(Clone, Serialize, Deserialize)] -struct Scheme { - name: String, - settings: Vec -} - -#[derive(Clone, Serialize, Deserialize)] -struct RoomConfig { - feature_size: u32, - map_type: String, - map_generator: u32, - maze_size: u32, - seed: String, - template: u32, - - ammo: Ammo, - scheme: Scheme, - script: String, - theme: String, - drawn_map: Option -} - -impl RoomConfig { - 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 - } - } -} - -fn client_teams_impl(teams: &[(ClientId, TeamInfo)], client_id: ClientId) - -> impl Iterator + Clone -{ - teams.iter().filter(move |(id, _)| *id == client_id).map(|(_, t)| t) -} - -fn map_config_from(c: &RoomConfig) -> Vec { - vec![c.feature_size.to_string(), c.map_type.to_string(), - c.map_generator.to_string(), c.maze_size.to_string(), - c.seed.to_string(), c.template.to_string()] -} - -fn game_config_from(c: &RoomConfig) -> Vec { - use crate::server::coretypes::GameCfg::*; - let mut v = vec![ - Ammo(c.ammo.name.to_string(), c.ammo.settings.clone()), - Scheme(c.scheme.name.to_string(), c.scheme.settings.clone()), - Script(c.script.to_string()), - Theme(c.theme.to_string())]; - if let Some(ref m) = c.drawn_map { - v.push(DrawnMap(m.to_string())) - } - v -} - -pub struct GameInfo { - pub teams_in_game: u8, - pub teams_at_start: Vec<(ClientId, TeamInfo)>, - pub left_teams: Vec, - pub msg_log: Vec, - pub sync_msg: Option, - pub is_paused: bool, - config: RoomConfig -} - -impl GameInfo { - fn new(teams: Vec<(ClientId, TeamInfo)>, config: RoomConfig) -> GameInfo { - GameInfo { - left_teams: Vec::new(), - msg_log: Vec::new(), - sync_msg: None, - is_paused: false, - teams_in_game: teams.len() as u8, - teams_at_start: teams, - config - } - } - - pub fn client_teams(&self, client_id: ClientId) -> impl Iterator + Clone { - client_teams_impl(&self.teams_at_start, client_id) - } -} - -#[derive(Serialize, Deserialize)] -pub struct RoomSave { - pub location: String, - config: RoomConfig -} - -bitflags!{ - pub struct RoomFlags: u8 { - const FIXED = 0b0000_0001; - const RESTRICTED_JOIN = 0b0000_0010; - const RESTRICTED_TEAM_ADD = 0b0000_0100; - const RESTRICTED_UNREGISTERED_PLAYERS = 0b0000_1000; - } -} - -pub struct HWRoom { - pub id: RoomId, - pub master_id: Option, - pub name: String, - pub password: Option, - pub greeting: String, - pub protocol_number: u16, - pub flags: RoomFlags, - - pub players_number: u8, - pub default_hedgehog_number: u8, - pub team_limit: u8, - pub ready_players_number: u8, - pub teams: Vec<(ClientId, TeamInfo)>, - config: RoomConfig, - pub voting: Option, - pub saves: HashMap, - pub game_info: Option -} - -impl HWRoom { - pub fn new(id: RoomId) -> HWRoom { - HWRoom { - id, - master_id: None, - name: String::new(), - password: None, - greeting: "".to_string(), - flags: RoomFlags::empty(), - protocol_number: 0, - players_number: 0, - default_hedgehog_number: 4, - team_limit: MAX_TEAMS_IN_ROOM, - ready_players_number: 0, - teams: Vec::new(), - config: RoomConfig::new(), - voting: None, - saves: HashMap::new(), - game_info: None - } - } - - pub fn hedgehogs_number(&self) -> u8 { - self.teams.iter().map(|(_, t)| t.hedgehogs_number).sum() - } - - pub fn addable_hedgehogs(&self) -> u8 { - MAX_HEDGEHOGS_IN_ROOM - self.hedgehogs_number() - } - - pub fn add_team(&mut self, owner_id: ClientId, mut team: TeamInfo, preserve_color: bool) -> &TeamInfo { - if !preserve_color { - team.color = iter::repeat(()).enumerate() - .map(|(i, _)| i as u8).take(u8::max_value() as usize + 1) - .find(|i| self.teams.iter().all(|(_, t)| t.color != *i)) - .unwrap_or(0u8) - }; - team.hedgehogs_number = if self.teams.is_empty() { - self.default_hedgehog_number - } else { - self.teams[0].1.hedgehogs_number.min(self.addable_hedgehogs()) - }; - self.teams.push((owner_id, team)); - &self.teams.last().unwrap().1 - } - - pub fn remove_team(&mut self, name: &str) { - if let Some(index) = self.teams.iter().position(|(_, t)| t.name == name) { - self.teams.remove(index); - } - } - - pub fn set_hedgehogs_number(&mut self, n: u8) -> Vec { - let mut names = Vec::new(); - let teams = match self.game_info { - Some(ref mut info) => &mut info.teams_at_start, - None => &mut self.teams - }; - - if teams.len() as u8 * n <= MAX_HEDGEHOGS_IN_ROOM { - for (_, team) in teams.iter_mut() { - team.hedgehogs_number = n; - names.push(team.name.clone()) - }; - self.default_hedgehog_number = n; - } - names - } - - pub fn find_team_and_owner_mut(&mut self, f: F) -> Option<(ClientId, &mut TeamInfo)> - where F: Fn(&TeamInfo) -> bool { - self.teams.iter_mut().find(|(_, t)| f(t)).map(|(id, t)| (*id, t)) - } - - pub fn find_team(&self, f: F) -> Option<&TeamInfo> - where F: Fn(&TeamInfo) -> bool { - self.teams.iter().find_map(|(_, t)| Some(t).filter(|t| f(&t))) - } - - pub fn client_teams(&self, client_id: ClientId) -> impl Iterator { - client_teams_impl(&self.teams, client_id) - } - - pub fn client_team_indices(&self, client_id: ClientId) -> Vec { - self.teams.iter().enumerate() - .filter(move |(_, (id, _))| *id == client_id) - .map(|(i, _)| i as u8).collect() - } - - pub fn find_team_owner(&self, team_name: &str) -> Option<(ClientId, &str)> { - self.teams.iter().find(|(_, t)| t.name == team_name) - .map(|(id, t)| (*id, &t.name[..])) - } - - pub fn find_team_color(&self, owner_id: ClientId) -> Option { - self.client_teams(owner_id).nth(0).map(|t| t.color) - } - - pub fn has_multiple_clans(&self) -> bool { - self.teams.iter().min_by_key(|(_, t)| t.color) != - self.teams.iter().max_by_key(|(_, t)| t.color) - } - - pub fn set_config(&mut self, cfg: GameCfg) { - let c = &mut self.config; - match cfg { - FeatureSize(s) => c.feature_size = s, - MapType(t) => c.map_type = t, - MapGenerator(g) => c.map_generator = g, - MazeSize(s) => c.maze_size = s, - Seed(s) => c.seed = s, - Template(t) => c.template = t, - - Ammo(n, s) => c.ammo = Ammo {name: n, settings: s}, - Scheme(n, s) => c.scheme = Scheme {name: n, settings: s}, - Script(s) => c.script = s, - Theme(t) => c.theme = t, - DrawnMap(m) => c.drawn_map = Some(m) - }; - } - - pub fn start_round(&mut self) { - if self.game_info.is_none() { - self.game_info = Some(GameInfo::new( - self.teams.clone(), self.config.clone())); - } - } - - pub fn is_fixed(&self) -> bool { - self.flags.contains(RoomFlags::FIXED) - } - pub fn is_join_restricted(&self) -> bool { - self.flags.contains(RoomFlags::RESTRICTED_JOIN) - } - pub fn is_team_add_restricted(&self) -> bool { - self.flags.contains(RoomFlags::RESTRICTED_TEAM_ADD) - } - pub fn are_unregistered_players_restricted(&self) -> bool { - self.flags.contains(RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS) - } - - pub fn set_is_fixed(&mut self, value: bool) { - self.flags.set(RoomFlags::FIXED, value) - } - pub fn set_join_restriction(&mut self, value: bool) { - self.flags.set(RoomFlags::RESTRICTED_JOIN, value) - } - pub fn set_team_add_restriction(&mut self, value: bool) { - self.flags.set(RoomFlags::RESTRICTED_TEAM_ADD, value) - } - pub fn set_unregistered_players_restriction(&mut self, value: bool) { - self.flags.set(RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS, value) - } - - fn flags_string(&self) -> String { - let mut result = "-".to_string(); - if self.game_info.is_some() { result += "g" } - if self.password.is_some() { result += "p" } - if self.is_join_restricted() { result += "j" } - if self.are_unregistered_players_restricted() { - result += "r" - } - result - } - - pub fn info(&self, master: Option<&HWClient>) -> Vec { - let c = &self.config; - vec![ - self.flags_string(), - self.name.clone(), - self.players_number.to_string(), - self.teams.len().to_string(), - master.map_or("[]", |c| &c.nick).to_string(), - c.map_type.to_string(), - c.script.to_string(), - c.scheme.name.to_string(), - c.ammo.name.to_string() - ] - } - - pub fn map_config(&self) -> Vec { - match self.game_info { - Some(ref info) => map_config_from(&info.config), - None => map_config_from(&self.config) - } - } - - pub fn game_config(&self) -> Vec { - match self.game_info { - Some(ref info) => game_config_from(&info.config), - None => game_config_from(&self.config) - } - } - - pub fn save_config(&mut self, name: String, location: String) { - self.saves.insert(name, RoomSave { location, config: self.config.clone() }); - } - - pub fn load_config(&mut self, name: &str) -> Option<&str> { - if let Some(save) = self.saves.get(name) { - self.config = save.config.clone(); - Some(&save.location[..]) - } else { - None - } - } - - pub fn delete_config(&mut self, name: &str) -> bool { - self.saves.remove(name).is_some() - } - - pub fn get_saves(&self) -> Result { - serde_yaml::to_string(&(&self.greeting, &self.saves)) - } - - pub fn set_saves(&mut self, text: &str) -> Result<(), serde_yaml::Error> { - serde_yaml::from_str::<(String, HashMap)>(text).map(|(greeting, saves)| { - self.greeting = greeting; - self.saves = saves; - }) - } - - pub fn team_info(owner: &HWClient, team: &TeamInfo) -> Vec { - let mut info = vec![ - team.name.clone(), - team.grave.clone(), - team.fort.clone(), - team.voice_pack.clone(), - team.flag.clone(), - owner.nick.clone(), - team.difficulty.to_string()]; - let hogs = team.hedgehogs.iter().flat_map(|h| - iter::once(h.name.clone()).chain(iter::once(h.hat.clone()))); - info.extend(hogs); - info - } -} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/server/server.rs --- a/gameServer2/src/server/server.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,156 +0,0 @@ -use slab; -use crate::utils; -use super::{ - client::HWClient, room::HWRoom, actions, handlers, - coretypes::{ClientId, RoomId}, - actions::{Destination, PendingMessage} -}; -use crate::protocol::messages::*; -use rand::{RngCore, thread_rng}; -use base64::{encode}; -use log::*; - -type Slab = slab::Slab; - - -pub struct HWServer { - pub clients: Slab, - pub rooms: Slab, - pub lobby_id: RoomId, - pub output: Vec<(Vec, HWServerMessage)>, - pub removed_clients: Vec, -} - -impl HWServer { - pub fn new(clients_limit: usize, rooms_limit: usize) -> HWServer { - let rooms = Slab::with_capacity(rooms_limit); - let clients = Slab::with_capacity(clients_limit); - let mut server = HWServer { - clients, rooms, - lobby_id: 0, - output: vec![], - removed_clients: vec![] - }; - server.lobby_id = server.add_room(); - server - } - - pub fn add_client(&mut self) -> ClientId { - let key: ClientId; - { - let entry = self.clients.vacant_entry(); - key = entry.key(); - let mut salt = [0u8; 18]; - thread_rng().fill_bytes(&mut salt); - - let client = HWClient::new(entry.key(), encode(&salt)); - entry.insert(client); - } - self.send(key, &Destination::ToSelf, HWServerMessage::Connected(utils::PROTOCOL_VERSION)); - key - } - - pub fn client_lost(&mut self, client_id: ClientId) { - actions::run_action(self, client_id, - actions::Action::ByeClient("Connection reset".to_string())); - } - - pub fn add_room(&mut self) -> RoomId { - let entry = self.rooms.vacant_entry(); - let key = entry.key(); - let room = HWRoom::new(entry.key()); - entry.insert(room); - key - } - - pub fn handle_msg(&mut self, client_id: ClientId, msg: HWProtocolMessage) { - debug!("Handling message {:?} for client {}", msg, client_id); - if self.clients.contains(client_id) { - handlers::handle(self, client_id, msg); - } - } - - fn get_recipients(&self, client_id: ClientId, destination: &Destination) -> Vec { - let mut ids = match *destination { - Destination::ToSelf => vec![client_id], - Destination::ToId(id) => vec![id], - Destination::ToAll {room_id: Some(id), ..} => - self.room_clients(id), - Destination::ToAll {protocol: Some(proto), ..} => - self.protocol_clients(proto), - Destination::ToAll {..} => - self.clients.iter().map(|(id, _)| id).collect::>() - }; - if let Destination::ToAll {skip_self: true, ..} = destination { - if let Some(index) = ids.iter().position(|id| *id == client_id) { - ids.remove(index); - } - } - ids - } - - pub fn send(&mut self, client_id: ClientId, destination: &Destination, message: HWServerMessage) { - let ids = self.get_recipients(client_id, &destination); - self.output.push((ids, message)); - } - - pub fn react(&mut self, client_id: ClientId, actions: Vec) { - for action in actions { - actions::run_action(self, client_id, action); - } - } - - pub fn lobby(&self) -> &HWRoom { &self.rooms[self.lobby_id] } - - pub fn has_room(&self, name: &str) -> bool { - self.rooms.iter().any(|(_, r)| r.name == name) - } - - pub fn find_room(&self, name: &str) -> Option<&HWRoom> { - self.rooms.iter().find_map(|(_, r)| Some(r).filter(|r| r.name == name)) - } - - pub fn find_room_mut(&mut self, name: &str) -> Option<&mut HWRoom> { - self.rooms.iter_mut().find_map(|(_, r)| Some(r).filter(|r| r.name == name)) - } - - pub fn find_client(&self, nick: &str) -> Option<&HWClient> { - self.clients.iter().find_map(|(_, c)| Some(c).filter(|c| c.nick == nick)) - } - - pub fn find_client_mut(&mut self, nick: &str) -> Option<&mut HWClient> { - self.clients.iter_mut().find_map(|(_, c)| Some(c).filter(|c| c.nick == nick)) - } - - pub fn select_clients(&self, f: F) -> Vec - where F: Fn(&(usize, &HWClient)) -> bool { - self.clients.iter().filter(f) - .map(|(_, c)| c.id).collect() - } - - pub fn room_clients(&self, room_id: RoomId) -> Vec { - self.select_clients(|(_, c)| c.room_id == Some(room_id)) - } - - pub fn protocol_clients(&self, protocol: u16) -> Vec { - self.select_clients(|(_, c)| c.protocol_number == protocol) - } - - pub fn other_clients_in_room(&self, self_id: ClientId) -> Vec { - let room_id = self.clients[self_id].room_id; - self.select_clients(|(id, c)| *id != self_id && c.room_id == room_id ) - } - - pub fn client_and_room(&mut self, client_id: ClientId) -> (&mut HWClient, Option<&mut HWRoom>) { - let c = &mut self.clients[client_id]; - if let Some(room_id) = c.room_id { - (c, Some(&mut self.rooms[room_id])) - } else { - (c, None) - } - } - - pub fn room(&mut self, client_id: ClientId) -> Option<&mut HWRoom> { - self.client_and_room(client_id).1 - } -} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 gameServer2/src/utils.rs --- a/gameServer2/src/utils.rs Thu Dec 13 10:49:30 2018 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -use std::iter::Iterator; -use mio; -use base64::{encode}; - -pub const PROTOCOL_VERSION : u32 = 3; -pub const SERVER: mio::Token = mio::Token(1_000_000_000); - -pub fn is_name_illegal(name: &str ) -> bool{ - name.len() > 40 || - name.trim().is_empty() || - name.chars().any(|c| - "$()*+?[]^{|}\x7F".contains(c) || - '\x00' <= c && c <= '\x1F') -} - -pub fn to_engine_msg(msg: T) -> String - where T: Iterator + Clone -{ - let mut tmp = Vec::new(); - tmp.push(msg.clone().count() as u8); - tmp.extend(msg); - encode(&tmp) -} - -pub fn protocol_version_string(protocol_number: u16) -> &'static str { - match protocol_number { - 17 => "0.9.7-dev", - 19 => "0.9.7", - 20 => "0.9.8-dev", - 21 => "0.9.8", - 22 => "0.9.9-dev", - 23 => "0.9.9", - 24 => "0.9.10-dev", - 25 => "0.9.10", - 26 => "0.9.11-dev", - 27 => "0.9.11", - 28 => "0.9.12-dev", - 29 => "0.9.12", - 30 => "0.9.13-dev", - 31 => "0.9.13", - 32 => "0.9.14-dev", - 33 => "0.9.14", - 34 => "0.9.15-dev", - 35 => "0.9.14.1", - 37 => "0.9.15", - 38 => "0.9.16-dev", - 39 => "0.9.16", - 40 => "0.9.17-dev", - 41 => "0.9.17", - 42 => "0.9.18-dev", - 43 => "0.9.18", - 44 => "0.9.19-dev", - 45 => "0.9.19", - 46 => "0.9.20-dev", - 47 => "0.9.20", - 48 => "0.9.21-dev", - 49 => "0.9.21", - 50 => "0.9.22-dev", - 51 => "0.9.22", - 52 => "0.9.23-dev", - 53 => "0.9.23", - 54 => "0.9.24-dev", - 55 => "0.9.24", - 56 => "0.9.25-dev", - _ => "Unknown" - } -} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uChat.pas --- a/hedgewars/uChat.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uChat.pas Thu Dec 13 10:51:07 2018 -0500 @@ -589,6 +589,15 @@ exit end; + if (copy(s, 2, 9) = 'help room') then + begin + if (gameType = gmtNet) then + SendConsoleCommand('/help') + else + AddChatString(#0 + shortstring(trcmd[sidCmdHelpRoomFail])); + exit; + end; + if (copy(s, 2, 4) = 'help') then begin AddChatString(#3 + shortstring(trcmd[sidCmdHeaderBasic])); @@ -608,6 +617,8 @@ AddChatString(#3 + shortstring(trcmd[sidCmdHistory])); AddChatString(#3 + shortstring(trcmd[sidCmdHelp])); AddChatString(#3 + shortstring(trcmd[sidCmdHelpTaunts])); + if gameType = gmtNet then + AddChatString(#3 + shortstring(trcmd[sidCmdHelpRoom])); exit end; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uGears.pas --- a/hedgewars/uGears.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uGears.pas Thu Dec 13 10:51:07 2018 -0500 @@ -62,12 +62,20 @@ var delay: LongWord; delay2: LongWord; - step: (stInit, stDelay, stChDmg, stSweep, stTurnStats, stChWin1, - stTurnReact, stAfterDelay, stChWin2, stWater, stChWin3, - stHealth, stSpawn, stNTurn); + step: (stInit, stDelay1, stChDmg, stSweep, stTurnStats, stChWin1, + stTurnReact, stDelay2, stChWin2, stWater, stChWin3, + stChKing, stSuddenDeath, stDelay3, stHealth, stSpawn, stDelay4, + stNTurn); NewTurnTick: LongWord; //SDMusic: shortstring; +const delaySDStart = 1600; + delaySDWarning = 1000; + delayDamageTagFull = 1500; + delayDamageTagShort = 500; + delayTurnReact = 800; + delayFinal = 100; + function CheckNoDamage: boolean; // returns TRUE in case of no damaged hhs var Gear: PGear; dmg: LongInt; @@ -86,7 +94,7 @@ CheckNoDamage:= false; dmg:= Gear^.Damage; - if Gear^.Health < dmg then + if (Gear^.Health < dmg) then begin Gear^.Active:= true; Gear^.Health:= 0 @@ -105,7 +113,15 @@ RenderHealth(Gear^.Hedgehog^); RecountTeamHealth(Gear^.Hedgehog^.Team); + end + else if ((GameFlags and gfKing) <> 0) and (not Gear^.Hedgehog^.Team^.hasKing) then + begin + Gear^.Active:= true; + Gear^.Health:= 0; + RenderHealth(Gear^.Hedgehog^); + RecountTeamHealth(Gear^.Hedgehog^.Team); end; + if (not isInMultiShoot) then Gear^.Karma:= 0; Gear^.Damage:= 0 @@ -114,6 +130,34 @@ end; end; +function DoDelay: boolean; +begin +if delay <= 0 then + delay:= 1 +else + dec(delay); +DoDelay:= delay = 0; +end; + +function CheckMinionsDie: boolean; +var Gear: PGear; +begin + CheckMinionsDie:= false; + if (GameFlags and gfKing) = 0 then + exit; + + Gear:= GearsList; + while Gear <> nil do + begin + if (Gear^.Kind = gtHedgehog) and (not Gear^.Hedgehog^.King) and (not Gear^.Hedgehog^.Team^.hasKing) then + begin + CheckMinionsDie:= true; + exit; + end; + Gear:= Gear^.NextGear; + end; +end; + procedure HealthMachine; var Gear: PGear; team: PTeam; @@ -269,22 +313,20 @@ ScriptCall('onEndTurn'); inc(step) end; - stDelay: - begin - if delay = 0 then - delay:= cInactDelay - else - dec(delay); - - if delay = 0 then - inc(step) - end; - + stDelay1: + if DoDelay() then + inc(step); stChDmg: if CheckNoDamage then inc(step) else - step:= stDelay; + begin + if (not bBetweenTurns) and (not isInMultiShoot) then + delay:= delayDamageTagShort + else + delay:= delayDamageTagFull; + step:= stDelay1; + end; stSweep: if SweepDirty then @@ -314,22 +356,16 @@ begin uStats.TurnReaction; uStats.TurnStatsReset; + delay:= delayTurnReact; inc(step) end else inc(step, 2); end; - stAfterDelay: - begin - if delay = 0 then - delay:= cInactDelay - else - dec(delay); - - if delay = 0 then - inc(step) - end; + stDelay2: + if DoDelay() then + inc(step); stChWin2: begin CheckForWin(); @@ -357,39 +393,79 @@ inc(step) end; - stHealth: + stChKing: + begin + if (not isInMultiShoot) and (CheckMinionsDie) then + step:= stChDmg + else + inc(step); + end; + stSuddenDeath: begin - if (cWaterRise <> 0) or (cHealthDecrease <> 0) then - begin - if (TotalRoundsPre = cSuddenDTurns) and (not SuddenDeath) and (not isInMultiShoot) then - StartSuddenDeath() - else if (TotalRoundsPre < cSuddenDTurns) and (not isInMultiShoot) then + if ((cWaterRise <> 0) or (cHealthDecrease <> 0)) and (not (isInMultiShoot or bBetweenTurns)) then + begin + // Start Sudden Death + if (TotalRoundsPre = cSuddenDTurns) and (not SuddenDeath) then + begin + StartSuddenDeath(); + delay:= delaySDStart; + inc(step); + end + // Show Sudden Death warning message + else if (TotalRoundsPre < cSuddenDTurns) and ((LastSuddenDWarn = -2) or (LastSuddenDWarn <> TotalRoundsPre)) then begin i:= cSuddenDTurns - TotalRoundsPre; s:= ansistring(inttostr(i)); - if i = 1 then - AddCaption(trmsg[sidRoundSD], capcolDefault, capgrpGameState) - else if (i = 2) or ((i > 0) and ((i mod 50 = 0) or ((i <= 25) and (i mod 5 = 0)))) then - AddCaption(FormatA(trmsg[sidRoundsSD], s), capcolDefault, capgrpGameState); - end; + // X rounds before SD. X = 1, 2, 3, 5, 7, 10, 15, 20, 25, 50, 100, ... + if (i > 0) and ((i <= 3) or (i = 7) or ((i mod 50 = 0) or ((i <= 25) and (i mod 5 = 0)))) then + begin + if i = 1 then + AddCaption(trmsg[sidRoundSD], capcolDefault, capgrpGameState) + else + AddCaption(FormatA(trmsg[sidRoundsSD], s), capcolDefault, capgrpGameState); + delay:= delaySDWarning; + inc(step); + LastSuddenDWarn:= TotalRoundsPre; + end + else + inc(step, 2); + end + else + inc(step, 2); + end + else + inc(step, 2); + end; + stDelay3: + if DoDelay() then + inc(step); + stHealth: + begin + if bBetweenTurns + or isInMultiShoot + or (TotalRoundsReal = -1) then + inc(step) + else + begin + bBetweenTurns:= true; + HealthMachine; + step:= stChDmg end; - if bBetweenTurns - or isInMultiShoot - or (TotalRoundsPre = -1) then - inc(step) - else - begin - bBetweenTurns:= true; - HealthMachine; - step:= stChDmg - end - end; + end; stSpawn: begin - if not isInMultiShoot then + if (not isInMultiShoot) then + begin SpawnBoxOfSmth; - inc(step) + delay:= delayFinal; + inc(step); + end + else + inc(step, 2) end; + stDelay4: + if DoDelay() then + inc(step); stNTurn: begin if isInMultiShoot then @@ -466,11 +542,11 @@ if IsClockRunning() then //(CurrentHedgehog^.CurAmmoType in [amShotgun, amDEagle, amSniperRifle]) begin - if (cHedgehogTurnTime >= 10000) + if (cHedgehogTurnTime > TurnTimeLeft) and (CurrentHedgehog^.Gear <> nil) and ((CurrentHedgehog^.Gear^.State and gstAttacked) = 0) and (not isGetAwayTime) and (ReadyTimeLeft = 0) then - if TurnTimeLeft = 5000 then + if (TurnTimeLeft = 5000) and (cHedgehogTurnTime >= 10000) then PlaySoundV(sndHurry, CurrentTeam^.voicepack) else if TurnTimeLeft = 4000 then PlaySound(sndCountdown4) @@ -1147,7 +1223,8 @@ or (Ammoz[CurrentHedgehog^.CurAmmoType].Ammo.Propz and ammoprop_DoesntStopTimerWhileAttacking <> 0) or ((GameFlags and gfInfAttack) <> 0) and (Ammoz[CurrentHedgehog^.CurAmmoType].Ammo.Propz and ammoprop_DoesntStopTimerWhileAttackingInInfAttackMode <> 0) or (CurrentHedgehog^.CurAmmoType = amSniperRifle)) - and (not(isInMultiShoot and ((Ammoz[CurrentHedgehog^.CurAmmoType].Ammo.Propz and ammoprop_DoesntStopTimerInMultiShoot) <> 0))); + and (not(isInMultiShoot and ((Ammoz[CurrentHedgehog^.CurAmmoType].Ammo.Propz and ammoprop_DoesntStopTimerInMultiShoot) <> 0))) + and (not LuaClockPaused); end; @@ -1332,7 +1409,7 @@ //typed const delay:= 0; delay2:= 0; - step:= stDelay; + step:= stDelay1; upd:= 0; //SDMusic:= 'hell.ogg'; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uGearsHandlersMess.pas --- a/hedgewars/uGearsHandlersMess.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uGearsHandlersMess.pas Thu Dec 13 10:51:07 2018 -0500 @@ -6023,6 +6023,8 @@ RenderHealth(resgear^.Hedgehog^); RecountTeamHealth(resgear^.Hedgehog^.Team); resgear^.Hedgehog^.Effects[heResurrected]:= 1; + if resgear^.Hedgehog^.King then + resgear^.Hedgehog^.Team^.hasKing:= true; { Reviving a hog implies its clan is now alive, too. } resgear^.Hedgehog^.Team^.Clan^.DeathLogged:= false; s:= ansistring(resgear^.Hedgehog^.Name); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uGearsHedgehog.pas --- a/hedgewars/uGearsHedgehog.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uGearsHedgehog.pas Thu Dec 13 10:51:07 2018 -0500 @@ -1307,7 +1307,7 @@ else if not isInMultiShoot then AllInactive:= false; -if (TurnTimeLeft = 0) or (HHGear^.Damage > 0) or (LuaEndTurnRequested = true) then +if (TurnTimeLeft = 0) or (HHGear^.Damage > 0) or (((GameFlags and gfKing) <> 0) and (not Hedgehog^.Team^.hasKing)) or (LuaEndTurnRequested = true) then begin if (Hedgehog^.CurAmmoType = amKnife) then LoadHedgehogHat(Hedgehog^, Hedgehog^.Hat); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uGearsList.pas --- a/hedgewars/uGearsList.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uGearsList.pas Thu Dec 13 10:51:07 2018 -0500 @@ -771,7 +771,6 @@ procedure DeleteGear(Gear: PGear); var team: PTeam; t,i: Longword; - k: boolean; cakeData: PCakeData; iterator: PGear; begin @@ -857,19 +856,14 @@ if Gear^.Hedgehog^.King then begin - // are there any other kings left? Just doing nil check. Presumably a mortally wounded king will get reaped soon enough - k:= false; + Gear^.Hedgehog^.Team^.hasKing:= false; for i:= 0 to Pred(team^.Clan^.TeamsNumber) do - if (team^.Clan^.Teams[i]^.Hedgehogs[0].Gear <> nil) then - k:= true; - if not k then - for i:= 0 to Pred(team^.Clan^.TeamsNumber) do - with team^.Clan^.Teams[i]^ do - for t:= 0 to cMaxHHIndex do - if Hedgehogs[t].Gear <> nil then - Hedgehogs[t].Gear^.Health:= 0 - else if (Hedgehogs[t].GearHidden <> nil) then - Hedgehogs[t].GearHidden^.Health:= 0 // hog is still hidden. if tardis should return though, lua, eh... + with team^.Clan^.Teams[i]^ do + for t:= 0 to cMaxHHIndex do + if Hedgehogs[t].Gear <> nil then + Hedgehogs[t].Gear^.Health:= 0 + else if (Hedgehogs[t].GearHidden <> nil) then + Hedgehogs[t].GearHidden^.Health:= 0 // hog is still hidden. if tardis should return though, lua, eh... end; // should be not CurrentHedgehog, but hedgehog of the last gear which caused damage to this hog diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uGearsRender.pas --- a/hedgewars/uGearsRender.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uGearsRender.pas Thu Dec 13 10:51:07 2018 -0500 @@ -1189,7 +1189,9 @@ dAngle := DxDy2Angle(int2hwfloat(ty - oy), int2hwfloat(tx - ox)) + 90; + Tint(Team^.Clan^.Color shl 8 or $FF); DrawSpriteRotatedF(sprFinger, tx, ty, RealTicks div 32 mod 16, 1, dAngle); + untint; end; @@ -1264,7 +1266,13 @@ //DrawTextureRotatedF(SpritesData[sprSnowDust].Texture, 1, 0, 0, Gear^.Target.X + WorldDx, Gear^.Target.Y + WorldDy, (RealTicks shr 2) mod 8, 1, 22, 22, (RealTicks shr 3) mod 360) DrawTextureRotatedF(SpritesData[sprSnowDust].Texture, 1/(1+(RealTicks shr 8) mod 5), 0, 0, Gear^.Target.X + WorldDx, Gear^.Target.Y + WorldDy, (RealTicks shr 2) mod 8, 1, 22, 22, (RealTicks shr 3) mod 360) else + begin + if CurrentHedgehog <> nil then + Tint(CurrentHedgehog^.Team^.Clan^.Color shl 8 or $FF); DrawSpriteRotatedF(sprTargetP, Gear^.Target.X + WorldDx, Gear^.Target.Y + WorldDy, 0, 0, (RealTicks shr 3) mod 360); + if CurrentHedgehog <> nil then + untint; + end; case Gear^.Kind of gtGrenade: DrawSpriteRotated(sprBomb, x, y, 0, Gear^.DirAngle); @@ -1467,7 +1475,13 @@ DrawSpriteRotatedF(sprTeleport, hwRound(HHGear^.X) + 1 + WorldDx, hwRound(HHGear^.Y) - 3 + WorldDy, 11 - Gear^.Pos, hwSign(HHGear^.dX), 0) end end; - gtSwitcher: DrawSprite(sprSwitch, x - 16, y - 56, (RealTicks shr 6) mod 12); + gtSwitcher: begin + setTintAdd(true); + Tint(Gear^.Hedgehog^.Team^.Clan^.Color shl 8 or $FF); + DrawSprite(sprSwitch, x - 16, y - 56, (RealTicks shr 6) mod 12); + untint; + setTintAdd(false); + end; gtTarget: begin Tint($FF, $FF, $FF, round($FF * Gear^.Timer / 1000)); DrawSprite(sprTarget, x - 16, y - 16, 0); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uInputHandler.pas --- a/hedgewars/uInputHandler.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uInputHandler.pas Thu Dec 13 10:51:07 2018 -0500 @@ -38,6 +38,10 @@ procedure ProcessKey(event: TSDL_KeyboardEvent); inline; procedure ProcessKey(code: LongInt; KeyDown: boolean); +{$IFDEF USE_AM_NUMCOLUMN} +function CheckDefaultSlotKeys: boolean; +{$ENDIF} + procedure ResetKbd; procedure ResetMouseWheel; procedure FreezeEnterKey; @@ -488,6 +492,27 @@ end; +{$IFDEF USE_AM_NUMCOLUMN} +function CheckDefaultSlotKeys: boolean; +{$IFDEF USE_TOUCH_INTERFACE} +begin + CheckDefaultSlotKeys:= false; +{$ELSE} +var i, code: LongInt; +begin + for i:=1 to cMaxSlotIndex do + begin + code:= KeyNameToCode('f'+IntToStr(i)); + if CurrentBinds.binds[CurrentBinds.indices[code]] <> 'slot '+char(i+48) then + begin + CheckDefaultSlotKeys:= false; + exit; + end; + end; + CheckDefaultSlotKeys:= true; +{$ENDIF} +end; +{$ENDIF} {$IFNDEF MOBILE} procedure SetBinds(var binds: TBinds); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uLand.pas --- a/hedgewars/uLand.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uLand.pas Thu Dec 13 10:51:07 2018 -0500 @@ -285,13 +285,13 @@ procedure GenDrawnMap; begin - ResizeLand(4096, 2048); + ResizeLand((4096 * max(min(cFeatureSize,24),3)) div 12, (2048 * max(min(cFeatureSize,24),3)) div 12); uLandPainted.Draw; - MaxHedgehogs:= 48; + MaxHedgehogs:= 64; hasGirders:= true; - playHeight:= 2048; - playWidth:= 4096; + playHeight:= LAND_HEIGHT; + playWidth:= LAND_WIDTH; leftX:= ((LAND_WIDTH - playWidth) div 2); rightX:= (playWidth + ((LAND_WIDTH - playWidth) div 2)) - 1; topY:= LAND_HEIGHT - playHeight; @@ -300,11 +300,11 @@ function SelectTemplate: LongInt; var l: LongInt; begin - SelectTemplate:= 0; + SelectTemplate:= 0; if (cReducedQuality and rqLowRes) <> 0 then SelectTemplate:= SmallTemplates[getrandom(Succ(High(SmallTemplates)))] else - begin + begin if cTemplateFilter = 0 then begin l:= getRandom(GroupedTemplatesCount); @@ -313,22 +313,22 @@ dec(l, TemplateCounts[cTemplateFilter]); until l < 0; end - else getRandom(1); + else getRandom(1); - case cTemplateFilter of - 0: OutError('Error selecting TemplateFilter. Ask unC0Rr about what you did wrong', true); - 1: SelectTemplate:= SmallTemplates[getrandom(TemplateCounts[cTemplateFilter])]; - 2: SelectTemplate:= MediumTemplates[getrandom(TemplateCounts[cTemplateFilter])]; - 3: SelectTemplate:= LargeTemplates[getrandom(TemplateCounts[cTemplateFilter])]; - 4: SelectTemplate:= CavernTemplates[getrandom(TemplateCounts[cTemplateFilter])]; - 5: SelectTemplate:= WackyTemplates[getrandom(TemplateCounts[cTemplateFilter])]; - // For lua only! - 6: begin - SelectTemplate:= min(LuaTemplateNumber,High(EdgeTemplates)); - GetRandom(2) // burn 1 - end - end - end; + case cTemplateFilter of + 0: OutError('Error selecting TemplateFilter. Ask unC0Rr about what you did wrong', true); + 1: SelectTemplate:= SmallTemplates[getrandom(TemplateCounts[cTemplateFilter])]; + 2: SelectTemplate:= MediumTemplates[getrandom(TemplateCounts[cTemplateFilter])]; + 3: SelectTemplate:= LargeTemplates[getrandom(TemplateCounts[cTemplateFilter])]; + 4: SelectTemplate:= CavernTemplates[getrandom(TemplateCounts[cTemplateFilter])]; + 5: SelectTemplate:= WackyTemplates[getrandom(TemplateCounts[cTemplateFilter])]; + // For lua only! + 6: begin + SelectTemplate:= min(LuaTemplateNumber,High(EdgeTemplates)); + GetRandom(2) // burn 1 + end + end + end; WriteLnToConsole('Selected template #'+inttostr(SelectTemplate)+' using filter #'+inttostr(cTemplateFilter)); end; @@ -886,7 +886,7 @@ mgRandom: GenTemplated(EdgeTemplates[SelectTemplate]); mgMaze: begin ResizeLand(4096,2048); GenMaze; end; mgPerlin: begin ResizeLand(4096,2048); GenPerlin; end; - mgDrawn: GenDrawnMap; + mgDrawn: begin cFeatureSize:= 3;GenDrawnMap; end; mgForts: MakeFortsPreview(); else OutError('Unknown mapgen', true); @@ -895,8 +895,16 @@ ScriptSetMapGlobals; // strict scaling needed here since preview assumes a rectangle - rh:= max(LAND_HEIGHT,2048); - rw:= max(LAND_WIDTH,4096); + if (cMapGen <> mgDrawn) then + begin + rh:= max(LAND_HEIGHT, 2048); + rw:= max(LAND_WIDTH, 4096); + end + else + begin + rh:= LAND_HEIGHT; + rw:= LAND_WIDTH + end; ox:= 0; if rw < rh*2 then begin @@ -937,7 +945,7 @@ mgRandom: GenTemplated(EdgeTemplates[SelectTemplate]); mgMaze: begin ResizeLand(4096,2048); GenMaze; end; mgPerlin: begin ResizeLand(4096,2048); GenPerlin; end; - mgDrawn: GenDrawnMap; + mgDrawn: begin cFeatureSize:= 3;GenDrawnMap; end; mgForts: MakeFortsPreview; else OutError('Unknown mapgen', true); @@ -945,9 +953,19 @@ ScriptSetMapGlobals; + // strict scaling needed here since preview assumes a rectangle - rh:= max(LAND_HEIGHT, 2048); - rw:= max(LAND_WIDTH, 4096); + if (cMapGen <> mgDrawn) then + begin + rh:= max(LAND_HEIGHT, 2048); + rw:= max(LAND_WIDTH, 4096); + end + else + begin + rh:= LAND_HEIGHT; + rw:= LAND_WIDTH + end; + ox:= 0; if rw < rh*2 then begin @@ -986,9 +1004,9 @@ procedure chSendLandDigest(var s: shortstring); var i: LongInt; - landPixelDigest : LongInt; + landPixelDigest : LongInt; begin - landPixelDigest:= 1; + landPixelDigest:= 1; for i:= 0 to LAND_HEIGHT-1 do landPixelDigest:= Adler32Update(landPixelDigest, @Land[i,0], LAND_WIDTH*2); s:= 'M' + IntToStr(syncedPixelDigest)+'|'+IntToStr(landPixelDigest); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uLandPainted.pas --- a/hedgewars/uLandPainted.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uLandPainted.pas Thu Dec 13 10:51:07 2018 -0500 @@ -58,6 +58,7 @@ rec:= prec^; rec.X:= SDLNet_Read16(@rec.X); rec.Y:= SDLNet_Read16(@rec.Y); + if rec.X < -318 then rec.X:= -318; if rec.X > 4096+318 then rec.X:= 4096+318; if rec.Y < -318 then rec.Y:= -318; @@ -81,7 +82,7 @@ var pe: PPointEntry; prevPoint: PointRec; radius: LongInt; - color: Longword; + color, Xoffset, Yoffset: Longword; lineNumber, linePoints: Longword; begin // shutup compiler @@ -89,6 +90,8 @@ prevPoint.Y:= 0; radius:= 0; linePoints:= 0; + Xoffset:= (LAND_WIDTH-(4096*max(min(cFeatureSize,24),3) div 12)) div 2; + Yoffset:= (LAND_HEIGHT-(2048*max(min(cFeatureSize,24),3) div 12)); pe:= pointsListHead; while (pe <> nil) and (pe^.point.flags and $80 = 0) do @@ -101,6 +104,8 @@ while(pe <> nil) do begin + pe^.point.X:= (LongInt(pe^.point.X) * max(min(cFeatureSize,24),3)) div 12 + Xoffset; + pe^.point.Y:= (LongInt(pe^.point.Y) * max(min(cFeatureSize,24),3)) div 12 + Yoffset; if (pe^.point.flags and $80 <> 0) then begin if (lineNumber > 0) and (linePoints = 0) and cAdvancedMapGenMode then @@ -113,9 +118,10 @@ else color:= lfBasic; radius:= (pe^.point.flags and $3F) * 5 + 3; + radius:= (radius * max(min(cFeatureSize,24),3)) div 12; linePoints:= FillRoundInLand(pe^.point.X, pe^.point.Y, radius, color); end - else + else begin inc(linePoints, DrawThickLine(prevPoint.X, prevPoint.Y, pe^.point.X, pe^.point.Y, radius, color)); end; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uScript.pas --- a/hedgewars/uScript.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uScript.pas Thu Dec 13 10:51:07 2018 -0500 @@ -686,11 +686,20 @@ function lc_spawnfakehealthcrate(L: Plua_State) : LongInt; Cdecl; var gear: PGear; -begin - if CheckLuaParamCount(L, 4,'SpawnFakeHealthCrate', 'x, y, explode, poison') then + explode, poison: boolean; + n: LongInt; +begin + if CheckAndFetchParamCountRange(L, 2, 4, 'SpawnFakeHealthCrate', 'x, y [, explode [, poison]]', n) then begin + explode:= false; + poison:= false; + if (n >= 3) and (not lua_isnil(L, 3)) then + explode:= lua_toboolean(L, 3); + if (n = 4) and (not lua_isnil(L, 4)) then + poison:= lua_toboolean(L, 4); + gear := SpawnFakeCrateAt(Trunc(lua_tonumber(L, 1)), Trunc(lua_tonumber(L, 2)), - HealthCrate, lua_toboolean(L, 3), lua_toboolean(L, 4)); + HealthCrate, explode, poison); if gear <> nil then lua_pushnumber(L, gear^.uid) else lua_pushnil(L) @@ -702,11 +711,20 @@ function lc_spawnfakeammocrate(L: PLua_State): LongInt; Cdecl; var gear: PGear; -begin - if CheckLuaParamCount(L, 4,'SpawnFakeAmmoCrate', 'x, y, explode, poison') then + explode, poison: boolean; + n: LongInt; +begin + if CheckAndFetchParamCountRange(L, 2, 4, 'SpawnFakeAmmoCrate', 'x, y [, explode [, poison]]', n) then begin + explode:= false; + poison:= false; + if (n >= 3) and (not lua_isnil(L, 3)) then + explode:= lua_toboolean(L, 3); + if (n = 4) and (not lua_isnil(L, 4)) then + poison:= lua_toboolean(L, 4); + gear := SpawnFakeCrateAt(Trunc(lua_tonumber(L, 1)), Trunc(lua_tonumber(L, 2)), - AmmoCrate, lua_toboolean(L, 3), lua_toboolean(L, 4)); + AmmoCrate, explode, poison); if gear <> nil then lua_pushnumber(L, gear^.uid) else lua_pushnil(L) @@ -718,11 +736,20 @@ function lc_spawnfakeutilitycrate(L: PLua_State): LongInt; Cdecl; var gear: PGear; -begin - if CheckLuaParamCount(L, 4,'SpawnFakeUtilityCrate', 'x, y, explode, poison') then + explode, poison: boolean; + n: LongInt; +begin + if CheckAndFetchParamCountRange(L, 2, 4, 'SpawnFakeUtilityCrate', 'x, y [, explode [, poison]]', n) then begin + explode:= false; + poison:= false; + if (n >= 3) and (not lua_isnil(L, 3)) then + explode:= lua_toboolean(L, 3); + if (n = 4) and (not lua_isnil(L, 4)) then + poison:= lua_toboolean(L, 4); + gear := SpawnFakeCrateAt(Trunc(lua_tonumber(L, 1)), Trunc(lua_tonumber(L, 2)), - UtilityCrate, lua_toboolean(L, 3), lua_toboolean(L, 4)); + UtilityCrate, explode, poison); if gear <> nil then lua_pushnumber(L, gear^.uid) else lua_pushnil(L) @@ -3276,6 +3303,22 @@ lc_setreadytimeleft:= 0; end; +function lc_setturntimepaused(L : Plua_State) : LongInt; Cdecl; +begin + if CheckLuaParamCount(L, 1, 'SetTurnTimePaused', 'isPaused') then + LuaClockPaused:= lua_toboolean(L, 1); + lc_setturntimepaused:= 0; +end; + +function lc_getturntimepaused(L : Plua_State) : LongInt; Cdecl; +begin + if CheckLuaParamCount(L, 0, 'GetTurnTimePaused', '') then + lua_pushboolean(L, LuaClockPaused) + else + lua_pushnil(L); + lc_getturntimepaused:= 1; +end; + function lc_startghostpoints(L : Plua_State) : LongInt; Cdecl; begin if CheckLuaParamCount(L, 1, 'StartGhostPoints', 'count') then @@ -4310,6 +4353,8 @@ lua_register(luaState, _P'Explode', @lc_explode); lua_register(luaState, _P'SetTurnTimeLeft', @lc_setturntimeleft); lua_register(luaState, _P'SetReadyTimeLeft', @lc_setreadytimeleft); +lua_register(luaState, _P'SetTurnTimePaused', @lc_setturntimepaused); +lua_register(luaState, _P'GetTurnTimePaused', @lc_getturntimepaused); // drawn map functions lua_register(luaState, _P'AddPoint', @lc_addPoint); lua_register(luaState, _P'FlushPoints', @lc_flushPoints); diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uSound.pas --- a/hedgewars/uSound.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uSound.pas Thu Dec 13 10:51:07 2018 -0500 @@ -490,11 +490,11 @@ GetFallbackV := sndUhOh else if (snd in [sndDrat, sndBugger]) then GetFallbackV := sndStupid - else if (snd in [sndGonnaGetYou, sndCutItOut, sndLeaveMeAlone]) then + else if (snd in [sndGonnaGetYou, sndIllGetYou, sndJustYouWait, sndCutItOut, sndLeaveMeAlone]) then GetFallbackV := sndRegret else if (snd in [sndOhDear, sndSoLong]) then GetFallbackV := sndByeBye - else if (snd = sndWhatThe) then + else if (snd in [sndWhatThe, sndUhOh]) then GetFallbackV := sndNooo else if (snd = sndRunAway) then GetFallbackV := sndOops @@ -502,14 +502,19 @@ GetFallbackV := sndReinforce else if (snd in [sndAmazing, sndBrilliant, sndExcellent]) then GetFallbackV := sndEnemyDown - // Hmm is for enemy turn start - else if snd = sndHmm then - // these are not ideal fallbacks, but those were the voices which were used in older versions - // for enemy turn start - if random(2) = 0 then - GetFallbackV := sndIllGetYou - else - GetFallbackV := sndJustYouWait + else if (snd = sndPoisonCough) then + GetFallbackV := sndPoisonMoan + else if (snd = sndPoisonMoan) then + GetFallbackV := sndPoisonCough + else if (snd = sndFlawless) then + GetFallbackV := sndVictory + else if (snd = sndSameTeam) then + GetFallbackV := sndTraitor + else if (snd = sndMelon) then + GetFallbackV := sndCover + // sndHmm is used for enemy turn start, so sndHello is an "okay" replacement + else if (snd = sndHmm) then + GetFallbackV := sndHello else GetFallbackV := sndNone; end; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uStats.pas --- a/hedgewars/uStats.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uStats.pas Thu Dec 13 10:51:07 2018 -0500 @@ -67,6 +67,7 @@ HitTargets : LongWord = 0; // Target (gtTarget) hits per turn AmmoUsedCount : Longword = 0; AmmoDamagingUsed : boolean = false; + FirstBlood : boolean = false; LeaveMeAlone : boolean = false; SkippedTurns: LongWord = 0; isTurnSkipped: boolean = false; @@ -254,8 +255,11 @@ killsCheck:= 0; // First blood (first damage, poison or kill) - if ((DamageTotal > 0) or (KillsTotal > 0) or (PoisonTotal > 0)) and ((CurrentHedgehog^.stats.DamageGiven = DamageTotal) and (CurrentHedgehog^.stats.StepKills = KillsTotal) and (PoisonTotal = PoisonTurn + PoisonClan)) then - AddVoice(sndFirstBlood, CurrentTeam^.voicepack) + if (not FirstBlood) and ((DamageTotal > 0) or (KillsTotal > 0) or (PoisonTotal > 0)) and ((CurrentHedgehog^.stats.DamageGiven = DamageTotal) and (CurrentHedgehog^.stats.StepKills = KillsTotal) and (PoisonTotal = PoisonTurn + PoisonClan)) then + begin + FirstBlood:= true; + AddVoice(sndFirstBlood, CurrentTeam^.voicepack); + end // Hog hurts, poisons or kills itself (except sacrifice) else if (CurrentHedgehog^.stats.Sacrificed = false) and ((CurrentHedgehog^.stats.StepDamageRecv > 0) or (CurrentHedgehog^.stats.StepPoisoned) or (CurrentHedgehog^.stats.StepDied)) then @@ -598,6 +602,7 @@ HitTargets := 0; AmmoUsedCount := 0; AmmoDamagingUsed := false; + FirstBlood:= false; LeaveMeAlone := false; SkippedTurns:= 0; isTurnSkipped:= false; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uTeams.pas --- a/hedgewars/uTeams.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uTeams.pas Thu Dec 13 10:51:07 2018 -0500 @@ -560,6 +560,7 @@ // Some initial King buffs if (GameFlags and gfKing) <> 0 then begin + hasKing:= true; Hedgehogs[0].King:= true; Hedgehogs[0].Hat:= 'crown'; Hedgehogs[0].Effects[hePoisoned] := 0; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uTypes.pas --- a/hedgewars/uTypes.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uTypes.pas Thu Dec 13 10:51:07 2018 -0500 @@ -444,6 +444,7 @@ voicepack: PVoicepack; PlayerHash: shortstring; // md5 hash of player name. For temporary enabling of hats as thank you. Hashed for privacy of players stats: TTeamStats; + hasKing: boolean; // true if team has a living king hasGone: boolean; skippedTurns: Longword; isGoneFlagPendingToBeSet, isGoneFlagPendingToBeUnset: boolean; @@ -511,7 +512,8 @@ sidCmdHeaderTaunts, sidCmdSpeech, sidCmdThink, sidCmdYell, sidCmdSpeechNumberHint, sidCmdHsa, sidCmdHta, sidCmdHya, sidCmdHurrah, sidCmdIlovelotsoflemonade, sidCmdJuggle, - sidCmdRollup, sidCmdShrug, sidCmdWave, sidCmdUnknown); + sidCmdRollup, sidCmdShrug, sidCmdWave, sidCmdUnknown, + sidCmdHelpRoom, sidCmdHelpRoomFail); // Events that are important for the course of the game or at least interesting for other reasons TEventId = (eidDied, eidDrowned, eidRoundStart, eidRoundWin, eidRoundDraw, diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uVariables.pas --- a/hedgewars/uVariables.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uVariables.pas Thu Dec 13 10:51:07 2018 -0500 @@ -104,6 +104,7 @@ IsGetAwayTime : boolean; GameOver : boolean; cSuddenDTurns : LongInt; + LastSuddenDWarn : LongInt; // last round in which the last SD warning appeared. -2 = no warning so far cDamagePercent : LongInt; cMineDudPercent : LongWord; cTemplateFilter : LongInt; @@ -261,6 +262,9 @@ LuaEndTurnRequested: boolean; LuaNoEndTurnTaunts: boolean; + // whether Lua requested to pause the clock + LuaClockPaused: boolean; + MaskedSounds : array[TSound] of boolean; LastVoice : TVoice; @@ -2798,12 +2802,13 @@ TurnClockActive := true; TagTurnTimeLeft := 0; cSuddenDTurns := 15; + LastSuddenDWarn := -2; cDamagePercent := 100; cRopePercent := 100; cGetAwayTime := 100; cMineDudPercent := 0; cTemplateFilter := 0; - cFeatureSize := 50; + cFeatureSize := 12; cMapGen := mgRandom; cHedgehogTurnTime := 45000; cMinesTime := 3000; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 hedgewars/uWorld.pas --- a/hedgewars/uWorld.pas Thu Dec 13 10:49:30 2018 -0500 +++ b/hedgewars/uWorld.pas Thu Dec 13 10:51:07 2018 -0500 @@ -63,6 +63,7 @@ , uCommands , uTeams , uDebug + , uInputHandler {$IFDEF USE_VIDEO_RECORDING} , uVideoRec {$ENDIF} @@ -415,7 +416,10 @@ STurns: LongInt; amSurface: PSDL_Surface; AMRect: TSDL_Rect; -{$IFDEF USE_AM_NUMCOLUMN}tmpsurf: PSDL_Surface;{$ENDIF} +{$IFDEF USE_AM_NUMCOLUMN} + tmpsurf: PSDL_Surface; + usesDefaultSlotKeys: boolean; +{$ENDIF} begin if cOnlyStats then exit(nil); @@ -451,6 +455,9 @@ x:= AMRect.x; y:= AMRect.y; +{$IFDEF USE_AM_NUMCOLUMN} + usesDefaultSlotKeys:= CheckDefaultSlotKeys; +{$ENDIF USE_AM_NUMCOLUMN} for i:= 0 to cMaxSlotIndex do if (i <> cHiddenSlotIndex) and (Ammo^[i, 0].Count > 0) then begin @@ -460,7 +467,13 @@ x:= AMRect.x; {$ENDIF} {$IFDEF USE_AM_NUMCOLUMN} - tmpsurf:= TTF_RenderUTF8_Blended(Fontz[fnt16].Handle, Str2PChar('F' + IntToStr(i+1)), cWhiteColorChannels); + // Ammo slot number column + if usesDefaultSlotKeys then + // F1, F2, F3, F4, ... + tmpsurf:= TTF_RenderUTF8_Blended(Fontz[fnt16].Handle, Str2PChar('F'+IntToStr(i+1)), cWhiteColorChannels) + else + // 1, 2, 3, 4, ... + tmpsurf:= TTF_RenderUTF8_Blended(Fontz[fnt16].Handle, Str2PChar(IntToStr(i+1)), cWhiteColorChannels); copyToXY(tmpsurf, amSurface, x + AMSlotPadding + (AMSlotSize shr 1) - (tmpsurf^.w shr 1), y + AMSlotPadding + (AMSlotSize shr 1) - (tmpsurf^.h shr 1)); @@ -600,12 +613,14 @@ if AMState = AMShowingUp then // show ammo menu begin - if (cReducedQuality and rqSlowMenu) <> 0 then + // No "appear" animation in low quality or playing with very short turn time. + if ((cReducedQuality and rqSlowMenu) <> 0) or (cHedgehogTurnTime <= 10000) then begin AMShiftX:= 0; AMShiftY:= 0; AMState:= AMShowing; end + // "Appear" animation else if AMAnimState < 1 then begin @@ -625,12 +640,14 @@ end; if AMState = AMHiding then // hide ammo menu begin - if (cReducedQuality and rqSlowMenu) <> 0 then + // No "disappear" animation (see above) + if ((cReducedQuality and rqSlowMenu) <> 0) or (cHedgehogTurnTime <= 10000) then begin AMShiftX:= AMShiftTargetX; AMShiftY:= AMShiftTargetY; AMState:= AMHidden; end + // "Disappear" animation else if AMAnimState < 1 then begin @@ -1146,7 +1163,9 @@ h:= -NameTagTex^.w - 24; if OwnerTex <> nil then h:= h - OwnerTex^.w - 4; + Tint(TeamsArray[t]^.Clan^.Color shl 8 or $FF); DrawSpriteRotatedF(sprFinger, h, cScreenHeight + DrawHealthY + smallScreenOffset + 2 + SpritesData[sprFinger].Width div 4, 0, 1, -90); + untint; end; end; end; @@ -1198,7 +1217,7 @@ r: TSDL_Rect; s: shortstring; offsetX, offsetY, screenBottom: LongInt; - replicateToLeft, replicateToRight, tmp: boolean; + replicateToLeft, replicateToRight, tmp, isNotHiddenByCinematic: boolean; {$IFDEF USE_VIDEO_RECORDING} a: Byte; {$ENDIF} @@ -1400,7 +1419,10 @@ if CurAmmoType = amBee then spr:= sprTargetBee else + begin spr:= sprTargetP; + Tint(Team^.Clan^.Color shl 8 or $FF); + end; if replicateToLeft then begin ShiftWorld(-1); @@ -1416,6 +1438,8 @@ end; DrawSpriteRotatedF(spr, TargetPoint.X + WorldDx, TargetPoint.Y + WorldDy, 0, 0, (RealTicks shr 3) mod 360); + if spr = sprTargetP then + untint; end; end; @@ -1442,7 +1466,8 @@ // This scale is used to keep the various widgets at the same dimension at all zoom levels SetScale(cDefaultZoomLevel); -// Cinematic Mode: Effects +isNotHiddenByCinematic:= true; +// Cinematic Mode: Determine effects and state if CinematicScript or (InCinematicMode and autoCameraOn and ((CurrentHedgehog = nil) or CurrentHedgehog^.Team^.ExtDriven or (CurrentHedgehog^.BotLevel <> 0) or (GameType = gmtDemo))) then @@ -1451,7 +1476,10 @@ begin inc(CinematicSteps, Lag); if CinematicSteps > 300 then - CinematicSteps:= 300; + begin + CinematicSteps:= 300; + isNotHiddenByCinematic:= false; + end; end; end else if CinematicSteps > 0 then @@ -1461,22 +1489,8 @@ CinematicSteps:= 0; end; -// Cinematic Mode: Render black bars -if CinematicSteps > 0 then - begin - r.x:= ViewLeftX; - r.w:= ViewWidth; - r.y:= ViewTopY; - CinematicBarH:= (ViewHeight * CinematicSteps) div 2048; - r.h:= CinematicBarH; - DrawRect(r, 0, 0, 0, $FF, true); - r.y:= ViewBottomY - r.h; - DrawRect(r, 0, 0, 0, $FF, true); - end; - - // Turn time -if UIDisplay <> uiNone then +if (UIDisplay <> uiNone) and (isNotHiddenByCinematic) then begin {$IFDEF USE_TOUCH_INTERFACE} offsetX:= cScreenHeight - 13; @@ -1515,34 +1529,14 @@ DrawSprite(sprFrame, -(cScreenWidth shr 1) + t - 4 + offsetY, cScreenHeight - offsetX, 0); end; -// Captions - DrawCaptions end; -{$IFDEF USE_TOUCH_INTERFACE} -// Draw buttons Related to the Touch interface -DrawScreenWidget(@arrowLeft); -DrawScreenWidget(@arrowRight); -DrawScreenWidget(@arrowUp); -DrawScreenWidget(@arrowDown); - -DrawScreenWidget(@fireButton); -DrawScreenWidget(@jumpWidget); -DrawScreenWidget(@AMWidget); -DrawScreenWidget(@pauseButton); -DrawScreenWidget(@utilityWidget); -{$ENDIF} - // Team bars -if UIDisplay = uiAll then +if (UIDisplay = uiAll) and (isNotHiddenByCinematic) then RenderTeamsHealth; -// Lag alert -if isInLag then - DrawSprite(sprLag, 32 - (cScreenWidth shr 1), 32, (RealTicks shr 7) mod 12); - // Wind bar -if UIDisplay <> uiNone then +if (UIDisplay <> uiNone) and (isNotHiddenByCinematic) then begin {$IFDEF USE_TOUCH_INTERFACE} offsetX:= cScreenHeight - 13; @@ -1576,7 +1570,7 @@ end; // Indicators for global effects (extra damage, low gravity) -if UIDisplay <> uiNone then +if (UIDisplay <> uiNone) and (isNotHiddenByCinematic) then begin {$IFDEF USE_TOUCH_INTERFACE} offsetX:= (cScreenWidth shr 1) - 95; @@ -1603,6 +1597,41 @@ end; end; +// Cinematic Mode: Render black bars +if CinematicSteps > 0 then + begin + r.x:= ViewLeftX; + r.w:= ViewWidth; + r.y:= ViewTopY; + CinematicBarH:= (ViewHeight * CinematicSteps) div 2048; + r.h:= CinematicBarH; + DrawRect(r, 0, 0, 0, $FF, true); + r.y:= ViewBottomY - r.h; + DrawRect(r, 0, 0, 0, $FF, true); + end; + +// Touchscreen interface widgets +{$IFDEF USE_TOUCH_INTERFACE} +DrawScreenWidget(@arrowLeft); +DrawScreenWidget(@arrowRight); +DrawScreenWidget(@arrowUp); +DrawScreenWidget(@arrowDown); + +DrawScreenWidget(@fireButton); +DrawScreenWidget(@jumpWidget); +DrawScreenWidget(@AMWidget); +DrawScreenWidget(@utilityWidget); +DrawScreenWidget(@pauseButton); +{$ENDIF} + +// Captions +if UIDisplay <> uiNone then + DrawCaptions; + +// Lag alert +if isInLag then + DrawSprite(sprLag, 32 - (cScreenWidth shr 1), 32, (RealTicks shr 7) mod 12); + // Chat DrawChat; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/CMakeLists.txt --- a/qmlfrontend/CMakeLists.txt Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/CMakeLists.txt Thu Dec 13 10:51:07 2018 -0500 @@ -15,6 +15,8 @@ "team.cpp" "team.h" "engine_instance.cpp" "engine_instance.h" "preview_image_provider.cpp" "preview_image_provider.h" - "engine_interface.h") + "engine_interface.h" + "preview_acceptor.cpp" "preview_acceptor.h" + ) target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Quick) diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/Page1.qml --- a/qmlfrontend/Page1.qml Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/Page1.qml Thu Dec 13 10:51:07 2018 -0500 @@ -2,24 +2,31 @@ import Hedgewars.Engine 1.0 Page1Form { + property var hwEngine + + Component { + id: hwEngineComponent + + HWEngine { + engineLibrary: "./libhedgewars_engine.so" + previewAcceptor: PreviewAcceptor + onPreviewImageChanged: previewImage.source = "image://preview/image" + onPreviewIsRendering: previewImage.source = "qrc:/res/iconTime.png" + } + } + + Component.onCompleted: { + hwEngine = hwEngineComponent.createObject() + } + tickButton.onClicked: { gameView.tick(100) } gameButton.onClicked: { - var engineInstance = HWEngine.runQuickGame() + var engineInstance = hwEngine.runQuickGame() gameView.engineInstance = engineInstance } button1.onClicked: { - HWEngine.getPreview() - } - - Connections { - target: HWEngine - onPreviewImageChanged: { - previewImage.source = "image://preview/image" - } - onPreviewIsRendering: { - previewImage.source = "qrc:/res/iconTime.png" - } + hwEngine.getPreview() } } diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/engine_instance.cpp --- a/qmlfrontend/engine_instance.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/engine_instance.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -1,6 +1,7 @@ #include "engine_instance.h" #include +#include #include #include @@ -12,37 +13,92 @@ return currentOpenglContext->getProcAddress(fn); } -EngineInstance::EngineInstance(QObject* parent) - : QObject(parent), m_instance(Engine::start_engine()) {} +EngineInstance::EngineInstance(const QString& libraryPath, QObject* parent) + : QObject(parent) { + QLibrary hwlib(libraryPath); + + if (!hwlib.load()) + qWarning() << "Engine library not found" << hwlib.errorString(); + + hedgewars_engine_protocol_version = + reinterpret_cast( + hwlib.resolve("hedgewars_engine_protocol_version")); + start_engine = + reinterpret_cast(hwlib.resolve("start_engine")); + generate_preview = reinterpret_cast( + hwlib.resolve("generate_preview")); + dispose_preview = reinterpret_cast( + hwlib.resolve("dispose_preview")); + cleanup = reinterpret_cast(hwlib.resolve("cleanup")); + + send_ipc = reinterpret_cast(hwlib.resolve("send_ipc")); + read_ipc = reinterpret_cast(hwlib.resolve("read_ipc")); -EngineInstance::~EngineInstance() { Engine::cleanup(m_instance); } + setup_current_gl_context = + reinterpret_cast( + hwlib.resolve("setup_current_gl_context")); + render_frame = + reinterpret_cast(hwlib.resolve("render_frame")); + advance_simulation = reinterpret_cast( + hwlib.resolve("advance_simulation")); + + m_isValid = hedgewars_engine_protocol_version && start_engine && + generate_preview && dispose_preview && cleanup && send_ipc && + read_ipc && setup_current_gl_context && render_frame && + advance_simulation; + emit isValidChanged(m_isValid); + + if (isValid()) { + qDebug() << "Loaded engine library with protocol version" + << hedgewars_engine_protocol_version(); + + m_instance = start_engine(); + } +} + +EngineInstance::~EngineInstance() { + if (m_isValid) cleanup(m_instance); +} void EngineInstance::sendConfig(const GameConfig& config) { for (auto b : config.config()) { - Engine::send_ipc(m_instance, reinterpret_cast(b.data()), - static_cast(b.size())); + send_ipc(m_instance, reinterpret_cast(b.data()), + static_cast(b.size())); } } void EngineInstance::advance(quint32 ticks) { - Engine::advance_simulation(m_instance, ticks); + advance_simulation(m_instance, ticks); } -void EngineInstance::renderFrame() { Engine::render_frame(m_instance); } +void EngineInstance::renderFrame() { render_frame(m_instance); } void EngineInstance::setOpenGLContext(QOpenGLContext* context) { currentOpenglContext = context; auto size = context->surface()->size(); - Engine::setup_current_gl_context( - m_instance, static_cast(size.width()), - static_cast(size.height()), &getProcAddress); + setup_current_gl_context(m_instance, static_cast(size.width()), + static_cast(size.height()), + &getProcAddress); } -Engine::PreviewInfo EngineInstance::generatePreview() { +QImage EngineInstance::generatePreview() { Engine::PreviewInfo pinfo; - Engine::generate_preview(m_instance, &pinfo); + generate_preview(m_instance, &pinfo); + + QVector colorTable; + colorTable.resize(256); + for (int i = 0; i < 256; ++i) colorTable[i] = qRgba(255, 255, 0, i); - return pinfo; + QImage previewImage(pinfo.land, static_cast(pinfo.width), + static_cast(pinfo.height), QImage::Format_Indexed8); + previewImage.setColorTable(colorTable); + + // Cannot use it here, since QImage refers to original bytes + // dispose_preview(m_instance); + + return previewImage; } + +bool EngineInstance::isValid() const { return m_isValid; } diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/engine_instance.h --- a/qmlfrontend/engine_instance.h Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/engine_instance.h Thu Dec 13 10:51:07 2018 -0500 @@ -1,31 +1,50 @@ #ifndef ENGINEINSTANCE_H #define ENGINEINSTANCE_H -#include "engine_interface.h" - +#include #include #include +#include "engine_interface.h" #include "game_config.h" class EngineInstance : public QObject { Q_OBJECT public: - explicit EngineInstance(QObject* parent = nullptr); + explicit EngineInstance(const QString& libraryPath, + QObject* parent = nullptr); ~EngineInstance(); + Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged) + void sendConfig(const GameConfig& config); void advance(quint32 ticks); void renderFrame(); void setOpenGLContext(QOpenGLContext* context); - Engine::PreviewInfo generatePreview(); + QImage generatePreview(); + + bool isValid() const; signals: + void isValidChanged(bool isValid); public slots: private: Engine::EngineInstance* m_instance; + + Engine::hedgewars_engine_protocol_version_t* + hedgewars_engine_protocol_version; + Engine::start_engine_t* start_engine; + Engine::generate_preview_t* generate_preview; + Engine::dispose_preview_t* dispose_preview; + Engine::cleanup_t* cleanup; + Engine::send_ipc_t* send_ipc; + Engine::read_ipc_t* read_ipc; + Engine::setup_current_gl_context_t* setup_current_gl_context; + Engine::render_frame_t* render_frame; + Engine::advance_simulation_t* advance_simulation; + bool m_isValid; }; #endif // ENGINEINSTANCE_H diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/engine_interface.h --- a/qmlfrontend/engine_interface.h Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/engine_interface.h Thu Dec 13 10:51:07 2018 -0500 @@ -18,10 +18,11 @@ unsigned char* land; } PreviewInfo; -typedef uint32_t protocol_version_t(); +typedef uint32_t hedgewars_engine_protocol_version_t(); typedef EngineInstance* start_engine_t(); typedef void generate_preview_t(EngineInstance* engine_state, PreviewInfo* preview); +typedef void dispose_preview_t(EngineInstance* engine_state); typedef void cleanup_t(EngineInstance* engine_state); typedef void send_ipc_t(EngineInstance* engine_state, uint8_t* buf, @@ -36,18 +37,6 @@ typedef bool advance_simulation_t(EngineInstance* engine_state, uint32_t ticks); -extern protocol_version_t* protocol_version; -extern start_engine_t* start_engine; -extern generate_preview_t* generate_preview; -extern cleanup_t* cleanup; - -extern send_ipc_t* send_ipc; -extern read_ipc_t* read_ipc; - -extern setup_current_gl_context_t* setup_current_gl_context; -extern render_frame_t* render_frame; -extern advance_simulation_t* advance_simulation; - #ifdef __cplusplus } }; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/hwengine.cpp --- a/qmlfrontend/hwengine.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/hwengine.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -1,64 +1,33 @@ +#include "hwengine.h" + #include -#include -#include +#include #include #include "engine_instance.h" #include "engine_interface.h" #include "game_view.h" -#include "preview_image_provider.h" - -#include "hwengine.h" +#include "preview_acceptor.h" -HWEngine::HWEngine(QQmlEngine* engine, QObject* parent) - : QObject(parent), - m_engine(engine), - m_previewProvider(new PreviewImageProvider()) { - m_engine->addImageProvider(QLatin1String("preview"), m_previewProvider); -} +HWEngine::HWEngine(QObject* parent) : QObject(parent) {} HWEngine::~HWEngine() {} -static QObject* hwengine_singletontype_provider(QQmlEngine* engine, - QJSEngine* scriptEngine) { - Q_UNUSED(scriptEngine) - - HWEngine* hwengine = new HWEngine(engine); - return hwengine; -} - -void HWEngine::exposeToQML() { - qDebug("HWEngine::exposeToQML"); - qmlRegisterSingletonType("Hedgewars.Engine", 1, 0, "HWEngine", - hwengine_singletontype_provider); - qmlRegisterType("Hedgewars.Engine", 1, 0, "GameView"); - qmlRegisterUncreatableType("Hedgewars.Engine", 1, 0, - "EngineInstance", - "Create by HWEngine run methods"); -} - void HWEngine::getPreview() { emit previewIsRendering(); m_gameConfig = GameConfig(); m_gameConfig.cmdSeed(QUuid::createUuid().toByteArray()); - EngineInstance engine; + EngineInstance engine(m_engineLibrary); + if (!engine.isValid()) // TODO: error notification + return; + engine.sendConfig(m_gameConfig); - Engine::PreviewInfo preview = engine.generatePreview(); - - QVector colorTable; - colorTable.resize(256); - for (int i = 0; i < 256; ++i) colorTable[i] = qRgba(255, 255, 0, i); + QImage previewImage = engine.generatePreview(); - QImage previewImage(preview.land, static_cast(preview.width), - static_cast(preview.height), - QImage::Format_Indexed8); - previewImage.setColorTable(colorTable); - previewImage.detach(); - - m_previewProvider->setImage(previewImage); + if (m_previewAcceptor) m_previewAcceptor->setImage(previewImage); emit previewImageChanged(); // m_runQueue->queue(m_gameConfig); @@ -74,9 +43,28 @@ m_gameConfig.cmdTeam(team1); m_gameConfig.cmdTeam(team2); - EngineInstance* engine = new EngineInstance(this); + EngineInstance* engine = new EngineInstance(m_engineLibrary, this); + return engine; // m_runQueue->queue(m_gameConfig); } int HWEngine::previewHedgehogsCount() const { return m_previewHedgehogsCount; } + +PreviewAcceptor* HWEngine::previewAcceptor() const { return m_previewAcceptor; } + +QString HWEngine::engineLibrary() const { return m_engineLibrary; } + +void HWEngine::setPreviewAcceptor(PreviewAcceptor* previewAcceptor) { + if (m_previewAcceptor == previewAcceptor) return; + + m_previewAcceptor = previewAcceptor; + emit previewAcceptorChanged(m_previewAcceptor); +} + +void HWEngine::setEngineLibrary(const QString& engineLibrary) { + if (m_engineLibrary == engineLibrary) return; + + m_engineLibrary = engineLibrary; + emit engineLibraryChanged(m_engineLibrary); +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/hwengine.h --- a/qmlfrontend/hwengine.h Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/hwengine.h Thu Dec 13 10:51:07 2018 -0500 @@ -8,25 +8,33 @@ #include "game_config.h" class QQmlEngine; -class PreviewImageProvider; class EngineInstance; +class PreviewAcceptor; class HWEngine : public QObject { Q_OBJECT Q_PROPERTY(int previewHedgehogsCount READ previewHedgehogsCount NOTIFY previewHedgehogsCountChanged) + Q_PROPERTY(PreviewAcceptor* previewAcceptor READ previewAcceptor WRITE + setPreviewAcceptor NOTIFY previewAcceptorChanged) + Q_PROPERTY(QString engineLibrary READ engineLibrary WRITE setEngineLibrary + NOTIFY engineLibraryChanged) public: - explicit HWEngine(QQmlEngine* engine, QObject* parent = nullptr); + explicit HWEngine(QObject* parent = nullptr); ~HWEngine(); - static void exposeToQML(); - Q_INVOKABLE void getPreview(); Q_INVOKABLE EngineInstance* runQuickGame(); int previewHedgehogsCount() const; + PreviewAcceptor* previewAcceptor() const; + QString engineLibrary() const; + + public slots: + void setPreviewAcceptor(PreviewAcceptor* previewAcceptor); + void setEngineLibrary(const QString& engineLibrary); signals: void previewIsRendering(); @@ -34,12 +42,15 @@ void previewHogCountChanged(int count); void gameFinished(); void previewHedgehogsCountChanged(int previewHedgehogsCount); + void previewAcceptorChanged(PreviewAcceptor* previewAcceptor); + void engineLibraryChanged(const QString& engineLibrary); private: QQmlEngine* m_engine; - PreviewImageProvider* m_previewProvider; GameConfig m_gameConfig; int m_previewHedgehogsCount; + PreviewAcceptor* m_previewAcceptor; + QString m_engineLibrary; }; #endif // HWENGINE_H diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/main.cpp --- a/qmlfrontend/main.cpp Thu Dec 13 10:49:30 2018 -0500 +++ b/qmlfrontend/main.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -4,66 +4,34 @@ #include #include "engine_interface.h" +#include "game_view.h" #include "hwengine.h" +#include "preview_acceptor.h" -namespace Engine { -protocol_version_t* protocol_version; -start_engine_t* start_engine; -generate_preview_t* generate_preview; -cleanup_t* cleanup; -send_ipc_t* send_ipc; -read_ipc_t* read_ipc; -setup_current_gl_context_t* setup_current_gl_context; -render_frame_t* render_frame; -advance_simulation_t* advance_simulation; -}; // namespace Engine - -void loadEngineLibrary() { -#ifdef Q_OS_WIN - QLibrary hwlib("./libhedgewars_engine.dll"); -#else - QLibrary hwlib("./libhedgewars_engine.so"); -#endif - - if (!hwlib.load()) - qWarning() << "Engine library not found" << hwlib.errorString(); +namespace Engine {}; // namespace Engine - Engine::protocol_version = reinterpret_cast( - hwlib.resolve("protocol_version")); - Engine::start_engine = - reinterpret_cast(hwlib.resolve("start_engine")); - Engine::generate_preview = reinterpret_cast( - hwlib.resolve("generate_preview")); - Engine::cleanup = - reinterpret_cast(hwlib.resolve("cleanup")); +static QObject* previewacceptor_singletontype_provider( + QQmlEngine* engine, QJSEngine* scriptEngine) { + Q_UNUSED(scriptEngine) - Engine::send_ipc = - reinterpret_cast(hwlib.resolve("send_ipc")); - Engine::read_ipc = - reinterpret_cast(hwlib.resolve("read_ipc")); - - Engine::setup_current_gl_context = - reinterpret_cast( - hwlib.resolve("setup_current_gl_context")); - Engine::render_frame = - reinterpret_cast(hwlib.resolve("render_frame")); - Engine::advance_simulation = reinterpret_cast( - hwlib.resolve("advance_simulation")); - - if (Engine::protocol_version) - qDebug() << "Loaded engine library with protocol version" - << Engine::protocol_version(); + PreviewAcceptor* acceptor = new PreviewAcceptor(engine); + return acceptor; } int main(int argc, char* argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); - loadEngineLibrary(); - QQmlApplicationEngine engine; - HWEngine::exposeToQML(); + qmlRegisterSingletonType( + "Hedgewars.Engine", 1, 0, "PreviewAcceptor", + previewacceptor_singletontype_provider); + qmlRegisterType("Hedgewars.Engine", 1, 0, "HWEngine"); + qmlRegisterType("Hedgewars.Engine", 1, 0, "GameView"); + qmlRegisterUncreatableType("Hedgewars.Engine", 1, 0, + "EngineInstance", + "Create by HWEngine run methods"); engine.load(QUrl(QLatin1String("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/preview_acceptor.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmlfrontend/preview_acceptor.cpp Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,15 @@ +#include "preview_acceptor.h" + +#include +#include + +#include "preview_image_provider.h" + +PreviewAcceptor::PreviewAcceptor(QQmlEngine *engine, QObject *parent) + : QObject(parent), m_previewProvider(new PreviewImageProvider()) { + engine->addImageProvider(QLatin1String("preview"), m_previewProvider); +} + +void PreviewAcceptor::setImage(const QImage &preview) { + m_previewProvider->setImage(preview); +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 qmlfrontend/preview_acceptor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmlfrontend/preview_acceptor.h Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,21 @@ +#ifndef PREVIEW_ACCEPTOR_H +#define PREVIEW_ACCEPTOR_H + +#include + +class QQmlEngine; +class PreviewImageProvider; + +class PreviewAcceptor : public QObject { + Q_OBJECT + public: + explicit PreviewAcceptor(QQmlEngine *engine, QObject *parent = nullptr); + + public slots: + void setImage(const QImage &preview); + + private: + PreviewImageProvider *m_previewProvider; +}; + +#endif // PREVIEW_ACCEPTOR_H diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-checker/src/main.rs --- a/rust/hedgewars-checker/src/main.rs Thu Dec 13 10:49:30 2018 -0500 +++ b/rust/hedgewars-checker/src/main.rs Thu Dec 13 10:51:07 2018 -0500 @@ -175,7 +175,7 @@ fn get_protocol_number(executable: &str) -> std::io::Result { let output = Command::new(executable).arg("--protocol").output()?; - Ok(u32::from_str(&String::from_utf8(output.stdout).unwrap().as_str()).unwrap_or(55)) + Ok(u32::from_str(&String::from_utf8(output.stdout).unwrap().trim()).unwrap_or(55)) } fn main() { diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/Cargo.toml Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,28 @@ +[package] +edition = "2018" +name = "hedgewars-server" +version = "0.0.1" +authors = [ "Andrey Korotaev " ] + +[features] +official-server = ["openssl"] +tls-connections = ["openssl"] +default = [] + +[dependencies] +rand = "0.5" +mio = "0.6" +slab = "0.4" +netbuf = "0.4" +nom = "4.1" +env_logger = "0.6" +log = "0.4" +base64 = "0.10" +bitflags = "1.0" +serde = "1.0" +serde_yaml = "0.8" +serde_derive = "1.0" +openssl = { version = "0.10", optional = true } + +[dev-dependencies] +proptest = "0.8" \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/main.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/main.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,62 @@ +#![allow(unused_imports)] +#![deny(bare_trait_objects)] + +//use std::io::*; +//use rand::Rng; +//use std::cmp::Ordering; +use mio::net::*; +use mio::*; +use log::*; + +mod utils; +mod server; +mod protocol; + +use crate::server::network::NetworkLayer; +use std::time::Duration; + +fn main() { + env_logger::init(); + + info!("Hedgewars game server, protocol {}", utils::PROTOCOL_VERSION); + + let address = "0.0.0.0:46631".parse().unwrap(); + let listener = TcpListener::bind(&address).unwrap(); + + let poll = Poll::new().unwrap(); + let mut hw_network = NetworkLayer::new(listener, 1024, 512); + hw_network.register_server(&poll).unwrap(); + + let mut events = Events::with_capacity(1024); + + loop { + let timeout = if hw_network.has_pending_operations() { + Some(Duration::from_millis(1)) + } else { + None + }; + poll.poll(&mut events, timeout).unwrap(); + + for event in events.iter() { + if event.readiness() & Ready::readable() == Ready::readable() { + match event.token() { + utils::SERVER => hw_network.accept_client(&poll).unwrap(), + Token(tok) => hw_network.client_readable(&poll, tok).unwrap(), + } + } + if event.readiness() & Ready::writable() == Ready::writable() { + match event.token() { + utils::SERVER => unreachable!(), + Token(tok) => hw_network.client_writable(&poll, tok).unwrap(), + } + } +// if event.kind().is_hup() || event.kind().is_error() { +// match event.token() { +// utils::SERVER => unreachable!(), +// Token(tok) => server.client_error(&poll, tok).unwrap(), +// } +// } + } + hw_network.on_idle(&poll).unwrap(); + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/protocol.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/protocol.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,47 @@ +use netbuf; +use std::{ + io::{Read, Result} +}; +use nom::{ + IResult, Err +}; + +pub mod messages; +#[cfg(test)] +pub mod test; +mod parser; + +pub struct ProtocolDecoder { + buf: netbuf::Buf, + consumed: usize, +} + +impl ProtocolDecoder { + pub fn new() -> ProtocolDecoder { + ProtocolDecoder { + buf: netbuf::Buf::new(), + consumed: 0, + } + } + + pub fn read_from(&mut self, stream: &mut R) -> Result { + self.buf.read_from(stream) + } + + pub fn extract_messages(&mut self) -> Vec { + let parse_result = parser::extract_messages(&self.buf[..]); + match parse_result { + Ok((tail, msgs)) => { + self.consumed = self.buf.len() - self.consumed - tail.len(); + msgs + }, + Err(Err::Incomplete(_)) => unreachable!(), + Err(Err::Error(_)) | Err(Err::Failure(_)) => unreachable!(), + } + } + + pub fn sweep(&mut self) { + self.buf.consume(self.consumed); + self.consumed = 0; + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/protocol/messages.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/protocol/messages.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,316 @@ +use crate::server::coretypes::{ + ServerVar, GameCfg, TeamInfo, + HedgehogInfo, VoteType +}; +use std::{ops, convert::From, iter::once}; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum HWProtocolMessage { + // core + Ping, + Pong, + Quit(Option), + //Cmd(String, Vec), + Global(String), + Watch(String), + ToggleServerRegisteredOnly, + SuperPower, + Info(String), + // not entered state + Nick(String), + Proto(u16), + Password(String, String), + Checker(u16, String, String), + // lobby + 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, + // in room + 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(String), + CallVote(Option), + Vote(bool), + ForceVote(bool), + Save(String, String), + Delete(String), + SaveRoom(String), + LoadRoom(String), + Malformed, + Empty, +} + +#[derive(Debug)] +pub enum HWServerMessage { + Ping, + Pong, + Bye(String), + Nick(String), + Proto(u16), + ServerAuth(String), + 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), + TeamAdd(Vec), + TeamRemove(String), + TeamAccepted(String), + TeamColor(String, u8), + HedgehogsNumber(String, u8), + ConfigEntry(String, Vec), + Kicked, + RunGame, + ForwardEngineMessage(Vec), + RoundFinished, + + ServerMessage(String), + Notice(String), + Warning(String), + Error(String), + Connected(u32), + Unreachable, + + //Deprecated messages + LegacyReady(bool, Vec) +} + +pub fn server_chat(msg: String) -> HWServerMessage { + HWServerMessage::ChatMsg{ nick: "[server]".to_string(), msg } +} + +impl GameCfg { + pub fn to_protocol(&self) -> (String, Vec) { + use crate::server::coretypes::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().into_iter()); + 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) + } +} + +macro_rules! const_braces { + ($e: expr) => { "{}\n" } +} + +macro_rules! msg { + [$($part: expr),*] => { + format!(concat!($(const_braces!($part)),*, "\n"), $($part),*); + }; +} + +#[cfg(test)] +macro_rules! several { + [$part: expr] => { once($part) }; + [$part: expr, $($other: expr),*] => { once($part).chain(several![$($other),*]) }; +} + +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(ServerVar), ??? + 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| several![&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(msg) => msg!["CMD", format!("GREETING {}", msg)], + //CallVote(Option<(String, Option)>) =>, ?? + 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)], + Malformed => msg!["A", "QUICK", "BROWN", "HOG", "JUMPS", "OVER", "THE", "LAZY", "DOG"], + Empty => msg![""], + _ => 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], + Bye(msg) => msg!["BYE", msg], + Nick(nick) => msg!["NICK", nick], + Proto(proto) => msg!["PROTO", proto], + ServerAuth(hash) => msg!["SERVER_AUTH", hash], + 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), + 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], + ServerMessage(msg) => msg!["SERVER_MESSAGE", msg], + Notice(msg) => msg!["NOTICE", msg], + Warning(msg) => msg!["WARNING", msg], + Error(msg) => msg!["ERROR", msg], + + LegacyReady(is_ready, nicks) => + construct_message(&[if *is_ready {"READY"} else {"NOT_READY"}], &nicks), + + _ => msg!["ERROR", "UNIMPLEMENTED"], + } + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/protocol/parser.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/protocol/parser.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,284 @@ +/** 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::*; + +use std::{ + str, str::FromStr, + ops::Range +}; +use super::{ + messages::{HWProtocolMessage, HWProtocolMessage::*} +}; +#[cfg(test)] +use { + super::test::gen_proto_msg, + proptest::{proptest, proptest_helper} +}; +use crate::server::coretypes::{ + HedgehogInfo, TeamInfo, GameCfg, VoteType, MAX_HEDGEHOGS_PER_TEAM +}; + +named!(end_of_message, tag!("\n\n")); +named!(str_line<&[u8], &str>, map_res!(not_line_ending, str::from_utf8)); +named!( a_line<&[u8], String>, map!(str_line, String::from)); +named!(cmd_arg<&[u8], String>, + map!(map_res!(take_until_either!(" \n"), str::from_utf8), String::from)); +named!( u8_line<&[u8], u8>, map_res!(str_line, FromStr::from_str)); +named!(u16_line<&[u8], u16>, map_res!(str_line, FromStr::from_str)); +named!(u32_line<&[u8], u32>, map_res!(str_line, FromStr::from_str)); +named!(yes_no_line<&[u8], bool>, alt!( + do_parse!(tag_no_case!("YES") >> (true)) + | do_parse!(tag_no_case!("NO") >> (false)))); +named!(opt_param<&[u8], Option >, alt!( + do_parse!(peek!(tag!("\n\n")) >> (None)) + | do_parse!(tag!("\n") >> s: str_line >> (Some(s.to_string()))))); +named!(spaces<&[u8], &[u8]>, preceded!(tag!(" "), eat_separator!(" "))); +named!(opt_space_param<&[u8], Option >, alt!( + do_parse!(peek!(tag!("\n\n")) >> (None)) + | do_parse!(spaces >> s: str_line >> (Some(s.to_string()))))); +named!(hog_line<&[u8], HedgehogInfo>, + do_parse!(name: str_line >> eol >> hat: str_line >> + (HedgehogInfo{name: name.to_string(), hat: hat.to_string()}))); +named!(_8_hogs<&[u8], [HedgehogInfo; MAX_HEDGEHOGS_PER_TEAM as usize]>, + do_parse!(h1: hog_line >> eol >> h2: hog_line >> eol >> + h3: hog_line >> eol >> h4: hog_line >> eol >> + h5: hog_line >> eol >> h6: hog_line >> eol >> + h7: hog_line >> eol >> h8: hog_line >> + ([h1, h2, h3, h4, h5, h6, h7, h8]))); +named!(voting<&[u8], VoteType>, alt!( + do_parse!(tag_no_case!("KICK") >> spaces >> n: a_line >> + (VoteType::Kick(n))) + | do_parse!(tag_no_case!("MAP") >> + n: opt!(preceded!(spaces, a_line)) >> + (VoteType::Map(n))) + | do_parse!(tag_no_case!("PAUSE") >> + (VoteType::Pause)) + | do_parse!(tag_no_case!("NEWSEED") >> + (VoteType::NewSeed)) + | do_parse!(tag_no_case!("HEDGEHOGS") >> spaces >> n: u8_line >> + (VoteType::HedgehogsPerTeam(n))))); + +/** Recognizes messages which do not take any parameters */ +named!(basic_message<&[u8], HWProtocolMessage>, alt!( + do_parse!(tag!("PING") >> (Ping)) + | do_parse!(tag!("PONG") >> (Pong)) + | do_parse!(tag!("LIST") >> (List)) + | do_parse!(tag!("BANLIST") >> (BanList)) + | do_parse!(tag!("GET_SERVER_VAR") >> (GetServerVar)) + | do_parse!(tag!("TOGGLE_READY") >> (ToggleReady)) + | do_parse!(tag!("START_GAME") >> (StartGame)) + | do_parse!(tag!("ROUNDFINISHED") >> _m: opt_param >> (RoundFinished)) + | do_parse!(tag!("TOGGLE_RESTRICT_JOINS") >> (ToggleRestrictJoin)) + | do_parse!(tag!("TOGGLE_RESTRICT_TEAMS") >> (ToggleRestrictTeams)) + | do_parse!(tag!("TOGGLE_REGISTERED_ONLY") >> (ToggleRegisteredOnly)) +)); + +/** Recognizes messages which take exactly one parameter */ +named!(one_param_message<&[u8], HWProtocolMessage>, alt!( + do_parse!(tag!("NICK") >> eol >> n: a_line >> (Nick(n))) + | do_parse!(tag!("INFO") >> eol >> n: a_line >> (Info(n))) + | do_parse!(tag!("CHAT") >> eol >> m: a_line >> (Chat(m))) + | do_parse!(tag!("PART") >> msg: opt_param >> (Part(msg))) + | do_parse!(tag!("FOLLOW") >> eol >> n: a_line >> (Follow(n))) + | do_parse!(tag!("KICK") >> eol >> n: a_line >> (Kick(n))) + | do_parse!(tag!("UNBAN") >> eol >> n: a_line >> (Unban(n))) + | do_parse!(tag!("EM") >> eol >> m: a_line >> (EngineMessage(m))) + | do_parse!(tag!("TEAMCHAT") >> eol >> m: a_line >> (TeamChat(m))) + | do_parse!(tag!("ROOM_NAME") >> eol >> n: a_line >> (RoomName(n))) + | do_parse!(tag!("REMOVE_TEAM") >> eol >> n: a_line >> (RemoveTeam(n))) + + | do_parse!(tag!("PROTO") >> eol >> d: u16_line >> (Proto(d))) + + | do_parse!(tag!("QUIT") >> msg: opt_param >> (Quit(msg))) +)); + +/** Recognizes messages preceded with CMD */ +named!(cmd_message<&[u8], HWProtocolMessage>, preceded!(tag!("CMD\n"), alt!( + do_parse!(tag_no_case!("STATS") >> (Stats)) + | do_parse!(tag_no_case!("FIX") >> (Fix)) + | do_parse!(tag_no_case!("UNFIX") >> (Unfix)) + | do_parse!(tag_no_case!("RESTART_SERVER") >> spaces >> tag!("YES") >> (RestartServer)) + | do_parse!(tag_no_case!("REGISTERED_ONLY") >> (ToggleServerRegisteredOnly)) + | do_parse!(tag_no_case!("SUPER_POWER") >> (SuperPower)) + | do_parse!(tag_no_case!("PART") >> m: opt_space_param >> (Part(m))) + | do_parse!(tag_no_case!("QUIT") >> m: opt_space_param >> (Quit(m))) + | do_parse!(tag_no_case!("DELEGATE") >> spaces >> n: a_line >> (Delegate(n))) + | do_parse!(tag_no_case!("SAVE") >> spaces >> n: cmd_arg >> spaces >> l: cmd_arg >> (Save(n, l))) + | do_parse!(tag_no_case!("DELETE") >> spaces >> n: a_line >> (Delete(n))) + | do_parse!(tag_no_case!("SAVEROOM") >> spaces >> r: a_line >> (SaveRoom(r))) + | do_parse!(tag_no_case!("LOADROOM") >> spaces >> r: a_line >> (LoadRoom(r))) + | do_parse!(tag_no_case!("GLOBAL") >> spaces >> m: a_line >> (Global(m))) + | do_parse!(tag_no_case!("WATCH") >> spaces >> i: a_line >> (Watch(i))) + | do_parse!(tag_no_case!("GREETING") >> spaces >> m: a_line >> (Greeting(m))) + | do_parse!(tag_no_case!("VOTE") >> spaces >> m: yes_no_line >> (Vote(m))) + | do_parse!(tag_no_case!("FORCE") >> spaces >> m: yes_no_line >> (ForceVote(m))) + | do_parse!(tag_no_case!("INFO") >> spaces >> n: a_line >> (Info(n))) + | do_parse!(tag_no_case!("MAXTEAMS") >> spaces >> n: u8_line >> (MaxTeams(n))) + | do_parse!(tag_no_case!("CALLVOTE") >> + v: opt!(preceded!(spaces, voting)) >> (CallVote(v))) + | do_parse!( + tag_no_case!("RND") >> alt!(spaces | peek!(end_of_message)) >> + v: str_line >> + (Rnd(v.split_whitespace().map(String::from).collect()))) +))); + +named!(complex_message<&[u8], HWProtocolMessage>, alt!( + do_parse!(tag!("PASSWORD") >> eol >> + p: a_line >> eol >> + s: a_line >> + (Password(p, s))) + | do_parse!(tag!("CHECKER") >> eol >> + i: u16_line >> eol >> + n: a_line >> eol >> + p: a_line >> + (Checker(i, n, p))) + | do_parse!(tag!("CREATE_ROOM") >> eol >> + n: a_line >> + p: opt_param >> + (CreateRoom(n, p))) + | do_parse!(tag!("JOIN_ROOM") >> eol >> + n: a_line >> + p: opt_param >> + (JoinRoom(n, p))) + | do_parse!(tag!("ADD_TEAM") >> eol >> + name: a_line >> eol >> + color: u8_line >> eol >> + grave: a_line >> eol >> + fort: a_line >> eol >> + voice_pack: a_line >> eol >> + flag: a_line >> eol >> + difficulty: u8_line >> eol >> + hedgehogs: _8_hogs >> + (AddTeam(Box::new(TeamInfo{ + name, color, grave, fort, + voice_pack, flag, difficulty, + hedgehogs, hedgehogs_number: 0 + })))) + | do_parse!(tag!("HH_NUM") >> eol >> + n: a_line >> eol >> + c: u8_line >> + (SetHedgehogsNumber(n, c))) + | do_parse!(tag!("TEAM_COLOR") >> eol >> + n: a_line >> eol >> + c: u8_line >> + (SetTeamColor(n, c))) + | do_parse!(tag!("BAN") >> eol >> + n: a_line >> eol >> + r: a_line >> eol >> + t: u32_line >> + (Ban(n, r, t))) + | do_parse!(tag!("BAN_IP") >> eol >> + n: a_line >> eol >> + r: a_line >> eol >> + t: u32_line >> + (BanIP(n, r, t))) + | do_parse!(tag!("BAN_NICK") >> eol >> + n: a_line >> eol >> + r: a_line >> eol >> + t: u32_line >> + (BanNick(n, r, t))) +)); + +named!(cfg_message<&[u8], HWProtocolMessage>, preceded!(tag!("CFG\n"), map!(alt!( + do_parse!(tag!("THEME") >> eol >> + name: a_line >> + (GameCfg::Theme(name))) + | do_parse!(tag!("SCRIPT") >> eol >> + name: a_line >> + (GameCfg::Script(name))) + | do_parse!(tag!("AMMO") >> eol >> + name: a_line >> + value: opt_param >> + (GameCfg::Ammo(name, value))) + | do_parse!(tag!("SCHEME") >> eol >> + name: a_line >> + values: opt!(preceded!(eol, separated_list!(eol, a_line))) >> + (GameCfg::Scheme(name, values.unwrap_or_default()))) + | do_parse!(tag!("FEATURE_SIZE") >> eol >> + value: u32_line >> + (GameCfg::FeatureSize(value))) + | do_parse!(tag!("MAP") >> eol >> + value: a_line >> + (GameCfg::MapType(value))) + | do_parse!(tag!("MAPGEN") >> eol >> + value: u32_line >> + (GameCfg::MapGenerator(value))) + | do_parse!(tag!("MAZE_SIZE") >> eol >> + value: u32_line >> + (GameCfg::MazeSize(value))) + | do_parse!(tag!("SEED") >> eol >> + value: a_line >> + (GameCfg::Seed(value))) + | do_parse!(tag!("TEMPLATE") >> eol >> + value: u32_line >> + (GameCfg::Template(value))) + | do_parse!(tag!("DRAWNMAP") >> eol >> + value: a_line >> + (GameCfg::DrawnMap(value))) +), Cfg))); + +named!(malformed_message<&[u8], HWProtocolMessage>, + do_parse!(separated_list!(eol, a_line) >> (Malformed))); + +named!(empty_message<&[u8], HWProtocolMessage>, + do_parse!(alt!(end_of_message | eol) >> (Empty))); + +named!(message<&[u8], HWProtocolMessage>, alt!(terminated!( + alt!( + basic_message + | one_param_message + | cmd_message + | complex_message + | cfg_message + ), end_of_message + ) + | terminated!(malformed_message, end_of_message) + | empty_message + ) +); + +named!(pub extract_messages<&[u8], Vec >, many0!(complete!(message))); + +#[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 demo\n\n"), Ok((&b""[..], Watch("demo".to_string())))); + 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!(extract_messages(b"QUIT\n1\n2\n\n"), Ok((&b""[..], vec![Malformed]))); + + assert_eq!(extract_messages(b"PING\n\nPING\n\nP"), Ok((&b"P"[..], vec![Ping, Ping]))); + assert_eq!(extract_messages(b"SING\n\nPING\n\n"), Ok((&b""[..], vec![Malformed, Ping]))); + assert_eq!(extract_messages(b"\n\n\n\nPING\n\n"), Ok((&b""[..], vec![Empty, Empty, Ping]))); + assert_eq!(extract_messages(b"\n\n\nPING\n\n"), Ok((&b""[..], vec![Empty, Empty, Ping]))); +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/protocol/test.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/protocol/test.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,168 @@ +use proptest::{ + test_runner::{TestRunner, Reason}, + arbitrary::{any, any_with, Arbitrary, StrategyFor}, + strategy::{Strategy, BoxedStrategy, Just, Map} +}; + +use crate::server::coretypes::{GameCfg, TeamInfo, HedgehogInfo}; + +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::server::coretypes::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 { + name, color, grave, fort, + voice_pack, flag,difficulty, + hedgehogs, hedgehogs_number: 0 + } + }).boxed() + } + + type Strategy = BoxedStrategy; +} + +pub fn gen_proto_msg() -> BoxedStrategy where { + let res = (0..58).no_shrink().prop_flat_map(|i| { + proto_msg_match!(i, def = Malformed, + 0 => Ping(), + 1 => Pong(), + 2 => Quit(Option), + //3 => Cmd + 4 => Global(Ascii), + 5 => Watch(Ascii), + 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(Ascii), + //49 => CallVote(Option<(String, Option)>), + 50 => Vote(bool), + 51 => ForceVote(bool), + 52 => Save(Ascii, Ascii), + 53 => Delete(Ascii), + 54 => SaveRoom(Ascii), + 55 => LoadRoom(Ascii), + 56 => Malformed(), + 57 => Empty() + )}); + res.boxed() +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,8 @@ +pub mod core; +pub mod client; +pub mod io; +pub mod room; +pub mod network; +pub mod coretypes; +mod actions; +mod handlers; diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/actions.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/actions.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,631 @@ +use std::{ + io, io::Write, + iter::once, + mem::replace +}; +use super::{ + core::HWServer, + room::{GameInfo, RoomFlags}, + client::HWClient, + coretypes::{ClientId, RoomId, GameCfg, VoteType}, + room::HWRoom, + handlers +}; +use crate::{ + protocol::messages::{ + HWProtocolMessage, + HWServerMessage, + HWServerMessage::*, + server_chat + }, + utils::to_engine_msg +}; +use rand::{thread_rng, Rng, distributions::Uniform}; + +pub enum Destination { + ToId(ClientId), + ToSelf, + ToAll { + room_id: Option, + protocol: Option, + skip_self: bool + } +} + +pub struct PendingMessage { + pub destination: Destination, + pub message: HWServerMessage +} + +impl PendingMessage { + pub fn send(message: HWServerMessage, client_id: ClientId) -> PendingMessage { + PendingMessage{ destination: Destination::ToId(client_id), message} + } + + pub fn send_self(message: HWServerMessage) -> PendingMessage { + PendingMessage{ destination: Destination::ToSelf, message } + } + + pub fn send_all(message: HWServerMessage) -> PendingMessage { + let destination = Destination::ToAll { + room_id: None, + protocol: None, + skip_self: false, + }; + PendingMessage{ destination, message } + } + + pub fn in_room(mut self, clients_room_id: RoomId) -> PendingMessage { + if let Destination::ToAll {ref mut room_id, ..} = self.destination { + *room_id = Some(clients_room_id) + } + self + } + + pub fn with_protocol(mut self, protocol_number: u16) -> PendingMessage { + if let Destination::ToAll {ref mut protocol, ..} = self.destination { + *protocol = Some(protocol_number) + } + self + } + + pub fn but_self(mut self) -> PendingMessage { + if let Destination::ToAll {ref mut skip_self, ..} = self.destination { + *skip_self = true + } + self + } + + pub fn action(self) -> Action { Send(self) } +} + +impl Into for PendingMessage { + fn into(self) -> Action { self.action() } +} + +impl HWServerMessage { + pub fn send(self, client_id: ClientId) -> PendingMessage { PendingMessage::send(self, client_id) } + pub fn send_self(self) -> PendingMessage { PendingMessage::send_self(self) } + pub fn send_all(self) -> PendingMessage { PendingMessage::send_all(self) } +} + +pub enum Action { + Send(PendingMessage), + RemoveClient, + ByeClient(String), + ReactProtocolMessage(HWProtocolMessage), + CheckRegistered, + JoinLobby, + AddRoom(String, Option), + RemoveRoom(RoomId), + MoveToRoom(RoomId), + MoveToLobby(String), + ChangeMaster(RoomId, Option), + RemoveTeam(String), + RemoveClientTeams, + SendRoomUpdate(Option), + StartRoomGame(RoomId), + SendTeamRemovalMessage(String), + FinishRoomGame(RoomId), + SendRoomData{to: ClientId, teams: bool, config: bool, flags: bool}, + AddVote{vote: bool, is_forced: bool}, + ApplyVoting(VoteType, RoomId), + Warn(String), + ProtocolError(String) +} + +use self::Action::*; + +pub fn run_action(server: &mut HWServer, client_id: usize, action: Action) { + match action { + Send(msg) => server.send(client_id, &msg.destination, msg.message), + ByeClient(msg) => { + let c = &server.clients[client_id]; + let nick = c.nick.clone(); + + if let Some(id) = c.room_id{ + if id != server.lobby_id { + server.react(client_id, vec![ + MoveToLobby(format!("quit: {}", msg.clone()))]); + } + } + + server.react(client_id, vec![ + LobbyLeft(nick, msg.clone()).send_all().action(), + Bye(msg).send_self().action(), + RemoveClient]); + }, + RemoveClient => { + server.removed_clients.push(client_id); + if server.clients.contains(client_id) { + server.clients.remove(client_id); + } + }, + ReactProtocolMessage(msg) => + handlers::handle(server, client_id, msg), + CheckRegistered => { + let client = &server.clients[client_id]; + if client.protocol_number > 0 && client.nick != "" { + let has_nick_clash = server.clients.iter().any( + |(id, c)| id != client_id && c.nick == client.nick); + + let actions = if !client.is_checker() && has_nick_clash { + if client.protocol_number < 38 { + vec![ByeClient("Nickname is already in use".to_string())] + } else { + server.clients[client_id].nick.clear(); + vec![Notice("NickAlreadyInUse".to_string()).send_self().action()] + } + } else { + vec![JoinLobby] + }; + server.react(client_id, actions); + } + }, + JoinLobby => { + server.clients[client_id].room_id = Some(server.lobby_id); + + let mut lobby_nicks = Vec::new(); + for (_, c) in server.clients.iter() { + if c.room_id.is_some() { + lobby_nicks.push(c.nick.clone()); + } + } + let joined_msg = LobbyJoined(lobby_nicks); + + let everyone_msg = LobbyJoined(vec![server.clients[client_id].nick.clone()]); + let flags_msg = ClientFlags( + "+i".to_string(), + server.clients.iter() + .filter(|(_, c)| c.room_id.is_some()) + .map(|(_, c)| c.nick.clone()) + .collect()); + let server_msg = ServerMessage("\u{1f994} is watching".to_string()); + let rooms_msg = Rooms(server.rooms.iter() + .filter(|(id, _)| *id != server.lobby_id) + .flat_map(|(_, r)| + r.info(r.master_id.map(|id| &server.clients[id]))) + .collect()); + server.react(client_id, vec![ + everyone_msg.send_all().but_self().action(), + joined_msg.send_self().action(), + flags_msg.send_self().action(), + server_msg.send_self().action(), + rooms_msg.send_self().action(), + ]); + }, + AddRoom(name, password) => { + let room_id = server.add_room();; + + let r = &mut server.rooms[room_id]; + let c = &mut server.clients[client_id]; + r.master_id = Some(c.id); + r.name = name; + r.password = password; + r.protocol_number = c.protocol_number; + + let actions = vec![ + RoomAdd(r.info(Some(&c))).send_all() + .with_protocol(r.protocol_number).action(), + MoveToRoom(room_id)]; + + server.react(client_id, actions); + }, + RemoveRoom(room_id) => { + let r = &mut server.rooms[room_id]; + let actions = vec![RoomRemove(r.name.clone()).send_all() + .with_protocol(r.protocol_number).action()]; + server.rooms.remove(room_id); + server.react(client_id, actions); + } + MoveToRoom(room_id) => { + let r = &mut server.rooms[room_id]; + let c = &mut server.clients[client_id]; + r.players_number += 1; + c.room_id = Some(room_id); + + let is_master = r.master_id == Some(c.id); + c.set_is_master(is_master); + c.set_is_ready(is_master); + c.set_is_joined_mid_game(false); + + if is_master { + r.ready_players_number += 1; + } + + let mut v = vec![ + RoomJoined(vec![c.nick.clone()]).send_all().in_room(room_id).action(), + ClientFlags("+i".to_string(), vec![c.nick.clone()]).send_all().action(), + SendRoomUpdate(None)]; + + if !r.greeting.is_empty() { + v.push(ChatMsg {nick: "[greeting]".to_string(), msg: r.greeting.clone()} + .send_self().action()); + } + + if !c.is_master() { + let team_names: Vec<_>; + if let Some(ref mut info) = r.game_info { + c.set_is_in_game(true); + c.set_is_joined_mid_game(true); + + { + let teams = info.client_teams(c.id); + c.teams_in_game = teams.clone().count() as u8; + c.clan = teams.clone().next().map(|t| t.color); + team_names = teams.map(|t| t.name.clone()).collect(); + } + + if !team_names.is_empty() { + info.left_teams.retain(|name| + !team_names.contains(&name)); + info.teams_in_game += team_names.len() as u8; + r.teams = info.teams_at_start.iter() + .filter(|(_, t)| !team_names.contains(&t.name)) + .cloned().collect(); + } + } else { + team_names = Vec::new(); + } + + v.push(SendRoomData{ to: client_id, teams: true, config: true, flags: true}); + + if let Some(ref info) = r.game_info { + v.push(RunGame.send_self().action()); + v.push(ClientFlags("+g".to_string(), vec![c.nick.clone()]) + .send_all().in_room(r.id).action()); + v.push(ForwardEngineMessage( + vec![to_engine_msg("e$spectate 1".bytes())]) + .send_self().action()); + v.push(ForwardEngineMessage(info.msg_log.clone()) + .send_self().action()); + + for name in &team_names { + v.push(ForwardEngineMessage( + vec![to_engine_msg(once(b'G').chain(name.bytes()))]) + .send_all().in_room(r.id).action()); + } + if info.is_paused { + v.push(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]) + .send_all().in_room(r.id).action()) + } + } + } + server.react(client_id, v); + } + SendRoomData {to, teams, config, flags} => { + let mut actions = Vec::new(); + let room_id = server.clients[client_id].room_id; + if let Some(r) = room_id.and_then(|id| server.rooms.get(id)) { + if config { + actions.push(ConfigEntry("FULLMAPCONFIG".to_string(), r.map_config()) + .send(to).action()); + for cfg in r.game_config() { + actions.push(cfg.to_server_msg().send(to).action()); + } + } + if teams { + let current_teams = match r.game_info { + Some(ref info) => &info.teams_at_start, + None => &r.teams + }; + for (owner_id, team) in current_teams.iter() { + actions.push(TeamAdd(HWRoom::team_info(&server.clients[*owner_id], &team)) + .send(to).action()); + actions.push(TeamColor(team.name.clone(), team.color) + .send(to).action()); + actions.push(HedgehogsNumber(team.name.clone(), team.hedgehogs_number) + .send(to).action()); + } + } + if flags { + if let Some(id) = r.master_id { + actions.push(ClientFlags("+h".to_string(), vec![server.clients[id].nick.clone()]) + .send(to).action()); + } + let nicks: Vec<_> = server.clients.iter() + .filter(|(_, c)| c.room_id == Some(r.id) && c.is_ready()) + .map(|(_, c)| c.nick.clone()).collect(); + if !nicks.is_empty() { + actions.push(ClientFlags("+r".to_string(), nicks) + .send(to).action()); + } + } + } + server.react(client_id, actions); + } + AddVote{vote, is_forced} => { + let mut actions = Vec::new(); + if let Some(r) = server.room(client_id) { + let mut result = None; + if let Some(ref mut voting) = r.voting { + if is_forced || voting.votes.iter().all(|(id, _)| client_id != *id) { + actions.push(server_chat("Your vote has been counted.".to_string()) + .send_self().action()); + voting.votes.push((client_id, vote)); + let i = voting.votes.iter(); + let pro = i.clone().filter(|(_, v)| *v).count(); + let contra = i.filter(|(_, v)| !*v).count(); + let success_quota = voting.voters.len() / 2 + 1; + if is_forced && vote || pro >= success_quota { + result = Some(true); + } else if is_forced && !vote || contra > voting.voters.len() - success_quota { + result = Some(false); + } + } else { + actions.push(server_chat("You already have voted.".to_string()) + .send_self().action()); + } + } else { + actions.push(server_chat("There's no voting going on.".to_string()) + .send_self().action()); + } + + if let Some(res) = result { + actions.push(server_chat("Voting closed.".to_string()) + .send_all().in_room(r.id).action()); + let voting = replace(&mut r.voting, None).unwrap(); + if res { + actions.push(ApplyVoting(voting.kind, r.id)); + } + } + } + + server.react(client_id, actions); + } + ApplyVoting(kind, room_id) => { + let mut actions = Vec::new(); + let mut id = client_id; + match kind { + VoteType::Kick(nick) => { + if let Some(c) = server.find_client(&nick) { + if c.room_id == Some(room_id) { + id = c.id; + actions.push(Kicked.send_self().action()); + actions.push(MoveToLobby("kicked".to_string())); + } + } + }, + VoteType::Map(None) => (), + VoteType::Map(Some(name)) => { + if let Some(location) = server.rooms[room_id].load_config(&name) { + actions.push(server_chat(location.to_string()) + .send_all().in_room(room_id).action()); + actions.push(SendRoomUpdate(None)); + for (_, c) in server.clients.iter() { + if c.room_id == Some(room_id) { + actions.push(SendRoomData{ + to: c.id, teams: false, + config: true, flags: false}) + } + } + } + }, + VoteType::Pause => { + if let Some(ref mut info) = server.rooms[room_id].game_info { + info.is_paused = !info.is_paused; + actions.push(server_chat("Pause toggled.".to_string()) + .send_all().in_room(room_id).action()); + actions.push(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]) + .send_all().in_room(room_id).action()); + } + }, + VoteType::NewSeed => { + let seed = thread_rng().gen_range(0, 1_000_000_000).to_string(); + let cfg = GameCfg::Seed(seed); + actions.push(cfg.to_server_msg().send_all().in_room(room_id).action()); + server.rooms[room_id].set_config(cfg); + }, + VoteType::HedgehogsPerTeam(number) => { + let r = &mut server.rooms[room_id]; + let nicks = r.set_hedgehogs_number(number); + actions.extend(nicks.into_iter().map(|n| + HedgehogsNumber(n, number).send_all().in_room(room_id).action() + )); + }, + } + server.react(id, actions); + } + MoveToLobby(msg) => { + let mut actions = Vec::new(); + let lobby_id = server.lobby_id; + if let (c, Some(r)) = server.client_and_room(client_id) { + r.players_number -= 1; + if c.is_ready() && r.ready_players_number > 0 { + r.ready_players_number -= 1; + } + if c.is_master() && (r.players_number > 0 || r.is_fixed()) { + actions.push(ChangeMaster(r.id, None)); + } + actions.push(ClientFlags("-i".to_string(), vec![c.nick.clone()]) + .send_all().action()); + } + server.react(client_id, actions); + actions = Vec::new(); + + if let (c, Some(r)) = server.client_and_room(client_id) { + c.room_id = Some(lobby_id); + if r.players_number == 0 && !r.is_fixed() { + actions.push(RemoveRoom(r.id)); + } else { + actions.push(RemoveClientTeams); + actions.push(RoomLeft(c.nick.clone(), msg) + .send_all().in_room(r.id).but_self().action()); + actions.push(SendRoomUpdate(Some(r.name.clone()))); + } + } + server.react(client_id, actions) + } + ChangeMaster(room_id, new_id) => { + let mut actions = Vec::new(); + let room_client_ids = server.room_clients(room_id); + let new_id = if server.room(client_id).map(|r| r.is_fixed()).unwrap_or(false) { + new_id + } else { + new_id.or_else(|| + room_client_ids.iter().find(|id| **id != client_id).cloned()) + }; + let new_nick = new_id.map(|id| server.clients[id].nick.clone()); + + if let (c, Some(r)) = server.client_and_room(client_id) { + match r.master_id { + Some(id) if id == c.id => { + c.set_is_master(false); + r.master_id = None; + actions.push(ClientFlags("-h".to_string(), vec![c.nick.clone()]) + .send_all().in_room(r.id).action()); + } + Some(_) => unreachable!(), + None => {} + } + r.master_id = new_id; + if !r.is_fixed() && c.protocol_number < 42 { + r.name.replace_range(.., new_nick.as_ref().map_or("[]", String::as_str)); + } + r.set_join_restriction(false); + r.set_team_add_restriction(false); + let is_fixed = r.is_fixed(); + r.set_unregistered_players_restriction(is_fixed); + if let Some(nick) = new_nick { + actions.push(ClientFlags("+h".to_string(), vec![nick]) + .send_all().in_room(r.id).action()); + } + } + if let Some(id) = new_id { + server.clients[id].set_is_master(true) + } + server.react(client_id, actions); + } + RemoveTeam(name) => { + let mut actions = Vec::new(); + if let (c, Some(r)) = server.client_and_room(client_id) { + r.remove_team(&name); + if let Some(ref mut info) = r.game_info { + info.left_teams.push(name.clone()); + } + actions.push(TeamRemove(name.clone()).send_all().in_room(r.id).action()); + actions.push(SendRoomUpdate(None)); + if r.game_info.is_some() && c.is_in_game() { + actions.push(SendTeamRemovalMessage(name)); + } + } + server.react(client_id, actions); + }, + RemoveClientTeams => { + if let (c, Some(r)) = server.client_and_room(client_id) { + let actions = r.client_teams(c.id).map(|t| RemoveTeam(t.name.clone())).collect(); + server.react(client_id, actions); + } + } + SendRoomUpdate(old_name) => { + if let (c, Some(r)) = server.client_and_room(client_id) { + let name = old_name.unwrap_or_else(|| r.name.clone()); + let actions = vec![RoomUpdated(name, r.info(Some(&c))) + .send_all().with_protocol(r.protocol_number).action()]; + server.react(client_id, actions); + } + }, + StartRoomGame(room_id) => { + let actions = { + let (room_clients, room_nicks): (Vec<_>, Vec<_>) = server.clients.iter() + .map(|(id, c)| (id, c.nick.clone())).unzip(); + let room = &mut server.rooms[room_id]; + + if !room.has_multiple_clans() { + vec![Warn("The game can't be started with less than two clans!".to_string())] + } else if room.protocol_number <= 43 && room.players_number != room.ready_players_number { + vec![Warn("Not all players are ready".to_string())] + } else if room.game_info.is_some() { + vec![Warn("The game is already in progress".to_string())] + } else { + room.start_round(); + for id in room_clients { + let c = &mut server.clients[id]; + c.set_is_in_game(false); + c.team_indices = room.client_team_indices(c.id); + } + vec![RunGame.send_all().in_room(room.id).action(), + SendRoomUpdate(None), + ClientFlags("+g".to_string(), room_nicks) + .send_all().in_room(room.id).action()] + } + }; + server.react(client_id, actions); + } + SendTeamRemovalMessage(team_name) => { + let mut actions = Vec::new(); + if let Some(r) = server.room(client_id) { + if let Some(ref mut info) = r.game_info { + let msg = once(b'F').chain(team_name.bytes()); + actions.push(ForwardEngineMessage(vec![to_engine_msg(msg)]). + send_all().in_room(r.id).but_self().action()); + info.teams_in_game -= 1; + if info.teams_in_game == 0 { + actions.push(FinishRoomGame(r.id)); + } + let remove_msg = to_engine_msg(once(b'F').chain(team_name.bytes())); + if let Some(m) = &info.sync_msg { + info.msg_log.push(m.clone()); + } + if info.sync_msg.is_some() { + info.sync_msg = None + } + info.msg_log.push(remove_msg.clone()); + actions.push(ForwardEngineMessage(vec![remove_msg]) + .send_all().in_room(r.id).but_self().action()); + } + } + server.react(client_id, actions); + } + FinishRoomGame(room_id) => { + let mut actions = Vec::new(); + + let r = &mut server.rooms[room_id]; + r.ready_players_number = 1; + actions.push(SendRoomUpdate(None)); + actions.push(RoundFinished.send_all().in_room(r.id).action()); + + if let Some(info) = replace(&mut r.game_info, None) { + for (_, c) in server.clients.iter() { + if c.room_id == Some(room_id) && c.is_joined_mid_game() { + actions.push(SendRoomData{ + to: c.id, teams: false, + config: true, flags: false}); + for name in &info.left_teams { + actions.push(TeamRemove(name.clone()) + .send(c.id).action()); + } + } + } + } + + let nicks: Vec<_> = server.clients.iter_mut() + .filter(|(_, c)| c.room_id == Some(room_id)) + .map(|(_, c)| { + c.set_is_ready(c.is_master()); + c.set_is_joined_mid_game(false); + c + }).filter_map(|c| if !c.is_master() { + Some(c.nick.clone()) + } else { + None + }).collect(); + + if !nicks.is_empty() { + let msg = if r.protocol_number < 38 { + LegacyReady(false, nicks) + } else { + ClientFlags("-r".to_string(), nicks) + }; + actions.push(msg.send_all().in_room(room_id).action()); + } + server.react(client_id, actions); + } + Warn(msg) => { + run_action(server, client_id, Warning(msg).send_self().action()); + } + ProtocolError(msg) => { + run_action(server, client_id, Error(msg).send_self().action()) + } + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/client.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/client.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,68 @@ +use super::coretypes::ClientId; +use bitflags::*; + +bitflags!{ + pub struct ClientFlags: u8 { + const IS_ADMIN = 0b0000_0001; + const IS_MASTER = 0b0000_0010; + const IS_READY = 0b0000_0100; + const IS_IN_GAME = 0b0000_1000; + const IS_JOINED_MID_GAME = 0b0001_0000; + const IS_CHECKER = 0b0010_0000; + + const NONE = 0b0000_0000; + const DEFAULT = Self::NONE.bits; + } +} + +pub struct HWClient { + pub id: ClientId, + pub room_id: Option, + pub nick: String, + pub web_password: String, + pub server_salt: String, + pub protocol_number: u16, + pub flags: ClientFlags, + pub teams_in_game: u8, + pub team_indices: Vec, + pub clan: Option +} + +impl HWClient { + pub fn new(id: ClientId, salt: String) -> HWClient { + HWClient { + id, + room_id: None, + nick: String::new(), + web_password: String::new(), + server_salt: salt, + protocol_number: 0, + flags: ClientFlags::DEFAULT, + teams_in_game: 0, + team_indices: Vec::new(), + clan: None, + } + } + + fn contains(& self, mask: ClientFlags) -> bool { + self.flags.contains(mask) + } + + fn set(&mut self, mask: ClientFlags, value: bool) { + self.flags.set(mask, value); + } + + pub fn is_admin(&self)-> bool { self.contains(ClientFlags::IS_ADMIN) } + pub fn is_master(&self)-> bool { self.contains(ClientFlags::IS_MASTER) } + pub fn is_ready(&self)-> bool { self.contains(ClientFlags::IS_READY) } + pub fn is_in_game(&self)-> bool { self.contains(ClientFlags::IS_IN_GAME) } + pub fn is_joined_mid_game(&self)-> bool { self.contains(ClientFlags::IS_JOINED_MID_GAME) } + pub fn is_checker(&self)-> bool { self.contains(ClientFlags::IS_CHECKER) } + + pub fn set_is_admin(&mut self, value: bool) { self.set(ClientFlags::IS_ADMIN, value) } + pub fn set_is_master(&mut self, value: bool) { self.set(ClientFlags::IS_MASTER, value) } + pub fn set_is_ready(&mut self, value: bool) { self.set(ClientFlags::IS_READY, value) } + pub fn set_is_in_game(&mut self, value: bool) { self.set(ClientFlags::IS_IN_GAME, value) } + pub fn set_is_joined_mid_game(&mut self, value: bool) { self.set(ClientFlags::IS_JOINED_MID_GAME, value) } + pub fn set_is_checker(&mut self, value: bool) { self.set(ClientFlags::IS_CHECKER, value) } +} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/core.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/core.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,158 @@ +use slab; +use crate::utils; +use super::{ + io::HWServerIO, + client::HWClient, room::HWRoom, actions, handlers, + coretypes::{ClientId, RoomId}, + actions::{Destination, PendingMessage} +}; +use crate::protocol::messages::*; +use rand::{RngCore, thread_rng}; +use base64::{encode}; +use log::*; + +type Slab = slab::Slab; + +pub struct HWServer { + pub clients: Slab, + pub rooms: Slab, + pub lobby_id: RoomId, + pub output: Vec<(Vec, HWServerMessage)>, + pub removed_clients: Vec, + pub io: Box +} + +impl HWServer { + pub fn new(clients_limit: usize, rooms_limit: usize, io: Box) -> HWServer { + let rooms = Slab::with_capacity(rooms_limit); + let clients = Slab::with_capacity(clients_limit); + let mut server = HWServer { + clients, rooms, + lobby_id: 0, + output: vec![], + removed_clients: vec![], + io + }; + server.lobby_id = server.add_room(); + server + } + + pub fn add_client(&mut self) -> ClientId { + let key: ClientId; + { + let entry = self.clients.vacant_entry(); + key = entry.key(); + let mut salt = [0u8; 18]; + thread_rng().fill_bytes(&mut salt); + + let client = HWClient::new(entry.key(), encode(&salt)); + entry.insert(client); + } + self.send(key, &Destination::ToSelf, HWServerMessage::Connected(utils::PROTOCOL_VERSION)); + key + } + + pub fn client_lost(&mut self, client_id: ClientId) { + actions::run_action(self, client_id, + actions::Action::ByeClient("Connection reset".to_string())); + } + + pub fn add_room(&mut self) -> RoomId { + let entry = self.rooms.vacant_entry(); + let key = entry.key(); + let room = HWRoom::new(entry.key()); + entry.insert(room); + key + } + + pub fn handle_msg(&mut self, client_id: ClientId, msg: HWProtocolMessage) { + debug!("Handling message {:?} for client {}", msg, client_id); + if self.clients.contains(client_id) { + handlers::handle(self, client_id, msg); + } + } + + fn get_recipients(&self, client_id: ClientId, destination: &Destination) -> Vec { + let mut ids = match *destination { + Destination::ToSelf => vec![client_id], + Destination::ToId(id) => vec![id], + Destination::ToAll {room_id: Some(id), ..} => + self.room_clients(id), + Destination::ToAll {protocol: Some(proto), ..} => + self.protocol_clients(proto), + Destination::ToAll {..} => + self.clients.iter().map(|(id, _)| id).collect::>() + }; + if let Destination::ToAll {skip_self: true, ..} = destination { + if let Some(index) = ids.iter().position(|id| *id == client_id) { + ids.remove(index); + } + } + ids + } + + pub fn send(&mut self, client_id: ClientId, destination: &Destination, message: HWServerMessage) { + let ids = self.get_recipients(client_id, &destination); + self.output.push((ids, message)); + } + + pub fn react(&mut self, client_id: ClientId, actions: Vec) { + for action in actions { + actions::run_action(self, client_id, action); + } + } + + pub fn lobby(&self) -> &HWRoom { &self.rooms[self.lobby_id] } + + pub fn has_room(&self, name: &str) -> bool { + self.rooms.iter().any(|(_, r)| r.name == name) + } + + pub fn find_room(&self, name: &str) -> Option<&HWRoom> { + self.rooms.iter().find_map(|(_, r)| Some(r).filter(|r| r.name == name)) + } + + pub fn find_room_mut(&mut self, name: &str) -> Option<&mut HWRoom> { + self.rooms.iter_mut().find_map(|(_, r)| Some(r).filter(|r| r.name == name)) + } + + pub fn find_client(&self, nick: &str) -> Option<&HWClient> { + self.clients.iter().find_map(|(_, c)| Some(c).filter(|c| c.nick == nick)) + } + + pub fn find_client_mut(&mut self, nick: &str) -> Option<&mut HWClient> { + self.clients.iter_mut().find_map(|(_, c)| Some(c).filter(|c| c.nick == nick)) + } + + pub fn select_clients(&self, f: F) -> Vec + where F: Fn(&(usize, &HWClient)) -> bool { + self.clients.iter().filter(f) + .map(|(_, c)| c.id).collect() + } + + pub fn room_clients(&self, room_id: RoomId) -> Vec { + self.select_clients(|(_, c)| c.room_id == Some(room_id)) + } + + pub fn protocol_clients(&self, protocol: u16) -> Vec { + self.select_clients(|(_, c)| c.protocol_number == protocol) + } + + pub fn other_clients_in_room(&self, self_id: ClientId) -> Vec { + let room_id = self.clients[self_id].room_id; + self.select_clients(|(id, c)| *id != self_id && c.room_id == room_id ) + } + + pub fn client_and_room(&mut self, client_id: ClientId) -> (&mut HWClient, Option<&mut HWRoom>) { + let c = &mut self.clients[client_id]; + if let Some(room_id) = c.room_id { + (c, Some(&mut self.rooms[room_id])) + } else { + (c, None) + } + } + + pub fn room(&mut self, client_id: ClientId) -> Option<&mut HWRoom> { + self.client_and_room(client_id).1 + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/coretypes.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/coretypes.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,72 @@ +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(u32), +} + +#[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)] +pub struct TeamInfo { + 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)] +pub struct HedgehogInfo { + pub name: String, + pub hat: String, +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum VoteType { + Kick(String), + Map(Option), + Pause, + NewSeed, + HedgehogsPerTeam(u8) +} + +#[derive(Clone, Debug)] +pub struct Voting { + pub ttl: u32, + pub voters: Vec, + pub votes: Vec<(ClientId, bool)>, + pub kind: VoteType +} + +impl Voting { + pub fn new(kind: VoteType, voters: Vec) -> Voting { + Voting { + kind, voters, ttl: 2, + votes: Vec::new() + } + } +} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/handlers.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/handlers.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,44 @@ +use mio; +use std::{io, io::Write}; + +use super::{ + core::HWServer, + actions::{Action, Action::*}, + coretypes::ClientId +}; +use crate::{ + protocol::messages::{ + HWProtocolMessage, + HWServerMessage::* + } +}; +use log::*; + +mod loggingin; +mod lobby; +mod inroom; +mod common; +mod checker; + +pub fn handle(server: &mut HWServer, client_id: ClientId, message: HWProtocolMessage) { + match message { + HWProtocolMessage::Ping => + server.react(client_id, vec![Pong.send_self().action()]), + HWProtocolMessage::Quit(Some(msg)) => + server.react(client_id, vec![ByeClient("User quit: ".to_string() + &msg)]), + HWProtocolMessage::Quit(None) => + server.react(client_id, vec![ByeClient("User quit".to_string())]), + HWProtocolMessage::Malformed => warn!("Malformed/unknown message"), + HWProtocolMessage::Empty => warn!("Empty message"), + _ => { + match server.clients[client_id].room_id { + None => + loggingin::handle(server, client_id, message), + Some(id) if id == server.lobby_id => + lobby::handle(server, client_id, message), + Some(id) => + inroom::handle(server, client_id, id, message) + } + }, + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/handlers/checker.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/handlers/checker.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,18 @@ +use mio; +use log::*; + +use crate::{ + server::{ + core::HWServer, + coretypes::ClientId, + }, + protocol::messages::{ + HWProtocolMessage + }, +}; + +pub fn handle(server: & mut HWServer, client_id: ClientId, message: HWProtocolMessage) { + match message { + _ => warn!("Unknown command"), + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/handlers/common.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/handlers/common.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,73 @@ +use crate::{ + server::{actions::Action, core::HWServer}, + protocol::messages::{ + HWProtocolMessage::{self, Rnd}, HWServerMessage::{self, ChatMsg}, + } +}; +use rand::{self, Rng, thread_rng}; + +pub fn rnd_reply(options: &[String]) -> HWServerMessage { + let mut rng = thread_rng(); + let reply = if options.is_empty() { + (*rng.choose(&["heads", "tails"]).unwrap()).to_owned() + } else { + rng.choose(&options).unwrap().clone() + }; + + ChatMsg { + nick: "[random]".to_owned(), + msg: reply.clone(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::messages::HWServerMessage::ChatMsg; + use crate::server::actions::{ + Action::{self, Send}, PendingMessage, + }; + + fn reply2string(r: HWServerMessage) -> String { + match r { + ChatMsg { msg: p, .. } => String::from(p), + _ => panic!("expected a ChatMsg"), + } + } + + fn run_handle_test(opts: Vec) { + let opts2 = opts.clone(); + for opt in opts { + while reply2string(rnd_reply(&opts2)) != opt {} + } + } + + /// This test terminates almost surely. + #[test] + fn test_handle_rnd_empty() { + run_handle_test(vec![]) + } + + /// This test terminates almost surely. + #[test] + fn test_handle_rnd_nonempty() { + run_handle_test(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]) + } + + /// This test terminates almost surely (strong law of large numbers) + #[test] + fn test_distribution() { + let eps = 0.000001; + let lim = 0.5; + let opts = vec![0.to_string(), 1.to_string()]; + let mut ones = 0; + let mut tries = 0; + + while tries < 1000 || ((ones as f64 / tries as f64) - lim).abs() >= eps { + tries += 1; + if reply2string(rnd_reply(&opts)) == 1.to_string() { + ones += 1; + } + } + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/handlers/inroom.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/handlers/inroom.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,486 @@ +use mio; + +use crate::{ + server::{ + coretypes::{ + ClientId, RoomId, Voting, VoteType, GameCfg, + MAX_HEDGEHOGS_PER_TEAM + }, + core::HWServer, + room::{HWRoom, RoomFlags}, + actions::{Action, Action::*} + }, + protocol::messages::{ + HWProtocolMessage, + HWServerMessage::*, + server_chat + }, + utils::is_name_illegal +}; +use std::{ + mem::swap +}; +use base64::{encode, decode}; +use super::common::rnd_reply; +use log::*; + +#[derive(Clone)] +struct ByMsg<'a> { + messages: &'a[u8] +} + +impl <'a> Iterator for ByMsg<'a> { + type Item = &'a[u8]; + + fn next(&mut self) -> Option<::Item> { + if let Some(size) = self.messages.get(0) { + let (msg, next) = self.messages.split_at(*size as usize + 1); + self.messages = next; + Some(msg) + } else { + None + } + } +} + +fn by_msg(source: &[u8]) -> ByMsg { + ByMsg {messages: source} +} + +const VALID_MESSAGES: &[u8] = + b"M#+LlRrUuDdZzAaSjJ,NpPwtgfhbc12345\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A"; +const NON_TIMED_MESSAGES: &[u8] = b"M#hb"; + +#[cfg(canhazslicepatterns)] +fn is_msg_valid(msg: &[u8], team_indices: &[u8]) -> bool { + match msg { + [size, typ, body..] => VALID_MESSAGES.contains(typ) + && match body { + [1...MAX_HEDGEHOGS_PER_TEAM, team, ..] if *typ == b'h' => + team_indices.contains(team), + _ => *typ != b'h' + }, + _ => false + } +} + +fn is_msg_valid(msg: &[u8], _team_indices: &[u8]) -> bool { + if let Some(typ) = msg.get(1) { + VALID_MESSAGES.contains(typ) + } else { + false + } +} + +fn is_msg_empty(msg: &[u8]) -> bool { + msg.get(1).filter(|t| **t == b'+').is_some() +} + +fn is_msg_timed(msg: &[u8]) -> bool { + msg.get(1).filter(|t| !NON_TIMED_MESSAGES.contains(t)).is_some() +} + +fn voting_description(kind: &VoteType) -> String { + format!("New voting started: {}", match kind { + VoteType::Kick(nick) => format!("kick {}", nick), + VoteType::Map(name) => format!("map {}", name.as_ref().unwrap()), + VoteType::Pause => "pause".to_string(), + VoteType::NewSeed => "new seed".to_string(), + VoteType::HedgehogsPerTeam(number) => format!("hedgehogs per team: {}", number) + }) +} + +fn room_message_flag(msg: &HWProtocolMessage) -> RoomFlags { + use crate::protocol::messages::HWProtocolMessage::*; + match msg { + ToggleRestrictJoin => RoomFlags::RESTRICTED_JOIN, + ToggleRestrictTeams => RoomFlags::RESTRICTED_TEAM_ADD, + ToggleRegisteredOnly => RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS, + _ => RoomFlags::empty() + } +} + +pub fn handle(server: &mut HWServer, client_id: ClientId, room_id: RoomId, message: HWProtocolMessage) { + use crate::protocol::messages::HWProtocolMessage::*; + match message { + Part(None) => server.react(client_id, vec![ + MoveToLobby("part".to_string())]), + Part(Some(msg)) => server.react(client_id, vec![ + MoveToLobby(format!("part: {}", msg))]), + Chat(msg) => { + let actions = { + let c = &mut server.clients[client_id]; + let chat_msg = ChatMsg {nick: c.nick.clone(), msg}; + vec![chat_msg.send_all().in_room(room_id).but_self().action()] + }; + server.react(client_id, actions); + }, + Fix => { + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_admin() { r.set_is_fixed(true) } + } + } + Unfix => { + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_admin() { r.set_is_fixed(false) } + } + } + Greeting(text) => { + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_admin() || c.is_master() && !r.is_fixed() { + r.greeting = text + } + } + } + RoomName(new_name) => { + let actions = + if is_name_illegal(&new_name) { + vec![Warn("Illegal room name! A room name must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] + } else if server.rooms[room_id].is_fixed() { + vec![Warn("Access denied.".to_string())] + } else if server.has_room(&new_name) { + vec![Warn("A room with the same name already exists.".to_string())] + } else { + let mut old_name = new_name.clone(); + swap(&mut server.rooms[room_id].name, &mut old_name); + vec![SendRoomUpdate(Some(old_name))] + }; + server.react(client_id, actions); + }, + ToggleReady => { + if let (c, Some(r)) = server.client_and_room(client_id) { + let flags = if c.is_ready() { + r.ready_players_number -= 1; + "-r" + } else { + r.ready_players_number += 1; + "+r" + }; + + let msg = if c.protocol_number < 38 { + LegacyReady(c.is_ready(), vec![c.nick.clone()]) + } else { + ClientFlags(flags.to_string(), vec![c.nick.clone()]) + }; + + let mut v = vec![msg.send_all().in_room(r.id).action()]; + + if r.is_fixed() && r.ready_players_number == r.players_number { + v.push(StartRoomGame(r.id)) + } + + c.set_is_ready(!c.is_ready()); + server.react(client_id, v); + } + } + AddTeam(info) => { + let mut actions = Vec::new(); + if let (c, Some(r)) = server.client_and_room(client_id) { + if r.teams.len() >= r.team_limit as usize { + actions.push(Warn("Too many teams!".to_string())) + } else if r.addable_hedgehogs() == 0 { + actions.push(Warn("Too many hedgehogs!".to_string())) + } else if r.find_team(|t| t.name == info.name) != None { + actions.push(Warn("There's already a team with same name in the list.".to_string())) + } else if r.game_info.is_some() { + actions.push(Warn("Joining not possible: Round is in progress.".to_string())) + } else if r.is_team_add_restricted() { + actions.push(Warn("This room currently does not allow adding new teams.".to_string())); + } else { + let team = r.add_team(c.id, *info, c.protocol_number < 42); + c.teams_in_game += 1; + c.clan = Some(team.color); + actions.push(TeamAccepted(team.name.clone()) + .send_self().action()); + actions.push(TeamAdd(HWRoom::team_info(&c, team)) + .send_all().in_room(room_id).but_self().action()); + actions.push(TeamColor(team.name.clone(), team.color) + .send_all().in_room(room_id).action()); + actions.push(HedgehogsNumber(team.name.clone(), team.hedgehogs_number) + .send_all().in_room(room_id).action()); + actions.push(SendRoomUpdate(None)); + } + } + server.react(client_id, actions); + }, + RemoveTeam(name) => { + let mut actions = Vec::new(); + if let (c, Some(r)) = server.client_and_room(client_id) { + match r.find_team_owner(&name) { + None => + actions.push(Warn("Error: The team you tried to remove does not exist.".to_string())), + Some((id, _)) if id != client_id => + actions.push(Warn("You can't remove a team you don't own.".to_string())), + Some((_, name)) => { + c.teams_in_game -= 1; + c.clan = r.find_team_color(c.id); + actions.push(Action::RemoveTeam(name.to_string())); + } + } + }; + server.react(client_id, actions); + }, + SetHedgehogsNumber(team_name, number) => { + if let (c, Some(r)) = server.client_and_room(client_id) { + let addable_hedgehogs = r.addable_hedgehogs(); + let actions = if let Some((_, team)) = r.find_team_and_owner_mut(|t| t.name == team_name) { + if !c.is_master() { + vec![ProtocolError("You're not the room master!".to_string())] + } else if number < 1 || number > MAX_HEDGEHOGS_PER_TEAM + || number > addable_hedgehogs + team.hedgehogs_number { + vec![HedgehogsNumber(team.name.clone(), team.hedgehogs_number) + .send_self().action()] + } else { + team.hedgehogs_number = number; + vec![HedgehogsNumber(team.name.clone(), number) + .send_all().in_room(room_id).but_self().action()] + } + } else { + vec![(Warn("No such team.".to_string()))] + }; + server.react(client_id, actions); + } + }, + SetTeamColor(team_name, color) => { + if let (c, Some(r)) = server.client_and_room(client_id) { + let mut owner_id = None; + let actions = if let Some((owner, team)) = r.find_team_and_owner_mut(|t| t.name == team_name) { + if !c.is_master() { + vec![ProtocolError("You're not the room master!".to_string())] + } else if false { + Vec::new() + } else { + owner_id = Some(owner); + team.color = color; + vec![TeamColor(team.name.clone(), color) + .send_all().in_room(room_id).but_self().action()] + } + } else { + vec![(Warn("No such team.".to_string()))] + }; + + if let Some(id) = owner_id { + server.clients[id].clan = Some(color); + } + + server.react(client_id, actions); + }; + }, + Cfg(cfg) => { + if let (c, Some(r)) = server.client_and_room(client_id) { + let actions = if r.is_fixed() { + vec![Warn("Access denied.".to_string())] + } else if !c.is_master() { + vec![ProtocolError("You're not the room master!".to_string())] + } else { + let cfg = match cfg { + GameCfg::Scheme(name, mut values) => { + if c.protocol_number == 49 && values.len() >= 2 { + let mut s = "X".repeat(50); + s.push_str(&values.pop().unwrap()); + values.push(s); + } + GameCfg::Scheme(name, values) + } + cfg => cfg + }; + + let v = vec![cfg.to_server_msg() + .send_all().in_room(r.id).but_self().action()]; + r.set_config(cfg); + v + }; + server.react(client_id, actions); + } + } + Save(name, location) => { + let actions = vec![server_chat(format!("Room config saved as {}", name)) + .send_all().in_room(room_id).action()]; + server.rooms[room_id].save_config(name, location); + server.react(client_id, actions); + } + SaveRoom(filename) => { + if server.clients[client_id].is_admin() { + let actions = match server.rooms[room_id].get_saves() { + Ok(text) => match server.io.write_file(&filename, &text) { + Ok(_) => vec![server_chat("Room configs saved successfully.".to_string()) + .send_self().action()], + Err(e) => { + warn!("Error while writing the config file \"{}\": {}", filename, e); + vec![Warn("Unable to save the room configs.".to_string())] + } + } + Err(e) => { + warn!("Error while serializing the room configs: {}", e); + vec![Warn("Unable to serialize the room configs.".to_string())] + } + }; + server.react(client_id, actions); + } + } + LoadRoom(filename) => { + if server.clients[client_id].is_admin() { + let actions = match server.io.read_file(&filename) { + Ok(text) => match server.rooms[room_id].set_saves(&text) { + Ok(_) => vec![server_chat("Room configs loaded successfully.".to_string()) + .send_self().action()], + Err(e) => { + warn!("Error while deserializing the room configs: {}", e); + vec![Warn("Unable to deserialize the room configs.".to_string())] + } + } + Err(e) => { + warn!("Error while reading the config file \"{}\": {}", filename, e); + vec![Warn("Unable to load the room configs.".to_string())] + } + }; + server.react(client_id, actions); + } + } + Delete(name) => { + let actions = if !server.rooms[room_id].delete_config(&name) { + vec![Warn(format!("Save doesn't exist: {}", name))] + } else { + vec![server_chat(format!("Room config {} has been deleted", name)) + .send_all().in_room(room_id).action()] + }; + server.react(client_id, actions); + } + CallVote(None) => { + server.react(client_id, vec![ + server_chat("Available callvote commands: kick , map , pause, newseed, hedgehogs ".to_string()) + .send_self().action()]) + } + CallVote(Some(kind)) => { + let is_in_game = server.rooms[room_id].game_info.is_some(); + let error = match &kind { + VoteType::Kick(nick) => { + if server.find_client(&nick).filter(|c| c.room_id == Some(room_id)).is_some() { + None + } else { + Some("/callvote kick: No such user!".to_string()) + } + }, + VoteType::Map(None) => { + let names: Vec<_> = server.rooms[room_id].saves.keys().cloned().collect(); + if names.is_empty() { + Some("/callvote map: No maps saved in this room!".to_string()) + } else { + Some(format!("Available maps: {}", names.join(", "))) + } + }, + VoteType::Map(Some(name)) => { + if server.rooms[room_id].saves.get(&name[..]).is_some() { + None + } else { + Some("/callvote map: No such map!".to_string()) + } + }, + VoteType::Pause => { + if is_in_game { + None + } else { + Some("/callvote pause: No game in progress!".to_string()) + } + }, + VoteType::NewSeed => { + None + }, + VoteType::HedgehogsPerTeam(number) => { + match number { + 1...MAX_HEDGEHOGS_PER_TEAM => None, + _ => Some("/callvote hedgehogs: Specify number from 1 to 8.".to_string()) + } + }, + }; + match error { + None => { + let msg = voting_description(&kind); + let voting = Voting::new(kind, server.room_clients(client_id)); + server.rooms[room_id].voting = Some(voting); + server.react(client_id, vec![ + server_chat(msg).send_all().in_room(room_id).action(), + AddVote{ vote: true, is_forced: false}]); + } + Some(msg) => { + server.react(client_id, vec![ + server_chat(msg).send_self().action()]) + } + } + } + Vote(vote) => { + server.react(client_id, vec![AddVote{ vote, is_forced: false }]); + } + ForceVote(vote) => { + let is_forced = server.clients[client_id].is_admin(); + server.react(client_id, vec![AddVote{ vote, is_forced }]); + } + ToggleRestrictJoin | ToggleRestrictTeams | ToggleRegisteredOnly => { + if server.clients[client_id].is_master() { + server.rooms[room_id].flags.toggle(room_message_flag(&message)); + } + server.react(client_id, vec![SendRoomUpdate(None)]); + } + StartGame => { + server.react(client_id, vec![StartRoomGame(room_id)]); + } + EngineMessage(em) => { + let mut actions = Vec::new(); + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.teams_in_game > 0 { + let decoding = decode(&em[..]).unwrap(); + let messages = by_msg(&decoding); + let valid = messages.filter(|m| is_msg_valid(m, &c.team_indices)); + let non_empty = valid.clone().filter(|m| !is_msg_empty(m)); + let sync_msg = valid.clone().filter(|m| is_msg_timed(m)) + .last().map(|m| if is_msg_empty(m) {Some(encode(m))} else {None}); + + let em_response = encode(&valid.flat_map(|msg| msg).cloned().collect::>()); + if !em_response.is_empty() { + actions.push(ForwardEngineMessage(vec![em_response]) + .send_all().in_room(r.id).but_self().action()); + } + let em_log = encode(&non_empty.flat_map(|msg| msg).cloned().collect::>()); + if let Some(ref mut info) = r.game_info { + if !em_log.is_empty() { + info.msg_log.push(em_log); + } + if let Some(msg) = sync_msg { + info.sync_msg = msg; + } + } + } + } + server.react(client_id, actions) + } + RoundFinished => { + let mut actions = Vec::new(); + if let (c, Some(r)) = server.client_and_room(client_id) { + if c.is_in_game() { + c.set_is_in_game(false); + actions.push(ClientFlags("-g".to_string(), vec![c.nick.clone()]). + send_all().in_room(r.id).action()); + if r.game_info.is_some() { + for team in r.client_teams(c.id) { + actions.push(SendTeamRemovalMessage(team.name.clone())); + } + } + } + } + server.react(client_id, actions) + }, + Rnd(v) => { + let result = rnd_reply(&v); + let mut echo = vec!["/rnd".to_string()]; + echo.extend(v.into_iter()); + let chat_msg = ChatMsg { + nick: server.clients[client_id].nick.clone(), + msg: echo.join(" ") + }; + server.react(client_id, vec![ + chat_msg.send_all().in_room(room_id).action(), + result.send_all().in_room(room_id).action()]) + }, + _ => warn!("Unimplemented!") + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/handlers/lobby.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/handlers/lobby.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,72 @@ +use mio; + +use crate::{ + server::{ + core::HWServer, + coretypes::ClientId, + actions::{Action, Action::*} + }, + protocol::messages::{ + HWProtocolMessage, + HWServerMessage::* + }, + utils::is_name_illegal +}; +use super::common::rnd_reply; +use log::*; + +pub fn handle(server: &mut HWServer, client_id: ClientId, message: HWProtocolMessage) { + use crate::protocol::messages::HWProtocolMessage::*; + match message { + CreateRoom(name, password) => { + let actions = + if is_name_illegal(&name) { + vec![Warn("Illegal room name! A room name must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] + } else if server.has_room(&name) { + vec![Warn("A room with the same name already exists.".to_string())] + } else { + let flags_msg = ClientFlags( + "+hr".to_string(), + vec![server.clients[client_id].nick.clone()]); + vec![AddRoom(name, password), + flags_msg.send_self().action()] + }; + server.react(client_id, actions); + }, + Chat(msg) => { + let actions = vec![ChatMsg {nick: server.clients[client_id].nick.clone(), msg} + .send_all().in_room(server.lobby_id).but_self().action()]; + server.react(client_id, actions); + }, + JoinRoom(name, _password) => { + let room = server.rooms.iter().find(|(_, r)| r.name == name); + let room_id = room.map(|(_, r)| r.id); + let nicks = server.clients.iter() + .filter(|(_, c)| c.room_id == room_id) + .map(|(_, c)| c.nick.clone()) + .collect(); + let c = &mut server.clients[client_id]; + + let actions = if let Some((_, r)) = room { + if c.protocol_number != r.protocol_number { + vec![Warn("Room version incompatible to your Hedgewars version!".to_string())] + } else if r.is_join_restricted() { + vec![Warn("Access denied. This room currently doesn't allow joining.".to_string())] + } else if r.players_number == u8::max_value() { + vec![Warn("This room is already full".to_string())] + } else { + vec![MoveToRoom(r.id), + RoomJoined(nicks).send_self().action()] + } + } else { + vec![Warn("No such room.".to_string())] + }; + server.react(client_id, actions); + }, + Rnd(v) => { + server.react(client_id, vec![rnd_reply(&v).send_self().action()]); + }, + List => warn!("Deprecated LIST message received"), + _ => warn!("Incorrect command in lobby state"), + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/handlers/loggingin.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/handlers/loggingin.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,99 @@ +use mio; + +use crate::{ + server::{ + client::HWClient, + core::HWServer, + coretypes::ClientId, + actions::{Action, Action::*} + }, + protocol::messages::{ + HWProtocolMessage, HWServerMessage::* + }, + utils::is_name_illegal +}; +#[cfg(feature = "official-server")] +use openssl::sha::sha1; +use std::fmt::{Formatter, LowerHex}; +use log::*; + +#[derive(PartialEq)] +struct Sha1Digest([u8; 20]); + +impl LowerHex for Sha1Digest { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +#[cfg(feature = "official-server")] +fn get_hash(client: &HWClient, salt1: &str, salt2: &str) -> Sha1Digest { + let s = format!("{}{}{}{}{}", salt1, salt2, + client.web_password, client.protocol_number, "!hedgewars"); + Sha1Digest(sha1(s.as_bytes())) +} + +pub fn handle(server: & mut HWServer, client_id: ClientId, message: HWProtocolMessage) { + match message { + HWProtocolMessage::Nick(nick) => { + let client = &mut server.clients[client_id]; + debug!("{} {}", nick, is_name_illegal(&nick)); + let actions = if client.room_id != None { + unreachable!() + } + else if !client.nick.is_empty() { + vec![ProtocolError("Nickname already provided.".to_string())] + } + else if is_name_illegal(&nick) { + vec![ByeClient("Illegal nickname! Nicknames must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string())] + } + else { + client.nick = nick.clone(); + vec![Nick(nick).send_self().action(), + CheckRegistered] + }; + + server.react(client_id, actions); + } + HWProtocolMessage::Proto(proto) => { + let client = &mut server.clients[client_id]; + let actions = if client.protocol_number != 0 { + vec![ProtocolError("Protocol already known.".to_string())] + } + else if proto == 0 { + vec![ProtocolError("Bad number.".to_string())] + } + else { + client.protocol_number = proto; + vec![Proto(proto).send_self().action(), + CheckRegistered] + }; + server.react(client_id, actions); + } + #[cfg(feature = "official-server")] + HWProtocolMessage::Password(hash, salt) => { + let c = &server.clients[client_id]; + + let client_hash = get_hash(c, &salt, &c.server_salt); + let server_hash = get_hash(c, &c.server_salt, &salt); + let actions = if client_hash == server_hash { + vec![ServerAuth(format!("{:x}", server_hash)).send_self().action(), + JoinLobby] + } else { + vec![ByeClient("Authentication failed".to_string())] + }; + server.react(client_id, actions); + } + #[cfg(feature = "official-server")] + HWProtocolMessage::Checker(protocol, nick, password) => { + let c = &mut server.clients[client_id]; + c.nick = nick; + c.web_password = password; + c.set_is_checker(true); + } + _ => warn!("Incorrect command in logging-in state"), + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/io.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/io.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,49 @@ +use std::{ + fs::{File, OpenOptions}, + io::{Read, Write, Result, Error, ErrorKind} +}; + +pub trait HWServerIO { + fn write_file(&mut self, name: &str, content: &str) -> Result<()>; + fn read_file(&mut self, name: &str) -> Result; +} + +pub struct EmptyServerIO {} + +impl EmptyServerIO { + pub fn new() -> Self { + Self {} + } +} + +impl HWServerIO for EmptyServerIO { + fn write_file(&mut self, _name: &str, _content: &str) -> Result<()> { + Ok(()) + } + + fn read_file(&mut self, _name: &str) -> Result { + Ok("".to_string()) + } +} + +pub struct FileServerIO {} + +impl FileServerIO { + pub fn new() -> Self { + Self {} + } +} + +impl HWServerIO for FileServerIO { + fn write_file(&mut self, name: &str, content: &str) -> Result<()> { + let mut writer = OpenOptions::new().create(true).write(true).open(name)?; + writer.write_all(content.as_bytes()) + } + + fn read_file(&mut self, name: &str) -> Result { + let mut reader = File::open(name)?; + let mut result = String::new(); + reader.read_to_string(&mut result)?; + Ok(result) + } +} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/network.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/network.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,444 @@ +extern crate slab; + +use std::{ + io, io::{Error, ErrorKind, Read, Write}, + net::{SocketAddr, IpAddr, Ipv4Addr}, + collections::HashSet, + mem::{swap, replace} +}; + +use mio::{ + net::{TcpStream, TcpListener}, + Poll, PollOpt, Ready, Token +}; +use netbuf; +use slab::Slab; +use log::*; + +use crate::{ + utils, + protocol::{ProtocolDecoder, messages::*} +}; +use super::{ + io::FileServerIO, + core::{HWServer}, + coretypes::ClientId +}; +#[cfg(feature = "tls-connections")] +use openssl::{ + ssl::{ + SslMethod, SslContext, Ssl, SslContextBuilder, + SslVerifyMode, SslFiletype, SslOptions, + SslStreamBuilder, HandshakeError, MidHandshakeSslStream, SslStream + }, + error::ErrorStack +}; + +const MAX_BYTES_PER_READ: usize = 2048; + +#[derive(Hash, Eq, PartialEq, Copy, Clone)] +pub enum NetworkClientState { + Idle, + NeedsWrite, + NeedsRead, + Closed, +} + +type NetworkResult = io::Result<(T, NetworkClientState)>; + +#[cfg(not(feature = "tls-connections"))] +pub enum ClientSocket { + Plain(TcpStream) +} + +#[cfg(feature = "tls-connections")] +pub enum ClientSocket { + SslHandshake(Option>), + SslStream(SslStream) +} + +impl ClientSocket { + fn inner(&self) -> &TcpStream { + #[cfg(not(feature = "tls-connections"))] + match self { + ClientSocket::Plain(stream) => stream, + } + + #[cfg(feature = "tls-connections")] + match self { + ClientSocket::SslHandshake(Some(builder)) => builder.get_ref(), + ClientSocket::SslHandshake(None) => unreachable!(), + ClientSocket::SslStream(ssl_stream) => ssl_stream.get_ref() + } + } +} + +pub struct NetworkClient { + id: ClientId, + socket: ClientSocket, + peer_addr: SocketAddr, + decoder: ProtocolDecoder, + buf_out: netbuf::Buf +} + +impl NetworkClient { + pub fn new(id: ClientId, socket: ClientSocket, peer_addr: SocketAddr) -> NetworkClient { + NetworkClient { + id, socket, peer_addr, + decoder: ProtocolDecoder::new(), + buf_out: netbuf::Buf::new() + } + } + + #[cfg(feature = "tls-connections")] + fn handshake_impl(&mut self, handshake: MidHandshakeSslStream) -> io::Result { + match handshake.handshake() { + Ok(stream) => { + self.socket = ClientSocket::SslStream(stream); + debug!("TLS handshake with {} ({}) completed", self.id, self.peer_addr); + Ok(NetworkClientState::Idle) + } + Err(HandshakeError::WouldBlock(new_handshake)) => { + self.socket = ClientSocket::SslHandshake(Some(new_handshake)); + Ok(NetworkClientState::Idle) + } + Err(HandshakeError::Failure(new_handshake)) => { + self.socket = ClientSocket::SslHandshake(Some(new_handshake)); + debug!("TLS handshake with {} ({}) failed", self.id, self.peer_addr); + Err(Error::new(ErrorKind::Other, "Connection failure")) + } + Err(HandshakeError::SetupFailure(_)) => unreachable!() + } + } + + fn read_impl(decoder: &mut ProtocolDecoder, source: &mut R, + id: ClientId, addr: &SocketAddr) -> NetworkResult> { + let mut bytes_read = 0; + let result = loop { + match decoder.read_from(source) { + Ok(bytes) => { + debug!("Client {}: read {} bytes", id, bytes); + bytes_read += bytes; + if bytes == 0 { + let result = if bytes_read == 0 { + info!("EOF for client {} ({})", id, addr); + (Vec::new(), NetworkClientState::Closed) + } else { + (decoder.extract_messages(), NetworkClientState::NeedsRead) + }; + break Ok(result); + } + else if bytes_read >= MAX_BYTES_PER_READ { + break Ok((decoder.extract_messages(), NetworkClientState::NeedsRead)) + } + } + Err(ref error) if error.kind() == ErrorKind::WouldBlock => { + let messages = if bytes_read == 0 { + Vec::new() + } else { + decoder.extract_messages() + }; + break Ok((messages, NetworkClientState::Idle)); + } + Err(error) => + break Err(error) + } + }; + decoder.sweep(); + result + } + + pub fn read(&mut self) -> NetworkResult> { + #[cfg(not(feature = "tls-connections"))] + match self.socket { + ClientSocket::Plain(ref mut stream) => + NetworkClient::read_impl(&mut self.decoder, stream, self.id, &self.peer_addr), + } + + #[cfg(feature = "tls-connections")] + match self.socket { + ClientSocket::SslHandshake(ref mut handshake_opt) => { + let handshake = std::mem::replace(handshake_opt, None).unwrap(); + Ok((Vec::new(), self.handshake_impl(handshake)?)) + }, + ClientSocket::SslStream(ref mut stream) => + NetworkClient::read_impl(&mut self.decoder, stream, self.id, &self.peer_addr) + } + } + + fn write_impl(buf_out: &mut netbuf::Buf, destination: &mut W) -> NetworkResult<()> { + let result = loop { + match buf_out.write_to(destination) { + Ok(bytes) if buf_out.is_empty() || bytes == 0 => + break Ok(((), NetworkClientState::Idle)), + Ok(_) => (), + Err(ref error) if error.kind() == ErrorKind::Interrupted + || error.kind() == ErrorKind::WouldBlock => { + break Ok(((), NetworkClientState::NeedsWrite)); + }, + Err(error) => + break Err(error) + } + }; + result + } + + pub fn write(&mut self) -> NetworkResult<()> { + let result = { + #[cfg(not(feature = "tls-connections"))] + match self.socket { + ClientSocket::Plain(ref mut stream) => + NetworkClient::write_impl(&mut self.buf_out, stream) + } + + #[cfg(feature = "tls-connections")] { + match self.socket { + ClientSocket::SslHandshake(ref mut handshake_opt) => { + let handshake = std::mem::replace(handshake_opt, None).unwrap(); + Ok(((), self.handshake_impl(handshake)?)) + } + ClientSocket::SslStream(ref mut stream) => + NetworkClient::write_impl(&mut self.buf_out, stream) + } + } + }; + + self.socket.inner().flush()?; + result + } + + pub fn send_raw_msg(&mut self, msg: &[u8]) { + self.buf_out.write_all(msg).unwrap(); + } + + pub fn send_string(&mut self, msg: &str) { + self.send_raw_msg(&msg.as_bytes()); + } + + pub fn send_msg(&mut self, msg: &HWServerMessage) { + self.send_string(&msg.to_raw_protocol()); + } +} + +#[cfg(feature = "tls-connections")] +struct ServerSsl { + context: SslContext +} + +pub struct NetworkLayer { + listener: TcpListener, + server: HWServer, + clients: Slab, + pending: HashSet<(ClientId, NetworkClientState)>, + pending_cache: Vec<(ClientId, NetworkClientState)>, + #[cfg(feature = "tls-connections")] + ssl: ServerSsl +} + +impl NetworkLayer { + pub fn new(listener: TcpListener, clients_limit: usize, rooms_limit: usize) -> NetworkLayer { + let server = HWServer::new(clients_limit, rooms_limit, Box::new(FileServerIO::new())); + let clients = Slab::with_capacity(clients_limit); + let pending = HashSet::with_capacity(2 * clients_limit); + let pending_cache = Vec::with_capacity(2 * clients_limit); + + NetworkLayer { + listener, server, clients, pending, pending_cache, + #[cfg(feature = "tls-connections")] + ssl: NetworkLayer::create_ssl_context() + } + } + + #[cfg(feature = "tls-connections")] + fn create_ssl_context() -> ServerSsl { + let mut builder = SslContextBuilder::new(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + builder.set_read_ahead(true); + builder.set_certificate_file("ssl/cert.pem", SslFiletype::PEM).unwrap(); + builder.set_private_key_file("ssl/key.pem", SslFiletype::PEM).unwrap(); + builder.set_options(SslOptions::NO_COMPRESSION); + builder.set_cipher_list("DEFAULT:!LOW:!RC4:!EXP").unwrap(); + ServerSsl { context: builder.build() } + } + + pub fn register_server(&self, poll: &Poll) -> io::Result<()> { + poll.register(&self.listener, utils::SERVER, Ready::readable(), + PollOpt::edge()) + } + + fn deregister_client(&mut self, poll: &Poll, id: ClientId) { + let mut client_exists = false; + if let Some(ref client) = self.clients.get(id) { + poll.deregister(client.socket.inner()) + .expect("could not deregister socket"); + info!("client {} ({}) removed", client.id, client.peer_addr); + client_exists = true; + } + if client_exists { + self.clients.remove(id); + } + } + + fn register_client(&mut self, poll: &Poll, id: ClientId, client_socket: ClientSocket, addr: SocketAddr) { + poll.register(client_socket.inner(), Token(id), + Ready::readable() | Ready::writable(), + PollOpt::edge()) + .expect("could not register socket with event loop"); + + let entry = self.clients.vacant_entry(); + let client = NetworkClient::new(id, client_socket, addr); + info!("client {} ({}) added", client.id, client.peer_addr); + entry.insert(client); + } + + fn flush_server_messages(&mut self) { + debug!("{} pending server messages", self.server.output.len()); + for (clients, message) in self.server.output.drain(..) { + debug!("Message {:?} to {:?}", message, clients); + let msg_string = message.to_raw_protocol(); + for client_id in clients { + if let Some(client) = self.clients.get_mut(client_id) { + client.send_string(&msg_string); + self.pending.insert((client_id, NetworkClientState::NeedsWrite)); + } + } + } + } + + fn create_client_socket(&self, socket: TcpStream) -> io::Result { + #[cfg(not(feature = "tls-connections"))] { + Ok(ClientSocket::Plain(socket)) + } + + #[cfg(feature = "tls-connections")] { + let ssl = Ssl::new(&self.ssl.context).unwrap(); + let mut builder = SslStreamBuilder::new(ssl, socket); + builder.set_accept_state(); + match builder.handshake() { + Ok(stream) => + Ok(ClientSocket::SslStream(stream)), + Err(HandshakeError::WouldBlock(stream)) => + Ok(ClientSocket::SslHandshake(Some(stream))), + Err(e) => { + debug!("OpenSSL handshake failed: {}", e); + Err(Error::new(ErrorKind::Other, "Connection failure")) + } + } + } + } + + pub fn accept_client(&mut self, poll: &Poll) -> io::Result<()> { + let (client_socket, addr) = self.listener.accept()?; + info!("Connected: {}", addr); + + let client_id = self.server.add_client(); + self.register_client(poll, client_id, self.create_client_socket(client_socket)?, addr); + self.flush_server_messages(); + + Ok(()) + } + + fn operation_failed(&mut self, poll: &Poll, client_id: ClientId, error: &Error, msg: &str) -> io::Result<()> { + let addr = if let Some(ref mut client) = self.clients.get_mut(client_id) { + client.peer_addr + } else { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) + }; + debug!("{}({}): {}", msg, addr, error); + self.client_error(poll, client_id) + } + + pub fn client_readable(&mut self, poll: &Poll, + client_id: ClientId) -> io::Result<()> { + let messages = + if let Some(ref mut client) = self.clients.get_mut(client_id) { + client.read() + } else { + warn!("invalid readable client: {}", client_id); + Ok((Vec::new(), NetworkClientState::Idle)) + }; + + match messages { + Ok((messages, state)) => { + for message in messages { + self.server.handle_msg(client_id, message); + } + match state { + NetworkClientState::NeedsRead => { + self.pending.insert((client_id, state)); + }, + NetworkClientState::Closed => + self.client_error(&poll, client_id)?, + _ => {} + }; + } + Err(e) => self.operation_failed( + poll, client_id, &e, + "Error while reading from client socket")? + } + + self.flush_server_messages(); + + if !self.server.removed_clients.is_empty() { + let ids: Vec<_> = self.server.removed_clients.drain(..).collect(); + for client_id in ids { + self.deregister_client(poll, client_id); + } + } + + Ok(()) + } + + pub fn client_writable(&mut self, poll: &Poll, + client_id: ClientId) -> io::Result<()> { + let result = + if let Some(ref mut client) = self.clients.get_mut(client_id) { + client.write() + } else { + warn!("invalid writable client: {}", client_id); + Ok(((), NetworkClientState::Idle)) + }; + + match result { + Ok(((), state)) if state == NetworkClientState::NeedsWrite => { + self.pending.insert((client_id, state)); + }, + Ok(_) => {} + Err(e) => self.operation_failed( + poll, client_id, &e, + "Error while writing to client socket")? + } + + Ok(()) + } + + pub fn client_error(&mut self, poll: &Poll, + client_id: ClientId) -> io::Result<()> { + self.deregister_client(poll, client_id); + self.server.client_lost(client_id); + + Ok(()) + } + + pub fn has_pending_operations(&self) -> bool { + !self.pending.is_empty() + } + + pub fn on_idle(&mut self, poll: &Poll) -> io::Result<()> { + if self.has_pending_operations() { + let mut cache = replace(&mut self.pending_cache, Vec::new()); + cache.extend(self.pending.drain()); + for (id, state) in cache.drain(..) { + match state { + NetworkClientState::NeedsRead => + self.client_readable(poll, id)?, + NetworkClientState::NeedsWrite => + self.client_writable(poll, id)?, + _ => {} + } + } + swap(&mut cache, &mut self.pending_cache); + } + Ok(()) + } +} diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/server/room.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/room.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,391 @@ +use std::{ + iter, collections::HashMap +}; +use crate::server::{ + coretypes::{ + ClientId, RoomId, TeamInfo, GameCfg, GameCfg::*, Voting, + MAX_HEDGEHOGS_PER_TEAM + }, + client::{HWClient} +}; +use bitflags::*; +use serde::{Serialize, Deserialize}; +use serde_derive::{Serialize, Deserialize}; +use serde_yaml; + +const MAX_TEAMS_IN_ROOM: u8 = 8; +const MAX_HEDGEHOGS_IN_ROOM: u8 = + MAX_HEDGEHOGS_PER_TEAM * MAX_HEDGEHOGS_PER_TEAM; + +#[derive(Clone, Serialize, Deserialize)] +struct Ammo { + name: String, + settings: Option +} + +#[derive(Clone, Serialize, Deserialize)] +struct Scheme { + name: String, + settings: Vec +} + +#[derive(Clone, Serialize, Deserialize)] +struct RoomConfig { + feature_size: u32, + map_type: String, + map_generator: u32, + maze_size: u32, + seed: String, + template: u32, + + ammo: Ammo, + scheme: Scheme, + script: String, + theme: String, + drawn_map: Option +} + +impl RoomConfig { + 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 + } + } +} + +fn client_teams_impl(teams: &[(ClientId, TeamInfo)], client_id: ClientId) + -> impl Iterator + Clone +{ + teams.iter().filter(move |(id, _)| *id == client_id).map(|(_, t)| t) +} + +fn map_config_from(c: &RoomConfig) -> Vec { + vec![c.feature_size.to_string(), c.map_type.to_string(), + c.map_generator.to_string(), c.maze_size.to_string(), + c.seed.to_string(), c.template.to_string()] +} + +fn game_config_from(c: &RoomConfig) -> Vec { + use crate::server::coretypes::GameCfg::*; + let mut v = vec![ + Ammo(c.ammo.name.to_string(), c.ammo.settings.clone()), + Scheme(c.scheme.name.to_string(), c.scheme.settings.clone()), + Script(c.script.to_string()), + Theme(c.theme.to_string())]; + if let Some(ref m) = c.drawn_map { + v.push(DrawnMap(m.to_string())) + } + v +} + +pub struct GameInfo { + pub teams_in_game: u8, + pub teams_at_start: Vec<(ClientId, TeamInfo)>, + pub left_teams: Vec, + pub msg_log: Vec, + pub sync_msg: Option, + pub is_paused: bool, + config: RoomConfig +} + +impl GameInfo { + fn new(teams: Vec<(ClientId, TeamInfo)>, config: RoomConfig) -> GameInfo { + GameInfo { + left_teams: Vec::new(), + msg_log: Vec::new(), + sync_msg: None, + is_paused: false, + teams_in_game: teams.len() as u8, + teams_at_start: teams, + config + } + } + + pub fn client_teams(&self, client_id: ClientId) -> impl Iterator + Clone { + client_teams_impl(&self.teams_at_start, client_id) + } +} + +#[derive(Serialize, Deserialize)] +pub struct RoomSave { + pub location: String, + config: RoomConfig +} + +bitflags!{ + pub struct RoomFlags: u8 { + const FIXED = 0b0000_0001; + const RESTRICTED_JOIN = 0b0000_0010; + const RESTRICTED_TEAM_ADD = 0b0000_0100; + const RESTRICTED_UNREGISTERED_PLAYERS = 0b0000_1000; + } +} + +pub struct HWRoom { + pub id: RoomId, + pub master_id: Option, + pub name: String, + pub password: Option, + pub greeting: String, + pub protocol_number: u16, + pub flags: RoomFlags, + + pub players_number: u8, + pub default_hedgehog_number: u8, + pub team_limit: u8, + pub ready_players_number: u8, + pub teams: Vec<(ClientId, TeamInfo)>, + config: RoomConfig, + pub voting: Option, + pub saves: HashMap, + pub game_info: Option +} + +impl HWRoom { + pub fn new(id: RoomId) -> HWRoom { + HWRoom { + id, + master_id: None, + name: String::new(), + password: None, + greeting: "".to_string(), + flags: RoomFlags::empty(), + protocol_number: 0, + players_number: 0, + default_hedgehog_number: 4, + team_limit: MAX_TEAMS_IN_ROOM, + ready_players_number: 0, + teams: Vec::new(), + config: RoomConfig::new(), + voting: None, + saves: HashMap::new(), + game_info: None + } + } + + pub fn hedgehogs_number(&self) -> u8 { + self.teams.iter().map(|(_, t)| t.hedgehogs_number).sum() + } + + pub fn addable_hedgehogs(&self) -> u8 { + MAX_HEDGEHOGS_IN_ROOM - self.hedgehogs_number() + } + + pub fn add_team(&mut self, owner_id: ClientId, mut team: TeamInfo, preserve_color: bool) -> &TeamInfo { + if !preserve_color { + team.color = iter::repeat(()).enumerate() + .map(|(i, _)| i as u8).take(u8::max_value() as usize + 1) + .find(|i| self.teams.iter().all(|(_, t)| t.color != *i)) + .unwrap_or(0u8) + }; + team.hedgehogs_number = if self.teams.is_empty() { + self.default_hedgehog_number + } else { + self.teams[0].1.hedgehogs_number.min(self.addable_hedgehogs()) + }; + self.teams.push((owner_id, team)); + &self.teams.last().unwrap().1 + } + + pub fn remove_team(&mut self, name: &str) { + if let Some(index) = self.teams.iter().position(|(_, t)| t.name == name) { + self.teams.remove(index); + } + } + + pub fn set_hedgehogs_number(&mut self, n: u8) -> Vec { + let mut names = Vec::new(); + let teams = match self.game_info { + Some(ref mut info) => &mut info.teams_at_start, + None => &mut self.teams + }; + + if teams.len() as u8 * n <= MAX_HEDGEHOGS_IN_ROOM { + for (_, team) in teams.iter_mut() { + team.hedgehogs_number = n; + names.push(team.name.clone()) + }; + self.default_hedgehog_number = n; + } + names + } + + pub fn find_team_and_owner_mut(&mut self, f: F) -> Option<(ClientId, &mut TeamInfo)> + where F: Fn(&TeamInfo) -> bool { + self.teams.iter_mut().find(|(_, t)| f(t)).map(|(id, t)| (*id, t)) + } + + pub fn find_team(&self, f: F) -> Option<&TeamInfo> + where F: Fn(&TeamInfo) -> bool { + self.teams.iter().find_map(|(_, t)| Some(t).filter(|t| f(&t))) + } + + pub fn client_teams(&self, client_id: ClientId) -> impl Iterator { + client_teams_impl(&self.teams, client_id) + } + + pub fn client_team_indices(&self, client_id: ClientId) -> Vec { + self.teams.iter().enumerate() + .filter(move |(_, (id, _))| *id == client_id) + .map(|(i, _)| i as u8).collect() + } + + pub fn find_team_owner(&self, team_name: &str) -> Option<(ClientId, &str)> { + self.teams.iter().find(|(_, t)| t.name == team_name) + .map(|(id, t)| (*id, &t.name[..])) + } + + pub fn find_team_color(&self, owner_id: ClientId) -> Option { + self.client_teams(owner_id).nth(0).map(|t| t.color) + } + + pub fn has_multiple_clans(&self) -> bool { + self.teams.iter().min_by_key(|(_, t)| t.color) != + self.teams.iter().max_by_key(|(_, t)| t.color) + } + + pub fn set_config(&mut self, cfg: GameCfg) { + let c = &mut self.config; + match cfg { + FeatureSize(s) => c.feature_size = s, + MapType(t) => c.map_type = t, + MapGenerator(g) => c.map_generator = g, + MazeSize(s) => c.maze_size = s, + Seed(s) => c.seed = s, + Template(t) => c.template = t, + + Ammo(n, s) => c.ammo = Ammo {name: n, settings: s}, + Scheme(n, s) => c.scheme = Scheme {name: n, settings: s}, + Script(s) => c.script = s, + Theme(t) => c.theme = t, + DrawnMap(m) => c.drawn_map = Some(m) + }; + } + + pub fn start_round(&mut self) { + if self.game_info.is_none() { + self.game_info = Some(GameInfo::new( + self.teams.clone(), self.config.clone())); + } + } + + pub fn is_fixed(&self) -> bool { + self.flags.contains(RoomFlags::FIXED) + } + pub fn is_join_restricted(&self) -> bool { + self.flags.contains(RoomFlags::RESTRICTED_JOIN) + } + pub fn is_team_add_restricted(&self) -> bool { + self.flags.contains(RoomFlags::RESTRICTED_TEAM_ADD) + } + pub fn are_unregistered_players_restricted(&self) -> bool { + self.flags.contains(RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS) + } + + pub fn set_is_fixed(&mut self, value: bool) { + self.flags.set(RoomFlags::FIXED, value) + } + pub fn set_join_restriction(&mut self, value: bool) { + self.flags.set(RoomFlags::RESTRICTED_JOIN, value) + } + pub fn set_team_add_restriction(&mut self, value: bool) { + self.flags.set(RoomFlags::RESTRICTED_TEAM_ADD, value) + } + pub fn set_unregistered_players_restriction(&mut self, value: bool) { + self.flags.set(RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS, value) + } + + fn flags_string(&self) -> String { + let mut result = "-".to_string(); + if self.game_info.is_some() { result += "g" } + if self.password.is_some() { result += "p" } + if self.is_join_restricted() { result += "j" } + if self.are_unregistered_players_restricted() { + result += "r" + } + result + } + + pub fn info(&self, master: Option<&HWClient>) -> Vec { + let c = &self.config; + vec![ + self.flags_string(), + self.name.clone(), + self.players_number.to_string(), + self.teams.len().to_string(), + master.map_or("[]", |c| &c.nick).to_string(), + c.map_type.to_string(), + c.script.to_string(), + c.scheme.name.to_string(), + c.ammo.name.to_string() + ] + } + + pub fn map_config(&self) -> Vec { + match self.game_info { + Some(ref info) => map_config_from(&info.config), + None => map_config_from(&self.config) + } + } + + pub fn game_config(&self) -> Vec { + match self.game_info { + Some(ref info) => game_config_from(&info.config), + None => game_config_from(&self.config) + } + } + + pub fn save_config(&mut self, name: String, location: String) { + self.saves.insert(name, RoomSave { location, config: self.config.clone() }); + } + + pub fn load_config(&mut self, name: &str) -> Option<&str> { + if let Some(save) = self.saves.get(name) { + self.config = save.config.clone(); + Some(&save.location[..]) + } else { + None + } + } + + pub fn delete_config(&mut self, name: &str) -> bool { + self.saves.remove(name).is_some() + } + + pub fn get_saves(&self) -> Result { + serde_yaml::to_string(&(&self.greeting, &self.saves)) + } + + pub fn set_saves(&mut self, text: &str) -> Result<(), serde_yaml::Error> { + serde_yaml::from_str::<(String, HashMap)>(text).map(|(greeting, saves)| { + self.greeting = greeting; + self.saves = saves; + }) + } + + pub fn team_info(owner: &HWClient, team: &TeamInfo) -> Vec { + let mut info = vec![ + team.name.clone(), + team.grave.clone(), + team.fort.clone(), + team.voice_pack.clone(), + team.flag.clone(), + owner.nick.clone(), + team.difficulty.to_string()]; + let hogs = team.hedgehogs.iter().flat_map(|h| + iter::once(h.name.clone()).chain(iter::once(h.hat.clone()))); + info.extend(hogs); + info + } +} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/hedgewars-server/src/utils.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/utils.rs Thu Dec 13 10:51:07 2018 -0500 @@ -0,0 +1,67 @@ +use std::iter::Iterator; +use mio; +use base64::{encode}; + +pub const PROTOCOL_VERSION : u32 = 3; +pub const SERVER: mio::Token = mio::Token(1_000_000_000); + +pub fn is_name_illegal(name: &str ) -> bool{ + name.len() > 40 || + name.trim().is_empty() || + name.chars().any(|c| + "$()*+?[]^{|}\x7F".contains(c) || + '\x00' <= c && c <= '\x1F') +} + +pub fn to_engine_msg(msg: T) -> String + where T: Iterator + Clone +{ + let mut tmp = Vec::new(); + tmp.push(msg.clone().count() as u8); + tmp.extend(msg); + encode(&tmp) +} + +pub fn protocol_version_string(protocol_number: u16) -> &'static str { + match protocol_number { + 17 => "0.9.7-dev", + 19 => "0.9.7", + 20 => "0.9.8-dev", + 21 => "0.9.8", + 22 => "0.9.9-dev", + 23 => "0.9.9", + 24 => "0.9.10-dev", + 25 => "0.9.10", + 26 => "0.9.11-dev", + 27 => "0.9.11", + 28 => "0.9.12-dev", + 29 => "0.9.12", + 30 => "0.9.13-dev", + 31 => "0.9.13", + 32 => "0.9.14-dev", + 33 => "0.9.14", + 34 => "0.9.15-dev", + 35 => "0.9.14.1", + 37 => "0.9.15", + 38 => "0.9.16-dev", + 39 => "0.9.16", + 40 => "0.9.17-dev", + 41 => "0.9.17", + 42 => "0.9.18-dev", + 43 => "0.9.18", + 44 => "0.9.19-dev", + 45 => "0.9.19", + 46 => "0.9.20-dev", + 47 => "0.9.20", + 48 => "0.9.21-dev", + 49 => "0.9.21", + 50 => "0.9.22-dev", + 51 => "0.9.22", + 52 => "0.9.23-dev", + 53 => "0.9.23", + 54 => "0.9.24-dev", + 55 => "0.9.24", + 56 => "0.9.25-dev", + _ => "Unknown" + } +} \ No newline at end of file diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/lib-hedgewars-engine/src/lib.rs --- a/rust/lib-hedgewars-engine/src/lib.rs Thu Dec 13 10:49:30 2018 -0500 +++ b/rust/lib-hedgewars-engine/src/lib.rs Thu Dec 13 10:51:07 2018 -0500 @@ -25,8 +25,8 @@ } #[no_mangle] -pub extern "C" fn protocol_version() -> u32 { - 56 +pub extern "C" fn hedgewars_engine_protocol_version() -> u32 { + 58 } #[no_mangle] @@ -42,14 +42,19 @@ (*engine_state).world.generate_preview(); - let land_preview = (*engine_state).world.preview(); + if let Some(land_preview) = (*engine_state).world.preview() { + *preview = PreviewInfo { + width: land_preview.width() as u32, + height: land_preview.height() as u32, + hedgehogs_number: 0, + land: land_preview.raw_pixels().as_ptr(), + }; + } +} - *preview = PreviewInfo { - width: land_preview.width() as u32, - height: land_preview.height() as u32, - hedgehogs_number: 0, - land: land_preview.raw_pixels().as_ptr(), - }; +#[no_mangle] +pub extern "C" fn dispose_preview(engine_state: &mut EngineInstance, preview: &mut PreviewInfo) { + (*engine_state).world.dispose_preview(); } #[no_mangle] diff -r 1ffa8bfc5c58 -r 94f10f69fe76 rust/lib-hedgewars-engine/src/world.rs --- a/rust/lib-hedgewars-engine/src/world.rs Thu Dec 13 10:49:30 2018 -0500 +++ b/rust/lib-hedgewars-engine/src/world.rs Thu Dec 13 10:51:07 2018 -0500 @@ -26,7 +26,7 @@ pub struct World { random_numbers_gen: LaggedFibonacciPRNG, - preview: Land2D, + preview: Option>, game_state: Option, } @@ -34,7 +34,7 @@ pub fn new() -> Self { Self { random_numbers_gen: LaggedFibonacciPRNG::new(&[]), - preview: Land2D::new(Size::new(0, 0), 0), + preview: None, game_state: None, } } @@ -43,7 +43,7 @@ self.random_numbers_gen = LaggedFibonacciPRNG::new(seed); } - pub fn preview(&self) -> &Land2D { + pub fn preview(&self) -> &Option> { &self.preview } @@ -63,7 +63,11 @@ let params = LandGenerationParameters::new(0u8, u8::max_value(), 5, false, false); let landgen = TemplatedLandGenerator::new(template()); - self.preview = landgen.generate_land(¶ms, &mut self.random_numbers_gen); + self.preview = Some(landgen.generate_land(¶ms, &mut self.random_numbers_gen)); + } + + pub fn dispose_preview(&mut self) { + self.preview = None } pub fn init(&mut self, template: OutlineTemplate) { diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Graphics/Finger.png Binary file share/hedgewars/Data/Graphics/Finger.png has changed diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Graphics/Switch.png Binary file share/hedgewars/Data/Graphics/Switch.png has changed diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Graphics/Targetp.png Binary file share/hedgewars/Data/Graphics/Targetp.png has changed diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Graphics/Targetp@2x.png Binary file share/hedgewars/Data/Graphics/Targetp@2x.png has changed diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Locale/de.txt --- a/share/hedgewars/Data/Locale/de.txt Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Locale/de.txt Thu Dec 13 10:51:07 2018 -0500 @@ -1420,3 +1420,5 @@ 06:24=/shrug: Igel mit den Achseln zucken lassen 06:25=/wave: Igel winken lassen 06:26=Unbekannter Befehl oder ungültige Parameter. Sag »/help« im Chat für eine Liste an Befehlen. +06:27=/help room: Raum-Chatbefehle auflisten +06:28=Du bist nicht online! diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Locale/en.txt --- a/share/hedgewars/Data/Locale/en.txt Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Locale/en.txt Thu Dec 13 10:51:07 2018 -0500 @@ -1325,3 +1325,5 @@ 06:24=/shrug: Make hedgehog shrug 06:25=/wave: Make hedgehog wave its hand 06:26=Unknown command or invalid parameters. Say “/help” in chat for a list of commands. +06:27=/help room: List room chat commands +06:28=You're not online! diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Maps/Basketball/map.lua --- a/share/hedgewars/Data/Maps/Basketball/map.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Maps/Basketball/map.lua Thu Dec 13 10:51:07 2018 -0500 @@ -10,7 +10,6 @@ CaseFreq = 0 MinesNum = 0 Explosives = 0 - Delay = 500 Map = 'BasketballField' -- Disable Sudden Death WaterRise = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Maps/FlightJoust/map.lua --- a/share/hedgewars/Data/Maps/FlightJoust/map.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Maps/FlightJoust/map.lua Thu Dec 13 10:51:07 2018 -0500 @@ -31,7 +31,6 @@ CaseFreq = 0 MinesNum = 0 Explosives = 0 - Delay = 500 SuddenDeathTurns = 99999 -- "disable" sudden death Theme = Compost end diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Maps/Knockball/map.lua --- a/share/hedgewars/Data/Maps/Knockball/map.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Maps/Knockball/map.lua Thu Dec 13 10:51:07 2018 -0500 @@ -10,7 +10,6 @@ CaseFreq = 0 MinesNum = 0 Explosives = 0 - Delay = 500 -- Disable Sudden Death WaterRise = 0 HealthDecrease = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/backstab.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/backstab.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/backstab.lua Thu Dec 13 10:51:07 2018 -0500 @@ -1016,7 +1016,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 Map = "Cave" Theme = "Nature" WaterRise = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/enemy.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/enemy.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/enemy.lua Thu Dec 13 10:51:07 2018 -0500 @@ -598,7 +598,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 Map = "Islands" Theme = "EarthRise" SuddenDeathTurns = 20 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/epil.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/epil.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/epil.lua Thu Dec 13 10:51:07 2018 -0500 @@ -395,7 +395,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 Map = "Hogville" Theme = "Nature" -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/family.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/family.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/family.lua Thu Dec 13 10:51:07 2018 -0500 @@ -580,7 +580,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 MapGen = mgDrawn Theme = "Hell" SuddenDeathTurns = 35 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/first_blood.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/first_blood.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/first_blood.lua Thu Dec 13 10:51:07 2018 -0500 @@ -150,20 +150,32 @@ princessFace = "Left" elderFace = "Left" +local ctrlJump, ctrlMissionPanel, ctrlAttack +if INTERFACE == "touch" then + ctrlJump = loc("Long Jump: Tap the [Curvy Arrow] button for long") + ctrlMissionPanel = loc("Hint: Pause the game to review the mission texts.") + ctrlAttack = loc("Attack: Tap the [Bomb]") +else + ctrlJump = loc("Long Jump: [Enter]") + ctrlMissionPanel = loc("Hint: Hold down [M] to review the mission texts.") + ctrlAttack = loc("Attack: [Space]") +end + goals = { - [startDialogue] = {loc("First Blood"), loc("First Steps"), loc("Press [Left] or [Right] to move around, [Enter] to jump"), 1, 4000}, - [onShroomAnim] = {loc("First Blood"), loc("A leap in a leap"), loc("Go on top of the flower") .. "|" .. loc("Hint: Hold down [M] to review the mission texts."), 1, 7000}, - [onFlowerAnim] = {loc("First Blood"), loc("Hightime"), loc("Collect the crate on the right.|Hint: Select the rope, [Up] or [Down] to aim, [Space] to fire, directional keys to move.|Ropes can be fired again in the air!"), 1, 7000}, - [tookParaAnim] = {loc("First Blood"), loc("Omnivore"), loc("Get on the head of the mole"), 1, 4000}, - [onMoleHeadAnim] = {loc("First Blood"), loc("The Leap of Faith"), loc("Use the parachute ([Space] while in air) to get the next crate"), 1, 4000}, + [startDialogue] = {loc("First Blood"), loc("First Steps"), loc("Press [Left] or [Right] to move around, [Long Jump] to jump forwards.") .. "| |" .. ctrlJump, 1, 4000}, + [onShroomAnim] = {loc("First Blood"), loc("A leap in a leap"), loc("Go on top of the flower.") .. "|" .. ctrlMissionPanel, 1, 7000}, + [onFlowerAnim] = {loc("First Blood"), loc("Hightime"), loc("Collect the crate on the right.") .. "|" .. loc("Hint: Select the rope, [Up] or [Down] to aim, [Attack] to fire, directional keys to move.") .. "|" .. loc("Ropes can be fired again in the air!") .. "| |" .. ctrlAttack, 1, 7000}, + [tookParaAnim] = {loc("First Blood"), loc("Omnivore"), loc("Get on the head of the mole."), 1, 4000}, + [onMoleHeadAnim] = {loc("First Blood"), loc("The Leap of Faith"), loc("Use the parachute to get the next crate.") .. "|" .. loc("Hint: Just select the parachute, it opens automatically when you fall."), 1, 4000}, [tookRope2Anim] = {loc("First Blood"), loc("The Rising"), loc("Get that crate!"), 1, 4000}, - [tookPunchAnim] = {loc("First Blood"), loc("The Slaughter"), loc("Destroy the targets!|Hint: Select the Shoryuken and hit [Space]|P.S. You can use it mid-air."), 1, 5000}, + [tookPunchAnim] = {loc("First Blood"), loc("The Slaughter"), loc("Destroy the targets!") .. "|" .. loc("Hint: Select the Shoryuken and hit [Attack].|P.S.: You can use it mid-air.") .. "| |" .. ctrlAttack, 1, 5000}, [challengeAnim] = {loc("First Blood"), loc("The Crate Frenzy"), loc("Collect the crates within the time limit!|If you fail, you'll have to try again."), 1, 5000}, [challengeFailedAnim] = {loc("First Blood"), loc("The Crate Frenzy"), loc("Collect the crates within the time limit!|If you fail, you'll have to try again."), 1, 5000}, [challengeCompletedAnim] = {loc("First Blood"), loc("The Ultimate Weapon"), loc("Get that crate!"), 1, 5000}, [beforeKillAnim] = {loc("First Blood"), loc("The First Blood"), loc("Kill the cannibal!"), 1, 5000}, - [closeCannim] = {loc("First Blood"), loc("The First Blood"), loc("KILL IT!"), 1, 5000} + [closeCannim] = {loc("First Blood"), loc("The First Blood"), loc("KILL IT!"), 1, 5000}, } + -----------------------------Animations-------------------------------- function Skipanim(anim) AnimSwitchHog(youngh) @@ -214,7 +226,7 @@ table.insert(startDialogue, {func = AnimJump, args = {youngh, "long"}}) table.insert(startDialogue, {func = AnimTurn, args = {princess, "Right"}}) table.insert(startDialogue, {func = AnimSwitchHog, args = {youngh}}) - table.insert(startDialogue, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("First Steps"), loc("Press [Left] or [Right] to move around, [Enter] to jump"), 1, 4000}}) + table.insert(startDialogue, {func = AnimShowMission, args = {youngh, unpack(goals[startDialogue])}}) AddSkipFunction(onShroomAnim, SkipOnShroom, {onShroomAnim}) table.insert(onShroomAnim, {func = AnimSay, args = {elderh, loc("I can see you have been training diligently."), SAY_SAY, 4000}, skipFunc = Skipanim, skipArgs = onShroomAnim}) @@ -224,14 +236,14 @@ table.insert(onShroomAnim, {func = AnimTurn, args = {elderh, "Left"}}) table.insert(onShroomAnim, {func = AnimSay, args = {princess, loc("He moves like an eagle in the sky."), SAY_THINK, 4000}}) table.insert(onShroomAnim, {func = AnimSwitchHog, args = {youngh}}) - table.insert(onShroomAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("A leap in a leap"), loc("Go on top of the flower") .. "|" .. loc("Hint: Press [Esc] to review the mission texts."), 1, 7000}}) + table.insert(onShroomAnim, {func = AnimShowMission, args = {youngh, unpack(goals[onShroomAnim])}}) AddSkipFunction(onFlowerAnim, Skipanim, {onFlowerAnim}) table.insert(onFlowerAnim, {func = AnimSay, args = {elderh, loc("See that crate farther on the right?"), SAY_SAY, 4000}}) table.insert(onFlowerAnim, {func = AnimSay, args = {elderh, loc("Swing, Leaks A Lot, on the wings of the wind!"), SAY_SAY, 6000}}) table.insert(onFlowerAnim, {func = AnimSay, args = {princess, loc("His arms are so strong!"), SAY_THINK, 4000}}) table.insert(onFlowerAnim, {func = AnimSwitchHog, args = {youngh}}) - table.insert(onFlowerAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("Hightime"), loc("Collect the crate on the right.|Hint: Select the rope, [Up] or [Down] to aim, [Space] to fire, directional keys to move.|Ropes can be fired again in the air!"), 1, 7000}}) + table.insert(onFlowerAnim, {func = AnimShowMission, args = {youngh, unpack(goals[onFlowerAnim])}}) AddSkipFunction(tookParaAnim, Skipanim, {tookParaAnim}) table.insert(tookParaAnim, {func = AnimGearWait, args = {youngh, 1000}, skipFunc = Skipanim, skipArgs = tookParaAnim}) @@ -239,14 +251,14 @@ table.insert(tookParaAnim, {func = AnimSay, args = {elderh, loc("Worry not, for it is a peaceful animal! There is no reason to be afraid..."), SAY_SHOUT, 5000}}) table.insert(tookParaAnim, {func = AnimSay, args = {elderh, loc("We all know what happens when you get frightened..."), SAY_SAY, 4000}}) table.insert(tookParaAnim, {func = AnimSay, args = {youngh, loc("So humiliating..."), SAY_SAY, 4000}}) - table.insert(tookParaAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("Omnivore"), loc("Get on the head of the mole"), 1, 4000}}) + table.insert(tookParaAnim, {func = AnimShowMission, args = {youngh, unpack(goals[tookParaAnim])}}) table.insert(tookParaAnim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(onMoleHeadAnim, Skipanim, {onMoleHeadAnim}) table.insert(onMoleHeadAnim, {func = AnimSay, args = {elderh, loc("Perfect! Now try to get the next crate without hurting yourself!"), SAY_SAY, 4000}, skipFunc = Skipanim, skipArgs = onMoleHeadAnim}) table.insert(onMoleHeadAnim, {func = AnimSay, args = {elderh, loc("The giant umbrella from the last crate should help break the fall."), SAY_SAY, 4000}}) table.insert(onMoleHeadAnim, {func = AnimSay, args = {princess, loc("He's so brave..."), SAY_THINK, 4000}}) - table.insert(onMoleHeadAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The Leap of Faith"), loc("Use the parachute ([Space] while in air) to get the next crate"), 1, 4000}}) + table.insert(onMoleHeadAnim, {func = AnimShowMission, args = {youngh, unpack(goals[onMoleHeadAnim])}}) table.insert(onMoleHeadAnim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(pastMoleHeadAnim, Skipanim, {pastMoleHeadAnim}) @@ -257,13 +269,13 @@ AddSkipFunction(tookRope2Anim, Skipanim, {tookRope2Anim}) table.insert(tookRope2Anim, {func = AnimSay, args = {elderh, loc("Impressive...you are still dry as the corpse of a hawk after a week in the desert..."), SAY_SAY, 5000}, skipFunc = Skipanim, skipArgs = tookRope2Anim}) table.insert(tookRope2Anim, {func = AnimSay, args = {elderh, loc("You probably know what to do next..."), SAY_SAY, 4000}}) - table.insert(tookRope2Anim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The Rising"), loc("Get that crate!"), 1, 4000}}) + table.insert(tookRope2Anim, {func = AnimShowMission, args = {youngh, unpack(goals[tookRope2Anim])}}) table.insert(tookRope2Anim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(tookPunchAnim, Skipanim, {tookPunchAnim}) table.insert(tookPunchAnim, {func = AnimSay, args = {elderh, loc("It is time to practice your fighting skills."), SAY_SAY, 4000}}) table.insert(tookPunchAnim, {func = AnimSay, args = {elderh, loc("Imagine those targets are the wolves that killed your parents! Take your anger out on them!"), SAY_SAY, 5000}}) - table.insert(tookPunchAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The Slaughter"), loc("Destroy the targets!|Hint: Select the Shoryuken and hit [Space]|P.S. You can use it mid-air."), 1, 5000}}) + table.insert(tookPunchAnim, {func = AnimShowMission, args = {youngh, unpack(goals[tookPunchAnim])}}) table.insert(tookPunchAnim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(challengeAnim, Skipanim, {challengeAnim}) @@ -276,7 +288,7 @@ AddSkipFunction(challengeFailedAnim, Skipanim, {challengeFailedAnim}) table.insert(challengeFailedAnim, {func = AnimSay, args = {elderh, loc("Hmmm...perhaps a little more time will help."), SAY_SAY, 4000}, skipFunc = Skipanim, skipArgs = challengeFailedAnim}) - table.insert(challengeFailedAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The Crate Frenzy"), loc("Collect the crates within the time limit!|If you fail, you'll have to try again."), 1, 5000}}) + table.insert(challengeFailedAnim, {func = AnimShowMission, args = {youngh, unpack(goals[challengeFailedAnim])}}) table.insert(challengeFailedAnim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(challengeCompletedAnim, Skipanim, {challengeCompletedAnim}) @@ -284,7 +296,7 @@ table.insert(challengeCompletedAnim, {func = AnimSay, args = {elderh, loc("You have proven yourself worthy to see our most ancient secret!"), SAY_SAY, 4000}}) table.insert(challengeCompletedAnim, {func = AnimSay, args = {elderh, loc("The weapon in that last crate was bestowed upon us by the ancients!"), SAY_SAY, 4000}}) table.insert(challengeCompletedAnim, {func = AnimSay, args = {elderh, loc("Use it with precaution!"), SAY_SAY, 4000}}) - table.insert(challengeCompletedAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The Ultimate Weapon"), loc("Get that crate!"), 1, 5000}}) + table.insert(challengeCompletedAnim, {func = AnimShowMission, args = {youngh, unpack(goals[challengeCompletedAnim])}}) table.insert(challengeCompletedAnim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(beforeKillAnim, Skipanim, {beforeKillAnim}) @@ -294,7 +306,7 @@ table.insert(beforeKillAnim, {func = AnimWait, args = {cannibal, 1000}}) table.insert(beforeKillAnim, {func = AnimSay, args = {elderh, loc("Destroy him, Leaks A Lot! He is responsible for the deaths of many of us!"), SAY_SHOUT, 4000}}) table.insert(beforeKillAnim, {func = AnimSay, args = {cannibal, loc("Oh, my!"), SAY_THINK, 4000}}) - table.insert(beforeKillAnim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The First Blood"), loc("Kill the cannibal!"), 1, 5000}}) + table.insert(beforeKillAnim, {func = AnimShowMission, args = {youngh, unpack(goals[beforeKillAnim])}}) table.insert(beforeKillAnim, {func = AnimSwitchHog, args = {youngh}}) AddSkipFunction(closeCannim, Skipanim, {closeCannim}) @@ -303,7 +315,7 @@ table.insert(closeCannim, {func = AnimSay, args = {cannibal, loc("If only I were given a chance to explain my being here..."), SAY_SAY, 4000}}) table.insert(closeCannim, {func = AnimSay, args = {elderh, loc("Do not let his words fool you, young one! He will stab you in the back as soon as you turn away!"), SAY_SAY, 6000}}) table.insert(closeCannim, {func = AnimSay, args = {elderh, loc("Here...pick your weapon!"), SAY_SAY, 5000}}) - table.insert(closeCannim, {func = AnimShowMission, args = {youngh, loc("First Blood"), loc("The First Blood"), loc("KILL IT!"), 1, 5000}}) + table.insert(closeCannim, {func = AnimShowMission, args = {youngh, unpack(goals[closeCannim])}}) table.insert(closeCannim, {func = AnimSwitchHog, args = {youngh}}) table.insert(cannKilledAnim, {func = AnimSay, args = {elderh, loc("Yes, yeees! You are now ready to enter the real world!"), SAY_SHOUT, 6000}}) @@ -410,7 +422,13 @@ end function DoMovedUntilJump() - ShowMission(loc("First Blood"), loc("Step By Step"), loc("Hint: Double Jump - Press [Backspace] twice"), -amSkip, 0) + local msg + if INTERFACE == "touch" then + msg = loc("Hint: Double Jump - Tap the [Curvy Arrow] twice") + else + msg = loc("Hint: Double Jump - Press [Backspace] twice") + end + ShowMission(loc("First Blood"), loc("Step By Step"), msg, -amSkip, 0) AddEvent(CheckOnShroom, {}, DoOnShroom, {}, 0) end @@ -601,7 +619,7 @@ PutTargets(1) AddEvent(CheckTargetsKilled, {}, DoTargetsKilled, {}, 1) AddEvent(CheckCannibalKilled, {}, DoCannibalKilledEarly, {}, 0) - ShowMission(loc("First Blood"), loc("The Bull's Eye"), loc("Destroy the targets!|Hint: [Up], [Down] to aim, [Space] to shoot"), 1, 5000) + ShowMission(loc("First Blood"), loc("The Bull's Eye"), loc("Destroy the targets!") .. "| |" .. ctrlAttack, 1, 5000) end function CheckTargetsKilled() @@ -725,7 +743,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 Map = "A_Classic_Fairytale_first_blood" Theme = "Nature" @@ -754,7 +771,14 @@ progress = tonumber(GetCampaignVar("Progress")) SetTurnTimeLeft(MAX_TURN_TIME) FollowGear(youngh) - ShowMission(loc("A Classic Fairytale"), loc("First Blood"), loc("Finish your training|Hint: Animations can be skipped with the [Precise] key."), -amSkip, 0) + local msgSkip + if INTERFACE == "touch" then + -- FIXME: Precise key is not available in Touch + msgSkip = "" + else + msgSkip = "|" .. loc("Hint: Cinematics can be skipped with the [Precise] key.") + end + ShowMission(loc("A Classic Fairytale"), loc("First Blood"), loc("Finish your training.") .. msgSkip, -amSkip, 0) HideHog(cannibal) AddAnim(startDialogue) diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/journey.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/journey.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/journey.lua Thu Dec 13 10:51:07 2018 -0500 @@ -1080,7 +1080,6 @@ MinesTime = 5000 end Explosives = 0 - Delay = 5 Map = "A_Classic_Fairytale_journey" Theme = "Nature" diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/queen.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/queen.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/queen.lua Thu Dec 13 10:51:07 2018 -0500 @@ -784,7 +784,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 MapGen = mgDrawn Theme = "Hell" SuddenDeathTurns = 20 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/shadow.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/shadow.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/shadow.lua Thu Dec 13 10:51:07 2018 -0500 @@ -144,6 +144,17 @@ cannibalDead = {} isHidden = {} +local grenadeHint = loc("Grenade hint: Set timer with the [Timer] controls, aim with [Up]/[Down].") .. "|" .. + loc("Hold [Attack] pressed to throw with more power.") +if INTERFACE == "touch" then + grenadeHint = grenadeHint .. "|" .. + loc("Change detonation timer: Tap the [Clock]") .. "|" .. + loc("Attack: Tap the [Bomb]") +else + grenadeHint = grenadeHint .. "|" .. + loc("Set detonation timer: [1]-[5]") .. "|" .. + loc("Attack: [Space]") +end --------------------------Anim skip functions-------------------------- function AfterRefusedAnim() @@ -316,7 +327,7 @@ return end stage = aloneStage - ShowMission(loc("The Shadow Falls"), loc("The Individualist"), loc("Defeat the cannibals!|Grenade hint: Set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"), 1, 8000) + ShowMission(loc("The Shadow Falls"), loc("The Individualist"), loc("Defeat the cannibals!") .. "|" .. grenadeHint, 1, 12000) AddAmmo(cannibals[6], amGrenade, 1) AddAmmo(cannibals[6], amFirePunch, 0) AddAmmo(cannibals[6], amBaseballBat, 0) @@ -844,7 +855,13 @@ if stage == loseStage then return end - ShowMission(loc("The Shadow Falls"), loc("Under Construction"), loc("Return to Leaks A Lot!") .. "|" .. loc("To place a girder, select it, use [Left] and [Right] to select angle and length, place with [Left Click]"), 1, 6000) + local ctrl = loc("Hint: To place a girder, select it,|then use [Left] and [Right] to select angle and length,|then choose a location for the girder.") + if INTERFACE == "touch" then + ctrl = ctrl .. "|" .. loc("Choose location: Tap the [Target] button, then tap on the spot you want to choose") + else + ctrl = ctrl .. "|" .. loc("Choose location: Left click") + end + ShowMission(loc("The Shadow Falls"), loc("Under Construction"), loc("Return to Leaks A Lot!") .. "|" .. ctrl, 1, 6000) end function CheckNeedWeapons() @@ -874,7 +891,8 @@ if stage == loseStage then return end - ShowMission(loc("The Shadow Falls"), loc("The guardian"), loc("Protect yourselves!|Grenade hint: Set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power").."|"..loc("Leaks A Lot must survive!"), 1, 8000) + + ShowMission(loc("The Shadow Falls"), loc("The guardian"), loc("Defeat the cannibals!") .."|".. loc("Leaks A Lot must survive!") .. "|" .. grenadeHint, 1, 12000) AddAmmo(dense, amSkip, 100) AddAmmo(dense, amSwitch, 100) AddAmmo(leaks, amSkip, 100) @@ -1002,7 +1020,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 Map = "A_Classic_Fairytale_shadow" Theme = "Nature" -- Disable Sudden Death @@ -1027,7 +1044,14 @@ AddAnim(startDialogue) AddFunction({func = AfterStartDialogue, args = {}}) AddEvent(CheckBrainiacDead, {}, DoBrainiacDead, {}, 0) - ShowMission(loc("The Shadow Falls"), loc("The First Encounter"), loc("Survive!|Hint: Cinematics can be skipped with the [Precise] key."), 1, 0) + local hint + if INTERFACE == "touch" then + -- FIXME: No precise key available in Touch yet. + hint = "" + else + hint = "|" .. loc("Hint: Cinematics can be skipped with the [Precise] key.") + end + ShowMission(loc("The Shadow Falls"), loc("The First Encounter"), loc("Survive!") .. hint, 1, 0) end function onGameTick() diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/united.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/united.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/united.lua Thu Dec 13 10:51:07 2018 -0500 @@ -432,7 +432,6 @@ MinesNum = 0 MinesTime = 3000 Explosives = 2 - Delay = 10 Map = "Hogville" Theme = "Nature" -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.lua Thu Dec 13 10:51:07 2018 -0500 @@ -107,7 +107,6 @@ CaseFreq = 0 MinesNum = 0 Explosives = 0 - Delay = 5 -- Disable Sudden Death WaterRise = 0 HealthDecrease = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death01.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death01.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death01.lua Thu Dec 13 10:51:07 2018 -0500 @@ -94,7 +94,6 @@ MinesNum = 3 MinesTime = 1500 Explosives = 2 - Delay = 3 HealthCaseAmount = 50 -- gfTagTeam makes it easier to skip the PAotH team GameFlags = gfTagTeam diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death02.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death02.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death02.lua Thu Dec 13 10:51:07 2018 -0500 @@ -66,7 +66,6 @@ Explosives = 0 Map = "death02_map" Theme = "Hell" - Delay = 600 -- this makes the messages between turns more readable -- Disable Sudden Death WaterRise = 0 HealthDecrease = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/desert01.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/desert01.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/desert01.lua Thu Dec 13 10:51:07 2018 -0500 @@ -91,7 +91,6 @@ MinesNum = 0 MinesTime = 1 Explosives = 0 - Delay = 3 HealthCaseAmount = 30 -- Disable Sudden Death HealthDecrease = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/desert02.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/desert02.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/desert02.lua Thu Dec 13 10:51:07 2018 -0500 @@ -63,7 +63,6 @@ GameFlags = gfOneClanMode Seed = 1 TurnTime = 8000 - Delay = 2 CaseFreq = 0 HealthCaseAmount = 50 MinesNum = 500 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/fruit01.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/fruit01.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/fruit01.lua Thu Dec 13 10:51:07 2018 -0500 @@ -112,7 +112,6 @@ MinesNum = 0 MinesTime = 1 Explosives = 0 - Delay = 3 -- Disable Sudden Death HealthDecrease = 0 WaterRise = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/fruit02.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/fruit02.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/fruit02.lua Thu Dec 13 10:51:07 2018 -0500 @@ -87,7 +87,6 @@ MinesNum = 0 MinesTime = 1 Explosives = 0 - Delay = 3 -- Disable Sudden Death HealthDecrease = 0 WaterRise = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/ice01.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/ice01.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/ice01.lua Thu Dec 13 10:51:07 2018 -0500 @@ -88,7 +88,6 @@ MinesNum = 0 MinesTime = 1 Explosives = 0 - Delay = 3 Map = "ice01_map" Theme = "Snow" -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon01.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon01.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon01.lua Thu Dec 13 10:51:07 2018 -0500 @@ -109,7 +109,6 @@ Explosives = 0 HealthDecrease = 0 WaterRise = 0 - Delay = 5 Map = "moon01_map" Theme = "Cheese" -- Because ofc moon is made of cheese :) -- Hog Solo diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Challenge/Basic_Training_-_Sniper_Rifle.lua --- a/share/hedgewars/Data/Missions/Challenge/Basic_Training_-_Sniper_Rifle.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Challenge/Basic_Training_-_Sniper_Rifle.lua Thu Dec 13 10:51:07 2018 -0500 @@ -123,8 +123,6 @@ MinesNum = 0 -- The number of explosives being placed Explosives = 0 - -- The delay between each round - Delay = 0 -- The map to be played Map = "Ropes" -- The theme to be used diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Challenge/User_Mission_-_Rope_Knock_Challenge.lua --- a/share/hedgewars/Data/Missions/Challenge/User_Mission_-_Rope_Knock_Challenge.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Challenge/User_Mission_-_Rope_Knock_Challenge.lua Thu Dec 13 10:51:07 2018 -0500 @@ -154,7 +154,6 @@ GameFlags = gfBorder + gfSolidLand TurnTime = 180 * 1000 - Delay = 500 Map = "Ropes" Theme = "Eyes" diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Challenge/User_Mission_-_That_Sinking_Feeling.lua --- a/share/hedgewars/Data/Missions/Challenge/User_Mission_-_That_Sinking_Feeling.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Challenge/User_Mission_-_That_Sinking_Feeling.lua Thu Dec 13 10:51:07 2018 -0500 @@ -36,10 +36,10 @@ MinesNum = 0 MinesTime = 3000 Explosives = 0 - Delay = 10 Map = "Islands" Theme = "City" - SuddenDeathTurns = 1 + HealthDecrease = 0 + WaterRise = 0 AddTeam(loc("Hapless Hogs"), -1, "Simple", "Island", "Default") hh[0] = AddHog(loc("Sinky"), 1, 100, "fr_lemon") diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Scenario/User_Mission_-_Dangerous_Ducklings.lua --- a/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Dangerous_Ducklings.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Dangerous_Ducklings.lua Thu Dec 13 10:51:07 2018 -0500 @@ -24,7 +24,6 @@ CaseFreq = 0 -- The frequency of crate drops MinesNum = 0 -- The number of mines being placed Explosives = 0 -- The number of explosives being placed - Delay = 0 -- The delay between each round Map = "Bath" -- The map to be played Theme = "Bath" -- The theme to be used -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Scenario/User_Mission_-_Diver.lua --- a/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Diver.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Diver.lua Thu Dec 13 10:51:07 2018 -0500 @@ -19,7 +19,6 @@ MinesNum = 0 -- The number of mines being placed MinesTime = 1000 Explosives = 0 -- The number of explosives being placed - Delay = 10 -- The delay between each round Map = "Hydrant" -- The map to be played Theme = "City" -- The theme to be used diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Scenario/User_Mission_-_Spooky_Tree.lua --- a/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Spooky_Tree.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Spooky_Tree.lua Thu Dec 13 10:51:07 2018 -0500 @@ -23,7 +23,6 @@ MinesNum = 0 -- The number of mines being placed MinesTime = 1 Explosives = 0 -- The number of explosives being placed - Delay = 10 -- The delay between each round Map = "Tree" -- The map to be played Theme = "Halloween" -- The theme to be used -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Scenario/User_Mission_-_Teamwork.lua --- a/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Teamwork.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Teamwork.lua Thu Dec 13 10:51:07 2018 -0500 @@ -17,7 +17,6 @@ MinesNum = 0 -- The number of mines being placed MinesTime = 1 Explosives = 0 -- The number of explosives being placed - Delay = 10 -- The delay between each round Map = "Mushrooms" -- The map to be played Theme = "Nature" -- The theme to be used -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Scenario/User_Mission_-_Teamwork_2.lua --- a/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Teamwork_2.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Scenario/User_Mission_-_Teamwork_2.lua Thu Dec 13 10:51:07 2018 -0500 @@ -22,7 +22,6 @@ WaterRise = 0 Explosives = 0 - Delay = 10 Map = "CrazyMission" Theme = "CrazyMission" diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Scenario/portal.lua --- a/share/hedgewars/Data/Missions/Scenario/portal.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Scenario/portal.lua Thu Dec 13 10:51:07 2018 -0500 @@ -13,7 +13,6 @@ CaseFreq = 0 -- The frequency of crate drops MinesNum = 0 -- The number of mines being placed Explosives = 0 -- The number of explosives being placed - Delay = 10 -- The delay between each round Map = "portal" -- The map to be played Theme = "Hell" -- The theme to be used -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Training/Basic_Training_-_Bazooka.lua --- a/share/hedgewars/Data/Missions/Training/Basic_Training_-_Bazooka.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Training/Basic_Training_-_Bazooka.lua Thu Dec 13 10:51:07 2018 -0500 @@ -130,23 +130,42 @@ end function newGamePhase() + local ctrl = "" -- Spawn targets, update wind and ammo, show instructions if gamePhase == 0 then + if INTERFACE == "desktop" then + ctrl = loc("Open ammo menu: [Right click]").."|".. + loc("Select weapon: [Left click]") + elseif INTERFACE == "touch" then + ctrl = loc("Open ammo menu: Tap the [Suitcase]") + end ShowMission(loc("Basic Bazooka Training"), loc("Select Weapon"), loc("To begin with the training, select the bazooka from the ammo menu!").."|".. - loc("Open ammo menu: [Right click]").."|".. - loc("Select weapon: [Left click]"), 2, 5000) + ctrl, 2, 5000) elseif gamePhase == 1 then - ShowMission(loc("Basic Bazooka Training"), loc("My First Bazooka"), loc("Let's get started!").."|".. + if INTERFACE == "desktop" then + ctrl = loc("Attack: [Space]").."|".. + loc("Aim: [Up]/[Down]").."|".. + loc("Walk: [Left]/[Right]") + elseif INTERFACE == "touch" then + ctrl = loc("Attack: Tap the [Bomb]").."|".. + loc("Aim: [Up]/[Down]").."|".. + loc("Walk: [Left]/[Right]") + end + ShowMission(loc("Basic Bazooka Training"), loc("My First Bazooka"), + loc("Let's get started!").."|".. loc("Launch some bazookas to destroy the targets!").."|".. loc("Hold the Attack key pressed for more power.").."|".. loc("Don't hit yourself!").."|".. - loc("Attack: [Space]").."|".. - loc("Aim: [Up]/[Down]").."|".. - loc("Walk: [Left]/[Right]"), 2, 10000) + ctrl, 2, 10000) spawnTargets() elseif gamePhase == 2 then + if INTERFACE == "desktop" then + ctrl = loc("You see the wind strength at the bottom right corner.") + elseif INTERFACE == "touch" then + ctrl = loc("You see the wind strength at the top.") + end ShowMission(loc("Basic Bazooka Training"), loc("Wind"), loc("Bazookas are influenced by wind.").."|".. - loc("You see the wind strength at the bottom right corner.").."|".. + ctrl.."|".. loc("Destroy the targets!"), 2, 5000) SetWind(50) spawnTargets() @@ -181,9 +200,12 @@ SetWind(-33) spawnTargets() elseif gamePhase == 6 then + if INTERFACE == "desktop" then + ctrl = loc("Precise Aim: [Left Shift] + [Up]/[Down]").."|" + end ShowMission(loc("Basic Bazooka Training"), loc("Final Targets"), loc("The final targets are quite tricky. You need to aim well.").."|".. - loc("Precise Aim: [Left Shift] + [Up]/[Down]").."|".. + ctrl.. loc("Hint: It might be easier if you vary the angle only slightly."), 2, 12000) SetWind(75) diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Training/Basic_Training_-_Grenade.lua --- a/share/hedgewars/Data/Missions/Training/Basic_Training_-_Grenade.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Training/Basic_Training_-_Grenade.lua Thu Dec 13 10:51:07 2018 -0500 @@ -96,10 +96,14 @@ AddGear(945, 498, gtTarget, 0, 0, 0, 0) -- Bounciness elseif gamePhase == 4 then - AddGear(323, 960, gtTarget, 0, 0, 0, 0) AddGear(1318, 208, gtTarget, 0, 0, 0, 0) AddGear(1697, 250, gtTarget, 0, 0, 0, 0) - AddGear(1852, 100, gtTarget, 0, 0, 0, 0) + if INTERFACE ~= "touch" then + -- These targets may be too hard in touch interface because you cannot set bounciness yet + -- FIXME: Allow these targets in touch when bounciness can be set + AddGear(323, 960, gtTarget, 0, 0, 0, 0) + AddGear(1852, 100, gtTarget, 0, 0, 0, 0) + end -- Grand Final elseif gamePhase == 5 then AddGear(186, 473, gtTarget, 0, 0, 0, 0) @@ -117,24 +121,42 @@ function newGamePhase() -- Spawn targets, update wind and ammo, show instructions + local ctrl = "" if gamePhase == 0 then + if INTERFACE == "desktop" then + ctrl = loc("Open ammo menu: [Right click]").."|".. + loc("Select weapon: [Left click]") + else + ctrl = loc("Open ammo menu: Tap the [Suitcase]") + end ShowMission(loc("Basic Grenade Training"), loc("Select Weapon"), loc("To begin with the training, select the grenade from the ammo menu!").."|".. - loc("Open ammo menu: [Right click]").."|".. - loc("Select weapon: [Left click]"), 2, 5000) + ctrl, 2, 5000) elseif gamePhase == 1 then + if INTERFACE == "desktop" then + ctrl = loc("Attack: [Space]").."|".. + loc("Aim: [Up]/[Down]").."|".. + loc("Change direction: [Left]/[Right]") + elseif INTERFACE == "touch" then + ctrl = loc("Attack: Tap the [Bomb]").."|".. + loc("Aim: [Up]/[Down]").."|".. + loc("Change direction: [Left]/[Right]") + end ShowMission(loc("Basic Grenade Training"), loc("Warming Up"), loc("Throw a grenade to destroy the target!").."|".. loc("Hold the Attack key pressed for more power.").."|".. - loc("Attack: [Space]").."|".. - loc("Aim: [Up]/[Down]").."|".. - loc("Change direction: [Left]/[Right]").."|".. + ctrl.."|".. loc("Note: Walking is disabled in this mission."), 2, 20000) spawnTargets() elseif gamePhase == 2 then + if INTERFACE == "desktop" then + ctrl = loc("Set detonation timer: [1]-[5]") + elseif INTERFACE == "touch" then + ctrl = loc("Change detonation timer: Tap the [Clock]") + end ShowMission(loc("Basic Grenade Training"), loc("Timer"), loc("You can change the detonation timer of grenades.").."|".. loc("Grenades explode after 1 to 5 seconds (you decide).").."|".. - loc("Set detonation timer: [1]-[5]"), 2, 15000) + ctrl, 2, 15000) spawnTargets() elseif gamePhase == 3 then ShowMission(loc("Basic Grenade Training"), loc("No Wind Influence"), loc("Unlike bazookas, grenades are not influenced by wind.").."|".. @@ -142,17 +164,28 @@ SetWind(50) spawnTargets() elseif gamePhase == 4 then - ShowMission(loc("Basic Grenade Training"), loc("Bounciness"), - loc("You can set the bounciness of grenades (and grenade-like weapons).").."|".. - loc("Grenades with high bounciness bounce a lot and behave chaotic.").."|".. - loc("With low bounciness, it barely bounces at all, but it is much more predictable.").."|".. - loc("Try out different bounciness levels to reach difficult targets.").."|".. - loc("Set bounciness: [Left Shift] + [1]-[5]"), - 2, 20000) + local caption = loc("Bounciness") + if INTERFACE == "desktop" then + ctrl = loc("You can set the bounciness of grenades (and grenade-like weapons).").."|".. + loc("Grenades with high bounciness bounce a lot and behave chaotic.").."|".. + loc("With low bounciness, it barely bounces at all, but it is much more predictable.").."|".. + loc("Try out different bounciness levels to reach difficult targets.").."|".. + loc("Set bounciness: [Left Shift] + [1]-[5]") + elseif INTERFACE == "touch" then + -- FIXME: Bounciness can't be set in touch yet. :( + caption = loc("Well done.") + ctrl = loc("You're doing well! Here are more targets for you.") + end + + ShowMission(loc("Basic Grenade Training"), caption, ctrl, 2, 20000) spawnTargets() elseif gamePhase == 5 then + if INTERFACE == "desktop" then + ctrl = loc("Precise Aim: [Left Shift] + [Up]/[Down]") + -- FIXME: No precise aim in touch interface yet :( + end ShowMission(loc("Basic Grenade Training"), loc("Final Targets"), loc("Good job! Now destroy the final targets to finish the training.").."|".. - loc("Precise Aim: [Left Shift] + [Up]/[Down]"), + ctrl, 2, 7000) spawnTargets() elseif gamePhase == 6 then diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Training/Basic_Training_-_Movement.lua --- a/share/hedgewars/Data/Missions/Training/Basic_Training_-_Movement.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Training/Basic_Training_-_Movement.lua Thu Dec 13 10:51:07 2018 -0500 @@ -241,7 +241,10 @@ crates[7] = SpawnHealthCrate(1198, 1750) -- Back Jumping 2 crates[8] = SpawnSupplyCrate(1851, 1402, amSwitch, 100) -- Switch Hedgehog crates[9] = SpawnHealthCrate(564, 1772) -- Health - crates[10] = SpawnHealthCrate(2290, 1622) -- Turning Around + -- FIXME: Not available in touch because no “precise” button + if INTERFACE ~= "touch" then + crates[10] = SpawnHealthCrate(2290, 1622) -- Turning Around + end end local function victory() @@ -265,11 +268,17 @@ loc("To finish hedgehog selection, just do anything|with him, like walking."), 2, 20000) else + local ctrl = "" + if INTERFACE == "desktop" then + ctrl = loc("Hit the “Switch Hedgehog” key until you have|selected Cappy, the hedgehog with the cap!").."|".. + loc("Switch hedgehog: [Tabulator]") + else + ctrl = loc("Tap the “rotating arrow” button on the left|until you have selected Cappy, the hedgehog with the cap!") + end ShowMission(loc("Basic Movement Training"), loc("Switch Hedgehog (2/3)"), loc("You have activated Switch Hedgehog!").."|".. loc("The spinning arrows above your hedgehog show|which hedgehog is selected right now.").."|".. - loc("Hit the “Switch Hedgehog” key until you have|selected Cappy, the hedgehog with the cap!").."|".. - loc("Switch hedgehog: [Tabulator]"), 2, 20000) + ctrl, 2, 20000) end end @@ -281,6 +290,7 @@ end function onGearDelete(gear) + local ctrl = "" -- Switching done if GetGearType(gear) == gtSwitcher then switcherGear = nil @@ -290,57 +300,99 @@ loc("Collect the remaining crates to complete the training."), 2, 0) else + if INTERFACE == "desktop" then + ctrl = loc("Open ammo menu: [Right click]").."|".. + loc("Attack: [Space]") + elseif INTERFACE == "touch" then + ctrl = loc("Open ammo menu: Tap the [Suitcase]").."|".. + loc("Attack: Tap the [Bomb]") + end ShowMission(loc("Basic Movement Training"), loc("Switch Hedgehog (Failed!)"), loc("Oops! You have selected the wrong hedgehog! Just try again.").."|".. loc("Select “Switch Hedgehog” from the ammo menu and|hit the “Attack” key to proceed.").."|".. - loc("Open ammo menu: [Right click]").."|".. - loc("Attack: [Space]"), 2, 0) + ctrl, 2, 0) end -- Crate collected (or destroyed, but this should not be possible) elseif gear == crates[1] then + if INTERFACE == "desktop" then + ctrl = loc("Long Jump: [Enter]") + elseif INTERFACE == "touch" then + ctrl = loc("Long Jump: Tap the [Curvy Arrow] button for long") + end ShowMission(loc("Basic Movement Training"), loc("Jumping"), loc("Get the next crate by jumping over the abyss.").."|".. loc("Careful, hedgehogs can't swim!").."|".. - loc("Long Jump: [Enter]"), 2, 5000) + ctrl, 2, 5000) elseif gear == crates[2] then victory() elseif gear == crates[4] then + if INTERFACE == "desktop" then + ctrl = loc("High Jump: [Backspace]").."|"..loc("Back Jump: [Backspace] ×2") + elseif INTERFACE == "touch" then + ctrl = loc("High Jump: Tap the [Curvy Arrow] shortly").."|"..loc("Back Jump: Double-tap the [Curvy Arrow]") + end ShowMission(loc("Basic Movement Training"), loc("Back Jumping (1/2)"), loc("For the next crate, you have to do back jumps.") .. "|" .. loc("To reach higher ground, walk to a ledge, look to the left, then do a back jump.") .. "|" .. - loc("High Jump: [Backspace]").."|"..loc("Back Jump: [Backspace] ×2"), 2, 6600) + ctrl, 2, 6600) elseif gear == crates[7] then + if INTERFACE == "desktop" then + ctrl = loc("High Jump: [Backspace]").."|"..loc("Back Jump: [Backspace] ×2") + elseif INTERFACE == "touch" then + ctrl = loc("High Jump: Tap the [Curvy Arrow] shortly").."|"..loc("Back Jump: Double-tap the [Curvy Arrow]") + end ShowMission(loc("Basic Movement Training"), loc("Back Jumping (2/2)"), loc("To get over the next obstacles, keep some distance from the wall before you back jump.").."|".. loc("Hint: To jump higher, wait a bit before you hit “High Jump” a second time.").."|".. - loc("High Jump: [Backspace]").."|"..loc("Back Jump: [Backspace] ×2"), 2, 15000) + ctrl, 2, 15000) elseif gear == crates[5] then + -- FIXME: Touch doesn't have precise aim yet :( + if INTERFACE == "desktop" then + ctrl = "|" .. + loc("You can also hold down the key for “Precise Aim” to prevent slipping.") .. "|" .. + loc("Precise Aim: [Left Shift]") + end ShowMission(loc("Basic Movement Training"), loc("Walking on Ice"), loc("These girders are slippery, like ice.").."|".. loc("And you need to move to the top!").."|".. - loc("If you don't want to slip away, you have to keep moving!").."|".. - loc("You can also hold down the key for “Precise Aim” to prevent slipping.").."|".. - loc("Precise Aim: [Left Shift]"), 2, 9000) + loc("If you don't want to slip away, you have to keep moving!").. + ctrl, 2, 9000) elseif gear == crates[6] then + -- FIXME: Touch doesn't have precise aim yet :( + if INTERFACE == "desktop" then + ctrl = "|" .. loc("Remember: Hold down [Left Shift] to prevent slipping") + end ShowMission(loc("Basic Movement Training"), loc("A mysterious Box"), - loc("The next crate is an utility crate.").."|"..loc("What's in the box, you ask? Let's find out!").."|".. - loc("Remember: Hold down [Left Shift] to prevent slipping"), 2, 6000) + loc("The next crate is an utility crate.").."|"..loc("What's in the box, you ask? Let's find out!").. + ctrl, 2, 6000) elseif gear == crates[8] then + if INTERFACE == "desktop" then + ctrl = loc("Open ammo menu: [Right click]").."|".. + loc("Attack: [Space]") + elseif INTERFACE == "touch" then + ctrl = loc("Open ammo menu: Tap the [Suitcase]").."|".. + loc("Attack: Tap the [Bomb]") + end ShowMission(loc("Basic Movement Training"), loc("Switch Hedgehog (1/3)"), loc("You have collected the “Switch Hedgehog” utility!").."|".. loc("This allows to select any hedgehog in your team!").."|".. loc("Select “Switch Hedgehog” from the ammo menu and|hit the “Attack” key.").."|".. - loc("Open ammo menu: [Right click]").."|".. - loc("Attack: [Space]"), 2, 30000) + ctrl, 2, 30000) elseif gear == crates[3] then ShowMission(loc("Basic Movement Training"), loc("Rubber"), loc("As you probably noticed, these rubber bands|are VERY elastic. Hedgehogs and many other|things will bounce off without taking any damage.").."|".. loc("Now try to get out of this bounce house|and take the next crate."), 2, 8000) elseif gear == crates[9] then + if INTERFACE == "desktop" then + ctrl = loc("Look around: [Mouse movement]") + elseif INTERFACE == "touch" then + ctrl = loc("Look around: [Tap or swipe on the screen]") + end ShowMission(loc("Basic Movement Training"), loc("Health"), loc("You just got yourself some extra health.|The more health your hedgehogs have, the better!").."|".. loc("Now go to the next crate.").."|".. - loc("Look around: [Mouse movement]"), 2, 10000) + ctrl, 2, 10000) elseif gear == crates[10] then + -- FIXME: This crate is unused in touch atm ShowMission(loc("Basic Movement Training"), loc("Turning Around"), loc("By the way, you can turn around without walking|by holding down Precise when you hit a walk control.").."|".. loc("Get the final crate to the right to complete the training.").."|".. @@ -369,17 +421,26 @@ -- This part is CRITICALLY important for all future missions. -- Because the player must know how to show the current mission texts again. -- We force the player to hit Attack before the actual training begins. + local ctrl = "" + if INTERFACE == "desktop" then + ctrl = loc("IMPORTANT: To see the mission panel again, hold the mission panel key.").."| |".. + loc("Note: This basic training assumes default controls.").."|".. + loc("Mission panel: [M]").."|".. + loc("Quit: [Esc]").."|".. + loc("Pause: [P]").."| |".. + loc("To begin with the training, hit the attack key!").."|".. + loc("Attack: [Space]") + elseif INTERFACE == "touch" then + ctrl = loc("IMPORTANT: To see the mission panel again, pause the game.").."| |".. + loc("Pause: Tap the [Pause] button").."| |".. + loc("To begin with the training, tap the attack button!").."|".. + loc("Attack: Tap the [Bomb]") + end ShowMission(loc("Basic Movement Training"), loc("Mission Panel"), loc("This is the mission panel.").."|".. loc("Here you will find the current mission instructions.").."|".. loc("Normally, the mission panel disappears after a few seconds.").."|".. - loc("IMPORTANT: To see the mission panel again, hold the mission panel key.").."| |".. - loc("Note: This basic training assumes default controls.").."|".. - loc("Mission panel: [M]").."|".. - loc("Quit: [Esc]").."|".. - loc("Pause: [P]").."| |".. - loc("To begin with the training, hit the attack key!").."|".. - loc("Attack: [Space]"), 2, 900000, true) + ctrl, 2, 900000, true) -- TODO: This and other training missions are currently hardcoding control names. -- This should be fixed eventually. diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Missions/Training/Basic_Training_-_Rope.lua --- a/share/hedgewars/Data/Missions/Training/Basic_Training_-_Rope.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Missions/Training/Basic_Training_-_Rope.lua Thu Dec 13 10:51:07 2018 -0500 @@ -194,10 +194,16 @@ end function onNewTurn() + local ctrl = "" if not wasFirstTurn then + if INTERFACE == "desktop" then + ctrl = loc("Open ammo menu: [Right click]") + elseif INTERFACE == "touch" then + ctrl = loc("Open ammo menu: Tap the [Suitcase]") + end ShowMission(loc("Basic Rope Training"), loc("Select Rope"), loc("Select the rope to begin!").."|".. - loc("Open ammo menu: [Right click]"), 2, 7500) + ctrl, 2, 7500) wasFirstTurn = true end if isInMineChallenge then @@ -212,11 +218,18 @@ -- First rope selection if not ropeSelected and GetCurAmmoType() == amRope then + local ctrl = "" + if INTERFACE == "desktop" then + ctrl = loc("Aim: [Up]/[Down]").."|".. + loc("Attack: [Space]") + elseif INTERFACE == "touch" then + ctrl = loc("Aim: [Up]/[Down]").."|".. + loc("Attack: Tap the [Bomb]") + end ShowMission(loc("Basic Rope Training"), loc("Getting Started"), loc("You can use the rope to reach new places.").."|".. loc("Aim at the ceiling and hold [Attack] pressed until the rope attaches.").."|".. - loc("Aim: [Up]/[Down]").."|".. - loc("Attack: [Space]"), 2, 15000) + ctrl, 2, 15000) ropeSelected = true -- Rope attach elseif ropeGear and band(GetState(ropeGear), gstCollision) ~= 0 then @@ -367,11 +380,18 @@ elseif GetGearType(gear) == gtRope then ropeGear = nil if ropeAttached and not target1Reached then + local ctrl = "" + if INTERFACE == "desktop" then + ctrl = loc("Aim: [Up]/[Down]").."|".. + loc("Attack: [Space]") + elseif INTERFACE == "touch" then + ctrl = loc("Aim: [Up]/[Down]").."|".. + loc("Attack: Tap the [Bomb]") + end ShowMission(loc("Basic Rope Training"), loc("How to Rope"), loc("Go to the target.").."|".. loc("Hold [Attack] to attach the rope.").."|".. - loc("Aim: [Up]/[Down]").."|".. - loc("Attack: [Space]"), 2, 13000) + ctrl, 2, 13000) ropeAttached = false end elseif GetGearType(gear) == gtMine then diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Scripts/Multiplayer/Frenzy.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Frenzy.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Frenzy.lua Thu Dec 13 10:51:07 2018 -0500 @@ -26,10 +26,13 @@ ruleSet = "" .. loc("RULES:") .. " |" .. loc("Each turn is only ONE SECOND!") .. "|" .. - loc("Use your ready time to think.") .. "|" .. - loc("Slot keys save time! (F1-F10 by default)") .. "| |" - for i=1, #frenzyAmmos do - ruleSet = ruleSet .. string.format(loc("Slot %d: %s"), i, GetAmmoName(frenzyAmmos[i])) .. "|" + loc("Use your ready time to think.") + if INTERFACE ~= "touch" then + ruleSet = ruleSet .. "|" .. + loc("Slot keys save time! (F1-F10 by default)") .. "| |" + for i=1, #frenzyAmmos do + ruleSet = ruleSet .. string.format(loc("Slot %d: %s"), i, GetAmmoName(frenzyAmmos[i])) .. "|" + end end ShowMission(loc("FRENZY"), @@ -40,7 +43,7 @@ function onGameInit() - if TurnTime > 10001 then + if TurnTime > 8000 then Ready = 8000 else Ready = TurnTime diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua Thu Dec 13 10:51:07 2018 -0500 @@ -1091,7 +1091,6 @@ end CaseFreq = 0 HealthCaseProb = 0 - Delay = 1000 SuddenDeathTurns = 50 WaterRise = 0 HealthDecrease = 0 diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Scripts/Multiplayer/The_Specialists.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/The_Specialists.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Scripts/Multiplayer/The_Specialists.lua Thu Dec 13 10:51:07 2018 -0500 @@ -175,7 +175,6 @@ function onGameInit() ClearGameFlags() EnableGameFlags(gfRandomOrder, gfResetWeps, gfInfAttack, gfPlaceHog, gfPerHogAmmo, gfSwitchHog) - Delay = 10 HealthCaseProb = 100 end diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Scripts/SpeedShoppa.lua --- a/share/hedgewars/Data/Scripts/SpeedShoppa.lua Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Scripts/SpeedShoppa.lua Thu Dec 13 10:51:07 2018 -0500 @@ -94,7 +94,6 @@ CaseFreq = 0 MinesNum = 0 Explosives = 0 - Delay = 10 Theme = params.theme Map = params.map -- Disable Sudden Death diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Sounds/voices/Default_ru/CMakeLists.txt --- a/share/hedgewars/Data/Sounds/voices/Default_ru/CMakeLists.txt Thu Dec 13 10:49:30 2018 -0500 +++ b/share/hedgewars/Data/Sounds/voices/Default_ru/CMakeLists.txt Thu Dec 13 10:51:07 2018 -0500 @@ -8,6 +8,7 @@ Firepunch*.ogg Flawless.ogg Hello.ogg +Hmm.ogg Hurry.ogg Illgetyou.ogg Incoming.ogg diff -r 1ffa8bfc5c58 -r 94f10f69fe76 share/hedgewars/Data/Sounds/voices/Default_ru/Hmm.ogg Binary file share/hedgewars/Data/Sounds/voices/Default_ru/Hmm.ogg has changed