--- 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)
--- 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
--- 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
--- 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;
}
--- 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("<a href=\"https://gcc.gnu.org\">GCC</a>: %1")).arg(__VERSION__));
+#elif defined(WIN32_VCPKG)
+ libinfo.append(QString(tr("<a href=\"https://visualstudio.microsoft.com\">VC++</a>: %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
--- 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);
--- 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."
+ ]
--- 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
--- 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
--- 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 <nickname>, map <name>, pause, newseed, hedgehogs"]
+ ["CHAT", nickServer, loc "Available callvote commands: hedgehogs <number>, pause, newseed, map <name>, kick <player>"]
]
handleCmd_inRoom ["CALLVOTE", "KICK"] = do
--- 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)"]
--- 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 <a.korotaev@hedgewars.org>" ]
-
-[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
--- 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();
- }
-}
--- 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<String>),
- //Cmd(String, Vec<String>),
- 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<String>),
- JoinRoom(String, Option<String>),
- Follow(String),
- Rnd(Vec<String>),
- 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<String>),
- Cfg(GameCfg),
- AddTeam(Box<TeamInfo>),
- 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<VoteType>),
- 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<String>),
- ChatMsg {nick: String, msg: String},
- ClientFlags(String, Vec<String>),
- Rooms(Vec<String>),
- RoomAdd(Vec<String>),
- RoomJoined(Vec<String>),
- RoomLeft(String, String),
- RoomRemove(String),
- RoomUpdated(String, Vec<String>),
- TeamAdd(Vec<String>),
- TeamRemove(String),
- TeamAccepted(String),
- TeamColor(String, u8),
- HedgehogsNumber(String, u8),
- ConfigEntry(String, Vec<String>),
- Kicked,
- RunGame,
- ForwardEngineMessage(Vec<String>),
- RoundFinished,
-
- ServerMessage(String),
- Notice(String),
- Warning(String),
- Error(String),
- Connected(u32),
- Unreachable,
-
- //Deprecated messages
- LegacyReady(bool, Vec<String>)
-}
-
-pub fn server_chat(msg: String) -> HWServerMessage {
- HWServerMessage::ChatMsg{ nick: "[server]".to_string(), msg }
-}
-
-impl GameCfg {
- pub fn to_protocol(&self) -> (String, Vec<String>) {
- 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::<Vec<_>>().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<String>)>) =>, ??
- 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"],
- }
- }
-}
--- 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<R: Read>(&mut self, stream: &mut R) -> Result<usize> {
- self.buf.read_from(stream)
- }
-
- pub fn extract_messages(&mut self) -> Vec<messages::HWProtocolMessage> {
- 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;
- }
-}
--- 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<String> >, 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<String> >, 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<HWProtocolMessage> >, 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])));
-}
--- 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<T>: Sized { fn into2(self) -> T; }
-impl <T> Into2<T> for T { fn into2(self) -> T { self } }
-impl Into2<Vec<String>> for Vec<Ascii> {
- fn into2(self) -> Vec<String> {
- self.into_iter().map(|x| x.0).collect()
- }
-}
-impl Into2<String> for Ascii { fn into2(self) -> String { self.0 } }
-impl Into2<Option<String>> for Option<Ascii>{
- fn into2(self) -> Option<String> { 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 = <String as Arbitrary>::Parameters;
-
- fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
- "[a-zA-Z0-9]+".prop_map(Ascii).boxed()
- }
-
- type Strategy = BoxedStrategy<Ascii>;
-}
-
-impl Arbitrary for GameCfg {
- type Parameters = ();
-
- fn arbitrary_with(_args: <Self as Arbitrary>::Parameters) -> <Self as Arbitrary>::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<Ascii>),
- 7 => Scheme(Ascii, Vec<Ascii>),
- 8 => Script(Ascii),
- 9 => Theme(Ascii),
- 10 => DrawnMap(Ascii))
- }).boxed()
- }
-
- type Strategy = BoxedStrategy<GameCfg>;
-}
-
-impl Arbitrary for TeamInfo {
- type Parameters = ();
-
- fn arbitrary_with(_args: <Self as Arbitrary>::Parameters) -> <Self as Arbitrary>::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<TeamInfo>;
-}
-
-pub fn gen_proto_msg() -> BoxedStrategy<HWProtocolMessage> where {
- let res = (0..58).no_shrink().prop_flat_map(|i| {
- proto_msg_match!(i, def = Malformed,
- 0 => Ping(),
- 1 => Pong(),
- 2 => Quit(Option<Ascii>),
- //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<Ascii>),
- 16 => JoinRoom(Ascii, Option<Ascii>),
- 17 => Follow(Ascii),
- 18 => Rnd(Vec<Ascii>),
- 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<Ascii>),
- 30 => Cfg(GameCfg),
- 31 => AddTeam(Box<TeamInfo>),
- 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<String>)>),
- 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()
-}
--- 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<RoomId>,
- protocol: Option<u16>,
- 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<Action> 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<String>),
- RemoveRoom(RoomId),
- MoveToRoom(RoomId),
- MoveToLobby(String),
- ChangeMaster(RoomId, Option<ClientId>),
- RemoveTeam(String),
- RemoveClientTeams,
- SendRoomUpdate(Option<String>),
- 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())
- }
- }
-}
--- 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<usize>,
- 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<u8>,
- pub clan: Option<u8>
-}
-
-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
--- 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<String>),
- Scheme(String, Vec<String>),
- 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<String>),
- Pause,
- NewSeed,
- HedgehogsPerTeam(u8)
-}
-
-#[derive(Clone, Debug)]
-pub struct Voting {
- pub ttl: u32,
- pub voters: Vec<ClientId>,
- pub votes: Vec<(ClientId, bool)>,
- pub kind: VoteType
-}
-
-impl Voting {
- pub fn new(kind: VoteType, voters: Vec<ClientId>) -> Voting {
- Voting {
- kind, voters, ttl: 2,
- votes: Vec::new()
- }
- }
-}
\ No newline at end of file
--- 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"),
- }
-}
--- 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<String>) {
- 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;
- }
- }
- }
-}
--- 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<<Self as Iterator>::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<String> {
- 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 <nickname>, map <name>, pause, newseed, hedgehogs <number>".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::<Vec<_>>());
- 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::<Vec<_>>());
- 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!")
- }
-}
--- 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"),
- }
-}
--- 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"),
- }
-}
--- 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)
- }
- },
- }
-}
--- 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;
--- 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<T> = io::Result<(T, NetworkClientState)>;
-
-#[cfg(not(feature = "tls-connections"))]
-pub enum ClientSocket {
- Plain(TcpStream)
-}
-
-#[cfg(feature = "tls-connections")]
-pub enum ClientSocket {
- SslHandshake(Option<MidHandshakeSslStream<TcpStream>>),
- SslStream(SslStream<TcpStream>)
-}
-
-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<TcpStream>) -> io::Result<NetworkClientState> {
- 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<R: Read>(decoder: &mut ProtocolDecoder, source: &mut R,
- id: ClientId, addr: &SocketAddr) -> NetworkResult<Vec<HWProtocolMessage>> {
- 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<Vec<HWProtocolMessage>> {
- #[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<W: Write>(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<NetworkClient>,
- 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<ClientSocket> {
- #[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(())
- }
-}
--- 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<String>
-}
-
-#[derive(Clone, Serialize, Deserialize)]
-struct Scheme {
- name: String,
- settings: Vec<String>
-}
-
-#[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<String>
-}
-
-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<Item = &TeamInfo> + Clone
-{
- teams.iter().filter(move |(id, _)| *id == client_id).map(|(_, t)| t)
-}
-
-fn map_config_from(c: &RoomConfig) -> Vec<String> {
- 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<GameCfg> {
- 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<String>,
- pub msg_log: Vec<String>,
- pub sync_msg: Option<String>,
- 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<Item = &TeamInfo> + 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<ClientId>,
- pub name: String,
- pub password: Option<String>,
- 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<Voting>,
- pub saves: HashMap<String, RoomSave>,
- pub game_info: Option<GameInfo>
-}
-
-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<String> {
- 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<F>(&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<F>(&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<Item = &TeamInfo> {
- client_teams_impl(&self.teams, client_id)
- }
-
- pub fn client_team_indices(&self, client_id: ClientId) -> Vec<u8> {
- 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<u8> {
- 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<String> {
- 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<String> {
- match self.game_info {
- Some(ref info) => map_config_from(&info.config),
- None => map_config_from(&self.config)
- }
- }
-
- pub fn game_config(&self) -> Vec<GameCfg> {
- 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<String, serde_yaml::Error> {
- 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<String, RoomSave>)>(text).map(|(greeting, saves)| {
- self.greeting = greeting;
- self.saves = saves;
- })
- }
-
- pub fn team_info(owner: &HWClient, team: &TeamInfo) -> Vec<String> {
- 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
--- 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<T> = slab::Slab<T>;
-
-
-pub struct HWServer {
- pub clients: Slab<HWClient>,
- pub rooms: Slab<HWRoom>,
- pub lobby_id: RoomId,
- pub output: Vec<(Vec<ClientId>, HWServerMessage)>,
- pub removed_clients: Vec<ClientId>,
-}
-
-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<ClientId> {
- 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::<Vec<_>>()
- };
- 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<actions::Action>) {
- 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<F>(&self, f: F) -> Vec<ClientId>
- 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<ClientId> {
- self.select_clients(|(_, c)| c.room_id == Some(room_id))
- }
-
- pub fn protocol_clients(&self, protocol: u16) -> Vec<ClientId> {
- self.select_clients(|(_, c)| c.protocol_number == protocol)
- }
-
- pub fn other_clients_in_room(&self, self_id: ClientId) -> Vec<ClientId> {
- 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
- }
-}
--- 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<T>(msg: T) -> String
- where T: Iterator<Item = u8> + 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
--- 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;
--- 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';
--- 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);
--- 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);
--- 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
--- 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);
--- 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);
--- 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);
--- 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;
--- 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);
--- 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;
--- 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;
--- 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;
--- 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,
--- 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;
--- 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;
--- 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)
--- 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()
}
}
--- 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 <QDebug>
+#include <QLibrary>
#include <QOpenGLFunctions>
#include <QSurface>
@@ -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<Engine::hedgewars_engine_protocol_version_t*>(
+ hwlib.resolve("hedgewars_engine_protocol_version"));
+ start_engine =
+ reinterpret_cast<Engine::start_engine_t*>(hwlib.resolve("start_engine"));
+ generate_preview = reinterpret_cast<Engine::generate_preview_t*>(
+ hwlib.resolve("generate_preview"));
+ dispose_preview = reinterpret_cast<Engine::dispose_preview_t*>(
+ hwlib.resolve("dispose_preview"));
+ cleanup = reinterpret_cast<Engine::cleanup_t*>(hwlib.resolve("cleanup"));
+
+ send_ipc = reinterpret_cast<Engine::send_ipc_t*>(hwlib.resolve("send_ipc"));
+ read_ipc = reinterpret_cast<Engine::read_ipc_t*>(hwlib.resolve("read_ipc"));
-EngineInstance::~EngineInstance() { Engine::cleanup(m_instance); }
+ setup_current_gl_context =
+ reinterpret_cast<Engine::setup_current_gl_context_t*>(
+ hwlib.resolve("setup_current_gl_context"));
+ render_frame =
+ reinterpret_cast<Engine::render_frame_t*>(hwlib.resolve("render_frame"));
+ advance_simulation = reinterpret_cast<Engine::advance_simulation_t*>(
+ 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<uint8_t*>(b.data()),
- static_cast<size_t>(b.size()));
+ send_ipc(m_instance, reinterpret_cast<uint8_t*>(b.data()),
+ static_cast<size_t>(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<quint16>(size.width()),
- static_cast<quint16>(size.height()), &getProcAddress);
+ setup_current_gl_context(m_instance, static_cast<quint16>(size.width()),
+ static_cast<quint16>(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<QRgb> 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<int>(pinfo.width),
+ static_cast<int>(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; }
--- 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 <QImage>
#include <QObject>
#include <QOpenGLContext>
+#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
--- 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
}
};
--- 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 <QDebug>
-#include <QLibrary>
-#include <QQmlEngine>
+#include <QImage>
#include <QUuid>
#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<HWEngine>("Hedgewars.Engine", 1, 0, "HWEngine",
- hwengine_singletontype_provider);
- qmlRegisterType<GameView>("Hedgewars.Engine", 1, 0, "GameView");
- qmlRegisterUncreatableType<EngineInstance>("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<QRgb> 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<int>(preview.width),
- static_cast<int>(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);
+}
--- 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
--- 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 <QQmlApplicationEngine>
#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<Engine::protocol_version_t*>(
- hwlib.resolve("protocol_version"));
- Engine::start_engine =
- reinterpret_cast<Engine::start_engine_t*>(hwlib.resolve("start_engine"));
- Engine::generate_preview = reinterpret_cast<Engine::generate_preview_t*>(
- hwlib.resolve("generate_preview"));
- Engine::cleanup =
- reinterpret_cast<Engine::cleanup_t*>(hwlib.resolve("cleanup"));
+static QObject* previewacceptor_singletontype_provider(
+ QQmlEngine* engine, QJSEngine* scriptEngine) {
+ Q_UNUSED(scriptEngine)
- Engine::send_ipc =
- reinterpret_cast<Engine::send_ipc_t*>(hwlib.resolve("send_ipc"));
- Engine::read_ipc =
- reinterpret_cast<Engine::read_ipc_t*>(hwlib.resolve("read_ipc"));
-
- Engine::setup_current_gl_context =
- reinterpret_cast<Engine::setup_current_gl_context_t*>(
- hwlib.resolve("setup_current_gl_context"));
- Engine::render_frame =
- reinterpret_cast<Engine::render_frame_t*>(hwlib.resolve("render_frame"));
- Engine::advance_simulation = reinterpret_cast<Engine::advance_simulation_t*>(
- 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<PreviewAcceptor>(
+ "Hedgewars.Engine", 1, 0, "PreviewAcceptor",
+ previewacceptor_singletontype_provider);
+ qmlRegisterType<HWEngine>("Hedgewars.Engine", 1, 0, "HWEngine");
+ qmlRegisterType<GameView>("Hedgewars.Engine", 1, 0, "GameView");
+ qmlRegisterUncreatableType<EngineInstance>("Hedgewars.Engine", 1, 0,
+ "EngineInstance",
+ "Create by HWEngine run methods");
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty()) return -1;
--- /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 <QImage>
+#include <QQmlEngine>
+
+#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);
+}
--- /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 <QObject>
+
+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
--- 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<u32> {
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() {
--- /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 <a.korotaev@hedgewars.org>" ]
+
+[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
--- /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();
+ }
+}
--- /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<R: Read>(&mut self, stream: &mut R) -> Result<usize> {
+ self.buf.read_from(stream)
+ }
+
+ pub fn extract_messages(&mut self) -> Vec<messages::HWProtocolMessage> {
+ 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;
+ }
+}
--- /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<String>),
+ //Cmd(String, Vec<String>),
+ 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<String>),
+ JoinRoom(String, Option<String>),
+ Follow(String),
+ Rnd(Vec<String>),
+ 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<String>),
+ Cfg(GameCfg),
+ AddTeam(Box<TeamInfo>),
+ 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<VoteType>),
+ 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<String>),
+ ChatMsg {nick: String, msg: String},
+ ClientFlags(String, Vec<String>),
+ Rooms(Vec<String>),
+ RoomAdd(Vec<String>),
+ RoomJoined(Vec<String>),
+ RoomLeft(String, String),
+ RoomRemove(String),
+ RoomUpdated(String, Vec<String>),
+ TeamAdd(Vec<String>),
+ TeamRemove(String),
+ TeamAccepted(String),
+ TeamColor(String, u8),
+ HedgehogsNumber(String, u8),
+ ConfigEntry(String, Vec<String>),
+ Kicked,
+ RunGame,
+ ForwardEngineMessage(Vec<String>),
+ RoundFinished,
+
+ ServerMessage(String),
+ Notice(String),
+ Warning(String),
+ Error(String),
+ Connected(u32),
+ Unreachable,
+
+ //Deprecated messages
+ LegacyReady(bool, Vec<String>)
+}
+
+pub fn server_chat(msg: String) -> HWServerMessage {
+ HWServerMessage::ChatMsg{ nick: "[server]".to_string(), msg }
+}
+
+impl GameCfg {
+ pub fn to_protocol(&self) -> (String, Vec<String>) {
+ 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::<Vec<_>>().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<String>)>) =>, ??
+ 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"],
+ }
+ }
+}
--- /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<String> >, 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<String> >, 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<HWProtocolMessage> >, 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])));
+}
--- /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<T>: Sized { fn into2(self) -> T; }
+impl <T> Into2<T> for T { fn into2(self) -> T { self } }
+impl Into2<Vec<String>> for Vec<Ascii> {
+ fn into2(self) -> Vec<String> {
+ self.into_iter().map(|x| x.0).collect()
+ }
+}
+impl Into2<String> for Ascii { fn into2(self) -> String { self.0 } }
+impl Into2<Option<String>> for Option<Ascii>{
+ fn into2(self) -> Option<String> { 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 = <String as Arbitrary>::Parameters;
+
+ fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
+ "[a-zA-Z0-9]+".prop_map(Ascii).boxed()
+ }
+
+ type Strategy = BoxedStrategy<Ascii>;
+}
+
+impl Arbitrary for GameCfg {
+ type Parameters = ();
+
+ fn arbitrary_with(_args: <Self as Arbitrary>::Parameters) -> <Self as Arbitrary>::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<Ascii>),
+ 7 => Scheme(Ascii, Vec<Ascii>),
+ 8 => Script(Ascii),
+ 9 => Theme(Ascii),
+ 10 => DrawnMap(Ascii))
+ }).boxed()
+ }
+
+ type Strategy = BoxedStrategy<GameCfg>;
+}
+
+impl Arbitrary for TeamInfo {
+ type Parameters = ();
+
+ fn arbitrary_with(_args: <Self as Arbitrary>::Parameters) -> <Self as Arbitrary>::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<TeamInfo>;
+}
+
+pub fn gen_proto_msg() -> BoxedStrategy<HWProtocolMessage> where {
+ let res = (0..58).no_shrink().prop_flat_map(|i| {
+ proto_msg_match!(i, def = Malformed,
+ 0 => Ping(),
+ 1 => Pong(),
+ 2 => Quit(Option<Ascii>),
+ //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<Ascii>),
+ 16 => JoinRoom(Ascii, Option<Ascii>),
+ 17 => Follow(Ascii),
+ 18 => Rnd(Vec<Ascii>),
+ 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<Ascii>),
+ 30 => Cfg(GameCfg),
+ 31 => AddTeam(Box<TeamInfo>),
+ 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<String>)>),
+ 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()
+}
--- /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;
--- /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<RoomId>,
+ protocol: Option<u16>,
+ 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<Action> 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<String>),
+ RemoveRoom(RoomId),
+ MoveToRoom(RoomId),
+ MoveToLobby(String),
+ ChangeMaster(RoomId, Option<ClientId>),
+ RemoveTeam(String),
+ RemoveClientTeams,
+ SendRoomUpdate(Option<String>),
+ 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())
+ }
+ }
+}
--- /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<usize>,
+ 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<u8>,
+ pub clan: Option<u8>
+}
+
+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
--- /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<T> = slab::Slab<T>;
+
+pub struct HWServer {
+ pub clients: Slab<HWClient>,
+ pub rooms: Slab<HWRoom>,
+ pub lobby_id: RoomId,
+ pub output: Vec<(Vec<ClientId>, HWServerMessage)>,
+ pub removed_clients: Vec<ClientId>,
+ pub io: Box<dyn HWServerIO>
+}
+
+impl HWServer {
+ pub fn new(clients_limit: usize, rooms_limit: usize, io: Box<dyn HWServerIO>) -> 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<ClientId> {
+ 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::<Vec<_>>()
+ };
+ 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<actions::Action>) {
+ 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<F>(&self, f: F) -> Vec<ClientId>
+ 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<ClientId> {
+ self.select_clients(|(_, c)| c.room_id == Some(room_id))
+ }
+
+ pub fn protocol_clients(&self, protocol: u16) -> Vec<ClientId> {
+ self.select_clients(|(_, c)| c.protocol_number == protocol)
+ }
+
+ pub fn other_clients_in_room(&self, self_id: ClientId) -> Vec<ClientId> {
+ 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
+ }
+}
--- /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<String>),
+ Scheme(String, Vec<String>),
+ 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<String>),
+ Pause,
+ NewSeed,
+ HedgehogsPerTeam(u8)
+}
+
+#[derive(Clone, Debug)]
+pub struct Voting {
+ pub ttl: u32,
+ pub voters: Vec<ClientId>,
+ pub votes: Vec<(ClientId, bool)>,
+ pub kind: VoteType
+}
+
+impl Voting {
+ pub fn new(kind: VoteType, voters: Vec<ClientId>) -> Voting {
+ Voting {
+ kind, voters, ttl: 2,
+ votes: Vec::new()
+ }
+ }
+}
\ No newline at end of file
--- /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)
+ }
+ },
+ }
+}
--- /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"),
+ }
+}
--- /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<String>) {
+ 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;
+ }
+ }
+ }
+}
--- /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<<Self as Iterator>::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 <nickname>, map <name>, pause, newseed, hedgehogs <number>".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::<Vec<_>>());
+ 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::<Vec<_>>());
+ 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!")
+ }
+}
--- /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"),
+ }
+}
--- /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"),
+ }
+}
--- /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<String>;
+}
+
+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<String> {
+ 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<String> {
+ 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
--- /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<T> = io::Result<(T, NetworkClientState)>;
+
+#[cfg(not(feature = "tls-connections"))]
+pub enum ClientSocket {
+ Plain(TcpStream)
+}
+
+#[cfg(feature = "tls-connections")]
+pub enum ClientSocket {
+ SslHandshake(Option<MidHandshakeSslStream<TcpStream>>),
+ SslStream(SslStream<TcpStream>)
+}
+
+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<TcpStream>) -> io::Result<NetworkClientState> {
+ 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<R: Read>(decoder: &mut ProtocolDecoder, source: &mut R,
+ id: ClientId, addr: &SocketAddr) -> NetworkResult<Vec<HWProtocolMessage>> {
+ 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<Vec<HWProtocolMessage>> {
+ #[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<W: Write>(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<NetworkClient>,
+ 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<ClientSocket> {
+ #[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(())
+ }
+}
--- /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<String>
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+struct Scheme {
+ name: String,
+ settings: Vec<String>
+}
+
+#[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<String>
+}
+
+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<Item = &TeamInfo> + Clone
+{
+ teams.iter().filter(move |(id, _)| *id == client_id).map(|(_, t)| t)
+}
+
+fn map_config_from(c: &RoomConfig) -> Vec<String> {
+ 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<GameCfg> {
+ 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<String>,
+ pub msg_log: Vec<String>,
+ pub sync_msg: Option<String>,
+ 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<Item = &TeamInfo> + 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<ClientId>,
+ pub name: String,
+ pub password: Option<String>,
+ 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<Voting>,
+ pub saves: HashMap<String, RoomSave>,
+ pub game_info: Option<GameInfo>
+}
+
+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<String> {
+ 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<F>(&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<F>(&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<Item = &TeamInfo> {
+ client_teams_impl(&self.teams, client_id)
+ }
+
+ pub fn client_team_indices(&self, client_id: ClientId) -> Vec<u8> {
+ 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<u8> {
+ 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<String> {
+ 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<String> {
+ match self.game_info {
+ Some(ref info) => map_config_from(&info.config),
+ None => map_config_from(&self.config)
+ }
+ }
+
+ pub fn game_config(&self) -> Vec<GameCfg> {
+ 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<String, serde_yaml::Error> {
+ 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<String, RoomSave>)>(text).map(|(greeting, saves)| {
+ self.greeting = greeting;
+ self.saves = saves;
+ })
+ }
+
+ pub fn team_info(owner: &HWClient, team: &TeamInfo) -> Vec<String> {
+ 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
--- /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<T>(msg: T) -> String
+ where T: Iterator<Item = u8> + 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
--- 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]
--- 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<u8>,
+ preview: Option<Land2D<u8>>,
game_state: Option<GameState>,
}
@@ -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<u8> {
+ pub fn preview(&self) -> &Option<Land2D<u8>> {
&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) {
Binary file share/hedgewars/Data/Graphics/Finger.png has changed
Binary file share/hedgewars/Data/Graphics/Switch.png has changed
Binary file share/hedgewars/Data/Graphics/Targetp.png has changed
Binary file share/hedgewars/Data/Graphics/Targetp@2x.png has changed
--- 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!
--- 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!
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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)
--- 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"
--- 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
--- 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()
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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"
--- 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")
--- 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
--- 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
--- 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
--- 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
--- 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"
--- 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
--- 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)
--- 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
--- 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.
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
Binary file share/hedgewars/Data/Sounds/voices/Default_ru/Hmm.ogg has changed