--- a/.hgtags Sat Sep 28 16:39:02 2019 +0300
+++ b/.hgtags Tue Oct 15 13:42:50 2019 -0400
@@ -89,3 +89,4 @@
afc089c39556bdd895892fa36ed47aaec83c3825 0.9.24.1-release
195208deff1dd3e22d303d4a92c2ba14be3b6623 Hedgewars-iOS-2.1
5e28098fb59379357a145b73380a1cd3839f643f 0.9.25-release
+3102d95a870e61385ee6951e30dc3be739210093 1.0.0-release
--- a/.travis.yml Sat Sep 28 16:39:02 2019 +0300
+++ b/.travis.yml Tue Oct 15 13:42:50 2019 -0400
@@ -52,6 +52,7 @@
before_install: |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
+ sudo add-apt-repository ppa:costamagnagianfranco/hedgewars-nightly -y
sudo apt-get update -qq
elif [ "$TRAVIS_OS_NAME" == "osx" ]; then
brew update
@@ -65,18 +66,7 @@
install: |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
- sudo apt-get install -y debhelper cmake dpkg-dev qtbase5-dev qtbase5-private-dev qttools5-dev-tools qttools5-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-net-dev bzip2 ghc libghc-mtl-dev libghc-vector-dev libghc-zlib-dev libghc-random-dev libghc-network-dev libghc-sandi-dev libghc-hslogger-dev libghc-utf8-string-dev libghc-sha-dev libghc-entropy-dev libghc-regex-tdfa-dev libghc-aeson-dev libghc-yaml-dev libghc-text-dev liblua5.1-0-dev fpc fp-compiler fp-units-misc libpng-dev fp-units-gfx libavcodec-dev libavformat-dev libglew1.6-dev
-
- # for xenial last availible version of libphysfs is 2.0.x, but we need >= 3.0
- # so... building from sources!
- wget https://icculus.org/physfs/downloads/physfs-3.0.1.tar.bz2
- tar -xjf physfs-3.0.1.tar.bz2
- mkdir physfs-3.0.1-build
- pushd physfs-3.0.1-build
- cmake ../physfs-3.0.1
- make
- sudo make install
- popd
+ sudo apt-get install -y cmake debhelper dpkg-dev fp-compiler fp-units-gfx fp-units-misc ghc libavcodec-dev libavformat-dev libghc-aeson-dev libghc-entropy-dev libghc-hslogger-dev libghc-mtl-dev libghc-network-dev libghc-parsec3-dev libghc-random-dev libghc-regex-tdfa-dev libghc-sandi-dev libghc-sha-dev libghc-text-dev libghc-utf8-string-dev libghc-vector-dev libghc-yaml-dev libghc-zlib-dev liblua5.1-dev libphysfs-dev libpng-dev libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-net-dev libsdl2-ttf-dev qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools
elif [ "$TRAVIS_OS_NAME" == "osx" ]; then
brew install qt5
brew install fpc glew physfs lua51 sdl2 sdl2_image sdl2_net sdl2_ttf ffmpeg ghc cabal-install
@@ -84,11 +74,11 @@
# use cabal install haskell deps, pas2c ones are covered by server
if [[ "$BUILD_ARGS" != *"NOSERVER"* ]]; then
cabal update
- cabal install --only-dependencies gameServer/hedgewars-server.cabal
+ cabal install --only-dependencies --cabal-file=gameServer/hedgewars-server.cabal
fi
if [[ "$BUILD_ARGS" == *"BUILD_ENGINE_C"* ]]; then
cabal update
- cabal install --only-dependencies tools/pas2c/pas2c.cabal
+ cabal install --only-dependencies --cabal-file=tools/pas2c/pas2c.cabal
fi
# avoid installing Sparkle, add default unit path
export BUILD_ARGS="$BUILD_ARGS -DNOAUTOUPDATE=1"
--- a/CMakeLists.txt Sat Sep 28 16:39:02 2019 +0300
+++ b/CMakeLists.txt Tue Oct 15 13:42:50 2019 -0400
@@ -54,7 +54,7 @@
if(BUILD_ENGINE_C AND NOT NOVIDEOREC)
- if((CMAKE_BUILD_TYPE MATCHES "RELEASE") OR (CMAKE_BUILD_TYPE MATCHES "RELWITHDEBUGINFO"))
+ if((CMAKE_BUILD_TYPE STREQUAL "Release") OR (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo"))
message("NOTE: Video recorder support disabled. It's incompatible with BUILD_ENGINE_C")
set(BUILD_ENGINE_C ON CACHE STRING "Required for BUILD_ENGINE_JS" FORCE)
else()
@@ -91,9 +91,9 @@
#versioning
set(CPACK_PACKAGE_VERSION_MAJOR 1)
set(CPACK_PACKAGE_VERSION_MINOR 0)
-set(CPACK_PACKAGE_VERSION_PATCH 0)
-set(HEDGEWARS_PROTO_VER 58)
-if((CMAKE_BUILD_TYPE MATCHES "RELEASE") OR (CMAKE_BUILD_TYPE MATCHES "RELWITHDEBUGINFO"))
+set(CPACK_PACKAGE_VERSION_PATCH 1)
+set(HEDGEWARS_PROTO_VER 60)
+if((CMAKE_BUILD_TYPE STREQUAL "Release") OR (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo"))
set(HEDGEWARS_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
else()
set(HEDGEWARS_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}-dev")
@@ -112,10 +112,9 @@
#when build type is not specified, assume Debug/Release according to build version information
if(CMAKE_BUILD_TYPE)
- string(TOUPPER ${CMAKE_BUILD_TYPE} CMAKE_BUILD_TYPE)
- if(NOT((CMAKE_BUILD_TYPE MATCHES "RELEASE") OR
- (CMAKE_BUILD_TYPE MATCHES "DEBUG") OR
- (CMAKE_BUILD_TYPE MATCHES "RELWITHDEBINFO")))
+ if(NOT((CMAKE_BUILD_TYPE STREQUAL "Release") OR
+ (CMAKE_BUILD_TYPE STREQUAL "Debug") OR
+ (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")))
set(CMAKE_BUILD_TYPE ${default_build_type} CACHE STRING "Build type (Debug/Release/RelWithDebInfo)" FORCE)
message(STATUS "Unknown build type ${CMAKE_BUILD_TYPE}, using default (${default_build_type})")
endif()
@@ -168,7 +167,7 @@
#get BUILD_TYPE and enable/disable optimisation
message(STATUS "Using ${CMAKE_BUILD_TYPE} configuration")
-if(CMAKE_BUILD_TYPE MATCHES "DEBUG")
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
list(APPEND haskell_flags "-Wall" # all warnings
"-debug" # debug mode
"-fno-warn-unused-do-bind"
--- a/CREDITS Sat Sep 28 16:39:02 2019 +0300
+++ b/CREDITS Tue Oct 15 13:42:50 2019 -0400
@@ -83,6 +83,8 @@
https://www.freesound.org/people/rombart/sounds/197800/
- Flamethrower sound originally by AslakHostaker (CC-0), adapted from
https://freesound.org/people/AslakHostaker/sounds/395039/
+- Dynamite fuse: Based off sound by apolloaiello (CC BY 3.0)
+ https://freesound.org/people/apolloaiello/sounds/329045/
- Landspray sound originally by Benboncan (CC BY 3.0), remixed from
https://freesound.org/people/Benboncan/sounds/82390/
- Portable Portal Device color switching sound by Wuzzy (CC-0)
--- a/ChangeLog.txt Sat Sep 28 16:39:02 2019 +0300
+++ b/ChangeLog.txt Tue Oct 15 13:42:50 2019 -0400
@@ -1,6 +1,6 @@
+ features
* bugfixes
-============== 1.0.0-dev (unreleased) ==============
+====================== 1.0.0 =======================
Highlights:
+ Campaigns now respect your team identity instead of overwriting it
+ Single missions now support team selection and track your progress
@@ -58,6 +58,8 @@
* Space Invasion: No longer allow to set start shield above shield limit
* Battalion, WxW: Crates drop between turns, when appropriate
* Battalion: Sudden Death effects are now like in the base game
+ * Battalion: Fix incorrect health boost in Highland mode
+ * Battalion: Fix points display not updating properly
* King Mode: Fix team sometimes not being killed properly if king drowned
* King Mode: Kill resurrected minions if king is not alive
* King Mode: Fix whole clan being killed if a king died
@@ -135,6 +137,7 @@
+ Show contour of flying saucer and air mines when in highly opaque water
+ Remove visual clutter in cut scenes
+ Add setting to set default/initial zoom
+ + Render arrow pointing to hog only one if playing with wrapped map
* Black clan color can now be used without visual problems
* Fix last 2 characters in demo chat being missing
* Hide most HUD elements in cinematic mode
@@ -169,6 +172,7 @@
* Fix force-locked schemes getting unlocked when changing map types
* Fix possible to select background-only or hidden themes indirectly by changing map type
* Disallow slash, backslash and colon characters in team and scheme names
+ * Increase user name length from 20 to 40
Sounds and voicepacks:
+ sndYoohoo has been split to sndYoohoo and sndKiss
@@ -395,7 +399,7 @@
Translations:
+ Translations kept up-to-date: German, Polish
- + Major translation updates: Russian, Japanese, Scottish Gaelic, Ukrainian
+ + Major translation updates: Russian, Japanese, Scottish Gaelic, Ukrainian, Italian
Lua API:
* Deprecation: Setting TurnTimeLeft/ReadyTimeLeft directly is deprecated and will become useless in future. Use the setter functions below
--- a/QTfrontend/CMakeLists.txt Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/CMakeLists.txt Tue Oct 15 13:42:50 2019 -0400
@@ -213,9 +213,9 @@
#when debugging, always prompt a console to see fronted messages
#TODO: check it doesn't interfere on UNIX
-if(CMAKE_BUILD_TYPE MATCHES "RELEASE" OR CMAKE_BUILD_TYPE MATCHES "RELWITHDEBINFO")
+if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(console_access "WIN32")
-endif(CMAKE_BUILD_TYPE MATCHES "RELEASE" OR CMAKE_BUILD_TYPE MATCHES "RELWITHDEBINFO")
+endif(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_executable(hedgewars ${console_access}
${hwfr_src}
--- a/QTfrontend/binds.cpp Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/binds.cpp Tue Oct 15 13:42:50 2019 -0400
@@ -84,7 +84,7 @@
{"!MULTI", QT_TRANSLATE_NOOP("binds (combination)", "precise + toggle hedgehog tags"), QT_TRANSLATE_NOOP("binds", "change hedgehog tag types"), NULL, NULL},
{"!MULTI", QT_TRANSLATE_NOOP("binds (combination)", "switch + toggle hedgehog tags"), QT_TRANSLATE_NOOP("binds", "toggle hedgehog tag translucency"), NULL, NULL},
- {"!MULTI", QT_TRANSLATE_NOOP("binds (combination)", "precise + switch + toggle hedgehog tags"), QT_TRANSLATE_NOOP("binds", "toggle HUD"), NULL, NULL},
+ {"!MULTI", QT_TRANSLATE_NOOP("binds (combination)", "precise + switch + toggle team bars"), QT_TRANSLATE_NOOP("binds", "toggle HUD"), NULL, NULL},
#ifdef VIDEOREC
{"record", "r", QT_TRANSLATE_NOOP("binds", "record"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Record video:")}
#endif
Binary file QTfrontend/hedgewars.ico has changed
--- a/QTfrontend/hwform.cpp Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/hwform.cpp Tue Oct 15 13:42:50 2019 -0400
@@ -1250,7 +1250,7 @@
noRegMsg.setIcon(QMessageBox::Information);
noRegMsg.setWindowTitle(QMessageBox::tr("Hedgewars - Nick not registered"));
noRegMsg.setWindowModality(Qt::WindowModal);
- noRegMsg.setText(tr("Your nickname is not registered.\nTo prevent someone else from using it,\nplease register it at www.hedgewars.org"));
+ noRegMsg.setText(tr("Your nickname is not registered.\nTo be able to rejoin games in progress and\nprevent someone else from using your nickname,\nplease register it at www.hedgewars.org."));
if (!config->passwordHash().isEmpty())
{
--- a/QTfrontend/res/credits.csv Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/res/credits.csv Tue Oct 15 13:42:50 2019 -0400
@@ -153,6 +153,7 @@
E,"Italian","Marco Bresciani","m.bresciani@email.it",
E,"Italian","Gianfranco Costamagna","costamagnagianfranco@yahoo.it",
E,"Italian",,"enricobe@hotmail.com","Enrico"
+E,"Italian","Pacella Marco Ernesto","pacella389@gmail.com","KIRA"
E,"Japanese","ADAM Etienne","etienne.adam@gmail.com",
E,"Japanese","Marco Bresciani","m.bresciani@email.it",
E,"Korean","Anthony Bellew","anthonyreflected@gmail.com",
--- a/QTfrontend/ui/page/pagegamestats.cpp Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/ui/page/pagegamestats.cpp Tue Oct 15 13:42:50 2019 -0400
@@ -121,7 +121,7 @@
btnRestart->setFixedHeight(81);
btnRestart->setStyleSheet("QPushButton{margin-top:24px}");
btnSave = addButton(":/res/Save.png", bottomLayout, 2, true);
- btnSave->setWhatsThis(tr("Save"));
+ saveDemoBtnEnabled(true);
btnSave->setStyleSheet("QPushButton{margin: 24px 0 0 0;}");
return bottomLayout;
@@ -174,6 +174,10 @@
void PageGameStats::saveDemoBtnEnabled(bool enabled)
{
btnSave->setEnabled(enabled);
+ if (enabled)
+ btnSave->setWhatsThis(tr("Save demo"));
+ else
+ btnSave->setWhatsThis(tr("Save demo (unavailable because the /lua command was used)"));
}
void PageGameStats::renderStats()
--- a/QTfrontend/ui/page/pageoptions.cpp Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/ui/page/pageoptions.cpp Tue Oct 15 13:42:50 2019 -0400
@@ -573,7 +573,7 @@
groupAccount->layout()->addWidget(labelNN, 0, 0);
editNetNick = new QLineEdit(groupAccount);
- editNetNick->setMaxLength(20);
+ editNetNick->setMaxLength(40);
editNetNick->setText(QLineEdit::tr("anonymous"));
groupAccount->layout()->addWidget(editNetNick, 0, 1);
--- a/QTfrontend/ui/widget/keybinder.cpp Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/ui/widget/keybinder.cpp Tue Oct 15 13:42:50 2019 -0400
@@ -209,12 +209,7 @@
curTable->insertRow(row);
curTable->setItem(row, 0, nameCell);
QTableWidgetItem * bindCell;
- // Check if the bind text is bad. This was discovered after the 1.0.0,
- // so we need a little workaround.
- bool is_broken_strbind = cbinds[i].strbind == "precise + switch + toggle hedgehog tags";
- // ^ should be "precise + switch + toggle team bars"
- // TODO: Remove is_broken_strbind after 1.0.0 release.
- if (cbinds[i].action != "!MULTI" && (!is_broken_strbind))
+ if (cbinds[i].action != "!MULTI")
{
bindCell = new QTableWidgetItem(comboBox->currentText());
nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
@@ -223,19 +218,7 @@
}
else
{
- // Apply workaround for the broken 1.0.0 strbind
- // TODO: Remove the workaround after 1.0.0 release and fix binds.cpp accordingly.
- if (is_broken_strbind)
- {
- // We simply construct the string from other strings we *do* have. :-)
- QString cellText =
- HWApplication::translate("binds", "precise aim") + " + " +
- HWApplication::translate("binds", "switch") + " + " +
- HWApplication::translate("binds", "toggle team bars");
- bindCell = new QTableWidgetItem(cellText);
- }
- else
- bindCell = new QTableWidgetItem(HWApplication::translate("binds (combination)", cbinds[i].strbind.toUtf8().constData()));
+ bindCell = new QTableWidgetItem(HWApplication::translate("binds (combination)", cbinds[i].strbind.toUtf8().constData()));
nameCell->setFlags(Qt::NoItemFlags);
bindCell->setFlags(Qt::NoItemFlags);
bindCell->setIcon(emptyIcon);
--- a/QTfrontend/util/DataManager.cpp Sat Sep 28 16:39:02 2019 +0300
+++ b/QTfrontend/util/DataManager.cpp Tue Oct 15 13:42:50 2019 -0400
@@ -28,7 +28,7 @@
#include <QSettings>
#include <QColor>
-#include <SDL2/SDL.h>
+#include <SDL.h>
#include "hwconsts.h"
#include "HWApplication.h"
--- a/cmake_modules/compilerchecks.cmake Sat Sep 28 16:39:02 2019 +0300
+++ b/cmake_modules/compilerchecks.cmake Tue Oct 15 13:42:50 2019 -0400
@@ -62,7 +62,7 @@
endif()
endif()
- if(CMAKE_BUILD_TYPE MATCHES "RELEASE" OR CMAKE_BUILD_TYPE MATCHES "RELWITHDEBINFO")
+ if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(CMAKE_REQUIRED_FLAGS "-Wl,--as-needed")
check_c_compiler_flag("" HAVE_ASNEEDED)
if(HAVE_ASNEEDED)
--- a/cmake_modules/cpackvars.cmake Sat Sep 28 16:39:02 2019 +0300
+++ b/cmake_modules/cpackvars.cmake Tue Oct 15 13:42:50 2019 -0400
@@ -1,6 +1,6 @@
# revision information in cpack-generated names
-if(CMAKE_BUILD_TYPE MATCHES DEBUG)
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(full_suffix "${HEDGEWARS_VERSION}-r${HEDGEWARS_REVISION}")
else()
set(full_suffix "${HEDGEWARS_VERSION}")
@@ -106,6 +106,13 @@
"^${CMAKE_CURRENT_SOURCE_DIR}/gameServer2"
"^${CMAKE_CURRENT_SOURCE_DIR}/rust"
"^${CMAKE_CURRENT_SOURCE_DIR}/qmlfrontend"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/hedgewars"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/hwengine"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/hedgewars-server"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/link\\\\.res"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/ppas\\\\.sh"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/libavwrapper\\\\.*"
+ "^${CMAKE_CURRENT_SOURCE_DIR}/bin/libphyslayer\\\\.*"
)
include(CPack)
--- a/cmake_modules/revinfo.cmake Sat Sep 28 16:39:02 2019 +0300
+++ b/cmake_modules/revinfo.cmake Tue Oct 15 13:42:50 2019 -0400
@@ -17,7 +17,7 @@
endif()
#let's assume that if you have hg you might be interested in debugging
- set(default_build_type "DEBUG")
+ set(default_build_type "Debug")
#write down hash and rev for easy picking should hg be missing
file(WRITE "${CMAKE_SOURCE_DIR}/share/version_info.txt" "Hedgewars versioning information, do not modify\nrev ${HEDGEWARS_REVISION}\nhash ${HEDGEWARS_HASH}\n")
@@ -30,12 +30,12 @@
set(HEDGEWARS_REVISION "GIT")
#let's assume that if you have git you might be interested in debugging
- set(default_build_type "DEBUG")
+ set(default_build_type "Debug")
#write down hash and rev for easy picking should hg be missing
file(WRITE "${CMAKE_SOURCE_DIR}/share/version_info.txt" "Hedgewars versioning information, do not modify\nrev ${HEDGEWARS_REVISION}\nhash ${HEDGEWARS_HASH}\n")
else()
- set(default_build_type "RELEASE")
+ set(default_build_type "Release")
# when compiling outside rev control, fetch revision and hash information from version_info.txt
find_file(version_info version_info.txt PATH ${CMAKE_SOURCE_DIR}/share)
if(version_info)
--- a/gameServer/CoreTypes.hs Sat Sep 28 16:39:02 2019 +0300
+++ b/gameServer/CoreTypes.hs Tue Oct 15 13:42:50 2019 -0400
@@ -313,8 +313,8 @@
True
False
"<h2><p align=center><a href=\"https://www.hedgewars.org/\">https://www.hedgewars.org/</a></p></h2>"
- "<font color=yellow><h3 align=center>Hedgewars 0.9.25 is out! Please update.</h3><p align=center><a href=https://hedgewars.org/download.html>Download page here</a></font>"
- 57 -- latestReleaseVersion
+ "<font color=yellow><h3 align=center>Hedgewars 1.0.0 is out! Please update.</h3><p align=center><a href=https://hedgewars.org/download.html>Download page here</a></font>"
+ 59 -- latestReleaseVersion
41 -- earliestCompatibleVersion
46631
""
--- a/gameServer/Utils.hs Sat Sep 28 16:39:02 2019 +0300
+++ b/gameServer/Utils.hs Tue Oct 15 13:42:50 2019 -0400
@@ -127,6 +127,8 @@
, (56, "0.9.25-dev")
, (57, "0.9.25")
, (58, "1.0.0-dev")
+ , (59, "1.0.0")
+ , (60, "1.0.1-dev")
]
askFromConsole :: B.ByteString -> IO B.ByteString
--- a/hedgewars/uAI.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uAI.pas Tue Oct 15 13:42:50 2019 -0400
@@ -107,6 +107,7 @@
BotLevel:= Me^.Hedgehog^.BotLevel;
windSpeed:= hwFloat2Float(cWindSpeed);
useThisActions:= false;
+Me^.AIHints:= Me^.AIHints and (not aihAmmosChanged);
for i:= 0 to Pred(Targets.Count) do
if (Targets.ar[i].Score >= 0) and (not StopThinking) then
@@ -432,7 +433,7 @@
switchCount:= HHHasAmmo(PGear(Me)^.Hedgehog^, amSwitch)
else switchCount:= 0;
-if ((Me^.State and gstAttacked) = 0) or isInMultiShoot or bonuses.activity then
+if ((Me^.State and gstAttacked) = 0) or isInMultiShoot or bonuses.activity or ((Me^.AIHints and aihAmmosChanged) <> 0) then
if Targets.Count > 0 then
begin
// iterate over current team hedgehogs
@@ -478,7 +479,7 @@
FillBonuses(false);
// Hog has no idea what to do. Use tardis or skip
- if not bonuses.activity then
+ if (not bonuses.activity) and ((Me^.AIHints and aihAmmosChanged) = 0) then
if (((GameFlags and gfInfAttack) <> 0) or (CurrentHedgehog^.MultiShootAttacks = 0)) and (HHHasAmmo(Me^.Hedgehog^, amTardis) > 0) and (CanUseTardis(Me^.Hedgehog^.Gear)) and (random(4) < 3) then
// Tardis brings hog to a random place. Perfect for clueless AI
begin
@@ -488,6 +489,7 @@
end
else
AddAction(BestActions, aia_Skip, 0, 250, 0, 0);
+ Me^.AIHints := ME^.AIHints and (not aihAmmosChanged);
end;
end else SDL_Delay(100)
--- a/hedgewars/uAmmos.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uAmmos.pas Tue Oct 15 13:42:50 2019 -0400
@@ -265,7 +265,9 @@
begin
PackAmmo(Ammo, Ammoz[AmmoType].Slot);
CurAmmoType:= amNothing
- end
+ end;
+if Hedgehog.BotLevel <> 0 then
+ Hedgehog.Gear^.AIHints := Hedgehog.Gear^.AIHints or aihAmmosChanged;
end;
procedure PackAmmo(Ammo: PHHAmmo; Slot: LongInt);
--- a/hedgewars/uConsts.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uConsts.pas Tue Oct 15 13:42:50 2019 -0400
@@ -321,6 +321,7 @@
// AI hints to be set for any gear
aihUsualProcessing = $00000000; // treat gear as usual
aihDoesntMatter = $00000001; // ignore gear in attack calculations and don't intentionally attack it
+ aihAmmosChanged = $00000002; // set when ammos were changed within this turn but not processed yet
// ammo properties
ammoprop_Timerable = $00000001; // can set timer
--- a/hedgewars/uGears.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uGears.pas Tue Oct 15 13:42:50 2019 -0400
@@ -43,6 +43,7 @@
procedure EndTurnCleanup;
procedure DrawGears;
procedure DrawGearsGui;
+procedure DrawFinger;
procedure FreeGearsList;
procedure AddMiscGears;
procedure AssignHHCoords;
@@ -728,6 +729,19 @@
end;
end;
+procedure DrawFinger;
+var Gear: PGear;
+ x, y: LongInt;
+begin
+if ((CurrentHedgehog <> nil) and (CurrentHedgehog^.Gear <> nil)) then
+ begin
+ Gear:= CurrentHedgehog^.Gear;
+ x:= hwRound(Gear^.X) + WorldDx;
+ y:= hwRound(Gear^.Y) + WorldDy;
+ RenderFinger(Gear, x, y);
+ end;
+end;
+
procedure FreeGearsList;
var t, tt: PGear;
begin
--- a/hedgewars/uGearsHandlersMess.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uGearsHandlersMess.pas Tue Oct 15 13:42:50 2019 -0400
@@ -2348,12 +2348,22 @@
doStepFallingGear(Gear);
AllInactive := false;
+ if (Gear^.SoundChannel <> -1) and ((Gear^.State and gstDrowning) <> 0) then
+ begin
+ StopSoundChan(Gear^.SoundChannel);
+ Gear^.SoundChannel:= -1;
+ end
+ else if Gear^.SoundChannel = -1 then
+ Gear^.SoundChannel := LoopSound(sndDynamiteFuse);
+ if (Gear^.State and gstDrowning) <> 0 then
+ exit;
if Gear^.Timer mod 166 = 0 then
inc(Gear^.Tag);
if Gear^.Timer = 1000 then // might need better timing
makeHogsWorry(Gear^.X, Gear^.Y, 75, Gear^.Kind);
if Gear^.Timer = 0 then
begin
+ StopSoundChan(Gear^.SoundChannel);
doMakeExplosion(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Boom, Gear^.Hedgehog, EXPLAutoSound);
DeleteGear(Gear);
exit
--- a/hedgewars/uGearsRender.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uGearsRender.pas Tue Oct 15 13:42:50 2019 -0400
@@ -38,6 +38,7 @@
procedure RenderGear(Gear: PGear; x, y: LongInt);
procedure RenderGearTimer(Gear: PGear; x, y: LongInt);
procedure RenderGearHealth(Gear: PGear; x, y: LongInt);
+procedure RenderFinger(Gear: PGear; ox, oy: LongInt);
procedure RenderHHGuiExtras(Gear: PGear; ox, oy: LongInt);
procedure RenderAirMineGuiExtras(Gear: PGear; ox, oy: LongInt);
procedure DrawHHOrder();
@@ -238,15 +239,12 @@
end;
-// Render some informational GUI next to hedgehog, like fuel and alternate weapon
-procedure RenderHHGuiExtras(Gear: PGear; ox, oy: LongInt);
+procedure RenderFinger(Gear: PGear; ox, oy: LongInt);
var HH: PHedgehog;
- sx, sy, tx, ty, t, hogLR: LongInt;
+ tx, ty, t: LongInt;
dAngle: real;
begin
HH:= Gear^.Hedgehog;
- sx:= ox + 1; // this offset is very common
- sy:= oy - 3;
if HH^.Unplaced then
exit;
if (Gear^.State and gstHHDeath) <> 0 then
@@ -256,7 +254,7 @@
if (CinematicScript) then
exit;
- // render finger (pointing arrow)
+ // render finger (arrow pointing to hog)
if bShowFinger and ((Gear^.State and gstHHDriven) <> 0) then
begin
ty := oy - 32;
@@ -294,6 +292,25 @@
DrawSpriteRotatedF(sprFinger, tx, ty, RealTicks div 32 mod 16, 1, dAngle);
untint;
end;
+end;
+
+
+// Render some informational GUI next to hedgehog, like fuel and alternate weapon
+procedure RenderHHGuiExtras(Gear: PGear; ox, oy: LongInt);
+var HH: PHedgehog;
+ sx, sy, hogLR: LongInt;
+begin
+ HH:= Gear^.Hedgehog;
+ sx:= ox + 1; // this offset is very common
+ sy:= oy - 3;
+ if HH^.Unplaced then
+ exit;
+ if (Gear^.State and gstHHDeath) <> 0 then
+ exit;
+ if (Gear^.State and gstHHGone) <> 0 then
+ exit;
+ if (CinematicScript) then
+ exit;
// render crosshair
if (CrosshairGear <> nil) and (Gear = CrosshairGear) then
@@ -1493,7 +1510,10 @@
DrawSpriteRotatedF(sprExplosivesRoll, x, y + 4, 1, 0, Gear^.DirAngle)
end;
gtDynamite: begin
- DrawSprite(sprDynamite, x - 16, y - 25, Gear^.Tag and 1, Gear^.Tag shr 1);
+ if ((Gear^.State and gstDrowning) = 0) then
+ DrawSprite(sprDynamite, x - 16, y - 25, Gear^.Tag and 1, Gear^.Tag shr 1)
+ else
+ DrawSprite(sprDynamiteDefused, x - 16, y - 25, Gear^.Tag and 1, Gear^.Tag shr 1);
if (random(3) = 0) and ((Gear^.State and gstDrowning) = 0) then
begin
vg:= AddVisualGear(hwRound(Gear^.X)+12-(Gear^.Tag shr 1), hwRound(Gear^.Y)-16, vgtStraightShot);
--- a/hedgewars/uSound.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uSound.pas Tue Oct 15 13:42:50 2019 -0400
@@ -332,7 +332,8 @@
(FileName: 'Hmm.ogg'; Path: ptVoices; AltPath: ptNone),// sndHmm
(FileName: 'Kiss.ogg'; Path: ptSounds; AltPath: ptNone),// sndKiss
(FileName: 'Flyaway.ogg'; Path: ptVoices; AltPath: ptNone),// sndFlyAway
- (FileName: 'planewater.ogg'; Path: ptSounds; AltPath: ptNone) // sndPlaneWater
+ (FileName: 'planewater.ogg'; Path: ptSounds; AltPath: ptNone),// sndPlaneWater
+ (FileName: 'dynamitefuse.ogg'; Path: ptSounds; AltPath: ptNone) // sndDynamiteFuse
);
@@ -345,11 +346,11 @@
{ Adjust for language suffix: Voicepacks can have an optional language suffix.
It's an underscore followed by an ISO 639-1 or ISO 639-2 language code.
- The suffix “_qau” is special, it will enable automatic language selection
+ The suffix "_qau" is special, it will enable automatic language selection
of this voicepack. For example, if team has set Default_qau as voicepack,
and the player language is Russian, the actual voicepack will be Default_ru,
provided it can be found on the disk.
- “qau” is a valid ISO 639-2 language code reserved for local use. }
+ "qau" is a valid ISO 639-2 language code reserved for local use. }
tmp:= Copy(name, Length(name) - 3, 4);
if (tmp = '_qau') then
name:= Copy(name, 1, Length(name) - 4);
--- a/hedgewars/uStats.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uStats.pas Tue Oct 15 13:42:50 2019 -0400
@@ -71,9 +71,9 @@
HitTargets : LongWord = 0; // Target (gtTarget) hits in turn
AmmoUsedCount : Longword = 0; // Number of times an ammo has been used this turn
AmmoDamagingUsed : boolean = false; // true if damaging ammo was used in turn
- FirstBlood : boolean = false; // true if the “First blood” taunt has been used in this game
- StepFirstBlood : boolean = false; // true if the “First blood” taunt is to be used this turn
- LeaveMeAlone : boolean = false; // true if the “Leave me alone” taunt is to be used this turn
+ FirstBlood : boolean = false; // true if the "First blood" taunt has been used in this game
+ StepFirstBlood : boolean = false; // true if the "First blood" taunt is to be used this turn
+ LeaveMeAlone : boolean = false; // true if the "Leave me alone" taunt is to be used this turn
SkippedTurns: LongWord = 0; // number of skipped turns in game
isTurnSkipped: boolean = false; // true if this turn was skipped
vpHurtSameClan: PVoicepack = nil; // voicepack of current clan (used for taunts)
--- a/hedgewars/uTeams.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uTeams.pas Tue Oct 15 13:42:50 2019 -0400
@@ -393,6 +393,8 @@
CurWeapon:= GetCurAmmoEntry(CurrentHedgehog^);
if CurWeapon^.Count = 0 then
CurrentHedgehog^.CurAmmoType:= amNothing;
+if CurrentHedgehog^.BotLevel <> 0 then
+ CurrentHedgehog^.Gear^.AIHints:= (CurrentHedgehog^.Gear^.AIHints and (not aihAmmosChanged));
with CurrentHedgehog^ do
begin
--- a/hedgewars/uTypes.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uTypes.pas Tue Oct 15 13:42:50 2019 -0400
@@ -94,8 +94,8 @@
sprFlakeL, sprSDFlakeL, sprCloudL, sprSDCloudL, sprCreeper, sprHandCreeper, sprMinigun,
sprSliderInverted, sprFingerBack, sprFingerBackInv, sprTargetPBack, sprTargetPBackInv,
sprHealthHud, sprHealthPoisonHud, sprVampHud, sprKarmaHud, sprMedicHud, sprMedicPoisonHud,
- sprHaloHud, sprInvulnHUD, sprAmPiano, sprHandLandGun, sprFirePunch, sprThroughWrap
- );
+ sprHaloHud, sprInvulnHUD, sprAmPiano, sprHandLandGun, sprFirePunch, sprThroughWrap,
+ sprDynamiteDefused);
// Gears that interact with other Gears and/or Land
// first row of gears (<gtExplosives) should be avoided when searching a spawn place
@@ -158,7 +158,7 @@
sndLandGun, sndCaseImpact, sndExtraDamage, sndFirePunchHit, sndGrenade, sndThisOneIsMine,
sndWhatThe, sndSoLong, sndOhDear, sndGonnaGetYou, sndDrat, sndBugger, sndAmazing,
sndBrilliant, sndExcellent, sndFire, sndWatchThis, sndRunAway, sndRevenge, sndCutItOut,
- sndLeaveMeAlone, sndOuch, sndHmm, sndKiss, sndFlyAway, sndPlaneWater);
+ sndLeaveMeAlone, sndOuch, sndHmm, sndKiss, sndFlyAway, sndPlaneWater, sndDynamiteFuse);
// Available ammo types to be used by hedgehogs
TAmmoType = (amNothing, amGrenade, amClusterBomb, amBazooka, amBee, amShotgun, amPickHammer, // 6
@@ -515,7 +515,8 @@
sidWinner2, sidWinner3, sidWinner4, sidWinner5, sidWinner6,
sidWinner7, sidWinnerAll, sidTeamGone, sidTeamBack, sidAutoSkip,
sidFPS, sidLuaParsingOff, sidLuaParsingOn, sidLuaParsingDenied,
- sidAmmoCount, sidChat, sidChatTeam, sidChatHog, sidUnknownGearValue);
+ sidAmmoCount, sidChat, sidChatTeam, sidChatHog, sidUnknownGearValue,
+ sidVideoRecLuaFail);
TCmdHelpStrId = (
sidCmdHeaderBasic, sidCmdTogglechat, sidCmdTeam, sidCmdMe,
--- a/hedgewars/uVariables.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uVariables.pas Tue Oct 15 13:42:50 2019 -0400
@@ -852,7 +852,10 @@
(FileName: 'amShoryuken'; Path: ptHedgehog; AltPath: ptNone; Texture: nil; Surface: nil;
Width: 32; Height: 32; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpMedium; getDimensions: false; getImageDimensions: true),// sprFirePunch
(FileName: 'throughWrap'; Path: ptGraphics; AltPath: ptNone; Texture: nil; Surface: nil;
- Width: 16; Height: 13; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpMedium; getDimensions: false; getImageDimensions: true) // sprTroughWrap
+ Width: 16; Height: 13; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpMedium; getDimensions: false; getImageDimensions: true),// sprTroughWrap
+ (FileName: 'dynamiteDefused'; Path: ptGraphics; AltPath: ptNone; Texture: nil; Surface: nil;
+ Width: 32; Height: 32; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpMedium; getDimensions: false; getImageDimensions: true) // sprDynamiteDefused
+
);
--- a/hedgewars/uVideoRec.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uVideoRec.pas Tue Oct 15 13:42:50 2019 -0400
@@ -48,7 +48,7 @@
procedure freeModule;
implementation
-uses uVariables, GLunit, SDLh, SysUtils, uUtils, uSound, uIO, uMisc, uTypes, uDebug;
+uses uVariables, GLunit, SDLh, SysUtils, uUtils, uSound, uChat, uIO, uMisc, uTypes, uDebug;
type TAddFileLogRaw = procedure (s: pchar); cdecl;
const AvwrapperLibName = {$IFDEF WIN32_VCPKG}'avwrapper'{$ELSE}'libavwrapper'{$ENDIF};
@@ -289,8 +289,8 @@
// Videos don't work if /lua command was used, so we forbid them
if luaCmdUsed then
begin
- // TODO: Show message to player
PlaySound(sndDenied);
+ AddChatString(#0 + shortstring(trmsg[sidVideoRecLuaFail]));
AddFileLog('Pre-recording prevented; /lua command was used before');
exit;
end;
--- a/hedgewars/uWorld.pas Sat Sep 28 16:39:02 2019 +0300
+++ b/hedgewars/uWorld.pas Tue Oct 15 13:42:50 2019 -0400
@@ -1299,6 +1299,10 @@
DrawGearsGui();
+// Finger (arrow pointing to hedgehog).
+// NOT wrapped like the other stuff because it might be confusing.
+DrawFinger();
+
// everything after this ChangeDepth will be drawn outside the screen
// note: negative parallax gears should last very little for a smooth stereo effect
ChangeDepth(RM, cStereo_Outside);
@@ -1546,9 +1550,13 @@
end;
end
// in gfInvulnerable mode ...
- else if (CurrentHedgehog^.Effects[heResurrectable] <> 0) then
- // show halo for resurrectable hog
- DrawSprite(sprHaloHud, (cScreenWidth div 2 - CurrentHedgehog^.HealthTagTex^.w - t - 2), i, 0);
+ else
+ begin
+ DrawSprite(sprInvulnHud, cScreenWidth div 2 - 28, i, 0);
+ if (CurrentHedgehog^.Effects[heResurrectable] <> 0) then
+ // show halo for resurrectable hog
+ DrawSprite(sprHaloHud, cScreenWidth div 2 - 30, i - SpritesData[sprHaloHud].Height + 1, 0);
+ end;
end
else
cDemoClockFPSOffsetY:= 0;
--- a/rust/hedgewars-server/src/core/indexslab.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/core/indexslab.rs Tue Oct 15 13:42:50 2019 -0400
@@ -19,6 +19,14 @@
}
}
+ pub fn get(&self, index: usize) -> Option<&T> {
+ self.data[index].as_ref()
+ }
+
+ pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
+ self.data[index].as_mut()
+ }
+
pub fn insert(&mut self, index: usize, value: T) {
if index >= self.data.len() {
self.data.reserve(index - self.data.len() + 1);
@@ -41,7 +49,7 @@
}
}
- pub fn iter(&self) -> impl Iterator<Item = (usize, &T)> {
+ pub fn iter(&self) -> impl Iterator<Item = (usize, &T)> + Clone {
self.data
.iter()
.enumerate()
--- a/rust/hedgewars-server/src/core/server.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/core/server.rs Tue Oct 15 13:42:50 2019 -0400
@@ -2,23 +2,46 @@
client::HwClient,
indexslab::IndexSlab,
room::HwRoom,
- types::{ClientId, RoomId},
+ types::{ClientId, RoomId, ServerVar},
};
use crate::{protocol::messages::HwProtocolMessage::Greeting, utils};
+use crate::core::server::JoinRoomError::WrongProtocol;
use bitflags::*;
use log::*;
use slab;
-use std::{borrow::BorrowMut, iter, num::NonZeroU16};
+use std::{borrow::BorrowMut, collections::HashSet, iter, num::NonZeroU16};
type Slab<T> = slab::Slab<T>;
+#[derive(Debug)]
+pub enum CreateRoomError {
+ InvalidName,
+ AlreadyExists,
+}
+
+#[derive(Debug)]
+pub enum JoinRoomError {
+ DoesntExist,
+ WrongProtocol,
+ Full,
+ Restricted,
+}
+
+#[derive(Debug)]
+pub struct UninitializedError();
+#[derive(Debug)]
+pub struct AccessError();
+
pub struct HwAnteClient {
pub nick: Option<String>,
pub protocol_number: Option<NonZeroU16>,
pub server_salt: String,
pub is_checker: bool,
pub is_local_admin: bool,
+ pub is_registered: bool,
+ pub is_admin: bool,
+ pub is_contributor: bool,
}
pub struct HwAnteroom {
@@ -38,12 +61,15 @@
server_salt: salt,
is_checker: false,
is_local_admin,
+ is_registered: false,
+ is_admin: false,
+ is_contributor: false,
};
self.clients.insert(client_id, client);
}
pub fn remove_client(&mut self, client_id: ClientId) -> Option<HwAnteClient> {
- let mut client = self.clients.remove(client_id);
+ let client = self.clients.remove(client_id);
client
}
}
@@ -91,6 +117,34 @@
}
}
+ #[inline]
+ pub fn client(&self, client_id: ClientId) -> &HwClient {
+ &self.clients[client_id]
+ }
+
+ #[inline]
+ pub fn client_mut(&mut self, client_id: ClientId) -> &mut HwClient {
+ &mut self.clients[client_id]
+ }
+
+ #[inline]
+ pub fn room(&self, room_id: RoomId) -> &HwRoom {
+ &self.rooms[room_id]
+ }
+
+ #[inline]
+ pub fn room_mut(&mut self, room_id: RoomId) -> &mut HwRoom {
+ &mut self.rooms[room_id]
+ }
+
+ #[inline]
+ pub fn is_admin(&self, client_id: ClientId) -> bool {
+ self.clients
+ .get(client_id)
+ .map(|c| c.is_admin())
+ .unwrap_or(false)
+ }
+
pub fn add_client(&mut self, client_id: ClientId, data: HwAnteClient) {
if let (Some(protocol), Some(nick)) = (data.protocol_number, data.nick) {
let mut client = HwClient::new(client_id, protocol.get(), nick);
@@ -98,6 +152,13 @@
#[cfg(not(feature = "official-server"))]
client.set_is_admin(data.is_local_admin);
+ #[cfg(feature = "official-server")]
+ {
+ client.set_is_registered(info.is_registered);
+ client.set_is_admin(info.is_admin);
+ client.set_is_contributor(info.is_contributor);
+ }
+
self.clients.insert(client_id, client);
}
}
@@ -106,8 +167,8 @@
self.clients.remove(client_id);
}
- pub fn get_greetings(&self, client_id: ClientId) -> &str {
- if self.clients[client_id].protocol_number < self.latest_protocol {
+ pub fn get_greetings(&self, client: &HwClient) -> &str {
+ if client.protocol_number < self.latest_protocol {
&self.greetings.for_old_protocols
} else {
&self.greetings.for_latest_protocol
@@ -115,29 +176,123 @@
}
#[inline]
+ pub fn get_client_nick(&self, client_id: ClientId) -> &str {
+ &self.clients[client_id].nick
+ }
+
+ #[inline]
pub fn create_room(
&mut self,
creator_id: ClientId,
name: String,
password: Option<String>,
- ) -> RoomId {
- create_room(
- &mut self.clients[creator_id],
- &mut self.rooms,
- name,
- password,
- )
+ ) -> Result<(&HwClient, &HwRoom), CreateRoomError> {
+ use CreateRoomError::*;
+ if utils::is_name_illegal(&name) {
+ Err(InvalidName)
+ } else if self.has_room(&name) {
+ Err(AlreadyExists)
+ } else {
+ Ok(create_room(
+ &mut self.clients[creator_id],
+ &mut self.rooms,
+ name,
+ password,
+ ))
+ }
+ }
+
+ pub fn join_room(
+ &mut self,
+ client_id: ClientId,
+ room_id: RoomId,
+ ) -> Result<(&HwClient, &HwRoom, impl Iterator<Item = &HwClient> + Clone), JoinRoomError> {
+ use JoinRoomError::*;
+ let room = &mut self.rooms[room_id];
+ let client = &mut self.clients[client_id];
+
+ if client.protocol_number != room.protocol_number {
+ Err(WrongProtocol)
+ } else if room.is_join_restricted() {
+ Err(Restricted)
+ } else if room.players_number == u8::max_value() {
+ Err(Full)
+ } else {
+ move_to_room(client, room);
+ let room_id = room.id;
+ Ok((
+ &self.clients[client_id],
+ &self.rooms[room_id],
+ self.clients.iter().map(|(_, c)| c),
+ ))
+ }
}
#[inline]
- pub fn move_to_room(&mut self, client_id: ClientId, room_id: RoomId) {
- move_to_room(&mut self.clients[client_id], &mut self.rooms[room_id])
+ pub fn join_room_by_name(
+ &mut self,
+ client_id: ClientId,
+ room_name: &str,
+ ) -> Result<(&HwClient, &HwRoom, impl Iterator<Item = &HwClient> + Clone), JoinRoomError> {
+ use JoinRoomError::*;
+ let room = self.rooms.iter().find(|(_, r)| r.name == room_name);
+ if let Some((_, room)) = room {
+ let room_id = room.id;
+ self.join_room(client_id, room_id)
+ } else {
+ Err(DoesntExist)
+ }
+ }
+
+ #[inline]
+ pub fn set_var(&mut self, client_id: ClientId, var: ServerVar) -> Result<(), AccessError> {
+ if self.clients[client_id].is_admin() {
+ match var {
+ ServerVar::MOTDNew(msg) => self.greetings.for_latest_protocol = msg,
+ ServerVar::MOTDOld(msg) => self.greetings.for_old_protocols = msg,
+ ServerVar::LatestProto(n) => self.latest_protocol = n,
+ }
+ Ok(())
+ } else {
+ Err(AccessError())
+ }
}
+ #[inline]
+ pub fn get_vars(&self, client_id: ClientId) -> Result<[ServerVar; 3], AccessError> {
+ if self.clients[client_id].is_admin() {
+ Ok([
+ ServerVar::MOTDNew(self.greetings.for_latest_protocol.clone()),
+ ServerVar::MOTDOld(self.greetings.for_old_protocols.clone()),
+ ServerVar::LatestProto(self.latest_protocol),
+ ])
+ } else {
+ Err(AccessError())
+ }
+ }
+
+ pub fn get_used_protocols(&self, client_id: ClientId) -> Result<Vec<u16>, AccessError> {
+ if self.clients[client_id].is_admin() {
+ let mut protocols: HashSet<_> = self
+ .clients
+ .iter()
+ .map(|(_, c)| c.protocol_number)
+ .chain(self.rooms.iter().map(|(_, r)| r.protocol_number))
+ .collect();
+ let mut protocols: Vec<_> = protocols.drain().collect();
+ protocols.sort();
+ Ok(protocols)
+ } else {
+ Err(AccessError())
+ }
+ }
+
+ #[inline]
pub fn has_room(&self, name: &str) -> bool {
self.find_room(name).is_some()
}
+ #[inline]
pub fn find_room(&self, name: &str) -> Option<&HwRoom> {
self.rooms
.iter()
@@ -234,12 +389,12 @@
entry.insert(room)
}
-fn create_room(
- client: &mut HwClient,
- rooms: &mut Slab<HwRoom>,
+fn create_room<'a, 'b>(
+ client: &'a mut HwClient,
+ rooms: &'b mut Slab<HwRoom>,
name: String,
password: Option<String>,
-) -> RoomId {
+) -> (&'a HwClient, &'b HwRoom) {
let room = allocate_room(rooms);
room.master_id = Some(client.id);
@@ -255,7 +410,7 @@
client.set_is_ready(true);
client.set_is_joined_mid_game(false);
- room.id
+ (client, room)
}
fn move_to_room(client: &mut HwClient, room: &mut HwRoom) {
--- a/rust/hedgewars-server/src/handlers.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/handlers.rs Tue Oct 15 13:42:50 2019 -0400
@@ -9,6 +9,7 @@
use self::{
actions::{Destination, DestinationGroup, PendingMessage},
inanteroom::LoginResult,
+ strings::*,
};
use crate::{
core::{
@@ -32,6 +33,7 @@
mod inanteroom;
mod inlobby;
mod inroom;
+mod strings;
#[derive(PartialEq, Debug)]
pub struct Sha1Digest([u8; 20]);
@@ -160,6 +162,16 @@
}
#[inline]
+ pub fn warn(&mut self, message: &str) {
+ self.add(Warning(message.to_string()).send_self());
+ }
+
+ #[inline]
+ pub fn error(&mut self, message: &str) {
+ self.add(Error(message.to_string()).send_self());
+ }
+
+ #[inline]
pub fn request_io(&mut self, task: IoTask) {
self.io_tasks.push(task)
}
@@ -240,7 +252,7 @@
LoginResult::Complete => {
if let Some(client) = server.anteroom.remove_client(client_id) {
server.add_client(client_id, client);
- common::join_lobby(server, response);
+ common::get_lobby_join_data(server, response);
}
}
LoginResult::Exit => {
@@ -262,7 +274,7 @@
let master_sign = if client.is_master() { "+" } else { "" };
let room_info = match client.room_id {
Some(room_id) => {
- let room = &server.rooms[room_id];
+ let room = server.room(room_id);
let status = match room.game_info {
Some(_) if client.teams_in_game == 0 => "(spectating)",
Some(_) => "(playing)",
@@ -284,37 +296,36 @@
];
response.add(Info(info).send_self())
} else {
- response
- .add(server_chat("Player is not online.".to_string()).send_self())
+ response.add(server_chat(USER_OFFLINE.to_string()).send_self())
}
}
HwProtocolMessage::ToggleServerRegisteredOnly => {
- if !server.clients[client_id].is_admin() {
- response.add(Warning("Access denied.".to_string()).send_self());
+ if !server.is_admin(client_id) {
+ response.warn(ACCESS_DENIED);
} else {
- server.set_is_registered_only(server.is_registered_only());
+ server.set_is_registered_only(!server.is_registered_only());
let msg = if server.is_registered_only() {
- "This server no longer allows unregistered players to join."
+ REGISTERED_ONLY_ENABLED
} else {
- "This server now allows unregistered players to join."
+ REGISTERED_ONLY_DISABLED
};
response.add(server_chat(msg.to_string()).send_all());
}
}
HwProtocolMessage::Global(msg) => {
- if !server.clients[client_id].is_admin() {
- response.add(Warning("Access denied.".to_string()).send_self());
+ if !server.is_admin(client_id) {
+ response.warn(ACCESS_DENIED);
} else {
response.add(global_chat(msg).send_all())
}
}
HwProtocolMessage::SuperPower => {
- if !server.clients[client_id].is_admin() {
- response.add(Warning("Access denied.".to_string()).send_self());
+ let client = server.client_mut(client_id);
+ if !client.is_admin() {
+ response.warn(ACCESS_DENIED);
} else {
- server.clients[client_id].set_has_super_power(true);
- response
- .add(server_chat("Super power activated.".to_string()).send_self())
+ client.set_has_super_power(true);
+ response.add(server_chat(SUPER_POWER.to_string()).send_self())
}
}
HwProtocolMessage::Watch(id) => {
@@ -325,13 +336,10 @@
#[cfg(not(feature = "official-server"))]
{
- response.add(
- Warning("This server does not support replays!".to_string())
- .send_self(),
- );
+ response.warn(REPLAY_NOT_SUPPORTED);
}
}
- _ => match server.clients[client_id].room_id {
+ _ => match server.client(client_id).room_id {
None => inlobby::handle(server, client_id, response, message),
Some(room_id) => {
inroom::handle(server, client_id, response, room_id, message)
@@ -374,38 +382,35 @@
match io_result {
IoResult::AccountRegistered(is_registered) => {
if !is_registered && server.is_registered_only() {
- response.add(
- Bye("This server only allows registered users to join.".to_string())
- .send_self(),
- );
+ response.add(Bye(REGISTRATION_REQUIRED.to_string()).send_self());
response.remove_client(client_id);
} else if is_registered {
let salt = server.anteroom.clients[client_id].server_salt.clone();
response.add(AskPassword(salt).send_self());
} else if let Some(client) = server.anteroom.remove_client(client_id) {
server.add_client(client_id, client);
- common::join_lobby(server, response);
+ common::get_lobby_join_data(server, response);
}
}
IoResult::Account(Some(info)) => {
response.add(ServerAuth(format!("{:x}", info.server_hash)).send_self());
- if let Some(client) = server.anteroom.remove_client(client_id) {
+ if let Some(mut client) = server.anteroom.remove_client(client_id) {
+ client.is_registered = info.is_registered;
+ client.is_admin = info.is_admin;
+ client.is_contributor = info.is_contributor;
server.add_client(client_id, client);
- let client = &mut server.clients[client_id];
- client.set_is_registered(info.is_registered);
- client.set_is_admin(info.is_admin);
- client.set_is_contributor(info.is_contributor);
- common::join_lobby(server, response);
+ common::get_lobby_join_data(server, response);
}
}
IoResult::Account(None) => {
- response.add(Error("Authentication failed.".to_string()).send_self());
+ response.error(AUTHENTICATION_FAILED);
response.remove_client(client_id);
}
IoResult::Replay(Some(replay)) => {
- let protocol = server.clients[client_id].protocol_number;
+ let client = server.client(client_id);
+ let protocol = client.protocol_number;
let start_msg = if protocol < 58 {
- RoomJoined(vec![server.clients[client_id].nick.clone()])
+ RoomJoined(vec![client.nick.clone()])
} else {
ReplayStart
};
@@ -421,32 +426,27 @@
}
}
IoResult::Replay(None) => {
- response.add(Warning("Could't load the replay".to_string()).send_self())
+ response.warn(REPLAY_LOAD_FAILED);
}
IoResult::SaveRoom(_, true) => {
- response.add(server_chat("Room configs saved successfully.".to_string()).send_self());
+ response.add(server_chat(ROOM_CONFIG_SAVED.to_string()).send_self());
}
IoResult::SaveRoom(_, false) => {
- response.add(Warning("Unable to save the room configs.".to_string()).send_self());
+ response.warn(ROOM_CONFIG_SAVE_FAILED);
}
IoResult::LoadRoom(room_id, Some(contents)) => {
if let Some(ref mut room) = server.rooms.get_mut(room_id) {
match room.set_saves(&contents) {
- Ok(_) => response.add(
- server_chat("Room configs loaded successfully.".to_string()).send_self(),
- ),
+ Ok(_) => response.add(server_chat(ROOM_CONFIG_LOADED.to_string()).send_self()),
Err(e) => {
warn!("Error while deserializing the room configs: {}", e);
- response.add(
- Warning("Unable to deserialize the room configs.".to_string())
- .send_self(),
- );
+ response.warn(ROOM_CONFIG_DESERIALIZE_FAILED);
}
}
}
}
IoResult::LoadRoom(_, None) => {
- response.add(Warning("Unable to load the room configs.".to_string()).send_self());
+ response.warn(ROOM_CONFIG_LOAD_FAILED);
}
}
}
--- a/rust/hedgewars-server/src/handlers/common.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/handlers/common.rs Tue Oct 15 13:42:50 2019 -0400
@@ -2,7 +2,7 @@
core::{
client::HwClient,
room::HwRoom,
- server::HwServer,
+ server::{HwServer, JoinRoomError},
types::{ClientId, GameCfg, RoomId, TeamInfo, Vote, VoteType},
},
protocol::messages::{
@@ -35,10 +35,10 @@
}
}
-pub fn join_lobby(server: &mut HwServer, response: &mut Response) {
+pub fn get_lobby_join_data(server: &HwServer, response: &mut Response) {
let client_id = response.client_id();
- let client = &server.clients[client_id];
+ let client = server.client(client_id);
let nick = vec![client.nick.clone()];
let mut flags = vec![];
if client.is_registered() {
@@ -69,7 +69,7 @@
),
];
- let server_msg = ServerMessage(server.get_greetings(client_id).to_string());
+ let server_msg = ServerMessage(server.get_greetings(client).to_string());
let rooms_msg = Rooms(
server
@@ -227,32 +227,43 @@
);
}
-pub fn enter_room(
- server: &mut HwServer,
- client_id: ClientId,
- room_id: RoomId,
+pub fn get_room_join_data<'a, I: Iterator<Item = &'a HwClient> + Clone>(
+ client: &HwClient,
+ room: &HwRoom,
+ room_clients: I,
response: &mut Response,
) {
- let nick = server.clients[client_id].nick.clone();
- server.move_to_room(client_id, room_id);
+ #[inline]
+ fn collect_nicks<'a, I, F>(clients: I, f: F) -> Vec<String>
+ where
+ I: Iterator<Item = &'a HwClient>,
+ F: Fn(&&'a HwClient) -> bool,
+ {
+ clients.filter(f).map(|c| &c.nick).cloned().collect()
+ }
- response.add(RoomJoined(vec![nick.clone()]).send_all().in_room(room_id));
+ let nick = client.nick.clone();
+ response.add(RoomJoined(vec![nick.clone()]).send_all().in_room(room.id));
response.add(ClientFlags(add_flags(&[Flags::InRoom]), vec![nick]).send_all());
- let nicks = server.collect_nicks(|(_, c)| c.room_id == Some(room_id));
+ let nicks = collect_nicks(room_clients.clone(), |c| c.room_id == Some(room.id));
response.add(RoomJoined(nicks).send_self());
- get_room_teams(server, room_id, client_id, response);
-
- let room = &server.rooms[room_id];
- get_room_config(room, client_id, response);
+ get_room_teams(room, client.id, response);
+ get_room_config(room, client.id, response);
let mut flag_selectors = [
(
Flags::RoomMaster,
- server.collect_nicks(|(_, c)| c.is_master()),
+ collect_nicks(room_clients.clone(), |c| c.is_master()),
),
- (Flags::Ready, server.collect_nicks(|(_, c)| c.is_ready())),
- (Flags::InGame, server.collect_nicks(|(_, c)| c.is_in_game())),
+ (
+ Flags::Ready,
+ collect_nicks(room_clients.clone(), |c| c.is_ready()),
+ ),
+ (
+ Flags::InGame,
+ collect_nicks(room_clients.clone(), |c| c.is_in_game()),
+ ),
];
for (flag, nicks) in &mut flag_selectors {
@@ -270,6 +281,16 @@
}
}
+pub fn get_room_join_error(error: JoinRoomError, response: &mut Response) {
+ use super::strings::*;
+ match error {
+ JoinRoomError::DoesntExist => response.warn(NO_ROOM),
+ JoinRoomError::WrongProtocol => response.warn(WRONG_PROTOCOL),
+ JoinRoomError::Full => response.warn(ROOM_FULL),
+ JoinRoomError::Restricted => response.warn(ROOM_JOIN_RESTRICTED),
+ }
+}
+
pub fn exit_room(server: &mut HwServer, client_id: ClientId, response: &mut Response, msg: &str) {
let client = &mut server.clients[client_id];
@@ -317,8 +338,8 @@
server.remove_client(client_id);
- response.add(LobbyLeft(nick, msg.to_string()).send_all());
- response.add(Bye("User quit: ".to_string() + &msg).send_self());
+ response.add(LobbyLeft(nick, msg.clone()).send_all());
+ response.add(Bye(msg).send_self());
response.remove_client(client_id);
}
@@ -354,13 +375,7 @@
}
}
-pub fn get_room_teams(
- server: &HwServer,
- room_id: RoomId,
- to_client: ClientId,
- response: &mut Response,
-) {
- let room = &server.rooms[room_id];
+pub fn get_room_teams(room: &HwRoom, to_client: ClientId, response: &mut Response) {
let current_teams = match room.game_info {
Some(ref info) => &info.teams_at_start,
None => &room.teams,
--- a/rust/hedgewars-server/src/handlers/inlobby.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/handlers/inlobby.rs Tue Oct 15 13:42:50 2019 -0400
@@ -1,10 +1,10 @@
use mio;
-use super::common::rnd_reply;
+use super::{common::rnd_reply, strings::*};
use crate::{
core::{
client::HwClient,
- server::HwServer,
+ server::{AccessError, CreateRoomError, HwServer, JoinRoomError},
types::{ClientId, ServerVar},
},
protocol::messages::{
@@ -23,41 +23,34 @@
message: HwProtocolMessage,
) {
use crate::protocol::messages::HwProtocolMessage::*;
+
match message {
- CreateRoom(name, password) => {
- if is_name_illegal(&name) {
- response.add(Warning("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()).send_self());
- } else if server.has_room(&name) {
- response.add(
- Warning("A room with the same name already exists.".to_string()).send_self(),
- );
- } else {
- let flags_msg = ClientFlags(
- add_flags(&[Flags::RoomMaster, Flags::Ready]),
- vec![server.clients[client_id].nick.clone()],
- );
-
- let room_id = server.create_room(client_id, name, password);
- let room = &server.rooms[room_id];
- let client = &server.clients[client_id];
-
+ CreateRoom(name, password) => match server.create_room(client_id, name, password) {
+ Err(CreateRoomError::InvalidName) => response.warn(ILLEGAL_ROOM_NAME),
+ Err(CreateRoomError::AlreadyExists) => response.warn(ROOM_EXISTS),
+ Ok((client, room)) => {
response.add(
RoomAdd(room.info(Some(&client)))
.send_all()
.with_protocol(room.protocol_number),
);
response.add(RoomJoined(vec![client.nick.clone()]).send_self());
- response.add(flags_msg.send_self());
-
+ response.add(
+ ClientFlags(
+ add_flags(&[Flags::RoomMaster, Flags::Ready]),
+ vec![client.nick.clone()],
+ )
+ .send_self(),
+ );
response.add(
ClientFlags(add_flags(&[Flags::InRoom]), vec![client.nick.clone()]).send_self(),
);
- };
- }
+ }
+ },
Chat(msg) => {
response.add(
ChatMsg {
- nick: server.clients[client_id].nick.clone(),
+ nick: server.get_client_nick(client_id).to_string(),
msg,
}
.send_all()
@@ -65,99 +58,62 @@
.but_self(),
);
}
- JoinRoom(name, _password) => {
- let room = server.rooms.iter().find(|(_, r)| r.name == name);
- let room_id = room.map(|(_, r)| r.id);
-
- let client = &mut server.clients[client_id];
-
- if let Some((_, room)) = room {
- if client.protocol_number != room.protocol_number {
- response.add(
- Warning("Room version incompatible to your Hedgewars version!".to_string())
- .send_self(),
- );
- } else if room.is_join_restricted() {
- response.add(
- Warning(
- "Access denied. This room currently doesn't allow joining.".to_string(),
- )
- .send_self(),
- );
- } else if room.players_number == u8::max_value() {
- response.add(Warning("This room is already full".to_string()).send_self());
- } else if let Some(room_id) = room_id {
- super::common::enter_room(server, client_id, room_id, response);
+ JoinRoom(name, _password) => match server.join_room_by_name(client_id, &name) {
+ Err(error) => super::common::get_room_join_error(error, response),
+ Ok((client, room, room_clients)) => {
+ super::common::get_room_join_data(client, room, room_clients, response)
+ }
+ },
+ Follow(nick) => {
+ if let Some(client) = server.find_client(&nick) {
+ if let Some(room_id) = client.room_id {
+ match server.join_room(client_id, room_id) {
+ Err(error) => super::common::get_room_join_error(error, response),
+ Ok((client, room, room_clients)) => {
+ super::common::get_room_join_data(client, room, room_clients, response)
+ }
+ }
+ } else {
+ response.warn(NO_ROOM);
}
} else {
- response.add(Warning("No such room.".to_string()).send_self());
+ response.warn(NO_USER);
}
}
- Follow(nick) => {
- if let Some(HwClient {
- room_id: Some(room_id),
- ..
- }) = server.find_client(&nick)
- {
- let room = &server.rooms[*room_id];
- response.add(Joining(room.name.clone()).send_self());
- super::common::enter_room(server, client_id, *room_id, response);
+ SetServerVar(var) => match server.set_var(client_id, var) {
+ Err(AccessError()) => response.warn(ACCESS_DENIED),
+ Ok(()) => response.add(server_chat(VARIABLE_UPDATED.to_string()).send_self()),
+ },
+ GetServerVar => match server.get_vars(client_id) {
+ Err(AccessError()) => response.warn(ACCESS_DENIED),
+ Ok(vars) => {
+ response.add(
+ ServerVars(vars.iter().flat_map(|v| v.to_protocol()).collect()).send_self(),
+ );
}
- }
- SetServerVar(var) => {
- if !server.clients[client_id].is_admin() {
- response.add(Warning("Access denied.".to_string()).send_self());
- } else {
- match var {
- ServerVar::MOTDNew(msg) => server.greetings.for_latest_protocol = msg,
- ServerVar::MOTDOld(msg) => server.greetings.for_old_protocols = msg,
- ServerVar::LatestProto(n) => server.latest_protocol = n,
- }
- }
- }
- GetServerVar => {
- if !server.clients[client_id].is_admin() {
- response.add(Warning("Access denied.".to_string()).send_self());
- } else {
- let vars: Vec<_> = [
- ServerVar::MOTDNew(server.greetings.for_latest_protocol.clone()),
- ServerVar::MOTDOld(server.greetings.for_old_protocols.clone()),
- ServerVar::LatestProto(server.latest_protocol),
- ]
- .iter()
- .flat_map(|v| v.to_protocol())
- .collect();
- response.add(ServerVars(vars).send_self());
- }
- }
+ },
Rnd(v) => {
response.add(rnd_reply(&v).send_self());
}
- Stats => {
- let mut protocols: HashSet<_> = server
- .clients
- .iter()
- .map(|(_, c)| c.protocol_number)
- .chain(server.rooms.iter().map(|(_, r)| r.protocol_number))
- .collect();
- let mut protocols: Vec<_> = protocols.drain().collect();
- protocols.sort();
-
- let mut html = Vec::with_capacity(protocols.len() + 2);
+ Stats => match server.get_used_protocols(client_id) {
+ Err(AccessError()) => response.warn(ACCESS_DENIED),
+ Ok(protocols) => {
+ let mut html = Vec::with_capacity(protocols.len() + 2);
- html.push("<table>".to_string());
- for protocol in protocols {
- html.push(format!(
- "<tr><td>{}</td><td>{}</td><td>{}</td></tr>",
- super::utils::protocol_version_string(protocol),
- server.protocol_clients(protocol).count(),
- server.protocol_rooms(protocol).count()
- ));
+ html.push("<table>".to_string());
+ for protocol in protocols {
+ html.push(format!(
+ "<tr><td>{}</td><td>{}</td><td>{}</td></tr>",
+ super::utils::protocol_version_string(protocol),
+ server.protocol_clients(protocol).count(),
+ server.protocol_rooms(protocol).count()
+ ));
+ }
+ html.push("</table>".to_string());
+
+ response.add(Warning(html.join("")).send_self());
}
- html.push("</table>".to_string());
-
- response.add(Warning(html.join("")).send_self());
- }
+ },
List => warn!("Deprecated LIST message received"),
_ => warn!("Incorrect command in lobby state"),
}
--- a/rust/hedgewars-server/src/handlers/inroom.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/handlers/inroom.rs Tue Oct 15 13:42:50 2019 -0400
@@ -52,7 +52,7 @@
[size, typ, body..MAX] => {
VALID_MESSAGES.contains(typ)
&& match body {
- [1...MAX_HEDGEHOGS_PER_TEAM, team, ..] if *typ == b'h' => {
+ [1..=MAX_HEDGEHOGS_PER_TEAM, team, ..] if *typ == b'h' => {
team_indices.contains(team)
}
_ => *typ != b'h',
@@ -272,12 +272,8 @@
Some((_, name)) => {
client.teams_in_game -= 1;
client.clan = room.find_team_color(client.id);
- super::common::remove_teams(
- room,
- vec![name.to_string()],
- client.is_in_game(),
- response,
- );
+ let names = vec![name.to_string()];
+ super::common::remove_teams(room, names, client.is_in_game(), response);
match room.game_info {
Some(ref info) if info.teams_in_game == 0 => {
@@ -438,7 +434,7 @@
}
VoteType::NewSeed => None,
VoteType::HedgehogsPerTeam(number) => match number {
- 1...MAX_HEDGEHOGS_PER_TEAM => None,
+ 1..=MAX_HEDGEHOGS_PER_TEAM => None,
_ => Some("/callvote hedgehogs: Specify number from 1 to 8.".to_string()),
},
};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hedgewars-server/src/handlers/strings.rs Tue Oct 15 13:42:50 2019 -0400
@@ -0,0 +1,23 @@
+pub const ACCESS_DENIED: &str = "Access denied.";
+pub const AUTHENTICATION_FAILED: &str = "Authentication failed.";
+pub const ILLEGAL_ROOM_NAME: &str = "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: $()*+?[]^{|}";
+pub const NO_ROOM: &str = "No such room.";
+pub const NO_USER: &str = "No such user.";
+pub const REPLAY_LOAD_FAILED: &str = "Could't load the replay";
+pub const REPLAY_NOT_SUPPORTED: &str = "This server does not support replays!";
+pub const REGISTRATION_REQUIRED: &str = "This server only allows registered users to join.";
+pub const REGISTERED_ONLY_ENABLED: &str =
+ "This server no longer allows unregistered players to join.";
+pub const REGISTERED_ONLY_DISABLED: &str = "This server now allows unregistered players to join.";
+pub const ROOM_CONFIG_SAVE_FAILED: &str = "Unable to save the room configs.";
+pub const ROOM_CONFIG_LOAD_FAILED: &str = "Unable to load the room configs.";
+pub const ROOM_CONFIG_DESERIALIZE_FAILED: &str = "Unable to deserialize the room configs.";
+pub const ROOM_CONFIG_LOADED: &str = "Room configs loaded successfully.";
+pub const ROOM_CONFIG_SAVED: &str = "Room configs saved successfully.";
+pub const ROOM_EXISTS: &str = "A room with the same name already exists.";
+pub const ROOM_FULL: &str = "This room is already full.";
+pub const ROOM_JOIN_RESTRICTED: &str = "Access denied. This room currently doesn't allow joining.";
+pub const SUPER_POWER: &str = "Super power activated.";
+pub const USER_OFFLINE: &str = "Player is not online.";
+pub const VARIABLE_UPDATED: &str = "Server variable has been updated.";
+pub const WRONG_PROTOCOL: &str = "Room version incompatible to your Hedgewars version!";
--- a/rust/hedgewars-server/src/protocol.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/protocol.rs Tue Oct 15 13:42:50 2019 -0400
@@ -24,7 +24,8 @@
fn recover(&mut self) -> bool {
self.is_recovering = match parser::malformed_message(&self.buf[..]) {
Ok((tail, ())) => {
- self.buf.consume(self.buf.len() - tail.len());
+ let length = tail.len();
+ self.buf.consume(self.buf.len() - length);
false
}
_ => {
@@ -50,7 +51,8 @@
match parser::message(&self.buf[..]) {
Ok((tail, message)) => {
messages.push(message);
- self.buf.consume(self.buf.len() - tail.len());
+ let length = tail.len();
+ self.buf.consume(self.buf.len() - length);
}
Err(nom::Err::Incomplete(_)) => break,
Err(nom::Err::Failure(e)) | Err(nom::Err::Error(e)) => {
--- a/rust/hedgewars-server/src/utils.rs Sat Sep 28 16:39:02 2019 +0300
+++ b/rust/hedgewars-server/src/utils.rs Tue Oct 15 13:42:50 2019 -0400
@@ -69,6 +69,8 @@
56 => "0.9.25-dev",
57 => "0.9.25",
58 => "1.0.0-dev",
+ 59 => "1.0.0",
+ 60 => "1.0.1-dev",
_ => "Unknown",
}
}
Binary file share/hedgewars/Data/Graphics/AmmoMenu/TurnsLeft.png has changed
Binary file share/hedgewars/Data/Graphics/dynamiteDefused.png has changed
--- a/share/hedgewars/Data/Locale/de.txt Sat Sep 28 16:39:02 2019 +0300
+++ b/share/hedgewars/Data/Locale/de.txt Tue Oct 15 13:42:50 2019 -0400
@@ -111,6 +111,7 @@
01:46=[Klan] %1: %2
01:47=[%1]: %2
01:48=?
+01:49=Videos können nicht aufgenommen werden, nachdem der /lua-Befehl benutzt wurde.
; Event messages
; Hog (%1) died
--- a/share/hedgewars/Data/Locale/en.txt Sat Sep 28 16:39:02 2019 +0300
+++ b/share/hedgewars/Data/Locale/en.txt Tue Oct 15 13:42:50 2019 -0400
@@ -121,6 +121,7 @@
01:47=[%1]: %2
; Symbol for unknown mine timer
01:48=?
+01:49=Videos can't be recorded after the /lua command was used.
; Event messages
; Normal hog (%1) died (0 health)
--- a/share/hedgewars/Data/Locale/hedgewars_it.ts Sat Sep 28 16:39:02 2019 +0300
+++ b/share/hedgewars/Data/Locale/hedgewars_it.ts Tue Oct 15 13:42:50 2019 -0400
@@ -32,51 +32,51 @@
<message>
<source>Dependency versions:</source>
<extracomment>For the version numbers of Hedgewars' software dependencies</extracomment>
- <translation type="unfinished"></translation>
+ <translation>Versioni di riferimento:</translation>
</message>
<message>
<source><a href="https://gcc.gnu.org">GCC</a>: %1</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://gcc.gnu.org">GCC</a>: %1</translation>
</message>
<message>
<source><a href="https://www.libsdl.org/">SDL2</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://www.libsdl.org/">SDL2</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://www.libsdl.org/">SDL2_mixer</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://www.libsdl.org/">SDL2_mixer</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://www.libsdl.org/">SDL2_net</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://www.libsdl.org/">SDL2_net</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://www.libsdl.org/">SDL2_image</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://www.libsdl.org/">SDL2_image</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://www.libsdl.org/">SDL2_ttf</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://www.libsdl.org/">SDL2_ttf</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://www.qt.io/developers/">Qt</a>: %1</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://www.qt.io/developers/">Qt</a>: %1</translation>
</message>
<message>
<source><a href="https://libav.org">libavcodec</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://libav.org">libavcodec</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://libav.org">libavformat</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://libav.org">libavformat</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://libav.org">libavutil</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://libav.org">libavutil</a>: %1.%2.%3</translation>
</message>
<message>
<source><a href="https://icculus.org/physfs/">PhysFS</a>: %1.%2.%3</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://icculus.org/physfs/">PhysFS</a>: %1.%2.%3</translation>
</message>
<message>
<source>Credits</source>
@@ -88,22 +88,22 @@
</message>
<message>
<source>%1 (alias %2)</source>
- <translation type="unfinished"></translation>
+ <translation>%1 (alias %2)</translation>
</message>
<message>
<source>%1 &lt;%2&gt;</source>
<extracomment>Part of credits. %1: Contributor name. %2: E-mail address</extracomment>
- <translation type="unfinished"></translation>
+ <translation>%1 &lt;%2&gt;</translation>
</message>
<message>
<source>%1: %2</source>
<extracomment>Part of credits. %1: Description of contribution. %2: Contributor name</extracomment>
- <translation type="unfinished"></translation>
+ <translation>%1: %2</translation>
</message>
<message>
<source>%1: %2 &lt;%3&gt;</source>
<extracomment>Part of credits. %1: Description of contribution. %2: Contributor name. %3: E-mail address</extracomment>
- <translation type="unfinished"></translation>
+ <translation>%1: %2 &lt;%3&gt;</translation>
</message>
<message>
<source>Extended Credits</source>
@@ -115,7 +115,7 @@
</message>
<message>
<source><a href="https://visualstudio.microsoft.com">VC++</a>: %1</source>
- <translation type="unfinished"></translation>
+ <translation><a href="https://visualstudio.microsoft.com">VC++</a>: %1</translation>
</message>
<message>
<source>Unknown Compiler: %1</source>
@@ -1449,15 +1449,15 @@
<message numerus="yes">
<source>The best killer is <b>%1</b> with <b>%2</b> kills in a turn.</source>
<translation>
- <numerusform><p>Il miglior killer è <b>%1</b> con <b>%2</b> uccisione in un turno.</p></numerusform>
- <numerusform><p>Il miglior killer è <b>%1</b> con <b>%2</b> uccisioni in un turno.</p></numerusform>
+ <numerusform>Il miglior killer è <b>%1</b> con <b>%2</b> uccisione in un turno.</numerusform>
+ <numerusform>Il miglior killer è <b>%1</b> con <b>%2</b> uccisioni in un turno.</numerusform>
</translation>
</message>
<message numerus="yes">
<source>A total of <b>%1</b> hedgehog(s) were killed during this round.</source>
<translation>
- <numerusform><p>Durante questo round è stato ucciso <b>%1</b> riccio in totale.</p></numerusform>
- <numerusform><p>Durante questo round sono stati uccisi <b>%1</b> ricci in totale.</p></numerusform>
+ <numerusform>Durante questo round è stato ucciso <b>%1</b> riccio in totale.</numerusform>
+ <numerusform>Durante questo round sono stati uccisi <b>%1</b> ricci in totale.</numerusform>
</translation>
</message>
<message numerus="yes">
@@ -3846,7 +3846,7 @@
</message>
<message>
<source>switch backwards</source>
- <translation type="unfinished"></translation>
+ <translation>Girati indietro</translation>
</message>
<message>
<source>change bounciness</source>
@@ -4027,7 +4027,7 @@
</message>
<message>
<source>Heads-up display:</source>
- <translation type="unfinished"></translation>
+ <translation>HUD:</translation>
</message>
<message>
<source>Talk to your clan or all participants:</source>
@@ -4516,7 +4516,7 @@
<name>credits</name>
<message>
<source>Programming</source>
- <translation type="unfinished"></translation>
+ <translation>Programmatori</translation>
</message>
<message>
<source>Game engine</source>
@@ -4536,7 +4536,7 @@
</message>
<message>
<source>Campaign support</source>
- <translation type="unfinished"></translation>
+ <translation>Curatori della campagna</translation>
</message>
<message>
<source>Theme customization improvements</source>
@@ -4560,11 +4560,11 @@
</message>
<message>
<source>Core map generators</source>
- <translation type="unfinished"></translation>
+ <translation>Curatori del generatore centrale delle mappe</translation>
</message>
<message>
<source>Perlin maps and other improvements</source>
- <translation type="unfinished"></translation>
+ <translation>Curatori delle mappe perlin e altri miglioramenti</translation>
</message>
<message>
<source>Maze maps</source>
@@ -4576,7 +4576,7 @@
</message>
<message>
<source>Most core weapons</source>
- <translation type="unfinished"></translation>
+ <translation>Curatori della maggior parte delle armi principali</translation>
</message>
<message>
<source>Air mine, rubber, others</source>
@@ -4672,7 +4672,7 @@
</message>
<message>
<source>Android netplay, portability abstraction</source>
- <translation type="unfinished"></translation>
+ <translation>Curatori del gioco su network Android, Portabilità</translation>
</message>
<message>
<source>WebGL port</source>
@@ -5203,7 +5203,7 @@
</message>
<message>
<source>Empty config entry.</source>
- <translation type="unfinished"></translation>
+ <translation>Ingresso di configurazione vuoto.</translation>
</message>
<message>
<source>Access denied.</source>
@@ -5247,7 +5247,7 @@
</message>
<message>
<source>Illegal room name! The 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: $()*+?[]^{|}</source>
- <translation type="unfinished"></translation>
+ <translation>Nome stanza invalido! Il nome della stanza deve contenere tra gli 1 e i 40 caratteri,non deve iniziare con uno spazio e non può contenere spazi prolungati,inoltre non può contenere questi caratteri: $()*+?[]^{|}</translation>
</message>
<message>
<source>A room with the same name already exists.</source>
@@ -5275,7 +5275,7 @@
</message>
<message>
<source>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: $()*+?[]^{|}</source>
- <translation type="unfinished"></translation>
+ <translation>Nome stanza invalido! Il nome della stanza deve contenere tra gli 1 e i 40 caratteri,non deve iniziare con uno spazio e non può contenere spazi prolungati,inoltre non può contenere questi caratteri: $()*+?[]^{|}</translation>
</message>
<message>
<source>No such room.</source>
@@ -5299,11 +5299,11 @@
</message>
<message>
<source>Nickname already provided.</source>
- <translation type="unfinished"></translation>
+ <translation>Nickname già fornito.</translation>
</message>
<message>
<source>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: $()*+?[]^{|}</source>
- <translation type="unfinished"></translation>
+ <translation>Nickname invalido! Il nickname deve contenere tra gli 1 e i 40 caratteri,non deve iniziare con uno spazio e non può contenere spazi prolungati,inoltre non può contenere questi caratteri: $()*+?[]^{|}</translation>
</message>
<message>
<source>Protocol already known.</source>
@@ -5351,27 +5351,27 @@
</message>
<message>
<source>/rnd: Flip a virtual coin and reply with 'heads' or 'tails'</source>
- <translation type="unfinished"></translation>
+ <translation>/rnd: Tira una moneta virtuale e rispone con testa o croce</translation>
</message>
<message>
<source>/rnd [A] [B] [C] [...]: Reply with a random word from the given list</source>
- <translation type="unfinished"></translation>
+ <translation>/rnd [A] [B] [C] [...]: Risponde con una delle parole nella lista in modo casuale</translation>
</message>
<message>
<source>/watch <id>: Watch a demo stored on the server with the given ID</source>
- <translation type="unfinished"></translation>
+ <translation>/watch <id>: Guarda un filmato presente nel server con questo ID</translation>
</message>
<message>
<source>/help: Show chat command help</source>
- <translation type="unfinished"></translation>
+ <translation>/help: Mostra un aiuto per i comandi di chat</translation>
</message>
<message>
<source>/callvote [arguments]: Start a vote</source>
- <translation type="unfinished"></translation>
+ <translation>/callvote [arguments]: Inizia una votazione</translation>
</message>
<message>
<source>/vote <yes/no>: Vote 'yes' or 'no' for active vote</source>
- <translation type="unfinished"></translation>
+ <translation>/vote <yes/no>: Vota 'si' o 'no' per la votazione corrente</translation>
</message>
<message>
<source>/delegate <player>: Surrender room control to player</source>
@@ -5379,7 +5379,7 @@
</message>
<message>
<source>/maxteams <N>: Limit maximum number of teams to N</source>
- <translation type="unfinished"></translation>
+ <translation>/maxteams <N>: Imposta il limite massimo di squadre a N</translation>
</message>
<message>
<source>/global <message>: Send global chat message which can be seen by everyone on the server</source>
@@ -5387,39 +5387,39 @@
</message>
<message>
<source>/registered_only: Toggle 'registered only' state. If enabled, only registered players can join server</source>
- <translation type="unfinished"></translation>
+ <translation>/registered_only: Abilita l'opzione solo iscritti. Se attivato, solo i giocatori registrati potranno partcipare al server</translation>
</message>
<message>
<source>/super_power: Activate your super power. With it you can enter any room and are protected from kicking. Expires when you leave server</source>
- <translation type="unfinished"></translation>
+ <translation>/super_power: Attiva il tuo superpoter. Con esso puoi entrare in ogni stanza e sei protetto dall'essere cacciato. L'effetto termina quando ti disconnetti</translation>
</message>
<message>
<source>/stats: Query server stats</source>
- <translation type="unfinished"></translation>
+ <translation>/stats: Richiedi le statistiche del server</translation>
</message>
<message>
<source>/force <yes/no>: Force vote result for active vote</source>
- <translation type="unfinished"></translation>
+ <translation>/force <yes/no>: Forza il risultato della votazione in corso</translation>
</message>
<message>
<source>/fix: Force this room to stay open when it is empty</source>
- <translation type="unfinished"></translation>
+ <translation>/fix: Imponi a questa stanza di rimanere aperta anche quando è vuota</translation>
</message>
<message>
<source>/unfix: Undo the /fix command</source>
- <translation type="unfinished"></translation>
+ <translation>/unfix: Annulla il comando /fix</translation>
</message>
<message>
<source>List of lobby chat commands:</source>
- <translation type="unfinished"></translation>
+ <translation>Lista dei comandi di chat per la lobby:</translation>
</message>
<message>
<source>List of room chat commands:</source>
- <translation type="unfinished"></translation>
+ <translation>Lista dei comandi di chat per la stanza:</translation>
</message>
<message>
<source>Commands for server admins only:</source>
- <translation type="unfinished"></translation>
+ <translation>Comandi per gli admins:</translation>
</message>
<message>
<source>room</source>
@@ -5443,11 +5443,11 @@
</message>
<message>
<source>/force: Please use 'yes' or 'no'.</source>
- <translation type="unfinished"></translation>
+ <translation>/force: Perfavore usa 'yes' o 'no'.</translation>
</message>
<message>
<source>/vote: Please use 'yes' or 'no'.</source>
- <translation type="unfinished"></translation>
+ <translation>/vote: Perfavore usa 'yes' or 'no'.</translation>
</message>
<message>
<source>Kicked</source>
@@ -5471,23 +5471,23 @@
</message>
<message>
<source>/greeting [message]: Set or clear greeting message to be shown to players who join the room</source>
- <translation type="unfinished"></translation>
+ <translation>/greeting [message]: Imposta il messaggio di benvenuto che verrà mostrato ai giocatori che entrano in stanza</translation>
</message>
<message>
<source>/save <config ID> <config name>: Add current room configuration as votable choice for /callvote map</source>
- <translation type="unfinished"></translation>
+ <translation>/save <config ID> <config name>: Aggiunge a questa stanza l'opzione votabile di configurazione con /callvote map</translation>
</message>
<message>
<source>/delete <config ID>: Delete a votable room configuration</source>
- <translation type="unfinished"></translation>
+ <translation>/delete <config ID>: Elimina l'opzione votabile di configurazione</translation>
</message>
<message>
<source>/saveroom <file name>: Save all votable room configurations (and the greeting) of this room into a file</source>
- <translation type="unfinished"></translation>
+ <translation>/saveroom <file name>: Salva tutte le opzioni di configurazione della mappa e i messaggi di benvenuto per questa stanza in un file</translation>
</message>
<message>
<source>/loadroom <file name>: Load votable room configurations (and greeting) from a file</source>
- <translation type="unfinished"></translation>
+ <translation>/loadroom <file name>: Carica l'opzione di configurazione della mappa e il messaggio di benvenuto dal file</translation>
</message>
<message>
<source>Super power activated.</source>
@@ -5495,7 +5495,7 @@
</message>
<message>
<source>Unknown command or invalid parameters. Say '/help' in chat for a list of commands.</source>
- <translation type="unfinished"></translation>
+ <translation>Comando sconosciuto o parametri illegali. Scrivi '/help' nella chat per la lista dei comandi di chat.</translation>
</message>
<message>
<source>You can't kick yourself!</source>
@@ -5531,11 +5531,11 @@
</message>
<message>
<source>/callvote kick: This is only allowed in rooms without a room master.</source>
- <translation type="unfinished"></translation>
+ <translation>/callvote kick: Questa opzione è valida solo nelle stanze senza un capostanza.</translation>
</message>
<message>
<source>/callvote map: No maps available.</source>
- <translation type="unfinished"></translation>
+ <translation>/callvote map: Nessuna mappa disponibile.</translation>
</message>
<message>
<source>You're the new room master!</source>
@@ -5543,7 +5543,7 @@
</message>
<message>
<source>/quit: Quit the server</source>
- <translation type="unfinished"></translation>
+ <translation>/quit: Esci dal server</translation>
</message>
<message>
<source>This command is only available in the lobby.</source>
@@ -5563,15 +5563,15 @@
</message>
<message>
<source>Available callvote commands: hedgehogs <number>, pause, newseed, map <name>, kick <player></source>
- <translation type="unfinished"></translation>
+ <translation>Comandi di votazione disponibili:: hedgehogs <number>, pause, newseed, map <name>, kick <player></translation>
</message>
<message>
<source>Please confirm server restart with '/restart_server yes'.</source>
- <translation type="unfinished"></translation>
+ <translation>Perfavore conferma il riavvio del server con '/restart_server yes'.</translation>
</message>
<message>
<source>Warning! Room name change flood protection activated</source>
- <translation type="unfinished"></translation>
+ <translation>Attenzione! Attivata la protezione per l'inondazione di messagi</translation>
</message>
</context>
</TS>
--- a/share/hedgewars/Data/Scripts/Multiplayer/HedgeEditor.lua Sat Sep 28 16:39:02 2019 +0300
+++ b/share/hedgewars/Data/Scripts/Multiplayer/HedgeEditor.lua Tue Oct 15 13:42:50 2019 -0400
@@ -3342,7 +3342,7 @@
reducedSpriteIDArrayFrames = {
1, 8, 4, 1, 1,
- AmmoTypeMax, AmmoTypeMax, 3, 4, 8, 1,
+ AmmoTypeMax, AmmoTypeMax, 3, 4, 9, 1,
1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
}
--- a/share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua Sat Sep 28 16:39:02 2019 +0300
+++ b/share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua Tue Oct 15 13:42:50 2019 -0400
@@ -59,6 +59,7 @@
local teamsDead = {}
local teamsDeleted = {}
local hogLimitHit = false
+local teamLimitHit = false
local cnthhs
local circles = {}
@@ -157,7 +158,7 @@
end
function limitHogsClan(gear)
- hogLimitHit = true
+ teamLimitHit = true
SetEffect(gear, heResurrectable, 0)
setGearValue(gear, "excess", true)
DeleteGear(gear)
@@ -197,8 +198,10 @@
cnthhs = 0
runOnHogsInTeam(limitHogsTeam, GetTeamName(i))
end
+ if teamLimitHit then
+ WriteLnToChat(loc("Only one team per clan allowed! Excess teams will be removed."))
+ end
if hogLimitHit then
- -- TODO: Update warning message to include excess teams as well
WriteLnToChat(loc("Only one hog per team allowed! Excess hogs will be removed."))
end
trackTeams()
Binary file share/hedgewars/Data/Sounds/dynamitefuse.ogg has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/replay2hwd.hs Tue Oct 15 13:42:50 2019 -0400
@@ -0,0 +1,237 @@
+{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}
+
+import qualified Data.ByteString.Char8 as B
+import Control.Exception as E
+import System.Environment
+import Control.Monad
+import qualified Data.Map as Map
+import Data.Word
+import Data.Int
+import qualified Codec.Binary.Base64 as Base64
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString as BW
+import qualified Codec.Compression.Zlib.Internal as ZI
+import qualified Codec.Compression.Zlib as Z
+import qualified Data.List as L
+import qualified Data.Set as Set
+import Data.Binary
+import Data.Binary.Put
+import Data.Bits
+import Control.Arrow
+import Data.Maybe
+import qualified Data.Either as Ei
+
+
+decompressWithoutExceptions :: BL.ByteString -> BL.ByteString
+decompressWithoutExceptions = BL.fromChunks . ZI.foldDecompressStreamWithInput chunk end err decomp
+ where
+ decomp = ZI.decompressST ZI.zlibFormat ZI.defaultDecompressParams
+ chunk = (:)
+ end _ = []
+ err = const $ [BW.empty]
+
+data HedgehogInfo =
+ HedgehogInfo B.ByteString B.ByteString
+ deriving (Show, Read)
+
+data TeamInfo =
+ TeamInfo
+ {
+ teamowner :: !B.ByteString,
+ teamname :: !B.ByteString,
+ teamcolor :: !B.ByteString,
+ teamgrave :: !B.ByteString,
+ teamfort :: !B.ByteString,
+ teamvoicepack :: !B.ByteString,
+ teamflag :: !B.ByteString,
+ isOwnerRegistered :: !Bool,
+ difficulty :: !Int,
+ hhnum :: !Int,
+ hedgehogs :: ![HedgehogInfo]
+ }
+ deriving (Show, Read)
+
+readInt_ :: (Num a) => B.ByteString -> a
+readInt_ str =
+ case B.readInt str of
+ Just (i, t) | B.null t -> fromIntegral i
+ _ -> 0
+
+toEngineMsg :: B.ByteString -> B.ByteString
+toEngineMsg msg = fromIntegral (BW.length msg) `BW.cons` msg
+
+em :: B.ByteString -> B.ByteString
+em = toEngineMsg
+
+eml :: [B.ByteString] -> B.ByteString
+eml = em . B.concat
+
+showB :: (Show a) => a -> B.ByteString
+showB = B.pack . show
+
+replayToDemo :: [TeamInfo]
+ -> Map.Map B.ByteString B.ByteString
+ -> Map.Map B.ByteString [B.ByteString]
+ -> [B.ByteString]
+ -> B.ByteString
+replayToDemo ti mParams prms msgs = if not sane then "" else (B.concat $ concat [
+ [em "TD"]
+ , maybeScript
+ , maybeMap
+ , [eml ["etheme ", head $ prms Map.! "THEME"]]
+ , [eml ["eseed ", mParams Map.! "SEED"]]
+ , [eml ["e$gmflags ", showB gameFlags]]
+ , schemeFlags
+ , schemeAdditional
+ , [eml ["e$template_filter ", mParams Map.! "TEMPLATE"]]
+ , [eml ["e$feature_size ", mParams Map.! "FEATURE_SIZE"]]
+ , [eml ["e$mapgen ", mapgen]]
+ , mapgenSpecific
+ , concatMap teamSetup ti
+ , map (Ei.fromRight "" . Base64.decode) $ reverse msgs
+ , [em "!"]
+ ])
+ where
+ keys1, keys2 :: Set.Set B.ByteString
+ keys1 = Set.fromList ["FEATURE_SIZE", "MAP", "MAPGEN", "MAZE_SIZE", "SEED", "TEMPLATE"]
+ keys2 = Set.fromList ["AMMO", "SCHEME", "SCRIPT", "THEME"]
+ sane = Set.null (keys1 Set.\\ Map.keysSet mParams)
+ && Set.null (keys2 Set.\\ Map.keysSet prms)
+ && (not . null . drop 41 $ scheme)
+ && (not . null . tail $ prms Map.! "AMMO")
+ && ((B.length . head . tail $ prms Map.! "AMMO") > 200)
+ mapGenTypes = ["+rnd+", "+maze+", "+drawn+", "+perlin+"]
+ scriptName = head . fromMaybe ["Normal"] $ Map.lookup "SCRIPT" prms
+ maybeScript = let s = scriptName in if s == "Normal" then [] else [eml ["escript Scripts/Multiplayer/", spaces2Underlining s, ".lua"]]
+ maybeMap = let m = mParams Map.! "MAP" in if m `elem` mapGenTypes then [] else [eml ["emap ", m]]
+ scheme = tail $ prms Map.! "SCHEME"
+ mapgen = mParams Map.! "MAPGEN"
+ mazeSizeMsg = eml ["e$maze_size ", mParams Map.! "MAZE_SIZE"]
+ mapgenSpecific = case mapgen of
+ "1" -> [mazeSizeMsg]
+ "2" -> [mazeSizeMsg]
+ "3" -> let d = head . fromMaybe [""] $ Map.lookup "DRAWNMAP" prms in if BW.length d <= 4 then [] else drawnMapData d
+ _ -> []
+ gameFlags :: Word32
+ gameFlags = foldl (\r (b, f) -> if b == "false" then r else r .|. f) 0 $ zip scheme gameFlagConsts
+ schemeFlags = map (\(v, (n, m)) -> eml [n, " ", showB $ (readInt_ v) * m])
+ $ filter (\(_, (n, _)) -> not $ B.null n)
+ $ zip (drop (length gameFlagConsts) scheme) schemeParams
+ schemeAdditional = let scriptParam = B.tail $ scheme !! 42 in [eml ["e$scriptparam ", scriptParam] | not $ B.null scriptParam]
+ ammoStr :: B.ByteString
+ ammoStr = head . tail $ prms Map.! "AMMO"
+ ammo = let l = B.length ammoStr `div` 4; ((a, b), (c, d)) = (B.splitAt l . fst &&& B.splitAt l . snd) . B.splitAt (l * 2) $ ammoStr in
+ (map (\(x, y) -> eml [x, " ", y]) $ zip ["eammloadt", "eammprob", "eammdelay", "eammreinf"] [a, b, c, d])
+ ++ [em "eammstore" | scheme !! 14 == "true" || scheme !! 20 == "false"]
+ initHealth = scheme !! 27
+ teamSetup :: TeamInfo -> [B.ByteString]
+ teamSetup t = (++) ammo $
+ eml ["eaddteam <hash> ", showB $ (1 + (readInt_ $ teamcolor t) :: Int) * 2113696, " ", teamname t]
+ : em "erdriven"
+ : eml ["efort ", teamfort t]
+ : take (2 * hhnum t) (
+ concatMap (\(HedgehogInfo hname hhat) -> [
+ eml ["eaddhh ", showB $ difficulty t, " ", initHealth, " ", hname]
+ , eml ["ehat ", hhat]
+ ])
+ $ hedgehogs t
+ )
+ infRopes = ammoStr `B.index` 7 == '9'
+ vamp = gameFlags .&. 0x00000200 /= 0
+ infattacks = gameFlags .&. 0x00100000 /= 0
+ spaces2Underlining = B.map (\c -> if c == ' ' then '_' else c)
+
+drawnMapData :: B.ByteString -> [B.ByteString]
+drawnMapData =
+ L.map (\m -> eml ["edraw ", BW.pack m])
+ . L.unfoldr by200
+ . BL.unpack
+ . unpackDrawnMap
+ where
+ by200 :: [a] -> Maybe ([a], [a])
+ by200 [] = Nothing
+ by200 m = Just $ L.splitAt 200 m
+
+unpackDrawnMap :: B.ByteString -> BL.ByteString
+unpackDrawnMap = either
+ (const BL.empty)
+ (decompressWithoutExceptions . BL.pack . drop 4 . BW.unpack)
+ . Base64.decode
+
+compressWithLength :: BL.ByteString -> BL.ByteString
+compressWithLength b = BL.drop 8 . encode . runPut $ do
+ put $ ((fromIntegral $ BL.length b)::Word32)
+ mapM_ putWord8 $ BW.unpack $ BL.toStrict $ Z.compress b
+
+packDrawnMap :: BL.ByteString -> B.ByteString
+packDrawnMap =
+ Base64.encode
+ . BL.toStrict
+ . compressWithLength
+
+prependGhostPoints :: [(Int16, Int16)] -> B.ByteString -> B.ByteString
+prependGhostPoints pts dm = packDrawnMap $ (runPut $ forM_ pts $ \(x, y) -> put x >> put y >> putWord8 99) `BL.append` unpackDrawnMap dm
+
+schemeParams :: [(B.ByteString, Int)]
+schemeParams = [
+ ("e$damagepct", 1)
+ , ("e$turntime", 1000)
+ , ("", 0)
+ , ("e$sd_turns", 1)
+ , ("e$casefreq", 1)
+ , ("e$minestime", 1000)
+ , ("e$minesnum", 1)
+ , ("e$minedudpct", 1)
+ , ("e$explosives", 1)
+ , ("e$airmines", 1)
+ , ("e$healthprob", 1)
+ , ("e$hcaseamount", 1)
+ , ("e$waterrise", 1)
+ , ("e$healthdec", 1)
+ , ("e$ropepct", 1)
+ , ("e$getawaytime", 1)
+ , ("e$worldedge", 1)
+ ]
+
+
+gameFlagConsts :: [Word32]
+gameFlagConsts = [
+ 0x00001000
+ , 0x00000010
+ , 0x00000004
+ , 0x00000008
+ , 0x00000020
+ , 0x00000040
+ , 0x00000080
+ , 0x00000100
+ , 0x00000200
+ , 0x00000400
+ , 0x00000800
+ , 0x00002000
+ , 0x00004000
+ , 0x00008000
+ , 0x00010000
+ , 0x00020000
+ , 0x00040000
+ , 0x00080000
+ , 0x00100000
+ , 0x00200000
+ , 0x00400000
+ , 0x00800000
+ , 0x01000000
+ , 0x02000000
+ , 0x04000000
+ ]
+
+loadReplay :: String -> IO (Maybe ([TeamInfo], [(B.ByteString, B.ByteString)], [(B.ByteString, [B.ByteString])], [B.ByteString]))
+loadReplay fileName = E.handle (\(e :: SomeException) -> return Nothing) $ do
+ liftM (Just . read) $ readFile fileName
+
+convert :: String -> IO ()
+convert fileName = do
+ Just (t, c1, c2, m) <- loadReplay fileName
+ B.writeFile (fileName ++ ".hwd") $ replayToDemo t (Map.fromList c1) (Map.fromList c2) m
+
+main = do
+ args <- getArgs
+ when (length args == 1) $ (convert (head args))