# HG changeset patch # User nemo # Date 1711305237 14400 # Node ID 4c523ed1d35ceab824f4064f4e1b06dabe84c61c # Parent 00bf5adba84998df4b6e2385474c54bf45f94d4e# Parent 64740eec84adaa1e632eb0d6e2e08742967bd9b3 merge the lobby suppression diff -r 64740eec84ad -r 4c523ed1d35c .hgignore --- a/.hgignore Sun Mar 24 14:05:06 2024 -0400 +++ b/.hgignore Sun Mar 24 14:33:57 2024 -0400 @@ -95,3 +95,5 @@ *.so *_autogen build/ +target/ +dist-newstyle diff -r 64740eec84ad -r 4c523ed1d35c .hgtags diff -r 64740eec84ad -r 4c523ed1d35c .travis.yml --- a/.travis.yml Sun Mar 24 14:05:06 2024 -0400 +++ b/.travis.yml Sun Mar 24 14:33:57 2024 -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" diff -r 64740eec84ad -r 4c523ed1d35c CMakeLists.txt --- a/CMakeLists.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/CMakeLists.txt Sun Mar 24 14:33:57 2024 -0400 @@ -1,8 +1,8 @@ +cmake_minimum_required(VERSION 2.6.4) + project(hedgewars) - #initialise cmake environment -cmake_minimum_required(VERSION 2.6.4) foreach(hwpolicy CMP0003 CMP0012 CMP0017 CMP0018) if(POLICY ${hwpolicy}) cmake_policy(SET ${hwpolicy} NEW) @@ -90,9 +90,9 @@ #versioning set(CPACK_PACKAGE_VERSION_MAJOR 1) -set(CPACK_PACKAGE_VERSION_MINOR 0) -set(CPACK_PACKAGE_VERSION_PATCH 2) -set(HEDGEWARS_PROTO_VER 59) +set(CPACK_PACKAGE_VERSION_MINOR 1) +set(CPACK_PACKAGE_VERSION_PATCH 0) +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() @@ -165,6 +165,10 @@ endif() +if(GHC_DYNAMIC) + list(APPEND haskell_flags "-dynamic") +endif() + #get BUILD_TYPE and enable/disable optimisation message(STATUS "Using ${CMAKE_BUILD_TYPE} configuration") if(CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -173,6 +177,7 @@ "-fno-warn-unused-do-bind" "-O0" ) + set(USE_DEBUG_LIBRARIES TRUE) else() list(APPEND haskell_flags "-w" # no warnings "-O2" @@ -245,7 +250,15 @@ if(PHYSFS_LIBRARY AND PHYSFS_INCLUDE_DIR) #use an IMPORTED tharget so that we can just use 'physfs' to link add_library(physfs UNKNOWN IMPORTED) - set_target_properties(physfs PROPERTIES IMPORTED_LOCATION ${PHYSFS_LIBRARY}) + if (DEFINED PHYSFS_LIBRARY_RELEASE) + if (${USE_DEBUG_LIBRARIES}) + set_target_properties(physfs PROPERTIES IMPORTED_LOCATION ${PHYSFS_LIBRARY_DEBUG}) + else() + set_target_properties(physfs PROPERTIES IMPORTED_LOCATION ${PHYSFS_LIBRARY_RELEASE}) + endif() + else() + set_target_properties(physfs PROPERTIES IMPORTED_LOCATION ${PHYSFS_LIBRARY}) + endif() else() message(FATAL_ERROR "Missing PhysFS! Install PhysFS to fix this.") endif() diff -r 64740eec84ad -r 4c523ed1d35c CREDITS --- a/CREDITS Sun Mar 24 14:05:06 2024 -0400 +++ b/CREDITS Sun Mar 24 14:33:57 2024 -0400 @@ -16,24 +16,42 @@ ========== = HATS ========== -- Robinator -> Terminator_Glasses (2010) +- Tiyuri -> Samurai (2008), WhySoSerious (2008) +- Palewolf -> spartan (2008), RobinHood (2008), clown* (2008), fr_orange (2008), fr_lemon (2008), fr_apple (2008), fr_banana (2008), pirate_jack (2008), pirate_jack_bandana (2008), kiss_* (2008), ushanka (2008), royalguard (2008), scif_swStormtrooper (2008), IndianChief (2008), beefeater (2008) +- joshua -> Bandit (2008) +- DrDickens -> poke_slowpoke (2008), mv_Venom (2008), thug (2009), Eva_00b (2009), Eva_00y (2009) +- alzen -> poke_pikachu (2008) +- Zippy -> Jason (2008), ntd_Falcon (2009) +- Howdy -> scif_Geordi (2009) +- Zept -> Ninja* (2009) +- acegikmo -> anzac (2009) +- Fwirt -> quotecap (2009) +- PhilPhil -> angel (2009) +- em3 -> 4gsuif (2009) +- Armagon -> ntd_Link (2010) +- Robinator -> Terminator_Glasses (2010), chuckl (2010) - shingo666 -> ntd_Samus (2010) -- MeinCookie95 -> InfernalHorns (2010), Mummy (2010), war_* (2010-2011) +- MeinCookie95 -> InfernalHorns (2010), Mummy (2010), vampirichog (2010), hogpharaoh (2010), bobby (2010), bobby2v (2011), war_* (2010-2011) - thuban -> Elvis (2010) -- Miphica -> Disguise (2010) +- Miphica -> Disguise (2010), zoo_Bat (2011) +- maqui -> zoo_Beaver (2010) - Blayde -> zoo_Deer (2010), zoo_Moose (2010) - hillis -> AkuAku (2010) -- Lortinak -> OldMan (2010), ShortHair_* (2010) -- chujoii -> scif_BrainSlug (2010), scif_BrainSlug2 (2010), Dragon (2010), dish_Ladle (2010), Laminaria (2010), Pantsu (2010), zoo_Pig (2010), Plunger (2010), dish_SauceBoatSilver (2010), ShaggyYeti (2010), Sleepwalker (2010), SunWukong (2010), dish_Teapot (2010), dish_Teacup (2010), Zombi (2010) -- Randy Broda -> cyclops (2011), TeamSoldier (2011) +- assassin_killer -> spcartman (2011), spkenny (2011), spkyle (2011), spstan (2011) +- Lortinak -> OldMan (2010), ShortHair_* (2010), sth_Shadow (2010), sth_Super (2010), sth_Metal (2010) +- Grunzer -> zoo_Porkey (2010) +- chujoii -> bubble (2012), car (2012), DayAndNight (2012), dish_Ladle (2010), dish_SauceBoatSilver (2010), dish_Teacup (2010), dish_Teapot (2010), Dauber (2012), Dragon (2010), lamp (2012), Laminaria (2010), Pantsu (2010), Plunger (2010), mechanicaltoy (2012), noface (2012), Sleepwalker (2010), ShaggyYeti (2010), scif_BrainSlug (2010), scif_BrainSlug2 (2010), scif_cosmonaut (2010), ShaggyYeti (2010), Sleepwalker (2010), SunWukong (2012), Zombi (2010), zoo_chicken (2012), zoo_elephant (2012), zoo_fish (2012), zoo_frog (2012), zoo_Pig (2010), zoo_snail (2012), zoo_turtle (2012) +- Randy Broda -> cyclops (2011), TeamSoldier (2011), Joker (2012), Evil (2012) - Zav -> zoo_octopus (2009) -- Star and Moon -> bishop (2011) +- Star and Moon -> bishop (2011), metalband (2011), Meteorhelmet (2013), tf_scout (2013), tf_demoman (2013) +- YoukaiCountry -> touhou_* (2011) - Gimo -> leprechaun [based on tophats] (2011) -- Terrington_Snyde -> pirate_eyepatch (2013), jester (2013) +- Terrington_Snyde -> pirate_eyepatch (2013), jester (2013), snorkel (2013), nurse (2013), doctor (2013), constructor (2013), punkman (2013) - Wohlstand -> policegirl [based on policecap and sm_daisy] (2014) - TheMadCharles -> barrelhider (CC BY 3.0) (2015) -- Trey Perry -> Other hats -- alfadur -> zoo_crocodile (2019) +- alfadur -> zoo_crocodile (2019), zoo_panda (2020) +- Trey Perry -> Some other hats + ========== = GRAVES @@ -41,6 +59,7 @@ - Randy Broda -> dragonball (2012) - CheezeMonkey -> pi (2011) - rosenholz -> Whisky (2013) +- alfadur -> Mushroom (2020), Teapot (2020) ================= = FRONTEND IMAGES @@ -83,6 +102,10 @@ 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/ +- Dynamite bounce: Based off sound by kev_durr (CC BY 3.0) + https://freesound.org/people/kev_durr/sounds/426455/ - 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) diff -r 64740eec84ad -r 4c523ed1d35c ChangeLog.txt --- a/ChangeLog.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/ChangeLog.txt Sun Mar 24 14:33:57 2024 -0400 @@ -1,5 +1,63 @@ + features * bugfixes +==================== 1.1.0-dev ===================== +Gameplay: + + Minigun push is now much stronger + + Easier to push hogs around with blowtorch + + Backjumps are now a bit easier + + Teach computer players how to ... + + - use drill strike, piano strike, air mine, cleaver, seduction, mudball + + - use resurrector, laser sight, low gravity + + - use mine strike (0 seconds only) + + - use RC plane (very basic) + + - drop mines from a cliff + + Low level computer players are more inaccurate with guns + + Computer player now takes Strong Wind game modifier into account + + Various small computer player improvements + + New taunt chat commands: /bubble, /happy + + Remove Vamprism and Resurrector ammos when playing in "Invulnerable" game modifier + * Fix many projectiles not being affected by Heavy Wind after turn end + * Fix hog getting stuck when opening parachute right after a shoryuken digging through land + * Fix game hanging if computer hog has nothing to attack + * Fix hog sometimes not falling after resurrection + * Fix hog not returning from TimeBox when all land was destroyed + * Fix hammer not digging when hitting hog with 0 health + +Campaigns: + + A Space Adventure: Spacetrip: Meteorite appears blown-up after victory + * A Classic Fairytale: Mission 1: Fix possibility of getting stuck in “Leap of Faith” section + * A Space Adventure: The First Stop: Fix broken victory condition when eliminating minions + * A Space Adventure: Killing the Specialists: Don't award player health if enemy hurts itself + +Styles: + + Racer: Allow to set turn time in game scheme + + Racer: Reset mines, air mines and sticky mines every turn + * Racer: Resize waypoints in custom-sized drawn maps + * Mutant: Fix impossible to become mutant after mutant is gone + +Content: + + New flags: serbia, montenegro + + New graves: Mushroom, Teapot + + New hat: zoo_panda + +Graphics / user interface: + + Add dynamite fuse and impact sounds + + Themes: Add fade-in and fade-out effects for background flakes + + Themes: Make Sudden Death jellyfish in Underwater theme rise + + In-Game chat size can now be adjusted. Hold Ctrl and press -, + or = while in chat input. Hold Shift for finer control + + The intial in-game chat size can be configured in the Frontend's “Video” settings tab + + Various small HUD tweaks + +Frontend: + + Sort ammos in weapon scheme editor + * Fix weapon schemes sometimes not being saved properly + * Fix world edge not being changable under macOS + +Lua: + + Add RopeKnocking library + + vgtSmallDamageTag: Can change dX, dY; add screen coordinates (Frame~=0) + * Fix crash when spawning a vgtSmallDamageTag + ====================== 1.0.0 ======================= Highlights: + Campaigns now respect your team identity instead of overwriting it diff -r 64740eec84ad -r 4c523ed1d35c INSTALL.md --- a/INSTALL.md Sun Mar 24 14:05:06 2024 -0400 +++ b/INSTALL.md Sun Mar 24 14:33:57 2024 -0400 @@ -81,6 +81,11 @@ - `regex-tdfa` - `binary` >= 0.8.5.1 +If you use the `Cabal` based build process: + - `zlib` is not needed. + - `network` >= 3.0 + - `network-bsd` >= 2.8.1 + Building -------- @@ -118,6 +123,7 @@ - `CMAKE_INSTALL_PREFIX`: Installation directory - `NOSERVER`: Set to `ON` to *not* build the server - `NOVIDEOREC`: Set to `ON` to *not* build the video recorder +- `GHC_DYNAMIC`: Set to `ON` to build dynamically-linked haskell object files and executables (needed for some distributions) ### Step 2: Make @@ -139,6 +145,16 @@ That's all! Enjoy! +### Building the Hedgewars Server only + +The Hedgewars Server can also be built separately using `Cabal`. All necessary +files, including the `hedgewars-server.cabal`, are in the `gameServer` +subdirectory. +For most users, the server isn't needed, and this possibility is targeted +primarily at packagers. If you don't know how to build Haskell projects with +`Cabal`, this option is likely not for you. Instead use the `cmake` based +instructions above. + Troubleshooting --------------- diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/CMakeLists.txt --- a/QTfrontend/CMakeLists.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/CMakeLists.txt Sun Mar 24 14:33:57 2024 -0400 @@ -14,7 +14,11 @@ include(CheckLibraryExists) find_package(SDL2 REQUIRED CONFIG) -find_package(SDL2_mixer 2 REQUIRED) #audio in SDLInteraction +if(WIN32 AND VCPKG_TOOLCHAIN) + find_package(SDL2_mixer REQUIRED CONFIG) #audio in SDLInteraction +else() + find_package(SDL2_mixer 2 REQUIRED) #audio in SDLInteraction +endif() include_directories(${SDL2_INCLUDE_DIRS}) include_directories(${SDL2_MIXER_INCLUDE_DIRS}) @@ -229,16 +233,13 @@ Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Network ) -list(APPEND HW_LINK_LIBS - ${SDL2_LIBRARIES} - ${SDL2_MIXER_LIBRARIES} - ) +if(WIN32 AND VCPKG_TOOLCHAIN) + list(APPEND HW_LINK_LIBS SDL2::SDL2 SDL2_mixer::SDL2_mixer) +else() + list(APPEND HW_LINK_LIBS ${SDL2_LIBRARIES} ${SDL2_MIXER_LIBRARY}) +endif() if(WIN32 AND NOT UNIX) - if(NOT SDL2_LIBRARIES) - list(APPEND HW_LINK_LIBS SDL2) - endif() - list(APPEND HW_LINK_LIBS ole32 oleaut32 diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/binds.cpp --- a/QTfrontend/binds.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/binds.cpp Sun Mar 24 14:33:57 2024 -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 diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/campaign.cpp --- a/QTfrontend/campaign.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/campaign.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -57,25 +57,29 @@ QSettings* teamfile = getCampTeamFile(campaignName, teamName); int progress = teamfile->value("Campaign " + campaignName + "/Progress", 0).toInt(); int unlockedMissions = teamfile->value("Campaign " + campaignName + "/UnlockedMissions", 0).toInt(); - // The CowardMode cheat unlocks all campaign missions, - // but as "punishment", none of them will be marked as completed. + QSettings campfile("physfs://Missions/Campaign/" + campaignName + "/campaign.ini", QSettings::IniFormat, 0); + campfile.setIniCodec("UTF-8"); + int totalMissions = campfile.value("MissionNum", 1).toInt(); + // The CowardMode cheat unlocks all campaign missions. // Added to make it easier to test campaigns. bool cheat = teamfile->value("Team/CowardMode", false).toBool(); - if(cheat) + if(progress>0 && unlockedMissions==0) { - return false; - } - else if(progress>0 && unlockedMissions==0) - { - QSettings campfile("physfs://Missions/Campaign/" + campaignName + "/campaign.ini", QSettings::IniFormat, 0); - campfile.setIniCodec("UTF-8"); - int totalMissions = campfile.value("MissionNum", 1).toInt(); - return (progress > (progress - missionInList)) || (progress >= totalMissions); + int maxMission; + if(cheat) + maxMission = totalMissions - (missionInList + 1); + else + maxMission = progress - missionInList; + return (progress > maxMission) || (progress >= totalMissions); } else if(unlockedMissions>0) { int fileMissionId = missionInList + 1; - int actualMissionId = teamfile->value(QString("Campaign %1/Mission%2").arg(campaignName, QString::number(fileMissionId)), false).toInt(); + int actualMissionId; + if(cheat) + actualMissionId = totalMissions - missionInList; + else + actualMissionId = teamfile->value(QString("Campaign %1/Mission%2").arg(campaignName, QString::number(fileMissionId)), false).toInt(); return teamfile->value(QString("Campaign %1/Mission%2Won").arg(campaignName, QString::number(actualMissionId)), false).toBool(); } else @@ -87,8 +91,7 @@ { QSettings* teamfile = getCampTeamFile(campaignName, teamName); bool won = teamfile->value("Campaign " + campaignName + "/Won", false).toBool(); - bool cheat = teamfile->value("Team/CowardMode", false).toBool(); - return won && !cheat; + return won; } QSettings* getCampMetaInfo() diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/game.cpp --- a/QTfrontend/game.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/game.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -600,10 +600,31 @@ arguments << "--internal"; //Must be passed as first argument arguments << "--port"; arguments << QString("%1").arg(ipc_port); +#ifdef _WIN32 + { + QString path = datadir->absolutePath(); + if (path == QLatin1String(path.toLatin1())) { + arguments << "--prefix"; + arguments << path; + } else { + arguments << "--prefix64"; + arguments << path.toUtf8().toBase64(); + } + path = cfgdir->absolutePath(); + if (path == QLatin1String(path.toLatin1())) { + arguments << "--user-prefix"; + arguments << path; + } else { + arguments << "--user-prefix64"; + arguments << path.toUtf8().toBase64(); + } + } +#else arguments << "--prefix"; arguments << datadir->absolutePath(); arguments << "--user-prefix"; - arguments << cfgdir->absolutePath(); + arguments << cfgdir->absolutePath(); +#endif arguments << "--locale"; // TODO: Don't bother translators with this nonsense and detect this file automatically. //: IMPORTANT: This text has a special meaning, do not translate it directly. This is the file name of translation files for the game engine, found in Data/Locale/. Usually, you replace “en” with the ISO-639-1 language code of your language. @@ -657,6 +678,8 @@ arguments << "--translucent-tags"; if (!config->isHolidaySillinessEnabled()) arguments << "--no-holiday-silliness"; + arguments << "--chat-size"; + arguments << QString::number(config->chatSize()); return arguments; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/gameuiconfig.cpp --- a/QTfrontend/gameuiconfig.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/gameuiconfig.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -178,6 +178,8 @@ if (m_binds[i].strbind.isEmpty() || m_binds[i].strbind == "default") m_binds[i].strbind = cbinds[i].strbind; } } + + Form->ui.pageOptions->sbChatSize->setValue(value("chat/size", 100).toInt()); } void GameUIConfig::reloadVideosValues(void) @@ -332,6 +334,8 @@ setValue(QString("colors/color%1").arg(i), model->item(i)->data().value().name()); } + setValue("chat/size", Form->ui.pageOptions->sbChatSize->value()); + sync(); } @@ -647,6 +651,11 @@ } } +int GameUIConfig::chatSize() +{ + return Form->ui.pageOptions->sbChatSize->value(); +} + quint8 GameUIConfig::volume() { return Form->ui.pageOptions->SLVolume->value() * 128 / 100; diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/gameuiconfig.h --- a/QTfrontend/gameuiconfig.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/gameuiconfig.h Sun Mar 24 14:33:57 2024 -0400 @@ -53,6 +53,7 @@ bool isShowFPSEnabled(); bool isAltDamageEnabled(); bool appendDateTimeToRecordName(); + int chatSize(); quint8 volume(); quint8 timerInterval(); QString netNick(); diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/hedgewars.ico Binary file QTfrontend/hedgewars.ico has changed diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/hedgewars.qrc --- a/QTfrontend/hedgewars.qrc Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/hedgewars.qrc Sun Mar 24 14:33:57 2024 -0400 @@ -1,6 +1,7 @@ ../share/hedgewars/Data/Graphics/AmmoMenu/Ammos_base.png + ../share/hedgewars/Data/Graphics/AmmoMenu/Ammos_ExtraDamage_comma.png ../share/hedgewars/Data/misc/keys.csv res/css/qt.css res/css/chat.css @@ -159,6 +160,7 @@ res/iconDud.png res/iconExplosive.png res/iconAirMine.png + res/iconSentry.png res/iconRope.png res/iconEarth.png res/iconScript.png diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/hwconsts.cpp.in --- a/QTfrontend/hwconsts.cpp.in Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/hwconsts.cpp.in Sun Mar 24 14:33:57 2024 -0400 @@ -54,6 +54,8 @@ QString * cEmptyAmmoStore = new QString( AMMOLINE_EMPTY_QT AMMOLINE_EMPTY_PROB AMMOLINE_EMPTY_DELAY AMMOLINE_EMPTY_CRATE ); int cAmmoNumber = cDefaultAmmoStore->size() / 4; +unsigned int ammoMenuAmmos[] = HW_AMMOMENU_ARRAY; +int cAmmoMenuRows = 6; QList< QPair > cDefaultAmmos = QList< QPair >() diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/hwconsts.h --- a/QTfrontend/hwconsts.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/hwconsts.h Sun Mar 24 14:33:57 2024 -0400 @@ -51,6 +51,8 @@ extern QStringList cQuickGameMaps; extern unsigned int colors[]; +extern unsigned int ammoMenuAmmos[]; +extern int cAmmoMenuRows; extern QString * netHost; extern quint16 netPort; @@ -119,3 +121,20 @@ 0xffffff01, /* yellow */ \ /* add new colors here */ \ 0 } + +/* The ammo types, sorted in the same way as in the ammo menu */ +#define HW_AMMOMENU_ARRAY {\ + 3, 4, 22, 29, 51, 55,\ + 1, 2, 26, 27, 40, 44,\ + 5, 10, 38, 45, 54, 59,\ + 12, 13, 14, 23, 25, 48,\ + 9, 11, 24, 30, 31, 47,\ + 16, 17, 28, 43, 50, 57,\ + 6, 18, 19, 46, 53, 56,\ + 8, 15, 20, 39, 41, 42,\ + 34, 36, 37, 49, 52, 58,\ + 7, 21, 32, 33, 35, 60\ +} + +/* Ammo ID for extra damage */ +#define HW_AMMOTYPE_EXTRADAMAGE 32 diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/hwform.cpp --- a/QTfrontend/hwform.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/hwform.cpp Sun Mar 24 14:33:57 2024 -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()) { @@ -1295,7 +1295,7 @@ void HWForm::NetAuthFailed() { - // Set the password blank if case the user tries to join and enter his password again + // Set the password blank if case the user tries to join and enter their password again config->clearTempHash(); //Try to login again diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/model/gameSchemeModel.cpp --- a/QTfrontend/model/gameSchemeModel.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/model/gameSchemeModel.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -63,14 +63,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(2) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; GameSchemeModel::GameSchemeModel(QObject* parent, const QString & directory) : @@ -134,14 +135,15 @@ << "minedudpct" // 33 << "explosives" // 34 << "airmines" // 35 - << "healthprobability" // 36 - << "healthcaseamount" // 37 - << "waterrise" // 38 - << "healthdecrease" // 39 - << "ropepct" // 40 - << "getawaytime" // 41 - << "worldedge" // 42 - << "scriptparam" // scriptparam 43 + << "sentries" // 36 + << "healthprobability" // 37 + << "healthcaseamount" // 38 + << "waterrise" // 39 + << "healthdecrease" // 40 + << "ropepct" // 41 + << "getawaytime" // 42 + << "worldedge" // 43 + << "scriptparam" // scriptparam 44 ; QList proMode; @@ -182,14 +184,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(2) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList shoppa; @@ -230,14 +233,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(0) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(0) // water rise amt 38 - << QVariant(0) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(0) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(0) // water rise amt 39 + << QVariant(0) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList cleanslate; @@ -278,14 +282,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(2) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList minefield; @@ -326,14 +331,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList barrelmayhem; @@ -374,14 +380,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(200) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList tunnelhogs; @@ -422,14 +429,15 @@ << QVariant(10) // mine dud pct 33 << QVariant(10) // explosives 34 << QVariant(4) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList timeless; @@ -470,14 +478,15 @@ << QVariant(10) // mine dud pct 33 << QVariant(2) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(30) // health case amt 37 - << QVariant(0) // water rise amt 38 - << QVariant(0) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(30) // health case amt 38 + << QVariant(0) // water rise amt 39 + << QVariant(0) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList thinkingportals; @@ -518,14 +527,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(5) // explosives 34 << QVariant(4) // air mines 35 - << QVariant(25) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(25) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList kingmode; @@ -566,14 +576,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(2) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList mutant; @@ -614,14 +625,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(2) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(0) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(0) // water rise amt 38 - << QVariant(0) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(0) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(0) // water rise amt 39 + << QVariant(0) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList construction; @@ -662,15 +674,16 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 // NOTE: If you change this, also change the defaults in the Construction Mode script - << QVariant("initialenergy=550, energyperround=50, maxenergy=1000, cratesperround=5") // scriptparam 43 + << QVariant("initialenergy=550, energyperround=50, maxenergy=1000, cratesperround=5") // scriptparam 44 ; QList specialists; @@ -711,15 +724,16 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(100) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(47) // water rise amt 38 - << QVariant(5) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 + << QVariant(0) // sentries 36 + << QVariant(100) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(47) // water rise amt 39 + << QVariant(5) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 // NOTE: If you change this, also change the defaults in the The Specialists script - << QVariant("t=SENDXHPL") // scriptparam 43 + << QVariant("t=SENDXHPL") // scriptparam 44 ; QList spaceinvasion; @@ -760,15 +774,16 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(0) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(0) // water rise amt 38 - << QVariant(0) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 + << QVariant(0) // sentries 36 + << QVariant(0) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(0) // water rise amt 39 + << QVariant(0) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 // NOTE: If you change this, also change the defaults in the Space Invasion script - << QVariant("rounds=3, shield=30, barrels=5, pings=2, barrelbonus=3, shieldbonus=30, timebonus=4") // scriptparam 43 + << QVariant("rounds=3, shield=30, barrels=5, pings=2, barrelbonus=3, shieldbonus=30, timebonus=4") // scriptparam 44 ; QList hedgeeditor; @@ -809,14 +824,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(35) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(0) // water rise amt 38 - << QVariant(0) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(35) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(0) // water rise amt 39 + << QVariant(0) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; QList racer; @@ -857,14 +873,15 @@ << QVariant(0) // mine dud pct 33 << QVariant(0) // explosives 34 << QVariant(0) // air mines 35 - << QVariant(0) // health case pct 36 - << QVariant(25) // health case amt 37 - << QVariant(0) // water rise amt 38 - << QVariant(0) // health dec amt 39 - << QVariant(100) // rope modfier 40 - << QVariant(100) // get away time 41 - << QVariant(0) // world edge 42 - << QVariant() // scriptparam 43 + << QVariant(0) // sentries 36 + << QVariant(0) // health case pct 37 + << QVariant(25) // health case amt 38 + << QVariant(0) // water rise amt 39 + << QVariant(0) // health dec amt 40 + << QVariant(100) // rope modfier 41 + << QVariant(100) // get away time 42 + << QVariant(0) // world edge 43 + << QVariant() // scriptparam 44 ; diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/model/roomslistmodel.cpp --- a/QTfrontend/model/roomslistmodel.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/model/roomslistmodel.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -27,10 +27,11 @@ #include "roomslistmodel.h" #include "MapModel.h" +#include "hwconsts.h" RoomsListModel::RoomsListModel(QObject *parent) : QAbstractTableModel(parent), - c_nColumns(9) + c_nColumns(10) { m_headerData = QStringList(); m_headerData << tr("In progress"); @@ -44,6 +45,7 @@ m_headerData << tr("Script"); m_headerData << tr("Rules"); m_headerData << tr("Weapons"); + m_headerData << tr("Version"); m_staticMapModel = DataManager::instance().staticMapModel(); m_missionMapModel = DataManager::instance().missionMapModel(); @@ -77,6 +79,59 @@ } +QString RoomsListModel::protoToVersion(const QString & proto) +{ + bool ok; + uint protoNum = proto.toUInt(&ok); + if (!ok) + return "Unknown"; + switch (protoNum) { + case 17: return "0.9.7-dev"; + case 19: return "0.9.7"; + case 20: return "0.9.8-dev"; + case 21: return "0.9.8"; + case 22: return "0.9.9-dev"; + case 23: return "0.9.9"; + case 24: return "0.9.10-dev"; + case 25: return "0.9.10"; + case 26: return "0.9.11-dev"; + case 27: return "0.9.11"; + case 28: return "0.9.12-dev"; + case 29: return "0.9.12"; + case 30: return "0.9.13-dev"; + case 31: return "0.9.13"; + case 32: return "0.9.14-dev"; + case 33: return "0.9.14"; + case 34: return "0.9.15-dev"; + case 35: return "0.9.14.1"; + case 37: return "0.9.15"; + case 38: return "0.9.16-dev"; + case 39: return "0.9.16"; + case 40: return "0.9.17-dev"; + case 41: return "0.9.17"; + case 42: return "0.9.18-dev"; + case 43: return "0.9.18"; + case 44: return "0.9.19-dev"; + case 45: return "0.9.19"; + case 46: return "0.9.20-dev"; + case 47: return "0.9.20"; + case 48: return "0.9.21-dev"; + case 49: return "0.9.21"; + case 50: return "0.9.22-dev"; + case 51: return "0.9.22"; + case 52: return "0.9.23-dev"; + case 53: return "0.9.23"; + case 54: return "0.9.24-dev"; + case 55: return "0.9.24"; + case 56: return "0.9.25-dev"; + case 57: return "0.9.25"; + case 58: return "1.0.0-dev"; + case 59: return "1.0.0"; + case 60: return "1.1.0-dev"; + default: return "Unknown"; + } +} + QVariant RoomsListModel::data(const QModelIndex &index, int role) const { int column = index.column(); @@ -101,9 +156,10 @@ || ((column != PlayerCountColumn) && (column != TeamCountColumn))) // only decorate name column if ((role != Qt::DecorationRole) || (column != NameColumn)) - // only dye map column - if ((role != Qt::ForegroundRole) || (column != MapColumn)) - return QVariant(); + if ((role != Qt::ForegroundRole)) + // UserRole is used for version column filtering + if ((role != Qt::UserRole)) + return QVariant(); // decorate room name based on room state if (role == Qt::DecorationRole) @@ -159,6 +215,10 @@ !m_missionMapModel->mapExists(content)) return QString ("? %1").arg(content); } + else if (column == VersionColumn) + { + return protoToVersion(content); + } return content; } @@ -166,16 +226,23 @@ // dye map names red if map not available if (role == Qt::ForegroundRole) { - if (content == "+rnd+" || - content == "+maze+" || - content == "+perlin+" || - content == "+drawn+" || - content == "+forts+" || - m_staticMapModel->mapExists(content) || - m_missionMapModel->mapExists(content)) - return QVariant(); - else - return QBrush(QColor("darkred")); + if (m_data[row][VersionColumn] != *cProtoVer) + return QBrush(QColor("darkgrey")); + + if (column == MapColumn) + { + if (content == "+rnd+" || + content == "+maze+" || + content == "+perlin+" || + content == "+drawn+" || + content == "+forts+" || + m_staticMapModel->mapExists(content) || + m_missionMapModel->mapExists(content)) + return QVariant(); + else + return QBrush(QColor("darkred")); + } + return QVariant(); } if (role == Qt::TextAlignmentRole) @@ -183,6 +250,9 @@ return (int)(Qt::AlignHCenter | Qt::AlignVCenter); } + if (role == Qt::UserRole && column == VersionColumn) + return content; + Q_ASSERT(false); return QVariant(); } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/model/roomslistmodel.h --- a/QTfrontend/model/roomslistmodel.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/model/roomslistmodel.h Sun Mar 24 14:33:57 2024 -0400 @@ -42,8 +42,10 @@ TeamCountColumn, OwnerColumn, MapColumn, + ScriptColumn, SchemeColumn, - WeaponsColumn + WeaponsColumn, + VersionColumn, }; explicit RoomsListModel(QObject *parent = 0); @@ -51,6 +53,7 @@ QVariant headerData(int section, Qt::Orientation orientation, int role) const; int rowCount(const QModelIndex & parent) const; int columnCount(const QModelIndex & parent) const; + int columnCountSupported() const { return c_nColumns; }; QVariant data(const QModelIndex &index, int role) const; public slots: @@ -66,6 +69,7 @@ QStringList m_headerData; MapModel * m_staticMapModel; MapModel * m_missionMapModel; + static QString protoToVersion(const QString & proto); }; #endif // HEDGEWARS_ROOMSLISTMODEL_H diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/net/newnetclient.cpp --- a/QTfrontend/net/newnetclient.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/net/newnetclient.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -394,7 +394,7 @@ if (lst[0] == "ROOMS") { - if(lst.size() % 9 != 1) + if(lst.size() % m_roomsListModel->columnCountSupported() != 1) { qWarning("Net: Malformed ROOMS message"); return; @@ -644,7 +644,7 @@ return; } - if(lst[0] == "ROOM" && lst.size() == 11 && lst[1] == "ADD") + if(lst[0] == "ROOM" && lst.size() == m_roomsListModel->columnCountSupported() + 2 && lst[1] == "ADD") { QStringList tmp = lst; tmp.removeFirst(); @@ -654,7 +654,7 @@ return; } - if(lst[0] == "ROOM" && lst.size() == 12 && lst[1] == "UPD") + if(lst[0] == "ROOM" && lst.size() == m_roomsListModel->columnCountSupported() + 3 && lst[1] == "UPD") { QStringList tmp = lst; tmp.removeFirst(); diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/net/recorder.cpp --- a/QTfrontend/net/recorder.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/net/recorder.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -156,6 +156,8 @@ arguments << config->audioCodec(); else arguments << "no"; + arguments << "--chat-size"; + arguments << QString::number(config->chatSize()); return arguments; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/credits.csv --- a/QTfrontend/res/credits.csv Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/res/credits.csv Sun Mar 24 14:33:57 2024 -0400 @@ -6,7 +6,7 @@ E,"Many engine improvements","Derek Pomery","nemo@m8y.org","nemo" E,"Many engine improvements","Carlos Vives","mail@carlosvives.es", E,"Many engine improvements","Richard Karolyi","sheepluva@ercatec.net","sheepluva" -E,,,"Wuzzy2@mail.ru","Wuzzy" +E,,,"Wuzzy@disroot.org","Wuzzy" E,,"Henrik Rostedt","henrik.rostedt@gmail.com", E,"Gamepad and Lua integration","Mario Liebisch","mario.liebisch@gmail.com", E,"Campaign support","Szabolcs Orbàn","szabibibi@gmail.com", @@ -32,7 +32,7 @@ E,"Many frontend improvements","Igor Ulyanov","disinbox@gmail.com", E,"Keybinds, feedback, maps and hats interfaces","Drew Gottlieb","gottlieb.drew@gmail.com", E,"Login dialogs, other improvements","Ondrej Skopek","skopekondrej@gmail.com", -E,,,"Wuzzy2@mail.ru","Wuzzy" +E,,,"Wuzzy@disroot.org","Wuzzy" E,,"Martin Minarik","ttsmj@pokec.sk", E,,"Kristian Lehmann","email@thexception.net", E,,"Henrik Rostedt","henrik.rostedt@gmail.com", @@ -42,7 +42,7 @@ E,"A Classic Fairytale","Szabolcs Orbàn","szabibibi@gmail.com", E,"A Space Adventure",,,"Master_ex" E,"Created Capture the Flag, Construction Mode, Control, HedgeEditor, Highlander, Racer, TechRacer, The Specialists, WxW",,,"mikade" -E,"Training, time-trial and target practice challenges, Bazooka Battlefield, Tentacle Terror, Big Armory, bugfixes and maintenance",,"Wuzzy2@mail.ru","Wuzzy" +E,"Training, time-trial and target practice challenges, Bazooka Battlefield, Tentacle Terror, Big Armory, bugfixes and maintenance",,"Wuzzy@disroot.org","Wuzzy" E,"Some styles and missions","John Lambert","redgrinner@gmail.com","redgrinner" E,"Battalion",,"Anachron14@gmx.de","Anachron" E,"Continental supplies",,,"Vatten" @@ -75,7 +75,7 @@ E,"Nature, Snow, City, Castle, Halloween, Island","John Dum","fizzy@gmail.com", E,"Bamboo, EarthRise, BambooPlinko","Joshua Frese","joshfrese@gmail.com", E,"Golf, Hoggywood, Stage",,,"RoFra" -E,"Hoggywood",,"Wuzzy2@mail.ru","Wuzzy" +E,"Hoggywood",,"Wuzzy@disroot.org","Wuzzy" E,"Cave, Olympics","Guillaume Englert","genglert@hybird.org", E,"Fruit, Cake","Randy Broda",,"Randy" E,"Art",,,"Zippy" @@ -107,10 +107,10 @@ E,"EvilChicken",,,"Dragonfly" E,"Lonely_Island","Maciej Mrozinski","mynick2@o2.pl","alzen" E,"Olympic","Guillaume Englert","genglert@hybird.org", -E,"Olympic",,"Wuzzy2@mail.ru","Wuzzy" +E,"Olympic",,"Wuzzy@disroot.org","Wuzzy" E,"Tank","Carlos Vives","mail@carlosvives.es", E,"Snail","John Dum","fizzy@gmail.com", -E,"Snail",,"Wuzzy2@mail.ru","Wuzzy" +E,"Snail",,"Wuzzy@disroot.org","Wuzzy" E,"SteelTower","Randy Broda",,"Randy" M,,,, U,"Hats, graves, other",,, @@ -121,7 +121,7 @@ E,,"John Dum","fizzy@gmail.com", E,,"Jonatan Nilsson","jonatanfan@gmail.com", E,,"Daniel Martin","elhombresinremedio@gmail.com","HSR" -E,"Various authors from www.freesound.org (see CREDITS text file)",, +E,"Various authors from www.freesound.org (see CREDITS text file)",,, S,"Music",,, E,"City, Rock, others","Daniel Martin","elhombresinremedio@gmail.com","HSR" E,"Compost",,,"HG" @@ -137,6 +137,7 @@ E,"Czech","Petr Řezáček","rezacek@gmail.com", E,"Chinese","Jie Luo","lililjlj@gmail.com", E,"Chinese",,"yuenfu.chiu@gmail.com","yuenfu" +E,"Chinese",,,"heretic43" E,"Finnish","Nina Kuisma","ninnnu@gmail.com", E,"Finnish","Janne Uusitupa",, E,"French","Antoine Turmel","geekshadow@gmail.com", @@ -146,7 +147,7 @@ E,"German","Peter Hüwe","PeterHuewe@gmx.de", E,"German","Mario Liebisch","mario.liebisch@gmail.com", E,"German","Richard Karolyi","sheepluva@ercatec.net","sheepluva" -E,"German",,"Wuzzy2@mail.ru","Wuzzy" +E,"German",,"Wuzzy@disroot.org","Wuzzy" E,"Greek",,"talos_kriti@yahoo.gr", E,"Hungarian",,,"z8w38" E,"Italian","Luca Bonora","bonora.luca@gmail.com", @@ -171,6 +172,7 @@ E,"Scottish Gaelic",,,"GunChleoc" E,"Slovak","Jose Riha",, E,"Spanish","Carlos Vives","mail@carlosvives.es", +E,"Spanish",,,"salvadorc17" E,"Swedish","Niklas Grahn","raewolusjoon@yaoo.com", E,"Swedish","Henrik Rostedt","henrik.rostedt@gmail.com", E,"Ukrainian","Eugene V. Lyubimkin","jackyf.devel@gmail.com", diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/css/april1.css --- a/QTfrontend/res/css/april1.css Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/res/css/april1.css Sun Mar 24 14:33:57 2024 -0400 @@ -406,6 +406,12 @@ height: 6px; border-radius: 3px; } +QSlider::handle::horizontal:hover { +background-color: yellow; +} +QSlider::handle::horizontal:pressed { +background-color: white; +} QSlider::handle::horizontal:disabled { background-color: #a0a0a0; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/css/birthday.css --- a/QTfrontend/res/css/birthday.css Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/res/css/birthday.css Sun Mar 24 14:33:57 2024 -0400 @@ -410,6 +410,12 @@ height: 6px; border-radius: 3px; } +QSlider::handle::horizontal:hover { +background-color: yellow; +} +QSlider::handle::horizontal:pressed { +background-color: white; +} QSlider::handle::horizontal:disabled { background-color: #a0a0a0; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/css/christmas.css --- a/QTfrontend/res/css/christmas.css Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/res/css/christmas.css Sun Mar 24 14:33:57 2024 -0400 @@ -393,6 +393,12 @@ margin: 2px 0px; background-color: #ffcc00; } +QSlider::handle::horizontal:hover { +background-color: yellow; +} +QSlider::handle::horizontal:pressed { +background-color: white; +} QSlider::groove::horizontal:disabled { background-color: #a0a0a0; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/css/easter.css --- a/QTfrontend/res/css/easter.css Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/res/css/easter.css Sun Mar 24 14:33:57 2024 -0400 @@ -390,6 +390,12 @@ margin: 2px 0px; background-color: #ffcc00; } +QSlider::handle::horizontal:hover { +background-color: yellow; +} +QSlider::handle::horizontal:pressed { +background-color: white; +} QSlider::groove::horizontal:disabled { background-color: #a0a0a0; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/css/qt.css --- a/QTfrontend/res/css/qt.css Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/res/css/qt.css Sun Mar 24 14:33:57 2024 -0400 @@ -397,6 +397,12 @@ height: 6px; border-radius: 3px; } +QSlider::handle::horizontal:hover { +background-color: yellow; +} +QSlider::handle::horizontal:pressed { +background-color: white; +} QSlider::handle::horizontal:disabled { background-color: #a0a0a0; } diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/res/iconSentry.png Binary file QTfrontend/res/iconSentry.png has changed diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pagegamestats.cpp --- a/QTfrontend/ui/page/pagegamestats.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pagegamestats.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -122,7 +122,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; @@ -175,6 +175,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() diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pagenetserver.cpp --- a/QTfrontend/ui/page/pagenetserver.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pagenetserver.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -122,7 +122,7 @@ sbPort->setValue(NETGAME_DEFAULT_PORT); } -// This function assumes that the user wants to share his server while connected to +// This function assumes that the user wants to share their server while connected to // the Internet and that he/she is using direct access (eg no NATs). To determine the // IP we briefly connect to Hedgewars website and fallback to user intervention // after 4 seconds of timeout. diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pageoptions.cpp --- a/QTfrontend/ui/page/pageoptions.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pageoptions.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -398,16 +398,29 @@ WeaponTooltip->setText(QCheckBox::tr("Show ammo menu tooltips")); groupGame->layout()->addWidget(WeaponTooltip, 10, 0, 1, 2); - groupGame->addDivider(); + // Chat size adjustment + QLabel *labelChatSize = new QLabel(groupGame); + labelChatSize->setText(QLabel::tr("Chat size (%)")); + labelChatSize->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + groupGame->layout()->addWidget(labelChatSize, 11, 0); + + sbChatSize = new QSpinBox(groupGame); + sbChatSize->setSingleStep(5); + sbChatSize->setMinimum(80); + sbChatSize->setMaximum(400); + sbChatSize->setValue(100); + groupGame->layout()->addWidget(sbChatSize, 11, 1, Qt::AlignLeft); + + groupGame->addDivider(); // row 12 lblTags = new QLabel(groupGame); lblTags->setText(QLabel::tr("Displayed tags above hogs and translucent tags")); - groupGame->layout()->addWidget(lblTags, 12, 0, 1, 2); + groupGame->layout()->addWidget(lblTags, 13, 0, 1, 2); tagsContainer = new QWidget(); QHBoxLayout * tagsLayout = new QHBoxLayout(tagsContainer); tagsLayout->setSpacing(0); - groupGame->layout()->addWidget(tagsContainer, 13, 0, 1, 2); + groupGame->layout()->addWidget(tagsContainer, 14, 0, 1, 2); CBTeamTag = new QCheckBox(groupGame); CBTeamTag->setText(QCheckBox::tr("Team")); @@ -713,6 +726,7 @@ BtnAssociateFiles->setText(QPushButton::tr("Associate file extensions")); BtnAssociateFiles->setVisible(!custom_data && !custom_config); groupMisc->layout()->addWidget(BtnAssociateFiles, 4, 0, 1, 2); + } #ifdef __APPLE__ diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pageoptions.h --- a/QTfrontend/ui/page/pageoptions.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pageoptions.h Sun Mar 24 14:33:57 2024 -0400 @@ -106,6 +106,7 @@ FPSEdit *fpsedit; QLabel *labelNN; + QSpinBox * sbChatSize; QSlider *SLVolume; QLabel *lblVolumeLevel; QLineEdit *editNetNick; diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pageroomslist.cpp --- a/QTfrontend/ui/page/pageroomslist.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pageroomslist.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -84,10 +84,14 @@ showJoinRestricted = new QAction(QAction::tr("Show join restricted"), stateMenu); showJoinRestricted->setCheckable(true); showJoinRestricted->setChecked(true); + showIncompatible = new QAction(QAction::tr("Show incompatible"), stateMenu); + showIncompatible->setCheckable(true); + showIncompatible->setChecked(true); stateMenu->addAction(showGamesInLobby); stateMenu->addAction(showGamesInProgress); stateMenu->addAction(showPassword); stateMenu->addAction(showJoinRestricted); + stateMenu->addAction(showIncompatible); btnState->setMenu(stateMenu); // Help/prompt message at top @@ -199,6 +203,7 @@ connect(showGamesInProgress, SIGNAL(triggered()), this, SLOT(onFilterChanged())); connect(showPassword, SIGNAL(triggered()), this, SLOT(onFilterChanged())); connect(showJoinRestricted, SIGNAL(triggered()), this, SLOT(onFilterChanged())); + connect(showIncompatible, SIGNAL(triggered()), this, SLOT(onFilterChanged())); connect(searchText, SIGNAL(textChanged (const QString &)), this, SLOT(onFilterChanged())); connect(this, SIGNAL(askJoinConfirmation (const QString &)), this, SLOT(onJoinConfirmation(const QString &)), Qt::QueuedConnection); @@ -232,6 +237,7 @@ { roomsModel = NULL; stateFilteredModel = NULL; + versionFilteredModel = NULL; initPage(); } @@ -554,7 +560,7 @@ void PageRoomsList::setModel(RoomsListModel * model) { // filter chain: - // model -> stateFilteredModel -> schemeFilteredModel -> + // model -> versionFilteredModel -> stateFilteredModel -> schemeFilteredModel -> // -> weaponsFilteredModel -> roomsModel (search filter+sorting) if (roomsModel == NULL) @@ -564,12 +570,17 @@ roomsModel->setSortCaseSensitivity(Qt::CaseInsensitive); roomsModel->sort(RoomsListModel::StateColumn, Qt::AscendingOrder); - stateFilteredModel = new QSortFilterProxyModel(this); + versionFilteredModel = new QSortFilterProxyModel(this); + versionFilteredModel->setDynamicSortFilter(true); + versionFilteredModel->setFilterKeyColumn(RoomsListModel::VersionColumn); + versionFilteredModel->setFilterRole(Qt::UserRole); + stateFilteredModel = new QSortFilterProxyModel(this); stateFilteredModel->setDynamicSortFilter(true); + stateFilteredModel->setFilterKeyColumn(RoomsListModel::StateColumn); + stateFilteredModel->setSourceModel(versionFilteredModel); roomsModel->setFilterKeyColumn(-1); // search in all columns - stateFilteredModel->setFilterKeyColumn(RoomsListModel::StateColumn); roomsModel->setFilterCaseSensitivity(Qt::CaseInsensitive); @@ -585,7 +596,7 @@ connect(roomsList->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(roomSelectionChanged(const QModelIndex &, const QModelIndex &))); } - stateFilteredModel->setSourceModel(model); + versionFilteredModel->setSourceModel(model); QHeaderView * h = roomsList->horizontalHeader(); @@ -638,6 +649,12 @@ bool stateProgress = showGamesInProgress->isChecked(); bool statePassword = showPassword->isChecked(); bool stateJoinRestricted = showJoinRestricted->isChecked(); + bool stateIncompatible = showIncompatible->isChecked(); + + if (!stateIncompatible) + versionFilteredModel->setFilterFixedString(*cProtoVer); + else + versionFilteredModel->setFilterFixedString(""); QString filter; if (!stateLobby && !stateProgress) diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pageroomslist.h --- a/QTfrontend/ui/page/pageroomslist.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pageroomslist.h Sun Mar 24 14:33:57 2024 -0400 @@ -93,10 +93,12 @@ QSettings * m_gameSettings; QSortFilterProxyModel * roomsModel; QSortFilterProxyModel * stateFilteredModel; + QSortFilterProxyModel * versionFilteredModel; QAction * showGamesInLobby; QAction * showGamesInProgress; QAction * showPassword; QAction * showJoinRestricted; + QAction * showIncompatible; QSplitter * m_splitter; GameSchemeModel * gameSchemeModel; diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pagescheme.cpp --- a/QTfrontend/ui/page/pagescheme.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pagescheme.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -191,6 +191,7 @@ QString wtMineDuds = tr("Likelihood of a mine being a dud. Does not affect mines placed by hedgehogs."); QString wtExplosives = tr("Average number of barrels to be placed a medium-sized island map. This number will be scaled for other maps."); QString wtAirMines = tr("Average number of air mines to be placed a medium-sized island map. This number will be scaled for other maps."); + QString wtSentries = tr("Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps."); QString wtWorldEdge = tr("Affects the left and right boundaries of the map"); QString wtGetAwayTime = tr("Time you get after an attack"); QString wtScriptParam = tr("Additional parameter to configure game styles. The meaning depends on the used style, refer to the documentation. When in doubt, leave it empty."); @@ -462,33 +463,50 @@ glBSLayout->addWidget(SB_AirMines,14,2,1,1); l = new QLabel(gbBasicSettings); + l->setText(QLabel::tr("Sentry Bots")); + l->setWhatsThis(wtSentries); + l->setWordWrap(true); + glBSLayout->addWidget(l,15,0,1,1); + l = new QLabel(gbBasicSettings); + l->setWhatsThis(wtSentries); + l->setFixedSize(32,32); + l->setPixmap(QPixmap(":/res/iconSentry.png")); + glBSLayout->addWidget(l,15,1,1,1); + SB_Sentries = new QSpinBox(gbBasicSettings); + SB_Sentries->setWhatsThis(wtSentries); + SB_Sentries->setRange(0, 200); + SB_Sentries->setValue(0); + SB_Sentries->setSingleStep(5); + glBSLayout->addWidget(SB_Sentries,15,2,1,1); + + l = new QLabel(gbBasicSettings); //: Label of game scheme setting for the time you get after an attack l->setText(QLabel::tr("% Retreat Time")); l->setWhatsThis(wtGetAwayTime); l->setWordWrap(true); - glBSLayout->addWidget(l,15,0,1,1); + glBSLayout->addWidget(l,16,0,1,1); l = new QLabel(gbBasicSettings); l->setWhatsThis(wtGetAwayTime); l->setFixedSize(32,32); l->setPixmap(QPixmap(":/res/iconTime.png")); - glBSLayout->addWidget(l,15,1,1,1); + glBSLayout->addWidget(l,16,1,1,1); SB_GetAwayTime = new QSpinBox(gbBasicSettings); SB_GetAwayTime->setWhatsThis(wtGetAwayTime); SB_GetAwayTime->setRange(0, 999); SB_GetAwayTime->setValue(100); SB_GetAwayTime->setSingleStep(25); - glBSLayout->addWidget(SB_GetAwayTime,15,2,1,1); + glBSLayout->addWidget(SB_GetAwayTime,16,2,1,1); l = new QLabel(gbBasicSettings); l->setText(QLabel::tr("World Edge")); l->setWhatsThis(wtWorldEdge); l->setWordWrap(true); - glBSLayout->addWidget(l,16,0,1,1); + glBSLayout->addWidget(l,17,0,1,1); l = new QLabel(gbBasicSettings); l->setWhatsThis(wtWorldEdge); l->setFixedSize(32,32); l->setPixmap(QPixmap(":/res/iconEarth.png")); - glBSLayout->addWidget(l,16,1,1,1); + glBSLayout->addWidget(l,17,1,1,1); CB_WorldEdge = new QComboBox(gbBasicSettings); CB_WorldEdge->setWhatsThis(wtWorldEdge); @@ -497,24 +515,24 @@ CB_WorldEdge->insertItem(2, tr("Bounce (Edges reflect)")); CB_WorldEdge->insertItem(3, tr("Sea (Edges connect to sea)")); /* CB_WorldEdge->insertItem(4, tr("Skybox")); */ - glBSLayout->addWidget(CB_WorldEdge,16,2,1,1); + glBSLayout->addWidget(CB_WorldEdge,17,2,1,1); l = new QLabel(gbBasicSettings); l->setText(QLabel::tr("Script parameter")); l->setWhatsThis(wtScriptParam); l->setWordWrap(true); - glBSLayout->addWidget(l,17,0,1,1); + glBSLayout->addWidget(l,18,0,1,1); l = new QLabel(gbBasicSettings); l->setWhatsThis(wtScriptParam); l->setFixedSize(32,32); l->setPixmap(QPixmap(":/res/iconScript.png")); - glBSLayout->addWidget(l,17,1,1,1); + glBSLayout->addWidget(l,18,1,1,1); LE_ScriptParam = new QLineEdit(gbBasicSettings); LE_ScriptParam->setWhatsThis(wtScriptParam); LE_ScriptParam->setMaxLength(240); - glBSLayout->addWidget(LE_ScriptParam,17,2,1,1); + glBSLayout->addWidget(LE_ScriptParam,18,2,1,1); L_name = new QLabel(gbBasicSettings); L_name->setText(QLabel::tr("Scheme Name:")); @@ -557,6 +575,7 @@ connect(BtnCopy, SIGNAL(clicked()), this, SLOT(copyRow())); connect(BtnNew, SIGNAL(clicked()), this, SLOT(newRow())); connect(BtnDelete, SIGNAL(clicked()), this, SLOT(deleteRow())); + connect(CB_WorldEdge, SIGNAL(currentIndexChanged(int)), this, SLOT(worldEdgeChanged(int))); mapper = new QDataWidgetMapper(this); connect(selectScheme, SIGNAL(currentIndexChanged(int)), mapper, SLOT(setCurrentIndex(int))); connect(selectScheme, SIGNAL(currentIndexChanged(int)), this, SLOT(schemeSelected(int))); @@ -609,14 +628,15 @@ mapper->addMapping(SB_MineDuds, 33); mapper->addMapping(SB_Explosives, 34); mapper->addMapping(SB_AirMines, 35); - mapper->addMapping(SB_HealthCrates, 36); - mapper->addMapping(SB_CrateHealth, 37); - mapper->addMapping(SB_WaterRise, 38); - mapper->addMapping(SB_HealthDecrease, 39); - mapper->addMapping(SB_RopeModifier, 40); - mapper->addMapping(SB_GetAwayTime, 41); - mapper->addMapping(CB_WorldEdge, 42, "currentIndex"); - mapper->addMapping(LE_ScriptParam, 43); + mapper->addMapping(SB_Sentries, 36); + mapper->addMapping(SB_HealthCrates, 37); + mapper->addMapping(SB_CrateHealth, 38); + mapper->addMapping(SB_WaterRise, 39); + mapper->addMapping(SB_HealthDecrease, 40); + mapper->addMapping(SB_RopeModifier, 41); + mapper->addMapping(SB_GetAwayTime, 42); + mapper->addMapping(CB_WorldEdge, 43, "currentIndex"); + mapper->addMapping(LE_ScriptParam, 44); mapper->toFirst(); @@ -684,6 +704,14 @@ }; } +void PageScheme::worldEdgeChanged(int n) +{ + if (mapper->itemDelegate()) + { + mapper->itemDelegate()->commitData(CB_WorldEdge); + } +} + void PageScheme::schemeSelected(int n) { int c = ((GameSchemeModel*)mapper->model())->numberOfDefaultSchemes; diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/page/pagescheme.h --- a/QTfrontend/ui/page/pagescheme.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/page/pagescheme.h Sun Mar 24 14:33:57 2024 -0400 @@ -90,6 +90,7 @@ MinesTimeSpinBox * SB_MinesTime; QSpinBox * SB_Mines; QSpinBox * SB_AirMines; + QSpinBox * SB_Sentries; QSpinBox * SB_MineDuds; QSpinBox * SB_Explosives; QSpinBox * SB_RopeModifier; @@ -107,6 +108,7 @@ void checkDupe(); private slots: + void worldEdgeChanged(int); void schemeSelected(int); void dataChanged(QModelIndex topLeft, QModelIndex bottomRight); }; diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/widget/gamecfgwidget.cpp --- a/QTfrontend/ui/widget/gamecfgwidget.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/widget/gamecfgwidget.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -346,18 +346,19 @@ bcfg << QString("e$minedudpct %1").arg(schemeData(33).toInt()).toUtf8(); bcfg << QString("e$explosives %1").arg(schemeData(34).toInt()).toUtf8(); bcfg << QString("e$airmines %1").arg(schemeData(35).toInt()).toUtf8(); - bcfg << QString("e$healthprob %1").arg(schemeData(36).toInt()).toUtf8(); - bcfg << QString("e$hcaseamount %1").arg(schemeData(37).toInt()).toUtf8(); - bcfg << QString("e$waterrise %1").arg(schemeData(38).toInt()).toUtf8(); - bcfg << QString("e$healthdec %1").arg(schemeData(39).toInt()).toUtf8(); - bcfg << QString("e$ropepct %1").arg(schemeData(40).toInt()).toUtf8(); - bcfg << QString("e$getawaytime %1").arg(schemeData(41).toInt()).toUtf8(); - bcfg << QString("e$worldedge %1").arg(schemeData(42).toInt()).toUtf8(); + bcfg << QString("e$sentries %1").arg(schemeData(36).toInt()).toUtf8(); + bcfg << QString("e$healthprob %1").arg(schemeData(37).toInt()).toUtf8(); + bcfg << QString("e$hcaseamount %1").arg(schemeData(38).toInt()).toUtf8(); + bcfg << QString("e$waterrise %1").arg(schemeData(39).toInt()).toUtf8(); + bcfg << QString("e$healthdec %1").arg(schemeData(40).toInt()).toUtf8(); + bcfg << QString("e$ropepct %1").arg(schemeData(41).toInt()).toUtf8(); + bcfg << QString("e$getawaytime %1").arg(schemeData(42).toInt()).toUtf8(); + bcfg << QString("e$worldedge %1").arg(schemeData(43).toInt()).toUtf8(); bcfg << QString("e$template_filter %1").arg(pMapContainer->getTemplateFilter()).toUtf8(); bcfg << QString("e$feature_size %1").arg(pMapContainer->getFeatureSize()).toUtf8(); bcfg << QString("e$mapgen %1").arg(mapgen).toUtf8(); - if(!schemeData(43).isNull()) - bcfg << QString("e$scriptparam %1").arg(schemeData(43).toString()).toUtf8(); + if(!schemeData(44).isNull()) + bcfg << QString("e$scriptparam %1").arg(schemeData(44).toString()).toUtf8(); else bcfg << QString("e$scriptparam ").toUtf8(); diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/widget/keybinder.cpp --- a/QTfrontend/ui/widget/keybinder.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/widget/keybinder.cpp Sun Mar 24 14:33:57 2024 -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); diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/widget/selectWeapon.cpp --- a/QTfrontend/ui/widget/selectWeapon.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/widget/selectWeapon.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -38,11 +38,20 @@ QImage getAmmoImage(int num) { - static QImage ammo(":Ammos.png"); - int x = num/(ammo.height()/32); - int y = (num-((ammo.height()/32)*x))*32; - x*=32; - return ammo.copy(x, y, 32, 32); + // Show ammo image for ammo selection menu + if (QLocale().decimalPoint() == "," && num == HW_AMMOTYPE_EXTRADAMAGE) { + // Special case: Extra Damage icon showing "1,5" instead of "1.5" if locale + // uses comma as decimal separator + static QImage extradamage(":Ammos_ExtraDamage_comma.png"); + return extradamage; + } else { + // Normal case: Pick icon from Ammos.png + static QImage ammo(":Ammos.png"); + int x = num/(ammo.height()/32); + int y = (num-((ammo.height()/32)*x))*32; + x*=32; + return ammo.copy(x, y, 32, 32); + } } SelWeaponItem::SelWeaponItem(bool allowInfinite, int iconNum, int wNum, QImage image, QImage imagegrey, QWidget* parent) : @@ -84,6 +93,16 @@ item->setEnabled(value); } +int SelWeaponWidget::readWeaponValue(const QChar chr, int max) +{ + int value = chr.digitValue(); + if (value == -1) + value = 0; + else if (value > max) + value = max; + return value; +} + SelWeaponWidget::SelWeaponWidget(int numItems, QWidget* parent) : QFrame(parent), m_numItems(numItems) @@ -183,25 +202,34 @@ int i = 0, k = 0; for(; i < m_numItems; ++i) { - // Hide amSkip (6) and amCreeper (57) - // TODO: Unhide amCreeper when this weapon is done - if (i == 6 || i == 57) continue; - if (k % 4 == 0) ++j; - SelWeaponItem * swi = new SelWeaponItem(true, i, currentState[i].digitValue(), QImage(":/res/ammopic.png"), QImage(":/res/ammopicgrey.png"), this); - weaponItems[i].append(swi); - p1Layout->addWidget(swi, j, k % 4); + if (k % cAmmoMenuRows == 0) + ++j; + unsigned int ammo = ammoMenuAmmos[i]; + // Hide amSkip (7) + if (ammo == 7) + continue; + // Hide unused amCreeper (58) + else if (ammo == 58) + { + ++k; + continue; + } + int a = ammo-1; // ammo ID for SelWeaponItem + SelWeaponItem * swi = new SelWeaponItem(true, a, readWeaponValue(currentState[a], 9), QImage(":/res/ammopic.png"), QImage(":/res/ammopicgrey.png"), this); + weaponItems[a].append(swi); + p1Layout->addWidget(swi, j, k % cAmmoMenuRows); - SelWeaponItem * pwi = new SelWeaponItem(false, i, currentState[numItems + i].digitValue(), QImage(":/res/ammopicbox.png"), QImage(":/res/ammopicboxgrey.png"), this); - weaponItems[i].append(pwi); - p2Layout->addWidget(pwi, j, k % 4); + SelWeaponItem * pwi = new SelWeaponItem(false, a, readWeaponValue(currentState[numItems + a], 8), QImage(":/res/ammopicbox.png"), QImage(":/res/ammopicboxgrey.png"), this); + weaponItems[a].append(pwi); + p2Layout->addWidget(pwi, j, k % cAmmoMenuRows); - SelWeaponItem * dwi = new SelWeaponItem(false, i, currentState[numItems*2 + i].digitValue(), QImage(":/res/ammopicdelay.png"), QImage(":/res/ammopicdelaygrey.png"), this); - weaponItems[i].append(dwi); - p3Layout->addWidget(dwi, j, k % 4); + SelWeaponItem * dwi = new SelWeaponItem(false, a, readWeaponValue(currentState[numItems*2 + a], 8), QImage(":/res/ammopicdelay.png"), QImage(":/res/ammopicdelaygrey.png"), this); + weaponItems[a].append(dwi); + p3Layout->addWidget(dwi, j, k % cAmmoMenuRows); - SelWeaponItem * awi = new SelWeaponItem(false, i, currentState[numItems*3 + i].digitValue(), QImage(":/res/ammopic.png"), QImage(":/res/ammopicgrey.png"), this); - weaponItems[i].append(awi); - p4Layout->addWidget(awi, j, k % 4); + SelWeaponItem * awi = new SelWeaponItem(false, a, readWeaponValue(currentState[numItems*3 + a], 8), QImage(":/res/ammopic.png"), QImage(":/res/ammopicgrey.png"), this); + weaponItems[a].append(awi); + p4Layout->addWidget(awi, j, k % cAmmoMenuRows); ++k; } @@ -229,10 +257,10 @@ { twi::iterator it = weaponItems.find(i); if (it == weaponItems.end()) continue; - it.value()[0]->setItemsNum(ammo[i].digitValue()); - it.value()[1]->setItemsNum(ammo[m_numItems + i].digitValue()); - it.value()[2]->setItemsNum(ammo[m_numItems*2 + i].digitValue()); - it.value()[3]->setItemsNum(ammo[m_numItems*3 + i].digitValue()); + it.value()[0]->setItemsNum(readWeaponValue(ammo[i], 9)); + it.value()[1]->setItemsNum(readWeaponValue(ammo[m_numItems + i], 8)); + it.value()[2]->setItemsNum(readWeaponValue(ammo[m_numItems*2 + i], 8)); + it.value()[3]->setItemsNum(readWeaponValue(ammo[m_numItems*3 + i], 8)); it.value()[0]->setEnabled(enable); it.value()[1]->setEnabled(enable); it.value()[2]->setEnabled(enable); @@ -331,6 +359,7 @@ file.close(); } emit weaponsEdited(curWeaponsName, m_name->text(), stateFull); + curWeaponsName = m_name->text(); } int SelWeaponWidget::operator [] (unsigned int weaponIndex) const diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/ui/widget/selectWeapon.h --- a/QTfrontend/ui/widget/selectWeapon.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/ui/widget/selectWeapon.h Sun Mar 24 14:33:57 2024 -0400 @@ -95,6 +95,7 @@ QGridLayout* p4Layout; QString fixWeaponSet(const QString & s); + int readWeaponValue(const QChar chr, int max); }; #endif // _SELECT_WEAPON_INCLUDED diff -r 64740eec84ad -r 4c523ed1d35c QTfrontend/weapons.h --- a/QTfrontend/weapons.h Sun Mar 24 14:05:06 2024 -0400 +++ b/QTfrontend/weapons.h Sun Mar 24 14:33:57 2024 -0400 @@ -16,10 +16,10 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#define AMMOLINE_EMPTY_QT "00000090000000000000000000000000000000000000000000000000000" -#define AMMOLINE_EMPTY_PROB "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_EMPTY_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_EMPTY_CRATE "13111103121111111231141111111111111112111111111111111111111" +#define AMMOLINE_EMPTY_QT "000000900000000000000000000000000000000000000000000000000000" +#define AMMOLINE_EMPTY_PROB "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_EMPTY_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_EMPTY_CRATE "131111031211111112311411111111111111121111111111111111111111" /* AmmoType lookup table (use monospace font / cursor movements) @@ -83,68 +83,69 @@ amAirMine-------------------------------------------------------------------------------| amCreeper--------------------------------------------------------------------------------| amMinigun---------------------------------------------------------------------------------| + amSentry-----------------------------------------------------------------------------------| */ -#define AMMOLINE_DEFAULT_QT "93919294221991210322351110012000000002111001010111110001000" -#define AMMOLINE_DEFAULT_PROB "04050405416006555465544647765766666661555101011154111111107" -#define AMMOLINE_DEFAULT_DELAY "00000000000002055000000400070040000000002200000006000200000" -#define AMMOLINE_DEFAULT_CRATE "13111103121111111231141111111111111112111111111111111111111" +#define AMMOLINE_DEFAULT_QT "939192942219912103223511100120000000021110010101111100010001" +#define AMMOLINE_DEFAULT_PROB "040504054160065554655446477657666666615551010111541111111073" +#define AMMOLINE_DEFAULT_DELAY "000000000000020550000004000700400000000022000000060002000000" +#define AMMOLINE_DEFAULT_CRATE "131111031211111112311411111111111111121111111111111111111111" -#define AMMOLINE_CRAZY_QT "99999999999999999929999999999999992999999999999999929991909" -#define AMMOLINE_CRAZY_PROB "11111101111111111111111111111111111111111111111111111111101" -#define AMMOLINE_CRAZY_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_CRAZY_CRATE "13111103121111111231141111111111111112111111111111111111111" +#define AMMOLINE_CRAZY_QT "999999999999999999299999999999999929999999999999999299919099" +#define AMMOLINE_CRAZY_PROB "111111011111111111111111111111111111111111111111111111111011" +#define AMMOLINE_CRAZY_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_CRAZY_CRATE "131111031211111112311411111111111111121111111111111111111111" -#define AMMOLINE_PROMODE_QT "90900090000000000000090000000000000000000000000000000000000" -#define AMMOLINE_PROMODE_PROB "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_PROMODE_DELAY "00000000000002055000000400070040000000002000000000000200000" -#define AMMOLINE_PROMODE_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_PROMODE_QT "909000900000000000000900000000000000000000000000000000000000" +#define AMMOLINE_PROMODE_PROB "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_PROMODE_DELAY "000000000000020550000004000700400000000020000000000002000000" +#define AMMOLINE_PROMODE_CRATE "111111011111111111111111111111111111111111111111111111111111" -#define AMMOLINE_SHOPPA_QT "00000099000000000000000000000000000000000000000000000000000" -#define AMMOLINE_SHOPPA_PROB "44444100442444022101121212224220000000020004000100110010101" -#define AMMOLINE_SHOPPA_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_SHOPPA_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_SHOPPA_QT "000000990000000000000000000000000000000000000000000000000000" +#define AMMOLINE_SHOPPA_PROB "444441004424440221011212122242200000000200040001001100101010" +#define AMMOLINE_SHOPPA_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_SHOPPA_CRATE "111111011111111111111111111111111111111111111111111111111111" -#define AMMOLINE_CLEAN_QT "10100090000100000110000000000000000000000000000010000000000" -#define AMMOLINE_CLEAN_PROB "04050405416006555465544647765766666661555101011154111211104" -#define AMMOLINE_CLEAN_DELAY "00000000000000000000000000000000000000000000000000000200000" -#define AMMOLINE_CLEAN_CRATE "13111103121111111231141111111111111112111111111111111111111" +#define AMMOLINE_CLEAN_QT "101000900001000001100000000000000000000000000000100000000000" +#define AMMOLINE_CLEAN_PROB "040504054160065554655446477657666666615551010111541112111040" +#define AMMOLINE_CLEAN_DELAY "000000000000000000000000000000000000000000000000000002000000" +#define AMMOLINE_CLEAN_CRATE "131111031211111112311411111111111111121111111111111111111111" -#define AMMOLINE_MINES_QT "00000099000900000003000000000000000000000000000000000000000" -#define AMMOLINE_MINES_PROB "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_MINES_DELAY "00000000000002055000000400070040000000002000000006000200000" -#define AMMOLINE_MINES_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_MINES_QT "000000990009000000030000000000000000000000000000000000000000" +#define AMMOLINE_MINES_PROB "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_MINES_DELAY "000000000000020550000004000700400000000020000000060002000000" +#define AMMOLINE_MINES_CRATE "111111011111111111111111111111111111111111111111111111111111" -#define AMMOLINE_PORTALS_QT "90000090020000000021000000000000001100000900000000000000000" -#define AMMOLINE_PORTALS_PROB "04050405416006555465544647765766666661555101011154111211102" -#define AMMOLINE_PORTALS_DELAY "00000000000002055000000400070040000000002000000006000200000" -#define AMMOLINE_PORTALS_CRATE "13111103121111111231141111111111111112111111111111111111111" +#define AMMOLINE_PORTALS_QT "900000900200000000210000000000000011000009000000000000000000" +#define AMMOLINE_PORTALS_PROB "040504054160065554655446477657666666615551010111541112111020" +#define AMMOLINE_PORTALS_DELAY "000000000000020550000004000700400000000020000000060002000000" +#define AMMOLINE_PORTALS_CRATE "131111031211111112311411111111111111121111111111111111111111" -#define AMMOLINE_ONEEVERY_QT "11111191111111111111111111111111111111111111111111111111101" -#define AMMOLINE_ONEEVERY_PROB "11111101111111111111111111111111111111111111111111111111101" -#define AMMOLINE_ONEEVERY_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_ONEEVERY_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_ONEEVERY_QT "111111911111111111111111111111111111111111111111111111111011" +#define AMMOLINE_ONEEVERY_PROB "111111011111111111111111111111111111111111111111111111111011" +#define AMMOLINE_ONEEVERY_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_ONEEVERY_CRATE "111111011111111111111111111111111111111111111111111111111111" -#define AMMOLINE_BRW_QT "33323392332332322323233131122113000003232203022022200020301" -#define AMMOLINE_BRW_PROB "00000000000000000000000000000000111110000000000000000000000" -#define AMMOLINE_BRW_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_BRW_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_BRW_QT "333233923323323223232331311221130000032322030220222000203010" +#define AMMOLINE_BRW_PROB "000000000000000000000000000000001111100000000000000000000000" +#define AMMOLINE_BRW_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_BRW_CRATE "111111011111111111111111111111111111111111111111111111111111" -#define AMMOLINE_HIGHLANDER_QT "11111191111111111111019111111111100101111101111001001011101" -#define AMMOLINE_HIGHLANDER_PROB "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_HIGHLANDER_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_HIGHLANDER_CRATE "00000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_HIGHLANDER_QT "111111911111111111110191111111111001011111011110010010111010" +#define AMMOLINE_HIGHLANDER_PROB "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_HIGHLANDER_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_HIGHLANDER_CRATE "000000000000000000000000000000000000000000000000000000000001" -#define AMMOLINE_CONSTRUCTION_QT "11000190000000100100900000000000000000000000000000000000000" -#define AMMOLINE_CONSTRUCTION_PROB "11111101111111100100011111101111111111111101111100101110101" -#define AMMOLINE_CONSTRUCTION_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_CONSTRUCTION_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_CONSTRUCTION_QT "110001900000001001009000000000000000000000000000000000000000" +#define AMMOLINE_CONSTRUCTION_PROB "111111011111111001000111111011111111111111011111001011101010" +#define AMMOLINE_CONSTRUCTION_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_CONSTRUCTION_CRATE "111111011111111111111111111111111111111111111111111111111111" -#define AMMOLINE_SHOPPAPRO_QT "00000099000000000000000000000000000000000000000000000000000" -#define AMMOLINE_SHOPPAPRO_PROB "44444000440444000000000000004000000000000000000000000000000" -#define AMMOLINE_SHOPPAPRO_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_SHOPPAPRO_CRATE "11111101111111111111111111111111111111111111111111111211111" +#define AMMOLINE_SHOPPAPRO_QT "000000990000000000000000000000000000000000000000000000000000" +#define AMMOLINE_SHOPPAPRO_PROB "444440004404440000000000000040000000000000000000000000000000" +#define AMMOLINE_SHOPPAPRO_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_SHOPPAPRO_CRATE "111111011111111111111111111111111111111111111111111112111111" -#define AMMOLINE_HEDGEEDITOR_QT "00000090000000000000000000000000000000000000000000000000000" -#define AMMOLINE_HEDGEEDITOR_PROB "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_HEDGEEDITOR_DELAY "00000000000000000000000000000000000000000000000000000000000" -#define AMMOLINE_HEDGEEDITOR_CRATE "11111101111111111111111111111111111111111111111111111111111" +#define AMMOLINE_HEDGEEDITOR_QT "000000900000000000000000000000000000000000000000000000000000" +#define AMMOLINE_HEDGEEDITOR_PROB "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_HEDGEEDITOR_DELAY "000000000000000000000000000000000000000000000000000000000000" +#define AMMOLINE_HEDGEEDITOR_CRATE "111111011111111111111111111111111111111111111111111111111111" diff -r 64740eec84ad -r 4c523ed1d35c README.md --- a/README.md Sun Mar 24 14:05:06 2024 -0400 +++ b/README.md Sun Mar 24 14:33:57 2024 -0400 @@ -144,6 +144,6 @@ Contact ------- * Homepage - https://hedgewars.org/ -* IRC channel - irc://irc.freenode.net/hedgewars +* IRC channel - irc://irc.libera.chat/hedgewars * Community forum - https://hedgewars.org/forum diff -r 64740eec84ad -r 4c523ed1d35c cmake_modules/cpackvars.cmake --- a/cmake_modules/cpackvars.cmake Sun Mar 24 14:05:06 2024 -0400 +++ b/cmake_modules/cpackvars.cmake Sun Mar 24 14:33:57 2024 -0400 @@ -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) diff -r 64740eec84ad -r 4c523ed1d35c cmake_modules/paths.cmake --- a/cmake_modules/paths.cmake Sun Mar 24 14:05:06 2024 -0400 +++ b/cmake_modules/paths.cmake Sun Mar 24 14:33:57 2024 -0400 @@ -61,7 +61,11 @@ #install_name_tool for libraries set(CMAKE_BUILD_WITH_INSTALL_NAME_DIR TRUE) set(CMAKE_INSTALL_NAME_DIR "@executable_path/../Frameworks") -else(APPLE AND NOT (${CMAKE_INSTALL_PREFIX} MATCHES "/usr")) +# should this be a separate if block like so +#if(NOT APPLE AND NOT (${CMAKE_INSTALL_PREFIX} MATCHES "/usr")) +# there were some conditions here that implied not setting the RPATH if installed to /usr +# but it was not being applied due to else not actually taking parameters (HT wuzzy) +else() #paths where to find libraries (final slash not optional): # - the first is relative to the executable # - the second is the same directory of the executable (so it runs in bin/) diff -r 64740eec84ad -r 4c523ed1d35c gameServer/Actions.hs --- a/gameServer/Actions.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/Actions.hs Sun Mar 24 14:33:57 2024 -0400 @@ -24,6 +24,7 @@ import qualified Data.Set as Set import qualified Data.Map as Map import qualified Data.List as L +import Data.Word import qualified Control.Exception as Exception import System.Log.Logger import Control.Monad @@ -65,6 +66,12 @@ ri <- clientRoomA liftM (map sendChan . filter (/= cl)) $ roomClientsS ri +othersChansProto :: StateT ServerState IO [(ClientChan, Word16)] +othersChansProto = do + cl <- client's id + ri <- clientRoomA + map (\ci -> (sendChan ci, clientProto ci)) . filter (/= cl) <$> roomClientsS ri + processAction :: Action -> StateT ServerState IO () @@ -72,6 +79,10 @@ io $ mapM_ (`writeChan` (msg `deepseq` msg)) (chans `deepseq` chans) +processAction (AnswerClientsByProto chansProto msgFunc) = + io $ mapM_ (\(chan, proto) -> writeChan chan (msgFunc proto)) chansProto + + processAction SendServerMessage = do chan <- client's sendChan protonum <- client's clientProto @@ -129,7 +140,7 @@ mapM_ processAction [ AnswerClients [chan] ["BYE", msg] - , ModifyClient (\c -> c{nick = "", isVisible = False}) -- this will effectively hide client from others while he isn't deleted from list + , ModifyClient (\c -> c{nick = "", isVisible = False}) -- this will effectively hide client from others while it isn't deleted from list ] s <- get @@ -279,8 +290,9 @@ ) newRoom' <- io $ room'sM rnc id ri - chans <- liftM (map sendChan) $! sameProtoClientsS proto - processAction $ AnswerClients chans ("ROOM" : "UPD" : oldRoomName : roomInfo proto (maybeNick newMaster) newRoom') + chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS + let oldRoomNameByProto = roomNameByProto oldRoomName (roomProto newRoom') + processAction $ AnswerClientsByProto chansProto (\p -> "ROOM" : "UPD" : oldRoomNameByProto p : roomInfo p (maybeNick newMaster) newRoom') processAction (AddRoom roomName roomPassword) = do @@ -300,10 +312,10 @@ processAction $ MoveToRoom rId - chans <- liftM (map sendChan) $! sameProtoClientsS proto + chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS mapM_ processAction [ - AnswerClients chans ("ROOM" : "ADD" : roomInfo proto n rm{playersIn = 1}) + AnswerClientsByProto chansProto (\p -> "ROOM" : "ADD" : roomInfo p n rm{playersIn = 1}) ] @@ -312,13 +324,13 @@ rnc <- gets roomsClients ri <- io $ clientRoomM rnc clId roomName <- io $ room'sM rnc name ri - others <- othersChans - proto <- client's clientProto - chans <- liftM (map sendChan) $! sameProtoClientsS proto + roomProto <- io $ room'sM rnc roomProto ri + others <- othersChansProto + chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS mapM_ processAction [ - AnswerClients chans ["ROOM", "DEL", roomName], - AnswerClients others ["ROOMABANDONED", roomName] + AnswerClientsByProto chansProto (\p -> ["ROOM", "DEL", roomNameByProto roomName roomProto p]), + AnswerClientsByProto others (\p -> ["ROOMABANDONED", roomNameByProto roomName roomProto p]) ] io $ removeRoom rnc ri @@ -331,8 +343,9 @@ ri <- io $ clientRoomM rnc clId rm <- io $ room'sM rnc id ri masterCl <- io $ client'sM rnc id `DT.mapM` (masterID rm) - chans <- liftM (map sendChan) $! sameProtoClientsS proto - processAction $ AnswerClients chans ("ROOM" : "UPD" : name rm : roomInfo proto (maybeNick masterCl) rm) + chansProto <- fmap (map (\c -> (sendChan c, clientProto c))) $! allClientsS + let thisRoomNameByProto = roomNameByProto (name rm) (roomProto rm) + processAction $ AnswerClientsByProto chansProto (\p -> "ROOM" : "UPD" : thisRoomNameByProto p : roomInfo p (maybeNick masterCl) rm) processAction UnreadyRoomClients = do @@ -536,7 +549,7 @@ rooms <- roomsM rnc mapM (\r -> (mapM (client'sM rnc id) $ masterID r) >>= \cn -> return $ roomInfo clProto (maybeNick cn) r) - $ filter (\r -> (roomProto r == clProto)) rooms + $ filter ((/=) 0 . roomProto) rooms mapM_ processAction . concat $ [ [AnswerClients clientsChans ["LOBBY:JOINED", clientNick]] diff -r 64740eec84ad -r 4c523ed1d35c gameServer/ClientIO.hs --- a/gameServer/ClientIO.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/ClientIO.hs Sun Mar 24 14:33:57 2024 -0400 @@ -20,6 +20,7 @@ module ClientIO where import qualified Control.Exception as Exception +import Control.Monad import Control.Monad.State import Control.Concurrent.Chan import Control.Concurrent diff -r 64740eec84ad -r 4c523ed1d35c gameServer/CoreTypes.hs --- a/gameServer/CoreTypes.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/CoreTypes.hs Sun Mar 24 14:33:57 2024 -0400 @@ -46,6 +46,7 @@ data Action = AnswerClients ![ClientChan] ![B.ByteString] + | AnswerClientsByProto ![(ClientChan, Word16)] !(Word16 -> [B.ByteString]) | SendServerMessage | SendServerVars | MoveToRoom RoomIndex diff -r 64740eec84ad -r 4c523ed1d35c gameServer/HWProtoChecker.hs --- a/gameServer/HWProtoChecker.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/HWProtoChecker.hs Sun Mar 24 14:33:57 2024 -0400 @@ -20,6 +20,7 @@ module HWProtoChecker where import Data.Maybe +import Control.Monad import Control.Monad.Reader -------------------------------------- import CoreTypes diff -r 64740eec84ad -r 4c523ed1d35c gameServer/HWProtoCore.hs --- a/gameServer/HWProtoCore.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/HWProtoCore.hs Sun Mar 24 14:33:57 2024 -0400 @@ -19,6 +19,7 @@ {-# LANGUAGE OverloadedStrings #-} module HWProtoCore where +import Control.Monad import Control.Monad.Reader import Data.Maybe import qualified Data.ByteString.Char8 as B diff -r 64740eec84ad -r 4c523ed1d35c gameServer/HWProtoInRoomState.hs --- a/gameServer/HWProtoInRoomState.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/HWProtoInRoomState.hs Sun Mar 24 14:33:57 2024 -0400 @@ -313,7 +313,8 @@ cl <- thisClient rs <- allRoomInfos rm <- thisRoom - chans <- sameProtoChans + chansProto <- allChansProto + let thisRoomNameByProto = roomNameByProto (name rm) (roomProto rm) return $ if illegalName newName then @@ -326,7 +327,7 @@ [Warning $ loc "A room with the same name already exists."] else [ModifyRoom roomUpdate, - AnswerClients chans ("ROOM" : "UPD" : name rm : roomInfo (clientProto cl) (nick cl) (roomUpdate rm)), + AnswerClientsByProto chansProto (\p -> "ROOM" : "UPD" : thisRoomNameByProto p : roomInfo p (nick cl) (roomUpdate rm)), RegisterEvent RoomNameUpdate] where roomUpdate r = r{name = newName} diff -r 64740eec84ad -r 4c523ed1d35c gameServer/HWProtoLobbyState.hs --- a/gameServer/HWProtoLobbyState.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/HWProtoLobbyState.hs Sun Mar 24 14:33:57 2024 -0400 @@ -21,6 +21,7 @@ import Data.Maybe import Data.List +import Control.Monad import Control.Monad.Reader import qualified Data.ByteString.Char8 as B -------------------------------------- @@ -40,7 +41,7 @@ (ci, irnc) <- ask let cl = irnc `client` ci rooms <- allRoomInfos - let roomsInfoList = concatMap (\r -> roomInfo (clientProto cl) (maybeNick . liftM (client irnc) $ masterID r) r) . filter (\r -> (roomProto r == clientProto cl)) + let roomsInfoList = concatMap (\r -> roomInfo (clientProto cl) (maybeNick . liftM (client irnc) $ masterID r) r) . filter ((/=) 0 . roomProto) return $ if hasAskedList cl then [] else [ ModifyClient (\c -> c{hasAskedList = True}) , AnswerClients [sendChan cl] ("ROOMS" : roomsInfoList rooms)] @@ -91,7 +92,9 @@ [] let clTeamsNames = map teamname clTeams return $ - if isNothing maybeRI then + if isNothing maybeRI && clientProto cl < 60 && B.isPrefixOf "[v" roomName then + [Warning $ loc "Room version incompatible to your Hedgewars version!"] + else if isNothing maybeRI then [Warning $ loc "No such room."] else if (not sameProto) && (not $ isAdministrator cl) then [Warning $ loc "Room version incompatible to your Hedgewars version!"] diff -r 64740eec84ad -r 4c523ed1d35c gameServer/HandlerUtils.hs --- a/gameServer/HandlerUtils.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/HandlerUtils.hs Sun Mar 24 14:33:57 2024 -0400 @@ -18,9 +18,11 @@ module HandlerUtils where +import Control.Monad import Control.Monad.Reader import qualified Data.ByteString.Char8 as B import Data.List +import Data.Word import RoomsAndClients import CoreTypes @@ -74,6 +76,11 @@ let p = clientProto (rnc `client` ci) return . map sendChan . filter (\c -> clientProto c == p) . map (client rnc) $ allClients rnc +allChansProto :: Reader (ClientIndex, IRnC) [(ClientChan, Word16)] +allChansProto = do + (ci, rnc) <- ask + return . map ((\c -> (sendChan c, clientProto c)) . client rnc) $ allClients rnc + answerClient :: [B.ByteString] -> Reader (ClientIndex, IRnC) [Action] answerClient msg = liftM ((: []) . flip AnswerClients msg) thisClientChans diff -r 64740eec84ad -r 4c523ed1d35c gameServer/OfficialServer/checker.hs --- a/gameServer/OfficialServer/checker.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/OfficialServer/checker.hs Sun Mar 24 14:33:57 2024 -0400 @@ -54,7 +54,7 @@ deriving Show serverAddress = "netserver.hedgewars.org" -protocolNumber = "55" +protocolNumber = "59" getLines :: Handle -> IO [B.ByteString] getLines h = g @@ -175,6 +175,7 @@ checkReplay home exe prefix chan msgs warningM "Check" "Started check" onPacket _ ("BYE" : xs) = error $ show xs + onPacket _ ("CHAT" : nickname : text) = infoM "Chat" $ ">>> " ++ show nickname ++ ": " ++ show text onPacket _ _ = return () @@ -189,6 +190,7 @@ updateGlobalLogger "Network" (setLevel WARNING) updateGlobalLogger "Check" (setLevel DEBUG) updateGlobalLogger "Engine" (setLevel DEBUG) + updateGlobalLogger "Chat" (setLevel DEBUG) d <- getHomeDirectory Right (login, password) <- runErrorT $ do diff -r 64740eec84ad -r 4c523ed1d35c gameServer/ServerState.hs --- a/gameServer/ServerState.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/ServerState.hs Sun Mar 24 14:33:57 2024 -0400 @@ -30,6 +30,7 @@ io ) where +import Control.Monad import Control.Monad.State.Strict import Data.Set as Set(Set) import Data.Word diff -r 64740eec84ad -r 4c523ed1d35c gameServer/Utils.hs --- a/gameServer/Utils.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/Utils.hs Sun Mar 24 14:33:57 2024 -0400 @@ -124,7 +124,7 @@ , (57, "0.9.25") , (58, "1.0.0-dev") , (59, "1.0.0") - , (60, "1.0.1-dev") + , (60, "1.1.0-dev") ] askFromConsole :: B.ByteString -> IO B.ByteString @@ -158,11 +158,16 @@ upperCase :: B.ByteString -> B.ByteString upperCase = UTF8.fromString . map Char.toUpper . UTF8.toString +roomNameByProto :: B.ByteString -> Word16 -> Word16 -> B.ByteString +roomNameByProto roomName roomProto clientProto + | clientProto < 60 && roomProto /= clientProto = B.concat [B.pack "[v", protoNumber2ver roomProto, B.pack "] ", roomName] + | otherwise = roomName + roomInfo :: Word16 -> B.ByteString -> RoomInfo -> [B.ByteString] roomInfo p n r | p < 46 = [ showB $ isJust $ gameInfo r, - name r, + roomNameByProto (name r) (roomProto r) p, showB $ playersIn r, showB $ length $ teams r, n, @@ -172,7 +177,18 @@ ] | p < 48 = [ showB $ isJust $ gameInfo r, - name r, + roomNameByProto (name r) (roomProto r) p, + showB $ playersIn r, + showB $ length $ teams r, + n, + Map.findWithDefault "+rnd+" "MAP" (mapParams r), + head (Map.findWithDefault ["Normal"] "SCRIPT" (params r)), + head (Map.findWithDefault ["Default"] "SCHEME" (params r)), + head (Map.findWithDefault ["Default"] "AMMO" (params r)) + ] + | p < 60 = [ + B.pack roomFlags, + roomNameByProto (name r) (roomProto r) p, showB $ playersIn r, showB $ length $ teams r, n, @@ -190,7 +206,8 @@ Map.findWithDefault "+rnd+" "MAP" (mapParams r), head (Map.findWithDefault ["Normal"] "SCRIPT" (params r)), head (Map.findWithDefault ["Default"] "SCHEME" (params r)), - head (Map.findWithDefault ["Default"] "AMMO" (params r)) + head (Map.findWithDefault ["Default"] "AMMO" (params r)), + showB $ roomProto r ] where roomFlags = concat [ diff -r 64740eec84ad -r 4c523ed1d35c gameServer/Votes.hs --- a/gameServer/Votes.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/Votes.hs Sun Mar 24 14:33:57 2024 -0400 @@ -19,6 +19,7 @@ {-# LANGUAGE OverloadedStrings #-} module Votes where +import Control.Monad import Control.Monad.Reader import Control.Monad.State.Strict import ServerState diff -r 64740eec84ad -r 4c523ed1d35c gameServer/hedgewars-server.cabal --- a/gameServer/hedgewars-server.cabal Sun Mar 24 14:05:06 2024 -0400 +++ b/gameServer/hedgewars-server.cabal Sun Mar 24 14:33:57 2024 -0400 @@ -1,5 +1,5 @@ Name: hedgewars-server -Version: 0.1 +Version: 1.1.0 Synopsis: hedgewars server Description: hedgewars server Homepage: https://www.hedgewars.org/ @@ -11,36 +11,71 @@ Cabal-version: >=1.10 +flag officialServer + description: Build for official server + default: False + manual: True + Executable hedgewars-server main-is: hedgewars-server.hs + other-modules: + Actions + ClientIO + CommandHelp + ConfigFile + Consts + CoreTypes + Data.TConfig + EngineInteraction + FloodDetection + HWProtoChecker + HWProtoCore + HWProtoInRoomState + HWProtoLobbyState + HWProtoNEState + HandlerUtils + JoinsMonitor + NetRoutines + OfficialServer.DBInteraction + Opts + RoomsAndClients + ServerCore + ServerState + Store + Utils + Votes default-language: Haskell2010 -- Don't forget to update INSTALL.md and .travis.yml when you change these dependencies! Build-depends: base >= 4.8, - containers, - vector, + binary >= 0.8.5.1, bytestring, - network >= 2.3 && < 3.0, - random, - time, - mtl >= 2, - sandi, + containers, + deepseq, + entropy, hslogger, + mtl >= 2, + network >= 3.0 && < 3.2, + network-bsd >= 2.8.1 && < 2.9, process, - deepseq, - utf8-string, + random, + regex-tdfa, + sandi, SHA, - entropy, - zlib >= 0.5.3 && < 0.7, - regex-tdfa, - binary >= 0.8.5.1, + time, + utf8-string, + vector -- These dependencies are for OFFICIAL_SERVER only and do not need to be mentioned in docs - yaml >= 0.8.30, - aeson, - text >= 1.2 + if flag(officialServer) + build-depends: + aeson, + text >= 1.2, + yaml >= 0.8.30, + zlib >= 0.5.3 && < 0.7 + cpp-options: -DOFFICIAL_SERVER if !os(windows) build-depends: unix diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/ArgParsers.pas --- a/hedgewars/ArgParsers.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/ArgParsers.pas Sun Mar 24 14:33:57 2024 -0400 @@ -106,6 +106,7 @@ WriteLn(stdout, ' --no-hogtag: Disable hedgehog name tags'); WriteLn(stdout, ' --no-healthtag: Disable hedgehog health tags'); WriteLn(stdout, ' --translucent-tags: Enable translucent name and health tags'); + WriteLn(stdout, ' --chat-size [default chat size in percent]'); WriteLn(stdout, ' --showfps: Show frames per second'); WriteLn(stdout, ''); WriteLn(stdout, 'Miscellaneous:'); @@ -243,13 +244,13 @@ end; function parseParameter(cmd:string; arg:string; var paramIndex:LongInt): Boolean; -const reallyAll: array[0..34] of shortstring = ( +const reallyAll: array[0..37] of shortstring = ( '--prefix', '--user-prefix', '--locale', '--fullscreen-width', '--fullscreen-height', '--width', '--height', '--maximized', '--frame-interval', '--volume','--nomusic', '--nosound', '--nodampen', '--fullscreen', '--showfps', '--altdmg', '--low-quality', '--raw-quality', '--stereo', '--nick', '--zoom', {internal} '--internal', '--port', '--recorder', '--landpreview', - {misc} '--stats-only', '--gci', '--help','--protocol', '--no-teamtag','--no-hogtag','--no-healthtag','--translucent-tags','--lua-test','--no-holiday-silliness'); + {misc} '--stats-only', '--gci', '--help','--protocol', '--no-teamtag','--no-hogtag','--no-healthtag','--translucent-tags','--lua-test','--no-holiday-silliness','--chat-size', '--prefix64', '--user-prefix64'); var cmdIndex: byte; begin parseParameter:= false; @@ -297,6 +298,9 @@ {--translucent-tags} 32 : cTagsMask := cTagsMask or htTransparent; {--lua-test} 33 : begin cTestLua := true; SetSound(false); cScriptName := getstringParameter(arg, paramIndex, parseParameter); WriteLn(stdout, 'Lua test file specified: ' + cScriptName);end; {--no-holiday-silliness} 34 : cHolidaySilliness:= false; + {--chat-size} 35 : cDefaultChatScale := 1.0 * getLongIntParameter(arg, paramIndex, parseParameter) / 100; + {--prefix64} 36: PathPrefix := DecodeBase64(getstringParameter(arg, paramIndex, parseParameter)); + {--user-prefix64} 37: UserPathPrefix := DecodeBase64(getstringParameter(arg, paramIndex, parseParameter)); else begin //Assume the first "non parameter" is the demo file, anything else is invalid diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/CMakeLists.txt --- a/hedgewars/CMakeLists.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/CMakeLists.txt Sun Mar 24 14:33:57 2024 -0400 @@ -1,10 +1,10 @@ enable_language(Pascal) find_package(SDL2 REQUIRED CONFIG) -find_package(SDL2_image 2 REQUIRED) -find_package(SDL2_net 2 REQUIRED) -find_package(SDL2_ttf 2 REQUIRED) -find_package(SDL2_mixer 2 REQUIRED) +find_package(SDL2_image REQUIRED CONFIG) +find_package(SDL2_net REQUIRED CONFIG) +find_package(SDL2_ttf REQUIRED CONFIG) +find_package(SDL2_mixer REQUIRED CONFIG) include(CheckLibraryExists) include(${CMAKE_MODULE_PATH}/utils.cmake) @@ -22,6 +22,12 @@ endif() endif(UNIX) +# FPC 3.2.2 does not create s COFF file for the engine icon, but still includes it +# in the list of files to be linked, leading to a linking failure +if(${CMAKE_Pascal_COMPILER_VERSION} VERSION_GREATER_EQUAL 3.2) + add_flag_append(CMAKE_Pascal_FLAGS "-dSKIP_RESOURCES") +endif() + # convert list into pascal array if(FONTS_DIRS) list(LENGTH FONTS_DIRS ndirs) @@ -168,8 +174,18 @@ endif() # PhysFS -get_filename_component(PHYSFS_LIBRARY_DIR ${PHYSFS_LIBRARY} PATH) -add_flag_append(CMAKE_Pascal_FLAGS "-Fl${PHYSFS_LIBRARY}") +if (DEFINED PHYSFS_LIBRARY_RELEASE) + if(${USE_DEBUG_LIBRARIES}) + get_filename_component(PHYSFS_LIBRARY_DIR ${PHYSFS_LIBRARY_DEBUG} PATH) + add_flag_append(CMAKE_Pascal_FLAGS "-Fl${PHYSFS_LIBRARY_DEBUG}") + else() + get_filename_component(PHYSFS_LIBRARY_DIR ${PHYSFS_LIBRARY_RELEASE} PATH) + add_flag_append(CMAKE_Pascal_FLAGS "-Fl${PHYSFS_LIBRARY_RELEASE}") + endif() +else() + get_filename_component(PHYSFS_LIBRARY_DIR ${PHYSFS_LIBRARY} PATH) + add_flag_append(CMAKE_Pascal_FLAGS "-Fl${PHYSFS_LIBRARY}") +endif() list(APPEND HW_LINK_LIBS physlayer) diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/hwengine.pas --- a/hedgewars/hwengine.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/hwengine.pas Sun Mar 24 14:33:57 2024 -0400 @@ -19,8 +19,10 @@ {$INCLUDE "options.inc"} {$IFDEF WINDOWS} +{$IFNDEF SKIP_RESOURCES} {$R res/hwengine.rc} {$ENDIF} +{$ENDIF} {$IFDEF HWLIBRARY} unit hwengine; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uAI.pas --- a/hedgewars/uAI.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uAI.pas Sun Mar 24 14:33:57 2024 -0400 @@ -100,15 +100,36 @@ procedure TestAmmos(var Actions: TActions; Me: PGear; rareChecks: boolean); var BotLevel: Byte; ap: TAttackParams; - Score, i, t, n, dAngle: LongInt; + Score, i, l, t, n, dAngle: LongInt; a, aa: TAmmoType; - useThisActions: boolean; + useThisActions, hasLowGrav: boolean; begin BotLevel:= Me^.Hedgehog^.BotLevel; -windSpeed:= hwFloat2Float(cWindSpeed); +aiWindSpeed:= hwFloat2Float(cWindSpeed); +aiLaserSighting:= (cLaserSighting) or (HHHasAmmo(Me^.Hedgehog^, amLaserSight) > 0); +aiGravity:= cGravity; +aiGravityf:= cGravityf; +aiHogsInTeam:= CountHogsInTeam(Me, true); +hasLowGrav:= HHHasAmmo(Me^.Hedgehog^, amLowGravity) > 0; useThisActions:= false; Me^.AIHints:= Me^.AIHints and (not aihAmmosChanged); +for l:= 0 to 1 do // 0 = test with normal gravity. 1 = test with low gravity +if (l = 0) or ((hasLowGrav) and (not cLowGravity)) then +begin +// simulate normal or low gravity +if (l = 0) then + begin + aiGravity:= cGravity; + aiGravityf:= cGravityf; + end +else if (l = 1) then + begin + // We calculate the values to better support scripts like Gravity. + // This might have a slight inaccuracy, but none have been spotted in testing. + aiGravity:= cGravity / _2; + aiGravityf:= cGravityf / 2; + end; for i:= 0 to Pred(Targets.Count) do if (Targets.ar[i].Score >= 0) and (not StopThinking) then begin @@ -120,10 +141,11 @@ if (CanUseAmmo[a]) and ((not rareChecks) or ((AmmoTests[a].flags and amtest_Rare) = 0)) and ((i = 0) or ((AmmoTests[a].flags and amtest_NoTarget) = 0)) + and ((l = 0) or ((AmmoTests[a].flags and amtest_NoLowGravity) = 0)) then begin {$HINTS OFF} - Score:= AmmoTests[a].proc(Me, Targets.ar[i], BotLevel, ap); + Score:= AmmoTests[a].proc(Me, Targets.ar[i], BotLevel, ap, AmmoTests[a].flags); {$HINTS ON} if (Score > BadTurn) and (Actions.Score + Score > BestActions.Score) then if (BestActions.Score < 0) or (Actions.Score + Score > BestActions.Score + Byte(BotLevel - 1) * 2048) then @@ -141,23 +163,36 @@ BestActions.Score:= Actions.Score + Score; - // if not between shots, activate invulnerability/vampirism if available + // if not between shots, activate invulnerability/vampirism/etc. if available if CurrentHedgehog^.MultiShootAttacks = 0 then begin - if (HHHasAmmo(Me^.Hedgehog^, amInvulnerable) > 0) and (Me^.Hedgehog^.Effects[heInvulnerable] = 0) then + if (not cLaserSighting) and (HHHasAmmo(Me^.Hedgehog^, amLaserSight) > 0) and ((AmmoTests[a].flags and amtest_LaserSight) <> 0) then + begin + AddAction(BestActions, aia_Weapon, Longword(amLaserSight), 80, 0, 0); + AddAction(BestActions, aia_attack, aim_push, 10, 0, 0); + AddAction(BestActions, aia_attack, aim_release, 10, 0, 0); + end; + if ((AmmoTests[a].flags and amtest_NoInvulnerable) = 0) and + (HHHasAmmo(Me^.Hedgehog^, amInvulnerable) > 0) and (Me^.Hedgehog^.Effects[heInvulnerable] = 0) then begin AddAction(BestActions, aia_Weapon, Longword(amInvulnerable), 80, 0, 0); AddAction(BestActions, aia_attack, aim_push, 10, 0, 0); AddAction(BestActions, aia_attack, aim_release, 10, 0, 0); end; - + if (l = 1) and (hasLowGrav) then + begin + AddAction(BestActions, aia_Weapon, Longword(amLowGravity), 80, 0, 0); + AddAction(BestActions, aia_attack, aim_push, 10, 0, 0); + AddAction(BestActions, aia_attack, aim_release, 10, 0, 0); + end; if (HHHasAmmo(Me^.Hedgehog^, amExtraDamage) > 0) and (cDamageModifier <> _1_5) then begin AddAction(BestActions, aia_Weapon, Longword(amExtraDamage), 80, 0, 0); AddAction(BestActions, aia_attack, aim_push, 10, 0, 0); AddAction(BestActions, aia_attack, aim_release, 10, 0, 0); end; - if (HHHasAmmo(Me^.Hedgehog^, amVampiric) > 0) and (not cVampiric) then + if (not cVampiric) and ((AmmoTests[a].flags and amtest_NoVampiric) = 0) and + (HHHasAmmo(Me^.Hedgehog^, amVampiric) > 0) then begin AddAction(BestActions, aia_Weapon, Longword(amVampiric), 80, 0, 0); AddAction(BestActions, aia_attack, aim_push, 10, 0, 0); @@ -165,21 +200,28 @@ end; end; + if (ap.Angle > 0) then + AddAction(BestActions, aia_LookRight, 0, 200, 0, 0) + else if (ap.Angle < 0) then + AddAction(BestActions, aia_LookLeft, 0, 200, 0, 0); + AddAction(BestActions, aia_Weapon, Longword(a), 300 + random(400), 0, 0); + if (Ammoz[a].Ammo.Propz and ammoprop_Timerable) <> 0 then + AddAction(BestActions, aia_Timer, ap.Time div 1000, 400, 0, 0); + + if ((Ammoz[a].Ammo.Propz and ammoprop_SetBounce) > 0) and (ap.Bounce > 0) then + begin + AddAction(BestActions, aia_Precise, aim_push, 10, 0, 0); + AddAction(BestActions, aia_Timer, ap.Bounce, 200, 0, 0); + AddAction(BestActions, aia_Precise, aim_release, 10, 0, 0); + end; + if (Ammoz[a].Ammo.Propz and ammoprop_NeedTarget) <> 0 then begin AddAction(BestActions, aia_Put, 0, 8, ap.AttackPutX, ap.AttackPutY) end; - if (ap.Angle > 0) then - AddAction(BestActions, aia_LookRight, 0, 200, 0, 0) - else if (ap.Angle < 0) then - AddAction(BestActions, aia_LookLeft, 0, 200, 0, 0); - - if (Ammoz[a].Ammo.Propz and ammoprop_Timerable) <> 0 then - AddAction(BestActions, aia_Timer, ap.Time div 1000, 400, 0, 0); - if (Ammoz[a].Ammo.Propz and ammoprop_NoCrosshair) = 0 then begin dAngle:= LongInt(Me^.Angle) - Abs(ap.Angle); @@ -216,12 +258,28 @@ n:= 1 else n:= ap.AttacksNum; AddAction(BestActions, aia_attack, aim_push, 650 + random(300), 0, 0); + if (a = amResurrector) and (BotLevel < 4) then + AddAction(BestActions, aia_Up, aim_push, 1, 0, 0); for t:= 2 to n do begin AddAction(BestActions, aia_attack, aim_push, 150, 0, 0); AddAction(BestActions, aia_attack, aim_release, ap.Power, 0, 0); end; - AddAction(BestActions, aia_attack, aim_release, ap.Power, 0, 0); + if (a = amResurrector) and (BotLevel < 4) then + begin + AddAction(BestActions, aia_Up, aim_release, ap.Power, 0, 0); + AddAction(BestActions, aia_attack, aim_release, 0, 0, 0); + end + else + AddAction(BestActions, aia_attack, aim_release, ap.Power, 0, 0); + + // Just for fun: 0.01% chance for kamikaze with "wishes" ;-) + if (a = amKamikaze) and (random(10000) = 0) then + begin + AddAction(BestActions, aia_Switch, 0, 1, 0, 0); + AddAction(BestActions, aia_Precise, aim_push, 1, 0, 0); + AddAction(BestActions, aia_Precise, aim_release, 5000, 0, 0); + end; end; if (Ammoz[a].Ammo.Propz and ammoprop_Track) <> 0 then @@ -242,12 +300,15 @@ or StopThinking end end; +aiGravity:= cGravity; +aiGravityf:= cGravityf; +end; procedure Walk(Me: PGear; var Actions: TActions); const FallPixForBranching = cHHRadius; var maxticks, oldticks, steps, tmp: Longword; - BaseRate, BestRate, Rate: LongInt; + BaseRate, BestRate, Rate, i: LongInt; GoInfo: TGoInfo; CanGo: boolean; AltMe: TGear; @@ -278,12 +339,12 @@ BestRate:= RatePlace(Me); BaseRate:= Max(BestRate, 0); -// switch to 'skip' if we cannot move because of mouse cursor being shown +// unselect weapon if we cannot move because of mouse cursor being shown if (Ammoz[Me^.Hedgehog^.CurAmmoType].Ammo.Propz and ammoprop_NeedTarget) <> 0 then - AddAction(Actions, aia_Weapon, Longword(amSkip), 100 + random(200), 0, 0); + AddAction(Actions, aia_Weapon, Longword(amNothing), 100 + random(200), 0, 0); if ((CurrentHedgehog^.MultiShootAttacks = 0) or ((Ammoz[Me^.Hedgehog^.CurAmmoType].Ammo.Propz and ammoprop_NoMoveAfter) = 0)) - and (CurrentHedgehog^.Effects[heArtillery] = 0) and (cGravityf <> 0) then + and (CurrentHedgehog^.Effects[heArtillery] = 0) and (aiGravityf <> 0) then begin tmp:= random(2) + 1; Push(Actions, Me^, tmp); @@ -321,6 +382,13 @@ AddAction(BestActions, aia_Weapon, Longword(amExtraTime), 80, 0, 0); AddAction(BestActions, aia_attack, aim_push, 10, 0, 0); AddAction(BestActions, aia_attack, aim_release, 10, 0, 0); + // Better bot levels know they can spam extra time if infinite + if (BotLevel < 3) and (HHHasAmmo(Me^.Hedgehog^, amExtraTime) = AMMO_INFINITE) then + for i:= 1 to 3 do + begin + AddAction(BestActions, aia_attack, aim_push, 100, 0, 0); + AddAction(BestActions, aia_attack, aim_release, 100, 0, 0); + end; end; break; @@ -422,6 +490,8 @@ Actions: TActions; begin dmgMod:= 0.01 * hwFloat2Float(cDamageModifier) * cDamagePercent; +aiGravity:= cGravity; +aiGravityf:= cGravityf; StartTicks:= GameTicks; currHedgehogIndex:= CurrentTeam^.CurrHedgehog; @@ -492,7 +562,14 @@ Me^.AIHints := ME^.AIHints and (not aihAmmosChanged); end; - end else SDL_Delay(100) + end + else + begin + // No target found, skip turn + BestActions.Count:= 0; + AddAction(BestActions, aia_Skip, 0, 250, 0, 0); + Me^.AIHints := ME^.AIHints and (not aihAmmosChanged); + end else begin BackMe:= Me^; @@ -545,11 +622,6 @@ ThinkingHH:= Me; FillTargets; -if Targets.Count = 0 then - begin - OutError('AI: no targets!?', false); - exit - end; FillBonuses(((Me^.State and gstAttacked) <> 0) and (not isInMultiShoot) and ((GameFlags and gfInfAttack) = 0)); diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uAIActions.pas --- a/hedgewars/uAIActions.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uAIActions.pas Sun Mar 24 14:33:57 2024 -0400 @@ -31,6 +31,7 @@ aia_Up = 5; aia_Down = 6; aia_Switch = 7; + aia_Precise = 8; aia_Weapon = $8000; aia_WaitXL = $8001; @@ -73,7 +74,7 @@ var PrevX: LongInt = 0; timedelta: Longword = 0; -const ActionIdToStr: array[0..7] of string[16] = ( +const ActionIdToStr: array[0..8] of string[16] = ( {aia_none} '', {aia_Left} 'left', {aia_Right} 'right', @@ -81,7 +82,8 @@ {aia_attack} 'attack', {aia_Up} 'up', {aia_Down} 'down', -{aia_Switch} 'switch' +{aia_Switch} 'switch', +{aia_Precise} 'precise' ); {$IFDEF TRACEAIACTIONS} diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uAIAmmoTests.pas --- a/hedgewars/uAIAmmoTests.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uAIAmmoTests.pas Sun Mar 24 14:33:57 2024 -0400 @@ -25,39 +25,60 @@ amtest_Rare = $00000001; // check only several positions amtest_NoTarget = $00000002; // each pos, but no targetting amtest_MultipleAttacks = $00000004; // test could result in multiple attacks, set AttacksNum + amtest_NoTrackFall = $00000008; // skip fall tracing. + amtest_LaserSight = $00000010; // supports laser sighting + amtest_NoVampiric = $00000020; // don't use vampirism with this ammo + amtest_NoInvulnerable = $00000040; // don't use invulnerable with this with ammo + amtest_NoLowGravity = $00000080; // don't use low gravity with this with ammo -var windSpeed: real; +var aiWindSpeed: real; + aiGravity: hwFloat; + aiGravityf: real; + aiLaserSighting: boolean; + aiHogsInTeam: LongInt; type TAttackParams = record - Time, AttacksNum: Longword; + Time, Bounce, AttacksNum: Longword; Angle, Power: LongInt; ExplX, ExplY, ExplR: LongInt; AttackPutX, AttackPutY: LongInt; end; -function TestBazooka(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestBee(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestSnowball(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestGrenade(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestMolotov(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestClusterBomb(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestWatermelon(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestDrillRocket(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestMortar(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestShotgun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestDesertEagle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestSniperRifle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestBaseballBat(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestFirePunch(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestWhip(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestKamikaze(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestAirAttack(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestTeleport(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestHammer(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestCake(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -function TestDynamite(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestBazooka(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestBee(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestSnowball(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestGrenade(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestMolotov(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestClusterBomb(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestWatermelon(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestDrillRocket(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestRCPlane(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestMortar(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestShotgun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestDesertEagle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestSniperRifle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestBaseballBat(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestFirePunch(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestWhip(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestKamikaze(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestAirAttack(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestDrillStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestMineStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestSineGun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestSMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestPiano(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestTeleport(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestHammer(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestResurrector(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestCake(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestSeduction(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestDynamite(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestKnife(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestAirMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +function TestMinigun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; -type TAmmoTestProc = function (Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +type TAmmoTestProc = function (Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; TAmmoTest = record proc: TAmmoTestProc; flags: Longword; @@ -70,34 +91,34 @@ (proc: @TestClusterBomb; flags: 0), // amClusterBomb (proc: @TestBazooka; flags: 0), // amBazooka (proc: @TestBee; flags: amtest_Rare), // amBee - (proc: @TestShotgun; flags: 0), // amShotgun + (proc: @TestShotgun; flags: amtest_LaserSight), // amShotgun (proc: nil; flags: 0), // amPickHammer (proc: nil; flags: 0), // amSkip (proc: nil; flags: 0), // amRope - (proc: nil; flags: 0), // amMine - (proc: @TestDesertEagle; flags: amtest_MultipleAttacks), // amDEagle + (proc: @TestMine; flags: amtest_NoTarget), // amMine + (proc: @TestDesertEagle; flags: amtest_MultipleAttacks or amtest_LaserSight), // amDEagle (proc: @TestDynamite; flags: amtest_NoTarget), // amDynamite (proc: @TestFirePunch; flags: amtest_NoTarget), // amFirePunch - (proc: @TestWhip; flags: amtest_NoTarget), // amWhip - (proc: @TestBaseballBat; flags: amtest_NoTarget), // amBaseballBat + (proc: @TestWhip; flags: amtest_NoTarget or amtest_NoInvulnerable), // amWhip + (proc: @TestBaseballBat; flags: amtest_NoTarget or amtest_NoInvulnerable), // amBaseballBat (proc: nil; flags: 0), // amParachute (proc: @TestAirAttack; flags: amtest_Rare), // amAirAttack - (proc: nil; flags: 0), // amMineStrike + (proc: @TestMineStrike; flags: amtest_Rare), // amMineStrike (proc: nil; flags: 0), // amBlowTorch (proc: nil; flags: 0), // amGirder (proc: nil; flags: 0), // amTeleport //(proc: @TestTeleport; flags: amtest_OnTurn), // amTeleport (proc: nil; flags: 0), // amSwitch (proc: @TestMortar; flags: 0), // amMortar - (proc: @TestKamikaze; flags: 0), // amKamikaze + (proc: @TestKamikaze; flags: amtest_LaserSight or amtest_NoInvulnerable or amtest_NoVampiric), // amKamikaze (proc: @TestCake; flags: amtest_Rare or amtest_NoTarget), // amCake - (proc: nil; flags: 0), // amSeduction + (proc: @TestSeduction; flags: amtest_NoTarget), // amSeduction (proc: @TestWatermelon; flags: 0), // amWatermelon (proc: nil; flags: 0), // amHellishBomb (proc: nil; flags: 0), // amNapalm (proc: @TestDrillRocket; flags: 0), // amDrill (proc: nil; flags: 0), // amBallgun - (proc: nil; flags: 0), // amRCPlane + (proc: @TestRCPlane; flags: amtest_LaserSight), // amRCPlane (proc: nil; flags: 0), // amLowGravity (proc: nil; flags: 0), // amExtraDamage (proc: nil; flags: 0), // amInvulnerable @@ -109,23 +130,24 @@ (proc: @TestMolotov; flags: 0), // amMolotov (proc: nil; flags: 0), // amBirdy (proc: nil; flags: 0), // amPortalGun - (proc: nil; flags: 0), // amPiano - (proc: @TestGrenade; flags: 0), // amGasBomb - (proc: @TestShotgun; flags: 0), // amSineGun + (proc: @TestPiano; flags: amtest_Rare or amtest_NoInvulnerable or amtest_NoVampiric), // amPiano + (proc: @TestGrenade; flags: amtest_NoTrackFall), // amGasBomb + (proc: @TestSineGun; flags: amtest_NoVampiric), // amSineGun (proc: nil; flags: 0), // amFlamethrower - (proc: @TestGrenade; flags: 0), // amSMine - (proc: @TestHammer; flags: amtest_NoTarget), // amHammer - (proc: nil; flags: 0), // amResurrector - (proc: nil; flags: 0), // amDrillStrike - (proc: nil; flags: 0), // amSnowball + (proc: @TestSMine; flags: 0), // amSMine + (proc: @TestHammer; flags: amtest_NoTarget or amtest_NoInvulnerable), // amHammer + (proc: @TestResurrector; flags: amtest_NoTarget or amtest_NoInvulnerable or amtest_NoVampiric or amtest_NoLowGravity), // amResurrector + (proc: @TestDrillStrike; flags: amtest_Rare), // amDrillStrike + (proc: @TestSnowball; flags: amtest_NoInvulnerable or amtest_NoVampiric), // amSnowball (proc: nil; flags: 0), // amTardis (proc: nil; flags: 0), // amLandGun (proc: nil; flags: 0), // amIceGun - (proc: nil; flags: 0), // amKnife + (proc: @TestKnife; flags: 0), // amKnife (proc: nil; flags: 0), // amRubber - (proc: nil; flags: 0), // amAirMine + (proc: @TestAirMine; flags: amtest_LaserSight), // amAirMine (proc: nil; flags: 0), // amCreeper - (proc: @TestShotgun; flags: 0) // amMinigun + (proc: @TestMinigun; flags: amtest_LaserSight), // amMinigun + (proc: nil; flags: 0) // amSentry ); implementation @@ -136,7 +158,7 @@ Metric:= abs(x1 - x2) + abs(y1 - y2) end; -function TestBazooka(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestBazooka(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const cExtraTime = 300; var Vx, Vy, r, mX, mY: real; rTime: LongInt; @@ -146,6 +168,7 @@ t: LongInt; value: LongInt; begin +Flags:= Flags; // avoid compiler hint mX:= hwFloat2Float(Me^.X); mY:= hwFloat2Float(Me^.Y); ap.Time:= 0; @@ -159,9 +182,9 @@ repeat rTime:= rTime + 300 + Level * 50 + random(300); if (WorldEdge = weWrap) and (random(2)=0) then - Vx:= - windSpeed * rTime * 0.5 + (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - mX) / rTime - else Vx:= - windSpeed * rTime * 0.5 + (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - mX) / rTime; - Vy:= cGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - mY) / rTime; + Vx:= - aiWindSpeed * rTime * 0.5 + (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - mX) / rTime + else Vx:= - aiWindSpeed * rTime * 0.5 + (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - mX) / rTime; + Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - mY) / rTime; r:= sqr(Vx) + sqr(Vy); if not (r > 1) then begin @@ -175,9 +198,9 @@ x:= x + dX; y:= y + dY; - dX:= dX + windSpeed; + dX:= dX + aiWindSpeed; //dX:= CheckBounce(x,dX); - dY:= dY + cGravityf; + dY:= dY + aiGravityf; dec(t) until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t < -cExtraTime); @@ -222,7 +245,7 @@ repeat x:= x + dx; y:= y + dy; - dy:= dy + cGravityf; + dy:= dy + aiGravityf; f:= ((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5)); dec(t) @@ -267,12 +290,13 @@ calcBeeFlight:= BadTurn end; -function TestBee(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestBee(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var i, j: LongInt; valueResult, v, a, p: LongInt; mX, mY: real; eX, eY: LongInt; begin +Flags:= Flags; // avoid compiler hint if Level > 1 then exit(BadTurn); @@ -320,7 +344,7 @@ TestBee:= BadTurn // no digging end; -function TestDrillRocket(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestDrillRocket(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var Vx, Vy, r, mX, mY: real; rTime: LongInt; EX, EY: LongInt; @@ -331,6 +355,7 @@ t2: real; timer: Longint; begin +Flags:= Flags; // avoid compiler hint if (Level > 3) then exit(BadTurn); mX:= hwFloat2Float(Me^.X); @@ -347,9 +372,9 @@ repeat rTime:= rTime + 300 + Level * 50 + random(300); if (WorldEdge = weWrap) and (random(2)=0) then - Vx:= - windSpeed * rTime * 0.5 + (targXWrap + AIrndSign(2) - mX) / rTime - else Vx:= - windSpeed * rTime * 0.5 + (Targ.Point.X + AIrndSign(2) - mX) / rTime; - Vy:= cGravityf * rTime * 0.5 - (Targ.Point.Y - 35 - mY) / rTime; + Vx:= - aiWindSpeed * rTime * 0.5 + (targXWrap + AIrndSign(2) - mX) / rTime + else Vx:= - aiWindSpeed * rTime * 0.5 + (Targ.Point.X + AIrndSign(2) - mX) / rTime; + Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y - 35 - mY) / rTime; r:= sqr(Vx) + sqr(Vy); if not (r > 1) then begin @@ -362,8 +387,8 @@ x:= CheckWrap(x); x:= x + dX; y:= y + dY; - dX:= dX + windSpeed; - dY:= dY + cGravityf; + dX:= dX + aiWindSpeed; + dY:= dY + aiGravityf; dec(t) until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (y > cWaterLine); @@ -389,10 +414,12 @@ EX:= trunc(x); EY:= trunc(y); // Try to prevent AI from thinking firing into water will cause a drowning - if (EY < cWaterLine-5) and (timer > 0) and (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) > 21) then exit(BadTurn); - if Level = 1 then + if (EY < cWaterLine-5) and (timer > 0) and (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) > 21) then + value:= BadTurn + else if Level = 1 then value:= RateExplosion(Me, EX, EY, 101, afTrackFall or afErasesLand) - else value:= RateExplosion(Me, EX, EY, 101); + else + value:= RateExplosion(Me, EX, EY, 101); if valueResult <= value then begin ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9)); @@ -407,8 +434,91 @@ TestDrillRocket:= valueResult end; +function TestRCPlane(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const + MIN_RANGE = 200; +var Vx, Vy, meX, meY, x, y: real; + rx, ry, valueResult: LongInt; + range, maxRange: integer; +begin +// This is a very simple test to let a RC plane fly in a straight line, without dropping any bombs +// TODO: Teach AI how to steer +// TODO: Teach AI how to drop bombs +// TODO: Teach AI how to predict fire behavior +Flags:= Flags; // avoid compiler hint +if Level = 5 then + exit(BadTurn) +else if Level = 4 then + maxRange:= 2200 +else if Level = 3 then + maxRange:= 2900 +else if Level = 2 then + maxRange:= 3500 +else + maxRange:= 3900; +TestRCPlane:= BadTurn; +ap.ExplR:= 0; +ap.Time:= 0; +ap.Power:= 1; +meX:= hwFloat2Float(Me^.X); +meY:= hwFloat2Float(Me^.Y); +x:= meX; +y:= meY; +range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y); +if ( range < MIN_RANGE ) or ( range > maxRange) then + exit(BadTurn); -function TestSnowball(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +Vx:= (Targ.Point.X - x) * 1 / 1024; +Vy:= (Targ.Point.Y - y) * 1 / 1024; +ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +repeat + x:= x + vX; + y:= y + vY; + rx:= trunc(x); + ry:= trunc(y); + if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 8)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 8)) then + begin + x:= x + vX * 8; + y:= y + vY * 8; + + // Intentionally low rating to discourage use + if Level = 1 then + valueResult:= RateExplosion(Me, rx, ry, 26, afTrackFall or afErasesLand) + else + valueResult:= RateExplosion(Me, rx, ry, 26); + + // Check range again in case the plane collided before target + range:= Metric(trunc(meX), trunc(meY), rx, ry); + if ( range < MIN_RANGE ) or ( range > maxRange) then + exit(BadTurn); + + // If impact location is close, above us and wind blows in our direction, + // there's a risk of fire flying towards us, so fail in this case. + if (Level < 3) and (range <= 600) and (meY >= ry) and + (((ap.Angle < 0) and (aiWindSpeed > 0)) or ((ap.Angle > 0) and (aiWindSpeed < 0))) then + exit(BadTurn); + + // Apply inaccuracy + if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 9))); + + if (valueResult <= 0) then + valueResult:= BadTurn; + exit(valueResult) + end +until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4) + or (x < 0) + or (y < 0) + or (trunc(x) > LAND_WIDTH) + or (trunc(y) > LAND_HEIGHT); + +TestRCPlane:= BadTurn +end; + +function TestSnowball(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const timeLimit = 5000; + Density : real = 0.5; var Vx, Vy, r: real; rTime: LongInt; EX, EY: LongInt; @@ -418,6 +528,7 @@ value: LongInt; begin +Flags:= Flags; // avoid compiler hint meX:= hwFloat2Float(Me^.X); meY:= hwFloat2Float(Me^.Y); ap.Time:= 0; @@ -427,14 +538,21 @@ if (WorldEdge = weWrap) then if (Targ.Point.X < meX) then targXWrap:= Targ.Point.X + (RightX-LeftX) - else targXWrap:= Targ.Point.X - (RightX-LeftX); + else + targXWrap:= Targ.Point.X - (RightX-LeftX); repeat - rTime:= rTime + 300 + Level * 50 + random(1000); + rTime:= rTime + 300 + Level * 50 + random(300); if (WorldEdge = weWrap) and (random(2)=0) then - Vx:= - windSpeed * rTime * 0.5 + ((targXWrap + AIrndSign(2)) - meX) / rTime - else Vx:= - windSpeed * rTime * 0.5 + ((Targ.Point.X + AIrndSign(2)) - meX) / rTime; - Vy:= cGravityf * rTime * 0.5 - (Targ.Point.Y - meY) / rTime; + Vx:= (targXWrap - meX) / rTime + else + Vx:= (Targ.Point.X - meX) / rTime; + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx + else + Vx:= -aiWindSpeed * rTime * 0.5 + Vx; + Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y - meY) / rTime; r:= sqr(Vx) + sqr(Vy); + if not (r > 1) then begin x:= meX; @@ -445,102 +563,150 @@ repeat x:= CheckWrap(x); x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density + else + dX:= dX + aiWindSpeed; + y:= y + dY; - dX:= dX + windSpeed; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; dec(t) - until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or - ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t <= 0); + until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 4)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 4))) or (trunc(y) > cWaterLine) or (t < -timeLimit); + EX:= trunc(x); EY:= trunc(y); - value:= RateShove(Me, trunc(x), trunc(y), 5, 1, trunc((abs(dX)+abs(dY))*20), -dX, -dY, afTrackFall); - // LOL copypasta: this is score for digging with... snowball - //if value = 0 then - // value:= - Metric(Targ.Point.X, Targ.Point.Y, EX, EY) div 64; + // Sanity check: Make sure we're not too close to impact location + if (Metric(trunc(meX), trunc(meY), EX, EY) <= 40) then + value:= BadTurn + // Rate attack + else if (t >= -timeLimit) and (EY <= cWaterLine) then + // radius intentionally set to 16 for shove because lower values don't work reliably + value:= RateShove(Me, EX, EY, 16, 0, trunc((abs(dX)+abs(dY))*20), dX, dY, afTrackFall) + else + value:= BadTurn; - if valueResult <= value then + if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then + value := BadTurn; + + if (valueResult < value) or ((valueResult = value) and (Level = 1)) then begin - ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9)); - ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1); - ap.ExplR:= 0; + ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 12)); + ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 22 + 1); ap.ExplX:= EX; ap.ExplY:= EY; valueResult:= value end; - end -until (rTime > 5050 - Level * 800); + end +until rTime > 5050 - Level * 800; TestSnowball:= valueResult end; -function TestMolotov(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -var Vx, Vy, r: real; - Score, EX, EY, valueResult: LongInt; - TestTime: LongInt; - targXWrap, x, y, dY, meX, meY: real; +function TestMolotov(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const timeLimit = 50; + Density : real = 2.0; +var Vx, Vy, r, meX, meY: real; + rTime: LongInt; + EX, EY: LongInt; + valueResult: LongInt; + targXWrap, x, y, dX, dY: real; t: LongInt; + value, range: LongInt; begin +Flags:= Flags; // avoid compiler hint meX:= hwFloat2Float(Me^.X); meY:= hwFloat2Float(Me^.Y); -valueResult:= BadTurn; -TestTime:= 0; +ap.Time:= 0; +rTime:= 350; ap.ExplR:= 0; if (WorldEdge = weWrap) then if (Targ.Point.X < meX) then targXWrap:= Targ.Point.X + (RightX-LeftX) else targXWrap:= Targ.Point.X - (RightX-LeftX); +valueResult:= BadTurn; repeat - inc(TestTime, 300); + rTime:= rTime + 300 + Level * 50 + random(300); if (WorldEdge = weWrap) and (random(2)=0) then - Vx:= (targXWrap - meX) / TestTime - else Vx:= (Targ.Point.X - meX) / TestTime; - Vy:= cGravityf * (TestTime div 2) - Targ.Point.Y - meY / TestTime; + Vx:= (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime + else + Vx:= (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime; + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx; + Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - meY) / rTime; r:= sqr(Vx) + sqr(Vy); + if not (r > 1) then begin x:= meX; y:= meY; + dX:= Vx; dY:= -Vy; - t:= TestTime; + t:= rTime; repeat x:= CheckWrap(x); - x:= x + Vx; + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; + y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; dec(t) - until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 6)) or - ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 6))) or (t = 0); + until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t < -timeLimit); + EX:= trunc(x); EY:= trunc(y); - if t < 50 then - Score:= RateExplosion(Me, EX, EY, 97) // average of 17 attempts, most good, but some failing spectacularly - else - Score:= BadTurn; + range:= Metric(trunc(meX), trunc(meY), EX, EY); - if valueResult < Score then + // Sanity check 1: Make sure we've hit a hedgehog or object + if not TestCollHogsOrObjects(EX, EY, 5) then + value:= BadTurn + // Sanity check 2: Make sure we're not too close to impact location + else if (range < 150) and (Level < 5) then + value:= BadTurn + // Sanity check 3: If impact location is close, above us and wind blows + // towards us, there's a risk of fire flying towards us, so fail in this case. + else if (Level < 3) and (range <= 1000) and (trunc(meY) >= EY) and + ((ap.Angle < 0) <> (aiWindSpeed < 0)) then + value:= BadTurn + // Timeout + else if t < -timeLimit then + value:= BadTurn + else + // Valid hit! + // Weapon does not actually explode, so this rating is an approximation + value:= RateExplosion(Me, EX, EY, 97); // average of 17 attempts, most good, but some failing spectacularly + + if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then + value := BadTurn; + + if (valueResult < value) or ((valueResult = value) and (Level < 3)) then begin - ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random(Level)); - ap.Power:= trunc(sqrt(r) * cMaxPower) + AIrndSign(random(Level) * 15); + ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9)); + ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1); ap.ExplR:= 100; ap.ExplX:= EX; ap.ExplY:= EY; - valueResult:= Score + valueResult:= value end; end -until (TestTime > 5050 - Level * 800); +until rTime > 5050 - Level * 800; TestMolotov:= valueResult end; -function TestGrenade(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestGrenade(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const tDelta = 24; + Density : real = 1.5; var Vx, Vy, r: real; Score, EX, EY, valueResult: LongInt; TestTime: LongInt; - targXWrap, x, y, meX, meY, dY: real; + targXWrap, x, y, meX, meY, dX, dY: real; t: LongInt; begin valueResult:= BadTurn; TestTime:= 0; +ap.Bounce:= 0; ap.ExplR:= 0; meX:= hwFloat2Float(Me^.X); meY:= hwFloat2Float(Me^.Y); @@ -552,27 +718,33 @@ inc(TestTime, 1000); if (WorldEdge = weWrap) and (random(2)=0) then Vx:= (targXWrap + AIrndOffset(Targ, Level) - meX) / (TestTime + tDelta) - else Vx:= (Targ.Point.X + AIrndOffset(Targ, Level) - meX) / (TestTime + tDelta); - Vy:= cGravityf * ((TestTime + tDelta) div 2) - (Targ.Point.Y - meY) / (TestTime + tDelta); + else + Vx:= (Targ.Point.X + AIrndOffset(Targ, Level) - meX) / (TestTime + tDelta); + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * (TestTime + tDelta) * 0.5 + Vx; + Vy:= aiGravityf * ((TestTime + tDelta) div 2) - (Targ.Point.Y - meY) / (TestTime + tDelta); r:= sqr(Vx) + sqr(Vy); if not (r > 1) then begin x:= meX; y:= meY; + dX:= Vx; dY:= -Vy; t:= TestTime; repeat x:= CheckWrap(x); - x:= x + Vx; + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; dec(t) until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t = 0); EX:= trunc(x); EY:= trunc(y); if t < 50 then - if Level = 1 then + if (Level = 1) and (Flags and amtest_NoTrackFall = 0) then Score:= RateExplosion(Me, EX, EY, 101, afTrackFall or afErasesLand) else Score:= RateExplosion(Me, EX, EY, 101) else @@ -594,38 +766,46 @@ TestGrenade:= valueResult end; -function TestClusterBomb(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestClusterBomb(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const tDelta = 24; + Density : real = 1.5; var Vx, Vy, r: real; Score, EX, EY, valueResult: LongInt; TestTime: Longword; - x, y, dY, meX, meY: real; + x, y, dX, dY, meX, meY: real; t: LongInt; begin +Flags:= Flags; // avoid compiler hint valueResult:= BadTurn; TestTime:= 500; +ap.Bounce:= 0; ap.ExplR:= 0; meX:= hwFloat2Float(Me^.X); meY:= hwFloat2Float(Me^.Y); repeat inc(TestTime, 900); + if (GameFlags and gfMoreWind) <> 0 then + Vx:= (-(aiWindSpeed / Density) * (TestTime + tDelta) * 0.5) + ((Targ.Point.X - meX) / (TestTime + tDelta)) // Try to overshoot slightly, seems to pay slightly better dividends in terms of hitting cluster - if meX 1) then begin x:= meX; + dX:= Vx; y:= meY; dY:= -Vy; t:= TestTime; repeat - x:= x + Vx; + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; dec(t) until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 5)) or ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 5))) or (t = 0); @@ -651,14 +831,16 @@ TestClusterBomb:= valueResult end; -function TestWatermelon(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestWatermelon(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const tDelta = 24; + Density : real = 2.0; var Vx, Vy, r: real; Score, EX, EY, valueResult: LongInt; TestTime: Longword; - targXWrap, x, y, dY, meX, meY: real; + targXWrap, x, y, dX, dY, meX, meY: real; t: LongInt; begin +Flags:= Flags; // avoid compiler hint valueResult:= BadTurn; TestTime:= 500; ap.ExplR:= 0; @@ -671,21 +853,28 @@ repeat inc(TestTime, 900); if (WorldEdge = weWrap) and (random(2)=0) then - Vx:= (targXWrap - meX) / (TestTime + tDelta) - else Vx:= (Targ.Point.X - meX) / (TestTime + tDelta); - Vy:= cGravityf * ((TestTime + tDelta) div 2) - ((Targ.Point.Y-50) - meY) / (TestTime + tDelta); + Vx:= (targXWrap - meX) / (TestTime + tDelta) + else + Vx:= (Targ.Point.X - meX) / (TestTime + tDelta); + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * (TestTime + tDelta) * 0.5 + Vx; + + Vy:= aiGravityf * ((TestTime + tDelta) div 2) - ((Targ.Point.Y-50) - meY) / (TestTime + tDelta); r:= sqr(Vx)+sqr(Vy); if not (r > 1) then begin x:= meX; + dX:= Vx; y:= meY; dY:= -Vy; t:= TestTime; repeat x:= CheckWrap(x); - x:= x + Vx; + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; dec(t) until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 6)) or ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 6))) or (t = 0); @@ -717,8 +906,8 @@ var A, B, D, T: real; C: LongInt; begin - A:= sqr(cGravityf); - B:= - cGravityf * (TY - MY) - 1; + A:= sqr(aiGravityf); + B:= - aiGravityf * (TY - MY) - 1; C:= sqr(TY - MY) + sqr(TX - MX); D:= sqr(B) - A * C; if D >= 0 then @@ -734,13 +923,14 @@ Solve:= 0 end; -function TestMortar(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; -//const tDelta = 24; +function TestMortar(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const Density : real = 1.0; var Vx, Vy: real; Score, EX, EY: LongInt; TestTime: Longword; - x, y, dY, meX, meY: real; + x, y, dX, dY, meX, meY: real; begin +Flags:= Flags; // avoid compiler hint TestMortar:= BadTurn; ap.ExplR:= 0; @@ -756,16 +946,21 @@ exit(BadTurn); Vx:= (Targ.Point.X - meX) / TestTime; - Vy:= cGravityf * (TestTime div 2) - (Targ.Point.Y - meY) / TestTime; + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * TestTime * 0.5 + Vx; + Vy:= aiGravityf * (TestTime div 2) - (Targ.Point.Y - meY) / TestTime; x:= meX; + dX:= Vx; y:= meY; dY:= -Vy; repeat - x:= x + Vx; + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; EX:= trunc(x); EY:= trunc(y); until (((Me = CurrentHedgehog^.Gear) and TestColl(EX, EY, 4)) or @@ -796,7 +991,7 @@ end; end; -function TestShotgun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestShotgun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const MIN_RANGE = 80; MAX_RANGE = 400; @@ -804,6 +999,7 @@ rx, ry, valueResult: LongInt; range: integer; begin +Flags:= Flags; // avoid compiler hint TestShotgun:= BadTurn; ap.ExplR:= 0; ap.Time:= 0; @@ -811,12 +1007,16 @@ x:= hwFloat2Float(Me^.X); y:= hwFloat2Float(Me^.Y); range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y); -if ( range < MIN_RANGE ) or ( range > MAX_RANGE ) then +// Range limits (laser sight can remove upper range limit) +if (range < MIN_RANGE) or ((range > MAX_RANGE) and (not aiLaserSighting) and (Level >= 4))then exit(BadTurn); Vx:= (Targ.Point.X - x) * 1 / 1024; Vy:= (Targ.Point.Y - y) * 1 / 1024; ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +// Apply inaccuracy +if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 10))); repeat x:= x + vX; y:= y + vY; @@ -849,11 +1049,12 @@ TestShotgun:= BadTurn end; -function TestDesertEagle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestDesertEagle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var Vx, Vy, x, y, t: real; d: Longword; ix, iy, valueResult: LongInt; begin +Flags:= Flags; // avoid compiler hint if (Level > 4) or (Targ.Score < 0) or (Targ.Kind <> gtHedgehog) then exit(BadTurn); Level:= Level; // avoid compiler hint ap.ExplR:= 1; @@ -870,6 +1071,9 @@ Vx:= (Targ.Point.X - x) * t; Vy:= (Targ.Point.Y - y) * t; ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +// Apply inaccuracy +if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 10))); d:= 0; ix:= trunc(x); @@ -902,11 +1106,12 @@ end; -function TestSniperRifle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestSniperRifle(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var Vx, Vy, x, y, t, dmg: real; d: Longword; //fallDmg: LongInt; begin +Flags:= Flags; // avoid compiler hint if (Level > 3) or (Targ.Score < 0) or (Targ.Kind <> gtHedgehog) then exit(BadTurn); Level:= Level; // avoid compiler hint ap.ExplR:= 0; @@ -923,6 +1128,9 @@ Vx:= (Targ.Point.X - x) * t; Vy:= (Targ.Point.Y - y) * t; ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +// Apply inaccuracy +inc(ap.Angle, AIrndSign(random((Level - 1) * 5))); + d:= 0; repeat @@ -944,11 +1152,12 @@ end; -function TestBaseballBat(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestBaseballBat(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var valueResult, a, v1, v2: LongInt; x, y, trackFall: LongInt; dx, dy: real; begin +Flags:= Flags; // avoid compiler hint Targ:= Targ; // avoid compiler hint if Level < 3 then trackFall:= afTrackFall @@ -996,10 +1205,11 @@ TestBaseballBat:= valueResult; end; -function TestFirePunch(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestFirePunch(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var valueResult, v1, v2, i: LongInt; x, y, trackFall: LongInt; begin +Flags:= Flags; // avoid compiler hint Targ:= Targ; // avoid compiler hint if Level = 1 then trackFall:= afTrackFall @@ -1054,10 +1264,11 @@ end; -function TestWhip(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestWhip(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var valueResult, v1, v2: LongInt; x, y, trackFall: LongInt; begin +Flags:= Flags; // avoid compiler hint Targ:= Targ; // avoid compiler hint if Level = 1 then trackFall:= afTrackFall @@ -1109,12 +1320,13 @@ TestWhip:= valueResult; end; -function TestKamikaze(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestKamikaze(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const step = 8; var valueResult, i, v, tx: LongInt; trackFall: LongInt; t, d, x, y, dx, dy, cx: real; begin +Flags:= Flags; // avoid compiler hint ap.ExplR:= 0; ap.Time:= 0; ap.Power:= 1; @@ -1126,6 +1338,10 @@ else exit(BadTurn); + // Don't sacrifice last hog + if aiHogsInTeam <= 1 then + exit(BadTurn); + valueResult:= 0; v:= 0; @@ -1144,7 +1360,10 @@ dx:= (Targ.Point.X - x) * t; dy:= (Targ.Point.Y - y) * t; - ap.Angle:= DxDy2AttackAnglef(dx, -dy) + ap.Angle:= DxDy2AttackAnglef(dx, -dy); + // Apply inaccuracy + if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 10))); end; if dx >= 0 then cx:= 0.45 else cx:= -0.45; @@ -1157,7 +1376,7 @@ valueResult:= valueResult + RateShove(Me, trunc(x), trunc(y) , 30, 30, 25 - , cx, -0.9, trackFall or afSetSkip); + , cx, -0.9, trackFall or afSetSkip or afIgnoreMe); end; if (d < 10) and (dx = 0) then @@ -1167,14 +1386,14 @@ tx:= trunc(x); v:= RateShove(Me, tx, trunc(y) , 30, 30, 25 - , -cx, -0.9, trackFall); + , -cx, -0.9, trackFall or afIgnoreMe); for i:= 1 to 512 div step - 2 do begin y:= y + dy; v:= v + RateShove(Me, tx, trunc(y) , 30, 30, 25 - , -cx, -0.9, trackFall or afSetSkip); + , -cx, -0.9, trackFall or afSetSkip or afIgnoreMe); end end; @@ -1187,18 +1406,19 @@ v:= RateShove(Me, trunc(x), trunc(y) , 30, 30, 25 - , cx, -0.9, trackFall); + , cx, -0.9, trackFall or afIgnoreMe); valueResult:= valueResult + v - KillScore * friendlyfactor div 100 * 1024; if v < 65536 then - inc(valueResult, RateExplosion(Me, trunc(x), trunc(y), 30)); + inc(valueResult, RateExplosion(Me, trunc(x), trunc(y), 30, afIgnoreMe)); TestKamikaze:= valueResult; end; -function TestHammer(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestHammer(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var rate: LongInt; begin +Flags:= Flags; // avoid compiler hint Level:= Level; // avoid compiler hint Targ:= Targ; @@ -1213,17 +1433,19 @@ TestHammer:= rate; end; -function TestAirAttack(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestAirAttack(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; const cShift = 4; -var bombsSpeed, X, Y, dY: real; + Density : real = 2.0; +var bombsSpeed, X, Y, dX, dY: real; b: array[0..9] of boolean; dmg: array[0..9] of LongInt; - fexit: boolean; - i, t, valueResult: LongInt; + fexit, firstHit: boolean; + i, t, valueResult, targetY: LongInt; begin +Flags:= Flags; // avoid compiler hint ap.ExplR:= 0; ap.Time:= 0; -if (Level > 3) or (cGravityf = 0) then +if (Level > 3) or (aiGravityf <= 0) then exit(BadTurn); ap.Angle:= 0; @@ -1232,8 +1454,10 @@ bombsSpeed:= hwFloat2Float(cBombsSpeed); X:= Targ.Point.X - 135 - cShift; // hh center - cShift -X:= X - bombsSpeed * sqrt(((Targ.Point.Y + 128) * 2) / cGravityf); -Y:= -128; +X:= X - bombsSpeed * sqrt(((Targ.Point.Y + 128) * 2) / aiGravityf); +Y:= topY - 300; + +dX:= bombsSpeed; dY:= 0; for i:= 0 to 9 do @@ -1242,11 +1466,14 @@ dmg[i]:= 0 end; valueResult:= 0; +firstHit:= false; repeat - X:= X + bombsSpeed; + X:= X + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; Y:= Y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; fexit:= true; for i:= 0 to 9 do @@ -1256,8 +1483,17 @@ if TestColl(trunc(X) + LongWord(i * 30), trunc(Y), 4) then begin b[i]:= false; - dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 58) + if Level = 1 then + dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 58, afTrackFall or afErasesLand) + else + dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 58); // 58 (instead of 60) for better prediction (hh moves after explosion of one of the rockets) + if (not firstHit) then + begin + firstHit:= true; + // remember Y of first hit, used for target pos + targetY:= trunc(Y); + end; end end; until fexit or (Y > cWaterLine); @@ -1267,6 +1503,10 @@ inc(valueResult, dmg[i]); t:= valueResult; ap.AttackPutX:= Targ.Point.X - 60; +if firstHit then + // this is not strictly neccessry, it's just to make sure + // the X is on the height of the first hit + ap.AttackPutY:= targetY; for i:= 0 to 3 do if dmg[i] <> BadTurn then @@ -1285,12 +1525,541 @@ TestAirAttack:= valueResult; end; +function TestResurrector(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +var rate, heal: LongInt; +begin +Flags:= Flags; // avoid compiler hint +Targ:= Targ; -function TestTeleport(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +if (Level = 5) then + exit(BadTurn); + +if (Me^.Health <= 1) then + exit(BadTurn); + +if (Level <= 2) and (Me^.Hedgehog^.Effects[hePoisoned] > 0) then + // Sacrifice almost all health if poisoned + heal:= Me^.Health - 1 +else + // Sacrifice up to 10% of own health + heal:= (Me^.Health div 10); + +ap.ExplR:= 0; +ap.Time:= 0; +if (Level >= 4) then + // slow resurrect + ap.Power:= max(512 * heal - 512, 10) +else + // fast resurrect + ap.Power:= max(16 * heal - 16, 10); + +// Time limit +ap.Power:= min(ap.Power, 5000); + +ap.Angle:= 0; + +rate:= RateResurrector(Me); +if rate <= 0 then + rate:= BadTurn; +TestResurrector:= rate; +end; + +function TestDrillStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const cShift = 4; + Density : real = 1.0; +var bombsSpeed, X, Y, dX, dY, drillX, drillY: real; + t2: real; + dmg: array[0..9] of LongInt; + collided, drilling, timerRuns, firstHit: boolean; + i, t, value, valueResult, attackTime, drillTimer, targetX, targetY: LongInt; +begin +Flags:= Flags; // avoid compiler hint +ap.ExplR:= 0; +if (Level > 3) or (aiGravityf <= 0) then + exit(BadTurn); + +ap.Angle:= 0; +targetX:= Targ.Point.X; +ap.AttackPutY:= Targ.Point.Y; + +bombsSpeed:= hwFloat2Float(cBombsSpeed); +X:= Targ.Point.X - 135 - cShift; // hh center - cShift +X:= X - bombsSpeed * sqrt(((Targ.Point.Y + 128) * 2) / aiGravityf); +Y:= topY - 300; + +valueResult:= 0; + +attackTime:= 6000; +while attackTime >= 0 do + begin + dec(attackTime, 1000); + value:= 0; + firstHit:= false; + for i:= 0 to 9 do + begin + dmg[i]:= 0; + drillX:= trunc(X) + LongWord(i * 30); + drillY:= trunc(Y); + dX:= bombsSpeed; + dY:= 0; + collided:= false; + drilling:= false; + timerRuns:= false; + drillTimer := attackTime; + + repeat + // Simulate in-air movement + drillX:= drillX + dX; + drillY:= drillY + dY; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; + dY:= dY + aiGravityf; + + if timerRuns then + dec(drillTimer); + + // Collided with land ... simulate drilling + if (drillTimer > 0) and TestCollExcludingObjects(trunc(drillX), trunc(drillY), 4) and + (Abs(Targ.Point.X - trunc(drillX)) + Abs(Targ.Point.Y - trunc(drillY)) > 21) then + begin + drilling := true; + timerRuns := true; + t2 := 0.5 / sqrt(sqr(dX) + sqr(dY)); + dX := dX * t2; + dY := dY * t2; + repeat + drillX:= drillX + dX; + drillY:= drillY + dY; + dec(drillTimer, 10); + if (Abs(Targ.Point.X - drillX) + Abs(Targ.Point.Y - drillY) < 22) + or (drillX < -32) + or (drillY < -32) + or (trunc(drillX) > LAND_WIDTH + 32) + or (trunc(drillY) > cWaterLine) + or (drillTimer <= 0) then + collided:= true + else if not TestCollExcludingObjects(trunc(drillX), trunc(drillY), 4) then + drilling:= false; + until (collided or (not drilling)); + end + // Collided with something else ... record collision + else if (drillTimer <= 0) or TestColl(trunc(drillX), trunc(drillY), 4) then + collided:= true; + + // Simulate explosion + if collided then + begin + if Level = 1 then + dmg[i]:= RateExplosion(Me, trunc(drillX), trunc(drillY), 58, afTrackFall or afErasesLand) + else + dmg[i]:= RateExplosion(Me, trunc(drillX), trunc(drillY), 58); + // 58 (instead of 60) for better prediction (hh moves after explosion of one of the rockets) + if not firstHit then + begin + targetY:= trunc(drillY); + firstHit:= true; + end; + end; + until collided or (drillY > cWaterLine); + end; + + // calculate score + for i:= 0 to 5 do + if dmg[i] <> BadTurn then + inc(value, dmg[i]); + t:= value; + targetX:= Targ.Point.X - 60 - cShift; + + for i:= 0 to 3 do + if dmg[i] <> BadTurn then + begin + dec(t, dmg[i]); + inc(t, dmg[i + 6]); + if t >= value then + begin + value:= t; + targetX:= Targ.Point.X - 30 - cShift + i * 30 + end + end; + + if value > valueResult then + begin + valueResult:= value; + ap.AttackPutX:= targetX; + if firstHit then + ap.AttackPutY:= targetY; + ap.Time:= attackTime; + end; +end; + +if valueResult <= 0 then + valueResult:= BadTurn +else + begin + // Weaker AI has chance to get the time wrong by 1-3 seconds + if Level = 5 then + // +/- 3 seconds + ap.Time:= ap.Time + (3 - random(7)) * 1000 + else if Level = 4 then + // +/- 2 seconds + ap.Time:= ap.Time + (2 - random(5)) * 1000 + else if Level = 3 then + // +/- 1 second + if (random(2) = 0) then + ap.Time:= ap.Time + (1 - random(3)) * 1000 + else if Level = 2 then + // 50% chance for +/- 1 second + if (random(2) = 0) then + ap.Time:= ap.Time + (1 - random(3)) * 1000; + ap.Time:= Min(5000, Max(1000, ap.Time)); + end; + +TestDrillStrike:= valueResult; +end; + +function TestMineStrike(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const cShift = 4; + Density : real = 1.0; +var minesSpeed, X, Y, dX, dY: real; + b: array[0..9] of boolean; + dmg: array[0..9] of LongInt; + fexit, firstHit: boolean; + i, t, valueResult, targetY: LongInt; +begin +Flags:= Flags; // avoid compiler hint +ap.ExplR:= 0; +ap.Time:= 0; + +// AI currently only supports cMinesTime = 0 because it's the most +// predictable. +// Other cMinesTime values are risky because of bouncy mines; +// so they are unsupported. +// TODO: Implement mine strike for other values of MineTime +// TODO: Teach AI to avoid hitting their own with mines +if (Level > 3) or (aiGravityf <= 0) or (cMinesTime <> 0) then + exit(BadTurn); + +ap.Angle:= 0; +ap.AttackPutX:= Targ.Point.X; +ap.AttackPutY:= Targ.Point.Y; + +minesSpeed:= hwFloat2Float(cBombsSpeed); +X:= Targ.Point.X - 135 - cShift; // hh center - cShift +X:= X - minesSpeed * sqrt(((Targ.Point.Y + 128) * 2) / aiGravityf); +Y:= topY - 300; +dX:= minesSpeed; +dY:= 0; + +for i:= 0 to 9 do + begin + b[i]:= true; + dmg[i]:= 0 + end; +valueResult:= 0; +firstHit:= false; + +repeat + X:= X + dX; + if (GameFlags and (gfMoreWind or gfInfAttack)) <> 0 then + dX:= dX + aiWindSpeed / Density; + Y:= Y + dY; + dY:= dY + aiGravityf; + fexit:= true; + + for i:= 0 to 9 do + if b[i] then + begin + fexit:= false; + if TestColl(trunc(X) + LongWord(i * 30), trunc(Y), 4) then + begin + b[i]:= false; + if Level = 1 then + dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 96, afTrackFall or afErasesLand) + else + dmg[i]:= RateExplosion(Me, trunc(X) + LongWord(i * 30), trunc(Y), 96); + + if (not firstHit) then + begin + targetY:= trunc(Y); + firstHit:= true; + end; + end + end; +until fexit or (Y > cWaterLine); + +for i:= 0 to 5 do + if dmg[i] <> BadTurn then + inc(valueResult, dmg[i]); +t:= valueResult; +ap.AttackPutX:= Targ.Point.X - 60; +if firstHit then + ap.AttackPutY:= targetY; + +for i:= 0 to 3 do + if dmg[i] <> BadTurn then + begin + dec(t, dmg[i]); + inc(t, dmg[i + 6]); + if t > valueResult then + begin + valueResult:= t; + ap.AttackPutX:= Targ.Point.X - 30 - cShift + i * 30 + end + end; + +if valueResult <= 0 then + valueResult:= BadTurn; +TestMineStrike:= valueResult; +end; + +function TestSineGun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const + MIN_RANGE = 40; + MAX_RANGE = 400; +var Vx, Vy, x, y: real; + rx, ry, value, valueResult: LongInt; + range: integer; + landStop: boolean; +begin +// TODO: Also simulate the sine gun's kickback and make sure the attacker +// doesn't get hurt + +Flags:= Flags; // avoid compiler hint +TestSineGun:= BadTurn; +valueResult:= 0; +ap.ExplR:= 0; +ap.Time:= 0; +ap.Power:= 1; +x:= hwFloat2Float(Me^.X); +y:= hwFloat2Float(Me^.Y); +range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y); +// Range limits +if (range < MIN_RANGE) or ((range > MAX_RANGE) and (Level >= 4)) then + exit(BadTurn); + +Vx:= (Targ.Point.X - x) * 1 / 1024; +Vy:= (Targ.Point.Y - y) * 1 / 1024; + +// Never shoot downwards or horizontal because it's dangerous +// and we don't have a proper check for kickback yet +if Vy >= 0 then + exit(BadTurn); + +landStop:= false; +ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +// Apply inaccuracy +if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 10))); +repeat + x:= x + vX; + y:= y + vY; + rx:= trunc(x); + ry:= trunc(y); + if (GameFlags and gfSolidLand) <> 0 then + // Stop projectile when colliding with indestructible land + landStop:= (Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 5); + + if landStop or + ((Me = CurrentHedgehog^.Gear) and TestCollHogsOrObjects(rx, ry, 5)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 5)) then + begin + x:= x + vX * 8; + y:= y + vY * 8; + value:= RateShove(Me, rx, ry, 5, 35, 50, vX, vY, afTrackFall); + + if (value > 0) then + inc(valueResult, value); + end +until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 5) + or (landStop) + or (Metric(trunc(x), trunc(y), hwRound(Me^.X), hwRound(Me^.Y)) > MAX_RANGE) + or (x < 0) + or (y < 0) + or (trunc(x) > LAND_WIDTH) + or (trunc(y) > LAND_HEIGHT); + +if valueResult <= 0 then + valueResult:= BadTurn; +TestSineGun:= valueResult; +end; + +function TestSMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const timeLimit = 50; + Density : real = 1.6; +var Vx, Vy, r, meX, meY: real; + rTime: LongInt; + EX, EY: LongInt; + valueResult: LongInt; + targXWrap, x, y, dX, dY: real; + t: LongInt; + value: LongInt; +begin +Flags:= Flags; // avoid compiler hint +meX:= hwFloat2Float(Me^.X); +meY:= hwFloat2Float(Me^.Y); +ap.Time:= 0; +rTime:= 350; +ap.ExplR:= 0; +if (WorldEdge = weWrap) then + if (Targ.Point.X < meX) then + targXWrap:= Targ.Point.X + (RightX-LeftX) + else targXWrap:= Targ.Point.X - (RightX-LeftX); +valueResult:= BadTurn; +repeat + rTime:= rTime + 300 + Level * 50 + random(300); + if (WorldEdge = weWrap) and (random(2)=0) then + Vx:= (targXWrap + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime + else + Vx:= (Targ.Point.X + AIrndSign(2) + AIrndOffset(Targ, Level) - meX) / rTime; + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx; + Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - meY) / rTime; + r:= sqr(Vx) + sqr(Vy); + + if not (r > 1) then + begin + x:= meX; + y:= meY; + dX:= Vx; + dY:= -Vy; + t:= rTime; + repeat + x:= CheckWrap(x); + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; + + y:= y + dY; + dY:= dY + aiGravityf; + dec(t) + until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 2)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 2))) or (t < -timeLimit); + + EX:= trunc(x); + EY:= trunc(y); + + if t >= -timeLimit then + if (Level = 1) and (Flags and amtest_NoTrackFall = 0) then + value:= RateExplosion(Me, EX, EY, 61, afTrackFall) + else + value:= RateExplosion(Me, EX, EY, 61) + else + value:= BadTurn; + + if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then + value := BadTurn; + + if (valueResult < value) or ((valueResult = value) and (Level < 3)) then + begin + ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 9)); + ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 17 + 1); + ap.ExplR:= 60; + ap.ExplX:= EX; + ap.ExplY:= EY; + valueResult:= value + end; + end +until rTime > 5050 - Level * 800; +if valueResult <> BadTurn then + // 27/20 is reuse bonus + valueResult:= valueResult * 27 div 20; +TestSMine:= valueResult +end; + +function TestPiano(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +// TODO: Test all 5 bounces +const BOUNCES = 1; // we only test 1 bounce to avoid the rating getting excessively high +var X, Y: real; + dmg: array[0..BOUNCES-1] of LongInt; + i, e, rate, valueResult, targetY: LongInt; + firstHit, solidBounce: boolean; +begin +Flags:= Flags; // avoid compiler hint +ap.ExplR:= 0; +ap.Time:= 0; +if (aiGravityf <= 0) then + exit(BadTurn); + +if (Level > 2) then + exit(BadTurn); + +// Don't sacrifice last hog +if aiHogsInTeam <= 1 then + exit(BadTurn); + +ap.Angle:= 0; +ap.AttackPutX:= Targ.Point.X; +ap.AttackPutY:= Targ.Point.Y; + +X:= Targ.Point.X; +Y:= -1024; + +for i:= 0 to BOUNCES-1 do + dmg[i]:= 0; + +i:= 1; +firstHit:= false; +solidBounce:= false; +repeat + // Piano goes down + if (not solidBounce) then + Y:= Y + 11; + if solidBounce or TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(X), trunc(Y), 32) then + begin + if (not firstHit) then + targetY:= trunc(Y); + firstHit:= true; + if (GameFlags and gfSolidLand) <> 0 then + // Don't change Y when indestructible land was hit + solidBounce:= true; + + for e:= -1 to 1 do + begin + // TODO: RateExplosion should remember hogs that already died in the simulation; + // because currently, dead hogs might still be counted for damage + rate:= RateExplosion(Me, trunc(X) + 30*e, trunc(Y)+40, 161, afIgnoreMe); + if rate <> BadTurn then + dmg[i]:= dmg[i] + rate; + end; + + if (i > 1) and (dmg[i] > 0) then + dmg[i]:= dmg[i] div 2; + inc(i); + if (not solidBounce) then + // Skip past the blast hole + Y:= Y + 41 + end; +until (i > BOUNCES) or (Y > cWaterLine); + +if (i = 0) and (Y > cWaterLine) then + exit(BadTurn); + +valueResult:= 0; +for i:= 0 to BOUNCES do + if dmg[i] <= BadTurn then + begin + valueResult:= BadTurn; + break; + end + else + inc(valueResult, dmg[i]); +ap.AttackPutX:= Targ.Point.X; +if firstHit then + ap.AttackPutY:= targetY; + +valueResult:= valueResult - KillScore * friendlyfactor div 100 * 1024; + +if valueResult <= 0 then + valueResult:= BadTurn; +TestPiano:= valueResult; +end; + +function TestTeleport(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var i, failNum: longword; maxTop: longword; begin +Flags:= Flags; // avoid compiler hint TestTeleport := BadTurn; exit(BadTurn); Level:= Level; // avoid compiler hint @@ -1350,10 +2119,11 @@ end; end; -function TestCake(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestCake(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; var valueResult, v1, v2: LongInt; cake: TGear; begin +Flags:= Flags; // avoid compiler hint Targ:= Targ; // avoid compiler hint if (Level > 2) then @@ -1406,22 +2176,49 @@ TestCake:= valueResult; end; -function TestDynamite(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams): LongInt; +function TestSeduction(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +var rate: LongInt; +begin +Flags:= Flags; // avoid compiler hint +Level:= Level; // avoid compiler hint +Targ:= Targ; + +if (Level = 5) then + exit(BadTurn); + +ap.ExplR:= 0; +ap.Time:= 0; +ap.Power:= 1; +ap.Angle:= 0; + +rate:= RateSeduction(Me); +if rate <= 0 then + rate:= BadTurn; +TestSeduction:= rate; +end; + +function TestDynamite(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const Density : real = 2.0; var valueResult: LongInt; x, y, dx, dy: real; EX, EY, t: LongInt; begin +Flags:= Flags; // avoid compiler hint Targ:= Targ; // avoid compiler hint x:= hwFloat2Float(Me^.X) + hwSign(Me^.dX) * 7; y:= hwFloat2Float(Me^.Y); dx:= hwSign(Me^.dX) * 0.03; +if (GameFlags and gfMoreWind) <> 0 then + dx:= -(aiWindSpeed / Density) + dx; dy:= 0; t:= 5000; repeat dec(t); + if (GameFlags and gfMoreWind) <> 0 then + dx:= dx + aiWindSpeed / Density; x:= x + dx; - dy:= dy + cGravityf; + dy:= dy + aiGravityf; y:= y + dy; if TestColl(trunc(x), trunc(y), 3) then @@ -1450,4 +2247,273 @@ TestDynamite:= valueResult end; +function TestMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const Density : real = 1.0; +var valueResult: LongInt; + x, y, dx, dy: real; + EX, EY, t: LongInt; +begin +Flags:= Flags; // avoid compiler hint +Targ:= Targ; // avoid compiler hint + +x:= hwFloat2Float(Me^.X) + hwSign(Me^.dX) * 7; +y:= hwFloat2Float(Me^.Y); +dx:= hwSign(Me^.dX) * 0.02; +if (GameFlags and gfMoreWind) <> 0 then + dx:= -(aiWindSpeed / Density) + dx; +dy:= 0; +t:= 10000; +repeat + dec(t); + if (GameFlags and gfMoreWind) <> 0 then + dx:= dx + aiWindSpeed / Density; + x:= x + dx; + dy:= dy + aiGravityf; + y:= y + dy; + if ((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 2)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 2)) then + t:= 0; +until t = 0; + +EX:= trunc(x); +EY:= trunc(y); + +if Level = 1 then + valueResult:= RateExplosion(Me, EX, EY, 51, afTrackFall or afErasesLand) +else + valueResult:= RateExplosion(Me, EX, EY, 51); + +if (valueResult > 0) then + begin + ap.Angle:= 0; + ap.Power:= 1; + ap.Time:= 0; + if (Level < 5) then + // Set minimum mine bounciness for improved aim + ap.Bounce:= 1 + else + ap.Bounce:= 0; + ap.ExplR:= 100; + ap.ExplX:= EX; + ap.ExplY:= EY + end else + valueResult:= BadTurn; + +TestMine:= valueResult +end; + +function TestKnife(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const timeLimit = 300; + Density : real = 4.0; +var Vx, Vy, r, meX, meY: real; + rTime: LongInt; + EX, EY: LongInt; + valueResult: LongInt; + targXWrap, x, y, dX, dY: real; + t: LongInt; + value, range: LongInt; +begin +Flags:= Flags; // avoid compiler hint +meX:= hwFloat2Float(Me^.X); +meY:= hwFloat2Float(Me^.Y); +ap.Time:= 0; +rTime:= 350; +ap.ExplR:= 0; +if (WorldEdge = weWrap) then + if (Targ.Point.X < meX) then + targXWrap:= Targ.Point.X + (RightX-LeftX) + else + targXWrap:= Targ.Point.X - (RightX-LeftX); +valueResult:= BadTurn; +repeat + rTime:= rTime + 300 + Level * 50 + random(300); + if (WorldEdge = weWrap) and (random(2)=0) then + Vx:= (targXWrap - meX) / rTime + else + Vx:= (Targ.Point.X - meX) / rTime; + if (GameFlags and gfMoreWind) <> 0 then + Vx:= -(aiWindSpeed / Density) * rTime * 0.5 + Vx; + Vy:= aiGravityf * rTime * 0.5 - (Targ.Point.Y + 1 - meY) / rTime; + r:= sqr(Vx) + sqr(Vy); + + if not (r > 1) then + begin + x:= meX; + y:= meY; + dX:= Vx; + dY:= -Vy; + t:= rTime; + repeat + x:= CheckWrap(x); + x:= x + dX; + if (GameFlags and gfMoreWind) <> 0 then + dX:= dX + aiWindSpeed / Density; + + y:= y + dY; + dY:= dY + aiGravityf; + dec(t) + until (((Me = CurrentHedgehog^.Gear) and TestColl(trunc(x), trunc(y), 7)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, trunc(x), trunc(y), 7))) or (t < -timeLimit); + + EX:= trunc(x); + EY:= trunc(y); + + // Sanity check: Make sure we're not too close to impact location + range:= Metric(trunc(meX), trunc(meY), EX, EY); + if (range <= 40) then + value:= BadTurn + // Timeout + else if t < -timeLimit then + value:= BadTurn + else + value:= RateShove(Me, EX, EY, 16, trunc(sqr((abs(dY)+abs(dX))*40000/10000)), 0, dX, dY, 0); + + if (value = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then + value := BadTurn; + + if (valueResult < value) or ((valueResult = value) and (Level = 1)) then + begin + ap.Angle:= DxDy2AttackAnglef(Vx, Vy) + AIrndSign(random((Level - 1) * 12)); + ap.Power:= trunc(sqrt(r) * cMaxPower) - random((Level - 1) * 22 + 1); + valueResult:= value + end; + end +until rTime > 5050 - Level * 800; +TestKnife:= valueResult +end; + +function TestAirMine(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const + MIN_RANGE = 160; + MAX_RANGE = 2612; +var Vx, Vy, meX, meY, x, y, r: real; + rx, ry, valueResult: LongInt; + range, maxRange: integer; +begin +Flags:= Flags; // avoid compiler hint +maxRange:= MAX_RANGE - ((Level - 1) * 300); +TestAirMine:= BadTurn; +ap.ExplR:= 60; +ap.Time:= 0; +meX:= hwFloat2Float(Me^.X); +meY:= hwFloat2Float(Me^.Y); +x:= meX; +y:= meY; + +// Rough first range check +range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y); +if ( range < MIN_RANGE ) or ( range > maxRange ) then + exit(BadTurn); + +Vx:= (Targ.Point.X - x) * 1 / 1024; +Vy:= (Targ.Point.Y - y) * 1 / 1024; +ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +repeat + x:= x + vX; + y:= y + vY; + rx:= trunc(x); + ry:= trunc(y); + if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 8)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 8)) then + begin + x:= x + vX * 8; + y:= y + vY * 8; + + if Level = 1 then + valueResult:= RateExplosion(Me, rx, ry, 61, afTrackFall) + else + valueResult:= RateExplosion(Me, rx, ry, 61); + + // Precise range calculation required to calculate power; + // The air mine is very sensitive to small changes in power. + r:= sqr(meX - rx) + sqr(meY - ry); + range:= trunc(sqrt(r)); + + if ( range < MIN_RANGE ) or ( range > maxRange ) then + exit(BadTurn); + ap.Power:= ((range + cHHRadius*2) * cMaxPower) div MAX_RANGE; + + // Apply inaccuracy + inc(ap.Power, (random(93*(Level-1)) - 31*(Level-1))); // Level 1 spread: -124 .. 248 + if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 10))); + + if (valueResult <= 0) then + valueResult:= BadTurn; + exit(valueResult) + end +until (abs(Targ.Point.X - trunc(x)) + abs(Targ.Point.Y - trunc(y)) < 4) + or (x < 0) + or (y < 0) + or (trunc(x) > LAND_WIDTH) + or (trunc(y) > LAND_HEIGHT); + +TestAirMine := BadTurn +end; + +function TestMinigun(Me: PGear; Targ: TTarget; Level: LongInt; var ap: TAttackParams; Flags: LongWord): LongInt; +const + MAX_RANGE = 400; +var Vx, Vy, x, y: real; + rx, ry, valueResult: LongInt; + range: integer; +begin +// This code is still very similar to TestShotgun, +// but it's a good simple estimate. +// TODO: Simulate random bullets +// TODO: Replace RateShotgun with something else +// TODO: Teach AI to move aim during shooting +Flags:= Flags; // avoid compiler hint +TestMinigun:= BadTurn; +ap.ExplR:= 0; +ap.Time:= 0; +ap.Power:= 1; +x:= hwFloat2Float(Me^.X); +y:= hwFloat2Float(Me^.Y); +range:= Metric(trunc(x), trunc(y), Targ.Point.X, Targ.Point.Y); +if ( range > MAX_RANGE ) then + exit(BadTurn); + +Vx:= (Targ.Point.X - x) * 1 / 1024; +Vy:= (Targ.Point.Y - y) * 1 / 1024; +ap.Angle:= DxDy2AttackAnglef(Vx, -Vy); +// Minigun angle is limited +if (ap.Angle < Ammoz[amMinigun].minAngle) or (ap.Angle > Ammoz[amMinigun].maxAngle) then + exit(BadTurn); + +// Apply inaccuracy +if (not aiLaserSighting) then + inc(ap.Angle, AIrndSign(random((Level - 1) * 10))); +repeat + x:= x + vX; + y:= y + vY; + rx:= trunc(x); + ry:= trunc(y); + if ((Me = CurrentHedgehog^.Gear) and TestColl(rx, ry, 1)) or + ((Me <> CurrentHedgehog^.Gear) and TestCollExcludingMe(Me^.Hedgehog^.Gear, rx, ry, 1)) then + begin + x:= x + vX * 8; + y:= y + vY * 8; + // TODO: Use different rating function + valueResult:= RateShotgun(Me, vX, vY, rx, ry); + + if (valueResult = 0) and (Targ.Kind = gtHedgehog) and (Targ.Score > 0) then + begin + if GameFlags and gfSolidLand = 0 then + valueResult:= 1024 - Metric(Targ.Point.X, Targ.Point.Y, rx, ry) div 64 + else valueResult := BadTurn + end + else + dec(valueResult, Level * 4000); + exit(valueResult) + end +until (Abs(Targ.Point.X - trunc(x)) + Abs(Targ.Point.Y - trunc(y)) < 4) + or (x < 0) + or (y < 0) + or (trunc(x) > LAND_WIDTH) + or (trunc(y) > LAND_HEIGHT); + +TestMinigun:= BadTurn +end; + end. diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uAIMisc.pas --- a/hedgewars/uAIMisc.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uAIMisc.pas Sun Mar 24 14:33:57 2024 -0400 @@ -27,6 +27,7 @@ afTrackFall = $00000001; afErasesLand = $00000002; afSetSkip = $00000004; + afIgnoreMe = $00000008; BadTurn = Low(LongInt) div 4; @@ -78,6 +79,7 @@ function RatePlace(Gear: PGear): LongInt; function CheckWrap(x: real): real; inline; function TestColl(x, y, r: LongInt): boolean; inline; +function TestCollHogsOrObjects(x, y, r: LongInt): boolean; inline; function TestCollExcludingObjects(x, y, r: LongInt): boolean; inline; function TestCollExcludingMe(Me: PGear; x, y, r: LongInt): boolean; inline; @@ -86,6 +88,8 @@ function RealRateExplosion(Me: PGear; x, y, r: LongInt; Flags: LongWord): LongInt; function RateShove(Me: PGear; x, y, r, power, kick: LongInt; gdX, gdY: real; Flags: LongWord): LongInt; function RateShotgun(Me: PGear; gdX, gdY: real; x, y: LongInt): LongInt; +function RateSeduction(Me: PGear): LongInt; +function RateResurrector(Me: PGear): LongInt; function RateHammer(Me: PGear): LongInt; function HHGo(Gear, AltGear: PGear; var GoInfo: TGoInfo): boolean; @@ -100,11 +104,12 @@ walkbonuses: Twalkbonuses; const KillScore = 200; + ResurrectScore = 100; var friendlyfactor: LongInt = 300; var dmgMod: real = 1.0; implementation -uses uCollisions, uVariables, uUtils, uGearsUtils; +uses uCollisions, uVariables, uUtils, uGearsUtils, uAIAmmoTests; var KnownExplosion: record @@ -135,13 +140,15 @@ (Gear <> ThinkingHH) and (Gear^.Health > Gear^.Damage) and (not Gear^.Hedgehog^.Team^.hasgone)) or + ((Gear^.Kind = gtGrave) and + (Gear^.Health = 0)) or ((Gear^.Kind = gtExplosives) and (Gear^.Health > Gear^.Damage)) or ((Gear^.Kind = gtMine) and (Gear^.Health = 0) and (Gear^.Damage < 35)) ) and - (Targets.Count < 256) then + (Targets.Count < 255) then begin with Targets.ar[Targets.Count] do begin @@ -168,6 +175,17 @@ inc(e) end; end + else if Gear^.Kind = gtGrave then + if (Gear^.Hedgehog^.Team^.Clan = CurrentTeam^.Clan) then + begin + Score:= ResurrectScore; + inc(f); + end + else + begin + Score:= -ResurrectScore; + inc(e); + end else if Gear^.Kind = gtExplosives then Score:= Gear^.Health - Gear^.Damage else if Gear^.Kind = gtMine then @@ -218,19 +236,29 @@ while Gear <> nil do begin case Gear^.Kind of - gtGrenade + gtAirAttack + , gtAirBomb + , gtBall + , gtBee + , gtBirdy , gtClusterBomb + , gtCake + , gtCluster + , gtDrill + , gtEgg , gtGasBomb - , gtShell - , gtAirAttack + , gtGrenade + , gtHellishBomb + , gtPiano + , gtPoisonCloud + , gtRCPlane + , gtMelonPiece + , gtMolotov , gtMortar - , gtWatermelon - , gtDrill - , gtAirBomb - , gtCluster - , gtMelonPiece - , gtBee - , gtMolotov: bonuses.activity:= true; + , gtNapalmBomb + , gtShell + , gtSnowball + , gtWatermelon: bonuses.activity:= true; gtCase: if (Gear^.AIHints and aihDoesntMatter) = 0 then AddBonus(hwRound(Gear^.X), hwRound(Gear^.Y) + 3, 37, 25); @@ -354,6 +382,7 @@ end; +// Check for collision with anything function TestCollWithEverything(x, y, r: LongInt): boolean; inline; begin if not CheckBounds(x, y, r) then @@ -368,6 +397,7 @@ TestCollWithEverything := false; end; +// Check for collision with non-objects function TestCollExcludingObjects(x, y, r: LongInt): boolean; inline; begin if not CheckBounds(x, y, r) then @@ -375,13 +405,14 @@ if (Land[y-r, x-r] > lfAllObjMask) or (Land[y+r, x-r] > lfAllObjMask) or - (Land[y-r, x-r] > lfAllObjMask) or + (Land[y-r, x+r] > lfAllObjMask) or (Land[y+r, x+r] > lfAllObjMask) then exit(true); TestCollExcludingObjects:= false; end; +// Check for collision with something other than current hedgehog or crate function TestColl(x, y, r: LongInt): boolean; inline; begin if not CheckBounds(x, y, r) then @@ -389,14 +420,29 @@ if (Land[y-r, x-r] and lfNotCurHogCrate <> 0) or (Land[y+r, x-r] and lfNotCurHogCrate <> 0) or - (Land[y+r, x-r] and lfNotCurHogCrate <> 0) or + (Land[y-r, x+r] and lfNotCurHogCrate <> 0) or (Land[y+r, x+r] and lfNotCurHogCrate <> 0) then exit(true); TestColl:= false; end; +// Check for collision with hedgehog or object +function TestCollHogsOrObjects(x, y, r: LongInt): boolean; inline; +begin + if not CheckBounds(x, y, r) then + exit(false); + if (Land[y-r, x-r] and lfAllObjMask <> 0) or + (Land[y+r, x-r] and lfAllObjMask <> 0) or + (Land[y-r, x+r] and lfAllObjMask <> 0) or + (Land[y+r, x+r] and lfAllObjMask <> 0) then + exit(true); + + TestCollHogsOrObjects:= false; +end; + +// Check for collision with something other than the given "Me" gear. // Wrapper to test various approaches. If it works reasonably, will just replace. // Right now, converting to hwFloat is a tad inefficient since the x/y were hwFloat to begin with... function TestCollExcludingMe(Me: PGear; x, y, r: LongInt): boolean; inline; @@ -430,7 +476,7 @@ x:= CheckWrap(x); x:= x + dX; y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; skipLandCheck:= skipLandCheck and (r <> 0) and (abs(eX-x) + abs(eY-y) < r) and ((abs(eX-x) < rCorner) or (abs(eY-y) < rCorner)); if not skipLandCheck and TestCollExcludingObjects(trunc(x), trunc(y), Target.Radius) then with Target do @@ -477,7 +523,7 @@ x:= CheckWrap(x); x:= x + dX; y:= y + dY; - dY:= dY + cGravityf; + dY:= dY + aiGravityf; { if ((trunc(y) and LAND_HEIGHT_MASK) = 0) and ((trunc(x) and LAND_WIDTH_MASK) = 0) then begin @@ -539,20 +585,22 @@ x:= round(CheckWrap(real(x))); fallDmg:= 0; rate:= 0; -// add our virtual position -with Targets.ar[Targets.Count] do - begin - Point.x:= hwRound(Me^.X); - Point.y:= hwRound(Me^.Y); - skip:= false; - matters:= true; - Kind:= gtHedgehog; - Density:= 1; - Radius:= cHHRadius; - Score:= - ThinkingHH^.Health - end; + +if (Flags and afIgnoreMe) = 0 then + // add our virtual position + with Targets.ar[Targets.Count] do + begin + Point.x:= hwRound(Me^.X); + Point.y:= hwRound(Me^.Y); + skip:= false; + matters:= true; + Kind:= gtHedgehog; + Density:= 1; + Radius:= cHHRadius; + Score:= - ThinkingHH^.Health + end; + // rate explosion - if (Flags and afErasesLand <> 0) and (GameFlags and gfSolidLand = 0) then erasure:= r else erasure:= 0; @@ -561,8 +609,9 @@ for i:= 0 to Targets.Count do if not Targets.ar[i].dead then with Targets.ar[i] do - if not matters then hadSkips:= true - else + if not matters then + hadSkips:= true + else begin dmg:= 0; dmgBase:= r + Radius div 2; @@ -832,6 +881,133 @@ ResetTargets; end; +function RateSeduction(Me: PGear): LongInt; +var pX, pY, i, r, rate, subrate, fallDmg: LongInt; + diffX, diffY: LongInt; + meX, meY, dX, dY: hwFloat; + pXr, pYr: real; + hadSkips: boolean; +begin +meX:= Me^.X; +meY:= Me^.Y; +rate:= 0; +hadSkips:= false; +for i:= 0 to Targets.Count do + if not Targets.ar[i].dead then + with Targets.ar[i] do + begin + pX:= Point.X; + pY:= Point.Y; + diffX:= pX - hwRound(meX); + diffY:= pY - hwRound(meY); + if (Me^.Hedgehog^.BotLevel < 4) and (abs(diffX) <= cHHRadius*2) and (diffY >= 0) and (diffY <= cHHRadius*2) then + // Don't use seduction if too close to other hog. We could be + // standing on it, so using seduction would remove the ground on + // which we stand on, which is dangerous + exit(BadTurn); + + if (not matters) then + hadSkips:= true + else if matters and (Kind = gtHedgehog) and (abs(pX - hwRound(meX)) + abs(pY - hwRound(meY)) < cSeductionDist) then + begin + r:= trunc(sqrt(sqr(abs(pX - hwRound(meX)))+sqr(abs(pY - hwRound(meY))))); + if r < cSeductionDist then + begin + + if (WorldEdge <> weWrap) or (not (hwAbs(meX - int2hwFloat(pX)) > int2hwFloat(cSeductionDist))) then + dX:= _50 * aiGravity * (meX - int2hwFloat(pX)) / _25 + else if (not (hwAbs(meX + int2hwFloat((RightX-LeftX) - pX)) > int2hwFloat(cSeductionDist))) then + dX:= _50 * aiGravity * (meX + (int2hwFloat((RightX-LeftX) - pX))) / _25 + else + dX:= _50 * aiGravity * (meX - (int2hwFloat((RightX-LeftX) - pX))) / _25; + dY:= -_450 * cMaxWindSpeed * 2; + + + pXr:= pX; + pYr:= pY; + fallDmg:= trunc(TraceShoveFall(pXr, pYr, hwFloat2Float(dX), hwFloat2Float(dY), Targets.ar[i]) * dmgMod); + + // rate damage + if fallDmg < 0 then // drowning + begin + if Score > 0 then + inc(rate, (KillScore + Score div 10) * 1024) // Add a bit of a bonus for bigger hog drownings + else + dec(rate, (KillScore * friendlyfactor div 100 - Score div 10) * 1024) // and more of a punishment for drowning bigger friendly hogs + end + else if (fallDmg) >= abs(Score) then // deadly fall damage + begin + dead:= true; + Targets.reset:= true; + if (hwFloat2Float(dX) < 0.035) then + begin + subrate:= RealRateExplosion(Me, round(pX), round(pY), 61, afErasesLand or afTrackFall); // hog explodes + if abs(subrate) > 2000 then + inc(rate, subrate) + end; + if Score > 0 then + inc(rate, KillScore * 1024 + (fallDmg)) // tiny bonus for dealing more damage than needed to kill + else + dec(rate, KillScore * friendlyfactor div 100 * 1024) + end + else if (fallDmg <> 0) then // non-deadly fall damage + if Score > 0 then + inc(rate, fallDmg * 1024) + else + dec(rate, fallDmg * friendlyfactor div 100 * 1024) + else // no damage, just shoved + if (Score < 0) then + dec(rate, 100); // small penalty for shoving friendly hogs as it might be dangerous + end; + end; + end; + +if hadSkips and (rate <= 0) then + RateSeduction:= BadTurn +else + RateSeduction:= rate * 1024; +end; + +function RateResurrector(Me: PGear): LongInt; +var i, r, rate, pX, pY: LongInt; + meX, meY: hwFloat; + hadSkips: boolean; +begin +meX:= Me^.X; +meY:= Me^.Y; +rate:= 0; +hadSkips:= false; +for i:= 0 to Targets.Count do + if (Targets.ar[i].Kind = gtGrave) and (not Targets.ar[i].dead) then + with Targets.ar[i] do + begin + pX:= Point.X; + pY:= Point.Y; + + if (not matters) then + hadSkips:= true + else if matters and (abs(pX - hwRound(meX)) + abs(pY - hwRound(meY)) < cResurrectorDist) then + begin + r:= trunc(sqrt(sqr(abs(pX - hwRound(meX)))+sqr(abs(pY - hwRound(meY))))); + if r < cResurrectorDist then + begin + if Score > 0 then + inc(rate, Score * 1024) + else + inc(rate, Score * friendlyFactor div 100 * 1024); + // a "dead" grave is a grave that we have resurrected + dead:= true; + Targets.reset:= true; + end; + end; + end; + +if hadSkips and (rate <= 0) then + RateResurrector:= BadTurn +else + RateResurrector:= rate * 1024; +end; + function RateHammer(Me: PGear): LongInt; var x, y, i, r, rate: LongInt; hadSkips: boolean; @@ -924,7 +1100,7 @@ if TestCollisionXwithGear(Gear, hwSign(Gear^.dX)) <> 0 then SetLittle(Gear^.dX); Gear^.X:= Gear^.X + Gear^.dX; inc(GoInfo.Ticks); - Gear^.dY:= Gear^.dY + cGravity; + Gear^.dY:= Gear^.dY + aiGravity; if Gear^.dY > _0_4 then exit(false); if (Gear^.dY.isNegative) and (TestCollisionYwithGear(Gear, -1) <> 0) then @@ -990,8 +1166,9 @@ if (Gear^.State and gstMoving) <> 0 then begin inc(GoInfo.Ticks); - Gear^.dY:= Gear^.dY + cGravity; - if Gear^.dY > _0_4 then + Gear^.dY:= Gear^.dY + aiGravity; + // taking fall damage? + if (Gear^.dY > _0_4) and (Gear^.Hedgehog^.Effects[heInvulnerable] = 0) then begin GoInfo.FallPix:= 0; // try ljump instead of fall with damage diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uAmmos.pas --- a/hedgewars/uAmmos.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uAmmos.pas Sun Mar 24 14:33:57 2024 -0400 @@ -113,7 +113,7 @@ // avoid things we already have by scheme // merge this into DisableSomeWeapons ? if ((a = amLowGravity) and ((GameFlags and gfLowGravity) <> 0)) - or ((a = amInvulnerable) and ((GameFlags and gfInvulnerable) <> 0)) + or ((a in [amInvulnerable, amResurrector, amVampiric]) and ((GameFlags and gfInvulnerable) <> 0)) or ((a = amLaserSight) and ((GameFlags and gfLaserSight) <> 0)) or ((a = amVampiric) and ((GameFlags and gfVampiric) <> 0)) or ((a = amExtraTime) and (cHedgehogTurnTime >= 1000000)) diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uChat.pas --- a/hedgewars/uChat.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uChat.pas Sun Mar 24 14:33:57 2024 -0400 @@ -27,6 +27,8 @@ procedure freeModule; procedure ReloadLines; procedure CleanupInput; +procedure CloseChat; +procedure RestoreChat; procedure AddChatString(s: shortstring); procedure DrawChat; procedure KeyPressChat(keysym: TSDL_Keysym); @@ -35,10 +37,11 @@ procedure TextInput(var event: TSDL_TextInputEvent); implementation -uses uInputHandler, uTypes, uVariables, uCommands, uUtils, uTextures, uRender, uIO, uScript, uRenderUtils, uLocale +uses uConsts, uInputHandler, uTypes, uVariables, uCommands, uUtils, uTextures, uRender, uIO, uScript, uRenderUtils, uStore, uLocale {$IFDEF USE_VIDEO_RECORDING}, uVideoRec{$ENDIF}; -const MaxStrIndex = 27; +const MaxStrIndex = 27; // Max. possible string index + MaxStrPartial = 7; // Max. displayed strings in normal mode MaxInputStrLen = 200; type TChatLine = record @@ -53,6 +56,7 @@ var Strs: array[0 .. MaxStrIndex] of TChatLine; MStrs: array[0 .. MaxStrIndex] of shortstring; LocalStrs: array[0 .. MaxStrIndex] of shortstring; + oldInput: shortstring; missedCount: LongWord; lastStr: LongWord; localLastStr: LongInt; @@ -94,8 +98,14 @@ ); -const Padding = 2; - ClHeight = 2 * Padding + 16; // font height +const PaddingFactor = 0.125; // relative to font size in pixels + +var Padding, ClHeight: integer; + LastChatScaleValue, LastUIScaleValue: real; + SkipNextInput: boolean; + +procedure UpdateInputLinePrefix(); forward; +procedure UpdateCursorCoords(); forward; // relevant for UTF-8 handling function IsFirstCharByte(c: char): boolean; inline; @@ -114,26 +124,106 @@ selectedPos:= -1; end; +procedure AdjustToUIScale(); +var fntSize, fntSizePx: integer; +begin + // don't do anything if no change + if (ChatScaleValue = LastChatScaleValue) and (UIScaleValue = LastUIScaleValue) then + exit; + + LastChatScaleValue:= ChatScaleValue; + LastUIScaleValue:= UIScaleValue; + + fntSize:= max(1, round(UIScaleValue * ChatScaleValue * cBaseChatFontHeight)); + + if Fontz[fntChat].Height <> fntSize then + begin + // adjust associated heights + Fontz[fntChat].Height:= fntSize; + Fontz[CJKfntChat].Height:= fntSize; + // reload if initialized already + if Fontz[fntChat].Handle <> nil then + LoadFont(fntChat); + if Fontz[CJKfntChat].Handle <> nil then + LoadFont(CJKfntChat); + end; + + // adjust line height etc. + fntSizePx:= round(cFontPxToPtRatio * fntSize); + Padding:= max(1, round(PaddingFactor * fntSizePx)); + + ClHeight:= 2 * Padding + fntSizePx; + + // clear cache of already rendered lines + ReloadLines(); + UpdateInputLinePrefix(); + UpdateCursorCoords(); +end; + +procedure ChatSizeInc(pxprecise: boolean); +var fntSize: integer; +begin +if pxprecise then + begin + fntSize:= Fontz[fntChat].Height; + inc(fntSize); + ChatScaleValue:= 1.0 * fntSize / cBaseChatFontHeight; + end +else + ChatScaleValue:= ChatScaleValue * (1.0 + cChatScaleRelDelta); +if ChatScaleValue > cMaxChatScaleValue then + ChatScaleValue:= cMaxChatScaleValue; +AdjustToUIScale(); +end; + +procedure ChatSizeDec(pxprecise: boolean); +var fntSize: integer; +begin +if pxprecise then + begin + fntSize:= Fontz[fntChat].Height; + dec(fntSize); + ChatScaleValue:= 1.0 * fntSize / cBaseChatFontHeight; + end +else + ChatScaleValue:= ChatScaleValue / (1.0 + cChatScaleRelDelta); +if ChatScaleValue < cMinChatScaleValue then + ChatScaleValue:= cMinChatScaleValue; +AdjustToUIScale(); +end; + +procedure chatSizeReset(); +begin +ChatScaleValue:= cDefaultChatScale; +AdjustToUIScale(); +end; + +function GetChatFont(str: shortstring): THWFONT; +begin + GetChatFont:= CheckCJKFont(ansistring(str), fntChat); +end; + procedure UpdateCursorCoords(); var font: THWFont; str : shortstring; coff, soff: LongInt; begin + AdjustToUIScale(); + if cursorPos = selectedPos then ResetSelection(); // calculate cursor offset str:= InputStr.s; - font:= CheckCJKFont(ansistring(str), fnt16); + font:= GetChatFont(str); // get only substring before cursor to determine length // SetLength(str, cursorPos); // makes pas2c unhappy str[0]:= char(cursorPos); // get render size of text TTF_SizeUTF8(Fontz[font].Handle, Str2PChar(str), @coff, nil); - - cursorX:= 2 + coff; + cursorX:= Padding + coff; // calculate selection width on screen if selectedPos >= 0 then @@ -171,7 +261,7 @@ FreeAndNilTexture(cl.Tex); -font:= CheckCJKFont(ansistring(str), fnt16); +font:= GetChatFont(str); // get render size of text TTF_SizeUTF8(Fontz[font].Handle, Str2PChar(str), @cl.Width, nil); @@ -245,8 +335,9 @@ procedure ReloadLines; var i: LongWord; begin - if InputStr.s <> '' then - SetLine(InputStr, InputStr.s, true); + // also reload empty input line (as chat size/scaling might have changed) + //if InputStr.s <> '' then + SetLine(InputStr, InputStr.s, true); for i:= 0 to MaxStrIndex do if Strs[i].s <> '' then begin @@ -294,19 +385,26 @@ selRect: TSDL_Rect; c: char; begin +AdjustToUIScale(); + ChatReady:= true; // maybe move to somewhere else? if ChatHidden and (not showAll) then visibleCount:= 0; // draw chat lines with some distance from screen border +left:= 4 - cScreenWidth div 2; {$IFDEF USE_TOUCH_INTERFACE} -left:= 4 - cScreenWidth div 2; -top := 55 + visibleCount * ClHeight; // we start with input line (if any) +i:= 55; {$ELSE} -left:= 4 - cScreenWidth div 2; -top := 10 + visibleCount * ClHeight; // we start with input line (if any) +i:= 10; {$ENDIF} +top := i + visibleCount * ClHeight; // we start with input line (if any) +if top > cScreenHeight - ClHeight - 60 then + begin + top:= cScreenHeight - ClHeight - 60; + top:= i + top - (top mod ClHeight); + end; // draw chat input line first and under all other lines if isInChatMode and (InputStr.Tex <> nil) then @@ -329,12 +427,12 @@ begin // draw cursor if ((RealTicks - LastKeyPressTick) and 512) < 256 then - DrawLineOnScreen(left + cursorX, top + 2, left + cursorX, top + ClHeight - 2, 2.0, $00, $FF, $FF, $FF); + DrawLineOnScreen(left + cursorX, top + Padding, left + cursorX, top + ClHeight - Padding, max(2, round(UIScaleValue * ChatScaleValue * 2.0)), $00, $FF, $FF, $FF); end else // draw selection begin - selRect.y:= top + 2; - selRect.h:= clHeight - 4; + selRect.y:= top + Padding; + selRect.h:= clHeight - 2 * Padding; if selectionDx < 0 then begin selRect.x:= left + cursorX + selectionDx; @@ -395,7 +493,7 @@ t := 1; // # of current line processed // draw lines in reverse order - while (((t < 7) and (Strs[i].Time > RealTicks)) or ((t <= MaxStrIndex + 1) and showAll)) + while (((t < MaxStrPartial) and (Strs[i].Time > RealTicks)) or ((t <= MaxStrIndex + 1) and showAll)) and (Strs[i].Tex <> nil) do begin top:= top - ClHeight; @@ -538,6 +636,18 @@ exit end; + if (copy(s, 2, 3) = 'ff ') then + begin + ParseCommand(s, true); + exit + end; + + if (copy(s, 2, 3) = 'sff') then + begin + ParseCommand(s, true); + exit + end; + // debugging commands if (copy(s, 2, 7) = 'debugvl') then // This command intentionally not documented in /help @@ -589,12 +699,15 @@ AddChatString(#3 + shortstring(trcmd[sidCmdHsa])); AddChatString(#3 + shortstring(trcmd[sidCmdHta])); AddChatString(#3 + shortstring(trcmd[sidCmdHya])); + AddChatString(#3 + shortstring(trcmd[sidCmdHappy])); + AddChatString(#3 + shortstring(trcmd[sidCmdWave])); AddChatString(#3 + shortstring(trcmd[sidCmdHurrah])); + AddChatString(#3 + shortstring(trcmd[sidCmdShrug])); + AddChatString(#3 + shortstring(trcmd[sidCmdSad])); AddChatString(#3 + shortstring(trcmd[sidCmdIlovelotsoflemonade])); AddChatString(#3 + shortstring(trcmd[sidCmdJuggle])); AddChatString(#3 + shortstring(trcmd[sidCmdRollup])); - AddChatString(#3 + shortstring(trcmd[sidCmdShrug])); - AddChatString(#3 + shortstring(trcmd[sidCmdWave])); + AddChatString(#3 + shortstring(trcmd[sidCmdBubble])); exit end; @@ -632,15 +745,14 @@ end; // hedghog animations/taunts and engine commands - if (not CurrentTeam^.ExtDriven) and (CurrentTeam^.Hedgehogs[0].BotLevel = 0) then - begin - for i:= Low(TWave) to High(TWave) do - if (s = Wavez[i].cmd) then - begin + for i:= Low(TWave) to High(TWave) do + if (s = Wavez[i].cmd) then + begin + // only works for local non-bot teams + if (not CurrentTeam^.ExtDriven) and (CurrentTeam^.Hedgehogs[0].BotLevel = 0) then ParseCommand('/taunt ' + char(i), true); - exit - end; - end; + exit; + end; for j:= Low(TChatCmd) to High(TChatCmd) do if (s = ChatCommandz[j].ChatCmd) then @@ -673,6 +785,45 @@ ResetKbd; end; +procedure OpenChat(s: shortstring); +var i: Integer; +begin + if GameState = gsConfirm then + ParseCommand('quit', true); + isInChatMode:= true; + SDL_StopTextInput(); + SDL_StartTextInput(); + //Make REALLY sure unexpected events are flushed (1 time is insufficient as of SDL 2.0.7) + for i := 1 to 2 do + begin + SDL_PumpEvents(); + SDL_FlushEvent(SDL_TEXTINPUT); + end; + if length(s) = 0 then + SetLine(InputStr, '', true) + else + begin + SetLine(InputStr, s, true); + cursorPos:= length(s); + UpdateCursorCoords(); + end; +end; + +procedure CloseChat; +begin + oldInput:= InputStr.s; + SetLine(InputStr, '', true); + ResetCursor(); + CleanupInput(); +end; + +procedure RestoreChat; +begin + if length(oldInput) > 0 then + OpenChat(oldInput); + oldInput:= ''; +end; + procedure DelBytesFromInputStrBack(endIdx: integer; count: byte); var startIdx: integer; begin @@ -953,7 +1104,9 @@ SetLine(InputStr, '', true); ResetCursor(); end - else CleanupInput + else + CleanupInput; + oldInput:= ''; end; SDL_SCANCODE_RETURN, SDL_SCANCODE_KP_ENTER: begin @@ -1102,6 +1255,33 @@ DeleteSelected(); end end; + // make chat bigger + SDL_SCANCODE_KP_PLUS, SDL_SCANCODE_EQUALS: + begin + if ctrl then + begin + ChatSizeInc(selMode); + SkipNextInput:= true; + end; + end; + // make chat smaller + SDL_SCANCODE_KP_MINUS, SDL_SCANCODE_MINUS: + begin + if ctrl then + begin + ChatSizeDec(selMode); + SkipNextInput:= true; + end; + end; + // revert chat size to default + SDL_SCANCODE_KP_0, SDL_SCANCODE_0: + begin + if ctrl then + begin + ChatSizeReset(); + SkipNextInput:= true; + end; + end; end; end; @@ -1110,16 +1290,25 @@ l: byte; isl: integer; begin + if SkipNextInput then + begin + SkipNextInput:= false; + exit; + end; + DeleteSelected(); + s:= ''; l:= 0; // fetch all bytes of character/input while event.text[l] <> #0 do begin - s[l + 1]:= event.text[l]; - inc(l) + if Length(s) < 255 then + begin + s[l + 1]:= event.text[l]; + inc(l) + end end; - if l > 0 then begin isl:= Length(InputStr.s); @@ -1192,27 +1381,12 @@ end; procedure chChat(var s: shortstring); -var i: Integer; begin s:= s; // avoid compiler hint - isInChatMode:= true; - SDL_StopTextInput(); - SDL_StartTextInput(); - //Make REALLY sure unexpected events are flushed (1 time is insufficient as of SDL 2.0.7) - for i := 1 to 2 do - begin - SDL_PumpEvents(); - SDL_FlushEvent(SDL_TEXTINPUT); - end; - //SDL_EnableKeyRepeat(200,45); if length(s) = 0 then - SetLine(InputStr, '', true) + OpenChat('') else - begin - SetLine(InputStr, '/clan ', true); - cursorPos:= 6; - UpdateCursorCoords(); - end; + OpenChat('/clan '); end; procedure initModule; @@ -1235,6 +1409,10 @@ ChatHidden:= false; firstDraw:= true; + LastChatScaleValue:= 0; + LastUIScaleValue:= 0; + SkipNextInput:= false; + InputLinePrefix.Tex:= nil; UpdateInputLinePrefix(); inputStr.s:= ''; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uCollisions.pas --- a/hedgewars/uCollisions.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uCollisions.pas Sun Mar 24 14:33:57 2024 -0400 @@ -52,6 +52,11 @@ cX, cY: LongInt; //for visual effects only end; +type TKickTest = record + kick: Boolean; + collisionMask: Word; + end; + procedure initModule; procedure freeModule; @@ -65,28 +70,37 @@ function CheckGearsLineCollision(Gear: PGear; oX, oY, tX, tY: hwFloat): PGearArray; function CheckAllGearsLineCollision(SourceGear: PGear; oX, oY, tX, tY: hwFloat): PGearArray; -function UpdateHitOrder(Gear: PGear; Order: LongInt): boolean; -procedure ClearHitOrderLeq(MinOrder: LongInt); +function UpdateHitOrder(Gear: PGear; Order: LongInt): boolean; inline; +function UpdateHitOrder(Gear: PGear; Order: LongInt; Global: boolean): boolean; inline; +function UpdateGlobalHitOrder(Gear: PGear; Order: LongInt): boolean; inline; +procedure ClearHitOrderLeq(MinOrder: LongInt); inline; +procedure ClearGlobalHitOrderLeq(MinOrder: LongInt); inline; procedure ClearHitOrder(); procedure RefillProximityCache(SourceGear: PGear; radius: LongInt); procedure RemoveFromProximityCache(Gear: PGear); procedure ClearProximityCache(); -function TestCollisionXwithGear(Gear: PGear; Dir: LongInt): Word; -function TestCollisionYwithGear(Gear: PGear; Dir: LongInt): Word; +function TestCollisionXImpl(centerX, centerY, radius, direction: LongInt; collisionMask: Word): Word; +function TestCollisionYImpl(centerX, centerY, radius, direction: LongInt; collisionMask: Word): Word; + +function TestCollisionXwithGear(Gear: PGear; Dir: LongInt): Word; inline; +function TestCollisionYwithGear(Gear: PGear; Dir: LongInt): Word; inline; + +function TestCollisionX(Gear: PGear; Dir: LongInt): Word; inline; +function TestCollisionY(Gear: PGear; Dir: LongInt): Word; inline; + +function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt): Word; inline; +function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; inline; +function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt): Word; inline; +function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; inline; + +function TestCollisionXKickImpl(centerX, centerY, radius, direction: LongInt; collisionMask, kickMask: Word): TKickTest; +function TestCollisionYKickImpl(centerX, centerY, radius, direction: LongInt; collisionMask, kickMask: Word): TKickTest; function TestCollisionXKick(Gear: PGear; Dir: LongInt): Word; function TestCollisionYKick(Gear: PGear; Dir: LongInt): Word; -function TestCollisionX(Gear: PGear; Dir: LongInt): Word; -function TestCollisionY(Gear: PGear; Dir: LongInt): Word; - -function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt): Word; inline; -function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; -function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt): Word; inline; -function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; - function TestRectangleForObstacle(x1, y1, x2, y2: LongInt; landOnly: boolean): boolean; function CheckCoordInWater(X, Y: LongInt): boolean; inline; @@ -111,6 +125,7 @@ cinfos: array[0..MAXRECTSINDEX] of TCollisionEntry; ga: TGearArray; ordera: TGearHitOrder; + globalordera: TGearHitOrder; proximitya: TGearProximityCache; procedure AddCI(Gear: PGear); @@ -329,51 +344,80 @@ end; end; -function UpdateHitOrder(Gear: PGear; Order: LongInt): boolean; +function UpdateHitOrderImpl(HitOrder: PGearHitOrder; Gear: PGear; Order: LongInt): boolean; var i: LongInt; begin -UpdateHitOrder:= true; -for i:= 0 to ordera.Count - 1 do - if ordera.ar[i] = Gear then + UpdateHitOrderImpl:= true; + for i := 0 to HitOrder^.Count - 1 do + if HitOrder^.ar[i] = Gear then begin - if Order <= ordera.order[i] then UpdateHitOrder:= false; - ordera.order[i]:= Max(ordera.order[i], order); - exit; + if Order <= HitOrder^.order[i] then + UpdateHitOrderImpl := false; + HitOrder^.order[i] := Max(HitOrder^.order[i], Order); + exit; end; -if ordera.Count > cMaxGearHitOrderInd then - UpdateHitOrder:= false -else + if HitOrder^.Count > cMaxGearHitOrderInd then + UpdateHitOrderImpl := false + else begin - ordera.ar[ordera.Count]:= Gear; - ordera.order[ordera.Count]:= Order; - Inc(ordera.Count); + HitOrder^.ar[HitOrder^.Count] := Gear; + HitOrder^.order[HitOrder^.Count] := Order; + Inc(HitOrder^.Count); end end; -procedure ClearHitOrderLeq(MinOrder: LongInt); +function UpdateHitOrder(Gear: PGear; Order: LongInt): boolean; inline; +begin + UpdateHitOrder := UpdateHitOrderImpl(@ordera, Gear, Order); +end; + +function UpdateHitOrder(Gear: PGear; Order: LongInt; Global: boolean): boolean; inline; +begin + if Global then + UpdateHitOrder := UpdateHitOrderImpl(@globalordera, Gear, Order) + else + UpdateHitOrder := UpdateHitOrderImpl(@ordera, Gear, Order) +end; + +function UpdateGlobalHitOrder(Gear: PGear; Order: LongInt): boolean; inline; +begin + UpdateGlobalHitOrder := UpdateHitOrderImpl(@globalordera, Gear, Order); +end; + +procedure ClearHitOrderLeqImpl(HitOrder: PGearHitOrder; MinOrder: LongInt); var i, freeIndex: LongInt; begin; -freeIndex:= 0; -i:= 0; + freeIndex:= 0; + i:= 0; -while i < ordera.Count do + while i < HitOrder^.Count do begin - if ordera.order[i] <= MinOrder then - Dec(ordera.Count) + if HitOrder^.order[i] <= MinOrder then + Dec(HitOrder^.Count) else + begin + if freeIndex < i then begin - if freeIndex < i then - begin - ordera.ar[freeIndex]:= ordera.ar[i]; - ordera.order[freeIndex]:= ordera.order[i]; - end; + HitOrder^.ar[freeIndex]:= HitOrder^.ar[i]; + HitOrder^.order[freeIndex]:= HitOrder^.order[i]; + end; Inc(freeIndex); - end; + end; Inc(i) end end; +procedure ClearHitOrderLeq(MinOrder: LongInt); inline; +begin + ClearHitOrderLeqImpl(@ordera, MinOrder); +end; + +procedure ClearGlobalHitOrderLeq(MinOrder: LongInt); inline; +begin + ClearHitOrderLeqImpl(@globalordera, MinOrder); +end; + procedure ClearHitOrder(); begin ordera.Count:= 0; @@ -423,194 +467,110 @@ proximitya.Count:= 0; end; -function TestCollisionXwithGear(Gear: PGear; Dir: LongInt): Word; -var x, y, i: LongInt; +function TestCollisionXImpl(centerX, centerY, radius, direction: LongInt; collisionMask: Word): Word; +var x, y, minY, maxY: LongInt; begin -// Special case to emulate the old intersect gear clearing, but with a bit of slop for pixel overlap -if (Gear^.CollisionMask = lfNotCurHogCrate) and (Gear^.Kind <> gtHedgehog) and (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) and - ((hwRound(Gear^.Hedgehog^.Gear^.X) + Gear^.Hedgehog^.Gear^.Radius + 16 < hwRound(Gear^.X) - Gear^.Radius) or - (hwRound(Gear^.Hedgehog^.Gear^.X) - Gear^.Hedgehog^.Gear^.Radius - 16 > hwRound(Gear^.X) + Gear^.Radius)) then - Gear^.CollisionMask:= lfAll; + if direction < 0 then + x := centerX - radius + else + x := centerX + radius; -x:= hwRound(Gear^.X); -if Dir < 0 then - x:= x - Gear^.Radius -else - x:= x + Gear^.Radius; - -if (x and LAND_WIDTH_MASK) = 0 then + if (x and LAND_WIDTH_MASK) = 0 then begin - y:= hwRound(Gear^.Y) - Gear^.Radius + 1; - i:= y + Gear^.Radius * 2 - 2; - repeat - if (y and LAND_HEIGHT_MASK) = 0 then - if Land[y, x] and Gear^.CollisionMask <> 0 then - exit(Land[y, x] and Gear^.CollisionMask); - inc(y) - until (y > i); + minY := max(centerY - radius + 1, 0); + maxY := min(centerY + radius - 1, LAND_HEIGHT - 1); + for y := minY to maxY do + if Land[y, x] and collisionMask <> 0 then + exit(Land[y, x] and collisionMask); end; -TestCollisionXwithGear:= 0 + TestCollisionXImpl := 0; end; -function TestCollisionYwithGear(Gear: PGear; Dir: LongInt): Word; -var x, y, i: LongInt; +function TestCollisionYImpl(centerX, centerY, radius, direction: LongInt; collisionMask: Word): Word; +var x, y, minX, maxX: LongInt; begin -// Special case to emulate the old intersect gear clearing, but with a bit of slop for pixel overlap -if (Gear^.CollisionMask = lfNotCurHogCrate) and (Gear^.Kind <> gtHedgehog) and (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) and - ((hwRound(Gear^.Hedgehog^.Gear^.Y) + Gear^.Hedgehog^.Gear^.Radius + 16 < hwRound(Gear^.Y) - Gear^.Radius) or - (hwRound(Gear^.Hedgehog^.Gear^.Y) - Gear^.Hedgehog^.Gear^.Radius - 16 > hwRound(Gear^.Y) + Gear^.Radius)) then - Gear^.CollisionMask:= lfAll; - -y:= hwRound(Gear^.Y); -if Dir < 0 then - y:= y - Gear^.Radius -else - y:= y + Gear^.Radius; + if direction < 0 then + y := centerY - radius + else + y := centerY + radius; -if (y and LAND_HEIGHT_MASK) = 0 then + if (y and LAND_HEIGHT_MASK) = 0 then begin - x:= hwRound(Gear^.X) - Gear^.Radius + 1; - i:= x + Gear^.Radius * 2 - 2; - repeat - if (x and LAND_WIDTH_MASK) = 0 then - if Land[y, x] and Gear^.CollisionMask <> 0 then - begin - exit(Land[y, x] and Gear^.CollisionMask) - end; - inc(x) - until (x > i); + minX := max(centerX - radius + 1, 0); + maxX := min(centerX + radius - 1, LAND_WIDTH - 1); + for x := minX to maxX do + if Land[y, x] and collisionMask <> 0 then + exit(Land[y, x] and collisionMask); end; -TestCollisionYwithGear:= 0 + TestCollisionYImpl := 0; +end; + +function TestCollisionX(Gear: PGear; Dir: LongInt): Word; inline; +begin + TestCollisionX := TestCollisionXImpl(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Radius, Dir, Gear^.CollisionMask and lfLandMask); +end; + +function TestCollisionY(Gear: PGear; Dir: LongInt): Word; inline; +begin + TestCollisionY := TestCollisionYImpl(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Radius, Dir, Gear^.CollisionMask and lfLandMask); end; -function TestCollisionXKick(Gear: PGear; Dir: LongInt): Word; -var x, y, mx, my, i: LongInt; - pixel: Word; +procedure LegacyFixupX(Gear: PGear); begin -pixel:= 0; -x:= hwRound(Gear^.X); -if Dir < 0 then - x:= x - Gear^.Radius -else - x:= x + Gear^.Radius; - -if (x and LAND_WIDTH_MASK) = 0 then - begin - y:= hwRound(Gear^.Y) - Gear^.Radius + 1; - i:= y + Gear^.Radius * 2 - 2; - repeat - if (y and LAND_HEIGHT_MASK) = 0 then - begin - if Land[y, x] and Gear^.CollisionMask <> 0 then - begin - if ((Land[y, x] and Gear^.CollisionMask) and lfLandMask) <> 0 then - exit(Land[y, x] and Gear^.CollisionMask) - else - pixel:= Land[y, x] and Gear^.CollisionMask; - end; - end; - inc(y) - until (y > i); - end; -TestCollisionXKick:= pixel; +// Special case to emulate the old intersect gear clearing, but with a bit of slop for pixel overlap + if (Gear^.CollisionMask = lfNotCurHogCrate) and (Gear^.Kind <> gtHedgehog) and (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) and + ((hwRound(Gear^.Hedgehog^.Gear^.X) + Gear^.Hedgehog^.Gear^.Radius + 16 < hwRound(Gear^.X) - Gear^.Radius) or + (hwRound(Gear^.Hedgehog^.Gear^.X) - Gear^.Hedgehog^.Gear^.Radius - 16 > hwRound(Gear^.X) + Gear^.Radius)) then + Gear^.CollisionMask:= lfAll; +end; -if pixel <> 0 then - begin - if hwAbs(Gear^.dX) < cHHKick then - exit; - if (Gear^.State and gstHHJumping <> 0) - and (hwAbs(Gear^.dX) < _0_4) then - exit; - - mx:= hwRound(Gear^.X); - my:= hwRound(Gear^.Y); +procedure LegacyFixupY(Gear: PGear); +begin +// Special case to emulate the old intersect gear clearing, but with a bit of slop for pixel overlap + if (Gear^.CollisionMask = lfNotCurHogCrate) and (Gear^.Kind <> gtHedgehog) and (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) and + ((hwRound(Gear^.Hedgehog^.Gear^.Y) + Gear^.Hedgehog^.Gear^.Radius + 16 < hwRound(Gear^.Y) - Gear^.Radius) or + (hwRound(Gear^.Hedgehog^.Gear^.Y) - Gear^.Hedgehog^.Gear^.Radius - 16 > hwRound(Gear^.Y) + Gear^.Radius)) then + Gear^.CollisionMask:= lfAll; +end; - for i:= 0 to Pred(Count) do - with cinfos[i] do - if (Gear <> cGear) and - ((mx > x) xor (Dir > 0)) and - ( - ((cGear^.Kind in [gtHedgehog, gtMine, gtKnife]) and ((Gear^.State and gstNotKickable) = 0)) or - // only apply X kick if the barrel is knocked over - ((cGear^.Kind = gtExplosives) and ((cGear^.State and gsttmpflag) <> 0)) - ) and - (sqr(mx - x) + sqr(my - y) <= sqr(Radius + Gear^.Radius + 2)) then - begin - with cGear^ do - begin - dX:= Gear^.dX; - dY:= Gear^.dY * _0_5; - State:= State or gstMoving; - if Kind = gtKnife then State:= State and (not gstCollision); - Active:= true - end; - DeleteCI(cGear); - exit(0); - end - end +function TestCollisionXwithGear(Gear: PGear; Dir: LongInt): Word; inline; +begin + LegacyFixupX(Gear); + TestCollisionXwithGear:= TestCollisionXImpl(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Radius, Dir, Gear^.CollisionMask); end; -function TestCollisionYKick(Gear: PGear; Dir: LongInt): Word; -var x, y, mx, my, myr, i: LongInt; - pixel: Word; +function TestCollisionYwithGear(Gear: PGear; Dir: LongInt): Word; inline; begin -pixel:= 0; -y:= hwRound(Gear^.Y); -if Dir < 0 then - y:= y - Gear^.Radius -else - y:= y + Gear^.Radius; + LegacyFixupY(Gear); + TestCollisionYwithGear:= TestCollisionYImpl(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Radius, Dir, Gear^.CollisionMask); +end; -if (y and LAND_HEIGHT_MASK) = 0 then - begin - x:= hwRound(Gear^.X) - Gear^.Radius + 1; - i:= x + Gear^.Radius * 2 - 2; - repeat - if (x and LAND_WIDTH_MASK) = 0 then - if Land[y, x] > 0 then - begin - if ((Land[y, x] and Gear^.CollisionMask) and lfLandMask) <> 0 then - exit(Land[y, x] and Gear^.CollisionMask) - else // if Land[y, x] <> 0 then - pixel:= Land[y, x] and Gear^.CollisionMask; - end; - inc(x) - until (x > i); - end; -TestCollisionYKick:= pixel; - -if pixel <> 0 then +function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; inline; +var collisionMask: Word; +begin + if withGear then begin - if hwAbs(Gear^.dY) < cHHKick then - exit; - if (Gear^.State and gstHHJumping <> 0) and (not Gear^.dY.isNegative) and (Gear^.dY < _0_4) then - exit; + LegacyFixupX(Gear); + collisionMask:= Gear^.CollisionMask; + end + else + collisionMask:= Gear^.CollisionMask and lfLandMask; - mx:= hwRound(Gear^.X); - my:= hwRound(Gear^.Y); - myr:= my+Gear^.Radius; + TestCollisionXwithXYShift := TestCollisionXImpl(hwRound(Gear^.X + ShiftX), hwRound(Gear^.Y) + ShiftY, Gear^.Radius, Dir, collisionMask) +end; - for i:= 0 to Pred(Count) do - with cinfos[i] do - if (Gear <> cGear) and - ((myr > y) xor (Dir > 0)) and - (Gear^.State and gstNotKickable = 0) and - (cGear^.Kind in [gtHedgehog, gtMine, gtKnife, gtExplosives]) and - (sqr(mx - x) + sqr(my - y) <= sqr(Radius + Gear^.Radius + 2)) then - begin - with cGear^ do - begin - if (Kind <> gtExplosives) or ((State and gsttmpflag) <> 0) then - dX:= Gear^.dX * _0_5; - dY:= Gear^.dY; - State:= State or gstMoving; - if Kind = gtKnife then State:= State and (not gstCollision); - Active:= true - end; - DeleteCI(cGear); - exit(0) - end +function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; inline; +var collisionMask: Word; +begin + if withGear then + begin + LegacyFixupY(Gear); + collisionMask:= Gear^.CollisionMask; end + else + collisionMask:= Gear^.CollisionMask and lfLandMask; + + TestCollisionYwithXYShift := TestCollisionYImpl(hwRound(Gear^.X) + ShiftX, hwRound(Gear^.Y) + ShiftY, Gear^.Radius, Dir, collisionMask) end; function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt): Word; inline; @@ -618,80 +578,163 @@ TestCollisionXwithXYShift:= TestCollisionXwithXYShift(Gear, ShiftX, ShiftY, Dir, true); end; -function TestCollisionXwithXYShift(Gear: PGear; ShiftX: hwFloat; ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; -begin -Gear^.X:= Gear^.X + ShiftX; -Gear^.Y:= Gear^.Y + int2hwFloat(ShiftY); -if withGear then - TestCollisionXwithXYShift:= TestCollisionXwithGear(Gear, Dir) -else TestCollisionXwithXYShift:= TestCollisionX(Gear, Dir); -Gear^.X:= Gear^.X - ShiftX; -Gear^.Y:= Gear^.Y - int2hwFloat(ShiftY) -end; - -function TestCollisionX(Gear: PGear; Dir: LongInt): Word; -var x, y, i: LongInt; -begin -x:= hwRound(Gear^.X); -if Dir < 0 then - x:= x - Gear^.Radius -else - x:= x + Gear^.Radius; - -if (x and LAND_WIDTH_MASK) = 0 then - begin - y:= hwRound(Gear^.Y) - Gear^.Radius + 1; - i:= y + Gear^.Radius * 2 - 2; - repeat - if (y and LAND_HEIGHT_MASK) = 0 then - if ((Land[y, x] and Gear^.CollisionMask) and lfLandMask) <> 0 then - exit(Land[y, x] and Gear^.CollisionMask); - inc(y) - until (y > i); - end; -TestCollisionX:= 0 -end; - -function TestCollisionY(Gear: PGear; Dir: LongInt): Word; -var x, y, i: LongInt; -begin -y:= hwRound(Gear^.Y); -if Dir < 0 then - y:= y - Gear^.Radius -else - y:= y + Gear^.Radius; - -if (y and LAND_HEIGHT_MASK) = 0 then - begin - x:= hwRound(Gear^.X) - Gear^.Radius + 1; - i:= x + Gear^.Radius * 2 - 2; - repeat - if (x and LAND_WIDTH_MASK) = 0 then - if ((Land[y, x] and Gear^.CollisionMask) and lfLandMask) <> 0 then - exit(Land[y, x] and Gear^.CollisionMask); - inc(x) - until (x > i); - end; -TestCollisionY:= 0 -end; - function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt): Word; inline; begin TestCollisionYwithXYShift:= TestCollisionYwithXYShift(Gear, ShiftX, ShiftY, Dir, true); end; -function TestCollisionYwithXYShift(Gear: PGear; ShiftX, ShiftY: LongInt; Dir: LongInt; withGear: boolean): Word; +function TestCollisionXKickImpl(centerX, centerY, radius, direction: LongInt; collisionMask, kickMask: Word): TKickTest; +var x, y, minY, maxY: LongInt; +begin + TestCollisionXKickImpl.kick := false; + TestCollisionXKickImpl.collisionMask := 0; + + if direction < 0 then + x := centerX - radius + else + x := centerX + radius; + + if (x and LAND_WIDTH_MASK) = 0 then + begin + minY := max(centerY - radius + 1, 0); + maxY := min(centerY + radius - 1, LAND_HEIGHT - 1); + for y := minY to maxY do + if Land[y, x] and collisionMask <> 0 then + begin + TestCollisionXKickImpl.kick := false; + TestCollisionXKickImpl.collisionMask := Land[y, x] and collisionMask; + exit + end + else if Land[y, x] and kickMask <> 0 then + begin + TestCollisionXKickImpl.kick := true; + TestCollisionXKickImpl.collisionMask := Land[y, x] and kickMask; + end; + end; +end; + +function TestCollisionYKickImpl(centerX, centerY, radius, direction: LongInt; collisionMask, kickMask: Word): TKickTest; +var x, y, minX, maxX: LongInt; begin -Gear^.X:= Gear^.X + int2hwFloat(ShiftX); -Gear^.Y:= Gear^.Y + int2hwFloat(ShiftY); + TestCollisionYKickImpl.kick := false; + TestCollisionYKickImpl.collisionMask := 0; + + if direction < 0 then + y := centerY - radius + else + y := centerY + radius; + + if (y and LAND_HEIGHT_MASK) = 0 then + begin + minX := max(centerX - radius + 1, 0); + maxX := min(centerX + radius - 1, LAND_WIDTH - 1); + for x := minX to maxX do + if Land[y, x] and collisionMask <> 0 then + begin + TestCollisionYKickImpl.kick := false; + TestCollisionYKickImpl.collisionMask := Land[y, x] and collisionMask; + exit + end + else if Land[y, x] and kickMask <> 0 then + begin + TestCollisionYKickImpl.kick := true; + TestCollisionYKickImpl.collisionMask := Land[y, x] and kickMask; + end; + end; +end; + +function TestCollisionXKick(Gear: PGear; Dir: LongInt): Word; +var centerX, centerY, i: LongInt; + test: TKickTest; + info: TCollisionEntry; +begin + test := TestCollisionXKickImpl( + hwRound(Gear^.X), hwRound(Gear^.Y), + Gear^.Radius, Dir, + Gear^.CollisionMask and lfLandMask, Gear^.CollisionMask); + + TestCollisionXKick := test.collisionMask; -if withGear then - TestCollisionYwithXYShift:= TestCollisionYwithGear(Gear, Dir) -else - TestCollisionYwithXYShift:= TestCollisionY(Gear, Dir); + if test.kick then + begin + if hwAbs(Gear^.dX) < cHHKick then + exit; + if ((Gear^.State and gstHHJumping) <> 0) and (hwAbs(Gear^.dX) < _0_4) then + exit; + + centerX := hwRound(Gear^.X); + centerY := hwRound(Gear^.Y); + + for i:= 0 to Pred(Count) do + begin + info:= cinfos[i]; + if (Gear <> info.cGear) + and ((centerX > info.X) xor (Dir > 0)) + and ((info.cGear^.State and gstNotKickable) = 0) + and ((info.cGear^.Kind in [gtHedgehog, gtMine, gtKnife, gtSentry]) + or (info.cGear^.Kind = gtExplosives) and ((info.cGear^.State and gsttmpflag) <> 0)) // only apply X kick if the barrel is knocked over + and (sqr(centerX - info.X) + sqr(centerY - info.Y) <= sqr(info.Radius + Gear^.Radius + 2)) then + begin + with info.cGear^ do + begin + dX := Gear^.dX; + dY := Gear^.dY * _0_5; + State := State or gstMoving; + if Kind = gtKnife then State := State and (not gstCollision); + Active:= true + end; + DeleteCI(info.cGear); + exit(0) + end + end + end +end; -Gear^.X:= Gear^.X - int2hwFloat(ShiftX); -Gear^.Y:= Gear^.Y - int2hwFloat(ShiftY) +function TestCollisionYKick(Gear: PGear; Dir: LongInt): Word; +var centerX, centerY, i: LongInt; + test: TKickTest; + info: TCollisionEntry; +begin + test := TestCollisionYKickImpl( + hwRound(Gear^.X), hwRound(Gear^.Y), + Gear^.Radius, Dir, + Gear^.CollisionMask and lfLandMask, Gear^.CollisionMask); + + TestCollisionYKick := test.collisionMask; + + if test.kick then + begin + if hwAbs(Gear^.dY) < cHHKick then + exit; + if ((Gear^.State and gstHHJumping) <> 0) and (not Gear^.dY.isNegative) and (Gear^.dY < _0_4) then + exit; + + centerX := hwRound(Gear^.X); + centerY := hwRound(Gear^.Y); + + for i := 0 to Pred(Count) do + begin + info := cinfos[i]; + if (Gear <> info.cGear) + and ((centerY + Gear^.Radius > info.Y) xor (Dir > 0)) + and (info.cGear^.State and gstNotKickable = 0) + and (info.cGear^.Kind in [gtHedgehog, gtMine, gtKnife, gtExplosives, gtSentry]) + and (sqr(centerX - info.X) + sqr(centerY - info.Y) <= sqr(info.Radius + Gear^.Radius + 2)) then + begin + with info.cGear^ do + begin + if (Kind <> gtExplosives) or ((State and gsttmpflag) <> 0) then + dX := Gear^.dX * _0_5; + dY := Gear^.dY; + State := State or gstMoving; + if Kind = gtKnife then State:= State and (not gstCollision); + Active := true + end; + DeleteCI(info.cGear); + exit(0) + end + end + end end; function TestRectangleForObstacle(x1, y1, x2, y2: LongInt; landOnly: boolean): boolean; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uCommandHandlers.pas --- a/hedgewars/uCommandHandlers.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uCommandHandlers.pas Sun Mar 24 14:33:57 2024 -0400 @@ -27,7 +27,7 @@ implementation uses uCommands, uTypes, uVariables, uIO, uDebug, uConsts, uScript, uUtils, SDLh, uWorld, uRandom, uCaptions - , uVisualGearsList, uGearsHedgehog + , uVisualGearsList, uGearsHedgehog, uChat {$IFDEF USE_VIDEO_RECORDING}, uVideoRec {$ENDIF}; var cTagsMasks : array[0..15] of byte = (7, 0, 0, 0, 0, 4, 5, 6, 15, 8, 8, 8, 8, 12, 13, 14); @@ -49,15 +49,16 @@ begin s:= s; // avoid compiler hint if (GameState = gsGame) then - begin - isInChatMode:= false; + begin + CloseChat; GameState:= gsConfirm; - end - else begin + end + else if GameState = gsConfirm then + begin GameState:= gsGame; - end; - + RestoreChat; + end; updateCursorVisibility; end; @@ -856,6 +857,11 @@ cAirMines:= StrToInt(s) end; +procedure chSentries(var s: shortstring); +begin +cSentries:= StrToInt(s) +end; + procedure chExplosives(var s: shortstring); begin cExplosives:= StrToInt(s) @@ -880,6 +886,7 @@ procedure chFastUntilLag(var s: shortstring); begin fastUntilLag:= StrToInt(s) <> 0; + fastForward:= fastUntilLag; if not fastUntilLag then begin @@ -889,6 +896,53 @@ end end; +procedure chFastForward(var cmd: shortstring); +var str0, str1, str2 : shortstring; + h, m, s : integer; +begin + if gameType <> gmtDemo then + exit; + if CountChar(cmd, ':') > 2 then + exit; + str0:= cmd; + SplitByChar(str0, str1, ':'); + SplitByChar(str1, str2, ':'); + if str2 <> '' then + begin + h:= StrToInt(str0); + m:= StrToInt(str1); + s:= StrToInt(str2) + end + else if str1 <> '' then + begin + h:= 0; + m:= StrToInt(str0); + s:= StrToInt(str1) + end + else + begin + h:= 0; + m:= 0; + s:= StrToInt(str0) + end; + FFGameTick:= (s + m * 60 + h * 60 * 60) * 1000; + if FFGameTick > GameTicks then + begin + fastUntilLag:= True; + fastForward:= True; + end +end; + +procedure chStopFastForward(var s: shortstring); +begin + if gameType <> gmtDemo then + exit; + fastUntilLag:= False; + fastForward:= False; + AddVisualGear(0, 0, vgtTeamHealthSorter); + AddVisualGear(0, 0, vgtSmoothWindBar) +end; + procedure chCampVar(var s:shortstring); begin CampaignVariable := s; @@ -980,6 +1034,7 @@ RegisterVariable('minedudpct',@chMineDudPercent, false); RegisterVariable('minesnum', @chLandMines , false); RegisterVariable('airmines', @chAirMines , false); + RegisterVariable('sentries', @chSentries , false); RegisterVariable('explosives',@chExplosives , false); RegisterVariable('gmflags' , @chGameFlags , false); RegisterVariable('turntime', @chHedgehogTurnTime, false); @@ -1020,6 +1075,8 @@ RegisterVariable('record' , @chRecord , true ); RegisterVariable('worldedge',@chWorldEdge , false); RegisterVariable('advmapgen',@chAdvancedMapGenMode, false); + RegisterVariable('ff' , @chFastForward , true); + RegisterVariable('sff' , @chStopFastForward, true); RegisterVariable('+mission', @chShowMission_p, true); RegisterVariable('-mission', @chShowMission_m, true); RegisterVariable('gearinfo', @chGearInfo , true ); diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uConsts.pas --- a/hedgewars/uConsts.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uConsts.pas Sun Mar 24 14:33:57 2024 -0400 @@ -171,6 +171,7 @@ cVisibleWater : LongInt = 128; cTeamHealthWidth : LongInt = 128; + cTeamHealthHeight : LongInt = round(19 * HDPIScaleFactor); cGearContourThreshold : LongInt = 179; // if water opacity is higher than this, draw contour for some gears when in water cifRandomize = $00000001; @@ -194,6 +195,7 @@ cBorderWidth = 6; // width of indestructible border // width of 3 allowed hogs to be knocked through with grenade + cCloudOffset = 1184; // Y offset for clouds (cloud height = LAND_HEIGHT-cCloudOffset) cHHRadius = 9; // hedgehog radius cHHStepTicks = 29; @@ -209,6 +211,7 @@ // some gear constants cBarrelHealth = 60; // initial barrel health + cSentryHealth = 60; // initial sentry health cShotgunRadius = 22; // radius of land a shotgun shot destroys cBlowTorchC = 6; // blow torch gear radius component (added to cHHRadius to get the full radius) cakeDmg = 75; // default cake damage @@ -218,18 +221,34 @@ cKbdMaxIndex = 65536;//need more room for the modifier keys // font stuff - cFontBorder = 2 * HDPIScaleFactor; - cFontPadding = 2 * HDPIScaleFactor; + cFontBorder = round(2 * HDPIScaleFactor); + cFontPadding = round(2 * HDPIScaleFactor); cDefaultBuildMaxDist = 256; // default max. building distance with girder/rubber cResurrectorDist = 100; // effect distance of resurrector cSeductionDist = 250; // effect distance of seduction ExtraTime = 30000; // amount of time (ms) given for using Extra Time + MaxMoreWindTime = 5000; // amount of time (ms) for land objects like gfMine to be affected after end of turn // do not change this value cDefaultZoomLevel = 2.0; // 100% zoom + // Maximum camera positions, values are "pixels outside the mainland" + cCamLimitX = 1920; // X (left/right) camera limit, no border. Note: Influences sndFlyAway + // Note: Also make sure it's far enough from airplane spawn + cCamLimitY = 2048; // Top Y camera limit, no border + cCamLimitBorderX = 1920; // X (left/right) camera limit, with border + cCamLimitBorderY = 2048; // Top Y camera limit, with border + + cFontPxToPtRatio = 1.3281472327365; + cBaseChatFontHeight = 12; + cChatScaleRelDelta = 0.1; + cMinChatScaleValue = 0.8; + cMaxChatScaleValue = 4.0; + + cDefaultUIScaleLevel = 1.0; + // game flags gfAny = $FFFFFFFF; // mask for all possible gameflags gfOneClanMode = $00000001; // Game does not end if there's only one clan in play. For missions diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uFloat.pas --- a/hedgewars/uFloat.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uFloat.pas Sun Mar 24 14:33:57 2024 -0400 @@ -118,6 +118,7 @@ _0_0128: hwFloat = (isNegative: false; QWordValue: 54975581); _0_02: hwFloat = (isNegative: false; QWordValue: 85899345); _0_03: hwFloat = (isNegative: false; QWordValue: 128849018); + _0_05: hwFloat = (isNegative: false; QWordValue: 214748365); _0_07: hwFloat = (isNegative: false; QWordValue: 300647710); _0_08: hwFloat = (isNegative: false; QWordValue: 343597383); _0_1: hwFloat = (isNegative: false; QWordValue: 429496730); @@ -297,8 +298,16 @@ operator * (const z1, z2: hwFloat) z : hwFloat; inline; begin z.isNegative:= z1.isNegative xor z2.isNegative; - z.QWordValue:= QWord(z1.Round) * z2.Frac + QWord(z1.Frac) * z2.Round + ((QWord(z1.Frac) * z2.Frac) shr 32); - z.Round:= z.Round + QWord(z1.Round) * z2.Round; + + if (z1.Round = 0) and (z2.Round = 0) then + begin + z.QWordValue:= (QWord(z1.Frac) * z2.Frac) shr 32; + end + else + begin + z.QWordValue:= QWord(z1.Round) * z2.Frac + QWord(z1.Frac) * z2.Round + ((QWord(z1.Frac) * z2.Frac) shr 32); + z.Round:= z.Round + QWord(z1.Round) * z2.Round; + end end; operator * (const z1: hwFloat; const z2: LongInt) z : hwFloat; inline; @@ -370,8 +379,8 @@ function hwSqrt1(const t: hwFloat): hwFloat; const pwr = 8; // even value, feel free to adjust - rThreshold: QWord = 1 shl (pwr + 32); - lThreshold: QWord = 1 shl (pwr div 2 + 32); + rThreshold: QWord = QWord(1) shl (pwr + 32); + lThreshold: QWord = QWord(1) shl (pwr div 2 + 32); var l, r: QWord; c: hwFloat; begin diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGame.pas --- a/hedgewars/uGame.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGame.pas Sun Mar 24 14:33:57 2024 -0400 @@ -81,7 +81,7 @@ begin if Lag > 100 then Lag:= 100 - else if (GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet)) then + else if (GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet)) or fastForward then Lag:= 2500; if (GameType = gmtDemo) then diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGears.pas --- a/hedgewars/uGears.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGears.pas Sun Mar 24 14:33:57 2024 -0400 @@ -58,7 +58,7 @@ uLocale, uAmmos, uStats, uVisualGears, uScript, uVariables, uCommands, uUtils, uTextures, uRenderUtils, uGearsRender, uCaptions, uGearsHedgehog, uGearsUtils, uGearsList, uGearsHandlersRope - , uVisualGearsList, uGearsHandlersMess, uAI; + , uVisualGearsList, uGearsHandlersMess, uAI, SDLh; var skipFlag: boolean; @@ -288,6 +288,7 @@ curHandledGear^.Tex:= RenderStringTex(trmsg[sidUnknownGearValue], $ff808080, fntSmall) else begin + FreeAndNilTexture(curHandledGear^.Tex); // Display mine timer with up to 1 decimal point of precision (rounded down) i:= curHandledGear^.Timer div 1000; j:= (curHandledGear^.Timer mod 1000) div 100; @@ -595,6 +596,11 @@ dec(TurnTimeLeft) end; +if (TurnTimeLeft = 0) and (ReadyTimeLeft = 0) then + inc(TimeNotInTurn) +else + TimeNotInTurn:= 0; + if skipFlag then begin if TagTurnTimeLeft = 0 then @@ -623,6 +629,8 @@ inc(GameTicks); if (OuchTauntTimer > 0) then dec(OuchTauntTimer); +if fastForward and (GameTicks = FFGameTick) then + ParseCommand('sff', true); end; //Purpose, to reset all transient attributes toggled by a utility and clean up various gears and effects at end of turn @@ -756,6 +764,92 @@ end; end; +procedure AddLandSentries(count: LongWord); +var i, x, y, swapIndex: LongInt; + positions: array[0..1023] of TPoint; + positionsCount, tries: LongInt; +begin + positionsCount := 0; + tries := 2048; + while (positionsCount < 1024) and (tries > 0) do + begin + x := leftX + cHHRadius + GetRandom(rightX - leftX - 2 * cHHRadius); + y := cHHRadius; + + while y < cWaterLine do + begin + repeat + inc(y, cHHRadius) + until (y >= cWaterLine) or (CountLand(x, y, cHHRadius - 1, 1, lfAll, 0) = 0); + + if y < cWaterLine then + begin + repeat + inc(y) + until (y >= cWaterLine) or (CountLand(x, y, cHHRadius - 1, 1, lfAll, 0) <> 0); + + if y < cWaterLine then + begin + swapIndex := GetRandom(positionsCount + 1); + if swapIndex = positionsCount then + begin + positions[positionsCount].X := x; + positions[positionsCount].Y := y; + end + else + begin + positions[positionsCount].X := positions[swapIndex].X; + positions[positionsCount].Y := positions[swapIndex].Y; + positions[swapIndex].X := x; + positions[swapIndex].Y := y; + end; + inc(positionsCount); + if positionsCount >= 1024 then + break; + inc(y, cHHRadius * 2) + end + else + dec(tries) + end + else + dec(tries) + end; + end; + + for i := 0 to min(count, positionsCount) - 1 do + AddGear(positions[i].X, positions[i].Y - cHHRadius, gtSentry, 0, _0, _0, 0)^.Hedgehog := nil; +end; + +function AddWaterSentries(count: Longword): Longword; +var i, x, y: LongInt; + positions: array[0..255] of TPoint; + positionsCount, tries: LongInt; +begin + AddWaterSentries := 0; + positionsCount := 0; + tries := 512; + + while (positionsCount < 256) and (tries > 0) do + begin + x := leftX + cHHRadius + GetRandom(rightX - leftX - 2 * cHHRadius); + y := cWaterLine - 3 * cHHRadius; + if (CountLand(x, y, cHHRadius - 1, 1, lfAll, 0) = 0) then + begin + positions[positionsCount].X := x; + positions[positionsCount].Y := y; + inc(positionsCount); + if positionsCount >= 256 then + break; + end; + end; + + for i := 0 to min(count, positionsCount) - 1 do + begin + AddGear(positions[i].X, positions[i].Y - cHHRadius, gtSentry, 0, _0, _0, 0)^.Hedgehog := nil; + inc(AddWaterSentries); + end; +end; + procedure AddMiscGears; var p,i,j,t,h,unplaced: Longword; rx, ry: LongInt; @@ -769,6 +863,7 @@ while (i < cLandMines) and (unplaced < 4) do begin Gear:= AddGear(0, 0, gtMine, 0, _0, _0, 0); + Gear^.Hedgehog := nil; FindPlace(Gear, false, 0, LAND_WIDTH); if Gear = nil then @@ -784,6 +879,7 @@ while (i < cExplosives) and (unplaced < 4) do begin Gear:= AddGear(0, 0, gtExplosives, 0, _0, _0, 0); + Gear^.Hedgehog := nil; FindPlace(Gear, false, 0, LAND_WIDTH); if Gear = nil then @@ -849,7 +945,8 @@ inc(i); AddFileLog('Placed Air Mine @ (' + inttostr(rx) + ',' + inttostr(ry) + ')'); if i < cAirMines then - Gear:= AddGear(0, 0, gtAirMine, 0, _0, _0, 0) + Gear:= AddGear(0, 0, gtAirMine, 0, _0, _0, 0); + Gear^.Hedgehog := nil end end else @@ -858,6 +955,11 @@ end; if p <> 0 then DeleteGear(Gear); +if cSentries > 10 then + AddLandSentries(cSentries - AddWaterSentries(cSentries div 10)) +else + AddLandSentries(cSentries); + if (GameFlags and gfLowGravity) <> 0 then begin cGravity:= cMaxWindSpeed; @@ -1456,7 +1558,8 @@ @doStepKnife, @doStepCreeper, @doStepMinigun, - @doStepMinigunBullet); + @doStepMinigunBullet, + @doStepSentryDeploy); begin doStepHandlers:= handlers; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGearsHandlersMess.pas --- a/hedgewars/uGearsHandlersMess.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGearsHandlersMess.pas Sun Mar 24 14:33:57 2024 -0400 @@ -141,6 +141,8 @@ procedure doStepMinigunWork(Gear: PGear); procedure doStepMinigun(Gear: PGear); procedure doStepMinigunBullet(Gear: PGear); +procedure ResetSentryState(Gear: PGear; state, timer: LongInt); +procedure doStepSentryDeploy(Gear: PGear); var upd: Longword; @@ -396,10 +398,10 @@ end; // clip velocity at 2 - over 1 per pixel, but really shouldn't cause many actual problems. - if Gear^.dX.Round > 1 then - Gear^.dX.QWordValue:= 8589934592; - if Gear^.dY.Round > 1 then - Gear^.dY.QWordValue:= 8589934592; + if Gear^.dX.QWordValue > 8160437862 then + Gear^.dX.QWordValue:= 8160437862; + if Gear^.dY.QWordValue > 8160437862 then + Gear^.dY.QWordValue:= 8160437862; if (Gear^.State and gstSubmersible <> 0) and CheckCoordInWater(gX, gY) then begin @@ -536,10 +538,15 @@ if isFalling and (Gear^.State and gstNoGravity = 0) then begin + // Apply gravity and wind Gear^.dY := Gear^.dY + cGravity; - if (GameFlags and gfMoreWind <> 0) and (TurnTimeLeft > 0) and - ((xland or land) = 0) and - ((Gear^.dX.QWordValue + Gear^.dY.QWordValue) > _0_02.QWordValue) then + if ((GameFlags and gfMoreWind) <> 0) and + // Disable gfMoreWind for land objects on turn end to prevent bouncing them forever + // This solution is rather ugly, in that it will occassionally suddenly wind physics + // while a gear is moving, this can be rather confusing. + // TODO: Find a way to make gfMoreWind-affected land objects settle more reliably + // and quickler without touching wind itselvs + ((not (Gear^.Kind in [gtMine, gtAirMine, gtSMine, gtKnife, gtExplosives, gtSentry])) or (TimeNotInTurn < MaxMoreWindTime)) then Gear^.dX := Gear^.dX + cWindSpeed / Gear^.Density end; @@ -565,7 +572,7 @@ (((Gear^.Radius < 3) and (Gear^.dY < -_0_1)) or ((Gear^.Radius >= 3) and ((Gear^.dX.QWordValue > _0_1.QWordValue) or (Gear^.dY.QWordValue > _0_1.QWordValue)))) then - PlaySound(TSound(ord(Gear^.ImpactSound) + LongInt(GetRandom(Gear^.nImpactSounds))), true); + PlaySound(TSound(ord(Gear^.ImpactSound) + LongInt(GetRandom(Gear^.nImpactSounds))), Gear^.Kind <> gtDynamite); end; //////////////////////////////////////////////////////////////////////////////// @@ -828,13 +835,14 @@ s: PSDL_Surface; p: PLongwordArray; lf: LongWord; + oldY: hwFloat; begin -inc(Gear^.Pos); gun:= (Gear^.State and gstTmpFlag) <> 0; move:= false; draw:= false; if gun then begin + inc(Gear^.Pos); Gear^.State:= Gear^.State and (not gstInvisible); doStepFallingGear(Gear); CheckCollision(Gear); @@ -851,12 +859,13 @@ end end else if GameTicks and $7 = 0 then - begin with Gear^ do begin + inc(Pos, 8); State:= State and (not gstInvisible); + oldY:= Y; X:= X + cWindSpeed * 3200 + dX; - Y:= Y + dY + cGravity * vobFallSpeed * 8; // using same value as flakes to try and get similar results + Y:= Y + dY + cGravity * (vobFallSpeed * 8); // using same value as flakes to try and get similar results xx:= hwRound(X); yy:= hwRound(Y); if vobVelocity <> 0 then @@ -872,7 +881,7 @@ move:= true else if (xx > snowRight) or (xx < snowLeft) then move:=true - else if (cGravity < _0) and (yy < LAND_HEIGHT-1200) then + else if (cGravity.isNegative) and (yy < LAND_HEIGHT-1200) then move:=true // Solid pixel encountered else if ((yy and LAND_HEIGHT_MASK) = 0) and ((xx and LAND_WIDTH_MASK) = 0) and (Land[yy, xx] <> 0) then @@ -885,33 +894,40 @@ X:= X - cWindSpeed * 1600 - dX; end // If there's room below, on the sides, fill the gaps - else if (((yy-1) and LAND_HEIGHT_MASK) = 0) and (((xx-(1*hwSign(cWindSpeed))) and LAND_WIDTH_MASK) = 0) and (Land[yy-1, (xx-(1*hwSign(cWindSpeed)))] = 0) then - begin - X:= X - _0_8 * hwSign(cWindSpeed); - Y:= Y - dY - cGravity * vobFallSpeed * 8; - end - else if (((yy-1) and LAND_HEIGHT_MASK) = 0) and (((xx-(2*hwSign(cWindSpeed))) and LAND_WIDTH_MASK) = 0) and (Land[yy-1, (xx-(2*hwSign(cWindSpeed)))] = 0) then - begin - X:= X - _0_8 * 2 * hwSign(cWindSpeed); - Y:= Y - dY - cGravity * vobFallSpeed * 8; - end - else if (((yy-1) and LAND_HEIGHT_MASK) = 0) and (((xx+(1*hwSign(cWindSpeed))) and LAND_WIDTH_MASK) = 0) and (Land[yy-1, (xx+(1*hwSign(cWindSpeed)))] = 0) then - begin - X:= X + _0_8 * hwSign(cWindSpeed); - Y:= Y - dY - cGravity * vobFallSpeed * 8; - end - else if (((yy-1) and LAND_HEIGHT_MASK) = 0) and (((xx+(2*hwSign(cWindSpeed))) and LAND_WIDTH_MASK) = 0) and (Land[yy-1, (xx+(2*hwSign(cWindSpeed)))] = 0) then - begin - X:= X + _0_8 * 2 * hwSign(cWindSpeed); - Y:= Y - dY - cGravity * vobFallSpeed * 8; - end + else if (((yy-1) and LAND_HEIGHT_MASK) = 0) then + begin + if (((xx - 1) and LAND_WIDTH_MASK) = 0) and (Land[yy - 1, (xx - 1)] = 0) then + begin + X:= X - _0_8; + Y:= oldY; + end + else if (((xx - 2) and LAND_WIDTH_MASK) = 0) and (Land[yy - 1, (xx - 2)] = 0) then + begin + X:= X - _1_6; + Y:= oldY; + end + else if (((xx + 1) and LAND_WIDTH_MASK) = 0) and (Land[yy - 1, (xx + 1)] = 0) then + begin + X:= X + _0_8; + Y:= oldY; + end + else if (((xx + 2) and LAND_WIDTH_MASK) = 0) and (Land[yy - 1, (xx + 2)] = 0) then + begin + X:= X + _1_6; + Y:= oldY; + end else + if ((((yy+1) and LAND_HEIGHT_MASK) = 0) and ((Land[yy + 1, xx] and $FF) <> 0)) then + move:=true + else + draw:= true + end // if there's an hog/object below do nothing else if ((((yy+1) and LAND_HEIGHT_MASK) = 0) and ((Land[yy+1, xx] and $FF) <> 0)) then move:=true else draw:= true end - end - end; + end; + if draw then with Gear^ do begin @@ -920,7 +936,6 @@ if (Pos > 20) and ((CurAmmoGear = nil) or (CurAmmoGear^.Kind <> gtRope)) then begin -////////////////////////////////// TODO - ASK UNC0RR FOR A GOOD HOME FOR THIS //////////////////////////////////// if not gun then begin dec(yy,3); @@ -963,9 +978,6 @@ p:= PLongWordArray(@(p^[s^.pitch shr 2])) end; - // Why is this here. For one thing, there's no test on +1 being safe. - //Land[py, px+1]:= lfBasic; - if allpx then UpdateLandTexture(xx, Pred(s^.h), yy, Pred(s^.w), true) else @@ -977,7 +989,6 @@ min(LAND_HEIGHT - yy, Pred(s^.h)), false // could this be true without unnecessarily creating blanks? ); end; -////////////////////////////////// TODO - ASK UNC0RR FOR A GOOD HOME FOR THIS //////////////////////////////////// end end; @@ -990,7 +1001,7 @@ end; Gear^.Pos:= 0; Gear^.X:= int2hwFloat(LongInt(GetRandom(snowRight - snowLeft)) + snowLeft); - if (cGravity < _0) and (yy < LAND_HEIGHT-1200) then + if (cGravity.isNegative) and (yy < LAND_HEIGHT-1200) then Gear^.Y:= int2hwFloat(LAND_HEIGHT - 50 - LongInt(GetRandom(50))) else Gear^.Y:= int2hwFloat(LAND_HEIGHT + LongInt(GetRandom(50)) - 1250); Gear^.State:= Gear^.State or gstInvisible; @@ -1212,7 +1223,7 @@ begin dec(i); if Collisions^.ar[i]^.Kind in - [gtMine, gtSMine, gtAirMine, gtKnife, gtCase, gtTarget, gtExplosives] then + [gtMine, gtSMine, gtAirMine, gtKnife, gtCase, gtTarget, gtExplosives, gtSentry] then begin Gear^.X := Collisions^.ar[i]^.X; Gear^.Y := Collisions^.ar[i]^.Y; @@ -1232,22 +1243,22 @@ AllInactive := false; if ((Gear^.State and gstAnimation) = 0) then - begin + begin dec(Gear^.Timer); if Gear^.Timer = 0 then - begin + begin PlaySound(sndShotgunFire); CreateShellForGear(Gear, 0); Gear^.State := Gear^.State or gstAnimation - end; - exit - end else - if(Gear^.Hedgehog^.Gear = nil) or ((Gear^.Hedgehog^.Gear^.State and gstMoving) <> 0) then - begin + end + else if (Gear^.Hedgehog^.Gear = nil) + or ((Gear^.Hedgehog^.Gear^.State and (gstMoving or gstHHDriven)) = gstMoving) then + begin DeleteGear(Gear); AfterAttack; - exit - end + end; + exit + end else inc(Gear^.Timer); @@ -1316,6 +1327,11 @@ VGear: PVisualGear; i, steps: LongWord; begin + if CurrentHedgehog^.Gear = nil then + begin + DeleteGear(Bullet); + exit + end; if Bullet^.PortalCounter = 0 then begin ox:= CurrentHedgehog^.Gear^.X + Int2hwFloat(GetLaunchX(CurrentHedgehog^.CurAmmoType, hwSign(CurrentHedgehog^.Gear^.dX), CurrentHedgehog^.Gear^.Angle)); @@ -1559,7 +1575,7 @@ begin if Gear^.Kind = gtMinigunBullet then begin - doMakeExplosion(hwRound(Gear^.X), hwRound(Gear^.Y), 5, + doMakeExplosion(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Karma, Gear^.Hedgehog, (EXPLNoDamage or EXPLDoNotTouchHH){ or EXPLDontDraw or EXPLNoGfx}); VGear := AddVisualGear(hwRound(Gear^.X + Gear^.dX * 5), hwRound(Gear^.Y + Gear^.dY * 5), vgtBulletHit); end @@ -1602,10 +1618,10 @@ procedure doStepDEagleShot(Gear: PGear); begin - Gear^.Data:= nil; - // remember who fired this - if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) then - Gear^.Data:= Pointer(Gear^.Hedgehog^.Gear); + if Gear^.Data = nil then + // remember who fired this + if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) then + Gear^.Data:= Pointer(Gear^.Hedgehog^.Gear); PlaySound(sndGun); ClearHitOrder(); @@ -1843,8 +1859,8 @@ procedure doStepBlowTorchWork(Gear: PGear); var HHGear: PGear; - b: boolean; - prevX: LongInt; + dig, hit: boolean; + newX, newY: hwFloat; begin AllInactive := false; WorldWrap(Gear); @@ -1852,6 +1868,7 @@ if Gear^.Hedgehog^.Gear = nil then begin + ClearProximityCache(); StopSoundChan(Gear^.SoundChannel); DeleteGear(Gear); AfterAttack; @@ -1862,74 +1879,82 @@ HedgehogChAngle(HHGear); - b := false; + dig := false; + hit := false; if abs(LongInt(HHGear^.Angle) - BTPrevAngle) > 7 then begin Gear^.dX := SignAs(AngleSin(HHGear^.Angle) * _0_5, Gear^.dX); Gear^.dY := AngleCos(HHGear^.Angle) * ( - _0_5); + BTPrevAngle := HHGear^.Angle; - b := true - end; - - if ((HHGear^.State and gstMoving) <> 0) then + dig := true + end; + + if (HHGear^.State and gstMoving) <> 0 then begin doStepHedgehogMoving(HHGear); if (HHGear^.State and gstHHDriven) = 0 then Gear^.Timer := 0 end; + if Gear^.Timer mod 1500 = 0 then + RefillProximityCache(Gear, 200); + if Gear^.Timer mod cHHStepTicks = 0 then begin - b := true; + dig := true; if Gear^.dX.isNegative then HHGear^.Message := (HHGear^.Message and (gmAttack or gmUp or gmDown)) or gmLeft else HHGear^.Message := (HHGear^.Message and (gmAttack or gmUp or gmDown)) or gmRight; - if ((HHGear^.State and gstMoving) = 0) then + if (HHGear^.State and gstMoving) = 0 then begin HHGear^.State := HHGear^.State and (not gstAttacking); - prevX := hwRound(HHGear^.X); - - // why the call to HedgehogStep then a further increment of X? - if (prevX = hwRound(HHGear^.X)) and - CheckLandValue(hwRound(HHGear^.X + SignAs(_6, HHGear^.dX)), hwRound(HHGear^.Y), - lfIndestructible) then HedgehogStep(HHGear); - - if (prevX = hwRound(HHGear^.X)) and - CheckLandValue(hwRound(HHGear^.X + SignAs(_6, HHGear^.dX)), hwRound(HHGear^.Y), - lfIndestructible) then HHGear^.X := HHGear^.X + SignAs(_1, HHGear^.dX); + + if CheckLandValue(hwRound(HHGear^.X + SignAs(_6, HHGear^.dX)), hwRound(HHGear^.Y), lfIndestructible) then + HedgehogStep(HHGear); + HHGear^.State := HHGear^.State or gstAttacking end; + newX := HHGear^.X + Gear^.dX * (cHHRadius + cBlowTorchC); + newY := HHGear^.Y + Gear^.dY * (cHHRadius + cBlowTorchC); + if CheckLandValue(hwRound(newX + SignAs(_6, Gear^.dX)), hwRound(newY), lfIndestructible) then + begin + Gear^.X := newX; + Gear^.Y := newY; + end; + inc(BTSteps); - if BTSteps = 7 then + if BTSteps = 11 then begin BTSteps := 0; - if CheckLandValue(hwRound(HHGear^.X + Gear^.dX * (cHHRadius + cBlowTorchC) + SignAs(_6,Gear^.dX)), hwRound(HHGear^.Y + Gear^.dY * (cHHRadius + cBlowTorchC)),lfIndestructible) then - begin - Gear^.X := HHGear^.X + Gear^.dX * (cHHRadius + cBlowTorchC); - Gear^.Y := HHGear^.Y + Gear^.dY * (cHHRadius + cBlowTorchC); - end; - HHGear^.State := HHGear^.State or gstNoDamage; - AmmoShove(Gear, Gear^.Boom, 15); - HHGear^.State := HHGear^.State and (not gstNoDamage) + hit := true end; end; - if b then + if dig then begin DrawTunnel(HHGear^.X + Gear^.dX * cHHRadius, - HHGear^.Y + Gear^.dY * cHHRadius - _1 - - ((hwAbs(Gear^.dX) / (hwAbs(Gear^.dX) + hwAbs(Gear^.dY))) * _0_5 * 7), - Gear^.dX, Gear^.dY, - cHHStepTicks, cHHRadius * 2 + 7); + HHGear^.Y + Gear^.dY * cHHRadius - _1 - + ((hwAbs(Gear^.dX) / (hwAbs(Gear^.dX) + hwAbs(Gear^.dY))) * _0_5 * 7), + Gear^.dX, Gear^.dY, + cHHStepTicks, cHHRadius * 2 + 7); + + HHGear^.State := HHGear^.State or gstNoDamage; + if hit then + AmmoShoveCache(Gear, Gear^.Boom, 15) + else + AmmoShoveCache(Gear, 0, 15); + HHGear^.State := HHGear^.State and (not gstNoDamage); end; if (TurnTimeLeft = 0) or (Gear^.Timer = 0) or ((HHGear^.Message and gmAttack) <> 0) then begin + ClearProximityCache(); StopSoundChan(Gear^.SoundChannel); HHGear^.Message := 0; HHGear^.State := HHGear^.State and (not gstNotKickable); @@ -1955,6 +1980,8 @@ cHHStepTicks, cHHRadius * 2 + 7); HHGear^.Message := 0; HHGear^.State := HHGear^.State or gstNotKickable; + RefillProximityCache(Gear, 200); + Gear^.SoundChannel := LoopSound(sndBlowTorch); Gear^.doStep := @doStepBlowTorchWork end; @@ -2184,7 +2211,7 @@ for t:= 0 to Pred(TeamsCount) do with TeamsArray[t]^ do for i:= 0 to cMaxHHIndex do - if (Hedgehogs[i].Gear <> nil) and (Hedgehogs[i].Effects[heFrozen] = 0) then + if (not Hedgehogs[i].Unplaced) and (Hedgehogs[i].Gear <> nil) and (Hedgehogs[i].Effects[heFrozen] = 0) and ((Hedgehogs[i].Gear^.State and gstInvisible) = 0) then begin tmpG:= Hedgehogs[i].Gear; tX:=Gear^.X-tmpG^.X; @@ -2349,12 +2376,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 @@ -3082,6 +3119,16 @@ AfterAttack; + // Delete parachute early if hog already collides + if (TestCollisionXwithGear(HHGear, -1) <> 0) and (TestCollisionXwithGear(HHGear, 1) <> 0) then + begin + HHGear^.dY := cGravity * 100; + isCursorVisible:= false; + ApplyAmmoChanges(HHGear^.Hedgehog^); + DeleteGear(Gear); + exit; + end; + // make sure hog doesn't end up facing in wrong direction due to high jump if (HHGear^.State and gstHHHJump) <> 0 then HHGear^.dX.isNegative := (not HHGear^.dX.isNegative); @@ -3155,7 +3202,7 @@ // Get rid of gear and cleanup if ((WorldEdge = weWrap) and (Gear^.FlightTime >= 4000)) or - ((WorldEdge <> weWrap) and (((hwRound(Gear^.X) - Gear^.Radius > (max(LAND_WIDTH,4096)+2048)) or (hwRound(Gear^.X) + Gear^.Radius < -2048) or ((Gear^.Message and gmDestroy) > 0)))) then + ((WorldEdge <> weWrap) and (((hwRound(Gear^.X) - Gear^.Radius > (LAND_WIDTH+2048)) or (hwRound(Gear^.X) + Gear^.Radius < -2048) or ((Gear^.Message and gmDestroy) > 0)))) then begin // fail-safe: instanly stop sound if it wasn't disabled before if (Gear^.SoundChannel <> -1) then @@ -3201,6 +3248,8 @@ begin AllInactive := false; + HHGear:= nil; + if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) then HHGear:= Gear^.Hedgehog^.Gear; @@ -3223,7 +3272,7 @@ if (WorldEdge = weWrap) then Gear^.X := int2hwFloat(CalcWorldWrap(Gear^.Target.X - max(384, LAND_WIDTH shr 2), 0)) else - Gear^.X := int2hwFloat(max(LAND_WIDTH,4096) + 2048); + Gear^.X := int2hwFloat(LAND_WIDTH + 2048); end; Gear^.Y := int2hwFloat(topY - 300); @@ -3267,6 +3316,9 @@ Gear^.Karma := 0; end; + if (GameFlags and gfInfAttack) = 0 then + FollowGear:= Gear; + end; //////////////////////////////////////////////////////////////////////////////// @@ -3471,6 +3523,11 @@ switchDir: Longword; oldUid: Longword; begin + if CurrentHedgehog^.Gear = nil then + begin + DeleteGear(Gear); + exit + end; AllInactive := false; if ((Gear^.Message and (not (gmSwitch or gmPrecise))) <> 0) or (TurnTimeLeft = 0) then @@ -4038,7 +4095,7 @@ //////////////////////////////////////////////////////////////////////////////// procedure doStepSeductionWork(Gear: PGear); var i: LongInt; - hogs: PGearArrayS; + hits: PGearArrayS; HHGear: PGear; begin AllInactive := false; @@ -4052,12 +4109,12 @@ exit; end; - hogs := GearsNear(Gear^.X, Gear^.Y, gtHedgehog, Gear^.Radius); - if hogs.size > 0 then - begin - for i:= 0 to hogs.size - 1 do - with hogs.ar^[i]^ do - if (hogs.ar^[i] <> CurrentHedgehog^.Gear) and (Hedgehog^.Effects[heFrozen] = 0) then + hits := GearsNear(Gear^.X, Gear^.Y, gtHedgehog, Gear^.Radius); + if hits.size > 0 then + begin + for i:= 0 to hits.size - 1 do + with hits.ar^[i]^ do + if (hits.ar^[i] <> CurrentHedgehog^.Gear) and (Hedgehog^.Effects[heFrozen] = 0) then begin if (WorldEdge <> weWrap) or (not (hwAbs(Gear^.X - X) > int2hwFloat(Gear^.Radius))) then dX:= _50 * cGravity * (Gear^.X - X) / _25 @@ -4071,6 +4128,19 @@ else if Hedgehog^.Effects[heFrozen] > 255 then Hedgehog^.Effects[heFrozen]:= 255 end ; + + hits := GearsNear(Gear^.X, Gear^.Y, gtSentry, Gear^.Radius); + if hits.size > 0 then + for i:= 0 to hits.size - 1 do + with hits.ar^[i]^ do + if (Gear^.Hedgehog <> nil) and (Hedgehog <> Gear^.Hedgehog) then + begin + dX := SignAs(_0, dX); + dY := -_0_15; + Hedgehog := Gear^.Hedgehog; + ResetSentryState(hits.ar^[i], 0, 10000); + end; + AfterAttack; DeleteGear(Gear); end; @@ -4883,7 +4953,7 @@ gear^.State := gear^.State or gstAnimation and (not gstTmpFlag); Gear^.doStep := @doStepBirdyAppear; - if CurrentHedgehog = nil then + if CurrentHedgehog^.Gear = nil then begin DeleteGear(Gear); exit @@ -4942,7 +5012,7 @@ procedure doPortalColorSwitch(); var CurWeapon: PAmmo; begin - if (CurrentHedgehog <> nil) and (CurrentHedgehog^.Gear <> nil) and ((CurrentHedgehog^.Gear^.State and gstHHDriven) <> 0) and ((CurrentHedgehog^.Gear^.Message and gmSwitch) <> 0) then + if (CurrentHedgehog^.Gear <> nil) and ((CurrentHedgehog^.Gear^.State and gstHHDriven) <> 0) and ((CurrentHedgehog^.Gear^.Message and gmSwitch) <> 0) then with CurrentHedgehog^ do if (CurAmmoType = amPortalGun) then begin @@ -5273,7 +5343,7 @@ if iterator^.dX.isNegative then iterator^.Angle:= 4096-iterator^.Angle; end; - if (CurrentHedgehog <> nil) and (CurrentHedgehog^.Gear <> nil) + if (CurrentHedgehog^.Gear <> nil) and (iterator = CurrentHedgehog^.Gear) and (CurAmmoGear <> nil) and (CurAmmoGear^.Kind = gtRope) @@ -5393,6 +5463,11 @@ s: hwFloat; CurWeapon: PAmmo; begin + if CurrentHedgehog^.Gear = nil then + begin + DeleteGear(newPortal); + exit + end; s:= Distance (newPortal^.dX, newPortal^.dY); // Adds the hog speed (only that part in/directly against shot direction) @@ -5407,62 +5482,61 @@ PlaySound(sndPortalShot); - if CurrentHedgehog <> nil then - with CurrentHedgehog^ do - begin - CurWeapon:= GetCurAmmoEntry(CurrentHedgehog^); - // let's save the HH's dX's direction so we can decide where the "top" of the portal hole - newPortal^.Elasticity.isNegative := CurrentHedgehog^.Gear^.dX.isNegative; - // when doing a backjump the dx is the opposite of the facing direction - if ((Gear^.State and gstHHHJump) <> 0) and (Effects[heArtillery] = 0) then - newPortal^.Elasticity.isNegative := not newPortal^.Elasticity.isNegative; - - // make portal gun look unloaded - if (CurWeapon <> nil) and (CurAmmoType = amPortalGun) then - CurWeapon^.Timer := CurWeapon^.Timer or 2; - - iterator := GearsList; - while iterator <> nil do - begin - if (iterator^.Kind = gtPortal) then - if (iterator <> newPortal) and (iterator^.Timer > 0) and (iterator^.Hedgehog = CurrentHedgehog) then - begin - if ((iterator^.Tag and 2) = (newPortal^.Tag and 2)) then - begin - iterator^.Timer:= 0; - end - else - begin - // link portals with each other - newPortal^.LinkedGear := iterator; - iterator^.LinkedGear := newPortal; - iterator^.Health := newPortal^.Health; - end; - end; - iterator^.PortalCounter:= 0; - iterator := iterator^.NextGear - end; - - if newPortal^.LinkedGear <> nil then - begin - // This jiggles gears, to ensure a portal connection just placed under a gear takes effect. - iterator:= GearsList; - while iterator <> nil do - begin - if not (iterator^.Kind in [gtPortal, gtAirAttack, gtKnife, gtSMine]) and ((iterator^.Hedgehog <> CurrentHedgehog) - or ((iterator^.Message and gmAllStoppable) = 0)) then - begin - iterator^.Active:= true; - if iterator^.dY.QWordValue = 0 then - iterator^.dY.isNegative:= false; - iterator^.State:= iterator^.State or gstMoving; - DeleteCI(iterator); - //inc(iterator^.dY.QWordValue,10); - end; - iterator:= iterator^.NextGear - end - end - end; + with CurrentHedgehog^ do + begin + CurWeapon:= GetCurAmmoEntry(CurrentHedgehog^); + // let's save the HH's dX's direction so we can decide where the "top" of the portal hole + newPortal^.Elasticity.isNegative := CurrentHedgehog^.Gear^.dX.isNegative; + // when doing a backjump the dx is the opposite of the facing direction + if ((Gear^.State and gstHHHJump) <> 0) and (Effects[heArtillery] = 0) then + newPortal^.Elasticity.isNegative := not newPortal^.Elasticity.isNegative; + + // make portal gun look unloaded + if (CurWeapon <> nil) and (CurAmmoType = amPortalGun) then + CurWeapon^.Timer := CurWeapon^.Timer or 2; + + iterator := GearsList; + while iterator <> nil do + begin + if (iterator^.Kind = gtPortal) then + if (iterator <> newPortal) and (iterator^.Timer > 0) and (iterator^.Hedgehog = CurrentHedgehog) then + begin + if ((iterator^.Tag and 2) = (newPortal^.Tag and 2)) then + begin + iterator^.Timer:= 0; + end + else + begin + // link portals with each other + newPortal^.LinkedGear := iterator; + iterator^.LinkedGear := newPortal; + iterator^.Health := newPortal^.Health; + end; + end; + iterator^.PortalCounter:= 0; + iterator := iterator^.NextGear + end; + + if newPortal^.LinkedGear <> nil then + begin + // This jiggles gears, to ensure a portal connection just placed under a gear takes effect. + iterator:= GearsList; + while iterator <> nil do + begin + if not (iterator^.Kind in [gtPortal, gtAirAttack, gtKnife, gtSMine]) and ((iterator^.Hedgehog <> CurrentHedgehog) + or ((iterator^.Message and gmAllStoppable) = 0)) then + begin + iterator^.Active:= true; + if iterator^.dY.QWordValue = 0 then + iterator^.dY.isNegative:= false; + iterator^.State:= iterator^.State or gstMoving; + DeleteCI(iterator); + //inc(iterator^.dY.QWordValue,10); + end; + iterator:= iterator^.NextGear + end + end + end; newPortal^.State := newPortal^.State and (not gstCollision); newPortal^.State := newPortal^.State or gstMoving; newPortal^.doStep := @doStepMovingPortal; @@ -5534,7 +5608,8 @@ 7: PlaySound(sndPiano7, false, false, true); 8: PlaySound(sndPiano8, false, false, true); end; - AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtNote); + if CurrentHedgehog^.Gear^.MsgParam <= 8 then + AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtNote); CurrentHedgehog^.Gear^.MsgParam := 0; CurrentHedgehog^.Gear^.Message := CurrentHedgehog^.Gear^.Message and (not gmSlot); end; @@ -6051,22 +6126,20 @@ if (tmp^.Kind = gtHedgehog) or (tmp^.Kind = gtMine) or (tmp^.Kind = gtExplosives) then begin dmg:= 0; - if (tmp^.Kind <> gtHedgehog) or (tmp^.Hedgehog^.Effects[heInvulnerable] = 0) then + if (tmp^.Kind <> gtHedgehog) or + ((tmp^.Hedgehog^.Effects[heInvulnerable] = 0) and ((tmp^.State and gstHHDeath) = 0)) then begin // base damage on remaining health dmg:= (tmp^.Health - tmp^.Damage); + // always rounding down + dmg:= dmg div Gear^.Boom; + if dmg > 0 then - begin - // always rounding down - dmg:= dmg div Gear^.Boom; - - if dmg > 0 then - ApplyDamage(tmp, CurrentHedgehog, dmg, dsHammer); - end; - tmp^.dY:= _0_03 * Gear^.Boom + ApplyDamage(tmp, CurrentHedgehog, dmg, dsHammer); + tmp^.dY:= _0_03 * Gear^.Boom end; - if (tmp^.Kind <> gtHedgehog) or (dmg > 0) or (tmp^.Health > tmp^.Damage) then + if ((tmp^.Kind = gtHedgehog) and ((tmp^.State and gstHHDeath) = 0)) or (tmp^.Health > tmp^.Damage) then begin tmp2:= AddGear(hwRound(tmp^.X), hwRound(tmp^.Y), gtHammerHit, 0, _0, _0, 0); tmp2^.LinkedGear:= tmp; @@ -6235,7 +6308,7 @@ LoadHedgehogHat(resgear^.Hedgehog^, 'Reserved/Zombie'); end; - hh^.Gear^.dY := _0; + hh^.Gear^.dY := -cLittle; hh^.Gear^.dX := _0; doStepHedgehogMoving(hh^.Gear); StopSoundChan(Gear^.SoundChannel); @@ -6337,6 +6410,7 @@ procedure doStepTardisWarp(Gear: PGear); var HH: PHedgehog; i,j,cnt: LongWord; + restoreBottomY: LongInt; s: ansistring; begin HH:= Gear^.Hedgehog; @@ -6433,8 +6507,37 @@ inc(cnt); if (cnt = 0) or SuddenDeathDmg or (Gear^.Timer = 0) then begin + // Place tardis if HH^.GearHidden <> nil then - FindPlace(HH^.GearHidden, false, 0, LAND_WIDTH,true); + begin + restoreBottomY:= cWaterLine; + // Place tardis at a random safe position + FindPlace(HH^.GearHidden, false, 0, LAND_WIDTH, restoreBottomY, true, false); + + // If in Sudden Death, rise the minimum possible spawn position to make + // it less likely for the hog to drown before its turn + if SuddenDeathActive and (cWaterRise > 0) then + begin + // Enough space to survive the water rise of 1 round. + // Also limit the highest spawn height to topY plus a small buffer zone + restoreBottomY:= max(topY + cHHRadius * 5, cWaterLine - cWaterRise * (TeamsCount + 1)); + // If gear is below the safe spawn height, place it again, + // but this time with the height limit in place + if (HH^.GearHidden <> nil) and (hwRound(HH^.GearHidden^.Y) > restoreBottomY) then + // Due to the reduced Y range, this one might fail for very aggressive SD water rise + begin + FindPlace(HH^.GearHidden, false, 0, LAND_WIDTH, restoreBottomY, true, false); + end; + // Still unsafe? Relax the height limit to a third of the map height above cWaterLine + if (HH^.GearHidden <> nil) and (hwRound(HH^.GearHidden^.Y) > restoreBottomY) then + begin + restoreBottomY:= cWaterLine - ((cWaterLine - topY) div 3); + // Even this might fail, but it's much less likely. If it fails, we still have the + // position of the first FindPlace as a fallback. + FindPlace(HH^.GearHidden, false, 0, LAND_WIDTH, restoreBottomY, true, false); + end; + end; + end; if HH^.GearHidden <> nil then begin @@ -7128,10 +7231,10 @@ procedure doStepMinigunBullet(Gear: PGear); begin - Gear^.Data:= nil; - // remember who fired this - if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) then - Gear^.Data:= Pointer(Gear^.Hedgehog^.Gear); + if Gear^.Data = nil then + // remember who fired this + if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) then + Gear^.Data:= Pointer(Gear^.Hedgehog^.Gear); Gear^.X := Gear^.X + Gear^.dX * 2; Gear^.Y := Gear^.Y + Gear^.dY * 2; @@ -7139,4 +7242,481 @@ Gear^.doStep := @doStepBulletWork end; +//////////////////////////////////////////////////////////////////////////////// + +function MakeSentryStep(Sentry: PGear; maxYStep: LongInt; TestOnly: Boolean): Boolean; +var x, y, offset, direction: LongInt; +begin + MakeSentryStep := false; + x := hwRound(Sentry^.X); + y := hwRound(Sentry^.Y); + direction := hwSign(Sentry^.dX); + + for offset := -maxYStep - 1 to maxYStep + 1 do + begin + if TestCollisionYImpl(x + direction, y + offset, Sentry^.Radius, 1, Sentry^.CollisionMask) <> 0 then + break; + end; + + if (offset >= -maxYStep) and (offset <= maxYStep) + and (TestCollisionYImpl(x + direction, y + offset, Sentry^.Radius, -1, Sentry^.CollisionMask) = 0) then + begin + if not TestOnly then + begin + Sentry^.X := Sentry^.X + signAs(_1, Sentry^.dX); + Sentry^.Y := Sentry^.Y + int2hwFloat(offset); + end; + MakeSentryStep := true + end +end; + +function MakeSentryJump(Sentry: PGear; maxXStep, maxYStep: LongInt): Boolean; +var x, y, offsetX, offsetY, direction: LongInt; + jumpTime: hwFloat; +begin + MakeSentryJump := false; + x := hwRound(Sentry^.X); + y := hwRound(Sentry^.Y); + offsetX := maxXStep - Sentry^.Radius; + direction := hwSign(Sentry^.dX); + + repeat + for offsetY := -maxYStep - 1 to maxYStep + 1 do + if TestCollisionYImpl(x + offsetX * direction, y + offsetY, Sentry^.Radius, 1, Sentry^.CollisionMask) <> 0 then + break; + if (offsetY >= -maxYStep) and (offsetY <= maxYStep) then + break; + Dec(offsetX, Sentry^.Radius); + until offsetX <= 0; + + if (offsetX >= Sentry^.Radius) and (not cGravity.isNegative) then + begin + Sentry^.dY := -_0_25; + jumpTime := _2 * Sentry^.dY / cGravity; + Sentry^.dX := SignAs(int2hwFloat(offsetX - Sentry^.Radius) / jumpTime, Sentry^.dX); + Sentry^.State := Sentry^.State or gstHHJumping; + MakeSentryJump := true; + end; +end; + +function TraceAttackPath(fromX, fromY, toX, toY, step: hwFloat; mask: Word): LongWord; +var distX, distY, dist, invDistance: HwFloat; + i, count: LongInt; +begin + TraceAttackPath := 0; + if (step < _1) + or ((hwRound(fromX) and LAND_WIDTH_MASK) <> 0) + or ((hwRound(toX) and LAND_WIDTH_MASK) <> 0) + or ((hwRound(fromY) and LAND_HEIGHT_MASK) <> 0) + or ((hwRound(toY) and LAND_HEIGHT_MASK) <> 0) then + exit; + + distX := toX - fromX; + distY := toY - fromY; + dist := Distance(distX, distY); + count := hwRound(dist / step); + + invDistance := step / dist; + distX := distX * invDistance; + distY := distY * invDistance; + + for i := 0 to count - 1 do + begin + if (Land[hwRound(fromY), hwRound(fromX)] and mask) <> 0 then + Inc(TraceAttackPath); + fromX := fromX + distX; + fromY := fromY + distY; + end +end; + +function CheckSentryAttackRange(Sentry: PGear; targetX, targetY: HwFloat): Boolean; +var distX, distY: hwFloat; +begin + distX := targetX - Sentry^.X; + distY := targetY - Sentry^.Y; + CheckSentryAttackRange := + (distX.isNegative = Sentry^.dX.isNegative) + and (distX.Round > 24) + and (distX.Round < 500) + and (hwAbs(distY) < hwAbs(distX * _1_5)) + and (TraceAttackPath(Sentry^.X, Sentry^.Y, targetX, targetY, _4, lfLandMask) <= 18); +end; + +procedure ResetSentryState(Gear: PGear; state, timer: LongInt); +begin + Gear^.Timer := timer; + Gear^.Tag := state; + Gear^.Target.X := 0; + Gear^.Target.Y := 0; + if Gear^.Karma <> 0 then + begin + ClearGlobalHitOrderLeq(Gear^.Karma); + Gear^.Karma := 0; + end; +end; + +function CheckSentryDestroyed(Sentry: PGear; damagedState: LongInt): Boolean; +begin + CheckSentryDestroyed := false; + if Sentry^.Damage > 0 then + begin + dec(Sentry^.Health, Sentry^.Damage); + Sentry^.Damage := 0; + if Sentry^.Health <= 0 then + begin + doMakeExplosion(hwRound(Sentry^.X), hwRound(Sentry^.Y), Sentry^.Boom, Sentry^.Hedgehog, EXPLAutoSound); + DeleteGear(Sentry); + CheckSentryDestroyed := true; + exit; + end + else + ResetSentryState(Sentry, damagedState, 10000) + end; + + if ((Sentry^.Health * 100) < random(cSentryHealth * 90)) and ((GameTicks and $FF) = 0) then + if Sentry^.Health * 2 < cSentryHealth then + AddVisualGear(hwRound(Sentry^.X) - 8 + Random(16), hwRound(Sentry^.Y) - 2, vgtSmoke) + else + AddVisualGear(hwRound(Sentry^.X) - 8 + Random(16), hwRound(Sentry^.Y) - 2, vgtSmokeWhite); +end; + +procedure AimSentry(Sentry: PGear); +var HHGear: PGear; +begin + if CurrentHedgehog <> nil then + begin + HHGear := CurrentHedgehog^.Gear; + if HHGear <> nil then + begin + Sentry^.Target.X := Sentry^.Target.X + hwSign(HHGear^.X - int2hwFloat(Sentry^.Target.X)); + Sentry^.Target.Y := Sentry^.Target.Y + hwSign(HHGear^.Y - int2hwFloat(Sentry^.Target.Y)); + end; + end; +end; + +procedure MakeSentryShot(Sentry: PGear); +var bullet: PGear; + distX, distY, invDistance: HwFloat; +begin + distX := int2hwFloat(Sentry^.Target.X) - Sentry^.X; + distY := int2hwFloat(Sentry^.Target.Y) - Sentry^.Y; + invDistance := _1 / Distance(distX, distY); + distX := distX * invDistance; + distY := distY * invDistance; + + bullet := AddGear( + hwRound(Sentry^.X), hwRound(Sentry^.Y), + gtMinigunBullet, 0, + distX * _0_9 + rndSign(getRandomf * _0_1), + distY * _0_9 + rndSign(getRandomf * _0_1), + 0); + + bullet^.Karma := 12; + bullet^.Pos := 1; // To tell apart from minigun bullets + bullet^.WDTimer := GameTicks; + bullet^.PortalCounter := 1; + bullet^.Elasticity := Sentry^.X; + bullet^.Friction := Sentry^.Y; + bullet^.Data := Pointer(Sentry); + bullet^.Hedgehog := Sentry^.Hedgehog; + + CreateShellForGear(Sentry, Sentry^.WDTimer and 1); + PlaySound(sndGun); +end; + +function GetSentryTarget(sentry: PGear): PGear; +var HHGear: PGear; + friendlyTarget: boolean; +begin + GetSentryTarget := nil; + friendlyTarget := false; + + if CurrentHedgehog <> nil then + begin + HHGear := CurrentHedgehog^.Gear; + if HHGear <> nil then + if ((HHGear^.State and gstHHDriven) <> 0) and (HHGear^.CollisionIndex < 0) then + begin + if sentry^.Hedgehog <> nil then + friendlyTarget := sentry^.Hedgehog^.Team^.Clan = CurrentHedgehog^.Team^.Clan; + if not friendlyTarget then + GetSentryTarget := HHGear; + end + end +end; + +procedure doStepSentryLand(Gear: PGear); +var HHGear: PGear; + land: Word; +const sentry_Idle = 0; + sentry_Walking = 1; + sentry_Aiming = 2; + sentry_Attacking = 3; + sentry_Reloading = 4; +begin + HHGear:= nil; + + if CheckGearDrowning(Gear) then + exit; + + if CheckSentryDestroyed(Gear, sentry_Reloading) then + exit; + + land := TestCollisionYwithGear(Gear, 1); + if Gear^.dY.isNegative or (land = 0) or + ((Gear^.dY.QWordValue > _0_01.QWordValue) and ((Gear^.State and gstHHJumping) = 0)) then + begin + DeleteCI(Gear); + doStepFallingGear(Gear); + if not (Gear^.Tag in [sentry_Idle, sentry_Reloading]) then + ResetSentryState(Gear, sentry_Idle, 1000); + exit; + end + else + begin + AddCI(Gear); + Gear^.State := Gear^.State and (not gstHHJumping); + Gear^.dX := SignAs(_0, Gear^.dX); + Gear^.dY := SignAs(_0, Gear^.dY); + end; + + if Gear^.Timer > 0 then dec(Gear^.Timer); + + if Gear^.Timer = 0 then + begin + DeleteCI(Gear); + if Gear^.Tag = sentry_Idle then + begin + Gear^.Tag := sentry_Walking; + Gear^.Timer := 1000 + GetRandom(3000); + if GetRandom(4) = 0 then + begin + if MakeSentryJump(Gear, 80, 60) then + Gear^.Timer := 4000 + else + Gear^.Timer := 1000; + Gear^.Tag := sentry_Idle; + end + else + begin + Gear^.dX.isNegative := GetRandom(2) = 1; + + if MakeSentryStep(Gear, 6, true) then + begin + if GetRandom(4) = 0 then + begin + Gear^.Timer := 2000; + Gear^.Tag := sentry_Idle; + end; + end + else + begin + Gear^.dX.isNegative := not Gear^.dX.isNegative; + if not MakeSentryStep(Gear, 6, true) then + begin + if GetRandom(2) = 0 then + begin + Gear^.dY := - _0_1; + if TestCollisionYKick(Gear, -1) = 0 then + Gear^.dY := - _0_25; + Gear^.Timer := 3000; + end + else + Gear^.Timer := 5000; + Gear^.Tag := sentry_Idle; + end; + end + end + end + else if Gear^.Tag in [sentry_Walking, sentry_Reloading] then + begin + Gear^.Tag := sentry_Idle; + Gear^.Timer := 1000 + GetRandom(1000); + end + else if Gear^.Tag = sentry_Aiming then + begin + if CheckSentryAttackRange(Gear, int2hwFloat(Gear^.Target.X), int2hwFloat(Gear^.Target.Y)) then + begin + Gear^.WDTimer := 5 + GetRandom(3); + Gear^.Tag := sentry_Attacking; + Gear^.Timer := 100; + end + else + begin + Gear^.Target.X := 0; + Gear^.Target.Y := 0; + Gear^.Tag := sentry_Idle; + Gear^.Timer := 5000; + end + end + else if Gear^.Tag = sentry_Attacking then + begin + MakeSentryShot(Gear); + + if Gear^.WDTimer = 0 then + ResetSentryState(Gear, sentry_Reloading, 6000 + GetRandom(2000)) + else + begin + dec(Gear^.WDTimer); + Gear^.Timer := 100; + end + end; + AddCI(Gear); + end; + + if (Gear^.Tag = sentry_Walking) and ((GameTicks and $1F) = 0) then + begin + DeleteCI(Gear); + if not MakeSentryStep(Gear, 6, false) then + Gear^.Timer := 0; + AddCI(Gear); + end; + + if ((GameTicks and $1F) = 0) and (Gear^.Tag = sentry_Aiming) then + AimSentry(Gear); + + if ((GameTicks and $FF) = 0) + and (Gear^.Tag in [sentry_Idle, sentry_Walking]) then + begin + HHGear := GetSentryTarget(Gear); + if HHGear <> nil then + if CheckSentryAttackRange(Gear, HHGear^.X, HHGear^.Y) then + begin + Gear^.Target.X := hwRound(HHGear^.X); + Gear^.Target.Y := hwRound(HHGear^.Y); + Gear^.Karma := GameTicks; + Gear^.Tag := sentry_Aiming; + Gear^.Timer := 1800 + GetRandom(400); + end + end +end; + +procedure doStepSentryWater(Gear: PGear); +var HHGear: PGear; +const sentry_Idle = 0; + sentry_Walking = 1; + sentry_Aiming = 2; + sentry_Attacking = 3; + sentry_Reloading = 4; +begin + if Gear^.Tag < 0 then + begin + CheckGearDrowning(Gear); + exit; + end; + + Gear^.Y := int2hwFloat(cWaterLine - 3 * Gear^.Radius); + if TestCollisionYImpl(hwRound(Gear^.X), hwRound(Gear^.Y), Gear^.Radius - 1, -1, Gear^.CollisionMask and lfLandMask) <> 0 then + begin + Gear^.Tag := -1; + exit; + end; + + if CheckSentryDestroyed(Gear, sentry_Reloading) then + exit; + + if Gear^.Timer > 0 then dec(Gear^.Timer); + + if Gear^.Timer = 0 then + begin + if Gear^.Tag = sentry_Idle then + begin + Gear^.Tag := sentry_Walking; + Gear^.Timer := 3000 + GetRandom(3000); + Gear^.dX.isNegative := GetRandom(2) = 1; + if TestCollisionXwithGear(Gear, hwSign(Gear^.dX)) <> 0 then + Gear^.dX.isNegative := not Gear^.dX.isNegative; + end + else if Gear^.Tag in [sentry_Walking, sentry_Reloading] then + begin + Gear^.Tag := sentry_Idle; + Gear^.Timer := 1000 + GetRandom(1000); + end + else if Gear^.Tag = sentry_Aiming then + begin + if CheckSentryAttackRange(Gear, int2hwFloat(Gear^.Target.X), int2hwFloat(Gear^.Target.Y)) then + begin + Gear^.WDTimer := 5 + GetRandom(3); + Gear^.Tag := sentry_Attacking; + Gear^.Timer := 100; + end + else + begin + Gear^.Target.X := 0; + Gear^.Target.Y := 0; + Gear^.Tag := sentry_Idle; + Gear^.Timer := 5000; + end + end + else if Gear^.Tag = sentry_Attacking then + begin + MakeSentryShot(Gear); + + if Gear^.WDTimer = 0 then + ResetSentryState(Gear, sentry_Reloading, 6000 + GetRandom(2000)) + else + begin + dec(Gear^.WDTimer); + Gear^.Timer := 100; + end + end; + end; + + if (Gear^.Tag = sentry_Walking) and ((GameTicks and $1F) = 0) then + begin + if TestCollisionXwithGear(Gear, hwSign(Gear^.dX)) = 0 then + begin + Gear^.dX := SignAs(_1, Gear^.dX); + Gear^.X := Gear^.X + Gear^.dX; + WorldWrap(Gear); + end + else + Gear^.Timer := 0 + end; + + if ((GameTicks and $1F) = 0) and (Gear^.Tag = sentry_Aiming) then + AimSentry(Gear); + + if ((GameTicks and $FF) = 0) + and (Gear^.Tag in [sentry_Idle, sentry_Walking]) then + begin + HHGear := GetSentryTarget(Gear); + if HHGear <> nil then + if CheckSentryAttackRange(Gear, HHGear^.X, HHGear^.Y) then + begin + Gear^.Target.X := hwRound(HHGear^.X); + Gear^.Target.Y := hwRound(HHGear^.Y); + Gear^.Karma := GameTicks; + Gear^.Tag := sentry_Aiming; + Gear^.Timer := 1800 + GetRandom(400); + end + end +end; + +procedure doStepSentryDeploy(Gear: PGear); +begin + Gear^.Tag := -1; + if hwRound(Gear^.Y) + 3 * Gear^.Radius >= cWaterLine then + begin + Gear^.Y := int2hwFloat(cWaterLine - 3 * Gear^.Radius); + if Gear^.Timer > 0 then dec(Gear^.Timer); + if Gear^.Timer = 0 then + begin + Gear^.Tag := 0; + Gear^.doStep := @doStepSentryWater; + end; + end + else if Gear^.dY.isNegative or (TestCollisionYwithGear(Gear, 1) = 0) then + doStepFallingGear(Gear) + else + begin + if Gear^.Timer > 0 then dec(Gear^.Timer); + if Gear^.Timer = 0 then + begin + Gear^.Tag := 0; + Gear^.doStep := @doStepSentryLand; + end; + end; +end; + end. diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGearsHedgehog.pas --- a/hedgewars/uGearsHedgehog.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGearsHedgehog.pas Sun Mar 24 14:33:57 2024 -0400 @@ -400,6 +400,7 @@ else newGear^.Tag:= 1; end; + amSentry: newGear:= AddGear(hwRound(lx) + hwSign(dX) * 7, hwRound(ly), gtSentry, 0, SignAs(_0_03, dX), _0, 0); amFirePunch: newGear:= AddGear(hwRound(lx) + hwSign(dX) * 10, hwRound(ly), gtFirePunch, 0, xx, _0, 0); amWhip: begin newGear:= AddGear(hwRound(lx) + hwSign(dX) * 10, hwRound(ly), gtWhip, 0, SignAs(_1, dX), - _0_8, 0); @@ -529,7 +530,8 @@ amMineStrike, amDrillStrike, amRubber, amMinigun: CurAmmoGear:= newGear; end; - if CurAmmoType = amCake then FollowGear:= newGear; + if (CurAmmoType = amCake) or (CurAmmoType = amPiano) then + FollowGear:= newGear; if ((CurAmmoType = amMine) or (CurAmmoType = amSMine) or (CurAmmoType = amAirMine)) and (GameFlags and gfInfAttack <> 0) then newGear^.FlightTime:= GameTicks + min(TurnTimeLeft,1000) @@ -1279,7 +1281,7 @@ uStats.hedgehogFlight(Gear, Gear^.FlightTime); Gear^.FlightTime:= 0; end; -if (WorldEdge = weNone) and (not Gear^.Hedgehog^.FlownOffMap) and (not isZero(Gear^.dX)) and (not isUnderwater) and ((Gear^.State and gstHHDriven) = 0) and (hwRound(Gear^.Y) < cWaterLine-300) and ((hwRound(Gear^.X) < leftX-2048) or (hwRound(Gear^.X) > rightX+2048)) then +if (WorldEdge = weNone) and (not hasBorder) and (not Gear^.Hedgehog^.FlownOffMap) and (not isZero(Gear^.dX)) and (not isUnderwater) and ((Gear^.State and gstHHDriven) = 0) and (hwRound(Gear^.Y) < cWaterLine-300) and ((hwRound(Gear^.X) < -cCamLimitX) or (hwRound(Gear^.X) > LAND_WIDTH+cCamLimitX)) then begin PlaySoundV(sndFlyAway, Gear^.Hedgehog^.Team^.voicepack); Gear^.Hedgehog^.FlownOffMap:= true; @@ -1409,7 +1411,7 @@ wasJumping:= ((HHGear^.State and gstHHJumping) <> 0); if ((HHGear^.Message and gmHJump) <> 0) and wasJumping and ((HHGear^.State and gstHHHJump) = 0) then - if (not (hwAbs(HHGear^.dX) > cLittle)) and (HHGear^.dY < -_0_02) then + if (not (hwAbs(HHGear^.dX) > cLittle)) and (HHGear^.dY < _0_05) then begin HHGear^.State:= HHGear^.State or gstHHHJump; HHGear^.dY:= -_0_25; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGearsList.pas --- a/hedgewars/uGearsList.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGearsList.pas Sun Mar 24 14:33:57 2024 -0400 @@ -107,6 +107,7 @@ (* gtCreeper *) , amCreeper (* gtMinigun *) , amMinigun (* gtMinigunBullet *) , amMinigun +(* gtSentry *) , amSentry ); @@ -259,6 +260,7 @@ gtSnowball, gtKnife, gtCreeper, + gtSentry, gtMolotov, gtFlake, gtGrave, @@ -315,7 +317,8 @@ gtPoisonCloud: Gear^.Boom := 20; gtKnife: Gear^.Boom := 40000; // arbitrary scaling factor since impact-based gtCreeper: Gear^.Boom := 100; - gtMinigunBullet: Gear^.Boom := 2; + gtMinigunBullet: Gear^.Boom := 2; + gtSentry: Gear^.Boom := 40; end; case Kind of @@ -560,13 +563,16 @@ end; gtDEagleShot: begin gear^.Radius:= 1; - gear^.Health:= 50 + gear^.Health:= 50; + gear^.Data:= nil; end; gtSniperRifleShot: begin gear^.Radius:= 1; gear^.Health:= 50 end; gtDynamite: begin + gear^.ImpactSound:= sndDynamiteImpact; + gear^.nImpactSounds:= 1; gear^.Radius:= 3; gear^.Elasticity:= _0_55; gear^.Friction:= _0_03; @@ -623,7 +629,7 @@ gear^.Friction:= _0_995 end; gtBlowTorch: begin - gear^.Radius:= cHHRadius + cBlowTorchC; + gear^.Radius:= cHHRadius + cBlowTorchC - 1; if gear^.Timer = 0 then gear^.Timer:= 7500 end; gtSwitcher: begin @@ -772,6 +778,7 @@ end; gtPoisonCloud: begin if gear^.Timer = 0 then gear^.Timer:= 5000; + gear^.WDTimer:= gear^.Timer; // initial timer gear^.dY:= int2hwfloat(-4 + longint(getRandom(8))) / 1000; gear^.Tint:= $C0C000C0 end; @@ -825,6 +832,19 @@ gtMinigunBullet: begin gear^.Radius:= 1; gear^.Health:= 2; + gear^.Karma:= 5; //impact radius + gear^.Pos:= 0; //uses non-global hit order + gear^.Data:= nil; + end; + gtSentry: begin + gear^.Radius:= cHHRadius; + gear^.Health:= cSentryHealth; + gear^.Friction:= _0_999; + gear^.Elasticity:= _0_35; + gear^.Density:= _3; + gear^.Tag:= 0; + gear^.Timer:= 1000; + gear^.WDTimer:= 0; end; gtGenericFaller:begin gear^.AdvBounce:= 1; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGearsRender.pas --- a/hedgewars/uGearsRender.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGearsRender.pas Sun Mar 24 14:33:57 2024 -0400 @@ -549,14 +549,17 @@ ly:= ly + ay; tx:= round(lx); ty:= round(ly); - // reached edge of land. - if ((ty and LAND_HEIGHT_MASK) <> 0) and (((ty < LAND_HEIGHT) and (ay < 0)) or ((ty >= TopY) and (ay > 0))) then + // reached top edge of land mask + if (WorldEdge <> weBounce) and (WorldEdge <> weWrap) and + ((ty and LAND_HEIGHT_MASK) <> 0) and (((ty < LAND_HEIGHT) and (ay < 0)) or ((ty >= TopY) and (ay > 0))) then begin // assume infinite beam. Extend it way out past camera tx:= round(lx + ax * (max(LAND_WIDTH,4096) div 2)); ty:= round(ly + ay * (max(LAND_WIDTH,4096) div 2)); break; end; + if ((WorldEdge = weWrap) or (WorldEdge = weBounce)) and ((ty < -cCamLimitY) or (ty >= TopY + cCamLimitY)) then + break; if ((hogLR < 0) and (tx < LeftX)) or ((hogLR > 0) and (tx >= RightX)) then if (WorldEdge = weWrap) then @@ -565,7 +568,7 @@ if hogLR < 0 then lx:= RightX - (ax - (lx - LeftX)) else - lx:= LeftX + (ax - (RightX - lx)); + lx:= LeftX + (-ax - (RightX - lx)); tx:= round(lx); inc(wraps); end @@ -573,16 +576,16 @@ // just stop break; - if ((tx and LAND_WIDTH_MASK) <> 0) and (((ax > 0) and (tx >= RightX)) or ((ax < 0) and (tx <= LeftX))) then + // reached horizontal edge of land mask + if ((tx and LAND_WIDTH_MASK) <> 0) and (((ax > 0) and (tx >= RightX)) or ((ax < 0) and (tx <= LeftX))) and + (WorldEdge <> weWrap) and (WorldEdge <> weBounce) then begin - if (WorldEdge <> weWrap) and (WorldEdge <> weBounce) then - // assume infinite beam. Extend it way out past camera - begin - tx:= round(lx + ax * (max(LAND_WIDTH,4096) div 2)); - ty:= round(ly + ay * (max(LAND_WIDTH,4096) div 2)); - end; + // assume infinite beam. Extend it way out past camera + tx:= round(lx + ax * (max(LAND_WIDTH,4096) div 2)); + ty:= round(ly + ay * (max(LAND_WIDTH,4096) div 2)); break; end; + inWorldBounds := ((ty and LAND_HEIGHT_MASK) or (tx and LAND_WIDTH_MASK)) = 0; end; @@ -662,8 +665,8 @@ end; gtBlowTorch: begin - DrawSpriteRotated(sprBlowTorch, hx, hy, sign, aangle); - DrawHedgehog(sx, sy, + DrawSpriteRotated(sprBlowTorch, ox + 8 * sign, oy - 2, sign, aangle); + DrawHedgehog(ox + 1, oy - 3, sign, 3, HH^.visStepPos div 2, @@ -673,8 +676,8 @@ begin DrawTextureF(curhat, 1, - sx, - sy - 5, + ox + 1, + oy - 8, 0, sign, 32, @@ -688,8 +691,8 @@ Tint(HH^.Team^.Clan^.Color shl 8 or $FF); DrawTextureF(curhat, 1, - sx, - sy - 5, + ox + 1, + oy - 8, tx, sign, 32, @@ -697,7 +700,8 @@ untint end end; - defaultPos:= false + defaultPos:= false; + sign:= hwSign(Gear^.dX); end; gtFirePunch: begin @@ -894,6 +898,7 @@ amClusterBomb: DrawSpriteRotated(sprHandCluster, hx, hy, sign, aangle); amDynamite: DrawSpriteRotated(sprHandDynamite, hx, hy, sign, aangle); amCreeper: DrawSpriteRotatedF(sprHandCreeper, hx, hy, 0, sign, aangle); + amSentry: DrawSpriteRotated(sprHandSentry, hx, hy, sign, aangle); amHellishBomb: DrawSpriteRotated(sprHandHellish, hx, hy, sign, aangle); amGasBomb: DrawSpriteRotated(sprHandCheese, hx, hy, sign, aangle); amMine: DrawSpriteRotated(sprHandMine, hx, hy, sign, aangle); @@ -1510,7 +1515,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); @@ -1653,8 +1661,8 @@ gtPoisonCloud: begin if Gear^.Timer < 1020 then Tint(Gear^.Tint and $FFFFFF00 or Gear^.Timer div 8) - else if Gear^.Timer > 3980 then - Tint(Gear^.Tint and $FFFFFF00 or (5000 - Gear^.Timer) div 8) + else if (Gear^.Timer > Gear^.WDTimer - 1020) and (Gear^.WDTimer > 2040) then + Tint(Gear^.Tint and $FFFFFF00 or (Gear^.WDTimer - Gear^.Timer) div 8) else Tint(Gear^.Tint); DrawTextureRotatedF(SpritesData[sprSmokeWhite].texture, 3, 0, 0, x, y, 0, 1, 22, 22, (RealTicks shr 4 + Gear^.UID * 100) mod 360); @@ -1738,7 +1746,11 @@ gtCreeper: if (Gear^.Hedgehog <> nil) and (Gear^.Hedgehog^.Gear <> nil) then DrawSpriteRotatedF(sprCreeper, x, y, 1, hwRound(SignAs(_1,Gear^.Hedgehog^.Gear^.X-Gear^.X)), 0) else DrawSpriteRotatedF(sprCreeper, x, y, 1, hwRound(SignAs(_1,Gear^.dX)), 0); - + gtSentry: begin + DrawSpriteRotated(sprSentry, x, y, hwSign(Gear^.dX), 0); + if Gear^.Hedgehog <> nil then + DrawCircle(x, y, Gear^.Radius, 2, Gear^.Hedgehog^.Team^.Clan^.Color shl 8 or $FF); + end; gtGenericFaller: begin // DEBUG: draw gtGenericFaller if Gear^.Tag <> 0 then diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uGearsUtils.pas --- a/hedgewars/uGearsUtils.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uGearsUtils.pas Sun Mar 24 14:33:57 2024 -0400 @@ -41,6 +41,9 @@ procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt); inline; procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity: boolean); +procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity, deleteOnFail: boolean); +procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right, Bottom: LongInt; skipProximity, deleteOnFail: boolean); +function CountLand(x, y, r, c: LongInt; mask, antimask: LongWord): LongInt; function CheckGearNear(Kind: TGearType; X, Y: hwFloat; rX, rY: LongInt): PGear; function CheckGearNear(Gear: PGear; Kind: TGearType; rX, rY: LongInt): PGear; @@ -55,6 +58,7 @@ function SpawnBoxOfSmth: PGear; procedure PlayBoxSpawnTaunt(Gear: PGear); procedure ShotgunShot(Gear: PGear); +function CountHogsInTeam(HHGear: PGear; countHidden: boolean): LongInt; function CanUseTardis(HHGear: PGear): boolean; procedure SetAllToActive; @@ -148,7 +152,8 @@ gtTarget, gtFlame, gtKnife, - gtExplosives: begin + gtExplosives, + gtSentry: begin // Run the calcs only once we know we have a type that will need damage tdX:= Gear^.X-fX; tdY:= Gear^.Y-fY; @@ -179,6 +184,8 @@ if (not GameOver) then Gear^.State:= (Gear^.State and (not gstWinner)); end; + if Gear^.Kind = gtSentry then + Gear^.State:= Gear^.State and (not gstHHJumping); Gear^.Active:= true; if Gear^.Kind <> gtFlame then FollowGear:= Gear; if Gear^.Kind = gtAirMine then @@ -317,25 +324,25 @@ uStats.HedgehogDamaged(Gear, AttackerHog, Damage, false); - if AprilOne and (Gear^.Hedgehog^.Hat = 'fr_tomato') and (Damage > 2) then - for i := 0 to random(min(Damage,20))+5 do - begin - vg:= AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtStraightShot); - if vg <> nil then - with vg^ do + if AprilOne and (Gear^.Hedgehog^.Hat = 'fr_tomato') and (Damage > 2) then + for i := 0 to random(min(Damage,20))+5 do begin - dx:= 0.001 * (random(100)+10); - dy:= 0.001 * (random(100)+10); - tdy:= -cGravityf; - if random(2) = 0 then - dx := -dx; - FrameTicks:= random(500) + 1000; - State:= ord(sprBubbles); - Tint:= $ff0000ff + vg:= AddVisualGear(hwRound(Gear^.X), hwRound(Gear^.Y), vgtStraightShot); + if vg <> nil then + with vg^ do + begin + dx:= 0.001 * (random(100)+10); + dy:= 0.001 * (random(100)+10); + tdy:= -cGravityf; + if random(2) = 0 then + dx := -dx; + FrameTicks:= random(500) + 1000; + State:= ord(sprBubbles); + Tint:= $ff0000ff + end end - end - end else - Gear^.Hedgehog:= AttackerHog; + end else if AttackerHog <> nil then + Gear^.Hedgehog:= AttackerHog; inc(Gear^.Damage, Damage); ScriptCall('onGearDamage', Gear^.UID, Damage); @@ -920,10 +927,20 @@ procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt); inline; begin - FindPlace(Gear, withFall, Left, Right, false); + FindPlace(Gear, withFall, Left, Right, false, true); end; -procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity: boolean); +procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity: boolean); inline; +begin + FindPlace(Gear, withFall, Left, Right, skipProximity, true); +end; + +procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity, deleteOnFail: boolean); inline; +begin + FindPlace(Gear, withFall, Left, Right, cWaterLine, skipProximity, deleteOnFail); +end; + +procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right, Bottom: LongInt; skipProximity, deleteOnFail: boolean); var x: LongInt; y, sy, dir: LongInt; ar: array[0..1023] of TPoint; @@ -952,36 +969,35 @@ repeat cnt:= 0; y:= min(1024, topY) - Gear^.Radius shl 1; - while y < cWaterLine do + while y < Bottom do begin repeat inc(y, 2); - until (y >= cWaterLine) or - (ignoreOverLap and (CountLand(x, y, Gear^.Radius - 1, 1, $FF00, 0) = 0)) or - (not ignoreOverLap and (CountLand(x, y, Gear^.Radius - 1, 1, $FFFF, 0) = 0)); - + until (y >= Bottom) or + (ignoreOverLap and (CountLand(x, y, Gear^.Radius - 1, 1, lfLandMask, 0) = 0)) or + (not ignoreOverLap and (CountLand(x, y, Gear^.Radius - 1, 1, lfAll, 0) = 0)); sy:= y; repeat inc(y); - until (y >= cWaterLine) or + until (y >= Bottom) or (ignoreOverlap and - (CountLand(x, y, Gear^.Radius - 1, 1, $FFFF, 0) <> 0)) or + (CountLand(x, y, Gear^.Radius - 1, 1, lfAll, 0) <> 0)) or (not ignoreOverlap and (CountLand(x, y, Gear^.Radius - 1, 1, lfLandMask, 0) <> 0)); - if (y - sy > Gear^.Radius * 2) and (y < cWaterLine) + if (y - sy > Gear^.Radius * 2) and (y < Bottom) and (((Gear^.Kind = gtExplosives) and (ignoreNearObjects or NoGearsToAvoid(x, y - Gear^.Radius, 60, 60)) - and (isSteadyPosition(x, y+1, Gear^.Radius - 1, 3, $FFFF) - or (CountLand(x, y+1, Gear^.Radius - 1, Gear^.Radius+1, $FFFF, 0) > Gear^.Radius) + and (isSteadyPosition(x, y+1, Gear^.Radius - 1, 3, lfAll) + or (CountLand(x, y+1, Gear^.Radius - 1, Gear^.Radius+1, lfAll, 0) > Gear^.Radius) )) or ((Gear^.Kind <> gtExplosives) and (ignoreNearObjects or NoGearsToAvoid(x, y - Gear^.Radius, 110, 110)) and (isSteadyPosition(x, y+1, Gear^.Radius - 1, 3, lfIce) - or (CountLand(x, y+1, Gear^.Radius - 1, Gear^.Radius+1, $FFFF, lfIce) <> 0) + or (CountLand(x, y+1, Gear^.Radius - 1, Gear^.Radius+1, lfAll, lfIce) <> 0) ))) then begin ar[cnt].X:= x; @@ -1025,18 +1041,17 @@ begin Gear^.X:= int2hwFloat(x); Gear^.Y:= int2hwFloat(y); - AddFileLog('Assigned Gear coordinates (' + inttostr(x) + ',' + inttostr(y) + ')'); + AddFileLog('FindPlace: Assigned Gear coordinates (' + inttostr(x) + ',' + inttostr(y) + ')'); end end else begin - OutError('Can''t find place for Gear', false); + OutError('FindPlace: Can''t find place for Gear', false); if Gear^.Kind = gtHedgehog then begin cnt:= 0; if GameTicks = 0 then begin - //AddFileLog('Trying to make a hole'); while (cnt < 1000) do begin inc(cnt); @@ -1047,20 +1062,22 @@ Gear^.State:= Gear^.State or gsttmpFlag; Gear^.X:= int2hwFloat(x); Gear^.Y:= int2hwFloat(y); - AddFileLog('Picked a spot for hog at coordinates (' + inttostr(hwRound(Gear^.X)) + ',' + inttostr(hwRound(Gear^.Y)) + ')'); + AddFileLog('FindPlace: Picked alternative spot for hog at coordinates (' + inttostr(hwRound(Gear^.X)) + ',' + inttostr(hwRound(Gear^.Y)) + ')'); cnt:= 2000 end end; end; - if cnt < 2000 then + if (deleteOnFail) and (cnt < 2000) then begin + AddFileLog('FindPlace: No place found, deleting hog'); Gear^.Hedgehog^.Effects[heResurrectable] := 0; DeleteGear(Gear); Gear:= nil end end - else + else if (deleteOnFail) then begin + AddFileLog('FindPlace: No place found, deleting Gear'); DeleteGear(Gear); Gear:= nil end @@ -1069,11 +1086,11 @@ function CheckGearNearImpl(Kind: TGearType; X, Y: hwFloat; rX, rY: LongInt; exclude: PGear): PGear; var t: PGear; - width, bound, dX, dY: hwFloat; + width, dX, dY: hwFloat; isHit: Boolean; - i, j: LongWord; + i, j, bound: LongWord; begin - bound:= _1_5 * int2hwFloat(max(rX, rY)); + bound:= max(rX, rY) * 3 div 2; rX:= sqr(rX); rY:= sqr(rY); width:= int2hwFloat(RightX - LeftX); @@ -1084,20 +1101,20 @@ with TeamsArray[j]^ do for i:= 0 to cMaxHHIndex do with Hedgehogs[i] do - if (Gear <> nil) and (Gear <> exclude) then + if (not Unplaced) and (Gear <> nil) and (Gear <> exclude) then begin // code duplication - could throw into an inline function I guess dX := X - Gear^.X; dY := Y - Gear^.Y; - isHit := (hwAbs(dX) + hwAbs(dY) < bound) + isHit := (dX.Round + dY.Round < bound) and (not ((hwSqr(dX) / rX + hwSqr(dY) / rY) > _1)); if (not isHit) and (WorldEdge = weWrap) then begin - if (hwAbs(dX - width) + hwAbs(dY) < bound) + if ((dX - width).Round + dY.Round < bound) and (not ((hwSqr(dX - width) / rX + hwSqr(dY) / rY) > _1)) then isHit := true - else if (hwAbs(dX + width) + hwAbs(dY) < bound) + else if ((dX + width).Round + dY.Round < bound) and (not ((hwSqr(dX + width) / rX + hwSqr(dY) / rY) > _1)) then isHit := true end; @@ -1119,15 +1136,15 @@ begin dX := X - t^.X; dY := Y - t^.Y; - isHit := (hwAbs(dX) + hwAbs(dY) < bound) + isHit := (dX.Round + dY.Round < bound) and (not ((hwSqr(dX) / rX + hwSqr(dY) / rY) > _1)); if (not isHit) and (WorldEdge = weWrap) then begin - if (hwAbs(dX - width) + hwAbs(dY) < bound) + if ((dX - width).Round + dY.Round < bound) and (not ((hwSqr(dX - width) / rX + hwSqr(dY) / rY) > _1)) then isHit := true - else if (hwAbs(dX + width) + hwAbs(dY) < bound) + else if ((dX + width).Round + dY.Round < bound) and (not ((hwSqr(dX + width) / rX + hwSqr(dY) / rY) > _1)) then isHit := true end; @@ -1260,7 +1277,8 @@ gtKnife, gtCase, gtTarget, - gtExplosives: begin + gtExplosives, + gtSentry: begin //addFileLog('ShotgunShot radius: ' + inttostr(Gear^.Radius) + ', t^.Radius = ' + inttostr(t^.Radius) + ', distance = ' + inttostr(dist) + ', dmg = ' + inttostr(dmg)); dmg:= 0; r:= Gear^.Radius + t^.Radius; @@ -1320,15 +1338,38 @@ DrawExplosion(hwRound(Gear^.X), hwRound(Gear^.Y), cShotgunRadius) end; +// Return number of living hogs in HHGear's team +// * HHGear: hog gear for which to count team hogs +// * countHidden: if true, also count hidden hogs (e.g. time-travel) +function CountHogsInTeam(HHGear: PGear; countHidden: boolean): LongInt; +var i, j, cnt: LongInt; + HH: PHedgehog; +begin + if HHGear = nil then + exit(0); + HH:= HHGear^.Hedgehog; + cnt:= 0; + for j:= 0 to Pred(HH^.Team^.Clan^.TeamsNumber) do + for i:= 0 to Pred(HH^.Team^.Clan^.Teams[j]^.HedgehogsNumber) do + if (HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear <> nil) + and ((HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear^.State and gstDrowning) = 0) + and (HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear^.Health > HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear^.Damage) then + inc(cnt) + else if countHidden and (HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].GearHidden <> nil) then + inc(cnt); + CountHogsInTeam:= cnt; +end; + + // Returns true if the given hog gear can use the tardis function CanUseTardis(HHGear: PGear): boolean; var usable: boolean; - i, j, cnt: LongInt; + cnt: LongInt; HH: PHedgehog; begin (* Conditions for not activating. - 1. Hog is last of his clan + 1. Hog is last of their clan 2. Sudden Death is in play 3. Hog is a king *) @@ -1337,13 +1378,7 @@ if HHGear <> nil then if (HHGear = nil) or (HH^.King) or (SuddenDeathActive) then usable:= false; - cnt:= 0; - for j:= 0 to Pred(HH^.Team^.Clan^.TeamsNumber) do - for i:= 0 to Pred(HH^.Team^.Clan^.Teams[j]^.HedgehogsNumber) do - if (HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear <> nil) - and ((HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear^.State and gstDrowning) = 0) - and (HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear^.Health > HH^.Team^.Clan^.Teams[j]^.Hedgehogs[i].Gear^.Damage) then - inc(cnt); + cnt:= CountHogsInTeam(HHGear, false); if (cnt < 2) then usable:= false; CanUseTardis:= usable; @@ -1377,7 +1412,10 @@ if (Ammo^.Kind in [gtDEagleShot, gtSniperRifleShot, gtMinigunBullet, gtFirePunch, gtKamikaze, gtWhip, gtShover]) and (((Ammo^.Data <> nil) and (PGear(Ammo^.Data) = Gear)) - or (not UpdateHitOrder(Gear, Ammo^.WDTimer))) then + or (not UpdateHitOrder( + Gear, + Ammo^.WDTimer, + (Ammo^.Kind = gtMinigunBullet) and (Ammo^.Pos <> 0)))) then continue; if ((Ammo^.Kind = gtFlame) or (Ammo^.Kind = gtBlowTorch)) and @@ -1398,7 +1436,8 @@ gtKnife, gtTarget, gtCase, - gtExplosives: + gtExplosives, + gtSentry: begin if (Ammo^.Kind in [gtFirePunch, gtKamikaze]) and (Gear^.Kind <> gtSMine) then PlaySound(sndFirePunchHit); @@ -1462,8 +1501,16 @@ end else if ((Ammo^.Kind <> gtFlame) or (Gear^.Kind = gtHedgehog)) and (Power <> 0) then begin - Gear^.dX:= Ammo^.dX * Power * _0_01; - Gear^.dY:= Ammo^.dY * Power * _0_01 + if (Ammo^.Kind in [gtMinigunBullet]) then + begin + Gear^.dX:= Gear^.dX + Ammo^.dX * Power * _0_01; + Gear^.dY:= Gear^.dY + Ammo^.dY * Power * _0_01 + end + else + begin + Gear^.dX:= Ammo^.dX * Power * _0_01; + Gear^.dY:= Ammo^.dY * Power * _0_01 + end end; if (not isZero(Gear^.dX)) or (not isZero(Gear^.dY)) then @@ -1864,9 +1911,10 @@ function HomingWrap(var Gear: PGear): boolean; var dist_center, dist_right, dist_left: hwFloat; begin + HomingWrap:= false; + if WorldEdge = weWrap then begin - HomingWrap:= false; // We just check the same target 3 times: // 1) in current section (no change) // 2) clone in the right section diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uInputHandler.pas --- a/hedgewars/uInputHandler.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uInputHandler.pas Sun Mar 24 14:33:57 2024 -0400 @@ -121,8 +121,6 @@ // Takes a control name (e.g. 'quit') and returns the corresponding // human-readable key name from SDL. -// FIXME: Does not work 100% for all keys yet, but at least it no -// longer hardcodes any key name. // TODO: Localize function KeyBindToName(bind: shortstring): shortstring; var code: LongInt; @@ -141,8 +139,14 @@ KeyBindToName:= name else begin - WriteLnToConsole('Error: KeyBindToName('+bind+') failed to find SDL key name!'); - KeyBindToName:= trmsg[sidUnknownKey]; + if KeyNames[code] <> '' then + // Return Hedgewars internal key name if SDL key name is empty + KeyBindToName:= KeyNames[code] + else + begin + WriteLnToConsole('Error: KeyBindToName('+bind+'): Hedgewars does not have internal key name for given bind!'); + KeyBindToName:= trmsg[sidUnknownKey]; + end; end; end; end; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uLocale.pas --- a/hedgewars/uLocale.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uLocale.pas Sun Mar 24 14:33:57 2024 -0400 @@ -164,6 +164,10 @@ 8: curArg:= arg9; end; + // Replace % sign in argument with ASCII ESC + // to prevent infinite loop below. + ReplaceChars(curArg, '%', Char($1B)); + repeat p:= Pos('%'+IntToStr(i+1), tempstr); if (p <> 0) then @@ -173,6 +177,8 @@ end; until (p = 0); end; + +ReplaceChars(tempstr, Char($1B), '%'); Format:= tempstr; end; @@ -196,6 +202,10 @@ 8: curArg:= arg9; end; + // Replace % sign in argument with ASCII ESC + // to prevent infinite loop below. + ReplaceCharsA(curArg, '%', Char($1B)); + repeat p:= Pos('%'+IntToStr(i+1), tempstr); if (p <> 0) then @@ -205,6 +215,8 @@ end; until (p = 0); end; + +ReplaceCharsA(tempstr, Char($1B), '%'); FormatA:= tempstr; end; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uRender.pas --- a/hedgewars/uRender.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uRender.pas Sun Mar 24 14:33:57 2024 -0400 @@ -50,6 +50,7 @@ procedure DrawCircle (X, Y, Radius, Width: LongInt); procedure DrawCircle (X, Y, Radius, Width: LongInt; r, g, b, a: Byte); +procedure DrawCircle (X, Y, Radius, Width: LongInt; color: LongWord); procedure DrawCircleFilled (X, Y, Radius: LongInt; r, g, b, a: Byte); procedure DrawLine (X0, Y0, X1, Y1, Width: Single; color: LongWord); inline; @@ -1566,6 +1567,13 @@ untint; end; +procedure DrawCircle(X, Y, Radius, Width: LongInt; color: LongWord); +begin + Tint(color); + DrawCircle(X, Y, Radius, Width); + untint; +end; + procedure DrawCircle(X, Y, Radius, Width: LongInt); var i: LongInt; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uRenderUtils.pas --- a/hedgewars/uRenderUtils.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uRenderUtils.pas Sun Mar 24 14:33:57 2024 -0400 @@ -29,6 +29,9 @@ procedure copyToXY(src, dest: PSDL_Surface; destX, destY: LongInt); inline; procedure copyToXYFromRect(src, dest: PSDL_Surface; srcX, srcY, srcW, srcH, destX, destY: LongInt); +function GetSurfaceFrameCoordinateX(Surface: PSDL_Surface; Frame, frameWidth, frameHeight: LongInt): LongInt; +function GetSurfaceFrameCoordinateY(Surface: PSDL_Surface; Frame, frameHeight: LongInt): LongInt; + procedure DrawSprite2Surf(sprite: TSprite; dest: PSDL_Surface; x,y: LongInt); inline; procedure DrawSpriteFrame2Surf(sprite: TSprite; dest: PSDL_Surface; x,y: LongInt; frame: LongInt); procedure DrawLine2Surf(dest: PSDL_Surface; x0,y0,x1,y1:LongInt; r,g,b: byte); @@ -78,6 +81,24 @@ WriteInRoundRect:= WriteInRoundRect(Surface, X, Y, Color, Font, s, 0); end;*) +function GetSurfaceFrameCoordinateX(Surface: PSDL_Surface; Frame, frameWidth, frameHeight: LongInt): LongInt; +var nx, ny: LongInt; +begin + nx:= Surface^.w div frameWidth; // number of horizontal frames + if nx = 0 then nx:= 1; // one frame is minimum + ny:= Surface^.h div frameHeight; // number of vertical frames + if ny = 0 then ny:= 1; + GetSurfaceFrameCoordinateX:= (Frame div ny) * frameWidth; +end; + +function GetSurfaceFrameCoordinateY(Surface: PSDL_Surface; Frame, frameHeight: LongInt): LongInt; +var ny: LongInt; +begin + ny:= Surface^.h div frameHeight; // number of vertical frames + if ny = 0 then ny:= 1; // one frame is minimum + GetSurfaceFrameCoordinateY:= (Frame mod ny) * frameHeight; +end; + function IsTooDarkToRead(TextColor: LongWord): boolean; inline; var clr: TSDL_Color; begin @@ -94,7 +115,7 @@ clr: TSDL_Color; begin TTF_SizeUTF8(Fontz[Font].Handle, PChar(s), @w, @h); - if (maxLength > 0) and (w > maxLength * HDPIScaleFactor) then w := maxLength * HDPIScaleFactor; + if (maxLength > 0) and (w > round(maxLength * HDPIScaleFactor)) then w := round(maxLength * HDPIScaleFactor); finalRect.x:= X; finalRect.y:= Y; finalRect.w:= w + cFontBorder * 2 + cFontPadding * 2; @@ -115,7 +136,7 @@ finalRect.x:= X + cFontBorder + cFontPadding; finalRect.y:= Y + cFontBorder; if SDLCheck(tmpsurf <> nil, 'TTF_RenderUTF8_Blended', true) then - exit; + exit(finalRect); SDL_UpperBlit(tmpsurf, @textRect, Surface, @finalRect); SDL_FreeSurface(tmpsurf); finalRect.x:= X; @@ -188,9 +209,9 @@ for iY:= srcY to lY do begin // src pixel index - spi:= iY * src^.w + iX; + spi:= iY * src^.pitch div 4 + iX; // dest pixel index - dpi:= (iY + dY) * dest^.w + (iX + dX); + dpi:= (iY + dY) * dest^.pitch div 4 + (iX + dX); // get src alpha (and set it as target alpha for now) aT:= (srcPixels^[spi] and AMask) shr AShift; @@ -322,6 +343,29 @@ end; +{$IFNDEF PAS2C} +// Wraps the text s by inserting breakStr as newlines with +// maximum column length maxCol. +// Same as Pascal's WrapText, but without the annoying +// behavior that text enclosed in " and ' disables word-wrapping +function SimpleWrapText(s, breakStr: string; maxCol: integer): string; +var + breakChars: set of char = [#9,' ','-']; +begin + // escape the " and ' characters before calling WrapText + // using ASCII ESC control character + s:= StringReplace(s, '"', #27+'Q', [rfReplaceAll]); + s:= StringReplace(s, '''', #27+'q', [rfReplaceAll]); + + s:= WrapText(s, #1, breakChars, maxCol); + + // Undo the escapes + s:= StringReplace(s, #27+'Q', '"', [rfReplaceAll]); + s:= StringReplace(s, #27+'q', '''', [rfReplaceAll]); + SimpleWrapText:= s; +end; +{$ENDIF} + function RenderStringTex(s: ansistring; Color: Longword; font: THWFont): PTexture; begin RenderStringTex:= RenderStringTexLim(s, Color, font, 0); @@ -341,7 +385,7 @@ font:= CheckCJKFont(s, font); w:= 0; h:= 0; // avoid compiler hints TTF_SizeUTF8(Fontz[font].Handle, PChar(s), @w, @h); - if (maxLength > 0) and (w > maxLength * HDPIScaleFactor) then w := maxLength * HDPIScaleFactor; + if (maxLength > 0) and (w > round(maxLength * HDPIScaleFactor)) then w := round(maxLength * HDPIScaleFactor); finalSurface:= SDL_CreateRGBSurface(SDL_SWSURFACE, w + cFontBorder*2 + cFontPadding*2, h + cFontBorder * 2, 32, RMask, GMask, BMask, AMask); @@ -413,9 +457,6 @@ var textWidth, textHeight, x, y, w, h, i, j, pos, line, numLines, edgeWidth, edgeHeight, cornerWidth, cornerHeight: LongInt; finalSurface, tmpsurf, rotatedEdge: PSDL_Surface; rect: TSDL_Rect; - {$IFNDEF PAS2C} - breakChars: set of char = [#9,' ','-']; - {$ENDIF} substr: ansistring; edge, corner, tail: TSPrite; begin @@ -444,10 +485,6 @@ edgeWidth:= SpritesData[edge].Width; cornerWidth:= SpritesData[corner].Width; cornerHeight:= SpritesData[corner].Height; - // This one screws up WrapText - //s:= 'This is the song that never ends. ''cause it goes on and on my friends. Some people, started singing it not knowing what it was. And they''ll just go on singing it forever just because... This is the song that never ends...'; - // This one does not - //s:= 'This is the song that never ends. cause it goes on and on my friends. Some people, started singing it not knowing what it was. And they will go on singing it forever just because... This is the song that never ends... '; numLines:= 0; @@ -464,7 +501,7 @@ w:= 0; i:= round(Sqrt(length(s)) * 2); {$IFNDEF PAS2C} - s:= WrapText(s, #1, breakChars, i); + s:= SimpleWrapText(s, #1, i); {$ENDIF} pos:= 1; line:= 0; // Find the longest line for the purposes of centring the text. Font dependant. diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uScript.pas --- a/hedgewars/uScript.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uScript.pas Sun Mar 24 14:33:57 2024 -0400 @@ -308,6 +308,32 @@ LuaToSoundOrd:= i; end; +function LuaToMsgStrIdOrd(L : Plua_State; i: LongInt; call, paramsyntax: shortstring): LongInt; inline; +begin + if lua_isnoneornil(L, i) then i:= -1 + else i:= Trunc(lua_tonumber(L, i)); + if (i < ord(Low(TMsgStrId))) or (i > ord(High(TMsgStrId))) then + begin + LuaCallError('Invalid message ID!', call, paramsyntax); + LuaToMsgStrIdOrd:= -1; + end + else + LuaToMsgStrIdOrd:= i; +end; + +function LuaToGoalStrIdOrd(L : Plua_State; i: LongInt; call, paramsyntax: shortstring): LongInt; inline; +begin + if lua_isnoneornil(L, i) then i:= -1 + else i:= Trunc(lua_tonumber(L, i)); + if (i < ord(Low(TGoalStrId))) or (i > ord(High(TGoalStrId))) then + begin + LuaCallError('Invalid goal string ID!', call, paramsyntax); + LuaToGoalStrIdOrd:= -1; + end + else + LuaToGoalStrIdOrd:= i; +end; + function LuaToHogEffectOrd(L : Plua_State; i: LongInt; call, paramsyntax: shortstring): LongInt; inline; begin if lua_isnoneornil(L, i) then i:= -1 @@ -456,13 +482,13 @@ function lc_setweapon(L : Plua_State) : LongInt; Cdecl; var at: LongInt; const - call = 'SetWeapon'; - params = 'ammoType'; + callStr = 'SetWeapon'; + paramsStr = 'ammoType'; begin // no point to run this without any CurrentHedgehog - if (CurrentHedgehog <> nil) and (CheckLuaParamCount(L, 1, call, params)) then + if (CurrentHedgehog <> nil) and (CheckLuaParamCount(L, 1, callStr, paramsStr)) then begin - at:= LuaToAmmoTypeOrd(L, 1, call, params); + at:= LuaToAmmoTypeOrd(L, 1, callStr, paramsStr); if at >= 0 then ParseCommand('setweap ' + char(at), true, true); end; @@ -472,10 +498,10 @@ // enable/disable cinematic effects function lc_setcinematicmode(L : Plua_State) : LongInt; Cdecl; const - call = 'SetCinematicMode'; - params = 'enable'; -begin - if (CheckLuaParamCount(L, 1, call, params)) then + callStr = 'SetCinematicMode'; + paramsStr = 'enable'; +begin + if (CheckLuaParamCount(L, 1, callStr, paramsStr)) then begin CinematicScript:= lua_toboolean(L, 1); end; @@ -486,10 +512,10 @@ function lc_setmaxbuilddistance(L : Plua_State) : LongInt; Cdecl; var np: LongInt; const - call = 'SetMaxBuildDistance'; - params = '[ distInPx ]'; -begin - if CheckAndFetchParamCountRange(L, 0, 1, call, params, np) then + callStr = 'SetMaxBuildDistance'; + paramsStr = '[ distInPx ]'; +begin + if CheckAndFetchParamCountRange(L, 0, 1, callStr, paramsStr, np) then begin if np = 0 then begin @@ -508,10 +534,10 @@ nextAmmo : TAmmo; s, a, cs, fa: LongInt; const - call = 'SetNextWeapon'; - params = ''; -begin - if (CurrentHedgehog <> nil) and (CheckLuaParamCount(L, 0, call, params)) then + callStr = 'SetNextWeapon'; + paramsStr = ''; +begin + if (CurrentHedgehog <> nil) and (CheckLuaParamCount(L, 0, callStr, paramsStr)) then begin at:= -1; with CurrentHedgehog^ do @@ -584,35 +610,76 @@ lc_hidemission:= 0; end; +function lc_getenginestring(L : Plua_state) : LongInt; Cdecl; +var stringType: shortstring; + msgId: LongInt; +const callStr = 'GetEngineString'; + paramsStr = 'stringType, msgId'; +begin + if CheckLuaParamCount(L, 2, callStr, paramsStr) then + begin + stringType:= lua_tostring(L, 1); + if (not lua_isnumber(L, 2)) then + begin + LuaError('Argument ''msgId'' must be a number!'); + lua_pushnil(L); + end + else if stringType = 'TMsgStrId' then + begin + msgId:= LuaToMsgStrIdOrd(L, 2, callStr, paramsStr); + if msgId = -1 then + lua_pushnil(L) + else + lua_pushstring(L, PChar(trmsg[TMsgStrId(msgId)])) + end + else if stringType = 'TGoalStrId' then + begin + msgId:= LuaToGoalStrIdOrd(L, 2, callStr, paramsStr); + if msgId = -1 then + lua_pushnil(L) + else + lua_pushstring(L, PChar(trgoal[TGoalStrId(msgId)])); + end + else + begin + LuaError('Invalid stringType!'); + lua_pushnil(L); + end + end + else + lua_pushnil(L); + lc_getenginestring:= 1; +end; + function lc_setammotexts(L : Plua_State) : LongInt; Cdecl; const - call = 'SetAmmoTexts'; - params = 'ammoType, name, caption, description [, showExtra]'; + callStr = 'SetAmmoTexts'; + paramsStr = 'ammoType, name, caption, description [, showExtra]'; var n: integer; showExtra: boolean; begin - if CheckAndFetchParamCount(L, 4, 5, call, params, n) then + if CheckAndFetchParamCount(L, 4, 5, callStr, paramsStr, n) then begin if n = 5 then showExtra:= lua_toboolean(L, 5) else showExtra:= true; - SetAmmoTexts(TAmmoType(LuaToAmmoTypeOrd(L, 1, call, params)), lua_tostringA(L, 2), lua_tostringA(L, 3), lua_tostringA(L, 4), showExtra); + SetAmmoTexts(TAmmoType(LuaToAmmoTypeOrd(L, 1, callStr, paramsStr)), lua_tostringA(L, 2), lua_tostringA(L, 3), lua_tostringA(L, 4), showExtra); end; lc_setammotexts:= 0; end; function lc_setammodescriptionappendix(L : Plua_State) : LongInt; Cdecl; const - call = 'SetAmmoDescriptionAppendix'; - params = 'ammoType, descAppend'; + callStr = 'SetAmmoDescriptionAppendix'; + paramsStr = 'ammoType, descAppend'; var ammoType: TAmmoType; descAppend: ansistring; begin - if CheckLuaParamCount(L, 2, call, params) then + if CheckLuaParamCount(L, 2, callStr, paramsStr) then begin - ammoType := TAmmoType(LuaToAmmoTypeOrd(L, 1, call, params)); + ammoType := TAmmoType(LuaToAmmoTypeOrd(L, 1, callStr, paramsStr)); descAppend := lua_tostringA(L, 2); trluaammoa[Ammoz[ammoType].NameId] := descAppend; end; @@ -667,16 +734,16 @@ function lc_addcaption(L : Plua_State) : LongInt; Cdecl; var cg: LongInt; const - call = 'AddCaption'; - params = 'text [, color, captiongroup]'; -begin - if CheckAndFetchParamCount(L, 1, 3, call, params, cg) then + callStr = 'AddCaption'; + paramsStr = 'text [, color, captiongroup]'; +begin + if CheckAndFetchParamCount(L, 1, 3, callStr, paramsStr, cg) then begin if cg = 1 then AddCaption(lua_tostringA(L, 1), capcolDefault, capgrpMessage) else begin - cg:= LuaToCapGroupOrd(L, 3, call, params); + cg:= LuaToCapGroupOrd(L, 3, callStr, paramsStr); if cg >= 0 then AddCaption(lua_tostringA(L, 1), Trunc(lua_tonumber(L, 2)) shr 8, TCapGroup(cg)); end @@ -846,12 +913,12 @@ dx, dy: hwFloat; gt: TGearType; const - call = 'AddGear'; - params = 'x, y, gearType, state, dx, dy, timer'; -begin - if CheckLuaParamCount(L, 7, call, params) then + callStr = 'AddGear'; + paramsStr = 'x, y, gearType, state, dx, dy, timer'; +begin + if CheckLuaParamCount(L, 7, callStr, paramsStr) then begin - t:= LuaToGearTypeOrd(L, 3, call, params); + t:= LuaToGearTypeOrd(L, 3, callStr, paramsStr); if t >= 0 then begin gt:= TGearType(t); @@ -893,13 +960,13 @@ vgt: TVisualGearType; uid: Longword; const - call = 'AddVisualGear'; - params = 'x, y, visualGearType, state, critical [, layer]'; + callStr = 'AddVisualGear'; + paramsStr = 'x, y, visualGearType, state, critical [, layer]'; begin uid:= 0; - if CheckAndFetchParamCount(L, 5, 6, call, params, n) then + if CheckAndFetchParamCount(L, 5, 6, callStr, paramsStr, n) then begin - s:= LuaToVisualGearTypeOrd(L, 3, call, params); + s:= LuaToVisualGearTypeOrd(L, 3, callStr, paramsStr); if s >= 0 then begin vgt:= TVisualGearType(s); @@ -1403,7 +1470,7 @@ end; FreeAndNilTexture(clan^.HealthTex); - clan^.HealthTex:= makeHealthBarTexture(cTeamHealthWidth + 5, clan^.Teams[0]^.NameTagTex^.h, clan^.Color); + clan^.HealthTex:= makeHealthBarTexture(cTeamHealthWidth + 5, cTeamHealthHeight, clan^.Color); end; lc_setclancolor:= 0 @@ -1816,12 +1883,12 @@ var gear : PGear; at, n, c: LongInt; const - call = 'AddAmmo'; - params = 'gearUid, ammoType [, ammoCount]'; -begin - if CheckAndFetchParamCount(L, 2, 3, call, params, n) then + callStr = 'AddAmmo'; + paramsStr = 'gearUid, ammoType [, ammoCount]'; +begin + if CheckAndFetchParamCount(L, 2, 3, callStr, paramsStr, n) then begin - at:= LuaToAmmoTypeOrd(L, 2, call, params); + at:= LuaToAmmoTypeOrd(L, 2, callStr, paramsStr); if (at >= 0) and (TAmmoType(at) <> amNothing) then begin gear:= GearByUID(Trunc(lua_tonumber(L, 1))); @@ -1845,15 +1912,15 @@ ammo : PAmmo; at : LongInt; const - call = 'GetAmmoCount'; - params = 'gearUid, ammoType'; -begin - if CheckLuaParamCount(L, 2, call, params) then + callStr = 'GetAmmoCount'; + paramsStr = 'gearUid, ammoType'; +begin + if CheckLuaParamCount(L, 2, callStr, paramsStr) then begin gear:= GearByUID(Trunc(lua_tonumber(L, 1))); if (gear <> nil) and (gear^.Hedgehog <> nil) then begin - at:= LuaToAmmoTypeOrd(L, 2, call, params); + at:= LuaToAmmoTypeOrd(L, 2, callStr, paramsStr); if at >= 0 then begin ammo:= GetAmmoEntry(gear^.Hedgehog^, TAmmoType(at)); @@ -1955,12 +2022,12 @@ var gear: PGear; t : LongInt; const - call = 'SetEffect'; - params = 'gearUid, effect, effectState'; -begin - if CheckLuaParamCount(L, 3, call, params) then + callStr = 'SetEffect'; + paramsStr = 'gearUid, effect, effectState'; +begin + if CheckLuaParamCount(L, 3, callStr, paramsStr) then begin - t:= LuaToHogEffectOrd(L, 2, call, params); + t:= LuaToHogEffectOrd(L, 2, callStr, paramsStr); if t >= 0 then begin gear := GearByUID(Trunc(lua_tonumber(L, 1))); @@ -1975,12 +2042,12 @@ var gear : PGear; t : LongInt; const - call = 'GetEffect'; - params = 'gearUid, effect'; -begin - if CheckLuaParamCount(L, 2, call, params) then + callStr = 'GetEffect'; + paramsStr = 'gearUid, effect'; +begin + if CheckLuaParamCount(L, 2, callStr, paramsStr) then begin - t:= LuaToHogEffectOrd(L, 2, call, params); + t:= LuaToHogEffectOrd(L, 2, callStr, paramsStr); if t >= 0 then begin gear:= GearByUID(Trunc(lua_tonumber(L, 1))); @@ -2068,10 +2135,10 @@ function lc_endturn(L : Plua_State) : LongInt; Cdecl; var n: LongInt; const - call = 'EndTurn'; - params = '[noTaunts]'; -begin - if CheckAndFetchParamCount(L, 0, 1, call, params, n) then + callStr = 'EndTurn'; + paramsStr = '[noTaunts]'; +begin + if CheckAndFetchParamCount(L, 0, 1, callStr, paramsStr, n) then if n >= 1 then LuaNoEndTurnTaunts:= lua_toboolean(L, 1); LuaEndTurnRequested:= true; @@ -2082,10 +2149,10 @@ var n, time: LongInt; respectFactor: Boolean; const - call = 'Retreat'; - params = 'time [, respectGetAwayTimeFactor]'; -begin - if CheckAndFetchParamCount(L, 1, 2, call, params, n) then + callStr = 'Retreat'; + paramsStr = 'time [, respectGetAwayTimeFactor]'; +begin + if CheckAndFetchParamCount(L, 1, 2, callStr, paramsStr, n) then begin IsGetAwayTime:= true; AttackBar:= 0; @@ -2121,12 +2188,12 @@ color, tn: shortstring; needsTn : boolean; const - call = 'SendStat'; - params = 'statInfoType, color [, teamname]'; -begin - if CheckAndFetchParamCount(L, 2, 3, call, params, n) then + callStr = 'SendStat'; + paramsStr = 'statInfoType, color [, teamname]'; +begin + if CheckAndFetchParamCount(L, 2, 3, callStr, paramsStr, n) then begin - i:= LuaToStatInfoTypeOrd(L, 1, call, params); + i:= LuaToStatInfoTypeOrd(L, 1, callStr, paramsStr); if i >= 0 then begin statInfo:= TStatInfoType(i); @@ -2135,9 +2202,9 @@ if (n = 3) <> needsTn then begin if n = 3 then - LuaCallError(EnumToStr(statInfo) + ' does not support the teamname parameter', call, params) + LuaCallError(EnumToStr(statInfo) + ' does not support the teamname parameter', callStr, paramsStr) else - LuaCallError(EnumToStr(statInfo) + ' requires the teamname parameter', call, params); + LuaCallError(EnumToStr(statInfo) + ' requires the teamname parameter', callStr, paramsStr); end else // count is correct! begin @@ -2239,12 +2306,12 @@ n, s: LongInt; instaVoice: boolean; const - call = 'PlaySound'; - params = 'soundId [, hhGearUid [, instaVoice]]'; -begin - if CheckAndFetchParamCountRange(L, 1, 3, call, params, n) then + callStr = 'PlaySound'; + paramsStr = 'soundId [, hhGearUid [, instaVoice]]'; +begin + if CheckAndFetchParamCountRange(L, 1, 3, callStr, paramsStr, n) then begin - s:= LuaToSoundOrd(L, 1, call, params); + s:= LuaToSoundOrd(L, 1, callStr, paramsStr); if s >= 0 then begin // no gear specified @@ -2272,12 +2339,12 @@ function lc_playmusicsound(L : Plua_State) : LongInt; Cdecl; var s: LongInt; const - call = 'PlayMusicSound'; - params = 'soundId'; -begin - if CheckLuaParamCount(L, 1, call, params) then + callStr = 'PlayMusicSound'; + paramsStr = 'soundId'; +begin + if CheckLuaParamCount(L, 1, callStr, paramsStr) then begin - s:= LuaToSoundOrd(L, 1, call, params); + s:= LuaToSoundOrd(L, 1, callStr, paramsStr); if s >= 0 then PlayMusicSound(TSound(s)) end; @@ -2287,12 +2354,12 @@ function lc_stopmusicsound(L : Plua_State) : LongInt; Cdecl; var s: LongInt; const - call = 'StopMusicSound'; - params = 'soundId'; -begin - if CheckLuaParamCount(L, 1, call, params) then + callStr = 'StopMusicSound'; + paramsStr = 'soundId'; +begin + if CheckLuaParamCount(L, 1, callStr, paramsStr) then begin - s:= LuaToSoundOrd(L, 1, call, params); + s:= LuaToSoundOrd(L, 1, callStr, paramsStr); if s >= 0 then StopMusicSound(TSound(s)) end; @@ -2304,12 +2371,12 @@ var s: LongInt; soundState: boolean; const - call = 'SetSoundMask'; - params = 'soundId, isMasked'; -begin - if CheckLuaParamCount(L, 2, call, params) then + callStr = 'SetSoundMask'; + paramsStr = 'soundId, isMasked'; +begin + if CheckLuaParamCount(L, 2, callStr, paramsStr) then begin - s:= LuaToSoundOrd(L, 1, call, params); + s:= LuaToSoundOrd(L, 1, callStr, paramsStr); if s <> Ord(sndNone) then begin soundState:= lua_toboolean(L, 2); @@ -2858,12 +2925,12 @@ function lc_setammo(L : Plua_State) : LongInt; Cdecl; var np, at: LongInt; const - call = 'SetAmmo'; - params = 'ammoType, count, probability, delay [, numberInCrate]'; -begin - if CheckAndFetchParamCount(L, 4, 5, call, params, np) then + callStr = 'SetAmmo'; + paramsStr = 'ammoType, count, probability, delay [, numberInCrate]'; +begin + if CheckAndFetchParamCount(L, 4, 5, callStr, paramsStr, np) then begin - at:= LuaToAmmoTypeOrd(L, 1, call, params); + at:= LuaToAmmoTypeOrd(L, 1, callStr, paramsStr); if at >= 0 then begin if np = 4 then @@ -2879,13 +2946,13 @@ function lc_getammo(L : Plua_State) : LongInt; Cdecl; var i, at, rawProb, probLevel: LongInt; const - call = 'GetAmmo'; - params = 'ammoType'; + callStr = 'GetAmmo'; + paramsStr = 'ammoType'; begin lc_getammo:= 0; - if CheckLuaParamCount(L, 1, call, params) then + if CheckLuaParamCount(L, 1, callStr, paramsStr) then begin - at:= LuaToAmmoTypeOrd(L, 1, call, params); + at:= LuaToAmmoTypeOrd(L, 1, callStr, paramsStr); if at >= 0 then begin // Ammo count @@ -2913,12 +2980,12 @@ function lc_setammodelay(L : Plua_State) : LongInt; Cdecl; var at, delay: LongInt; const - call = 'SetAmmoDelay'; - params = 'ammoType, delay'; -begin - if CheckLuaParamCount(L, 2, call, params) then + callStr = 'SetAmmoDelay'; + paramsStr = 'ammoType, delay'; +begin + if CheckLuaParamCount(L, 2, callStr, paramsStr) then begin - at:= LuaToAmmoTypeOrd(L, 1, call, params); + at:= LuaToAmmoTypeOrd(L, 1, callStr, paramsStr); delay:= Trunc(lua_tonumber(L, 2)); if (at >= 0) and (TAmmoType(at) <> amNothing) then begin @@ -3065,11 +3132,11 @@ i, n : LongInt; placed, behind, flipHoriz, flipVert : boolean; const - call = 'PlaceSprite'; - params = 'x, y, sprite, frameIdx, tint, behind, flipHoriz, flipVert [, landFlag, ... ]'; + callStr = 'PlaceSprite'; + paramsStr = 'x, y, sprite, frameIdx, tint, behind, flipHoriz, flipVert [, landFlag, ... ]'; begin placed:= false; - if CheckAndFetchLuaParamMinCount(L, 4, call, params, n) then + if CheckAndFetchLuaParamMinCount(L, 4, callStr, paramsStr, n) then begin if not lua_isnoneornil(L, 5) then tint := Trunc(lua_tonumber(L, 5)) @@ -3089,12 +3156,12 @@ for i:= 9 to n do lf:= lf or Trunc(lua_tonumber(L, i)); - n:= LuaToSpriteOrd(L, 3, call, params); + n:= LuaToSpriteOrd(L, 3, callStr, paramsStr); if n >= 0 then begin spr:= TSprite(n); if SpritesData[spr].Surface = nil then - LuaError(call + ': ' + EnumToStr(spr) + ' cannot be placed! (required information not loaded)' ) + LuaError(callStr + ': ' + EnumToStr(spr) + ' cannot be placed! (required information not loaded)' ) else placed:= ForcePlaceOnLand( Trunc(lua_tonumber(L, 1)) - SpritesData[spr].Width div 2, @@ -3113,10 +3180,10 @@ i, n : LongInt; eraseOnLFMatch, onlyEraseLF, flipHoriz, flipVert : boolean; const - call = 'EraseSprite'; - params = 'x, y, sprite, frameIdx, eraseOnLFMatch, onlyEraseLF, flipHoriz, flipVert [, landFlag, ... ]'; -begin - if CheckAndFetchLuaParamMinCount(L, 4, call, params, n) then + callStr = 'EraseSprite'; + paramsStr = 'x, y, sprite, frameIdx, eraseOnLFMatch, onlyEraseLF, flipHoriz, flipVert [, landFlag, ... ]'; +begin + if CheckAndFetchLuaParamMinCount(L, 4, callStr, paramsStr, n) then begin if not lua_isnoneornil(L, 5) then eraseOnLFMatch := lua_toboolean(L, 5) @@ -3136,12 +3203,12 @@ for i:= 9 to n do lf:= lf or Trunc(lua_tonumber(L, i)); - n:= LuaToSpriteOrd(L, 3, call, params); + n:= LuaToSpriteOrd(L, 3, callStr, paramsStr); if n >= 0 then begin spr:= TSprite(n); if SpritesData[spr].Surface = nil then - LuaError(call + ': ' + EnumToStr(spr) + ' cannot be placed! (required information not loaded)' ) + LuaError(callStr + ': ' + EnumToStr(spr) + ' cannot be placed! (required information not loaded)' ) else EraseLand( Trunc(lua_tonumber(L, 1)) - SpritesData[spr].Width div 2, @@ -3388,12 +3455,12 @@ function lc_getammoname(L : Plua_state) : LongInt; Cdecl; var np, at: LongInt; ignoreOverwrite: Boolean; -const call = 'GetAmmoName'; - params = 'ammoType [, ignoreOverwrite ]'; -begin - if CheckAndFetchParamCountRange(L, 1, 2, call, params, np) then +const callStr = 'GetAmmoName'; + paramsStr = 'ammoType [, ignoreOverwrite ]'; +begin + if CheckAndFetchParamCountRange(L, 1, 2, callStr, paramsStr, np) then begin - at:= LuaToAmmoTypeOrd(L, 1, call, params); + at:= LuaToAmmoTypeOrd(L, 1, callStr, paramsStr); ignoreOverwrite := false; if np > 1 then ignoreOverwrite := lua_toboolean(L, 2); @@ -3412,15 +3479,15 @@ var at: LongInt; weapon: PAmmo; gear: PGear; -const call = 'GetAmmoTimer'; - params = 'gearUid, ammoType'; -begin - if CheckLuaParamCount(L, 2, call, params) then +const callStr = 'GetAmmoTimer'; + paramsStr = 'gearUid, ammoType'; +begin + if CheckLuaParamCount(L, 2, callStr, paramsStr) then begin gear:= GearByUID(Trunc(lua_tonumber(L, 1))); if (gear <> nil) and (gear^.Hedgehog <> nil) then begin - at:= LuaToAmmoTypeOrd(L, 2, call, params); + at:= LuaToAmmoTypeOrd(L, 2, callStr, paramsStr); weapon:= GetAmmoEntry(gear^.Hedgehog^, TAmmoType(at)); if (Ammoz[TAmmoType(at)].Ammo.Propz and ammoprop_Timerable) <> 0 then lua_pushnumber(L, weapon^.Timer) @@ -3600,10 +3667,10 @@ function lc_endluatest(L : Plua_State) : LongInt; Cdecl; var rstring: shortstring; const - call = 'EndLuaTest'; - params = 'TEST_SUCCESSFUL or TEST_FAILED'; -begin - if CheckLuaParamCount(L, 1, call, params) then + callStr = 'EndLuaTest'; + paramsStr = 'TEST_SUCCESSFUL or TEST_FAILED'; +begin + if CheckLuaParamCount(L, 1, callStr, paramsStr) then begin case Trunc(lua_tonumber(L, 1)) of @@ -3611,7 +3678,7 @@ HaltTestFailed: rstring:= 'FAILED'; else begin - LuaCallError('Parameter must be either ' + params, call, params); + LuaCallError('Parameter must be either ' + paramsStr, callStr, paramsStr); exit(0); end; end; @@ -4257,6 +4324,8 @@ spr: TSprite; mg : TMapGen; we : TWorldEdge; + msi: TMsgStrId; + gsi: TGoalStrId; begin // initialize lua luaState:= lua_open; @@ -4375,6 +4444,13 @@ for we:= Low(TWorldEdge) to High(TWorldEdge) do ScriptSetInteger(EnumToStr(we), ord(we)); +// register message IDs +for msi:= Low(TMsgStrId) to High(TMsgStrId) do + ScriptSetInteger(EnumToStr(msi), ord(msi)); + +for gsi:= Low(TGoalStrId) to High(TGoalStrId) do + ScriptSetInteger(EnumToStr(gsi), ord(gsi)); + ScriptSetLongWord('capcolDefault' , capcolDefaultLua); ScriptSetLongWord('capcolSetting' , capcolSettingLua); @@ -4485,6 +4561,7 @@ lua_register(luaState, _P'ParseCommand', @lc_parsecommand); lua_register(luaState, _P'ShowMission', @lc_showmission); lua_register(luaState, _P'HideMission', @lc_hidemission); +lua_register(luaState, _P'GetEngineString', @lc_getenginestring); lua_register(luaState, _P'SetAmmoTexts', @lc_setammotexts); lua_register(luaState, _P'SetAmmoDescriptionAppendix', @lc_setammodescriptionappendix); lua_register(luaState, _P'AddCaption', @lc_addcaption); diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uSound.pas --- a/hedgewars/uSound.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uSound.pas Sun Mar 24 14:33:57 2024 -0400 @@ -332,7 +332,9 @@ (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 + (FileName: 'dynamiteimpact.ogg'; Path: ptSounds; AltPath: ptNone) // sndDynamiteImpact ); diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uStats.pas --- a/hedgewars/uStats.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uStats.pas Sun Mar 24 14:33:57 2024 -0400 @@ -105,15 +105,20 @@ end; procedure HedgehogDamaged(Gear: PGear; Attacker: PHedgehog; Damage: Longword; killed: boolean); +var sameClan: Boolean; begin -if Attacker^.Team^.Clan = Gear^.Hedgehog^.Team^.Clan then +sameClan := false; +if Attacker <> nil then + sameClan := Attacker^.Team^.Clan = Gear^.Hedgehog^.Team^.Clan; + +if sameClan then vpHurtSameClan:= Gear^.Hedgehog^.Team^.voicepack else begin if not FirstBlood then StepFirstBlood:= true; vpHurtEnemy:= Gear^.Hedgehog^.Team^.voicepack; - if (not killed) and (not bDuringWaterRise) then + if (Attacker <> nil) and (not killed) and (not bDuringWaterRise) then begin // Check if victim got attacked by RevengeHog again if (Gear^.Hedgehog^.RevengeHog <> nil) and (Gear^.Hedgehog^.RevengeHog = Attacker) and (Gear^.Hedgehog^.stats.StepRevenge = false) then @@ -141,7 +146,8 @@ if (not bDuringWaterRise) then begin - inc(Attacker^.stats.StepDamageGiven, Damage); + if Attacker <> nil then + inc(Attacker^.stats.StepDamageGiven, Damage); inc(Gear^.Hedgehog^.stats.StepDamageRecv, Damage); end; @@ -157,7 +163,7 @@ if bDuringWaterRise then inc(KillsSD) - else + else if Attacker <> nil then begin inc(Attacker^.stats.StepKills); inc(Attacker^.Team^.stats.Kills); diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uStore.pas --- a/hedgewars/uStore.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uStore.pas Sun Mar 24 14:33:57 2024 -0400 @@ -33,6 +33,7 @@ function makeHealthBarTexture(w, h, Color: Longword): PTexture; procedure AddProgress; procedure FinishProgress; +procedure LoadFont(font: THWFont); function LoadImage(const filename: shortstring; imageFlags: LongInt): PSDL_Surface; // loads an image from the games data files @@ -115,7 +116,7 @@ clr.b:= Color and $FF; clr.a:= $FF; tmpsurf:= TTF_RenderUTF8_Blended(Fontz[Font].Handle, s, clr); -if tmpsurf = nil then exit; +if tmpsurf = nil then exit(finalRect); tmpsurf:= doSurfaceConversion(tmpsurf); if tmpsurf <> nil then @@ -327,9 +328,9 @@ for t:= 0 to Pred(ClansCount) do with ClansArray[t]^ do - HealthTex:= makeHealthBarTexture(cTeamHealthWidth + 5, 19 * HDPIScaleFactor, Color); + HealthTex:= makeHealthBarTexture(cTeamHealthWidth + 5, cTeamHealthHeight, Color); -GenericHealthTexture:= makeHealthBarTexture(cTeamHealthWidth + 5, 19 * HDPIScaleFactor, cWhiteColor) +GenericHealthTexture:= makeHealthBarTexture(cTeamHealthWidth + 5, cTeamHealthHeight, cWhiteColor) end; @@ -362,23 +363,35 @@ end end; -procedure LoadFonts(); +procedure LoadFont(font: THWFont); var s: shortstring; - fi: THWFont; +begin + with Fontz[font] do + begin + if Handle <> nil then + begin + TTF_CloseFont(Handle); + Handle:= nil; + end; + s:= cPathz[ptFonts] + '/' + Name; + WriteToConsole(msgLoading + s + ' (' + inttostr(Height) + 'pt)... '); + Handle:= TTF_OpenFontRW(rwopsOpenRead(s), true, Height); + if SDLCheck(Handle <> nil, 'TTF_OpenFontRW', true) then exit; + TTF_SetFontStyle(Handle, style); + WriteLnToConsole(msgOK) + end; +end; + +procedure LoadFonts(); +var fi: THWFont; begin AddFileLog('LoadFonts();'); if (not cOnlyStats) then for fi:= Low(THWFont) to High(THWFont) do - with Fontz[fi] do - begin - s:= cPathz[ptFonts] + '/' + Name; - WriteToConsole(msgLoading + s + ' (' + inttostr(Height) + 'pt)... '); - Handle:= TTF_OpenFontRW(rwopsOpenRead(s), true, Height); - if SDLCheck(Handle <> nil, 'TTF_OpenFontRW', true) then exit; - TTF_SetFontStyle(Handle, style); - WriteLnToConsole(msgOK) - end; + begin + LoadFont(fi); + end; end; procedure StoreLoad(reload: boolean); @@ -461,12 +474,30 @@ end; if (ii in [sprAMAmmos, sprAMAmmosBW]) then begin + // Optionally add ammos overlay from HWP file tmpoverlay := LoadDataImage(Path, copy(FileName, 1, length(FileName)-5), (imflags and (not ifCritical))); if tmpoverlay <> nil then begin copyToXY(tmpoverlay, tmpsurf, 0, 0); SDL_FreeSurface(tmpoverlay) - end + end; + + // Replace ExtraDamage icon with a variant showing "1,5" instead of "1.5" + // if the current locale uses a comma as a decimal separator. + if lDecimalSeparator = ',' then + begin + if ii = sprAMAmmos then + tmpoverlay:= LoadDataImage(ptAmmoMenu, 'Ammos_ExtraDamage_comma', ifNone) + else + tmpoverlay:= LoadDataImage(ptAmmoMenu, 'Ammos_bw_ExtraDamage_comma', ifNone); + if tmpoverlay <> nil then + begin + copyToXY(tmpoverlay, tmpsurf, + GetSurfaceFrameCoordinateX(tmpsurf, ord(amExtraDamage)-1, SpritesData[ii].Width, SpritesData[ii].Height), + GetSurfaceFrameCoordinateY(tmpsurf, ord(amExtraDamage)-1, SpritesData[ii].Height)); + SDL_FreeSurface(tmpoverlay); + end; + end; end; if (ii in [sprSky, sprSkyL, sprSkyR, sprHorizont, sprHorizontL, sprHorizontR]) then begin diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uTypes.pas --- a/hedgewars/uTypes.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uTypes.pas Sun Mar 24 14:33:57 2024 -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, sprHogBubble, sprHappy, sprSentry, sprHandSentry); // Gears that interact with other Gears and/or Land // first row of gears ( 0) then + a[i]:= c2; +until (i <= 0); +end; { ReplaceChars } + +// Replace all characters c1 with c2 in antistring a +procedure ReplaceCharsA(var a: ansistring; c1, c2: char); +var i: LongInt; +begin +repeat + i:= Pos(c1, a); + if (i > 0) then + a[i]:= c2; +until (i <= 0); +end; { ReplaceCharsA } + function EnumToStr(const en : TGearType) : shortstring; overload; begin EnumToStr:= GetEnumName(TypeInfo(TGearType), ord(en)) @@ -341,6 +381,15 @@ EnumToStr := GetEnumName(TypeInfo(TWorldEdge), ord(en)) end; +function EnumToStr(const en: TMsgStrId) : shortstring; overload; +begin +EnumToStr := GetEnumName(TypeInfo(TMsgStrId), ord(en)) +end; + +function EnumToStr(const en: TGoalStrId) : shortstring; overload; +begin +EnumToStr := GetEnumName(TypeInfo(TGoalStrId), ord(en)) +end; function Min(a, b: LongInt): LongInt; begin diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uVariables.pas --- a/hedgewars/uVariables.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uVariables.pas Sun Mar 24 14:33:57 2024 -0400 @@ -86,6 +86,7 @@ SpeedStart : LongWord; fastUntilLag : boolean; + fastForward : boolean; fastScrolling : boolean; autoCameraOn : boolean; @@ -93,6 +94,7 @@ CampaignVariable: shortstring; MissionVariable : shortstring; GameTicks : LongWord; + FFGameTick : LongWord; OuchTauntTimer : LongWord; // Timer which blocks sndOuch from being played too often and fast GameState : TGameState; GameType : TGameType; @@ -105,6 +107,7 @@ TurnClockActive : boolean; TagTurnTimeLeft : Longword; ReadyTimeLeft : Longword; + TimeNotInTurn : Longword; // Milliseconds that passed while no turn is active IsGetAwayTime : boolean; GameOver : boolean; cSuddenDTurns : LongInt; @@ -140,6 +143,9 @@ zoom : GLfloat; // current zoom ZoomValue : GLfloat; // aimed zoom UserZoom : GLfloat; // user-chosen initial and default zoom + ChatScaleValue : real; + cDefaultChatScale: real; + UIScaleValue : real; cWaterLine : LongInt; cGearScrEdgesDist: LongInt; @@ -176,6 +182,7 @@ cLandMines : Longword; cAirMines : Longword; + cSentries : Longword; cExplosives : Longword; cScriptName : shortstring; @@ -262,6 +269,8 @@ // for tracking the limits of the visible grid based on cScaleFactor ViewLeftX, ViewRightX, ViewBottomY, ViewTopY, ViewWidth, ViewHeight: LongInt; + // for tracking the limits of the visible UI space based on cUIScaleFactor + UIWidth, UIHeight: LongInt; // for debugging the view limits visually cViewLimitsDebug: boolean; @@ -348,28 +357,36 @@ const FontzInit: array[THWFont] of THHFont = ( (Handle: nil; - Height: 12*HDPIScaleFactor; + Height: round(12*HDPIScaleFactor); style: TTF_STYLE_NORMAL; Name: 'DejaVuSans-Bold.ttf'), (Handle: nil; - Height: 24*HDPIScaleFactor; + Height: round(24*HDPIScaleFactor); style: TTF_STYLE_NORMAL; Name: 'DejaVuSans-Bold.ttf'), (Handle: nil; - Height: 10*HDPIScaleFactor; + Height: round(10*HDPIScaleFactor); + style: TTF_STYLE_NORMAL; + Name: 'DejaVuSans-Bold.ttf'), + (Handle: nil; // fntChat + Height: round(12*HDPIScaleFactor); style: TTF_STYLE_NORMAL; Name: 'DejaVuSans-Bold.ttf') {$IFNDEF MOBILE}, // remove chinese fonts for now (Handle: nil; - Height: 12*HDPIScaleFactor; + Height: round(12*HDPIScaleFactor); style: TTF_STYLE_NORMAL; Name: 'wqy-zenhei.ttc'), (Handle: nil; - Height: 24*HDPIScaleFactor; + Height: round(24*HDPIScaleFactor); style: TTF_STYLE_NORMAL; Name: 'wqy-zenhei.ttc'), (Handle: nil; - Height: 10*HDPIScaleFactor; + Height: round(10*HDPIScaleFactor); + style: TTF_STYLE_NORMAL; + Name: 'wqy-zenhei.ttc'), + (Handle: nil; // CJKfntChat + Height: round(12*HDPIScaleFactor); style: TTF_STYLE_NORMAL; Name: 'wqy-zenhei.ttc') {$ENDIF} @@ -852,7 +869,17 @@ (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 + (FileName: 'Bubble'; Path: ptHedgehog; AltPath: ptNone; Texture: nil; Surface: nil; + Width: 32; Height: 38; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpLowest; getDimensions: false; getImageDimensions: true),// sprHogBubble + (FileName: 'Happy'; Path: ptHedgehog; AltPath: ptNone; Texture: nil; Surface: nil; + Width: 32; Height: 32; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpLowest; getDimensions: false; getImageDimensions: true),// sprHappy + (FileName: 'Duck'; 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),// sprSentry + (FileName: 'amDuck'; Path: ptHedgehog; AltPath: ptNone; Texture: nil; Surface: nil; + Width: 64; Height: 64; imageWidth: 0; imageHeight: 0; saveSurf: false; critical: true; checkSum: false; priority: tpMedium; getDimensions: false; getImageDimensions: true) // sprHandSentry ); @@ -871,7 +898,9 @@ (Sprite: sprHurrah; FramesCount: 14; Interval: 125; cmd: '/hurrah'; Voice: sndNone; VoiceDelay: 0), (Sprite: sprLemonade; FramesCount: 24; Interval: 125; cmd: '/ilovelotsoflemonade'; Voice: sndNone; VoiceDelay: 0), (Sprite: sprShrug; FramesCount: 24; Interval: 125; cmd: '/shrug'; Voice: sndNone; VoiceDelay: 0), - (Sprite: sprJuggle; FramesCount: 49; Interval: 38; cmd: '/juggle'; Voice: sndNone; VoiceDelay: 0) + (Sprite: sprJuggle; FramesCount: 49; Interval: 38; cmd: '/juggle'; Voice: sndNone; VoiceDelay: 0), + (Sprite:sprHogBubble; FramesCount: 19; Interval: 125; cmd: '/bubble'; Voice: sndNone; VoiceDelay: 0), + (Sprite: sprHappy; FramesCount: 14; Interval: 125; cmd: '/happy'; Voice: sndNone; VoiceDelay: 0) ); type @@ -2531,7 +2560,32 @@ PosCount: 0; PosSprite: sprWater; ejectX: 0; //23; - ejectY: 0) //-6; + ejectY: 0), //-6; +// Sentry + (NameId: sidSentry; + NameTex: nil; + Probability: 100; + NumberInCase: 1; + Ammo: (Propz: ammoprop_NoCrosshair or + ammoprop_AttackInMove or + ammoprop_DontHold; + Count: 1; + NumPerTurn: 0; + Timer: 0; + Pos: 0; + AmmoType: amSentry; + AttackVoice: sndLaugh; + Bounciness: defaultBounciness); + Slot: 9; + TimeAfterTurn: 3000; + minAngle: 0; + maxAngle: 0; + isDamaging: true; + SkipTurns: 0; + PosCount: 0; + PosSprite: sprWater; + ejectX: 10; + ejectY: -5) ); var @@ -2592,6 +2646,7 @@ SyncTexture, ConfirmTexture: PTexture; cScaleFactor: GLfloat; + cUIScaleFactor: float; cStereoDepth: GLfloat; SupportNPOTT: Boolean; Step: LongInt; @@ -2717,6 +2772,8 @@ cAudioCodec := ''; {$ENDIF} + cDefaultChatScale:= 1.0; + cTagsMask:= htTeamName or htName or htHealth; cPrevTagsMask:= cTagsMask; end; @@ -2875,6 +2932,7 @@ CursorMovementX := 0; CursorMovementY := 0; GameTicks := 0; + FFGameTick := 0; OuchTauntTimer := 0; CheckSum := 0; cWaterLine := LAND_HEIGHT; @@ -2890,6 +2948,7 @@ GameOver := false; TurnClockActive := true; TagTurnTimeLeft := 0; + TimeNotInTurn := 0; cSuddenDTurns := 15; LastSuddenDWarn := -2; cInitHealth := 100; @@ -2920,9 +2979,18 @@ cMaxCaseDrops := 5; cLandMines := 4; cAirMines := 0; + cSentries := 0; cExplosives := 2; GameState := Low(TGameState); + + if cDefaultChatScale < cMinChatScaleValue then + cDefaultChatScale := cMinChatScaleValue + else if cDefaultChatScale > cMaxChatScaleValue then + cDefaultChatScale := cMaxChatScaleValue; + ChatScaleValue := cDefaultChatScale; + UIScaleValue := cDefaultUIScaleLevel; + WeaponTooltipTex:= nil; cLaserSighting := false; cLaserSightingSniper := false; @@ -2943,6 +3011,7 @@ isForceMission := false; SpeedStart := 0; fastUntilLag := false; + fastForward := false; fastScrolling := false; autoCameraOn := true; cSeed := ''; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uVideoRec.pas --- a/hedgewars/uVideoRec.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uVideoRec.pas Sun Mar 24 14:33:57 2024 -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; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uVisualGears.pas --- a/hedgewars/uVisualGears.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uVisualGears.pas Sun Mar 24 14:33:57 2024 -0400 @@ -55,7 +55,7 @@ begin if cAltDamage then begin - Gear:= AddVisualGear(X, Y, vgtSmallDamageTag); + Gear:= AddVisualGear(X, Y, vgtSmallDamageTag, Damage); if Gear <> nil then with Gear^ do Tex:= RenderStringTex(ansistring(inttostr(Damage)), Color, fntSmall); @@ -265,7 +265,17 @@ end else if (Gear^.Tex <> nil) and (((Gear^.State = 0) and ((Gear^.Hedgehog = nil) or (Gear^.Hedgehog^.Team = CurrentTeam))) or (Gear^.State = 2)) then DrawTextureCentered(round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy, Gear^.Tex); - vgtSmallDamageTag: DrawTextureCentered(round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy, Gear^.Tex); + vgtSmallDamageTag: if Gear^.Tex <> nil then + begin + if Gear^.Frame = 0 then + DrawTextureCentered(round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy, Gear^.Tex) + else + begin + SetScale(cDefaultZoomLevel); + DrawTexture(round(Gear^.X), round(Gear^.Y), Gear^.Tex); + SetScale(zoom); + end + end; vgtHealthTag: if Gear^.Tex <> nil then begin if Gear^.Frame = 0 then @@ -463,7 +473,7 @@ var i: LongInt; begin for i:= 0 to cCloudsNumber - 1 do - AddVisualGear(cLeftScreenBorder + i * LongInt(cScreenSpace div (cCloudsNumber + 1)), LAND_HEIGHT-1184, vgtCloud, 0, true) + AddVisualGear(cLeftScreenBorder + i * LongInt(cScreenSpace div (cCloudsNumber + 1)), LAND_HEIGHT-cCloudOffset, vgtCloud, 0, true) end; procedure ChangeToSDClouds; @@ -484,10 +494,15 @@ end else vg:= vg^.NextGear; for j:= 0 to cSDCloudsNumber - 1 do - AddVisualGear(cLeftScreenBorder + j * LongInt(cScreenSpace div (cSDCloudsNumber + 1)), LAND_HEIGHT-1184, vgtCloud, 0, true) + AddVisualGear(cLeftScreenBorder + j * LongInt(cScreenSpace div (cSDCloudsNumber + 1)), LAND_HEIGHT-cCloudOffset, vgtCloud, 0, true) end; end; +procedure AddFlake; inline; +begin + AddVisualGear(cLeftScreenBorder + random(cScreenSpace), LAND_HEIGHT-cCloudOffset+ random(cCloudOffset), vgtFlake); +end; + procedure AddFlakes; var i: LongInt; begin @@ -496,10 +511,10 @@ if hasBorder or (not cSnow) then for i:= 0 to Pred(vobCount * cScreenSpace div 4096) do - AddVisualGear(cLeftScreenBorder + random(cScreenSpace), random(1024+200) - 100 + LAND_HEIGHT, vgtFlake) + AddFlake else for i:= 0 to Pred((vobCount * cScreenSpace div 4096) div 3) do - AddVisualGear(cLeftScreenBorder + random(cScreenSpace), random(1024+200) - 100 + LAND_HEIGHT, vgtFlake); + AddFlake; end; procedure ChangeToSDFlakes; @@ -526,10 +541,10 @@ end; if hasBorder or (not cSnow) then for i:= 0 to Pred(vobSDCount * cScreenSpace div 4096) do - AddVisualGear(cLeftScreenBorder + random(cScreenSpace), random(1024+200) - 100 + LAND_HEIGHT, vgtFlake) + AddFlake else for i:= 0 to Pred((vobSDCount * cScreenSpace div 4096) div 3) do - AddVisualGear(cLeftScreenBorder + random(cScreenSpace), random(1024+200) - 100 + LAND_HEIGHT, vgtFlake); + AddFlake; end; procedure initModule; diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uVisualGearsHandlers.pas --- a/hedgewars/uVisualGearsHandlers.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uVisualGearsHandlers.pas Sun Mar 24 14:33:57 2024 -0400 @@ -43,7 +43,7 @@ procedure doStepEgg(Gear: PVisualGear; Steps: Longword); procedure doStepFire(Gear: PVisualGear; Steps: Longword); procedure doStepShell(Gear: PVisualGear; Steps: Longword); -procedure doStepSmallDamage(Gear: PVisualGear; Steps: Longword); +procedure doStepSmallDamageTag(Gear: PVisualGear; Steps: Longword); procedure doStepBubble(Gear: PVisualGear; Steps: Longword); procedure doStepSteam(Gear: PVisualGear; Steps: Longword); procedure doStepAmmo(Gear: PVisualGear; Steps: Longword); @@ -79,11 +79,12 @@ procedure doStepFlake(Gear: PVisualGear; Steps: Longword); var sign: real; - moved, rising, outside: boolean; - vfc, vft: LongWord; + moved, rising, outside, fallingFadeIn: boolean; + vfc, vft, diff: LongWord; spawnMargin: LongInt; const randMargin = 50; + maxFallSpeedForFadeIn = 750; begin if SuddenDeathDmg then begin @@ -103,12 +104,14 @@ Y:= Y + (dY + tdY + cGravityf * vobSDFallSpeed) * Steps * Gear^.Scale; vfc:= vobSDFramesCount; vft:= vobSDFrameTicks; + fallingFadeIn := vobSDFallSpeed <= maxFallSpeedForFadeIn; end else begin Y:= Y + (dY + tdY + cGravityf * vobFallSpeed) * Steps * Gear^.Scale; vfc:= vobFramesCount; vft:= vobFrameTicks; + fallingFadeIn := vobFallSpeed <= maxFallSpeedForFadeIn; end; if vft > 0 then @@ -180,17 +183,60 @@ // flake fell far below map? outside:= (not rising) and (round(Y) - spawnMargin + randMargin > LAND_HEIGHT); // if not, did it rise far above map? - outside:= outside or (rising and (round(Y) < LAND_HEIGHT - 1024 - spawnMargin - randMargin)); + outside:= outside or (rising and (round(Y) < LAND_HEIGHT - (cCloudOffset - 110))); // if flake left acceptable vertical area, respawn it opposite side if outside then begin - X:= cLeftScreenBorder + random(cScreenSpace); if rising then - Y:= Y + (1024 + spawnMargin + random(50)) + // rising flake + begin + if State = 0 then + begin + // fade out rising flake + diff:= (LAND_HEIGHT - (cCloudOffset - 110)) - round(Y); + diff:= Min(diff*2, $FF); + if diff >= $FF then + begin + // end of fade-out + diff:= $FF; + State:= 1; + end; + Tint:= (Tint and $FFFFFF00) or ($FF - diff); + end + else + begin + // reset and move back to bottom + Y:= LAND_HEIGHT + spawnMargin + random(50); + moved:= true; + State:= 0; + Tint:= Tint or $FF; + end; + end else + // falling flake + begin + // move back to top Y:= Y - (1024 + spawnMargin + random(50)); - moved:= true; + moved:= true; + // activate fade-in if not falling too fast + if fallingFadeIn then + begin + State:= $FF; + Tint:= (Tint and $FFFFFF00) or ($FF - State); + end; + end; + if moved then + X:= cLeftScreenBorder + random(cScreenSpace); + end + else if (not rising) and (State > 0) then + begin + // quickly fade in falling flake after appearing at top + if State > 16 then + Dec(State, 16) + else + State:= 0; + Tint:= (Tint and $FFFFFF00) or ($FF - State); end; if moved then @@ -229,7 +275,7 @@ t := 8 * Gear^.Scale * hwFloat2Float(AngleSin(s mod 2048)); if (s < 2048) then t := -t; -Gear^.Y := LAND_HEIGHT - 1184 + LongInt(Gear^.Timer mod 8) + t; +Gear^.Y := LAND_HEIGHT - cCloudOffset + LongInt(Gear^.Timer mod 8) + t; if round(Gear^.X) < cLeftScreenBorder then Gear^.X:= Gear^.X + cScreenSpace @@ -356,9 +402,17 @@ dec(Gear^.FrameTicks, Steps) end; -procedure doStepSmallDamage(Gear: PVisualGear; Steps: Longword); +procedure doStepSmallDamageTag(Gear: PVisualGear; Steps: Longword); +var s: shortstring; begin -Gear^.Y:= Gear^.Y - 0.02 * Steps; +if Gear^.Tex = nil then + begin + s:= IntToStr(Gear^.State); + Gear^.Tex:= RenderStringTex(ansistring(s), cWhiteColor, fntSmall); + end; + +Gear^.X:= Gear^.X + Gear^.dX * Steps; +Gear^.Y:= Gear^.Y + Gear^.dY * Steps; if Gear^.FrameTicks <= Steps then DeleteVisualGear(Gear) @@ -990,7 +1044,7 @@ @doStepExpl, @doStepExpl, @doStepFire, - @doStepSmallDamage, + @doStepSmallDamageTag, @doStepTeamHealthSorter, @doStepSpeechBubble, @doStepBubble, diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uVisualGearsList.pas --- a/hedgewars/uVisualGearsList.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uVisualGearsList.pas Sun Mar 24 14:33:57 2024 -0400 @@ -64,6 +64,8 @@ sp: real; begin AddVisualGear:= nil; +if fastUntilLag and (not Critical) then + exit; if (GameType <> gmtRecord) and (((GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet)) or fastScrolling) and // we are scrolling now (not Critical)) then @@ -100,6 +102,7 @@ case Kind of vgtFlake: begin + State:= 0; Timer:= 0; tdX:= 0; tdY:= 0; @@ -176,7 +179,10 @@ vgtShell: FrameTicks:= 500; vgtSmallDamageTag: begin - gear^.FrameTicks:= 1100 + gear^.Frame:= 0; + gear^.FrameTicks:= 1100; + gear^.dX:= 0; + gear^.dY:= -0.02; end; vgtBubble: begin diff -r 64740eec84ad -r 4c523ed1d35c hedgewars/uWorld.pas --- a/hedgewars/uWorld.pas Sun Mar 24 14:05:06 2024 -0400 +++ b/hedgewars/uWorld.pas Sun Mar 24 14:33:57 2024 -0400 @@ -877,6 +877,43 @@ end end; +// Force camera to stay within a certain area +procedure CameraBounds; +var lowBound: LongInt; +begin +if (not hasBorder) then + begin + if WorldDy > (-(cScreenHeight / cScaleFactor) + cScreenHeight div 2 - TopY + cCamLimitY) then + WorldDy:= (-trunc(cScreenHeight / cScaleFactor) + cScreenHeight div 2 - TopY + cCamLimitY); + if (RightX - LeftX + cCamLimitX * 2) div 2 < cScreenWidth / cScaleFactor then + WorldDx:= -((LeftX + RightX) div 2) + else + begin + if WorldDx < -LAND_WIDTH - cCamLimitX + (cScreenWidth / cScaleFactor) then + WorldDx:= -LAND_WIDTH - cCamLimitX + trunc(cScreenWidth / cScaleFactor); + if WorldDx > cCamLimitX - (cScreenWidth / cScaleFactor) then + WorldDx:= cCamLimitX - trunc(cScreenWidth / cScaleFactor); + end; + end +else + begin + if WorldDy > (-(cScreenHeight / cScaleFactor) + cScreenHeight div 2 - TopY + cCamLimitBorderY) then + WorldDy:= (-trunc(cScreenHeight / cScaleFactor) + cScreenHeight div 2 - TopY + cCamLimitBorderY); + if (RightX - LeftX + cCamLimitBorderX * 2) div 2 < cScreenWidth / cScaleFactor then + WorldDx:= -((LeftX + RightX) div 2) + else + begin + if WorldDx > -LeftX + cCamLimitBorderX - (cScreenWidth / cScaleFactor) then + WorldDx:= -LeftX + cCamLimitBorderX - trunc(cScreenWidth / cScaleFactor); + if WorldDx < -RightX - cCamLimitBorderX + (cScreenWidth / cScaleFactor) then + WorldDx:= -RightX - cCamLimitBorderX + trunc(cScreenWidth / cScaleFactor); + end; + end; + +lowBound:= trunc(cScreenHeight / cScaleFactor) + cScreenHeight div 2 - cWaterLine - (cVisibleWater + trunc(CinematicBarH / (cScaleFactor / 2.0))); +if WorldDy < lowBound then + WorldDy:= lowBound; +end; procedure DrawWorld(Lag: LongInt); begin @@ -897,7 +934,9 @@ ZoomValue:= zoom; if (not isPaused) and (not isAFK) and (GameType <> gmtRecord) then - MoveCamera; + MoveCamera + else if (isPaused) then + CameraBounds; if cStereoMode = smNone then begin @@ -1280,7 +1319,7 @@ // line at airplane height for certain airstrike types (when spawning height is important) with CurrentHedgehog^ do if (isCursorVisible) and ((CurAmmoType = amNapalm) or (CurAmmoType = amMineStrike) or (((GameFlags and gfMoreWind) <> 0) and ((CurAmmoType = amDrillStrike) or (CurAmmoType = amAirAttack)))) then - DrawLine(-3000, topY-300, 7000, topY-300, 3.0, (Team^.Clan^.Color shr 16), (Team^.Clan^.Color shr 8) and $FF, Team^.Clan^.Color and $FF, $FF); + DrawLine(-cCamLimitX, topY-300, LAND_WIDTH + cCamLimitX, topY-300, 3.0, (Team^.Clan^.Color shr 16), (Team^.Clan^.Color shr 8) and $FF, Team^.Clan^.Color and $FF, $FF); // gear HUD extras (fuel indicator, secondary ammo, etc.) if replicateToLeft then @@ -1504,13 +1543,14 @@ i:= t + pauseButton.frame.y + pauseButton.frame.h; {$ENDIF} + inc(t, CurrentHedgehog^.HealthTagTex^.h); + cDemoClockFPSOffsetY:= t; + // Hide health and healh icons in gfInvulnerable mode (except heResurrectable) if ((GameFlags and gfInvulnerable) = 0) then begin // Health tag DrawTexture(cScreenWidth div 2 - CurrentHedgehog^.HealthTagTex^.w - 16, i, CurrentHedgehog^.HealthTagTex); - inc(t, CurrentHedgehog^.HealthTagTex^.h); - cDemoClockFPSOffsetY:= t; t:= SpritesData[sprHealthHud].Width + 18; // Main health icon. Appearance depends on game mode and poisoning state @@ -1550,9 +1590,21 @@ 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 + // Invulnerable + inc(t, 8); + DrawSprite(sprInvulnHud, cScreenWidth div 2 - t, i, 0); + if (CurrentHedgehog^.Effects[heResurrectable] <> 0) then + // show halo for resurrectable hog + DrawSprite(sprHaloHud, cScreenWidth div 2 - t - 2, i - SpritesData[sprHaloHud].Height + 1, 0); + // Vampirism + if cVampiric then + begin + inc(t, SpritesData[sprVampHud].Width + 2); + DrawSprite(sprVampHud, (cScreenWidth div 2 - t), i, 0); + end; + end; end else cDemoClockFPSOffsetY:= 0; @@ -1889,7 +1941,7 @@ var PrevSentPointTime: LongWord = 0; procedure MoveCamera; -var EdgesDist, wdy, shs,z, dstX: LongInt; +var EdgesDist, shs,z, dstX: LongInt; inbtwnTrgtAttks: Boolean; begin {$IFNDEF MOBILE} @@ -1937,12 +1989,11 @@ WorldDx:= WorldDx + rightX - leftX; end; -wdy:= trunc(cScreenHeight / cScaleFactor) + cScreenHeight div 2 - cWaterLine - (cVisibleWater + trunc(CinematicBarH / (cScaleFactor / 2.0))); -if WorldDy < wdy then - WorldDy:= wdy; - if ((CursorPoint.X = prevPoint.X) and (CursorPoint.Y = prevpoint.Y)) then + begin + CameraBounds; exit; + end; if (AMState = AMShowingUp) or (AMState = AMShowing) then begin @@ -1955,6 +2006,7 @@ if CursorPoint.Y < cScreenHeight - (AmmoRect.y + AmmoRect.h - AMSlotSize - 5) then//check bottom CursorPoint.Y:= cScreenHeight - (AmmoRect.y + AmmoRect.h - AMSlotSize - 5); prevPoint:= CursorPoint; + CameraBounds; exit end; @@ -1974,16 +2026,16 @@ if (CurrentTeam^.ExtDriven and isCursorVisible and autoCameraOn) or (not CurrentTeam^.ExtDriven and isCursorVisible) or ((FollowGear <> nil) and autoCameraOn) then begin - if CursorPoint.X < - cScreenWidth div 2 + EdgesDist then + if CursorPoint.X < - trunc(cScreenWidth / cScaleFactor) + EdgesDist then begin - WorldDx:= WorldDx - CursorPoint.X - cScreenWidth div 2 + EdgesDist; - CursorPoint.X:= - cScreenWidth div 2 + EdgesDist + WorldDx:= WorldDx - CursorPoint.X - trunc(cScreenWidth / cScaleFactor) + EdgesDist; + CursorPoint.X:= - trunc(cScreenWidth / cScaleFactor) + EdgesDist end else - if CursorPoint.X > cScreenWidth div 2 - EdgesDist then + if CursorPoint.X > trunc(cScreenWidth / cScaleFactor) - EdgesDist then begin - WorldDx:= WorldDx - CursorPoint.X + cScreenWidth div 2 - EdgesDist; - CursorPoint.X:= cScreenWidth div 2 - EdgesDist + WorldDx:= WorldDx - CursorPoint.X + trunc(cScreenWidth / cScaleFactor) - EdgesDist; + CursorPoint.X:= trunc(cScreenWidth / cScaleFactor) - EdgesDist end; shs:= min(cScreenHeight div 2 - trunc(cScreenHeight / cScaleFactor) + EdgesDist, cScreenHeight - EdgesDist); @@ -2010,14 +2062,10 @@ // this moves the camera according to CursorPoint X and Y prevPoint:= CursorPoint; -if WorldDy > LAND_HEIGHT + 1024 then - WorldDy:= LAND_HEIGHT + 1024; -if WorldDy < wdy then - WorldDy:= wdy; -if WorldDx < - LAND_WIDTH - 1024 then - WorldDx:= - LAND_WIDTH - 1024; -if WorldDx > 1024 then - WorldDx:= 1024; + +// enforce camera bounds +CameraBounds(); + end; procedure ShowMission(caption, subcaption, text: ansistring; icon, time : LongInt); diff -r 64740eec84ad -r 4c523ed1d35c misc/flags_js.xhtml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/flags_js.xhtml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,234 @@ + + + + + Hedgewars Flags + + + + + +

List of Hedgewars flags

+ + + diff -r 64740eec84ad -r 4c523ed1d35c misc/graves_js_anim.xhtml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/graves_js_anim.xhtml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,319 @@ + + + + + Hedgewars Graves + + + + + +

List of Hedgewars graves

+ + + diff -r 64740eec84ad -r 4c523ed1d35c misc/hats_js_anim.xhtml --- a/misc/hats_js_anim.xhtml Sun Mar 24 14:05:06 2024 -0400 +++ b/misc/hats_js_anim.xhtml Sun Mar 24 14:33:57 2024 -0400 @@ -6,7 +6,7 @@ @@ -187,8 +395,8 @@

List of Hedgewars hats

diff -r 64740eec84ad -r 4c523ed1d35c misc/libphyslayer/CMakeLists.txt --- a/misc/libphyslayer/CMakeLists.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/misc/libphyslayer/CMakeLists.txt Sun Mar 24 14:33:57 2024 -0400 @@ -17,7 +17,11 @@ set_target_properties(physlayer PROPERTIES VERSION 1.0 SOVERSION 1.0) -target_link_libraries(physlayer ${SDL2_LIBRARIES} lua physfs) +if(WIN32 AND VCPKG_TOOLCHAIN) + target_link_libraries(physlayer SDL2::SDL2 lua physfs) +else() + target_link_libraries(physlayer ${SDL2_LIBRARIES} lua physfs) +endif() install(TARGETS physlayer RUNTIME DESTINATION ${target_binary_install_dir} LIBRARY DESTINATION ${target_library_install_dir} ARCHIVE DESTINATION ${target_library_install_dir}) diff -r 64740eec84ad -r 4c523ed1d35c misc/libphyslayer/hwpacksmounter.c --- a/misc/libphyslayer/hwpacksmounter.c Sun Mar 24 14:05:06 2024 -0400 +++ b/misc/libphyslayer/hwpacksmounter.c Sun Mar 24 14:33:57 2024 -0400 @@ -8,6 +8,8 @@ { char ** filesList = PHYSFS_enumerateFiles("/"); char **i; + + if (!filesList) return; for (i = filesList; *i != NULL; i++) { diff -r 64740eec84ad -r 4c523ed1d35c misc/libphyslayer/physfscompat.h --- a/misc/libphyslayer/physfscompat.h Sun Mar 24 14:05:06 2024 -0400 +++ b/misc/libphyslayer/physfscompat.h Sun Mar 24 14:33:57 2024 -0400 @@ -21,6 +21,10 @@ #include "physfs.h" +#if defined(_WIN32) +#define PHYSFS_DECL __declspec(dllexport) +#endif + #if PHYSFS_VER_MAJOR == 2 #if PHYSFS_VER_MINOR == 0 diff -r 64740eec84ad -r 4c523ed1d35c misc/racer.yaml --- a/misc/racer.yaml Sun Mar 24 14:05:06 2024 -0400 +++ b/misc/racer.yaml Sun Mar 24 14:33:57 2024 -0400 @@ -43,7 +43,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -105,7 +105,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -167,7 +167,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -229,7 +229,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -291,7 +291,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -353,7 +353,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -415,7 +415,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -477,7 +477,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -539,7 +539,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -601,7 +601,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -663,7 +663,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -725,7 +725,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -787,7 +787,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -849,7 +849,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -911,7 +911,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -973,7 +973,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1035,7 +1035,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1097,7 +1097,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -1159,7 +1159,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1221,7 +1221,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1283,7 +1283,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -1345,7 +1345,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1407,7 +1407,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1469,7 +1469,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -1531,7 +1531,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' @@ -1593,7 +1593,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '1' @@ -1655,7 +1655,7 @@ - 'false' - 'false' - '100' - - '30' + - '90' - '100' - '50' - '0' diff -r 64740eec84ad -r 4c523ed1d35c project_files/HedgewarsMobile/Classes/SupportViewController.m --- a/project_files/HedgewarsMobile/Classes/SupportViewController.m Sun Mar 24 14:05:06 2024 -0400 +++ b/project_files/HedgewarsMobile/Classes/SupportViewController.m Sun Mar 24 14:33:57 2024 -0400 @@ -131,7 +131,7 @@ urlString = @"https://www.hedgewars.org"; break; case 3: - urlString = @"https://webchat.freenode.net/?channels=hedgewars"; + urlString = @"https://web.libera.chat/#hedgewars"; break; default: DLog(@"No way"); diff -r 64740eec84ad -r 4c523ed1d35c project_files/hwc/CMakeLists.txt --- a/project_files/hwc/CMakeLists.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/project_files/hwc/CMakeLists.txt Sun Mar 24 14:33:57 2024 -0400 @@ -112,7 +112,7 @@ ${LUA_LIBRARY} ${OPENGL_LIBRARY} ${SDL2_LIBRARIES} - ${SDL2_MIXER_LIBRARIES} + ${SDL2_MIXER_LIBRARY} ${SDL2_NET_LIBRARIES} ${SDL2_IMAGE_LIBRARIES} ${SDL2_TTF_LIBRARIES} diff -r 64740eec84ad -r 4c523ed1d35c project_files/hwc/rtl/sysutils.c --- a/project_files/hwc/rtl/sysutils.c Sun Mar 24 14:05:06 2024 -0400 +++ b/project_files/hwc/rtl/sysutils.c Sun Mar 24 14:33:57 2024 -0400 @@ -83,7 +83,7 @@ // Semi-dummy implementation of FormatDateTime string255 fpcrtl_formatDateTime(string255 FormatStr, TDateTime DateTime) { - string255 buffer = STRINIT(FormatStr.str); + string255 buffer = FormatStr; time_t rawtime; struct tm* my_tm; diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/CMakeLists.txt --- a/qmlfrontend/CMakeLists.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/CMakeLists.txt Sun Mar 24 14:33:57 2024 -0400 @@ -1,7 +1,10 @@ -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.8) project(qmlfrontend LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/Page1.qml --- a/qmlfrontend/Page1.qml Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/Page1.qml Sun Mar 24 14:33:57 2024 -0400 @@ -2,66 +2,67 @@ import Hedgewars.Engine 1.0 Page1Form { - focus: true + focus: true - property HWEngine hwEngine - property NetSession netSession + property HWEngine hwEngine + property NetSession netSession + + Component { + id: hwEngineComponent - Component { - id: hwEngineComponent - - HWEngine { - engineLibrary: "./libhedgewars_engine.so" - previewAcceptor: PreviewAcceptor - onPreviewImageChanged: previewImage.source = "image://preview/image" - onPreviewIsRendering: previewImage.source = "qrc:/res/iconTime.png" + HWEngine { + engineLibrary: "../rust/lib-hedgewars-engine/target/debug/libhedgewars_engine.so" + dataPath: "../share/hedgewars/Data" + previewAcceptor: PreviewAcceptor + onPreviewImageChanged: previewImage.source = "image://preview/image" + onPreviewIsRendering: previewImage.source = "qrc:/res/iconTime.png" + } } - } - Component { - id: netSessionComponent + Component { + id: netSessionComponent - NetSession { - nickname: "test0272" - url: "hwnet://gameserver.hedgewars.org:46632" + NetSession { + nickname: "test0272" + url: "hwnet://gameserver.hedgewars.org:46632" + } } - } - Component.onCompleted: { - hwEngine = hwEngineComponent.createObject() - } + Component.onCompleted: { + hwEngine = hwEngineComponent.createObject() + } - tickButton { - onClicked: { - tickButton.visible = false - gameView.tick(100) + tickButton { + onClicked: { + tickButton.visible = false + gameView.tick(100) + } } - } - gameButton { - visible: !gameView.engineInstance - onClicked: { - var engineInstance = hwEngine.runQuickGame() - gameView.engineInstance = engineInstance + gameButton { + visible: !gameView.engineInstance + onClicked: { + const engineInstance = hwEngine.runQuickGame() + gameView.engineInstance = engineInstance + } } - } - button1 { - visible: !gameView.engineInstance - onClicked: { - hwEngine.getPreview() + button1 { + visible: !gameView.engineInstance + onClicked: { + hwEngine.getPreview() + } + } + netButton.onClicked: { + netSession = netSessionComponent.createObject() + netSession.open() } - } - netButton.onClicked: { - netSession = netSessionComponent.createObject() - netSession.open() - } + + Keys.onPressed: { + if (event.key === Qt.Key_Enter) + gameView.engineInstance.longEvent(Engine.Attack, Engine.Set) + } - Keys.onPressed: { - if (event.key === Qt.Key_Enter) - gameView.engineInstance.longEvent(Engine.Attack, Engine.Set) - } - - Keys.onReleased: { - if (event.key === Qt.Key_Enter) - gameView.engineInstance.longEvent(Engine.Attack, Engine.Unset) - } + Keys.onReleased: { + if (event.key === Qt.Key_Enter) + gameView.engineInstance.longEvent(Engine.Attack, Engine.Unset) + } } diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/engine_instance.cpp --- a/qmlfrontend/engine_instance.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/engine_instance.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -6,15 +6,15 @@ #include static QOpenGLContext* currentOpenglContext = nullptr; -extern "C" void (*getProcAddress(const char* fn))() { +extern "C" void* getProcAddress(const char* fn) { if (!currentOpenglContext) return nullptr; else - return currentOpenglContext->getProcAddress(fn); + return reinterpret_cast(currentOpenglContext->getProcAddress(fn)); } -EngineInstance::EngineInstance(const QString& libraryPath, QObject* parent) - : QObject(parent) { +EngineInstance::EngineInstance(const QString& libraryPath, const QString&dataPath, QObject* parent) + : QObject(parent), m_instance{nullptr, nullptr} { QLibrary hwlib(libraryPath); if (!hwlib.load()) @@ -62,52 +62,52 @@ qDebug() << "Loaded engine library with protocol version" << hedgewars_engine_protocol_version(); - m_instance = start_engine(); + m_instance = std::unique_ptr( + start_engine(reinterpret_cast(dataPath.toUtf8().data())), + cleanup); } else { qDebug("Engine library load failed"); } } -EngineInstance::~EngineInstance() { - if (m_isValid) cleanup(m_instance); -} +EngineInstance::~EngineInstance() = default; void EngineInstance::sendConfig(const GameConfig& config) { for (auto b : config.config()) { - send_ipc(m_instance, reinterpret_cast(b.data()), + send_ipc(m_instance.get(), reinterpret_cast(b.data()), static_cast(b.size())); } } void EngineInstance::advance(quint32 ticks) { - advance_simulation(m_instance, ticks); + advance_simulation(m_instance.get(), ticks); } void EngineInstance::moveCamera(const QPoint& delta) { - move_camera(m_instance, delta.x(), delta.y()); + move_camera(m_instance.get(), delta.x(), delta.y()); } void EngineInstance::simpleEvent(Engine::SimpleEventType event_type) { - simple_event(m_instance, event_type); + simple_event(m_instance.get(), event_type); } void EngineInstance::longEvent(Engine::LongEventType event_type, Engine::LongEventState state) { - long_event(m_instance, event_type, state); + long_event(m_instance.get(), event_type, state); } void EngineInstance::positionedEvent(Engine::PositionedEventType event_type, qint32 x, qint32 y) { - positioned_event(m_instance, event_type, x, y); + positioned_event(m_instance.get(), event_type, x, y); } -void EngineInstance::renderFrame() { render_frame(m_instance); } +void EngineInstance::renderFrame() { render_frame(m_instance.get()); } void EngineInstance::setOpenGLContext(QOpenGLContext* context) { currentOpenglContext = context; auto size = context->surface()->size(); - setup_current_gl_context(m_instance, static_cast(size.width()), + setup_current_gl_context(m_instance.get(), static_cast(size.width()), static_cast(size.height()), &getProcAddress); } @@ -115,7 +115,7 @@ QImage EngineInstance::generatePreview() { Engine::PreviewInfo pinfo; - generate_preview(m_instance, &pinfo); + generate_preview(m_instance.get(), &pinfo); QVector colorTable; colorTable.resize(256); @@ -126,7 +126,7 @@ previewImage.setColorTable(colorTable); // Cannot use it here, since QImage refers to original bytes - // dispose_preview(m_instance); + // dispose_preview(m_instance.get()); return previewImage; } diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/engine_instance.h --- a/qmlfrontend/engine_instance.h Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/engine_instance.h Sun Mar 24 14:33:57 2024 -0400 @@ -4,6 +4,7 @@ #include #include #include +#include #include "engine_interface.h" #include "game_config.h" @@ -12,7 +13,7 @@ Q_OBJECT public: - explicit EngineInstance(const QString& libraryPath, + explicit EngineInstance(const QString& libraryPath,const QString& dataPath, QObject* parent = nullptr); ~EngineInstance(); @@ -38,7 +39,7 @@ qint32 y); private: - Engine::EngineInstance* m_instance; + std::unique_ptr m_instance; Engine::hedgewars_engine_protocol_version_t* hedgewars_engine_protocol_version; diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/engine_interface.h --- a/qmlfrontend/engine_interface.h Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/engine_interface.h Sun Mar 24 14:33:57 2024 -0400 @@ -4,8 +4,7 @@ #include #include -#ifdef __cplusplus -#define ENUM_CLASS enum +#include "../rust/lib-hedgewars-engine/target/lib-hedgewars-engine.hpp" #ifndef Q_NAMESPACE #define Q_NAMESPACE @@ -21,69 +20,35 @@ namespace Engine { extern "C" { -#else -#define ENUM_CLASS enum class -#endif - -typedef struct _EngineInstance EngineInstance; - -typedef struct { - uint32_t width; - uint32_t height; - uint8_t hedgehogs_number; - unsigned char* land; -} PreviewInfo; -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); +using EngineInstance = hwengine::EngineInstance; +using PreviewInfo = hwengine::PreviewInfo; -typedef void send_ipc_t(EngineInstance* engine_state, uint8_t* buf, - size_t size); -typedef size_t read_ipc_t(EngineInstance* engine_state, uint8_t* buf, - size_t size); - -typedef void setup_current_gl_context_t(EngineInstance* engine_state, - uint16_t width, uint16_t height, - void (*(const char*))()); -typedef void render_frame_t(EngineInstance* engine_state); +using hedgewars_engine_protocol_version_t = + decltype(hwengine::hedgewars_engine_protocol_version); -typedef bool advance_simulation_t(EngineInstance* engine_state, uint32_t ticks); - -typedef void move_camera_t(EngineInstance* engine_state, int32_t delta_x, - int32_t delta_y); - -ENUM_CLASS SimpleEventType{ - SwitchHedgehog, Timer, LongJump, HighJump, Accept, Deny, -}; - -ENUM_CLASS LongEventType{ - ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Precision, Attack, -}; +using start_engine_t = decltype(hwengine::start_engine); +using generate_preview_t = decltype(hwengine::generate_preview); +using dispose_preview_t = decltype(hwengine::dispose_preview); +using cleanup_t = decltype(hwengine::cleanup); +using send_ipc_t = decltype(hwengine::send_ipc); +using read_ipc_t = decltype(hwengine::read_ipc); +using setup_current_gl_context_t = decltype(hwengine::setup_current_gl_context); +using render_frame_t = decltype(hwengine::render_frame); +using advance_simulation_t = decltype(hwengine::advance_simulation); +using move_camera_t = decltype(hwengine::move_camera); -ENUM_CLASS LongEventState{ - Set, - Unset, -}; +using simple_event_t = decltype(hwengine::simple_event); +using long_event_t = decltype(hwengine::long_event); +using positioned_event_t = decltype(hwengine::positioned_event); -ENUM_CLASS PositionedEventType{ - CursorMove, - CursorClick, -}; +using SimpleEventType = hwengine::SimpleEventType; +using LongEventType = hwengine::LongEventType; +using LongEventState = hwengine::LongEventState; +using PositionedEventType = hwengine::PositionedEventType; -typedef void simple_event_t(EngineInstance* engine_state, - SimpleEventType event_type); -typedef void long_event_t(EngineInstance* engine_state, - LongEventType event_type, LongEventState state); -typedef void positioned_event_t(EngineInstance* engine_state, - PositionedEventType event_type, int32_t x, - int32_t y); } // extern "C" -#ifdef __cplusplus Q_NAMESPACE Q_ENUM_NS(SimpleEventType) @@ -97,6 +62,5 @@ Q_DECLARE_METATYPE(Engine::LongEventType) Q_DECLARE_METATYPE(Engine::LongEventState) Q_DECLARE_METATYPE(Engine::PositionedEventType) -#endif #endif // ENGINE_H diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/game_view.cpp --- a/qmlfrontend/game_view.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/game_view.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -42,7 +42,9 @@ void GameView::cleanup() { m_renderer.reset(); } void GameView::setEngineInstance(EngineInstance* engineInstance) { - if (m_engineInstance == engineInstance) return; + if (m_engineInstance == engineInstance) { + return; + } cleanup(); m_engineInstance = engineInstance; @@ -59,23 +61,27 @@ &GameViewRenderer::paint, Qt::DirectConnection); } - if (m_windowChanged || (m_viewportSize != window()->size())) { + if (m_windowChanged || (m_viewportSize != size())) { m_windowChanged = false; if (m_engineInstance) m_engineInstance->setOpenGLContext(window()->openglContext()); - m_viewportSize = window()->size(); + m_viewportSize = size().toSize(); m_centerPoint = QPoint(m_viewportSize.width(), m_viewportSize.height()) / 2; } if (m_engineInstance) { - QPoint mousePos = mapFromGlobal(QCursor::pos()).toPoint(); - m_engineInstance->moveCamera(mousePos - m_centerPoint); - QCursor::setPos(mapToGlobal(m_centerPoint).toPoint()); + const auto delta = mapFromGlobal(QCursor::pos()).toPoint() - m_centerPoint; + + m_engineInstance->moveCamera(delta); + + QCursor::setPos(window()->screen(), mapToGlobal(m_centerPoint).toPoint()); } - if (m_renderer) m_renderer->tick(m_delta); + if (m_renderer) { + m_renderer->tick(m_delta); + } } GameViewRenderer::GameViewRenderer() @@ -90,7 +96,9 @@ } void GameViewRenderer::paint() { - if (m_delta == 0) return; + if (m_delta == 0) { + return; + } if (m_engineInstance) { m_engineInstance->advance(m_delta); diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/hwengine.cpp --- a/qmlfrontend/hwengine.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/hwengine.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -9,7 +9,7 @@ #include "game_view.h" #include "preview_acceptor.h" -HWEngine::HWEngine(QObject* parent) : QObject(parent) {} +HWEngine::HWEngine(QObject* parent) : QObject(parent), m_dataPath{QStringLiteral("Data")} {} HWEngine::~HWEngine() {} @@ -19,7 +19,7 @@ m_gameConfig = GameConfig(); m_gameConfig.cmdSeed(QUuid::createUuid().toByteArray()); - EngineInstance engine(m_engineLibrary); + EngineInstance engine(m_engineLibrary, m_dataPath); if (!engine.isValid()) // TODO: error notification return; @@ -43,7 +43,8 @@ m_gameConfig.cmdTeam(team1); m_gameConfig.cmdTeam(team2); - EngineInstance* engine = new EngineInstance(m_engineLibrary, this); + EngineInstance* engine = new EngineInstance(m_engineLibrary, m_dataPath, this); + engine->sendConfig(m_gameConfig); return engine; // m_runQueue->queue(m_gameConfig); @@ -68,3 +69,16 @@ m_engineLibrary = engineLibrary; emit engineLibraryChanged(m_engineLibrary); } + +const QString &HWEngine::dataPath() const +{ + return m_dataPath; +} + +void HWEngine::setDataPath(const QString &newDataPath) +{ + if (m_dataPath == newDataPath) + return; + m_dataPath = newDataPath; + emit dataPathChanged(); +} diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/hwengine.h --- a/qmlfrontend/hwengine.h Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/hwengine.h Sun Mar 24 14:33:57 2024 -0400 @@ -20,6 +20,7 @@ setPreviewAcceptor NOTIFY previewAcceptorChanged) Q_PROPERTY(QString engineLibrary READ engineLibrary WRITE setEngineLibrary NOTIFY engineLibraryChanged) + Q_PROPERTY(QString dataPath READ dataPath WRITE setDataPath NOTIFY dataPathChanged) public: explicit HWEngine(QObject* parent = nullptr); @@ -32,7 +33,10 @@ PreviewAcceptor* previewAcceptor() const; QString engineLibrary() const; - public slots: + const QString &dataPath() const; + void setDataPath(const QString &newDataPath); + +public slots: void setPreviewAcceptor(PreviewAcceptor* previewAcceptor); void setEngineLibrary(const QString& engineLibrary); @@ -45,12 +49,15 @@ void previewAcceptorChanged(PreviewAcceptor* previewAcceptor); void engineLibraryChanged(const QString& engineLibrary); - private: + void dataPathChanged(); + +private: QQmlEngine* m_engine; GameConfig m_gameConfig; int m_previewHedgehogsCount; PreviewAcceptor* m_previewAcceptor; QString m_engineLibrary; + QString m_dataPath; }; #endif // HWENGINE_H diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/net_session.cpp --- a/qmlfrontend/net_session.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/net_session.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -340,7 +340,7 @@ QString hash = QCryptographicHash::hash(m_clientSalt.toLatin1() .append(m_serverSalt.toLatin1()) - .append(m_passwordHash) + .append(m_passwordHash.toLatin1()) .append(QByteArray::number(cProtocolVersion)) .append("!hedgewars"), QCryptographicHash::Sha1) @@ -349,7 +349,7 @@ m_serverHash = QCryptographicHash::hash(m_serverSalt.toLatin1() .append(m_clientSalt.toLatin1()) - .append(m_passwordHash) + .append(m_passwordHash.toLatin1()) .append(QByteArray::number(cProtocolVersion)) .append("!hedgewars"), QCryptographicHash::Sha1) diff -r 64740eec84ad -r 4c523ed1d35c qmlfrontend/players_model.cpp --- a/qmlfrontend/players_model.cpp Sun Mar 24 14:05:06 2024 -0400 +++ b/qmlfrontend/players_model.cpp Sun Mar 24 14:33:57 2024 -0400 @@ -355,10 +355,10 @@ stream << "; this list is used by Hedgewars - do not edit it unless you know " "what you're doing!" - << endl; + << Qt::endl; foreach (const QString &nick, set.values()) - stream << nick << endl; + stream << nick << Qt::endl; txt.close(); } diff -r 64740eec84ad -r 4c523ed1d35c rust/fpnum/src/lib.rs --- a/rust/fpnum/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/fpnum/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -344,7 +344,7 @@ FPNum { sign_mask: POSITIVE_MASK, - value: integral_sqrt_ext(sqr) as u64, + value: integral_sqrt_ext(sqr), } } } @@ -473,7 +473,7 @@ bin_assign_op_impl!(FPPoint, ops::DivAssign, div_assign, /); pub fn integral_sqrt(value: u64) -> u64 { - let mut digits = (64u32 - 1).saturating_sub(value.leading_zeros()) & 0xFFFF_FFFE; + let mut digits = (64u32 - 1).saturating_sub(value.leading_zeros()) & 0xFE; let mut result = if value == 0 { 0u64 } else { 1u64 }; while digits != 0 { @@ -487,21 +487,18 @@ result } -pub fn integral_sqrt_ext(mut value: u128) -> u128 { - let mut digit_sqr = - 0x40000000_00000000_00000000_00000000u128.wrapping_shr(value.leading_zeros() & 0xFFFF_FFFE); - let mut result = 0u128; +pub fn integral_sqrt_ext(value: u128) -> u64 { + let mut digits = (128u32 - 1).saturating_sub(value.leading_zeros()) & 0xFE; + let mut result = if value == 0 { 0u64 } else { 1u64 }; - while digit_sqr != 0 { - let approx = result + digit_sqr; - result >>= 1; + while digits != 0 { + result <<= 1; + if ((result + 1) as u128).pow(2) <= value >> (digits - 2) { + result += 1; + } + digits -= 2; + } - if approx <= value { - value -= approx; - result += digit_sqr; - } - digit_sqr >>= 2; - } result } @@ -514,7 +511,7 @@ FPNum { sign_mask: POSITIVE_MASK, - value: integral_sqrt_ext(sqr) as u64, + value: integral_sqrt_ext(sqr), } } @@ -523,7 +520,6 @@ AngleCos */ -#[cfg(test)] #[test] fn basics() { let n = fp!(15 / 2); @@ -541,6 +537,8 @@ assert_eq!((-n).round(), -7); assert_eq!(f64::from(fp!(5/2)), 2.5f64); + + assert_eq!(integral_sqrt_ext(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), 0xFFFF_FFFF_FFFF_FFFF); } #[test] diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-checker/Cargo.toml --- a/rust/hedgewars-checker/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-checker/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -5,11 +5,14 @@ edition = "2018" [dependencies] -rust-ini = "0.13" -dirs = "1.0" -argparse = "0.2.2" +rust-ini = "0.19" +dirs = "5.0" +argparse = "0.2" log = "0.4" -stderrlog = "0.4" +stderrlog = "0.5" netbuf = "0.4" tempfile = "3.0" -base64 = "0.9.3" +base64 = "0.21" +hedgewars-network-protocol = { path = "../hedgewars-network-protocol" } +anyhow = "1.0" +tokio = {version="1", features = ["full"]} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-checker/src/main.rs --- a/rust/hedgewars-checker/src/main.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-checker/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,38 +1,26 @@ +use anyhow::{anyhow, bail, Result}; use argparse::{ArgumentParser, Store}; +use base64::{engine::general_purpose, Engine}; +use hedgewars_network_protocol::{ + messages::HwProtocolMessage as ClientMessage, messages::HwServerMessage::*, parser, +}; use ini::Ini; +use log::{debug, info, warn}; use netbuf::Buf; -use log::{debug, warn, info}; -use std::{ - io::Write, - net::TcpStream, - process::Command, - str::FromStr -}; - -type CheckError = Box; +use std::{io::Write, str::FromStr}; +use tokio::time::MissedTickBehavior; +use tokio::{io, io::AsyncWriteExt, net::TcpStream, process::Command, sync::mpsc}; -fn extract_packet(buf: &mut Buf) -> Option { - let packet_end = (&buf[..]).windows(2).position(|window| window == b"\n\n")?; - - let mut tail = buf.split_off(packet_end); - - std::mem::swap(&mut tail, buf); - - buf.consume(2); - - Some(tail) -} - -fn check(executable: &str, data_prefix: &str, buffer: &[u8]) -> Result>, CheckError> { +async fn check(executable: &str, data_prefix: &str, buffer: &[String]) -> Result> { let mut replay = tempfile::NamedTempFile::new()?; - for line in buffer.split(|b| *b == '\n' as u8) { - replay.write(&base64::decode(line)?)?; + for line in buffer.iter() { + replay.write_all(&general_purpose::STANDARD.decode(line)?)?; } let temp_file_path = replay.path(); - let mut home_dir = dirs::home_dir().unwrap(); + let mut home_dir = dirs::home_dir().ok_or(anyhow!("Home path not detected"))?; home_dir.push(".hedgewars"); debug!("Checking replay in {}", temp_file_path.to_string_lossy()); @@ -46,152 +34,251 @@ .arg("--nosound") .arg("--stats-only") .arg(temp_file_path) - .output()?; + //.spawn()? + //.wait_with_output() + .output() + .await?; + + debug!("Engine finished!"); let mut result = Vec::new(); let mut engine_lines = output .stderr - .split(|b| *b == '\n' as u8) + .split(|b| *b == b'\n') .skip_while(|l| *l != b"WINNERS" && *l != b"DRAW"); + // debug!("Engine lines: {:?}", &engine_lines); + loop { match engine_lines.next() { - Some(b"DRAW") => result.push(b"DRAW".to_vec()), + Some(b"DRAW") => result.push("DRAW".to_owned()), Some(b"WINNERS") => { - result.push(b"WINNERS".to_vec()); + result.push("WINNERS".to_owned()); let winners = engine_lines.next().unwrap(); let winners_num = u32::from_str(&String::from_utf8(winners.to_vec())?)?; - result.push(winners.to_vec()); + result.push(String::from_utf8(winners.to_vec())?); for _i in 0..winners_num { - result.push(engine_lines.next().unwrap().to_vec()); + result.push(String::from_utf8(engine_lines.next().unwrap().to_vec())?); } } Some(b"GHOST_POINTS") => { - result.push(b"GHOST_POINTS".to_vec()); + result.push("GHOST_POINTS".to_owned()); let points = engine_lines.next().unwrap(); let points_num = u32::from_str(&String::from_utf8(points.to_vec())?)? * 2; - result.push(points.to_vec()); + result.push(String::from_utf8(points.to_vec())?); for _i in 0..points_num { - result.push(engine_lines.next().unwrap().to_vec()); + result.push(String::from_utf8(engine_lines.next().unwrap().to_vec())?); } } Some(b"ACHIEVEMENT") => { - result.push(b"ACHIEVEMENT".to_vec()); + result.push("ACHIEVEMENT".to_owned()); for _i in 0..4 { - result.push(engine_lines.next().unwrap().to_vec()); + result.push(String::from_utf8(engine_lines.next().unwrap().to_vec())?); } } _ => break, } } - if result.len() > 0 { + // println!("Engine lines: {:?}", &result); + + if !result.is_empty() { Ok(result) } else { - Err("no data from engine".into()) + bail!("no data from engine") } } -fn connect_and_run( - username: &str, - password: &str, - protocol_number: u32, +async fn check_loop( executable: &str, data_prefix: &str, -) -> Result<(), CheckError> { + results_sender: mpsc::Sender>>, + mut replay_receiver: mpsc::Receiver>, +) -> Result<()> { + while let Some(replay) = replay_receiver.recv().await { + results_sender + .send(check(executable, data_prefix, &replay).await) + .await?; + } + + Ok(()) +} + +async fn connect_and_run( + username: &str, + password: &str, + protocol_number: u16, + replay_sender: mpsc::Sender>, + mut results_receiver: mpsc::Receiver>>, +) -> Result<()> { info!("Connecting..."); - let mut stream = TcpStream::connect("hedgewars.org:46631")?; - stream.set_nonblocking(false)?; + let mut stream = TcpStream::connect("hedgewars.org:46631").await?; let mut buf = Buf::new(); + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + loop { - buf.read_from(&mut stream)?; + let r = tokio::select! { + _ = interval.tick() => { + // Send Ping + stream.write_all(ClientMessage::Ping.to_raw_protocol().as_bytes()).await?; + None + }, + _ = stream.readable() => None, + r = results_receiver.recv() => r + }; + + //println!("Loop: {:?}", &r); + + if let Some(execution_result) = r { + match execution_result { + Ok(result) => { + info!("Checked"); + debug!("Check result: [{:?}]", result); - while let Some(msg) = extract_packet(&mut buf) { - if msg[..].starts_with(b"CONNECTED") { - info!("Connected"); - let p = format!( - "CHECKER\n{}\n{}\n{}\n\n", - protocol_number, username, password - ); - stream.write(p.as_bytes())?; - } else if msg[..].starts_with(b"PING") { - stream.write(b"PONG\n\n")?; - } else if msg[..].starts_with(b"LOGONPASSED") { - info!("Logged in"); - stream.write(b"READY\n\n")?; - } else if msg[..].starts_with(b"REPLAY") { - info!("Got a replay"); - match check(executable, data_prefix, &msg[7..]) { - Ok(result) => { - info!("Checked"); - debug!( - "Check result: [{}]", - String::from_utf8_lossy(&result.join(&(',' as u8))) - ); + stream + .write_all( + ClientMessage::CheckedOk(result) + .to_raw_protocol() + .as_bytes(), + ) + .await?; + stream + .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes()) + .await?; + } + Err(e) => { + info!("Check failed: {:?}", e); + stream + .write_all( + ClientMessage::CheckedFail("error".to_owned()) + .to_raw_protocol() + .as_bytes(), + ) + .await?; + stream + .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes()) + .await?; + } + } + } else { + let mut msg = [0; 4096]; + // Try to read data, this may still fail with `WouldBlock` + // if the readiness event is a false positive. + match stream.try_read(&mut msg) { + Ok(n) => { + //println!("{:?}", &msg); + buf.write_all(&msg[0..n])?; + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {} + Err(e) => { + return Err(e.into()); + } + } + } + + while let Ok((tail, msg)) = parser::server_message(buf.as_ref()) { + let tail_len = tail.len(); + buf.consume(buf.len() - tail_len); + + // println!("Message from server: {:?}", &msg); - stream.write(b"CHECKED\nOK\n")?; - stream.write(&result.join(&('\n' as u8)))?; - stream.write(b"\n\nREADY\n\n")?; - } - Err(e) => { - info!("Check failed: {:?}", e); - stream.write(b"CHECKED\nFAIL\nerror\n\nREADY\n\n")?; + match msg { + Connected(_, _) => { + info!("Connected"); + stream + .write_all( + ClientMessage::Checker( + protocol_number, + username.to_owned(), + password.to_owned(), + ) + .to_raw_protocol() + .as_bytes(), + ) + .await?; + } + Ping => { + stream + .write_all(ClientMessage::Pong.to_raw_protocol().as_bytes()) + .await?; + } + Pong => { + // do nothing + } + LogonPassed => { + stream + .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes()) + .await?; + } + Replay(lines) => { + info!("Got a replay"); + replay_sender.send(lines).await?; + } + Bye(message) => { + warn!("Received BYE: {}", message); + return Ok(()); + } + ChatMsg { nick, msg } => { + info!("Chat [{}]: {}", nick, msg); + } + RoomAdd(fields) => { + let mut l = fields.into_iter(); + info!("Room added: {}", l.nth(1).unwrap()); + } + RoomUpdated(name, fields) => { + let mut l = fields.into_iter(); + let new_name = l.nth(1).unwrap(); + + if name != new_name { + info!("Room renamed: {}", new_name); } } - } else if msg[..].starts_with(b"BYE") { - warn!("Received BYE: {}", String::from_utf8_lossy(&msg[..])); - return Ok(()); - } else if msg[..].starts_with(b"CHAT") { - let body = String::from_utf8_lossy(&msg[5..]); - let mut l = body.lines(); - info!("Chat [{}]: {}", l.next().unwrap(), l.next().unwrap()); - } else if msg[..].starts_with(b"ROOM") { - let body = String::from_utf8_lossy(&msg[5..]); - let mut l = body.lines(); - if let Some(action) = l.next() { - if action == "ADD" { - info!("Room added: {}", l.skip(1).next().unwrap()); - } + RoomRemove(_) => { + // ignore } - } else if msg[..].starts_with(b"ERROR") { - warn!("Received ERROR: {}", String::from_utf8_lossy(&msg[..])); - return Ok(()); - } else { - warn!( - "Unknown protocol command: {}", - String::from_utf8_lossy(&msg[..]) - ) + Error(message) => { + warn!("Received ERROR: {}", message); + return Ok(()); + } + something => { + warn!("Unexpected protocol command: {:?}", something) + } } } } } -fn get_protocol_number(executable: &str) -> std::io::Result { - let output = Command::new(executable).arg("--protocol").output()?; +async fn get_protocol_number(executable: &str) -> Result { + let output = Command::new(executable).arg("--protocol").output().await?; - Ok(u32::from_str(&String::from_utf8(output.stdout).unwrap().trim()).unwrap_or(55)) + Ok(u16::from_str(String::from_utf8(output.stdout)?.trim()).unwrap_or(55)) } -fn main() { +#[tokio::main] +async fn main() -> Result<()> { stderrlog::new() .verbosity(3) .timestamp(stderrlog::Timestamp::Second) .module(module_path!()) - .init() - .unwrap(); + .init()?; - let mut frontend_settings = dirs::home_dir().unwrap(); + let mut frontend_settings = dirs::home_dir().ok_or(anyhow!("Home path not detected"))?; frontend_settings.push(".hedgewars/settings.ini"); let i = Ini::load_from_file(frontend_settings.to_str().unwrap()).unwrap(); - let username = i.get_from(Some("net"), "nick").unwrap(); - let password = i.get_from(Some("net"), "passwordhash").unwrap(); + let username = i + .get_from(Some("net"), "nick") + .ok_or(anyhow!("Nickname not found in frontend config"))?; + let password = i + .get_from(Some("net"), "passwordhash") + .ok_or(anyhow!("Password not found in frontend config"))?; let mut exe = "/usr/local/bin/hwengine".to_string(); let mut prefix = "/usr/local/share/hedgewars/Data".to_string(); @@ -208,29 +295,24 @@ info!("Executable: {}", exe); info!("Data dir: {}", prefix); - let protocol_number = get_protocol_number(&exe.as_str()).unwrap_or_default(); + let protocol_number = get_protocol_number(exe.as_str()).await?; info!("Using protocol number {}", protocol_number); - connect_and_run(&username, &password, protocol_number, &exe, &prefix).unwrap(); -} + let (replay_sender, replay_receiver) = mpsc::channel(1); + let (results_sender, results_receiver) = mpsc::channel(1); -#[cfg(test)] -#[test] -fn test() { - let mut buf = Buf::new(); - buf.extend(b"Hell"); - if let Some(_) = extract_packet(&mut buf) { - assert!(false) - } + let (network_result, checker_result) = tokio::join!( + connect_and_run( + username, + password, + protocol_number, + replay_sender, + results_receiver + ), + check_loop(&exe, &prefix, results_sender, replay_receiver) + ); - buf.extend(b"o\n\nWorld"); - - let packet2 = extract_packet(&mut buf).unwrap(); - assert_eq!(&buf[..], b"World"); - assert_eq!(&packet2[..], b"Hello"); - - if let Some(_) = extract_packet(&mut buf) { - assert!(false) - } + network_result?; + checker_result } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-engine-messages/Cargo.toml --- a/rust/hedgewars-engine-messages/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-engine-messages/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -5,6 +5,6 @@ edition = "2018" [dependencies] -nom = "4.1" +nom = "7.1" byteorder = "1.2" queues = "1.1" diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-engine-messages/src/messages.rs --- a/rust/hedgewars-engine-messages/src/messages.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-engine-messages/src/messages.rs Sun Mar 24 14:33:57 2024 -0400 @@ -128,7 +128,7 @@ #[derive(Debug, PartialEq, Clone)] pub enum EngineMessage { - Unknown, + Unknown(Vec), Empty, Synced(SyncedEngineMessage, u32), Unsynced(UnsyncedEngineMessage), @@ -172,7 +172,7 @@ NextTurn => em![b'N'], Switch => em![b'S'], Timer(t) => vec![b'0' + t], - Slot(s) => vec![b'~' , *s], + Slot(s) => vec![b'~', *s], SetWeapon(s) => vec![b'~', *s], Put(x, y) => { let mut v = vec![b'p']; @@ -180,14 +180,14 @@ v.write_i24::(*y).unwrap(); v - }, + } CursorMove(x, y) => { let mut v = vec![b'P']; v.write_i24::(*x).unwrap(); v.write_i24::(*y).unwrap(); v - }, + } HighJump => em![b'J'], LongJump => em![b'j'], Skip => em![b','], @@ -242,7 +242,7 @@ fn to_unwrapped(&self) -> Vec { use self::EngineMessage::*; match self { - Unknown => unreachable!("you're not supposed to construct such messages"), + Unknown(_) => unreachable!("you're not supposed to construct such messages"), Empty => unreachable!("you're not supposed to construct such messages"), Synced(SyncedEngineMessage::TimeWrap, _) => vec![b'#', 0xff, 0xff], Synced(msg, timestamp) => { diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-engine-messages/src/parser.rs --- a/rust/hedgewars-engine-messages/src/parser.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-engine-messages/src/parser.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,125 +1,169 @@ +use std::str; + +use nom::branch::alt; +use nom::bytes::streaming::*; +use nom::combinator::*; +use nom::error::{ErrorKind, ParseError}; +use nom::multi::*; +use nom::number::streaming::*; +use nom::sequence::{pair, preceded, terminated, tuple}; +use nom::{Err, IResult, Parser}; + use crate::messages::{ ConfigEngineMessage::*, EngineMessage::*, KeystrokeAction::*, SyncedEngineMessage::*, UnorderedEngineMessage::*, *, }; -use nom::{Err::Error, *}; -use std::str; -macro_rules! eof_slice ( - ($i:expr,) => ( - { - if ($i).input_len() == 0 { - Ok(($i, $i)) - } else { - Err(Error(error_position!($i, ErrorKind::Eof::))) - } +fn eof_slice(i: I) -> IResult +where + I: nom::InputLength + Clone, +{ + if i.input_len() == 0 { + Ok((i.clone(), i)) + } else { + Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof))) } - ); -); +} +fn unrecognized_message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + map(rest, |i: &[u8]| Unknown(i.to_owned()))(input) +} -named!(unrecognized_message<&[u8], EngineMessage>, - do_parse!(rest >> (Unknown)) -); +fn string_tail(input: &[u8]) -> IResult<&[u8], String> { + map_res(rest, str::from_utf8)(input).map(|(i, s)| (i, s.to_owned())) +} -named!(string_tail<&[u8], String>, map!(map_res!(rest, str::from_utf8), String::from)); - -named!(length_without_timestamp<&[u8], usize>, - map_opt!(rest_len, |l| if l > 2 { Some(l - 2) } else { None } ) -); +fn length_without_timestamp(input: &[u8]) -> IResult<&[u8], usize> { + map_opt(rest_len, |l| if l > 2 { Some(l - 2) } else { None })(input) +} -named!(synced_message<&[u8], SyncedEngineMessage>, alt!( - do_parse!(tag!("L") >> (Left(Press))) - | do_parse!(tag!("l") >> ( Left(Release) )) - | do_parse!(tag!("R") >> ( Right(Press) )) - | do_parse!(tag!("r") >> ( Right(Release) )) - | do_parse!(tag!("U") >> ( Up(Press) )) - | do_parse!(tag!("u") >> ( Up(Release) )) - | do_parse!(tag!("D") >> ( Down(Press) )) - | do_parse!(tag!("d") >> ( Down(Release) )) - | do_parse!(tag!("Z") >> ( Precise(Press) )) - | do_parse!(tag!("z") >> ( Precise(Release) )) - | do_parse!(tag!("A") >> ( Attack(Press) )) - | do_parse!(tag!("a") >> ( Attack(Release) )) - | do_parse!(tag!("N") >> ( NextTurn )) - | do_parse!(tag!("j") >> ( LongJump )) - | do_parse!(tag!("J") >> ( HighJump )) - | do_parse!(tag!("S") >> ( Switch )) - | do_parse!(tag!(",") >> ( Skip )) - | do_parse!(tag!("1") >> ( Timer(1) )) - | do_parse!(tag!("2") >> ( Timer(2) )) - | do_parse!(tag!("3") >> ( Timer(3) )) - | do_parse!(tag!("4") >> ( Timer(4) )) - | do_parse!(tag!("5") >> ( Timer(5) )) - | do_parse!(tag!("p") >> x: be_i24 >> y: be_i24 >> ( Put(x, y) )) - | do_parse!(tag!("P") >> x: be_i24 >> y: be_i24 >> ( CursorMove(x, y) )) - | do_parse!(tag!("f") >> s: string_tail >> ( SyncedEngineMessage::TeamControlLost(s) )) - | do_parse!(tag!("g") >> s: string_tail >> ( SyncedEngineMessage::TeamControlGained(s) )) - | do_parse!(tag!("t") >> t: be_u8 >> ( Taunt(t) )) - | do_parse!(tag!("w") >> w: be_u8 >> ( SetWeapon(w) )) - | do_parse!(tag!("~") >> s: be_u8 >> ( Slot(s) )) - | do_parse!(tag!("+") >> ( Heartbeat )) -)); +fn synced_message(input: &[u8]) -> IResult<&[u8], SyncedEngineMessage> { + alt(( + alt(( + map(tag(b"L"), |_| Left(Press)), + map(tag(b"l"), |_| Left(Release)), + map(tag(b"R"), |_| Right(Press)), + map(tag(b"r"), |_| Right(Release)), + map(tag(b"U"), |_| Up(Press)), + map(tag(b"u"), |_| Up(Release)), + map(tag(b"D"), |_| Down(Press)), + map(tag(b"d"), |_| Down(Release)), + map(tag(b"Z"), |_| Precise(Press)), + map(tag(b"z"), |_| Precise(Release)), + map(tag(b"A"), |_| Attack(Press)), + map(tag(b"a"), |_| Attack(Release)), + map(tag(b"N"), |_| NextTurn), + map(tag(b"j"), |_| LongJump), + map(tag(b"J"), |_| HighJump), + map(tag(b"S"), |_| Switch), + )), + alt(( + map(tag(b","), |_| Skip), + map(tag(b"1"), |_| Timer(1)), + map(tag(b"2"), |_| Timer(2)), + map(tag(b"3"), |_| Timer(3)), + map(tag(b"4"), |_| Timer(4)), + map(tag(b"5"), |_| Timer(5)), + map(tuple((tag(b"p"), be_i24, be_i24)), |(_, x, y)| Put(x, y)), + map(tuple((tag(b"P"), be_i24, be_i24)), |(_, x, y)| { + CursorMove(x, y) + }), + map(preceded(tag(b"f"), string_tail), TeamControlLost), + map(preceded(tag(b"g"), string_tail), TeamControlGained), + map(preceded(tag(b"t"), be_u8), Taunt), + map(preceded(tag(b"w"), be_u8), SetWeapon), + map(preceded(tag(b"~"), be_u8), Slot), + map(tag(b"+"), |_| Heartbeat), + )), + ))(input) +} -named!(unsynced_message<&[u8], UnsyncedEngineMessage>, alt!( - do_parse!(tag!("F") >> s: string_tail >> ( UnsyncedEngineMessage::TeamControlLost(s) )) - | do_parse!(tag!("G") >> s: string_tail >> ( UnsyncedEngineMessage::TeamControlGained(s) )) - | do_parse!(tag!("h") >> s: string_tail >> ( UnsyncedEngineMessage::HogSay(s) )) - | do_parse!(tag!("s") >> s: string_tail >> ( UnsyncedEngineMessage::ChatMessage(s)) ) - | do_parse!(tag!("b") >> s: string_tail >> ( UnsyncedEngineMessage::TeamMessage(s)) ) // TODO: wtf is the format -)); +fn unsynced_message(input: &[u8]) -> IResult<&[u8], UnsyncedEngineMessage> { + alt(( + map( + preceded(tag(b"F"), string_tail), + UnsyncedEngineMessage::TeamControlLost, + ), + map( + preceded(tag(b"G"), string_tail), + UnsyncedEngineMessage::TeamControlGained, + ), + map( + preceded(tag(b"h"), string_tail), + UnsyncedEngineMessage::HogSay, + ), + map( + preceded(tag(b"s"), string_tail), + UnsyncedEngineMessage::ChatMessage, + ), + map( + preceded(tag(b"b"), string_tail), + UnsyncedEngineMessage::TeamMessage, + ), + ))(input) +} -named!(unordered_message<&[u8], UnorderedEngineMessage>, alt!( - do_parse!(tag!("?") >> ( Ping )) - | do_parse!(tag!("!") >> ( Pong )) - | do_parse!(tag!("E") >> s: string_tail >> ( UnorderedEngineMessage::Error(s)) ) - | do_parse!(tag!("W") >> s: string_tail >> ( Warning(s)) ) - | do_parse!(tag!("M") >> s: string_tail >> ( GameSetupChecksum(s)) ) - | do_parse!(tag!("o") >> ( StopSyncing )) - | do_parse!(tag!("I") >> ( PauseToggled )) -)); - -named!(config_message<&[u8], ConfigEngineMessage>, alt!( - do_parse!(tag!("C") >> (ConfigRequest)) - | do_parse!(tag!("eseed ") >> s: string_tail >> ( SetSeed(s)) ) -)); - -named!(timestamped_message<&[u8], (SyncedEngineMessage, u16)>, - do_parse!(msg: length_value!(length_without_timestamp, terminated!(synced_message, eof_slice!())) - >> timestamp: be_u16 - >> ((msg, timestamp)) - ) -); +fn unordered_message(input: &[u8]) -> IResult<&[u8], UnorderedEngineMessage> { + alt(( + map(tag(b"?"), |_| Ping), + map(tag(b"!"), |_| Pong), + map(preceded(tag(b"E"), string_tail), Error), + map(preceded(tag(b"W"), string_tail), Warning), + map(preceded(tag(b"M"), string_tail), GameSetupChecksum), + map(tag(b"o"), |_| StopSyncing), + map(tag(b"I"), |_| PauseToggled), + ))(input) +} -named!(unwrapped_message<&[u8], EngineMessage>, - alt!( - map!(timestamped_message, |(m, t)| Synced(m, t as u32)) - | do_parse!(tag!("#") >> (Synced(TimeWrap, 65535))) - | map!(unordered_message, |m| Unordered(m)) - | map!(unsynced_message, |m| Unsynced(m)) - | map!(config_message, |m| Config(m)) - | unrecognized_message -)); +fn config_message(input: &[u8]) -> IResult<&[u8], ConfigEngineMessage> { + alt(( + map(tag(b"C"), |_| ConfigRequest), + map(preceded(tag(b"eseed "), string_tail), SetSeed), + map(preceded(tag(b"e$feature_size "), string_tail), |s| { + SetFeatureSize(s.parse().unwrap_or_default()) + }), + ))(input) +} -named!(length_specifier<&[u8], u16>, alt!( - verify!(map!(take!(1), |a : &[u8]| a[0] as u16), |l| l < 64) - | map!(take!(2), |a| (a[0] as u16 - 64) * 256 + a[1] as u16 + 64) - ) -); - -named!(empty_message<&[u8], EngineMessage>, - do_parse!(tag!("\0") >> (Empty)) -); +fn timestamped_message(input: &[u8]) -> IResult<&[u8], (SyncedEngineMessage, u16)> { + terminated(pair(synced_message, be_u16), eof_slice)(input) +} +fn unwrapped_message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + alt(( + map(timestamped_message, |(m, t)| { + EngineMessage::Synced(m, t as u32) + }), + map(tag(b"#"), |_| Synced(TimeWrap, 65535u32)), + map(unordered_message, Unordered), + map(unsynced_message, Unsynced), + map(config_message, Config), + unrecognized_message, + ))(input) +} -named!(non_empty_message<&[u8], EngineMessage>, - length_value!(length_specifier, terminated!(unwrapped_message, eof_slice!()))); +fn length_specifier(input: &[u8]) -> IResult<&[u8], u16> { + alt(( + verify(map(take(1usize), |a: &[u8]| a[0] as u16), |&l| l < 64), + map(take(2usize), |a: &[u8]| { + (a[0] as u16 - 64) * 256 + a[1] as u16 + 64 + }), + ))(input) +} -named!(message<&[u8], EngineMessage>, alt!( - empty_message - | non_empty_message - ) -); +fn empty_message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + map(tag(b"\0"), |_| Empty)(input) +} + +fn non_empty_message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + map_parser(length_data(length_specifier), unwrapped_message)(input) +} -named!(pub extract_messages<&[u8], Vec >, many0!(complete!(message))); +fn message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + alt((empty_message, non_empty_message))(input) +} + +pub fn extract_messages(input: &[u8]) -> IResult<&[u8], Vec> { + many0(complete(message))(input) +} pub fn extract_message(buf: &[u8]) -> Option<(usize, EngineMessage)> { let parse_result = message(buf); @@ -177,10 +221,13 @@ #[test] fn parse_incorrect_messages() { assert_eq!(message(b"\x00"), Ok((&b""[..], Empty))); - assert_eq!(message(b"\x01\x00"), Ok((&b""[..], Unknown))); + assert_eq!(message(b"\x01\x00"), Ok((&b""[..], Unknown(vec![0])))); // garbage after correct message - assert_eq!(message(b"\x04La\x01\x02"), Ok((&b""[..], Unknown))); + assert_eq!( + message(b"\x04La\x01\x02"), + Ok((&b""[..], Unknown(vec![76, 97, 1, 2]))) + ); } #[test] @@ -193,6 +240,9 @@ assert_eq!(string_tail(b"abc"), Ok((&b""[..], String::from("abc")))); assert_eq!(extract_message(b"\x02#"), None); + + assert_eq!(synced_message(b"L"), Ok((&b""[..], Left(Press)))); + assert_eq!( extract_message(b"\x01#"), Some((2, Synced(TimeWrap, 65535))) diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-engine/Cargo.toml --- a/rust/hedgewars-engine/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-engine/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -7,4 +7,4 @@ [dependencies] lib-hedgewars-engine = { path = "../lib-hedgewars-engine" } libloading = "0.5.0" - +getopts = "0.2" diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-engine/src/main.rs --- a/rust/hedgewars-engine/src/main.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-engine/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -2,13 +2,81 @@ use libloading::{Library, Symbol}; use std::ops::Deref; +use std::cmp::{min,max}; +use std::env; +use getopts::Options; +use std::io::prelude::*; +use std::io::{self, Read}; +use std::net::{Shutdown, TcpStream}; struct EngineInstance {} struct Engine<'a> { protocol_version: Symbol<'a, unsafe fn() -> u32>, start_engine: Symbol<'a, unsafe fn() -> *mut EngineInstance>, + generate_preview: Symbol<'a, unsafe fn(engine_state: &mut EngineInstance, preview: &mut PreviewInfo)>, + dispose_preview: Symbol<'a, unsafe fn(engine_state: &mut EngineInstance, preview: &mut PreviewInfo)>, cleanup: Symbol<'a, unsafe fn(engine_state: *mut EngineInstance)>, + send_ipc: Symbol<'a, unsafe fn(engine_state: &mut EngineInstance, buf: *const u8, size: usize)>, + read_ipc: Symbol<'a, unsafe fn(engine_state: &mut EngineInstance, buf: *mut u8, size: usize) -> usize>, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct PreviewInfo { + width: u32, + height: u32, + hedgehogs_number: u8, + land: *const u8, +} + +const PREVIEW_WIDTH: u32 = 256; +const PREVIEW_HEIGHT: u32 = 128; +const PREVIEW_NPIXELS: usize = (PREVIEW_WIDTH * PREVIEW_HEIGHT) as usize; +const SCALE_FACTOR: u32 = 16; +const VALUE_PER_INPIXEL: u8 = 1; + +/// Resizes the land preview from the library into appropriate format for --preview command. +/// +/// # Arguments +/// +/// * `mono_pixels` - Raw pixels of a land preview (monochrome, 0 = empty, else = filled) +/// * `in_width` - Width of the preview stored in `mono_pixels` +/// * `in_height` - Height of the preview stored in `mono_pixels` +/// * `preview_pixels` - Used as **output** for a resized and (kinda) anti-aliased grayscale preview +fn resize_mono_preview(mono_pixels: &[u8], in_width: u32, in_height: u32, preview_pixels: &mut [u8]) { + + assert!(mono_pixels.len() == (in_width * in_height) as usize); + + let v_offset: u32 = max(0, PREVIEW_HEIGHT as i64 - (in_height / SCALE_FACTOR) as i64) as u32; + let h_offset: u32 = max(0, (PREVIEW_WIDTH as i64 / 2) - (in_width / SCALE_FACTOR / 2) as i64) as u32; + + for y in v_offset..PREVIEW_HEIGHT { + + let in_y = v_offset + (y * SCALE_FACTOR); + + for x in h_offset..(PREVIEW_WIDTH - h_offset) { + + let in_x = h_offset + (x * SCALE_FACTOR); + + let out_px_address = (PREVIEW_WIDTH * y + x) as usize; + + let mut in_px_address = (in_width * in_y + in_x) as usize; + + let mut value = 0; + + for i in 0..SCALE_FACTOR as usize { + for j in 0..SCALE_FACTOR as usize { + if (value < 0xff) && (mono_pixels[in_px_address + j] != 0) { + value += VALUE_PER_INPIXEL; + } + } + in_px_address += in_width as usize; + } + + preview_pixels[out_px_address] = value; + } + } } fn main() { @@ -16,11 +84,104 @@ unsafe { let engine = Engine { - protocol_version: hwlib.get(b"protocol_version").unwrap(), + protocol_version: hwlib.get(b"hedgewars_engine_protocol_version").unwrap(), start_engine: hwlib.get(b"start_engine").unwrap(), + generate_preview: hwlib.get(b"generate_preview").unwrap(), + dispose_preview: hwlib.get(b"dispose_preview").unwrap(), cleanup: hwlib.get(b"cleanup").unwrap(), + send_ipc: hwlib.get(b"send_ipc").unwrap(), + read_ipc: hwlib.get(b"read_ipc").unwrap(), }; println!("Hedgewars engine, protocol version {}", engine.protocol_version.deref()()); + + let args: Vec = env::args().collect(); + + let mut opts = getopts::Options::new(); + opts.optflag("", "internal", "[internal]"); + opts.optflag("", "landpreview", "[internal]"); + opts.optflag("", "recorder", "[internal]"); + opts.optopt("", "port", "[internal]", "PORT"); + opts.optopt("", "user-prefix", "Set the path to the custom data folder to find game content", "PATH_TO_FOLDER"); + opts.optopt("", "prefix", "Set the path to the system game data folder", "PATH_TO_FOLDER"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => { m } + Err(f) => { panic!(f.to_string()) } + }; + + let engine_state = &mut *engine.start_engine.deref()(); + + let port: String = matches.opt_str("port").expect("Need IPC port number!"); + + println!("PORT: {}", port); + + if matches.opt_present("landpreview") { + + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Failed to connect to IPC port. Feelsbadman."); + + //stream.write(b"\x01C").unwrap(); // config + //stream.write(b"\x01?").unwrap(); // ping + + let mut buf = [0;1]; + loop { + let bytes_read = stream.read(&mut buf).unwrap(); + if bytes_read == 0 { + break; + } + engine.send_ipc.deref()(engine_state, &buf[0], buf.len()); + // this looks like length 1 is being announced + if buf[0] == 1 { + let bytes_read = stream.read(&mut buf).unwrap(); + if bytes_read == 0 { + break; + } + if buf[0] == 33 { + println!("Ping? Pong!"); + break; + } + } + }; + + let preview_info = &mut PreviewInfo { + width: 0, + height: 0, + hedgehogs_number: 0, + land: std::ptr::null(), + }; + + println!("Generating preview..."); + + engine.generate_preview.deref()(engine_state, preview_info); + + //println!("Preview: w = {}, h = {}, n = {}", preview_info.width, preview_info.height, preview_info.hedgehogs_number); + + let land_size: usize = (preview_info.width * preview_info.height) as usize; + + let land_array: &[u8] = std::slice::from_raw_parts(preview_info.land, land_size); + + const PREVIEW_WIDTH: u32 = 256; + const PREVIEW_HEIGHT: u32 = 128; + + println!("Resizing preview..."); + + let preview_image: &mut [u8] = &mut [0; PREVIEW_NPIXELS]; + resize_mono_preview(land_array, preview_info.width, preview_info.height, preview_image); + + println!("Sending preview..."); + + stream.write(preview_image).unwrap(); + stream.flush().unwrap(); + stream.write(&[preview_info.hedgehogs_number]).unwrap(); + stream.flush().unwrap(); + + println!("Preview sent, disconnect"); + + stream.shutdown(Shutdown::Both).expect("IPC shutdown call failed"); + + engine.dispose_preview.deref()(engine_state, preview_info); + } + + engine.cleanup.deref()(engine_state); } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,13 @@ +[package] +name = "hedgewars-network-protocol" +version = "0.1.0" +authors = ["Andrey Korotaev "] +edition = "2021" + +[dependencies] +nom = "7.1" +serde_derive = "1.0" +serde = "1.0" + +[dev-dependencies] +proptest = "1.0" \ No newline at end of file diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/lib.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,5 @@ +pub mod messages; +pub mod parser; +#[cfg(test)] +mod tests; +pub mod types; diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/messages.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/messages.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,423 @@ +use crate::types::{GameCfg, ServerVar, TeamInfo, VoteType}; +use std::iter::once; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum HwProtocolMessage { + // common messages + Ping, + Pong, + Quit(Option), + Global(String), + Watch(u32), + ToggleServerRegisteredOnly, + SuperPower, + Info(String), + // anteroom messages + Nick(String), + Proto(u16), + Password(String, String), + Checker(u16, String, String), + // lobby messages + List, + Chat(String), + CreateRoom(String, Option), + JoinRoom(String, Option), + Follow(String), + Rnd(Vec), + Kick(String), + Ban(String, String, u32), + BanIp(String, String, u32), + BanNick(String, String, u32), + BanList, + Unban(String), + SetServerVar(ServerVar), + GetServerVar, + RestartServer, + Stats, + // room messages + Part(Option), + Cfg(GameCfg), + AddTeam(Box), + RemoveTeam(String), + SetHedgehogsNumber(String, u8), + SetTeamColor(String, u8), + ToggleReady, + StartGame, + EngineMessage(String), + RoundFinished, + ToggleRestrictJoin, + ToggleRestrictTeams, + ToggleRegisteredOnly, + RoomName(String), + Delegate(String), + TeamChat(String), + MaxTeams(u8), + Fix, + Unfix, + Greeting(Option), + CallVote(Option), + Vote(bool), + ForceVote(bool), + Save(String, String), + Delete(String), + SaveRoom(String), + LoadRoom(String), + CheckerReady, + CheckedOk(Vec), + CheckedFail(String), +} + +#[derive(Debug, Clone, Copy)] +pub enum ProtocolFlags { + InRoom, + RoomMaster, + Ready, + InGame, + Registered, + Admin, + Contributor, +} + +impl ProtocolFlags { + #[inline] + fn flag_char(&self) -> char { + match self { + ProtocolFlags::InRoom => 'i', + ProtocolFlags::RoomMaster => 'h', + ProtocolFlags::Ready => 'r', + ProtocolFlags::InGame => 'g', + ProtocolFlags::Registered => 'u', + ProtocolFlags::Admin => 'a', + ProtocolFlags::Contributor => 'c', + } + } + + #[inline] + fn format(prefix: char, flags: &[ProtocolFlags]) -> String { + once(prefix) + .chain(flags.iter().map(|f| f.flag_char())) + .collect() + } +} + +#[inline] +pub fn add_flags(flags: &[ProtocolFlags]) -> String { + ProtocolFlags::format('+', flags) +} + +#[inline] +pub fn remove_flags(flags: &[ProtocolFlags]) -> String { + ProtocolFlags::format('-', flags) +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum HwServerMessage { + Connected(String, u32), + Redirect(u16), + + Ping, + Pong, + Bye(String), + + Nick(String), + Proto(u16), + AskPassword(String), + ServerAuth(String), + LogonPassed, + + LobbyLeft(String, String), + LobbyJoined(Vec), + ChatMsg { nick: String, msg: String }, + ClientFlags(String, Vec), + Rooms(Vec), + RoomAdd(Vec), + RoomJoined(Vec), + RoomLeft(String, String), + RoomRemove(String), + RoomUpdated(String, Vec), + Joining(String), + TeamAdd(Vec), + TeamRemove(String), + TeamAccepted(String), + TeamColor(String, u8), + HedgehogsNumber(String, u8), + ConfigEntry(String, Vec), + Kicked, + RunGame, + ForwardEngineMessage(Vec), + RoundFinished, + ReplayStart, + + Info(Vec), + ServerMessage(String), + ServerVars(Vec), + Notice(String), + Warning(String), + Error(String), + + Replay(Vec), + + //Deprecated messages + LegacyReady(bool, Vec), +} + +fn special_chat(nick: &str, msg: String) -> HwServerMessage { + HwServerMessage::ChatMsg { + nick: nick.to_string(), + msg, + } +} + +pub fn server_chat(msg: String) -> HwServerMessage { + special_chat("[server]", msg) +} + +pub fn global_chat(msg: String) -> HwServerMessage { + special_chat("(global notice)", msg) +} + +impl ServerVar { + pub fn to_protocol(&self) -> Vec { + use ServerVar::*; + match self { + MOTDNew(s) => vec!["MOTD_NEW".to_string(), s.clone()], + MOTDOld(s) => vec!["MOTD_OLD".to_string(), s.clone()], + LatestProto(n) => vec!["LATEST_PROTO".to_string(), n.to_string()], + } + } +} + +impl VoteType { + pub fn to_protocol(&self) -> Vec { + use VoteType::*; + match self { + Kick(nick) => vec!["KICK".to_string(), nick.clone()], + Map(None) => vec!["MAP".to_string()], + Map(Some(name)) => vec!["MAP".to_string(), name.clone()], + Pause => vec!["PAUSE".to_string()], + NewSeed => vec!["NEWSEED".to_string()], + HedgehogsPerTeam(count) => vec!["HEDGEHOGS".to_string(), count.to_string()], + } + } +} + +impl GameCfg { + pub fn to_protocol(&self) -> (String, Vec) { + use GameCfg::*; + match self { + FeatureSize(s) => ("FEATURE_SIZE".to_string(), vec![s.to_string()]), + MapType(t) => ("MAP".to_string(), vec![t.to_string()]), + MapGenerator(g) => ("MAPGEN".to_string(), vec![g.to_string()]), + MazeSize(s) => ("MAZE_SIZE".to_string(), vec![s.to_string()]), + Seed(s) => ("SEED".to_string(), vec![s.to_string()]), + Template(t) => ("TEMPLATE".to_string(), vec![t.to_string()]), + + Ammo(n, None) => ("AMMO".to_string(), vec![n.to_string()]), + Ammo(n, Some(s)) => ("AMMO".to_string(), vec![n.to_string(), s.to_string()]), + Scheme(n, s) if s.is_empty() => ("SCHEME".to_string(), vec![n.to_string()]), + Scheme(n, s) => ("SCHEME".to_string(), { + let mut v = vec![n.to_string()]; + v.extend(s.clone()); + v + }), + Script(s) => ("SCRIPT".to_string(), vec![s.to_string()]), + Theme(t) => ("THEME".to_string(), vec![t.to_string()]), + DrawnMap(m) => ("DRAWNMAP".to_string(), vec![m.to_string()]), + } + } + + pub fn to_server_msg(&self) -> HwServerMessage { + let (name, args) = self.to_protocol(); + HwServerMessage::ConfigEntry(name, args) + } +} + +impl TeamInfo { + pub fn to_protocol(&self) -> Vec { + let mut info = vec![ + self.name.clone(), + self.grave.clone(), + self.fort.clone(), + self.voice_pack.clone(), + self.flag.clone(), + self.owner.clone(), + self.difficulty.to_string(), + ]; + let hogs = self + .hedgehogs + .iter() + .flat_map(|h| once(h.name.clone()).chain(once(h.hat.clone()))); + info.extend(hogs); + info + } +} + +macro_rules! const_braces { + ($e: expr) => { + "{}\n" + }; +} + +macro_rules! msg { + [$($part: expr),*] => { + format!(concat!($(const_braces!($part)),*, "\n"), $($part),*) + }; +} + +impl HwProtocolMessage { + /** Converts the message to a raw `String`, which can be sent over the network. + * + * This is the inverse of the `message` parser. + */ + pub fn to_raw_protocol(&self) -> String { + use self::HwProtocolMessage::*; + match self { + Ping => msg!["PING"], + Pong => msg!["PONG"], + Quit(None) => msg!["QUIT"], + Quit(Some(msg)) => msg!["QUIT", msg], + Global(msg) => msg!["CMD", format!("GLOBAL {}", msg)], + Watch(name) => msg!["CMD", format!("WATCH {}", name)], + ToggleServerRegisteredOnly => msg!["CMD", "REGISTERED_ONLY"], + SuperPower => msg!["CMD", "SUPER_POWER"], + Info(info) => msg!["CMD", format!("INFO {}", info)], + Nick(nick) => msg!("NICK", nick), + Proto(version) => msg!["PROTO", version], + Password(p, s) => msg!["PASSWORD", p, s], + Checker(i, n, p) => msg!["CHECKER", i, n, p], + List => msg!["LIST"], + Chat(msg) => msg!["CHAT", msg], + CreateRoom(name, None) => msg!["CREATE_ROOM", name], + CreateRoom(name, Some(password)) => msg!["CREATE_ROOM", name, password], + JoinRoom(name, None) => msg!["JOIN_ROOM", name], + JoinRoom(name, Some(password)) => msg!["JOIN_ROOM", name, password], + Follow(name) => msg!["FOLLOW", name], + Rnd(args) => { + if args.is_empty() { + msg!["CMD", "RND"] + } else { + msg!["CMD", format!("RND {}", args.join(" "))] + } + } + Kick(name) => msg!["KICK", name], + Ban(name, reason, time) => msg!["BAN", name, reason, time], + BanIp(ip, reason, time) => msg!["BAN_IP", ip, reason, time], + BanNick(nick, reason, time) => msg!("BAN_NICK", nick, reason, time), + BanList => msg!["BANLIST"], + Unban(name) => msg!["UNBAN", name], + SetServerVar(var) => construct_message(&["SET_SERVER_VAR"], &var.to_protocol()), + GetServerVar => msg!["GET_SERVER_VAR"], + RestartServer => msg!["CMD", "RESTART_SERVER YES"], + Stats => msg!["CMD", "STATS"], + Part(None) => msg!["PART"], + Part(Some(msg)) => msg!["PART", msg], + Cfg(config) => { + let (name, args) = config.to_protocol(); + msg!["CFG", name, args.join("\n")] + } + AddTeam(info) => msg![ + "ADD_TEAM", + info.name, + info.color, + info.grave, + info.fort, + info.voice_pack, + info.flag, + info.difficulty, + &(info.hedgehogs.iter()) + .flat_map(|h| [&h.name[..], &h.hat[..]]) + .collect::>() + .join("\n") + ], + RemoveTeam(name) => msg!["REMOVE_TEAM", name], + SetHedgehogsNumber(team, number) => msg!["HH_NUM", team, number], + SetTeamColor(team, color) => msg!["TEAM_COLOR", team, color], + ToggleReady => msg!["TOGGLE_READY"], + StartGame => msg!["START_GAME"], + EngineMessage(msg) => msg!["EM", msg], + RoundFinished => msg!["ROUNDFINISHED"], + ToggleRestrictJoin => msg!["TOGGLE_RESTRICT_JOINS"], + ToggleRestrictTeams => msg!["TOGGLE_RESTRICT_TEAMS"], + ToggleRegisteredOnly => msg!["TOGGLE_REGISTERED_ONLY"], + RoomName(name) => msg!["ROOM_NAME", name], + Delegate(name) => msg!["CMD", format!("DELEGATE {}", name)], + TeamChat(msg) => msg!["TEAMCHAT", msg], + MaxTeams(count) => msg!["CMD", format!("MAXTEAMS {}", count)], + Fix => msg!["CMD", "FIX"], + Unfix => msg!["CMD", "UNFIX"], + Greeting(None) => msg!["CMD", "GREETING"], + Greeting(Some(msg)) => msg!["CMD", format!("GREETING {}", msg)], + CallVote(None) => msg!["CMD", "CALLVOTE"], + CallVote(Some(vote)) => { + msg!["CMD", format!("CALLVOTE {}", &vote.to_protocol().join(" "))] + } + Vote(msg) => msg!["CMD", format!("VOTE {}", if *msg { "YES" } else { "NO" })], + ForceVote(msg) => msg!["CMD", format!("FORCE {}", if *msg { "YES" } else { "NO" })], + Save(name, location) => msg!["CMD", format!("SAVE {} {}", name, location)], + Delete(name) => msg!["CMD", format!("DELETE {}", name)], + SaveRoom(name) => msg!["CMD", format!("SAVEROOM {}", name)], + LoadRoom(name) => msg!["CMD", format!("LOADROOM {}", name)], + CheckerReady => msg!["READY"], + CheckedOk(args) => msg!["CHECKED", "OK", args.join("\n")], + CheckedFail(message) => msg!["CHECKED", "FAIL", message], + } + } +} + +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(message, protocol_version) => msg!["CONNECTED", message, protocol_version], + Redirect(port) => msg!["REDIRECT", port], + Bye(msg) => msg!["BYE", msg], + Nick(nick) => msg!["NICK", nick], + Proto(proto) => msg!["PROTO", proto], + AskPassword(salt) => msg!["ASKPASSWORD", salt], + ServerAuth(hash) => msg!["SERVER_AUTH", hash], + LogonPassed => msg!["LOGONPASSED"], + LobbyLeft(nick, msg) => msg!["LOBBY:LEFT", nick, msg], + LobbyJoined(nicks) => construct_message(&["LOBBY:JOINED"], &nicks), + ClientFlags(flags, nicks) => construct_message(&["CLIENT_FLAGS", flags], &nicks), + Rooms(info) => construct_message(&["ROOMS"], &info), + RoomAdd(info) => construct_message(&["ROOM", "ADD"], &info), + RoomJoined(nicks) => construct_message(&["JOINED"], &nicks), + RoomLeft(nick, msg) => msg!["LEFT", nick, msg], + RoomRemove(name) => msg!["ROOM", "DEL", name], + RoomUpdated(name, info) => construct_message(&["ROOM", "UPD", name], &info), + Joining(name) => msg!["JOINING", name], + TeamAdd(info) => construct_message(&["ADD_TEAM"], &info), + TeamRemove(name) => msg!["REMOVE_TEAM", name], + TeamAccepted(name) => msg!["TEAM_ACCEPTED", name], + TeamColor(name, color) => msg!["TEAM_COLOR", name, color], + HedgehogsNumber(name, number) => msg!["HH_NUM", name, number], + ConfigEntry(name, values) => construct_message(&["CFG", name], &values), + Kicked => msg!["KICKED"], + RunGame => msg!["RUN_GAME"], + ForwardEngineMessage(em) => construct_message(&["EM"], &em), + RoundFinished => msg!["ROUND_FINISHED"], + ChatMsg { nick, msg } => msg!["CHAT", nick, msg], + Info(info) => construct_message(&["INFO"], &info), + ServerMessage(msg) => msg!["SERVER_MESSAGE", msg], + ServerVars(vars) => construct_message(&["SERVER_VARS"], &vars), + Notice(msg) => msg!["NOTICE", msg], + Warning(msg) => msg!["WARNING", msg], + Error(msg) => msg!["ERROR", msg], + ReplayStart => msg!["REPLAY_START"], + Replay(em) => construct_message(&["REPLAY"], &em), + + LegacyReady(is_ready, nicks) => { + construct_message(&[if *is_ready { "READY" } else { "NOT_READY" }], &nicks) + } + } + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/parser.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/parser.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,675 @@ +/** The parsers for the chat and multiplayer protocol. The main parser is `message`. + * # Protocol + * All messages consist of `\n`-separated strings. The end of a message is + * indicated by a double newline - `\n\n`. + * + * For example, a nullary command like PING will be actually sent as `PING\n\n`. + * A unary command, such as `START_GAME nick` will be actually sent as `START_GAME\nnick\n\n`. + */ +use nom::{ + branch::alt, + bytes::complete::{tag, tag_no_case, take_until, take_while}, + character::complete::{newline, not_line_ending}, + combinator::{map, peek}, + error::{ErrorKind, ParseError}, + multi::separated_list0, + sequence::{delimited, pair, preceded, terminated, tuple}, + Err, IResult, Parser +}; + +use std::{ + num::ParseIntError, + str, + str::{FromStr, Utf8Error}, +}; + +use crate::{ + messages::{HwProtocolMessage, HwProtocolMessage::*, HwServerMessage}, + types::{GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType}, +}; + +#[derive(Debug, PartialEq)] +pub struct HwProtocolError {} + +impl HwProtocolError { + pub fn new() -> Self { + HwProtocolError {} + } +} + +impl ParseError for HwProtocolError { + fn from_error_kind(_input: I, _kind: ErrorKind) -> Self { + HwProtocolError::new() + } + + fn append(_input: I, _kind: ErrorKind, _other: Self) -> Self { + HwProtocolError::new() + } +} + +impl From for HwProtocolError { + fn from(_: Utf8Error) -> Self { + HwProtocolError::new() + } +} + +impl From for HwProtocolError { + fn from(_: ParseIntError) -> Self { + HwProtocolError::new() + } +} + +pub type HwResult<'a, O> = IResult<&'a [u8], O, HwProtocolError>; + +fn end_of_message(input: &[u8]) -> HwResult<&[u8]> { + tag("\n\n")(input) +} + +fn convert_utf8(input: &[u8]) -> HwResult<&str> { + match str::from_utf8(input) { + Ok(str) => Ok((b"", str)), + Err(utf_err) => Result::Err(Err::Failure(utf_err.into())), + } +} + +fn convert_from_str(str: &str) -> HwResult +where + T: FromStr, +{ + match T::from_str(str) { + Ok(x) => Ok((b"", x)), + Err(format_err) => Result::Err(Err::Failure(format_err.into())), + } +} + +fn str_line(input: &[u8]) -> HwResult<&str> { + let (i, text) = not_line_ending(<&[u8]>::clone(&input))?; + if i != input { + Ok((i, convert_utf8(text)?.1)) + } else { + Err(Err::Error(HwProtocolError::new())) + } +} + +fn a_line(input: &[u8]) -> HwResult { + map(str_line, String::from)(input) +} + +fn cmd_arg(input: &[u8]) -> HwResult { + let delimiters = b" \n"; + let (i, str) = take_while(move |c| !delimiters.contains(&c))(<&[u8]>::clone(&input))?; + if i != input { + Ok((i, convert_utf8(str)?.1.to_string())) + } else { + Err(Err::Error(HwProtocolError::new())) + } +} + +fn u8_line(input: &[u8]) -> HwResult { + let (i, str) = str_line(input)?; + Ok((i, convert_from_str(str)?.1)) +} + +fn u16_line(input: &[u8]) -> HwResult { + let (i, str) = str_line(input)?; + Ok((i, convert_from_str(str)?.1)) +} + +fn u32_line(input: &[u8]) -> HwResult { + let (i, str) = str_line(input)?; + Ok((i, convert_from_str(str)?.1)) +} + +fn yes_no_line(input: &[u8]) -> HwResult { + alt(( + map(tag_no_case(b"YES"), |_| true), + map(tag_no_case(b"NO"), |_| false), + ))(input) +} + +fn opt_arg(input: &[u8]) -> HwResult> { + alt(( + map(peek(end_of_message), |_| None), + map(preceded(tag("\n"), a_line), Some), + ))(input) +} + +fn spaces(input: &[u8]) -> HwResult<&[u8]> { + preceded(tag(" "), take_while(|c| c == b' '))(input) +} + +fn opt_space_arg(input: &[u8]) -> HwResult> { + alt(( + map(peek(end_of_message), |_| None), + map(preceded(spaces, a_line), Some), + ))(input) +} + +fn hedgehog_array(input: &[u8]) -> HwResult<[HedgehogInfo; 8]> { + fn hedgehog_line(input: &[u8]) -> HwResult { + map( + tuple((terminated(a_line, newline), a_line)), + |(name, hat)| HedgehogInfo { name, hat }, + )(input) + } + + let (i, (h1, h2, h3, h4, h5, h6, h7, h8)) = tuple(( + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + terminated(hedgehog_line, newline), + hedgehog_line, + ))(input)?; + + Ok((i, [h1, h2, h3, h4, h5, h6, h7, h8])) +} + +fn voting(input: &[u8]) -> HwResult { + alt(( + map(tag_no_case("PAUSE"), |_| VoteType::Pause), + map(tag_no_case("NEWSEED"), |_| VoteType::NewSeed), + map( + preceded(pair(tag_no_case("KICK"), spaces), a_line), + VoteType::Kick, + ), + map( + preceded(pair(tag_no_case("HEDGEHOGS"), spaces), u8_line), + VoteType::HedgehogsPerTeam, + ), + map(preceded(tag_no_case("MAP"), opt_space_arg), VoteType::Map), + ))(input) +} + +fn no_arg_message(input: &[u8]) -> HwResult { + fn message( + name: &str, + msg: HwProtocolMessage, + ) -> impl Fn(&[u8]) -> HwResult + '_ { + move |i| map(tag(name), |_| msg.clone())(i) + } + + alt(( + message("PING", Ping), + message("PONG", Pong), + message("LIST", List), + message("BANLIST", BanList), + message("GET_SERVER_VAR", GetServerVar), + message("TOGGLE_READY", ToggleReady), + message("START_GAME", StartGame), + message("TOGGLE_RESTRICT_JOINS", ToggleRestrictJoin), + message("TOGGLE_RESTRICT_TEAMS", ToggleRestrictTeams), + message("TOGGLE_REGISTERED_ONLY", ToggleRegisteredOnly), + message("READY", CheckerReady), + ))(input) +} + +fn single_arg_message(input: &[u8]) -> HwResult { + fn message<'a, T: 'a, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult + '_ + where + F: Parser<&'a [u8], T, HwProtocolError> + 'a, + G: FnMut(T) -> HwProtocolMessage + 'a + { + map(preceded(tag(name), parser), constructor) + } + + alt(( + message("NICK\n", a_line, Nick), + message("INFO\n", a_line, Info), + message("CHAT\n", a_line, Chat), + message("PART", opt_arg, Part), + message("FOLLOW\n", a_line, Follow), + message("KICK\n", a_line, Kick), + message("UNBAN\n", a_line, Unban), + message("EM\n", a_line, EngineMessage), + message("TEAMCHAT\n", a_line, TeamChat), + message("ROOM_NAME\n", a_line, RoomName), + message("REMOVE_TEAM\n", a_line, RemoveTeam), + message("ROUNDFINISHED", opt_arg, |_| RoundFinished), + message("PROTO\n", u16_line, Proto), + message("QUIT", opt_arg, Quit), + message("CHECKED\nFAIL\n", a_line, CheckedFail), + ))(input) +} + +fn cmd_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { + fn cmd_no_arg( + name: &str, + msg: HwProtocolMessage, + ) -> impl Fn(&[u8]) -> HwResult + '_ { + move |i| map(tag_no_case(name), |_| msg.clone())(i) + } + + fn cmd_single_arg<'a, T, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage> + where + F: Fn(&'a [u8]) -> HwResult<'a, T>, + G: Fn(T) -> HwProtocolMessage, + { + map( + preceded(pair(tag_no_case(name), spaces), parser), + constructor, + ) + } + + fn cmd_no_arg_message(input: &[u8]) -> HwResult { + alt(( + cmd_no_arg("STATS", Stats), + cmd_no_arg("FIX", Fix), + cmd_no_arg("UNFIX", Unfix), + cmd_no_arg("REGISTERED_ONLY", ToggleServerRegisteredOnly), + cmd_no_arg("SUPER_POWER", SuperPower), + ))(input) + } + + fn cmd_single_arg_message(input: &[u8]) -> HwResult { + alt(( + cmd_single_arg("RESTART_SERVER", |i| tag("YES")(i), |_| RestartServer), + cmd_single_arg("DELEGATE", a_line, Delegate), + cmd_single_arg("DELETE", a_line, Delete), + cmd_single_arg("SAVEROOM", a_line, SaveRoom), + cmd_single_arg("LOADROOM", a_line, LoadRoom), + cmd_single_arg("GLOBAL", a_line, Global), + cmd_single_arg("WATCH", u32_line, Watch), + cmd_single_arg("VOTE", yes_no_line, Vote), + cmd_single_arg("FORCE", yes_no_line, ForceVote), + cmd_single_arg("INFO", a_line, Info), + cmd_single_arg("MAXTEAMS", u8_line, MaxTeams), + cmd_single_arg("CALLVOTE", voting, |v| CallVote(Some(v))), + ))(input) + } + + preceded( + tag("CMD\n"), + alt(( + cmd_no_arg_message, + cmd_single_arg_message, + map(tag_no_case("CALLVOTE"), |_| CallVote(None)), + map(preceded(tag_no_case("GREETING"), opt_space_arg), Greeting), + map(preceded(tag_no_case("PART"), opt_space_arg), Part), + map(preceded(tag_no_case("QUIT"), opt_space_arg), Quit), + map( + preceded( + tag_no_case("SAVE"), + pair(preceded(spaces, cmd_arg), preceded(spaces, cmd_arg)), + ), + |(n, l)| Save(n, l), + ), + map( + preceded( + tag_no_case("RND"), + alt(( + map(peek(end_of_message), |_| vec![]), + preceded(spaces, separated_list0(spaces, cmd_arg)), + )), + ), + Rnd, + ), + )), + )(input) +} + +fn config_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { + fn cfg_single_arg<'a, T: 'a, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult + '_ + where + F: Parser<&'a [u8], T, HwProtocolError> + 'a, + G: Fn(T) -> GameCfg + 'a, + { + map(preceded(pair(tag(name), newline), parser), constructor) + } + + let (i, cfg) = preceded( + tag("CFG\n"), + alt(( + cfg_single_arg("THEME", a_line, GameCfg::Theme), + cfg_single_arg("SCRIPT", a_line, GameCfg::Script), + cfg_single_arg("MAP", a_line, GameCfg::MapType), + cfg_single_arg("MAPGEN", u32_line, GameCfg::MapGenerator), + cfg_single_arg("MAZE_SIZE", u32_line, GameCfg::MazeSize), + cfg_single_arg("TEMPLATE", u32_line, GameCfg::Template), + cfg_single_arg("FEATURE_SIZE", u32_line, GameCfg::FeatureSize), + cfg_single_arg("SEED", a_line, GameCfg::Seed), + cfg_single_arg("DRAWNMAP", a_line, GameCfg::DrawnMap), + preceded(pair(tag("AMMO"), newline), |i| { + let (i, name) = a_line(i)?; + let (i, value) = opt_arg(i)?; + Ok((i, GameCfg::Ammo(name, value))) + }), + preceded( + pair(tag("SCHEME"), newline), + map( + pair( + a_line, + alt(( + map(peek(end_of_message), |_| None), + map(preceded(newline, separated_list0(newline, a_line)), Some), + )), + ), + |(name, values)| GameCfg::Scheme(name, values.unwrap_or_default()), + ), + ), + )), + )(input)?; + Ok((i, Cfg(cfg))) +} + +fn server_var_message(input: &[u8]) -> HwResult { + map( + preceded( + tag("SET_SERVER_VAR\n"), + alt(( + map(preceded(tag("MOTD_NEW\n"), a_line), ServerVar::MOTDNew), + map(preceded(tag("MOTD_OLD\n"), a_line), ServerVar::MOTDOld), + map( + preceded(tag("LATEST_PROTO\n"), u16_line), + ServerVar::LatestProto, + ), + )), + ), + SetServerVar, + )(input) +} + +fn complex_message(input: &[u8]) -> HwResult { + alt(( + preceded( + pair(tag("PASSWORD"), newline), + map(pair(terminated(a_line, newline), a_line), |(pass, salt)| { + Password(pass, salt) + }), + ), + preceded( + pair(tag("CHECKER"), newline), + map( + tuple(( + terminated(u16_line, newline), + terminated(a_line, newline), + a_line, + )), + |(protocol, name, pass)| Checker(protocol, name, pass), + ), + ), + preceded( + pair(tag("CREATE_ROOM"), newline), + map(pair(a_line, opt_arg), |(name, pass)| CreateRoom(name, pass)), + ), + preceded( + pair(tag("JOIN_ROOM"), newline), + map(pair(a_line, opt_arg), |(name, pass)| JoinRoom(name, pass)), + ), + preceded( + pair(tag("ADD_TEAM"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(u8_line, newline), + terminated(a_line, newline), + terminated(a_line, newline), + terminated(a_line, newline), + terminated(a_line, newline), + terminated(u8_line, newline), + hedgehog_array, + )), + |(name, color, grave, fort, voice_pack, flag, difficulty, hedgehogs)| { + AddTeam(Box::new(TeamInfo { + owner: String::new(), + name, + color, + grave, + fort, + voice_pack, + flag, + difficulty, + hedgehogs, + hedgehogs_number: 0, + })) + }, + ), + ), + preceded( + pair(tag("HH_NUM"), newline), + map( + pair(terminated(a_line, newline), u8_line), + |(name, count)| SetHedgehogsNumber(name, count), + ), + ), + preceded( + pair(tag("TEAM_COLOR"), newline), + map( + pair(terminated(a_line, newline), u8_line), + |(name, color)| SetTeamColor(name, color), + ), + ), + preceded( + pair(tag("BAN"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(a_line, newline), + u32_line, + )), + |(name, reason, time)| Ban(name, reason, time), + ), + ), + preceded( + pair(tag("BAN_IP"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(a_line, newline), + u32_line, + )), + |(ip, reason, time)| BanIp(ip, reason, time), + ), + ), + preceded( + pair(tag("BAN_NICK"), newline), + map( + tuple(( + terminated(a_line, newline), + terminated(a_line, newline), + u32_line, + )), + |(nick, reason, time)| BanNick(nick, reason, time), + ), + ), + map( + preceded( + tag("CHECKED\nOK"), + alt(( + map(peek(end_of_message), |_| None), + map(preceded(newline, separated_list0(newline, a_line)), Some), + )), + ), + |values| CheckedOk(values.unwrap_or_default()), + ), + ))(input) +} + +pub fn malformed_message(input: &[u8]) -> HwResult<()> { + map(terminated(take_until(&b"\n\n"[..]), end_of_message), |_| ())(input) +} + +pub fn message(input: &[u8]) -> HwResult { + delimited( + take_while(|c| c == b'\n'), + alt(( + no_arg_message, + single_arg_message, + cmd_message, + config_message, + server_var_message, + complex_message, + )), + end_of_message, + )(input) +} + +pub fn server_message(input: &[u8]) -> HwResult { + use HwServerMessage::*; + + fn single_arg_message<'a, T: 'a, F, G>( + name: &'a str, + parser: F, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult + '_ + where + F: Parser<&'a [u8], T, HwProtocolError> + 'a, + G: Fn(T) -> HwServerMessage + 'a, + { + map( + preceded(terminated(tag(name), newline), parser), + constructor, + ) + } + + fn list_message<'a, G>( + name: &'a str, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult + '_ + where + G: Fn(Vec) -> HwServerMessage + 'a, + { + map( + preceded( + tag(name), + alt(( + map(peek(end_of_message), |_| None), + map(preceded(newline, separated_list0(newline, a_line)), Some), + )), + ), + move |values| constructor(values.unwrap_or_default()), + ) + } + + fn string_and_list_message<'a, G>( + name: &'a str, + constructor: G, + ) -> impl FnMut(&'a [u8]) -> HwResult + '_ + where + G: Fn(String, Vec) -> HwServerMessage + 'a, + { + preceded( + pair(tag(name), newline), + map( + pair( + a_line, + alt(( + map(peek(end_of_message), |_| None), + map(preceded(newline, separated_list0(newline, a_line)), Some), + )), + ), + move |(name, values)| constructor(name, values.unwrap_or_default()), + ), + ) + } + + fn message( + name: &str, + msg: HwServerMessage, + ) -> impl Fn(&[u8]) -> HwResult + '_ { + move |i| map(tag(name), |_| msg.clone())(i) + } + + delimited( + take_while(|c| c == b'\n'), + alt(( + alt(( + message("PING", Ping), + message("PONG", Pong), + message("LOGONPASSED", LogonPassed), + message("KICKED", Kicked), + message("RUN_GAME", RunGame), + message("ROUND_FINISHED", RoundFinished), + message("REPLAY_START", ReplayStart), + )), + alt(( + single_arg_message("REDIRECT", u16_line, Redirect), + single_arg_message("BYE", a_line, Bye), + single_arg_message("NICK", a_line, Nick), + single_arg_message("PROTO", u16_line, Proto), + single_arg_message("ASKPASSWORD", a_line, AskPassword), + single_arg_message("SERVER_AUTH", a_line, ServerAuth), + single_arg_message("ROOM\nDEL", a_line, RoomRemove), + single_arg_message("JOINING", a_line, Joining), + single_arg_message("REMOVE_TEAM", a_line, TeamRemove), + single_arg_message("TEAM_ACCEPTED", a_line, TeamAccepted), + single_arg_message("SERVER_MESSAGE", a_line, ServerMessage), + single_arg_message("NOTICE", a_line, Notice), + single_arg_message("WARNING", a_line, Warning), + single_arg_message("ERROR", a_line, Error), + )), + alt(( + preceded( + pair(tag("LOBBY:LEFT"), newline), + map(pair(terminated(a_line, newline), a_line), |(nick, msg)| { + LobbyLeft(nick, msg) + }), + ), + preceded( + pair(tag("CHAT"), newline), + map(pair(terminated(a_line, newline), a_line), |(nick, msg)| { + ChatMsg { nick, msg } + }), + ), + preceded( + pair(tag("TEAM_COLOR"), newline), + map( + pair(terminated(a_line, newline), u8_line), + |(name, color)| TeamColor(name, color), + ), + ), + preceded( + pair(tag("HH_NUM"), newline), + map( + pair(terminated(a_line, newline), u8_line), + |(name, count)| HedgehogsNumber(name, count), + ), + ), + preceded( + pair(tag("CONNECTED"), newline), + map( + pair(terminated(a_line, newline), u32_line), + |(msg, server_protocol_version)| Connected(msg, server_protocol_version), + ), + ), + preceded( + pair(tag("LEFT"), newline), + map(pair(terminated(a_line, newline), a_line), |(nick, msg)| { + RoomLeft(nick, msg) + }), + ), + )), + alt(( + string_and_list_message("CLIENT_FLAGS", ClientFlags), + string_and_list_message("ROOM\nUPD", RoomUpdated), + string_and_list_message("CFG", ConfigEntry), + )), + alt(( + list_message("LOBBY:JOINED", LobbyJoined), + list_message("ROOMS", Rooms), + list_message("ROOM\nADD", RoomAdd), + list_message("JOINED", RoomJoined), + list_message("ADD_TEAM", TeamAdd), + list_message("EM", ForwardEngineMessage), + list_message("INFO", Info), + list_message("SERVER_VARS", ServerVars), + list_message("REPLAY", Replay), + )), + )), + end_of_message, + )(input) +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/tests/mod.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/tests/mod.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2 @@ +mod parser; +mod test; diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/tests/parser.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/tests/parser.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,76 @@ +use crate::{ + parser::HwProtocolError, + parser::{message, server_message}, + types::GameCfg, +}; + +#[test] +fn parse_test() { + use crate::messages::HwProtocolMessage::*; + + assert_eq!(message(b"PING\n\n"), Ok((&b""[..], Ping))); + assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame))); + assert_eq!( + message(b"NICK\nit's me\n\n"), + Ok((&b""[..], Nick("it's me".to_string()))) + ); + assert_eq!(message(b"PROTO\n51\n\n"), Ok((&b""[..], Proto(51)))); + assert_eq!( + message(b"QUIT\nbye-bye\n\n"), + Ok((&b""[..], Quit(Some("bye-bye".to_string())))) + ); + assert_eq!(message(b"QUIT\n\n"), Ok((&b""[..], Quit(None)))); + assert_eq!( + message(b"CMD\nwatch 49471\n\n"), + Ok((&b""[..], Watch(49471))) + ); + assert_eq!( + message(b"BAN\nme\nbad\n77\n\n"), + Ok((&b""[..], Ban("me".to_string(), "bad".to_string(), 77))) + ); + + assert_eq!(message(b"CMD\nPART\n\n"), Ok((&b""[..], Part(None)))); + assert_eq!( + message(b"CMD\nPART _msg_\n\n"), + Ok((&b""[..], Part(Some("_msg_".to_string())))) + ); + + assert_eq!(message(b"CMD\nRND\n\n"), Ok((&b""[..], Rnd(vec![])))); + assert_eq!( + message(b"CMD\nRND A B\n\n"), + Ok((&b""[..], Rnd(vec![String::from("A"), String::from("B")]))) + ); + + assert_eq!( + message(b"CFG\nSCHEME\na\nA\n\n"), + Ok(( + &b""[..], + Cfg(GameCfg::Scheme("a".to_string(), vec!["A".to_string()])) + )) + ); + + assert_eq!( + message(b"QUIT\n1\n2\n\n"), + Err(nom::Err::Error(HwProtocolError::new())) + ); +} + +#[test] +fn parse_server_messages_test() { + use crate::messages::HwServerMessage::*; + + assert_eq!(server_message(b"PING\n\n"), Ok((&b""[..], Ping))); + + assert_eq!( + server_message(b"JOINING\nnoone\n\n"), + Ok((&b""[..], Joining("noone".to_string()))) + ); + + assert_eq!( + server_message(b"CLIENT_FLAGS\naaa\nA\nB\n\n"), + Ok(( + &b""[..], + ClientFlags("aaa".to_string(), vec!["A".to_string(), "B".to_string()]) + )) + ) +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/tests/test.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/tests/test.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,325 @@ +use crate::{ + messages::{HwProtocolMessage, HwServerMessage}, + parser::{message, server_message}, + types::ServerVar::*, + types::*, + types::{GameCfg, ServerVar, TeamInfo, VoteType}, +}; + +use proptest::{ + arbitrary::{any, Arbitrary}, + proptest, + strategy::{BoxedStrategy, Just, Strategy}, +}; + +// Due to inability to define From between Options +pub trait Into2: Sized { + fn into2(self) -> T; +} +impl Into2 for T { + fn into2(self) -> T { + self + } +} +impl Into2> for Vec { + fn into2(self) -> Vec { + self.into_iter().map(|x| x.0).collect() + } +} +impl Into2 for Ascii { + fn into2(self) -> String { + self.0 + } +} +impl Into2> for Option { + fn into2(self) -> Option { + self.map(|x| x.0) + } +} + +#[macro_export] +macro_rules! proto_msg_case { + ($val: ident()) => { + Just($val) + }; + ($val: ident($arg: ty)) => { + any::<$arg>().prop_map(|v| $val(v.into2())) + }; + ($val: ident($arg1: ty, $arg2: ty)) => { + any::<($arg1, $arg2)>().prop_map(|v| $val(v.0.into2(), v.1.into2())) + }; + ($val: ident($arg1: ty, $arg2: ty, $arg3: ty)) => { + any::<($arg1, $arg2, $arg3)>().prop_map(|v| $val(v.0.into2(), v.1.into2(), v.2.into2())) + }; +} + +macro_rules! proto_msg_match { +($var: expr, def = $default: expr, $($num: expr => $constr: ident $res: tt),*) => ( + match $var { + $($num => (proto_msg_case!($constr $res)).boxed()),*, + _ => Just($default).boxed() + } +) +} + +/// Wrapper type for generating non-empty strings +#[derive(Debug)] +pub struct Ascii(String); + +impl Arbitrary for Ascii { + type Parameters = ::Parameters; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + "[a-zA-Z0-9]+".prop_map(Ascii).boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for GameCfg { + type Parameters = (); + + fn arbitrary_with(_args: ::Parameters) -> ::Strategy { + use crate::types::GameCfg::*; + (0..10) + .no_shrink() + .prop_flat_map(|i| { + proto_msg_match!(i, def = FeatureSize(0), + 0 => FeatureSize(u32), + 1 => MapType(Ascii), + 2 => MapGenerator(u32), + 3 => MazeSize(u32), + 4 => Seed(Ascii), + 5 => Template(u32), + 6 => Ammo(Ascii, Option), + 7 => Scheme(Ascii, Vec), + 8 => Script(Ascii), + 9 => Theme(Ascii), + 10 => DrawnMap(Ascii)) + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for TeamInfo { + type Parameters = (); + + fn arbitrary_with(_args: ::Parameters) -> ::Strategy { + ( + "[a-z]+", + 0u8..127u8, + "[a-z]+", + "[a-z]+", + "[a-z]+", + "[a-z]+", + 0u8..127u8, + ) + .prop_map(|(name, color, grave, fort, voice_pack, flag, difficulty)| { + fn hog(n: u8) -> HedgehogInfo { + HedgehogInfo { + name: format!("hog{}", n), + hat: format!("hat{}", n), + } + } + let hedgehogs = [ + hog(1), + hog(2), + hog(3), + hog(4), + hog(5), + hog(6), + hog(7), + hog(8), + ]; + TeamInfo { + owner: String::new(), + name, + color, + grave, + fort, + voice_pack, + flag, + difficulty, + hedgehogs, + hedgehogs_number: 0, + } + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for ServerVar { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (0..=2) + .no_shrink() + .prop_flat_map(|i| { + proto_msg_match!(i, def = ServerVar::LatestProto(0), + 0 => MOTDNew(Ascii), + 1 => MOTDOld(Ascii), + 2 => LatestProto(u16) + ) + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +impl Arbitrary for VoteType { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use VoteType::*; + (0..=4) + .no_shrink() + .prop_flat_map(|i| { + proto_msg_match!(i, def = VoteType::Pause, + 0 => Kick(Ascii), + 1 => Map(Option), + 2 => Pause(), + 3 => NewSeed(), + 4 => HedgehogsPerTeam(u8) + ) + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +pub fn gen_proto_msg() -> BoxedStrategy where { + use HwProtocolMessage::*; + + let res = (0..=58).no_shrink().prop_flat_map(|i| { + proto_msg_match!(i, def = Ping, + 0 => Ping(), + 1 => Pong(), + 2 => Quit(Option), + 4 => Global(Ascii), + 5 => Watch(u32), + 6 => ToggleServerRegisteredOnly(), + 7 => SuperPower(), + 8 => Info(Ascii), + 9 => Nick(Ascii), + 10 => Proto(u16), + 11 => Password(Ascii, Ascii), + 12 => Checker(u16, Ascii, Ascii), + 13 => List(), + 14 => Chat(Ascii), + 15 => CreateRoom(Ascii, Option), + 16 => JoinRoom(Ascii, Option), + 17 => Follow(Ascii), + 18 => Rnd(Vec), + 19 => Kick(Ascii), + 20 => Ban(Ascii, Ascii, u32), + 21 => BanIp(Ascii, Ascii, u32), + 22 => BanNick(Ascii, Ascii, u32), + 23 => BanList(), + 24 => Unban(Ascii), + 25 => SetServerVar(ServerVar), + 26 => GetServerVar(), + 27 => RestartServer(), + 28 => Stats(), + 29 => Part(Option), + 30 => Cfg(GameCfg), + 31 => AddTeam(Box), + 32 => RemoveTeam(Ascii), + 33 => SetHedgehogsNumber(Ascii, u8), + 34 => SetTeamColor(Ascii, u8), + 35 => ToggleReady(), + 36 => StartGame(), + 37 => EngineMessage(Ascii), + 38 => RoundFinished(), + 39 => ToggleRestrictJoin(), + 40 => ToggleRestrictTeams(), + 41 => ToggleRegisteredOnly(), + 42 => RoomName(Ascii), + 43 => Delegate(Ascii), + 44 => TeamChat(Ascii), + 45 => MaxTeams(u8), + 46 => Fix(), + 47 => Unfix(), + 48 => Greeting(Option), + 49 => CallVote(Option), + 50 => Vote(bool), + 51 => ForceVote(bool), + 52 => Save(Ascii, Ascii), + 53 => Delete(Ascii), + 54 => SaveRoom(Ascii), + 55 => LoadRoom(Ascii), + 56 => CheckerReady(), + 57 => CheckedOk(Vec), + 58 => CheckedFail(Ascii) + ) + }); + res.boxed() +} + +pub fn gen_server_msg() -> BoxedStrategy where { + use HwServerMessage::*; + + let res = (0..=38).no_shrink().prop_flat_map(|i| { + proto_msg_match!(i, def = Ping, + 0 => Connected(Ascii, u32), + 1 => Redirect(u16), + 2 => Ping(), + 3 => Pong(), + 4 => Bye(Ascii), + 5 => Nick(Ascii), + 6 => Proto(u16), + 7 => AskPassword(Ascii), + 8 => ServerAuth(Ascii), + 9 => LogonPassed(), + 10 => LobbyLeft(Ascii, Ascii), + 11 => LobbyJoined(Vec), + // 12 => ChatMsg { Ascii, Ascii }, + 13 => ClientFlags(Ascii, Vec), + 14 => Rooms(Vec), + 15 => RoomAdd(Vec), + 16=> RoomJoined(Vec), + 17 => RoomLeft(Ascii, Ascii), + 18 => RoomRemove(Ascii), + 19 => RoomUpdated(Ascii, Vec), + 20 => Joining(Ascii), + 21 => TeamAdd(Vec), + 22 => TeamRemove(Ascii), + 23 => TeamAccepted(Ascii), + 24 => TeamColor(Ascii, u8), + 25 => HedgehogsNumber(Ascii, u8), + 26 => ConfigEntry(Ascii, Vec), + 27 => Kicked(), + 28 => RunGame(), + 29 => ForwardEngineMessage(Vec), + 30 => RoundFinished(), + 31 => ReplayStart(), + 32 => Info(Vec), + 33 => ServerMessage(Ascii), + 34 => ServerVars(Vec), + 35 => Notice(Ascii), + 36 => Warning(Ascii), + 37 => Error(Ascii), + 38 => Replay(Vec) + ) + }); + res.boxed() +} + +proptest! { + #[test] + fn is_parser_composition_idempotent(ref msg in gen_proto_msg()) { + println!("!! Msg: {:?}, Bytes: {:?} !!", msg, msg.to_raw_protocol().as_bytes()); + assert_eq!(message(msg.to_raw_protocol().as_bytes()), Ok((&b""[..], msg.clone()))) + } + + #[test] + fn is_server_message_parser_composition_idempotent(ref msg in gen_server_msg()) { + println!("!! Msg: {:?}, Bytes: {:?} !!", msg, msg.to_raw_protocol().as_bytes()); + assert_eq!(server_message(msg.to_raw_protocol().as_bytes()), Ok((&b""[..], msg.clone()))) + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-network-protocol/src/types.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-network-protocol/src/types.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,165 @@ +use serde_derive::{Deserialize, Serialize}; + +pub const MAX_HEDGEHOGS_PER_TEAM: u8 = 8; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum ServerVar { + MOTDNew(String), + MOTDOld(String), + LatestProto(u16), +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum GameCfg { + FeatureSize(u32), + MapType(String), + MapGenerator(u32), + MazeSize(u32), + Seed(String), + Template(u32), + + Ammo(String, Option), + Scheme(String, Vec), + Script(String), + Theme(String), + DrawnMap(String), +} + +#[derive(PartialEq, Eq, Clone, Debug, Default)] +pub struct TeamInfo { + pub owner: String, + pub name: String, + pub color: u8, + pub grave: String, + pub fort: String, + pub voice_pack: String, + pub flag: String, + pub difficulty: u8, + pub hedgehogs_number: u8, + pub hedgehogs: [HedgehogInfo; MAX_HEDGEHOGS_PER_TEAM as usize], +} + +#[derive(PartialEq, Eq, Clone, Debug, Default)] +pub struct HedgehogInfo { + pub name: String, + pub hat: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Ammo { + pub name: String, + pub settings: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Scheme { + pub name: String, + pub settings: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct RoomConfig { + pub feature_size: u32, + pub map_type: String, + pub map_generator: u32, + pub maze_size: u32, + pub seed: String, + pub template: u32, + + pub ammo: Ammo, + pub scheme: Scheme, + pub script: String, + pub theme: String, + pub drawn_map: Option, +} + +impl RoomConfig { + pub fn new() -> RoomConfig { + RoomConfig { + feature_size: 12, + map_type: "+rnd+".to_string(), + map_generator: 0, + maze_size: 0, + seed: "seed".to_string(), + template: 0, + + ammo: Ammo { + name: "Default".to_string(), + settings: None, + }, + scheme: Scheme { + name: "Default".to_string(), + settings: Vec::new(), + }, + script: "Normal".to_string(), + theme: "\u{1f994}".to_string(), + drawn_map: None, + } + } + + pub fn set_config(&mut self, cfg: GameCfg) { + match cfg { + GameCfg::FeatureSize(s) => self.feature_size = s, + GameCfg::MapType(t) => self.map_type = t, + GameCfg::MapGenerator(g) => self.map_generator = g, + GameCfg::MazeSize(s) => self.maze_size = s, + GameCfg::Seed(s) => self.seed = s, + GameCfg::Template(t) => self.template = t, + + GameCfg::Ammo(n, s) => { + self.ammo = Ammo { + name: n, + settings: s, + } + } + GameCfg::Scheme(n, s) => { + self.scheme = Scheme { + name: n, + settings: s, + } + } + GameCfg::Script(s) => self.script = s, + GameCfg::Theme(t) => self.theme = t, + GameCfg::DrawnMap(m) => self.drawn_map = Some(m), + }; + } + + pub fn to_map_config(&self) -> Vec { + vec![ + self.feature_size.to_string(), + self.map_type.to_string(), + self.map_generator.to_string(), + self.maze_size.to_string(), + self.seed.to_string(), + self.template.to_string(), + ] + } + + pub fn to_game_config(&self) -> Vec { + use GameCfg::*; + let mut v = vec![ + Ammo(self.ammo.name.to_string(), self.ammo.settings.clone()), + Scheme(self.scheme.name.to_string(), self.scheme.settings.clone()), + Script(self.script.to_string()), + Theme(self.theme.to_string()), + ]; + if let Some(ref m) = self.drawn_map { + v.push(DrawnMap(m.to_string())) + } + v + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum VoteType { + Kick(String), + Map(Option), + Pause, + NewSeed, + HedgehogsPerTeam(u8), +} + +pub struct Vote { + pub is_pro: bool, + pub is_forced: bool, +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/Cargo.toml --- a/rust/hedgewars-server/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -1,31 +1,34 @@ [package] -edition = "2018" +edition = "2021" name = "hedgewars-server" -version = "0.0.1" +version = "0.9.0" authors = [ "Andrey Korotaev " ] [features] -official-server = ["openssl", "mysql"] -tls-connections = ["openssl"] +tls-connections = ["tokio-native-tls"] +official-server = ["mysql_async", "sha1", "tls-connections"] default = [] [dependencies] -getopts = "0.2.18" -rand = "0.6" -mio = "0.6" -mio-extras = "2.0.5" -slab = "0.4" -netbuf = "0.4" -nom = "5.0" -env_logger = "0.6" +base64 = "0.13" +bitflags = "1.3" +bytes = "1.1" +chrono = "0.4" +env_logger = "0.8" +getopts = "0.2" log = "0.4" -base64 = "0.10" -bitflags = "1.0" +mysql_async = { version = "0.29.0", optional = true } +nom = "7.1" +rand = "0.8" serde = "1.0" serde_yaml = "0.8" serde_derive = "1.0" -openssl = { version = "0.10", optional = true } -mysql = { version = "15.0", optional = true } +sha1 = { version = "0.10.0", optional = true } +slab = "0.4" +tokio = { version = "1.36", features = ["full"]} +tokio-native-tls = { version = "0.3", optional = true } + +hedgewars-network-protocol = { path = "../hedgewars-network-protocol" } [dev-dependencies] -proptest = "0.9" +proptest = "1.0" diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/core.rs --- a/rust/hedgewars-server/src/core.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/core.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,3 +1,4 @@ +pub mod anteroom; pub mod client; pub mod indexslab; pub mod room; diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/core/anteroom.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/core/anteroom.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,89 @@ +use super::{indexslab::IndexSlab, types::ClientId}; +use chrono::{offset, DateTime}; +use std::{iter::Iterator, num::NonZeroU16}; + +pub struct HwAnteroomClient { + pub nick: Option, + pub protocol_number: Option, + 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, +} + +struct Ipv4AddrRange { + min: [u8; 4], + max: [u8; 4], +} + +impl Ipv4AddrRange { + fn contains(&self, addr: [u8; 4]) -> bool { + (0..4).all(|i| self.min[i] <= addr[i] && addr[i] <= self.max[i]) + } +} + +struct BanCollection { + ban_ips: Vec, + ban_timeouts: Vec>, + ban_reasons: Vec, +} + +impl BanCollection { + fn new() -> Self { + //todo!("add nick bans"); + Self { + ban_ips: vec![], + ban_timeouts: vec![], + ban_reasons: vec![], + } + } + + fn find(&self, addr: [u8; 4]) -> Option { + let time = offset::Utc::now(); + self.ban_ips + .iter() + .enumerate() + .find(|(i, r)| r.contains(addr) && time < self.ban_timeouts[*i]) + .map(|(i, _)| self.ban_reasons[i].clone()) + } +} + +pub struct HwAnteroom { + pub clients: IndexSlab, + bans: BanCollection, +} + +impl HwAnteroom { + pub fn new(clients_limit: usize) -> Self { + let clients = IndexSlab::with_capacity(clients_limit); + HwAnteroom { + clients, + bans: BanCollection::new(), + } + } + + pub fn find_ip_ban(&self, addr: [u8; 4]) -> Option { + self.bans.find(addr) + } + + pub fn add_client(&mut self, client_id: ClientId, salt: String, is_local_admin: bool) { + let client = HwAnteroomClient { + nick: None, + protocol_number: None, + 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 { + let client = self.clients.remove(client_id); + client + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/core/client.rs --- a/rust/hedgewars-server/src/core/client.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/core/client.rs Sun Mar 24 14:33:57 2024 -0400 @@ -2,16 +2,15 @@ use bitflags::*; bitflags! { - pub struct ClientFlags: u16 { + 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 IS_CONTRIBUTOR = 0b0100_0000; - const HAS_SUPER_POWER = 0b1000_0000; - const IS_REGISTERED = 0b0001_0000_0000; + const IS_CONTRIBUTOR = 0b0001_0000; + const HAS_SUPER_POWER = 0b0010_0000; + const IS_REGISTERED = 0b0100_0000; + const IS_MODERATOR = 0b1000_0000; const NONE = 0b0000_0000; const DEFAULT = Self::NONE.bits; @@ -31,6 +30,7 @@ impl HwClient { pub fn new(id: ClientId, protocol_number: u16, nick: String) -> HwClient { + //todo!("add quiet flag"); HwClient { id, nick, @@ -63,12 +63,6 @@ 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 is_contributor(&self) -> bool { self.contains(ClientFlags::IS_CONTRIBUTOR) } @@ -91,12 +85,6 @@ 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) - } pub fn set_is_contributor(&mut self, value: bool) { self.set(ClientFlags::IS_CONTRIBUTOR, value) } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/core/room.rs --- a/rust/hedgewars-server/src/core/room.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/core/room.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,17 +1,18 @@ use super::{ client::HwClient, - types::{ - ClientId, GameCfg, GameCfg::*, RoomConfig, RoomId, TeamInfo, Voting, MAX_HEDGEHOGS_PER_TEAM, - }, + types::{ClientId, RoomId, Voting}, }; use bitflags::*; +use hedgewars_network_protocol::types::{ + GameCfg, GameCfg::*, RoomConfig, TeamInfo, MAX_HEDGEHOGS_PER_TEAM, +}; use serde::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize}; use serde_yaml; use std::{collections::HashMap, iter}; pub const MAX_TEAMS_IN_ROOM: u8 = 8; -pub const MAX_HEDGEHOGS_IN_ROOM: u8 = MAX_HEDGEHOGS_PER_TEAM * MAX_HEDGEHOGS_PER_TEAM; +pub const MAX_HEDGEHOGS_IN_ROOM: u8 = MAX_TEAMS_IN_ROOM * MAX_HEDGEHOGS_PER_TEAM; fn client_teams_impl( teams: &[(ClientId, TeamInfo)], @@ -24,13 +25,12 @@ } pub struct GameInfo { - pub teams_in_game: u8, - pub teams_at_start: Vec<(ClientId, TeamInfo)>, + pub original_teams: Vec<(ClientId, TeamInfo)>, pub left_teams: Vec, pub msg_log: Vec, pub sync_msg: Option, pub is_paused: bool, - config: RoomConfig, + original_config: RoomConfig, } impl GameInfo { @@ -40,14 +40,30 @@ msg_log: Vec::new(), sync_msg: None, is_paused: false, - teams_in_game: teams.len() as u8, - teams_at_start: teams, - config, + original_teams: teams, + original_config: config, } } pub fn client_teams(&self, client_id: ClientId) -> impl Iterator + Clone { - client_teams_impl(&self.teams_at_start, client_id) + client_teams_impl(&self.original_teams, client_id) + } + + pub fn mark_left_teams<'a, I>(&mut self, team_names: I) + where + I: Iterator, + { + if let Some(m) = &self.sync_msg { + self.msg_log.push(m.clone()); + self.sync_msg = None + } + + for team_name in team_names { + self.left_teams.push(team_name.clone()); + + let remove_msg = crate::utils::to_engine_msg(iter::once(b'F').chain(team_name.bytes())); + self.msg_log.push(remove_msg); + } } } @@ -62,7 +78,7 @@ const FIXED = 0b0000_0001; const RESTRICTED_JOIN = 0b0000_0010; const RESTRICTED_TEAM_ADD = 0b0000_0100; - const RESTRICTED_UNREGISTERED_PLAYERS = 0b0000_1000; + const REGISTRATION_REQUIRED = 0b0000_1000; } } @@ -142,18 +158,15 @@ &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) { + pub fn remove_team(&mut self, team_name: &str) { + if let Some(index) = self.teams.iter().position(|(_, t)| t.name == team_name) { self.teams.remove(index); } } pub fn set_hedgehogs_number(&mut self, n: u8) -> Vec { let mut names = Vec::new(); - let teams = match self.game_info { - Some(ref mut info) => &mut info.teams_at_start, - None => &mut self.teams, - }; + let teams = &mut self.teams; if teams.len() as u8 * n <= MAX_HEDGEHOGS_IN_ROOM { for (_, team) in teams.iter_mut() { @@ -165,6 +178,12 @@ names } + pub fn teams_in_game(&self) -> Option { + self.game_info + .as_ref() + .map(|info| (info.original_teams.len() - info.left_teams.len()) as u8) + } + pub fn find_team_and_owner_mut(&mut self, f: F) -> Option<(ClientId, &mut TeamInfo)> where F: Fn(&TeamInfo) -> bool, @@ -239,9 +258,8 @@ 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 is_registration_required(&self) -> bool { + self.flags.contains(RoomFlags::REGISTRATION_REQUIRED) } pub fn set_is_fixed(&mut self, value: bool) { @@ -254,8 +272,7 @@ 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) + self.flags.set(RoomFlags::REGISTRATION_REQUIRED, value) } fn flags_string(&self) -> String { @@ -269,7 +286,7 @@ if self.is_join_restricted() { result += "j" } - if self.are_unregistered_players_restricted() { + if self.is_registration_required() { result += "r" } result @@ -290,23 +307,27 @@ ] } + pub fn config(&self) -> &RoomConfig { + &self.config + } + pub fn active_config(&self) -> &RoomConfig { match self.game_info { - Some(ref info) => &info.config, + Some(ref info) => &info.original_config, None => &self.config, } } pub fn map_config(&self) -> Vec { match self.game_info { - Some(ref info) => info.config.to_map_config(), + Some(ref info) => info.original_config.to_map_config(), None => self.config.to_map_config(), } } pub fn game_config(&self) -> Vec { match self.game_info { - Some(ref info) => info.config.to_game_config(), + Some(ref info) => info.original_config.to_game_config(), None => self.config.to_game_config(), } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/core/server.rs --- a/rust/hedgewars-server/src/core/server.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/core/server.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,18 +1,20 @@ use super::{ + anteroom::HwAnteroomClient, client::HwClient, indexslab::IndexSlab, room::HwRoom, - types::{ClientId, RoomId, ServerVar}, + types::{CheckerId, ClientId, RoomId, Voting}, }; -use crate::{protocol::messages::HwProtocolMessage::Greeting, utils}; +use crate::utils; +use hedgewars_network_protocol::types::{GameCfg, ServerVar, TeamInfo, Vote, VoteType}; -use crate::core::server::JoinRoomError::WrongProtocol; +use crate::server::replaystorage::ReplayStorage; + use bitflags::*; use log::*; -use slab; -use std::{borrow::BorrowMut, collections::HashSet, iter, num::NonZeroU16}; - -type Slab = slab::Slab; +use rand::{self, seq::SliceRandom, thread_rng, Rng}; +use slab::Slab; +use std::{borrow::BorrowMut, cmp::min, collections::HashSet, iter, mem::replace}; #[derive(Debug)] pub enum CreateRoomError { @@ -24,8 +26,122 @@ pub enum JoinRoomError { DoesntExist, WrongProtocol, + WrongPassword, Full, Restricted, + RegistrationRequired, +} + +#[derive(Debug)] +pub enum LeaveRoomResult { + RoomRemoved, + RoomRemains { + is_empty: bool, + was_master: bool, + was_in_game: bool, + new_master: Option, + removed_teams: Vec, + }, +} + +#[derive(Debug)] +pub struct ChangeMasterResult { + pub old_master_id: Option, + pub new_master_id: ClientId, +} + +#[derive(Debug)] +pub enum ChangeMasterError { + NoAccess, + AlreadyMaster, + NoClient, + ClientNotInRoom, +} + +#[derive(Debug)] +pub enum AddTeamError { + TooManyTeams, + TooManyHedgehogs, + TeamAlreadyExists, + Restricted, +} + +#[derive(Debug)] +pub enum RemoveTeamError { + NoTeam, + TeamNotOwned, +} + +#[derive(Debug)] +pub enum ModifyTeamError { + NoTeam, + NotMaster, +} + +#[derive(Debug)] +pub enum SetTeamCountError { + InvalidNumber, + NotMaster, +} + +#[derive(Debug)] +pub enum SetHedgehogsError { + NoTeam, + InvalidNumber(u8), + NotMaster, +} + +#[derive(Debug)] +pub enum SetConfigError { + NotMaster, + RoomFixed, +} + +#[derive(Debug)] +pub enum ModifyRoomNameError { + AccessDenied, + InvalidName, + DuplicateName, +} + +#[derive(Debug)] +pub enum StartVoteError { + VotingInProgress, +} + +#[derive(Debug)] +pub enum VoteEffect { + Kicked(ClientId, LeaveRoomResult), + Map(String), + Pause, + NewSeed(GameCfg), + HedgehogsPerTeam(u8, Vec), +} + +#[derive(Debug)] +pub enum VoteResult { + Submitted, + Succeeded(VoteEffect), + Failed, +} + +#[derive(Debug)] +pub enum VoteError { + NoVoting, + AlreadyVoted, +} + +#[derive(Debug)] +pub enum StartGameError { + NotEnoughClans, + NotReady, + AlreadyInGame, +} + +#[derive(Debug)] +pub struct EndGameResult { + pub left_teams: Vec, + pub unreadied_nicks: Vec, } #[derive(Debug)] @@ -33,47 +149,6 @@ #[derive(Debug)] pub struct AccessError(); -pub struct HwAnteClient { - pub nick: Option, - pub protocol_number: Option, - 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 { - pub clients: IndexSlab, -} - -impl HwAnteroom { - pub fn new(clients_limit: usize) -> Self { - let clients = IndexSlab::with_capacity(clients_limit); - HwAnteroom { clients } - } - - pub fn add_client(&mut self, client_id: ClientId, salt: String, is_local_admin: bool) { - let client = HwAnteClient { - nick: None, - protocol_number: None, - 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 { - let client = self.clients.remove(client_id); - client - } -} - pub struct ServerGreetings { pub for_latest_protocol: String, pub for_old_protocols: String, @@ -94,26 +169,48 @@ } } +pub struct HwChecker { + pub id: ClientId, + pub is_ready: bool, +} + +impl HwChecker { + pub fn new(id: ClientId) -> Self { + Self { + id, + is_ready: false, + } + } + + pub fn set_is_ready(&mut self, ready: bool) { + self.is_ready = ready + } +} + pub struct HwServer { - pub clients: IndexSlab, - pub rooms: Slab, - pub anteroom: HwAnteroom, - pub latest_protocol: u16, - pub flags: ServerFlags, - pub greetings: ServerGreetings, + clients: IndexSlab, + rooms: Slab, + checkers: IndexSlab, + latest_protocol: u16, + flags: ServerFlags, + greetings: ServerGreetings, + replay_storage: Option, } impl HwServer { pub fn new(clients_limit: usize, rooms_limit: usize) -> Self { + //todo!("add reconnection IDs"); let rooms = Slab::with_capacity(rooms_limit); let clients = IndexSlab::with_capacity(clients_limit); + let checkers = IndexSlab::new(); Self { clients, rooms, - anteroom: HwAnteroom::new(clients_limit), + checkers, greetings: ServerGreetings::new(), latest_protocol: 58, flags: ServerFlags::empty(), + replay_storage: None, } } @@ -123,8 +220,18 @@ } #[inline] - pub fn client_mut(&mut self, client_id: ClientId) -> &mut HwClient { - &mut self.clients[client_id] + pub fn get_checker_mut(&mut self, checker_id: CheckerId) -> Option<&mut HwChecker> { + self.checkers.get_mut(checker_id) + } + + #[inline] + pub fn has_client(&self, client_id: ClientId) -> bool { + self.clients.contains(client_id) + } + + #[inline] + pub fn iter_clients(&self) -> impl Iterator + Clone { + self.clients.iter().map(|(_, c)| c) } #[inline] @@ -133,8 +240,38 @@ } #[inline] - pub fn room_mut(&mut self, room_id: RoomId) -> &mut HwRoom { - &mut self.rooms[room_id] + pub fn get_room(&self, room_id: RoomId) -> Option<&HwRoom> { + self.rooms.get(room_id) + } + + #[inline] + fn get_room_mut(&mut self, room_id: RoomId) -> Option<&mut HwRoom> { + self.rooms.get_mut(room_id) + } + + #[inline] + pub fn iter_rooms(&self) -> impl Iterator { + self.rooms.iter().map(|(_, r)| r) + } + + #[inline] + pub fn client_and_room(&self, client_id: ClientId, room_id: RoomId) -> (&HwClient, &HwRoom) { + (&self.clients[client_id], &self.rooms[room_id]) + } + + #[inline] + fn client_and_room_mut(&mut self, client_id: ClientId) -> Option<(&mut HwClient, &mut HwRoom)> { + let client = &mut self.clients[client_id]; + if let Some(room_id) = client.room_id { + Some((client, &mut self.rooms[room_id])) + } else { + None + } + } + + #[inline] + pub fn get_room_control(&mut self, client_id: ClientId) -> Option { + HwRoomControl::new(self, client_id) } #[inline] @@ -145,18 +282,24 @@ .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) { + #[inline] + pub fn is_checker(&self, client_id: ClientId) -> bool { + self.checkers.contains(client_id) + } + + pub fn add_client(&mut self, client_id: ClientId, data: HwAnteroomClient) { + if data.is_checker { + self.checkers.insert(client_id, HwChecker::new(client_id)); + } else if let (Some(protocol), Some(nick)) = (data.protocol_number, data.nick) { let mut client = HwClient::new(client_id, protocol.get(), nick); - client.set_is_checker(data.is_checker); #[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); + client.set_is_registered(data.is_registered); + client.set_is_admin(data.is_admin); + client.set_is_contributor(data.is_contributor); } self.clients.insert(client_id, client); @@ -176,11 +319,6 @@ } #[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, @@ -206,6 +344,7 @@ &mut self, client_id: ClientId, room_id: RoomId, + room_password: Option<&str>, ) -> Result<(&HwClient, &HwRoom, impl Iterator + Clone), JoinRoomError> { use JoinRoomError::*; let room = &mut self.rooms[room_id]; @@ -213,8 +352,15 @@ if client.protocol_number != room.protocol_number { Err(WrongProtocol) + } else if room.password.is_some() + && room_password != room.password.as_deref() + && !client.has_super_power() + { + Err(WrongPassword) } else if room.is_join_restricted() { Err(Restricted) + } else if room.is_registration_required() { + Err(RegistrationRequired) } else if room.players_number == u8::max_value() { Err(Full) } else { @@ -223,7 +369,8 @@ Ok(( &self.clients[client_id], &self.rooms[room_id], - self.clients.iter().map(|(_, c)| c), + self.iter_clients() + .filter(move |c| c.room_id == Some(room_id)), )) } } @@ -233,17 +380,26 @@ &mut self, client_id: ClientId, room_name: &str, + room_password: Option<&str>, ) -> Result<(&HwClient, &HwRoom, impl Iterator + 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) + self.join_room(client_id, room_id, room_password) } else { Err(DoesntExist) } } + pub fn enable_super_power(&mut self, client_id: ClientId) -> bool { + let client = &mut self.clients[client_id]; + if client.is_admin() { + client.set_has_super_power(true); + } + client.is_admin() + } + #[inline] pub fn set_var(&mut self, client_id: ClientId, var: ServerVar) -> Result<(), AccessError> { if self.clients[client_id].is_admin() { @@ -299,7 +455,7 @@ .find_map(|(_, r)| Some(r).filter(|r| r.name == name)) } - pub fn find_room_mut(&mut self, name: &str) -> Option<&mut HwRoom> { + 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)) @@ -311,13 +467,13 @@ .find_map(|(_, c)| Some(c).filter(|c| c.nick == nick)) } - pub fn find_client_mut(&mut self, nick: &str) -> Option<&mut HwClient> { + 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 all_clients(&self) -> impl Iterator + '_ { + pub fn iter_client_ids(&self) -> impl Iterator + '_ { self.clients.iter().map(|(id, _)| id) } @@ -335,7 +491,7 @@ self.rooms.iter().filter(f).map(|(_, c)| c.id) } - pub fn collect_clients(&self, f: F) -> Vec + pub fn collect_client_ids(&self, f: F) -> Vec where F: Fn(&(usize, &HwClient)) -> bool, { @@ -353,25 +509,25 @@ .collect() } - pub fn lobby_clients(&self) -> impl Iterator + '_ { + pub fn lobby_client_ids(&self) -> impl Iterator + '_ { self.filter_clients(|(_, c)| c.room_id == None) } - pub fn room_clients(&self, room_id: RoomId) -> impl Iterator + '_ { + pub fn room_client_ids(&self, room_id: RoomId) -> impl Iterator + '_ { self.filter_clients(move |(_, c)| c.room_id == Some(room_id)) } - pub fn protocol_clients(&self, protocol: u16) -> impl Iterator + '_ { + pub fn protocol_client_ids(&self, protocol: u16) -> impl Iterator + '_ { self.filter_clients(move |(_, c)| c.protocol_number == protocol) } - pub fn protocol_rooms(&self, protocol: u16) -> impl Iterator + '_ { + pub fn protocol_room_ids(&self, protocol: u16) -> impl Iterator + '_ { self.filter_rooms(move |(_, r)| r.protocol_number == protocol) } - pub fn other_clients_in_room(&self, self_id: ClientId) -> Vec { + pub fn other_client_ids_in_room(&self, self_id: ClientId) -> Vec { let room_id = self.clients[self_id].room_id; - self.collect_clients(|(id, c)| *id != self_id && c.room_id == room_id) + self.collect_client_ids(|(id, c)| *id != self_id && c.room_id == room_id) } pub fn is_registered_only(&self) -> bool { @@ -381,6 +537,597 @@ pub fn set_is_registered_only(&mut self, value: bool) { self.flags.set(ServerFlags::REGISTERED_ONLY, value) } + + pub fn set_room_saves(&mut self, room_id: RoomId, text: &str) -> Result<(), serde_yaml::Error> { + if let Some(room) = self.rooms.get_mut(room_id) { + room.set_saves(text) + } else { + Ok(()) + } + } +} + +pub struct HwRoomControl<'a> { + server: &'a mut HwServer, + client_id: ClientId, + room_id: RoomId, + is_room_removed: bool, +} + +impl<'a> HwRoomControl<'a> { + #[inline] + pub fn new(server: &'a mut HwServer, client_id: ClientId) -> Option { + if let Some(room_id) = server.clients[client_id].room_id { + Some(Self { + server, + client_id, + room_id, + is_room_removed: false, + }) + } else { + None + } + } + + #[inline] + pub fn cleanup_room(self) { + if self.is_room_removed { + self.server.rooms.remove(self.room_id); + } + } + + #[inline] + pub fn server(&self) -> &HwServer { + self.server + } + + #[inline] + pub fn client(&self) -> &HwClient { + &self.server.clients[self.client_id] + } + + #[inline] + fn client_mut(&mut self) -> &mut HwClient { + &mut self.server.clients[self.client_id] + } + + #[inline] + pub fn room(&self) -> &HwRoom { + &self.server.rooms[self.room_id] + } + + #[inline] + fn room_mut(&mut self) -> &mut HwRoom { + &mut self.server.rooms[self.room_id] + } + + #[inline] + pub fn get(&self) -> (&HwClient, &HwRoom) { + (self.client(), self.room()) + } + + #[inline] + fn get_mut(&mut self) -> (&mut HwClient, &mut HwRoom) { + ( + &mut self.server.clients[self.client_id], + &mut self.server.rooms[self.room_id], + ) + } + + pub fn change_client<'b: 'a>(self, client_id: ClientId) -> Option> { + let room_id = self.room_id; + HwRoomControl::new(self.server, client_id).filter(|c| c.room_id == room_id) + } + + fn remove_from_room(&mut self, client_id: ClientId) -> LeaveRoomResult { + let (client, room) = self.server.client_and_room_mut(client_id).expect("Caller should have ensured the client is in this room"); + room.players_number -= 1; + client.room_id = None; + + let is_empty = room.players_number == 0; + let is_fixed = room.is_fixed(); + let was_master = room.master_id == Some(client.id); + let was_in_game = client.is_in_game(); + let mut removed_teams = vec![]; + + if is_empty && !is_fixed { + if client.is_ready() && room.ready_players_number > 0 { + room.ready_players_number -= 1; + } + + if let Some(ref mut info) = room.game_info { + removed_teams = info + .client_teams(client.id) + .map(|t| t.name.clone()) + .collect(); + info.mark_left_teams(removed_teams.iter()); + } else { + removed_teams = room + .client_teams(client.id) + .map(|t| t.name.clone()) + .collect(); + for team_name in &removed_teams { + room.remove_team(team_name); + } + } + + if client.is_master() && !is_fixed { + client.set_is_master(false); + room.master_id = None; + } + } + + client.set_is_ready(false); + client.set_is_in_game(false); + + if !is_fixed { + if room.players_number == 0 { + self.is_room_removed = true + } else if room.master_id == None { + let protocol_number = room.protocol_number; + let new_master_id = self.server.room_client_ids(self.room_id).next(); + + if let Some(new_master_id) = new_master_id { + let room = self.room_mut(); + room.master_id = Some(new_master_id); + let new_master = &mut self.server.clients[new_master_id]; + new_master.set_is_master(true); + + if protocol_number < 42 { + let nick = new_master.nick.clone(); + self.room_mut().name = nick; + } + + let room = self.room_mut(); + room.set_join_restriction(false); + room.set_team_add_restriction(false); + room.set_unregistered_players_restriction(true); + } + } + } + + if is_empty && !is_fixed { + LeaveRoomResult::RoomRemoved + } else { + LeaveRoomResult::RoomRemains { + is_empty, + was_master, + was_in_game, + new_master: self.room().master_id, + removed_teams, + } + } + } + + pub fn leave_room(&mut self) -> LeaveRoomResult { + self.remove_from_room(self.client_id) + } + + pub fn change_master( + &mut self, + new_master_nick: String, + ) -> Result { + use ChangeMasterError::*; + let (client, room) = self.get_mut(); + + if client.is_admin() || room.master_id == Some(client.id) { + let new_master_id = self + .server + .clients + .iter() + .find(|(_, c)| c.nick == new_master_nick) + .map(|(id, _)| id); + + match new_master_id { + Some(new_master_id) if new_master_id == self.client_id => Err(AlreadyMaster), + Some(new_master_id) => { + let new_master = &mut self.server.clients[new_master_id]; + if new_master.room_id == Some(self.room_id) { + self.server.clients[new_master_id].set_is_master(true); + let old_master_id = self.room().master_id; + + if let Some(master_id) = old_master_id { + self.server.clients[master_id].set_is_master(false); + } + self.room_mut().master_id = Some(new_master_id); + Ok(ChangeMasterResult { + old_master_id, + new_master_id, + }) + } else { + Err(ClientNotInRoom) + } + } + None => Err(NoClient), + } + } else { + Err(NoAccess) + } + } + + pub fn start_vote(&mut self, kind: VoteType) -> Result<(), StartVoteError> { + use StartVoteError::*; + match self.room().voting { + Some(_) => Err(VotingInProgress), + None => { + let voting = Voting::new(kind, self.server.room_client_ids(self.room_id).collect()); + self.room_mut().voting = Some(voting); + Ok(()) + } + } + } + + fn apply_vote(&mut self, kind: VoteType) -> Option { + match kind { + VoteType::Kick(nick) => { + if let Some(kicked_id) = self + .server + .find_client(&nick) + .filter(|c| c.room_id == Some(self.room_id)) + .map(|c| c.id) + { + let leave_result = self.remove_from_room(kicked_id); + Some(VoteEffect::Kicked(kicked_id, leave_result)) + } else { + None + } + } + VoteType::Map(None) => None, + VoteType::Map(Some(name)) => self + .load_config(&name) + .map(|s| VoteEffect::Map(s.to_string())), + VoteType::Pause => Some(VoteEffect::Pause).filter(|_| self.toggle_pause()), + VoteType::NewSeed => { + let seed = thread_rng().gen_range(0..1_000_000_000).to_string(); + let cfg = GameCfg::Seed(seed); + //todo!("Protocol backwards compatibility"); + self.room_mut().set_config(cfg.clone()); + Some(VoteEffect::NewSeed(cfg)) + } + VoteType::HedgehogsPerTeam(number) => { + let nicks = self.set_hedgehogs_number(number); + Some(VoteEffect::HedgehogsPerTeam(number, nicks)) + } + } + } + + pub fn vote(&mut self, vote: Vote) -> Result { + use self::{VoteError::*, VoteResult::*}; + let client_id = self.client_id; + if let Some(ref mut voting) = self.room_mut().voting { + if vote.is_forced || voting.votes.iter().all(|(id, _)| client_id != *id) { + voting.votes.push((client_id, vote.is_pro)); + 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 vote.is_forced && vote.is_pro || pro >= success_quota { + let voting = self.room_mut().voting.take().unwrap(); + if let Some(effect) = self.apply_vote(voting.kind) { + Ok(Succeeded(effect)) + } else { + Ok(Failed) + } + } else if vote.is_forced && !vote.is_pro + || contra > voting.voters.len() - success_quota + { + Ok(Failed) + } else { + Ok(Submitted) + } + } else { + Err(AlreadyVoted) + } + } else { + Err(NoVoting) + } + } + + pub fn toggle_flag(&mut self, flags: super::room::RoomFlags) -> bool { + let (client, room) = self.get_mut(); + if client.is_master() { + room.flags.toggle(flags); + } + client.is_master() + } + + pub fn fix_room(&mut self) -> Result<(), AccessError> { + let (client, room) = self.get_mut(); + if client.is_admin() { + room.set_is_fixed(true); + room.set_join_restriction(false); + room.set_team_add_restriction(false); + room.set_unregistered_players_restriction(true); + Ok(()) + } else { + Err(AccessError()) + } + } + + pub fn unfix_room(&mut self) -> Result<(), AccessError> { + let (client, room) = self.get_mut(); + if client.is_admin() { + room.set_is_fixed(false); + Ok(()) + } else { + Err(AccessError()) + } + } + + pub fn set_room_name(&mut self, mut name: String) -> Result { + use ModifyRoomNameError::*; + let room_exists = self.server.has_room(&name); + let (client, room) = self.get_mut(); + if room.is_fixed() || room.master_id != Some(client.id) { + Err(AccessDenied) + } else if utils::is_name_illegal(&name) { + Err(InvalidName) + } else if room_exists { + Err(DuplicateName) + } else { + std::mem::swap(&mut room.name, &mut name); + Ok(name) + } + } + + pub fn set_room_greeting(&mut self, greeting: Option) -> Result<(), AccessError> { + let (client, room) = self.get_mut(); + if client.is_admin() { + room.greeting = greeting.unwrap_or(String::new()); + Ok(()) + } else { + Err(AccessError()) + } + } + + pub fn set_room_max_teams(&mut self, count: u8) -> Result<(), SetTeamCountError> { + use SetTeamCountError::*; + let (client, room) = self.get_mut(); + if !client.is_master() { + Err(NotMaster) + } else if !(2..=super::room::MAX_TEAMS_IN_ROOM).contains(&count) { + Err(InvalidNumber) + } else { + room.max_teams = count; + Ok(()) + } + } + + pub fn set_team_hedgehogs_number( + &mut self, + team_name: &str, + number: u8, + ) -> Result<(), SetHedgehogsError> { + use SetHedgehogsError::*; + let (client, room) = self.get_mut(); + let addable_hedgehogs = room.addable_hedgehogs(); + if let Some((_, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) { + let max_hedgehogs = min( + super::room::MAX_HEDGEHOGS_IN_ROOM, + addable_hedgehogs + team.hedgehogs_number, + ); + if !client.is_master() { + Err(NotMaster) + } else if !(1..=max_hedgehogs).contains(&number) { + Err(InvalidNumber(team.hedgehogs_number)) + } else { + team.hedgehogs_number = number; + Ok(()) + } + } else { + Err(NoTeam) + } + } + + pub fn set_hedgehogs_number(&mut self, number: u8) -> Vec { + self.room_mut().set_hedgehogs_number(number) + } + + pub fn add_team(&mut self, mut info: Box) -> Result<&TeamInfo, AddTeamError> { + use AddTeamError::*; + let (client, room) = self.get_mut(); + if room.teams.len() >= room.max_teams as usize { + Err(TooManyTeams) + } else if room.addable_hedgehogs() == 0 { + Err(TooManyHedgehogs) + } else if room.find_team(|t| t.name == info.name) != None { + Err(TeamAlreadyExists) + } else if room.is_team_add_restricted() { + Err(Restricted) + } else { + info.owner = client.nick.clone(); + let team = room.add_team(client.id, *info, client.protocol_number < 42); + client.teams_in_game += 1; + client.clan = Some(team.color); + Ok(team) + } + } + + pub fn remove_team(&mut self, team_name: &str) -> Result<(), RemoveTeamError> { + use RemoveTeamError::*; + let (client, room) = self.get_mut(); + match room.find_team_owner(team_name) { + None => Err(NoTeam), + Some((id, _)) if id != client.id => Err(RemoveTeamError::TeamNotOwned), + Some(_) => { + client.teams_in_game -= 1; + client.clan = room.find_team_color(client.id); + room.remove_team(team_name); + Ok(()) + } + } + } + + pub fn set_team_color(&mut self, team_name: &str, color: u8) -> Result<(), ModifyTeamError> { + use ModifyTeamError::*; + let (client, room) = self.get_mut(); + if let Some((owner, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) { + if !client.is_master() { + Err(NotMaster) + } else { + team.color = color; + self.server.clients[owner].clan = Some(color); + Ok(()) + } + } else { + Err(NoTeam) + } + } + + pub fn set_config(&mut self, cfg: GameCfg) -> Result<(), SetConfigError> { + use SetConfigError::*; + let (client, room) = self.get_mut(); + if room.is_fixed() { + Err(RoomFixed) + } else if !client.is_master() { + Err(NotMaster) + } else { + let cfg = match cfg { + GameCfg::Scheme(name, mut values) => { + if client.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, + }; + room.set_config(cfg); + Ok(()) + } + } + + pub fn save_config(&mut self, name: String, location: String) { + self.room_mut().save_config(name, location); + } + + pub fn load_config(&mut self, name: &str) -> Option<&str> { + self.room_mut().load_config(name) + } + + pub fn delete_config(&mut self, name: &str) -> bool { + self.room_mut().delete_config(name) + } + + pub fn toggle_ready(&mut self) -> bool { + let (client, room) = self.get_mut(); + client.set_is_ready(!client.is_ready()); + if client.is_ready() { + room.ready_players_number += 1; + } else { + room.ready_players_number -= 1; + } + client.is_ready() + } + + pub fn start_game(&mut self) -> Result, StartGameError> { + use StartGameError::*; + let (room_clients, room_nicks): (Vec<_>, Vec<_>) = self + .server + .clients + .iter() + .map(|(id, c)| (id, c.nick.clone())) + .unzip(); + + let room = self.room_mut(); + + if !room.has_multiple_clans() { + Err(NotEnoughClans) + } else if room.protocol_number <= 43 && room.players_number != room.ready_players_number { + Err(NotReady) + } else if room.game_info.is_some() { + Err(AlreadyInGame) + } else { + room.start_round(); + for id in room_clients { + let team_indices = self.room().client_team_indices(id); + let c = &mut self.server.clients[id]; + c.set_is_in_game(true); + c.team_indices = team_indices; + } + Ok(room_nicks) + } + } + + pub fn toggle_pause(&mut self) -> bool { + if let Some(ref mut info) = self.room_mut().game_info { + info.is_paused = !info.is_paused; + } + self.room_mut().game_info.is_some() + } + + pub fn leave_game(&mut self) -> Option> { + let (client, room) = self.get_mut(); + let client_left = client.is_in_game(); + if client_left { + client.set_is_in_game(false); + + if let Some(ref mut info) = room.game_info { + let team_names: Vec<_> = info + .client_teams(client.id) + .map(|t| t.name.clone()) + .collect(); + + info.mark_left_teams(team_names.iter()); + + Some(team_names) + } else { + None + } + } else { + None + } + } + + pub fn end_game(&mut self) -> Option { + let room = self.room_mut(); + room.ready_players_number = room.master_id.is_some() as u8; + + if let Some(mut info) = replace(&mut room.game_info, None) { + let room_id = room.id; + for team_name in &info.left_teams { + room.remove_team(team_name); + } + + let unreadied_nicks: Vec<_> = self + .server + .clients + .iter_mut() + .filter(|(_, c)| c.room_id == Some(room_id)) + .map(|(_, c)| { + c.set_is_ready(c.is_master()); + c + }) + .filter_map(|c| { + if !c.is_master() { + Some(c.nick.clone()) + } else { + None + } + }) + .collect(); + + Some(EndGameResult { + left_teams: replace(&mut info.left_teams, vec![]), + unreadied_nicks, + }) + } else { + None + } + } + + pub fn log_engine_msg(&mut self, log_msg: String, sync_msg: Option>) { + if let Some(ref mut info) = self.room_mut().game_info { + if !log_msg.is_empty() { + info.msg_log.push(log_msg); + } + if let Some(msg) = sync_msg { + info.sync_msg = msg; + } + } + } } fn allocate_room(rooms: &mut Slab) -> &mut HwRoom { @@ -408,7 +1155,10 @@ client.room_id = Some(room.id); client.set_is_master(true); client.set_is_ready(true); - client.set_is_joined_mid_game(false); + client.set_is_in_game(false); + client.clan = None; + client.teams_in_game = 0; + client.team_indices = vec![]; (client, room) } @@ -419,7 +1169,6 @@ room.players_number += 1; client.room_id = Some(room.id); - client.set_is_joined_mid_game(room.game_info.is_some()); client.set_is_in_game(room.game_info.is_some()); if let Some(ref mut info) = room.game_info { @@ -430,13 +1179,6 @@ if !team_names.is_empty() { info.left_teams.retain(|name| !team_names.contains(&name)); - info.teams_in_game += team_names.len() as u8; - room.teams = info - .teams_at_start - .iter() - .filter(|(_, t)| !team_names.contains(&t.name)) - .cloned() - .collect(); } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/core/types.rs --- a/rust/hedgewars-server/src/core/types.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/core/types.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,158 +1,10 @@ +use hedgewars_network_protocol::types::{RoomConfig, TeamInfo, VoteType}; use serde_derive::{Deserialize, Serialize}; +pub type CheckerId = usize; pub type ClientId = usize; pub type RoomId = usize; -pub const MAX_HEDGEHOGS_PER_TEAM: u8 = 8; - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum ServerVar { - MOTDNew(String), - MOTDOld(String), - LatestProto(u16), -} - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum GameCfg { - FeatureSize(u32), - MapType(String), - MapGenerator(u32), - MazeSize(u32), - Seed(String), - Template(u32), - - Ammo(String, Option), - Scheme(String, Vec), - Script(String), - Theme(String), - DrawnMap(String), -} - -#[derive(PartialEq, Eq, Clone, Debug)] -pub struct TeamInfo { - pub owner: String, - pub name: String, - pub color: u8, - pub grave: String, - pub fort: String, - pub voice_pack: String, - pub flag: String, - pub difficulty: u8, - pub hedgehogs_number: u8, - pub hedgehogs: [HedgehogInfo; MAX_HEDGEHOGS_PER_TEAM as usize], -} - -#[derive(PartialEq, Eq, Clone, Debug)] -pub struct HedgehogInfo { - pub name: String, - pub hat: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Ammo { - pub name: String, - pub settings: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Scheme { - pub name: String, - pub settings: Vec, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct RoomConfig { - pub feature_size: u32, - pub map_type: String, - pub map_generator: u32, - pub maze_size: u32, - pub seed: String, - pub template: u32, - - pub ammo: Ammo, - pub scheme: Scheme, - pub script: String, - pub theme: String, - pub drawn_map: Option, -} - -impl RoomConfig { - pub fn new() -> RoomConfig { - RoomConfig { - feature_size: 12, - map_type: "+rnd+".to_string(), - map_generator: 0, - maze_size: 0, - seed: "seed".to_string(), - template: 0, - - ammo: Ammo { - name: "Default".to_string(), - settings: None, - }, - scheme: Scheme { - name: "Default".to_string(), - settings: Vec::new(), - }, - script: "Normal".to_string(), - theme: "\u{1f994}".to_string(), - drawn_map: None, - } - } - - pub fn set_config(&mut self, cfg: GameCfg) { - match cfg { - GameCfg::FeatureSize(s) => self.feature_size = s, - GameCfg::MapType(t) => self.map_type = t, - GameCfg::MapGenerator(g) => self.map_generator = g, - GameCfg::MazeSize(s) => self.maze_size = s, - GameCfg::Seed(s) => self.seed = s, - GameCfg::Template(t) => self.template = t, - - GameCfg::Ammo(n, s) => { - self.ammo = Ammo { - name: n, - settings: s, - } - } - GameCfg::Scheme(n, s) => { - self.scheme = Scheme { - name: n, - settings: s, - } - } - GameCfg::Script(s) => self.script = s, - GameCfg::Theme(t) => self.theme = t, - GameCfg::DrawnMap(m) => self.drawn_map = Some(m), - }; - } - - pub fn to_map_config(&self) -> Vec { - vec![ - self.feature_size.to_string(), - self.map_type.to_string(), - self.map_generator.to_string(), - self.maze_size.to_string(), - self.seed.to_string(), - self.template.to_string(), - ] - } - - pub fn to_game_config(&self) -> Vec { - use GameCfg::*; - let mut v = vec![ - Ammo(self.ammo.name.to_string(), self.ammo.settings.clone()), - Scheme(self.scheme.name.to_string(), self.scheme.settings.clone()), - Script(self.script.to_string()), - Theme(self.theme.to_string()), - ]; - if let Some(ref m) = self.drawn_map { - v.push(DrawnMap(m.to_string())) - } - v - } -} - #[derive(Debug)] pub struct Replay { pub config: RoomConfig, @@ -160,20 +12,6 @@ pub message_log: Vec, } -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum VoteType { - Kick(String), - Map(Option), - Pause, - NewSeed, - HedgehogsPerTeam(u8), -} - -pub struct Vote { - pub is_pro: bool, - pub is_forced: bool, -} - #[derive(Clone, Debug)] pub struct Voting { pub ttl: u32, diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers.rs --- a/rust/hedgewars-server/src/handlers.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,4 +1,3 @@ -use mio; use std::{ cmp::PartialEq, collections::HashMap, @@ -11,18 +10,24 @@ inanteroom::LoginResult, strings::*, }; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ + anteroom::HwAnteroom, room::RoomSave, server::HwServer, - types::{ClientId, GameCfg, Replay, RoomId, TeamInfo}, + types::{ClientId, Replay, RoomId}, }, - protocol::messages::{ + utils, +}; +use hedgewars_network_protocol::{ + messages::{ global_chat, server_chat, HwProtocolMessage, HwProtocolMessage::EngineMessage, HwServerMessage, HwServerMessage::*, }, - utils, + types::{GameCfg, TeamInfo}, }; + use base64::encode; use log::*; use rand::{thread_rng, RngCore}; @@ -82,6 +87,20 @@ } } +pub struct ServerState { + pub server: HwServer, + pub anteroom: HwAnteroom, +} + +impl ServerState { + pub fn new(clients_limit: usize, rooms_limit: usize) -> Self { + Self { + server: HwServer::new(clients_limit, rooms_limit), + anteroom: HwAnteroom::new(clients_limit), + } + } +} + #[derive(Debug)] pub struct AccountInfo { pub is_registered: bool, @@ -101,6 +120,10 @@ client_salt: String, server_salt: String, }, + GetCheckerAccount { + nick: String, + password: String, + }, GetReplay { id: u32, }, @@ -119,6 +142,7 @@ pub enum IoResult { AccountRegistered(bool), Account(Option), + CheckerAccount { is_registered: bool }, Replay(Option), SaveRoom(RoomId, bool), LoadRoom(RoomId, Option), @@ -219,10 +243,10 @@ Destination::ToIds(ids) => ids, Destination::ToAll { group, skip_self } => { let mut ids: Vec<_> = match group { - DestinationGroup::All => server.all_clients().collect(), - DestinationGroup::Lobby => server.lobby_clients().collect(), - DestinationGroup::Protocol(proto) => server.protocol_clients(proto).collect(), - DestinationGroup::Room(id) => server.room_clients(id).collect(), + DestinationGroup::All => server.iter_client_ids().collect(), + DestinationGroup::Lobby => server.lobby_client_ids().collect(), + DestinationGroup::Protocol(proto) => server.protocol_client_ids(proto).collect(), + DestinationGroup::Room(id) => server.room_client_ids(id).collect(), }; if skip_self { @@ -237,7 +261,7 @@ } pub fn handle( - server: &mut HwServer, + state: &mut ServerState, client_id: ClientId, response: &mut Response, message: HwProtocolMessage, @@ -246,35 +270,42 @@ HwProtocolMessage::Ping => response.add(Pong.send_self()), HwProtocolMessage::Pong => (), _ => { - if server.anteroom.clients.contains(client_id) { - match inanteroom::handle(server, client_id, response, message) { + if state.anteroom.clients.contains(client_id) { + match inanteroom::handle(state, client_id, response, message) { LoginResult::Unchanged => (), LoginResult::Complete => { - if let Some(client) = server.anteroom.remove_client(client_id) { - server.add_client(client_id, client); - common::get_lobby_join_data(server, response); + if let Some(client) = state.anteroom.remove_client(client_id) { + let is_checker = client.is_checker; + state.server.add_client(client_id, client); + if !is_checker { + common::get_lobby_join_data(&state.server, response); + } } } LoginResult::Exit => { - server.anteroom.remove_client(client_id); + state.anteroom.remove_client(client_id); response.remove_client(client_id); } } - } else if server.clients.contains(client_id) { + } else if state.server.has_client(client_id) { match message { HwProtocolMessage::Quit(Some(msg)) => { - common::remove_client(server, response, "User quit: ".to_string() + &msg); + common::remove_client( + &mut state.server, + response, + "User quit: ".to_string() + &msg, + ); } HwProtocolMessage::Quit(None) => { - common::remove_client(server, response, "User quit".to_string()); + common::remove_client(&mut state.server, response, "User quit".to_string()); } HwProtocolMessage::Info(nick) => { - if let Some(client) = server.find_client(&nick) { + if let Some(client) = state.server.find_client(&nick) { let admin_sign = if client.is_admin() { "@" } else { "" }; let master_sign = if client.is_master() { "+" } else { "" }; let room_info = match client.room_id { Some(room_id) => { - let room = server.room(room_id); + let room = state.server.room(room_id); let status = match room.game_info { Some(_) if client.teams_in_game == 0 => "(spectating)", Some(_) => "(playing)", @@ -300,11 +331,13 @@ } } HwProtocolMessage::ToggleServerRegisteredOnly => { - if !server.is_admin(client_id) { + if !state.server.is_admin(client_id) { response.warn(ACCESS_DENIED); } else { - server.set_is_registered_only(!server.is_registered_only()); - let msg = if server.is_registered_only() { + state + .server + .set_is_registered_only(!state.server.is_registered_only()); + let msg = if state.server.is_registered_only() { REGISTERED_ONLY_ENABLED } else { REGISTERED_ONLY_DISABLED @@ -313,21 +346,20 @@ } } HwProtocolMessage::Global(msg) => { - if !server.is_admin(client_id) { + if !state.server.is_admin(client_id) { response.warn(ACCESS_DENIED); } else { response.add(global_chat(msg).send_all()) } } HwProtocolMessage::SuperPower => { - let client = server.client_mut(client_id); - if !client.is_admin() { + if state.server.enable_super_power(client_id) { + response.add(server_chat(SUPER_POWER.to_string()).send_self()) + } else { response.warn(ACCESS_DENIED); - } else { - client.set_has_super_power(true); - response.add(server_chat(SUPER_POWER.to_string()).send_self()) } } + #[allow(unused_variables)] HwProtocolMessage::Watch(id) => { #[cfg(feature = "official-server")] { @@ -339,11 +371,9 @@ response.warn(REPLAY_NOT_SUPPORTED); } } - _ => 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) - } + _ => match state.server.get_room_control(client_id) { + None => inlobby::handle(&mut state.server, client_id, response, message), + Some(control) => inroom::handle(control, response, message), }, } } @@ -352,62 +382,87 @@ } pub fn handle_client_accept( - server: &mut HwServer, + state: &mut ServerState, client_id: ClientId, response: &mut Response, + addr: [u8; 4], is_local: bool, -) { - let mut salt = [0u8; 18]; - thread_rng().fill_bytes(&mut salt); +) -> bool { + let ban_reason = Some(addr) + .filter(|_| !is_local) + .and_then(|a| state.anteroom.find_ip_ban(a)); + if let Some(reason) = ban_reason { + response.add(HwServerMessage::Bye(reason).send_self()); + response.remove_client(client_id); + false + } else { + let mut salt = [0u8; 18]; + thread_rng().fill_bytes(&mut salt); - server - .anteroom - .add_client(client_id, encode(&salt), is_local); + state + .anteroom + .add_client(client_id, encode(&salt), is_local); - response.add(HwServerMessage::Connected(utils::SERVER_VERSION).send_self()); + response.add( + HwServerMessage::Connected(utils::SERVER_MESSAGE.to_owned(), utils::SERVER_VERSION) + .send_self(), + ); + true + } } -pub fn handle_client_loss(server: &mut HwServer, client_id: ClientId, response: &mut Response) { - if server.anteroom.remove_client(client_id).is_none() { - common::remove_client(server, response, "Connection reset".to_string()); +pub fn handle_client_loss(state: &mut ServerState, client_id: ClientId, response: &mut Response) { + if state.anteroom.remove_client(client_id).is_none() { + common::remove_client(&mut state.server, response, "Connection reset".to_string()); } } pub fn handle_io_result( - server: &mut HwServer, + state: &mut ServerState, client_id: ClientId, response: &mut Response, io_result: IoResult, ) { match io_result { IoResult::AccountRegistered(is_registered) => { - if !is_registered && server.is_registered_only() { + if !is_registered && state.server.is_registered_only() { 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::get_lobby_join_data(server, response); + let client = &state.anteroom.clients[client_id]; + response.add(AskPassword(client.server_salt.clone()).send_self()); + } else if let Some(client) = state.anteroom.remove_client(client_id) { + state.server.add_client(client_id, client); + common::get_lobby_join_data(&state.server, response); } } + IoResult::Account(None) => { + response.add(Bye(AUTHENTICATION_FAILED.to_string()).send_self()); + response.remove_client(client_id); + } IoResult::Account(Some(info)) => { response.add(ServerAuth(format!("{:x}", info.server_hash)).send_self()); - if let Some(mut client) = server.anteroom.remove_client(client_id) { + if let Some(mut client) = state.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); - common::get_lobby_join_data(server, response); + state.server.add_client(client_id, client); + common::get_lobby_join_data(&state.server, response); } } - IoResult::Account(None) => { - response.error(AUTHENTICATION_FAILED); - response.remove_client(client_id); + IoResult::CheckerAccount { is_registered } => { + if is_registered { + if let Some(client) = state.anteroom.remove_client(client_id) { + state.server.add_client(client_id, client); + response.add(LogonPassed.send_self()); + } + } else { + response.add(Bye(NO_CHECKER_RIGHTS.to_string()).send_self()); + response.remove_client(client_id); + } } IoResult::Replay(Some(replay)) => { - let client = server.client(client_id); + let client = state.server.client(client_id); let protocol = client.protocol_number; let start_msg = if protocol < 58 { RoomJoined(vec![client.nick.clone()]) @@ -416,8 +471,8 @@ }; response.add(start_msg.send_self()); - common::get_room_config_impl(&replay.config, client_id, response); - common::get_teams(replay.teams.iter(), client_id, response); + common::get_room_config_impl(&replay.config, Destination::ToSelf, response); + common::get_teams(replay.teams.iter(), Destination::ToSelf, response); response.add(RunGame.send_self()); response.add(ForwardEngineMessage(replay.message_log).send_self()); @@ -435,13 +490,11 @@ 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_CONFIG_LOADED.to_string()).send_self()), - Err(e) => { - warn!("Error while deserializing the room configs: {}", e); - response.warn(ROOM_CONFIG_DESERIALIZE_FAILED); - } + match state.server.set_room_saves(room_id, &contents) { + Ok(_) => response.add(server_chat(ROOM_CONFIG_LOADED.to_string()).send_self()), + Err(e) => { + warn!("Error while deserializing the room configs: {}", e); + response.warn(ROOM_CONFIG_DESERIALIZE_FAILED); } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/actions.rs --- a/rust/hedgewars-server/src/handlers/actions.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/actions.rs Sun Mar 24 14:33:57 2024 -0400 @@ -4,14 +4,18 @@ room::HwRoom, room::{GameInfo, RoomFlags}, server::HwServer, - types::{ClientId, GameCfg, RoomId, VoteType}, + types::{ClientId, RoomId}, }, - protocol::messages::{server_chat, HwProtocolMessage, HwServerMessage, HwServerMessage::*}, utils::to_engine_msg, }; +use hedgewars_network_protocol::{ + messages::{server_chat, HwProtocolMessage, HwServerMessage, HwServerMessage::*}, + types::{GameCfg, VoteType}, +}; use rand::{distributions::Uniform, thread_rng, Rng}; use std::{io, io::Write, iter::once, mem::replace}; +#[derive(Clone)] pub enum DestinationGroup { All, Lobby, @@ -19,6 +23,7 @@ Protocol(u16), } +#[derive(Clone)] pub enum Destination { ToId(ClientId), ToIds(Vec), @@ -99,17 +104,31 @@ } } -impl HwServerMessage { - pub fn send(self, client_id: ClientId) -> PendingMessage { +pub trait ToPendingMessage { + fn send(self, client_id: ClientId) -> PendingMessage; + fn send_many(self, client_ids: Vec) -> PendingMessage; + fn send_self(self) -> PendingMessage; + fn send_all(self) -> PendingMessage; + fn send_to_destination(self, destination: Destination) -> PendingMessage; +} + +impl ToPendingMessage for HwServerMessage { + fn send(self, client_id: ClientId) -> PendingMessage { PendingMessage::send(self, client_id) } - pub fn send_many(self, client_ids: Vec) -> PendingMessage { + fn send_many(self, client_ids: Vec) -> PendingMessage { PendingMessage::send_many(self, client_ids) } - pub fn send_self(self) -> PendingMessage { + fn send_self(self) -> PendingMessage { PendingMessage::send_self(self) } - pub fn send_all(self) -> PendingMessage { + fn send_all(self) -> PendingMessage { PendingMessage::send_all(self) } + fn send_to_destination(self, destination: Destination) -> PendingMessage { + PendingMessage { + destination, + message: self, + } + } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/checker.rs --- a/rust/hedgewars-server/src/handlers/checker.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/checker.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,13 +1,27 @@ use log::*; -use mio; + +use crate::core::{server::HwServer, types::CheckerId}; +use hedgewars_network_protocol::messages::HwProtocolMessage; -use crate::{ - core::{server::HwServer, types::ClientId}, - protocol::messages::HwProtocolMessage, -}; - -pub fn handle(_server: &mut HwServer, _client_id: ClientId, message: HwProtocolMessage) { +pub fn handle( + server: &mut HwServer, + checker_id: CheckerId, + _response: &mut super::Response, + message: HwProtocolMessage, +) { match message { + HwProtocolMessage::CheckerReady => { + server + .get_checker_mut(checker_id) + .map(|c| c.set_is_ready(true)); + warn!("Unimplemented") + } + HwProtocolMessage::CheckedOk(info) => { + warn!("Unimplemented") + } + HwProtocolMessage::CheckedFail(message) => { + warn!("Unimplemented") + } _ => warn!("Unknown command"), } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/common.rs --- a/rust/hedgewars-server/src/handlers/common.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/common.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,22 +1,29 @@ +use super::{ + actions::{Destination, DestinationGroup}, + Response, +}; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ client::HwClient, room::HwRoom, - server::{HwServer, JoinRoomError}, - types::{ClientId, GameCfg, RoomId, TeamInfo, Vote, VoteType}, + server::{ + EndGameResult, HwRoomControl, HwServer, JoinRoomError, LeaveRoomResult, StartGameError, + VoteEffect, VoteError, VoteResult, + }, + types::{ClientId, RoomId}, }, - protocol::messages::{ + utils::to_engine_msg, +}; +use hedgewars_network_protocol::{ + messages::{ add_flags, remove_flags, server_chat, HwProtocolMessage::{self, Rnd}, HwServerMessage::{self, *}, ProtocolFlags as Flags, }, - utils::to_engine_msg, + types::{GameCfg, RoomConfig, TeamInfo, Vote, VoteType, MAX_HEDGEHOGS_PER_TEAM}, }; - -use super::Response; - -use crate::core::types::RoomConfig; use rand::{self, seq::SliceRandom, thread_rng, Rng}; use std::{iter::once, mem::replace}; @@ -73,10 +80,9 @@ let rooms_msg = Rooms( server - .rooms - .iter() - .filter(|(_, r)| r.protocol_number == client.protocol_number) - .flat_map(|(_, r)| r.info(r.master_id.map(|id| &server.clients[id]))) + .iter_rooms() + .filter(|r| r.protocol_number == client.protocol_number) + .flat_map(|r| r.info(r.master_id.map(|id| server.client(id)))) .collect(), ); @@ -98,135 +104,6 @@ response.add(rooms_msg.send_self()); } -pub fn remove_teams( - room: &mut HwRoom, - team_names: Vec, - is_in_game: bool, - response: &mut Response, -) { - if let Some(ref mut info) = room.game_info { - for team_name in &team_names { - info.left_teams.push(team_name.clone()); - - if is_in_game { - let msg = once(b'F').chain(team_name.bytes()); - response.add( - ForwardEngineMessage(vec![to_engine_msg(msg)]) - .send_all() - .in_room(room.id) - .but_self(), - ); - - info.teams_in_game -= 1; - - 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()); - info.sync_msg = None - } - info.msg_log.push(remove_msg.clone()); - - response.add( - ForwardEngineMessage(vec![remove_msg]) - .send_all() - .in_room(room.id) - .but_self(), - ); - } - } - } - - for team_name in team_names { - room.remove_team(&team_name); - response.add(TeamRemove(team_name).send_all().in_room(room.id)); - } -} - -fn remove_client_from_room( - client: &mut HwClient, - room: &mut HwRoom, - response: &mut Response, - msg: &str, -) { - room.players_number -= 1; - if room.players_number > 0 || room.is_fixed() { - if client.is_ready() && room.ready_players_number > 0 { - room.ready_players_number -= 1; - } - - let team_names: Vec<_> = room - .client_teams(client.id) - .map(|t| t.name.clone()) - .collect(); - remove_teams(room, team_names, client.is_in_game(), response); - - if room.players_number > 0 { - response.add( - RoomLeft(client.nick.clone(), msg.to_string()) - .send_all() - .in_room(room.id) - .but_self(), - ); - } - - if client.is_master() && !room.is_fixed() { - client.set_is_master(false); - response.add( - ClientFlags( - remove_flags(&[Flags::RoomMaster]), - vec![client.nick.clone()], - ) - .send_all() - .in_room(room.id), - ); - room.master_id = None; - } - } - - client.room_id = None; - - let update_msg = if room.players_number == 0 && !room.is_fixed() { - RoomRemove(room.name.clone()) - } else { - RoomUpdated(room.name.clone(), room.info(Some(&client))) - }; - response.add(update_msg.send_all().with_protocol(room.protocol_number)); - - response.add(ClientFlags(remove_flags(&[Flags::InRoom]), vec![client.nick.clone()]).send_all()); -} - -pub fn change_master( - server: &mut HwServer, - room_id: RoomId, - new_master_id: ClientId, - response: &mut Response, -) { - let room = &mut server.rooms[room_id]; - if let Some(master_id) = room.master_id { - server.clients[master_id].set_is_master(false); - response.add( - ClientFlags( - remove_flags(&[Flags::RoomMaster]), - vec![server.clients[master_id].nick.clone()], - ) - .send_all() - .in_room(room_id), - ) - } - - room.master_id = Some(new_master_id); - server.clients[new_master_id].set_is_master(true); - - response.add( - ClientFlags( - add_flags(&[Flags::RoomMaster]), - vec![server.clients[new_master_id].nick.clone()], - ) - .send_all() - .in_room(room_id), - ); -} - pub fn get_room_join_data<'a, I: Iterator + Clone>( client: &HwClient, room: &HwRoom, @@ -234,42 +111,67 @@ response: &mut Response, ) { #[inline] - fn collect_nicks<'a, I, F>(clients: I, f: F) -> Vec + fn partition_nicks<'a, I, F>(clients: I, f: F) -> (Vec, Vec) where - I: Iterator, + I: Iterator + Clone, F: Fn(&&'a HwClient) -> bool, { - clients.filter(f).map(|c| &c.nick).cloned().collect() + ( + clients + .clone() + .filter(|c| f(c)) + .map(|c| &c.nick) + .cloned() + .collect(), + clients + .filter(|c| !f(c)) + .map(|c| &c.nick) + .cloned() + .collect(), + ) } 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 = collect_nicks(room_clients.clone(), |c| c.room_id == Some(room.id)); + response.add( + RoomJoined(vec![nick.clone()]) + .send_all() + .in_room(room.id) + .but_self(), + ); + response.add(ClientFlags(add_flags(&[Flags::InRoom]), vec![nick.clone()]).send_all()); + let nicks = room_clients.clone().map(|c| c.nick.clone()).collect(); response.add(RoomJoined(nicks).send_self()); - get_room_teams(room, client.id, response); - get_room_config(room, client.id, response); - let mut flag_selectors = [ ( Flags::RoomMaster, - collect_nicks(room_clients.clone(), |c| c.is_master()), + partition_nicks(room_clients.clone(), |c| c.is_master()), ), ( Flags::Ready, - collect_nicks(room_clients.clone(), |c| c.is_ready()), + partition_nicks(room_clients.clone(), |c| c.is_ready()), ), ( Flags::InGame, - collect_nicks(room_clients.clone(), |c| c.is_in_game()), + partition_nicks(room_clients.clone(), |c| c.is_in_game()), ), ]; - for (flag, nicks) in &mut flag_selectors { - response.add(ClientFlags(add_flags(&[*flag]), replace(nicks, vec![])).send_self()); + for (flag, (set_nicks, cleared_nicks)) in &mut flag_selectors { + if !set_nicks.is_empty() { + response.add(ClientFlags(add_flags(&[*flag]), replace(set_nicks, vec![])).send_self()); + } + + if !cleared_nicks.is_empty() { + response.add( + ClientFlags(remove_flags(&[*flag]), replace(cleared_nicks, vec![])).send_self(), + ); + } } + get_active_room_teams(room, Destination::ToSelf, response); + get_active_room_config(room, Destination::ToSelf, response); + if !room.greeting.is_empty() { response.add( ChatMsg { @@ -279,45 +181,142 @@ .send_self(), ); } + + if let Some(info) = &room.game_info { + response.add( + ClientFlags(add_flags(&[Flags::Ready, Flags::InGame]), vec![nick]) + .send_all() + .in_room(room.id), + ); + response.add(RunGame.send_self()); + + response.add( + ForwardEngineMessage( + once(to_engine_msg("e$spectate 1".bytes())) + .chain(info.msg_log.iter().cloned()) + .collect(), + ) + .send_self(), + ); + + for team in info.client_teams(client.id) { + response.add( + ForwardEngineMessage(vec![to_engine_msg(once(b'G').chain(team.name.bytes()))]) + .send_all() + .in_room(room.id), + ); + } + + if info.is_paused { + response.add(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]).send_self()); + } + + for (_, original_team) in &info.original_teams { + if let Some(team) = room.find_team(|team| team.name == original_team.name) { + if team != original_team { + response.add(TeamRemove(original_team.name.clone()).send_self()); + response.add(TeamAdd(team.to_protocol()).send_self()); + } + } else { + response.add(TeamRemove(original_team.name.clone()).send_self()); + } + } + + for (_, team) in &room.teams { + if !info.original_teams.iter().any(|(_, t)| t.name == team.name) { + response.add(TeamAdd(team.to_protocol()).send_self()); + } + } + + get_room_config_impl(room.config(), Destination::ToSelf, response); + } } 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::WrongProtocol => response.warn(INCOMPATIBLE_ROOM_PROTOCOL), + JoinRoomError::WrongPassword => { + response.add(Notice("WrongPassword".to_string()).send_self()) + } JoinRoomError::Full => response.warn(ROOM_FULL), JoinRoomError::Restricted => response.warn(ROOM_JOIN_RESTRICTED), + JoinRoomError::RegistrationRequired => response.warn(ROOM_REGISTRATION_REQUIRED), + } +} + +pub fn get_remove_teams_data( + room_id: RoomId, + was_in_game: bool, + removed_teams: Vec, + response: &mut Response, +) { + if was_in_game { + for team_name in &removed_teams { + let remove_msg = to_engine_msg(once(b'F').chain(team_name.bytes())); + + response.add( + ForwardEngineMessage(vec![remove_msg]) + .send_all() + .in_room(room_id) + .but_self(), + ); + } + } else { + for team_name in removed_teams { + response.add(TeamRemove(team_name).send_all().in_room(room_id)); + } } } -pub fn exit_room(server: &mut HwServer, client_id: ClientId, response: &mut Response, msg: &str) { - let client = &mut server.clients[client_id]; +pub fn get_room_leave_result( + server: &HwServer, + room: &HwRoom, + leave_message: &str, + result: LeaveRoomResult, + response: &mut Response, +) { + let client = server.client(response.client_id); + response.add(ClientFlags(remove_flags(&[Flags::InRoom]), vec![client.nick.clone()]).send_all()); - if let Some(room_id) = client.room_id { - let room = &mut server.rooms[room_id]; - - remove_client_from_room(client, room, response, msg); + match result { + LeaveRoomResult::RoomRemoved => { + response.add( + RoomRemove(room.name.clone()) + .send_all() + .with_protocol(room.protocol_number), + ); + } - if !room.is_fixed() { - if room.players_number == 0 { - server.rooms.remove(room_id); - } else if room.master_id == None { - let new_master_id = server.room_clients(room_id).next(); - if let Some(new_master_id) = new_master_id { - let new_master_nick = server.clients[new_master_id].nick.clone(); - let room = &mut server.rooms[room_id]; - room.master_id = Some(new_master_id); - server.clients[new_master_id].set_is_master(true); + LeaveRoomResult::RoomRemains { + is_empty, + was_master, + new_master, + was_in_game, + removed_teams, + } => { + if !is_empty { + response.add( + RoomLeft(client.nick.clone(), leave_message.to_string()) + .send_all() + .in_room(room.id) + .but_self(), + ); + } - if room.protocol_number < 42 { - room.name = new_master_nick.clone(); - } + if was_master { + response.add( + ClientFlags( + remove_flags(&[Flags::RoomMaster]), + vec![client.nick.clone()], + ) + .send_all() + .in_room(room.id), + ); - room.set_join_restriction(false); - room.set_team_add_restriction(false); - room.set_unregistered_players_restriction(true); - + if let Some(new_master_id) = new_master { + let new_master_nick = server.client(new_master_id).nick.clone(); response.add( ClientFlags(add_flags(&[Flags::RoomMaster]), vec![new_master_nick]) .send_all() @@ -325,16 +324,28 @@ ); } } + + get_remove_teams_data(room.id, was_in_game, removed_teams, response); + + response.add( + RoomUpdated(room.name.clone(), room.info(Some(&client))) + .send_all() + .with_protocol(room.protocol_number), + ); } } } pub fn remove_client(server: &mut HwServer, response: &mut Response, msg: String) { let client_id = response.client_id(); - let client = &mut server.clients[client_id]; + let client = server.client(client_id); let nick = client.nick.clone(); - exit_room(server, client_id, response, &msg); + if let Some(mut room_control) = server.get_room_control(client_id) { + let room_id = room_control.room().id; + let result = room_control.leave_room(); + get_room_leave_result(server, server.room(room_id), &msg, result, response); + } server.remove_client(client_id); @@ -353,106 +364,187 @@ response.add(update_msg.send_all().with_protocol(room.protocol_number)); } -pub fn get_room_config_impl(config: &RoomConfig, to_client: ClientId, response: &mut Response) { - response.add(ConfigEntry("FULLMAPCONFIG".to_string(), config.to_map_config()).send(to_client)); +pub fn get_room_config_impl( + config: &RoomConfig, + destination: Destination, + response: &mut Response, +) { + response.add( + ConfigEntry("FULLMAPCONFIG".to_string(), config.to_map_config()) + .send_to_destination(destination.clone()), + ); for cfg in config.to_game_config() { - response.add(cfg.to_server_msg().send(to_client)); + response.add(cfg.to_server_msg().send_to_destination(destination.clone())); } } -pub fn get_room_config(room: &HwRoom, to_client: ClientId, response: &mut Response) { - get_room_config_impl(room.active_config(), to_client, response); +pub fn get_active_room_config(room: &HwRoom, destination: Destination, response: &mut Response) { + get_room_config_impl(room.active_config(), destination, response); } -pub fn get_teams<'a, I>(teams: I, to_client: ClientId, response: &mut Response) +pub fn get_teams<'a, I>(teams: I, destination: Destination, response: &mut Response) where I: Iterator, { for team in teams { - response.add(TeamAdd(team.to_protocol()).send(to_client)); - response.add(TeamColor(team.name.clone(), team.color).send(to_client)); - response.add(HedgehogsNumber(team.name.clone(), team.hedgehogs_number).send(to_client)); + response.add(TeamAdd(team.to_protocol()).send_to_destination(destination.clone())); + response + .add(TeamColor(team.name.clone(), team.color).send_to_destination(destination.clone())); + response.add( + HedgehogsNumber(team.name.clone(), team.hedgehogs_number) + .send_to_destination(destination.clone()), + ); } } -pub fn get_room_teams(room: &HwRoom, to_client: ClientId, response: &mut Response) { +pub fn get_active_room_teams(room: &HwRoom, destination: Destination, response: &mut Response) { let current_teams = match room.game_info { - Some(ref info) => &info.teams_at_start, + Some(ref info) => &info.original_teams, None => &room.teams, }; - get_teams(current_teams.iter().map(|(_, t)| t), to_client, response); + get_teams(current_teams.iter().map(|(_, t)| t), destination, response); } pub fn get_room_flags( server: &HwServer, room_id: RoomId, - to_client: ClientId, + destination: Destination, response: &mut Response, ) { - let room = &server.rooms[room_id]; + let room = server.room(room_id); if let Some(id) = room.master_id { response.add( ClientFlags( add_flags(&[Flags::RoomMaster]), - vec![server.clients[id].nick.clone()], + vec![server.client(id).nick.clone()], ) - .send(to_client), + .send_to_destination(destination.clone()), ); } - let nicks: Vec<_> = server - .clients - .iter() - .filter(|(_, c)| c.room_id == Some(room_id) && c.is_ready()) - .map(|(_, c)| c.nick.clone()) - .collect(); + let nicks = server.collect_nicks(|(_, c)| c.room_id == Some(room_id) && c.is_ready()); + if !nicks.is_empty() { - response.add(ClientFlags(add_flags(&[Flags::Ready]), nicks).send(to_client)); + response + .add(ClientFlags(add_flags(&[Flags::Ready]), nicks).send_to_destination(destination)); } } -pub fn apply_voting_result( - server: &mut HwServer, - room_id: RoomId, +pub fn check_vote( + server: &HwServer, + room: &HwRoom, + kind: &VoteType, response: &mut Response, - kind: VoteType, -) { - match kind { +) -> bool { + let error = match &kind { VoteType::Kick(nick) => { - if let Some(client) = server.find_client(&nick) { - if client.room_id == Some(room_id) { - let id = client.id; - response.add(Kicked.send(id)); - exit_room(server, id, response, "kicked"); - } + 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) => (), + VoteType::Map(None) => { + let names: Vec<_> = room.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 let Some(location) = server.rooms[room_id].load_config(&name) { - response.add( - server_chat(location.to_string()) - .send_all() - .in_room(room_id), - ); - let room = &server.rooms[room_id]; - let room_master = if let Some(id) = room.master_id { - Some(&server.clients[id]) - } else { - None - }; - get_room_update(None, room, room_master, response); - - for (_, client) in server.clients.iter() { - if client.room_id == Some(room_id) { - super::common::get_room_config(&server.rooms[room_id], client.id, response); - } - } + if room.saves.get(&name[..]).is_some() { + None + } else { + Some("/callvote map: No such map!".to_string()) } } VoteType::Pause => { - if let Some(ref mut info) = server.rooms[room_id].game_info { - info.is_paused = !info.is_paused; + if room.game_info.is_some() { + 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 => true, + Some(msg) => { + response.add(server_chat(msg).send_self()); + false + } + } +} + +pub fn get_vote_data( + room_id: RoomId, + result: &Result, + response: &mut Response, +) { + match result { + Ok(VoteResult::Submitted) => { + response.add(server_chat("Your vote has been counted.".to_string()).send_self()) + } + Ok(VoteResult::Succeeded(_) | VoteResult::Failed) => response.add( + server_chat("Voting closed.".to_string()) + .send_all() + .in_room(room_id), + ), + Err(VoteError::NoVoting) => { + response.add(server_chat("There's no voting going on.".to_string()).send_self()) + } + Err(VoteError::AlreadyVoted) => { + response.add(server_chat("You already have voted.".to_string()).send_self()) + } + } +} + +pub fn handle_vote( + room_control: HwRoomControl, + result: Result, + response: &mut super::Response, +) { + let room_id = room_control.room().id; + get_vote_data(room_control.room().id, &result, response); + + if let Ok(VoteResult::Succeeded(effect)) = result { + match effect { + VoteEffect::Kicked(kicked_id, leave_result) => { + response.add(Kicked.send(kicked_id)); + get_room_leave_result( + room_control.server(), + room_control.room(), + "kicked", + leave_result, + response, + ); + } + VoteEffect::Map(location) => { + let msg = server_chat(location.to_string()); + let room = room_control.room(); + response.add(msg.send_all().in_room(room.id)); + + let room_master = room.master_id.map(|id| room_control.server().client(id)); + + get_room_update(None, room, room_master, response); + + let room_destination = Destination::ToAll { + group: DestinationGroup::Room(room.id), + skip_self: false, + }; + get_active_room_config(room, room_destination, response); + } + VoteEffect::Pause => { response.add( server_chat("Pause toggled.".to_string()) .send_all() @@ -464,163 +556,73 @@ .in_room(room_id), ); } - } - VoteType::NewSeed => { - let seed = thread_rng().gen_range(0, 1_000_000_000).to_string(); - let cfg = GameCfg::Seed(seed); - response.add(cfg.to_server_msg().send_all().in_room(room_id)); - server.rooms[room_id].set_config(cfg); - } - VoteType::HedgehogsPerTeam(number) => { - let r = &mut server.rooms[room_id]; - let nicks = r.set_hedgehogs_number(number); - - response.extend( - nicks - .into_iter() - .map(|n| HedgehogsNumber(n, number).send_all().in_room(room_id)), - ); - } - } -} - -fn add_vote(room: &mut HwRoom, response: &mut Response, vote: Vote) -> Option { - let client_id = response.client_id; - let mut result = None; - - if let Some(ref mut voting) = room.voting { - if vote.is_forced || voting.votes.iter().all(|(id, _)| client_id != *id) { - response.add(server_chat("Your vote has been counted.".to_string()).send_self()); - voting.votes.push((client_id, vote.is_pro)); - 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 vote.is_forced && vote.is_pro || pro >= success_quota { - result = Some(true); - } else if vote.is_forced && !vote.is_pro || contra > voting.voters.len() - success_quota - { - result = Some(false); + VoteEffect::NewSeed(cfg) => { + response.add(cfg.to_server_msg().send_all().in_room(room_id)); } - } else { - response.add(server_chat("You already have voted.".to_string()).send_self()); - } - } else { - response.add(server_chat("There's no voting going on.".to_string()).send_self()); - } - - result -} - -pub fn submit_vote(server: &mut HwServer, vote: Vote, response: &mut Response) { - let client_id = response.client_id; - let client = &server.clients[client_id]; - - if let Some(room_id) = client.room_id { - let room = &mut server.rooms[room_id]; - - if let Some(res) = add_vote(room, response, vote) { - response.add( - server_chat("Voting closed.".to_string()) - .send_all() - .in_room(room.id), - ); - let voting = replace(&mut room.voting, None).unwrap(); - if res { - apply_voting_result(server, room_id, response, voting.kind); + VoteEffect::HedgehogsPerTeam(number, team_names) => { + response.extend( + team_names + .into_iter() + .map(|n| HedgehogsNumber(n, number).send_all().in_room(room_id)), + ); } } } } -pub fn start_game(server: &mut HwServer, room_id: RoomId, response: &mut Response) { - 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]; +pub fn get_start_game_data( + server: &HwServer, + room_id: RoomId, + result: Result, StartGameError>, + response: &mut Response, +) { + match result { + Ok(room_nicks) => { + let room = server.room(room_id); + response.add(RunGame.send_all().in_room(room.id)); + response.add( + ClientFlags(add_flags(&[Flags::InGame]), room_nicks) + .send_all() + .in_room(room.id), + ); - if !room.has_multiple_clans() { - response.add( - Warning("The game can't be started with less than two clans!".to_string()).send_self(), - ); - } else if room.protocol_number <= 43 && room.players_number != room.ready_players_number { - response.add(Warning("Not all players are ready".to_string()).send_self()); - } else if room.game_info.is_some() { - response.add(Warning("The game is already in progress".to_string()).send_self()); - } else { - room.start_round(); - for id in room_clients { - let c = &mut server.clients[id]; - c.set_is_in_game(true); - c.team_indices = room.client_team_indices(c.id); + let room_master = room.master_id.map(|id| server.client(id)); + get_room_update(None, room, room_master, response); } - response.add(RunGame.send_all().in_room(room.id)); - response.add( - ClientFlags(add_flags(&[Flags::InGame]), room_nicks) - .send_all() - .in_room(room.id), - ); - - let room_master = if let Some(id) = room.master_id { - Some(&server.clients[id]) - } else { - None - }; - get_room_update(None, room, room_master, response); + Err(StartGameError::NotEnoughClans) => { + response.warn("The game can't be started with less than two clans!") + } + Err(StartGameError::NotReady) => response.warn("Not all players are ready"), + Err(StartGameError::AlreadyInGame) => response.warn("The game is already in progress"), } } -pub fn end_game(server: &mut HwServer, room_id: RoomId, response: &mut Response) { - let room = &mut server.rooms[room_id]; - room.ready_players_number = 1; - let room_master = if let Some(id) = room.master_id { - Some(&server.clients[id]) - } else { - None - }; +pub fn get_end_game_result( + server: &HwServer, + room_id: RoomId, + result: EndGameResult, + response: &mut Response, +) { + let room = server.room(room_id); + let room_master = room.master_id.map(|id| server.client(id)); + get_room_update(None, room, room_master, response); response.add(RoundFinished.send_all().in_room(room_id)); - if let Some(info) = replace(&mut room.game_info, None) { - for (_, client) in server.clients.iter() { - if client.room_id == Some(room_id) && client.is_joined_mid_game() { - super::common::get_room_config(room, client.id, response); - response.extend( - info.left_teams - .iter() - .map(|name| TeamRemove(name.clone()).send(client.id)), - ); - } - } - } + response.extend( + result + .left_teams + .iter() + .filter(|name| room.find_team(|t| t.name == **name).is_some()) + .map(|name| TeamRemove(name.clone()).send_all().in_room(room.id)), + ); - 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 room.protocol_number < 38 { - LegacyReady(false, nicks) - } else { - ClientFlags(remove_flags(&[Flags::Ready]), nicks) - }; - response.add(msg.send_all().in_room(room_id)); + if !result.unreadied_nicks.is_empty() { + response.add( + ClientFlags(remove_flags(&[Flags::Ready]), result.unreadied_nicks) + .send_all() + .in_room(room_id), + ); } } @@ -628,7 +630,7 @@ mod tests { use super::*; use crate::handlers::actions::PendingMessage; - use crate::protocol::messages::HwServerMessage::ChatMsg; + use hedgewars_network_protocol::messages::HwServerMessage::ChatMsg; fn reply2string(r: HwServerMessage) -> String { match r { @@ -655,21 +657,4 @@ fn test_handle_rnd_nonempty() { run_handle_test(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]) } - - /// This test terminates almost surely (strong law of large numbers) - #[test] - fn test_distribution() { - let eps = 0.000001; - let lim = 0.5; - let opts = vec![0.to_string(), 1.to_string()]; - let mut ones = 0; - let mut tries = 0; - - while tries < 1000 || ((ones as f64 / tries as f64) - lim).abs() >= eps { - tries += 1; - if reply2string(rnd_reply(&opts)) == 1.to_string() { - ones += 1; - } - } - } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/inanteroom.rs --- a/rust/hedgewars-server/src/handlers/inanteroom.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/inanteroom.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,18 +1,18 @@ -use mio; - +use super::strings::*; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ + anteroom::{HwAnteroom, HwAnteroomClient}, client::HwClient, - server::{HwAnteClient, HwAnteroom, HwServer}, + server::HwServer, types::ClientId, }, - protocol::messages::{HwProtocolMessage, HwProtocolMessage::LoadRoom, HwServerMessage::*}, utils::is_name_illegal, }; - +use hedgewars_network_protocol::messages::{ + HwProtocolMessage, HwProtocolMessage::LoadRoom, HwServerMessage::*, +}; use log::*; -#[cfg(feature = "official-server")] -use openssl::sha::sha1; use std::{ fmt::{Formatter, LowerHex}, num::NonZeroU16, @@ -26,24 +26,18 @@ fn completion_result<'a, I>( mut other_clients: I, - client: &mut HwAnteClient, + client: &mut HwAnteroomClient, response: &mut super::Response, ) -> LoginResult where - I: Iterator, + I: Iterator, { - let has_nick_clash = - other_clients.any(|(_, c)| !c.is_checker() && c.nick == *client.nick.as_ref().unwrap()); + let has_nick_clash = other_clients.any(|c| c.nick == *client.nick.as_ref().unwrap()); if has_nick_clash { - if client.protocol_number.unwrap().get() < 38 { - response.add(Bye("User quit: Nickname is already in use".to_string()).send_self()); - LoginResult::Exit - } else { - client.nick = None; - response.add(Notice("NickAlreadyInUse".to_string()).send_self()); - LoginResult::Unchanged - } + client.nick = None; + response.add(Notice("NickAlreadyInUse".to_string()).send_self()); + LoginResult::Unchanged } else { #[cfg(feature = "official-server")] { @@ -61,50 +55,51 @@ } pub fn handle( - server: &mut HwServer, + server_state: &mut super::ServerState, client_id: ClientId, response: &mut super::Response, message: HwProtocolMessage, ) -> LoginResult { + //todo!("Handle parsing of empty nicks") match message { HwProtocolMessage::Quit(_) => { response.add(Bye("User quit".to_string()).send_self()); LoginResult::Exit } HwProtocolMessage::Nick(nick) => { - let client = &mut server.anteroom.clients[client_id]; + let client = &mut server_state.anteroom.clients[client_id]; if client.nick.is_some() { - response.add(Error("Nickname already provided.".to_string()).send_self()); + response.error(NICKNAME_PROVIDED); LoginResult::Unchanged } else if is_name_illegal(&nick) { - response.add(Bye("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()).send_self()); + response.add(Bye(ILLEGAL_CLIENT_NAME.to_string()).send_self()); LoginResult::Exit } else { client.nick = Some(nick.clone()); response.add(Nick(nick).send_self()); if client.protocol_number.is_some() { - completion_result(server.clients.iter(), client, response) + completion_result(server_state.server.iter_clients(), client, response) } else { LoginResult::Unchanged } } } HwProtocolMessage::Proto(proto) => { - let client = &mut server.anteroom.clients[client_id]; + let client = &mut server_state.anteroom.clients[client_id]; if client.protocol_number.is_some() { - response.add(Error("Protocol already known.".to_string()).send_self()); + response.error(PROTOCOL_PROVIDED); LoginResult::Unchanged - } else if proto == 0 { - response.add(Error("Bad number.".to_string()).send_self()); - LoginResult::Unchanged + } else if proto < 48 { + response.add(Bye(PROTOCOL_TOO_OLD.to_string()).send_self()); + LoginResult::Exit } else { client.protocol_number = NonZeroU16::new(proto); response.add(Proto(proto).send_self()); if client.nick.is_some() { - completion_result(server.clients.iter(), client, response) + completion_result(server_state.server.iter_clients(), client, response) } else { LoginResult::Unchanged } @@ -112,7 +107,7 @@ } #[cfg(feature = "official-server")] HwProtocolMessage::Password(hash, salt) => { - let client = &server.anteroom.clients[client_id]; + let client = &server_state.anteroom.clients[client_id]; if let (Some(nick), Some(protocol)) = (client.nick.as_ref(), client.protocol_number) { response.request_io(super::IoTask::GetAccount { @@ -128,19 +123,31 @@ } #[cfg(feature = "official-server")] HwProtocolMessage::Checker(protocol, nick, password) => { - let client = &mut server.anteroom.clients[client_id]; + let client = &mut server_state.anteroom.clients[client_id]; if protocol == 0 { - response.add(Error("Bad number.".to_string()).send_self()); + response.error("Bad number."); LoginResult::Unchanged } else { client.protocol_number = NonZeroU16::new(protocol); - client.nick = Some(nick); client.is_checker = true; - LoginResult::Complete + #[cfg(not(feature = "official-server"))] + { + response.request_io(super::IoTask::GetCheckerAccount { + nick: nick, + password: password, + }); + LoginResult::Unchanged + } + + #[cfg(feature = "official-server")] + { + response.add(LogonPassed.send_self()); + LoginResult::Complete + } } } _ => { - warn!("Incorrect command in logging-in state"); + warn!("Incorrect command in anteroom"); LoginResult::Unchanged } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/inlobby.rs --- a/rust/hedgewars-server/src/handlers/inlobby.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/inlobby.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,17 +1,19 @@ -use mio; - use super::{common::rnd_reply, strings::*}; +use crate::handlers::{actions::ToPendingMessage, checker}; use crate::{ core::{ client::HwClient, server::{AccessError, CreateRoomError, HwServer, JoinRoomError}, - types::{ClientId, ServerVar}, + types::ClientId, }, - protocol::messages::{ + utils::is_name_illegal, +}; +use hedgewars_network_protocol::{ + messages::{ add_flags, remove_flags, server_chat, HwProtocolMessage, HwServerMessage::*, ProtocolFlags as Flags, }, - utils::is_name_illegal, + types::ServerVar, }; use log::*; use std::{collections::HashSet, convert::identity}; @@ -22,7 +24,12 @@ response: &mut super::Response, message: HwProtocolMessage, ) { - use crate::protocol::messages::HwProtocolMessage::*; + use hedgewars_network_protocol::messages::HwProtocolMessage::*; + + //todo!("add kick/ban handlers"); + //todo!("add command for forwarding lobby chat into rooms + //todo!("report player account age") + //todo!("port listing rooms for incompatible protocols")) match message { CreateRoom(name, password) => match server.create_room(client_id, name, password) { @@ -37,20 +44,18 @@ response.add(RoomJoined(vec![client.nick.clone()]).send_self()); response.add( ClientFlags( - add_flags(&[Flags::RoomMaster, Flags::Ready]), + add_flags(&[Flags::RoomMaster, Flags::Ready, Flags::InRoom]), vec![client.nick.clone()], ) - .send_self(), - ); - response.add( - ClientFlags(add_flags(&[Flags::InRoom]), vec![client.nick.clone()]).send_self(), + .send_all(), ); } }, Chat(msg) => { + //todo!("add client quiet flag"); response.add( ChatMsg { - nick: server.get_client_nick(client_id).to_string(), + nick: server.client(client_id).nick.clone(), msg, } .send_all() @@ -58,16 +63,18 @@ .but_self(), ); } - 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) + JoinRoom(name, password) => { + match server.join_room_by_name(client_id, &name, password.as_deref()) { + 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) { + match server.join_room(client_id, room_id, None) { 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) @@ -105,8 +112,8 @@ html.push(format!( "{}{}{}", super::utils::protocol_version_string(protocol), - server.protocol_clients(protocol).count(), - server.protocol_rooms(protocol).count() + server.protocol_client_ids(protocol).count(), + server.protocol_room_ids(protocol).count() )); } html.push("".to_string()); diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/inroom.rs --- a/rust/hedgewars-server/src/handlers/inroom.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/inroom.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,21 +1,27 @@ -use mio; - -use super::common::rnd_reply; -use crate::utils::to_engine_msg; +use super::{common::rnd_reply, strings::*}; +use crate::core::room::GameInfo; +use crate::core::server::{AddTeamError, SetTeamCountError}; +use crate::handlers::actions::ToPendingMessage; use crate::{ core::{ room::{HwRoom, RoomFlags, MAX_TEAMS_IN_ROOM}, - server::HwServer, - types, - types::{ClientId, GameCfg, RoomId, VoteType, Voting, MAX_HEDGEHOGS_PER_TEAM}, + server::{ + ChangeMasterError, ChangeMasterResult, HwRoomControl, HwServer, LeaveRoomResult, + ModifyTeamError, StartGameError, + }, + types::{ClientId, RoomId, Voting}, }, - protocol::messages::{ + utils::{is_name_illegal, to_engine_msg}, +}; +use base64::{decode, encode}; +use hedgewars_network_protocol::{ + messages::{ add_flags, remove_flags, server_chat, HwProtocolMessage, HwServerMessage::*, ProtocolFlags as Flags, }, - utils::is_name_illegal, + types, + types::{GameCfg, VoteType, MAX_HEDGEHOGS_PER_TEAM}, }; -use base64::{decode, encode}; use log::*; use std::{cmp::min, iter::once, mem::swap}; @@ -46,10 +52,9 @@ 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..MAX] => { + [size, typ, body @ ..] => { VALID_MESSAGES.contains(typ) && match body { [1..=MAX_HEDGEHOGS_PER_TEAM, team, ..] if *typ == b'h' => { @@ -62,14 +67,6 @@ } } -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() } @@ -94,33 +91,40 @@ } fn room_message_flag(msg: &HwProtocolMessage) -> RoomFlags { - use crate::protocol::messages::HwProtocolMessage::*; + use hedgewars_network_protocol::messages::HwProtocolMessage::*; match msg { ToggleRestrictJoin => RoomFlags::RESTRICTED_JOIN, ToggleRestrictTeams => RoomFlags::RESTRICTED_TEAM_ADD, - ToggleRegisteredOnly => RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS, + ToggleRegisteredOnly => RoomFlags::REGISTRATION_REQUIRED, _ => RoomFlags::empty(), } } pub fn handle( - server: &mut HwServer, - client_id: ClientId, + mut room_control: HwRoomControl, response: &mut super::Response, - room_id: RoomId, message: HwProtocolMessage, ) { - let client = &mut server.clients[client_id]; - let room = &mut server.rooms[room_id]; + let (client, room) = room_control.get(); + let (client_id, room_id) = (client.id, room.id); - use crate::protocol::messages::HwProtocolMessage::*; + use hedgewars_network_protocol::messages::HwProtocolMessage::*; match message { Part(msg) => { let msg = match msg { Some(s) => format!("part: {}", s), None => "part".to_string(), }; - super::common::exit_room(server, client_id, response, &msg); + + let result = room_control.leave_room(); + super::common::get_room_leave_result( + room_control.server(), + room_control.room(), + &msg, + result, + response, + ); + room_control.cleanup_room(); } Chat(msg) => { response.add( @@ -133,10 +137,8 @@ ); } TeamChat(msg) => { - let room = &server.rooms[room_id]; - if let Some(ref info) = room.game_info { + if room.game_info.is_some() { if let Some(clan_color) = room.find_team_color(client_id) { - let client = &server.clients[client_id]; let engine_msg = to_engine_msg(format!("b{}]{}\x20\x20", client.nick, msg).bytes()); let team = room.clan_team_owners(clan_color).collect(); @@ -145,207 +147,149 @@ } } Fix => { - if client.is_admin() { - room.set_is_fixed(true); - room.set_join_restriction(false); - room.set_team_add_restriction(false); - room.set_unregistered_players_restriction(true); + if let Err(_) = room_control.fix_room() { + response.warn(ACCESS_DENIED) } } Unfix => { - if client.is_admin() { - room.set_is_fixed(false); + if let Err(_) = room_control.unfix_room() { + response.warn(ACCESS_DENIED) } } Greeting(text) => { - if client.is_admin() || client.is_master() && !room.is_fixed() { - room.greeting = text.unwrap_or(String::new()); + if let Err(_) = room_control.set_room_greeting(text) { + response.warn(ACCESS_DENIED) } } MaxTeams(count) => { - if !client.is_master() { - response.add(Warning("You're not the room master!".to_string()).send_self()); - } else if !(2..=MAX_TEAMS_IN_ROOM).contains(&count) { - response - .add(Warning("/maxteams: specify number from 2 to 8".to_string()).send_self()); - } else { - server.rooms[room_id].max_teams = count; - } + use crate::core::server::SetTeamCountError; + match room_control.set_room_max_teams(count) { + Ok(()) => {} + Err(SetTeamCountError::NotMaster) => response.warn(NOT_MASTER), + Err(SetTeamCountError::InvalidNumber) => { + response.warn("/maxteams: specify number from 2 to 8") + } + }; } RoomName(new_name) => { - if is_name_illegal(&new_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(&new_name) { - response.add( - Warning("A room with the same name already exists.".to_string()).send_self(), - ); - } else { - let room = &mut server.rooms[room_id]; - if room.is_fixed() || room.master_id != Some(client_id) { - response.add(Warning("Access denied.".to_string()).send_self()); - } else { - let mut old_name = new_name.clone(); - let client = &server.clients[client_id]; - swap(&mut room.name, &mut old_name); - super::common::get_room_update(Some(old_name), room, Some(&client), response); + use crate::core::server::ModifyRoomNameError; + match room_control.set_room_name(new_name) { + Ok(old_name) => { + let (client, room) = room_control.get(); + super::common::get_room_update(Some(old_name), room, Some(client), response) } + Err(ModifyRoomNameError::AccessDenied) => response.warn(ACCESS_DENIED), + Err(ModifyRoomNameError::InvalidName) => response.warn(ILLEGAL_ROOM_NAME), + Err(ModifyRoomNameError::DuplicateName) => response.warn(ROOM_EXISTS), } } ToggleReady => { - let flags = if client.is_ready() { - room.ready_players_number -= 1; - remove_flags(&[Flags::Ready]) + let flags = if room_control.toggle_ready() { + add_flags(&[Flags::Ready]) } else { - room.ready_players_number += 1; - add_flags(&[Flags::Ready]) + remove_flags(&[Flags::Ready]) }; + let (client, room) = room_control.get(); let msg = if client.protocol_number < 38 { LegacyReady(client.is_ready(), vec![client.nick.clone()]) } else { ClientFlags(flags, vec![client.nick.clone()]) }; - response.add(msg.send_all().in_room(room.id)); - client.set_is_ready(!client.is_ready()); + response.add(msg.send_all().in_room(room_id)); if room.is_fixed() && room.ready_players_number == room.players_number { - super::common::start_game(server, room_id, response); + let result = room_control.start_game(); + super::common::get_start_game_data( + room_control.server(), + room_id, + result, + response, + ); } } - AddTeam(mut info) => { - if room.teams.len() >= room.max_teams as usize { - response.add(Warning("Too many teams!".to_string()).send_self()); - } else if room.addable_hedgehogs() == 0 { - response.add(Warning("Too many hedgehogs!".to_string()).send_self()); - } else if room.find_team(|t| t.name == info.name) != None { - response.add( - Warning("There's already a team with same name in the list.".to_string()) - .send_self(), - ); - } else if room.game_info.is_some() { - response.add( - Warning("Joining not possible: Round is in progress.".to_string()).send_self(), - ); - } else if room.is_team_add_restricted() { - response.add( - Warning("This room currently does not allow adding new teams.".to_string()) - .send_self(), - ); - } else { - info.owner = client.nick.clone(); - let team = room.add_team(client.id, *info, client.protocol_number < 42); - client.teams_in_game += 1; - client.clan = Some(team.color); - response.add(TeamAccepted(team.name.clone()).send_self()); - response.add( - TeamAdd(team.to_protocol()) - .send_all() - .in_room(room_id) - .but_self(), - ); - response.add( - TeamColor(team.name.clone(), team.color) - .send_all() - .in_room(room_id), - ); - response.add( - HedgehogsNumber(team.name.clone(), team.hedgehogs_number) - .send_all() - .in_room(room_id), - ); + AddTeam(info) => { + use crate::core::server::AddTeamError; + match room_control.add_team(info) { + Ok(team) => { + response.add(TeamAccepted(team.name.clone()).send_self()); + response.add( + TeamAdd(team.to_protocol()) + .send_all() + .in_room(room_id) + .but_self(), + ); + response.add( + TeamColor(team.name.clone(), team.color) + .send_all() + .in_room(room_id), + ); + response.add( + HedgehogsNumber(team.name.clone(), team.hedgehogs_number) + .send_all() + .in_room(room_id), + ); - let room_master = if let Some(id) = room.master_id { - Some(&server.clients[id]) - } else { - None - }; - super::common::get_room_update(None, room, room_master, response); + let room = room_control.room(); + let room_master = room.master_id.map(|id| room_control.server().client(id)); + super::common::get_room_update(None, room, room_master, response); + } + Err(AddTeamError::TooManyTeams) => response.warn(TOO_MANY_TEAMS), + Err(AddTeamError::TooManyHedgehogs) => response.warn(TOO_MANY_HEDGEHOGS), + Err(AddTeamError::TeamAlreadyExists) => response.warn(TEAM_EXISTS), + Err(AddTeamError::Restricted) => response.warn(TEAM_ADD_RESTRICTED), } } - RemoveTeam(name) => match room.find_team_owner(&name) { - None => response.add( - Warning("Error: The team you tried to remove does not exist.".to_string()) - .send_self(), - ), - Some((id, _)) if id != client_id => response - .add(Warning("You can't remove a team you don't own.".to_string()).send_self()), - Some((_, name)) => { - client.teams_in_game -= 1; - client.clan = room.find_team_color(client.id); - let names = vec![name.to_string()]; - super::common::remove_teams(room, names, client.is_in_game(), response); + RemoveTeam(name) => { + use crate::core::server::RemoveTeamError; + match room_control.remove_team(&name) { + Ok(()) => { + let (client, room) = room_control.get(); - match room.game_info { - Some(ref info) if info.teams_in_game == 0 => { - super::common::end_game(server, room_id, response) - } - _ => (), + let removed_teams = vec![name]; + super::common::get_remove_teams_data(room_id, false, removed_teams, response); } + Err(RemoveTeamError::NoTeam) => response.warn(NO_TEAM_TO_REMOVE), + Err(RemoveTeamError::TeamNotOwned) => response.warn(TEAM_NOT_OWNED), } - }, + } SetHedgehogsNumber(team_name, number) => { - let addable_hedgehogs = room.addable_hedgehogs(); - if let Some((_, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) { - let max_hedgehogs = min( - MAX_HEDGEHOGS_PER_TEAM, - addable_hedgehogs + team.hedgehogs_number, - ); - if !client.is_master() { - response.add(Error("You're not the room master!".to_string()).send_self()); - } else if !(1..=max_hedgehogs).contains(&number) { - response - .add(HedgehogsNumber(team.name.clone(), team.hedgehogs_number).send_self()); - } else { - team.hedgehogs_number = number; + use crate::core::server::SetHedgehogsError; + match room_control.set_team_hedgehogs_number(&team_name, number) { + Ok(()) => { response.add( - HedgehogsNumber(team.name.clone(), number) + HedgehogsNumber(team_name.clone(), number) .send_all() .in_room(room_id) .but_self(), ); } - } else { - response.add(Warning("No such team.".to_string()).send_self()); + Err(SetHedgehogsError::NotMaster) => response.error(NOT_MASTER), + Err(SetHedgehogsError::NoTeam) => response.warn(NO_TEAM), + Err(SetHedgehogsError::InvalidNumber(previous_number)) => { + response.add(HedgehogsNumber(team_name.clone(), previous_number).send_self()) + } } } - SetTeamColor(team_name, color) => { - if let Some((owner, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) { - if !client.is_master() { - response.add(Error("You're not the room master!".to_string()).send_self()); - } else { - team.color = color; - response.add( - TeamColor(team.name.clone(), color) - .send_all() - .in_room(room_id) - .but_self(), - ); - server.clients[owner].clan = Some(color); - } - } else { - response.add(Warning("No such team.".to_string()).send_self()); - } - } + SetTeamColor(team_name, color) => match room_control.set_team_color(&team_name, color) { + Ok(()) => response.add( + TeamColor(team_name, color) + .send_all() + .in_room(room_id) + .but_self(), + ), + Err(ModifyTeamError::NoTeam) => response.warn(NO_TEAM), + Err(ModifyTeamError::NotMaster) => response.error(NOT_MASTER), + }, Cfg(cfg) => { - if room.is_fixed() { - response.add(Warning("Access denied.".to_string()).send_self()); - } else if !client.is_master() { - response.add(Error("You're not the room master!".to_string()).send_self()); - } else { - let cfg = match cfg { - GameCfg::Scheme(name, mut values) => { - if client.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, - }; - - response.add(cfg.to_server_msg().send_all().in_room(room.id).but_self()); - room.set_config(cfg); + use crate::core::server::SetConfigError; + let msg = cfg.to_server_msg(); + match room_control.set_config(cfg) { + Ok(()) => { + response.add(msg.send_all().in_room(room_control.room().id).but_self()); + } + Err(SetConfigError::NotMaster) => response.error(NOT_MASTER), + Err(SetConfigError::RoomFixed) => response.warn(ACCESS_DENIED), } } Save(name, location) => { @@ -354,7 +298,7 @@ .send_all() .in_room(room_id), ); - room.save_config(name, location); + room_control.save_config(name, location); } #[cfg(feature = "official-server")] SaveRoom(filename) => { @@ -367,10 +311,7 @@ }), Err(e) => { warn!("Error while serializing the room configs: {}", e); - response.add( - Warning("Unable to serialize the room configs.".to_string()) - .send_self(), - ) + response.warn("Unable to serialize the room configs.") } } } @@ -382,7 +323,7 @@ } } Delete(name) => { - if !room.delete_config(&name) { + if !room_control.delete_config(&name) { response.add(Warning(format!("Save doesn't exist: {}", name)).send_self()); } else { response.add( @@ -393,102 +334,62 @@ } } CallVote(None) => { + //todo!("implement ghost points") response.add(server_chat("Available callvote commands: kick , map , pause, newseed, hedgehogs ".to_string()) .send_self()); } CallVote(Some(kind)) => { - let is_in_game = room.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 room.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).collect()); - let room = &mut server.rooms[room_id]; - room.voting = Some(voting); - response.add(server_chat(msg).send_all().in_room(room_id)); - super::common::submit_vote( - server, - types::Vote { + use crate::core::server::StartVoteError; + let room_id = room_control.room().id; + if super::common::check_vote( + room_control.server(), + room_control.room(), + &kind, + response, + ) { + match room_control.start_vote(kind.clone()) { + Ok(()) => { + let msg = voting_description(&kind); + response.add(server_chat(msg).send_all().in_room(room_id)); + let vote_result = room_control.vote(types::Vote { is_pro: true, is_forced: false, - }, - response, - ); - } - Some(msg) => { - response.add(server_chat(msg).send_self()); + }); + super::common::handle_vote(room_control, vote_result, response); + } + Err(StartVoteError::VotingInProgress) => { + response.add( + server_chat("There is already voting in progress".to_string()) + .send_self(), + ); + } } } } Vote(vote) => { - super::common::submit_vote( - server, - types::Vote { - is_pro: vote, - is_forced: false, - }, - response, - ); + let vote_result = room_control.vote(types::Vote { + is_pro: vote, + is_forced: false, + }); + super::common::handle_vote(room_control, vote_result, response); } ForceVote(vote) => { let is_forced = client.is_admin(); - super::common::submit_vote( - server, - types::Vote { - is_pro: vote, - is_forced, - }, - response, - ); + let vote_result = room_control.vote(types::Vote { + is_pro: vote, + is_forced, + }); + super::common::handle_vote(room_control, vote_result, response); } ToggleRestrictJoin | ToggleRestrictTeams | ToggleRegisteredOnly => { - if client.is_master() { - room.flags.toggle(room_message_flag(&message)); + if room_control.toggle_flag(room_message_flag(&message)) { + let (client, room) = room_control.get(); super::common::get_room_update(None, room, Some(&client), response); } } StartGame => { - super::common::start_game(server, room_id, response); + let result = room_control.start_game(); + super::common::get_start_game_data(room_control.server(), room_id, result, response); } EngineMessage(em) => { if client.teams_in_game > 0 { @@ -514,98 +415,87 @@ ); } let em_log = encode(&non_empty.flat_map(|msg| msg).cloned().collect::>()); - if let Some(ref mut info) = room.game_info { - if !em_log.is_empty() { - info.msg_log.push(em_log); - } - if let Some(msg) = sync_msg { - info.sync_msg = msg; - } - } + + room_control.log_engine_msg(em_log, sync_msg); } } RoundFinished => { - let mut game_ended = false; - if client.is_in_game() { - client.set_is_in_game(false); + if let Some(team_names) = room_control.leave_game() { + let (client, room) = room_control.get(); response.add( ClientFlags(remove_flags(&[Flags::InGame]), vec![client.nick.clone()]) .send_all() .in_room(room.id), ); - let team_names: Vec<_> = room - .client_teams(client_id) - .map(|t| t.name.clone()) - .collect(); - - if let Some(ref mut info) = room.game_info { - info.teams_in_game -= team_names.len() as u8; - if info.teams_in_game == 0 { - game_ended = true; - } - for team_name in team_names { - let msg = once(b'F').chain(team_name.bytes()); - response.add( - ForwardEngineMessage(vec![to_engine_msg(msg)]) - .send_all() - .in_room(room_id) - .but_self(), - ); + for team_name in team_names { + let msg = once(b'F').chain(team_name.bytes()); + response.add( + ForwardEngineMessage(vec![to_engine_msg(msg)]) + .send_all() + .in_room(room_id) + .but_self(), + ); + } - 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()); - response.add( - ForwardEngineMessage(vec![remove_msg]) - .send_all() - .in_room(room_id) - .but_self(), + if let Some(0) = room.teams_in_game() { + if let Some(result) = room_control.end_game() { + super::common::get_end_game_result( + room_control.server(), + room_id, + result, + response, ); } } } - if game_ended { - super::common::end_game(server, room_id, response) - } } Rnd(v) => { let result = rnd_reply(&v); let mut echo = vec!["/rnd".to_string()]; - echo.extend(v.into_iter()); + echo.extend(v); let chat_msg = ChatMsg { - nick: server.clients[client_id].nick.clone(), + nick: client.nick.clone(), msg: echo.join(" "), }; response.add(chat_msg.send_all().in_room(room_id)); response.add(result.send_all().in_room(room_id)); } - Delegate(nick) => { - let delegate_id = server.find_client(&nick).map(|c| (c.id, c.room_id)); - let client = &server.clients[client_id]; - if !(client.is_admin() || client.is_master()) { + Delegate(nick) => match room_control.change_master(nick) { + Ok(ChangeMasterResult { + old_master_id, + new_master_id, + }) => { + if let Some(master_id) = old_master_id { + response.add( + ClientFlags( + remove_flags(&[Flags::RoomMaster]), + vec![room_control.server().client(master_id).nick.clone()], + ) + .send_all() + .in_room(room_id), + ); + } response.add( - Warning("You're not the room master or a server admin!".to_string()) - .send_self(), - ) - } else { - match delegate_id { - None => response.add(Warning("Player is not online.".to_string()).send_self()), - Some((id, _)) if id == client_id => response - .add(Warning("You're already the room master.".to_string()).send_self()), - Some((_, id)) if id != Some(room_id) => response - .add(Warning("The player is not in your room.".to_string()).send_self()), - Some((id, _)) => { - super::common::change_master(server, room_id, id, response); - } - } + ClientFlags( + add_flags(&[Flags::RoomMaster]), + vec![room_control.server().client(new_master_id).nick.clone()], + ) + .send_all() + .in_room(room_id), + ); } - } + Err(ChangeMasterError::NoAccess) => { + response.warn("You're not the room master or a server admin!") + } + Err(ChangeMasterError::AlreadyMaster) => { + response.warn("You're already the room master.") + } + Err(ChangeMasterError::NoClient) => response.warn("Player is not online."), + Err(ChangeMasterError::ClientNotInRoom) => { + response.warn("The player is not in your room.") + } + }, _ => warn!("Unimplemented!"), } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/handlers/strings.rs --- a/rust/hedgewars-server/src/handlers/strings.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/handlers/strings.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,23 +1,39 @@ -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!"; +pub const ACCESS_DENIED: &str = "Access denied."; +pub const AUTHENTICATION_FAILED: &str = "Authentication failed"; +pub const BAD_NUMBER: &str = "Bad number."; +pub const ILLEGAL_CLIENT_NAME: &str = "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: $()*+?[]^{|}"; +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 NICKNAME_PROVIDED: &str = "Nickname already provided."; +pub const NO_CHECKER_RIGHTS: &str = "No checker rights"; +pub const NO_ROOM: &str = "No such room."; +pub const NO_TEAM: &str = "No such team."; +pub const NO_TEAM_TO_REMOVE: &str = "Error: The team you tried to remove does not exist."; +pub const NO_USER: &str = "No such user."; +pub const NOT_MASTER: &str = "You're not the room master!"; +pub const PROTOCOL_PROVIDED: &str = "Protocol already known."; +pub const PROTOCOL_TOO_OLD: &str = "Protocol version is too old"; +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 ROOM_REGISTRATION_REQUIRED: &str = + "Access denied. This room is for registered users only."; +pub const SUPER_POWER: &str = "Super power activated."; +pub const TEAM_EXISTS: &str = "There's already a team with same name in the list."; +pub const TEAM_NOT_OWNED: &str = "You can't remove a team you don't own."; +pub const TEAM_ADD_RESTRICTED: &str = "This room currently does not allow adding new teams."; +pub const TOO_MANY_HEDGEHOGS: &str = "Too many hedgehogs!"; +pub const TOO_MANY_TEAMS: &str = "Too many teams!"; +pub const USER_OFFLINE: &str = "Player is not online."; +pub const VARIABLE_UPDATED: &str = "Server variable has been updated."; +pub const INCOMPATIBLE_ROOM_PROTOCOL: &str = "Room version incompatible to your Hedgewars version!"; diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/main.rs --- a/rust/hedgewars-server/src/main.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,10 +1,12 @@ +#![forbid(unsafe_code)] #![allow(unused_imports)] +#![allow(dead_code)] +#![allow(unused_variables)] #![deny(bare_trait_objects)] use getopts::Options; use log::*; -use mio::{net::*, *}; -use std::{env, str::FromStr as _, time::Duration}; +use std::{env, net::SocketAddr, str::FromStr as _}; mod core; mod handlers; @@ -16,7 +18,8 @@ const PROGRAM_NAME: &'_ str = "Hedgewars Game Server"; -fn main() { +#[tokio::main] +async fn main() -> tokio::io::Result<()> { env_logger::init(); info!("Hedgewars game server, protocol {}", utils::SERVER_VERSION); @@ -24,91 +27,31 @@ let args: Vec = env::args().collect(); let mut opts = Options::new(); + //todo!("Add options for cert paths"); opts.optopt("p", "port", "port - defaults to 46631", "PORT"); opts.optflag("h", "help", "help"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(e) => { println!("{}\n{}", e, opts.short_usage("")); - return; + return Ok(()); } }; if matches.opt_present("h") { println!("{}", opts.usage(PROGRAM_NAME)); - return; + return Ok(()); } let port = matches .opt_str("p") .and_then(|s| u16::from_str(&s).ok()) .unwrap_or(46631); - let address = format!("0.0.0.0:{}", port).parse().unwrap(); - - let listener = TcpListener::bind(&address).unwrap(); - - let poll = Poll::new().unwrap(); - let mut hw_builder = NetworkLayerBuilder::default().with_listener(listener); - - #[cfg(feature = "tls-connections")] - { - let address = format!("0.0.0.0:{}", port + 1).parse().unwrap(); - hw_builder = hw_builder.with_secure_listener(TcpListener::bind(&address).unwrap()); - } + let address: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap(); - let mut hw_network = hw_builder.build(); - hw_network.register(&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(); + let server = tokio::net::TcpListener::bind(address).await.unwrap(); - for event in events.iter() { - if event.readiness() & Ready::readable() == Ready::readable() { - match event.token() { - token @ utils::SERVER_TOKEN | token @ utils::SECURE_SERVER_TOKEN => { - match hw_network.accept_client(&poll, token) { - Ok(()) => (), - Err(e) => debug!("Error accepting client: {}", e), - } - } - utils::TIMER_TOKEN => match hw_network.handle_timeout(&poll) { - Ok(()) => (), - Err(e) => debug!("Error in timer event: {}", e), - }, - #[cfg(feature = "official-server")] - utils::IO_TOKEN => match hw_network.handle_io_result(&poll) { - Ok(()) => (), - Err(e) => debug!("Error in IO task: {}", e), - }, - Token(token) => match hw_network.client_readable(&poll, token) { - Ok(()) => (), - Err(e) => debug!("Error reading from client socket {}: {}", token, e), - }, - } - } - if event.readiness() & Ready::writable() == Ready::writable() { - match event.token() { - utils::SERVER_TOKEN - | utils::SECURE_SERVER_TOKEN - | utils::TIMER_TOKEN - | utils::IO_TOKEN => unreachable!(), - Token(token) => match hw_network.client_writable(&poll, token) { - Ok(()) => (), - Err(e) => debug!("Error writing to client socket {}: {}", token, e), - }, - } - } - } + let mut hw_network = NetworkLayerBuilder::default().with_listener(server).build(); - match hw_network.on_idle(&poll) { - Ok(()) => (), - Err(e) => debug!("Error in idle handler: {}", e), - }; - } + hw_network.run().await; + Ok(()) } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/protocol.rs --- a/rust/hedgewars-server/src/protocol.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/protocol.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,69 +1,120 @@ -use self::parser::message; +use bytes::{Buf, BufMut, BytesMut}; use log::*; -use netbuf; -use std::io::{Read, Result}; +use std::{ + error::Error, + fmt::{Debug, Display, Formatter}, + io, + io::ErrorKind, + marker::Unpin, + time::Duration, +}; +use tokio::{io::AsyncReadExt, time::timeout}; + +use crate::protocol::ProtocolError::Timeout; +use hedgewars_network_protocol::{ + messages::HwProtocolMessage, + parser::HwProtocolError, + parser::{malformed_message, message}, +}; -pub mod messages; -mod parser; -#[cfg(test)] -pub mod test; +#[derive(Debug)] +pub enum ProtocolError { + Eof, + Timeout, + Network(Box), +} + +impl Display for ProtocolError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ProtocolError::Eof => write!(f, "Connection reset by peer"), + ProtocolError::Timeout => write!(f, "Read operation timed out"), + ProtocolError::Network(source) => write!(f, "{:?}", source), + } + } +} + +impl Error for ProtocolError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + if let Self::Network(source) = self { + Some(source.as_ref()) + } else { + None + } + } +} + +pub type Result = std::result::Result; pub struct ProtocolDecoder { - buf: netbuf::Buf, + buffer: BytesMut, + read_timeout: Duration, is_recovering: bool, } impl ProtocolDecoder { - pub fn new() -> ProtocolDecoder { + pub fn new(read_timeout: Duration) -> ProtocolDecoder { ProtocolDecoder { - buf: netbuf::Buf::new(), + buffer: BytesMut::with_capacity(1024), + read_timeout, is_recovering: false, } } fn recover(&mut self) -> bool { - self.is_recovering = match parser::malformed_message(&self.buf[..]) { + self.is_recovering = match malformed_message(&self.buffer[..]) { Ok((tail, ())) => { - let length = tail.len(); - self.buf.consume(self.buf.len() - length); + let remaining = tail.len(); + self.buffer.advance(self.buffer.len() - remaining); false } _ => { - self.buf.consume(self.buf.len()); + self.buffer.clear(); true } }; !self.is_recovering } - pub fn read_from(&mut self, stream: &mut R) -> Result { - let count = self.buf.read_from(stream)?; - if count > 0 && self.is_recovering { - self.recover(); - } - Ok(count) - } - - pub fn extract_messages(&mut self) -> Vec { - let mut messages = vec![]; - if !self.is_recovering { - while !self.buf.is_empty() { - match parser::message(&self.buf[..]) { - Ok((tail, message)) => { - messages.push(message); - 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)) => { - debug!("Invalid message: {:?}", e); - if !self.recover() || self.buf.is_empty() { - break; - } - } + fn extract_message(&mut self) -> Option { + if !self.is_recovering || self.recover() { + match message(&self.buffer[..]) { + Ok((tail, message)) => { + let remaining = tail.len(); + self.buffer.advance(self.buffer.len() - remaining); + return Some(message); + } + Err(nom::Err::Incomplete(_)) => {} + Err(nom::Err::Failure(e) | nom::Err::Error(e)) => { + debug!("Invalid message: {:?}", e); + self.recover(); } } } - messages + None + } + + pub async fn read_from( + &mut self, + stream: &mut R, + ) -> Result { + use ProtocolError::*; + + loop { + if !self.buffer.has_remaining() { + //todo!("ensure the buffer doesn't grow indefinitely") + match timeout(self.read_timeout, stream.read_buf(&mut self.buffer)).await { + Err(_) => return Err(Timeout), + Ok(Err(e)) => return Err(Network(Box::new(e))), + Ok(Ok(0)) => return Err(Eof), + Ok(Ok(_)) => (), + }; + } + while !self.buffer.is_empty() { + if let Some(result) = self.extract_message() { + return Ok(result); + } + } + } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/protocol/messages.rs --- a/rust/hedgewars-server/src/protocol/messages.rs Sun Mar 24 14:05:06 2024 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,429 +0,0 @@ -use crate::core::types::{GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType}; -use std::{convert::From, iter::once, ops}; - -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum HwProtocolMessage { - // common messages - Ping, - Pong, - Quit(Option), - Global(String), - Watch(u32), - ToggleServerRegisteredOnly, - SuperPower, - Info(String), - // anteroom messages - Nick(String), - Proto(u16), - Password(String, String), - Checker(u16, String, String), - // lobby messages - List, - Chat(String), - CreateRoom(String, Option), - JoinRoom(String, Option), - Follow(String), - Rnd(Vec), - Kick(String), - Ban(String, String, u32), - BanIp(String, String, u32), - BanNick(String, String, u32), - BanList, - Unban(String), - SetServerVar(ServerVar), - GetServerVar, - RestartServer, - Stats, - // room messages - Part(Option), - Cfg(GameCfg), - AddTeam(Box), - RemoveTeam(String), - SetHedgehogsNumber(String, u8), - SetTeamColor(String, u8), - ToggleReady, - StartGame, - EngineMessage(String), - RoundFinished, - ToggleRestrictJoin, - ToggleRestrictTeams, - ToggleRegisteredOnly, - RoomName(String), - Delegate(String), - TeamChat(String), - MaxTeams(u8), - Fix, - Unfix, - Greeting(Option), - CallVote(Option), - Vote(bool), - ForceVote(bool), - Save(String, String), - Delete(String), - SaveRoom(String), - LoadRoom(String), -} - -#[derive(Debug, Clone, Copy)] -pub enum ProtocolFlags { - InRoom, - RoomMaster, - Ready, - InGame, - Registered, - Admin, - Contributor, -} - -impl ProtocolFlags { - #[inline] - fn flag_char(&self) -> char { - match self { - ProtocolFlags::InRoom => 'i', - ProtocolFlags::RoomMaster => 'h', - ProtocolFlags::Ready => 'r', - ProtocolFlags::InGame => 'g', - ProtocolFlags::Registered => 'u', - ProtocolFlags::Admin => 'a', - ProtocolFlags::Contributor => 'c', - } - } - - #[inline] - fn format(prefix: char, flags: &[ProtocolFlags]) -> String { - once(prefix) - .chain(flags.iter().map(|f| f.flag_char())) - .collect() - } -} - -#[inline] -pub fn add_flags(flags: &[ProtocolFlags]) -> String { - ProtocolFlags::format('+', flags) -} - -#[inline] -pub fn remove_flags(flags: &[ProtocolFlags]) -> String { - ProtocolFlags::format('-', flags) -} - -#[derive(Debug)] -pub enum HwServerMessage { - Connected(u32), - Redirect(u16), - - Ping, - Pong, - Bye(String), - - Nick(String), - Proto(u16), - AskPassword(String), - ServerAuth(String), - - LobbyLeft(String, String), - LobbyJoined(Vec), - ChatMsg { nick: String, msg: String }, - ClientFlags(String, Vec), - Rooms(Vec), - RoomAdd(Vec), - RoomJoined(Vec), - RoomLeft(String, String), - RoomRemove(String), - RoomUpdated(String, Vec), - Joining(String), - TeamAdd(Vec), - TeamRemove(String), - TeamAccepted(String), - TeamColor(String, u8), - HedgehogsNumber(String, u8), - ConfigEntry(String, Vec), - Kicked, - RunGame, - ForwardEngineMessage(Vec), - RoundFinished, - ReplayStart, - - Info(Vec), - ServerMessage(String), - ServerVars(Vec), - Notice(String), - Warning(String), - Error(String), - Unreachable, - - //Deprecated messages - LegacyReady(bool, Vec), -} - -fn special_chat(nick: &str, msg: String) -> HwServerMessage { - HwServerMessage::ChatMsg { - nick: nick.to_string(), - msg, - } -} - -pub fn server_chat(msg: String) -> HwServerMessage { - special_chat("[server]", msg) -} - -pub fn global_chat(msg: String) -> HwServerMessage { - special_chat("(global notice)", msg) -} - -impl ServerVar { - pub fn to_protocol(&self) -> Vec { - use ServerVar::*; - match self { - MOTDNew(s) => vec!["MOTD_NEW".to_string(), s.clone()], - MOTDOld(s) => vec!["MOTD_OLD".to_string(), s.clone()], - LatestProto(n) => vec!["LATEST_PROTO".to_string(), n.to_string()], - } - } -} - -impl VoteType { - pub fn to_protocol(&self) -> Vec { - use VoteType::*; - match self { - Kick(nick) => vec!["KICK".to_string(), nick.clone()], - Map(None) => vec!["MAP".to_string()], - Map(Some(name)) => vec!["MAP".to_string(), name.clone()], - Pause => vec!["PAUSE".to_string()], - NewSeed => vec!["NEWSEED".to_string()], - HedgehogsPerTeam(count) => vec!["HEDGEHOGS".to_string(), count.to_string()], - } - } -} - -impl GameCfg { - pub fn to_protocol(&self) -> (String, Vec) { - use GameCfg::*; - match self { - FeatureSize(s) => ("FEATURE_SIZE".to_string(), vec![s.to_string()]), - MapType(t) => ("MAP".to_string(), vec![t.to_string()]), - MapGenerator(g) => ("MAPGEN".to_string(), vec![g.to_string()]), - MazeSize(s) => ("MAZE_SIZE".to_string(), vec![s.to_string()]), - Seed(s) => ("SEED".to_string(), vec![s.to_string()]), - Template(t) => ("TEMPLATE".to_string(), vec![t.to_string()]), - - Ammo(n, None) => ("AMMO".to_string(), vec![n.to_string()]), - Ammo(n, Some(s)) => ("AMMO".to_string(), vec![n.to_string(), s.to_string()]), - Scheme(n, s) if s.is_empty() => ("SCHEME".to_string(), vec![n.to_string()]), - Scheme(n, s) => ("SCHEME".to_string(), { - let mut v = vec![n.to_string()]; - v.extend(s.clone().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) - } -} - -impl TeamInfo { - pub fn to_protocol(&self) -> Vec { - let mut info = vec![ - self.name.clone(), - self.grave.clone(), - self.fort.clone(), - self.voice_pack.clone(), - self.flag.clone(), - self.owner.clone(), - self.difficulty.to_string(), - ]; - let hogs = self - .hedgehogs - .iter() - .flat_map(|h| once(h.name.clone()).chain(once(h.hat.clone()))); - info.extend(hogs); - info - } -} - -macro_rules! const_braces { - ($e: expr) => { - "{}\n" - }; -} - -macro_rules! msg { - [$($part: expr),*] => { - format!(concat!($(const_braces!($part)),*, "\n"), $($part),*); - }; -} - -#[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(var) => construct_message(&["SET_SERVER_VAR"], &var.to_protocol()), - GetServerVar => msg!["GET_SERVER_VAR"], - RestartServer => msg!["CMD", "RESTART_SERVER YES"], - Stats => msg!["CMD", "STATS"], - Part(None) => msg!["PART"], - Part(Some(msg)) => msg!["PART", msg], - Cfg(config) => { - let (name, args) = config.to_protocol(); - msg!["CFG", name, args.join("\n")] - } - AddTeam(info) => msg![ - "ADD_TEAM", - info.name, - info.color, - info.grave, - info.fort, - info.voice_pack, - info.flag, - info.difficulty, - info.hedgehogs - .iter() - .flat_map(|h| several![&h.name[..], &h.hat[..]]) - .collect::>() - .join("\n") - ], - RemoveTeam(name) => msg!["REMOVE_TEAM", name], - SetHedgehogsNumber(team, number) => msg!["HH_NUM", team, number], - SetTeamColor(team, color) => msg!["TEAM_COLOR", team, color], - ToggleReady => msg!["TOGGLE_READY"], - StartGame => msg!["START_GAME"], - EngineMessage(msg) => msg!["EM", msg], - RoundFinished => msg!["ROUNDFINISHED"], - ToggleRestrictJoin => msg!["TOGGLE_RESTRICT_JOINS"], - ToggleRestrictTeams => msg!["TOGGLE_RESTRICT_TEAMS"], - ToggleRegisteredOnly => msg!["TOGGLE_REGISTERED_ONLY"], - RoomName(name) => msg!["ROOM_NAME", name], - Delegate(name) => msg!["CMD", format!("DELEGATE {}", name)], - TeamChat(msg) => msg!["TEAMCHAT", msg], - MaxTeams(count) => msg!["CMD", format!("MAXTEAMS {}", count)], - Fix => msg!["CMD", "FIX"], - Unfix => msg!["CMD", "UNFIX"], - Greeting(None) => msg!["CMD", "GREETING"], - Greeting(Some(msg)) => msg!["CMD", format!("GREETING {}", msg)], - CallVote(None) => msg!["CMD", "CALLVOTE"], - CallVote(Some(vote)) => { - msg!["CMD", format!("CALLVOTE {}", &vote.to_protocol().join(" "))] - } - Vote(msg) => msg!["CMD", format!("VOTE {}", if *msg { "YES" } else { "NO" })], - ForceVote(msg) => msg!["CMD", format!("FORCE {}", if *msg { "YES" } else { "NO" })], - Save(name, location) => msg!["CMD", format!("SAVE {} {}", name, location)], - Delete(name) => msg!["CMD", format!("DELETE {}", name)], - SaveRoom(name) => msg!["CMD", format!("SAVEROOM {}", name)], - LoadRoom(name) => msg!["CMD", format!("LOADROOM {}", name)], - _ => panic!("Protocol message not yet implemented"), - } - } -} - -fn construct_message(header: &[&str], msg: &[String]) -> String { - let mut v: Vec<_> = header.iter().cloned().collect(); - v.extend(msg.iter().map(|s| &s[..])); - v.push("\n"); - v.join("\n") -} - -impl HwServerMessage { - pub fn to_raw_protocol(&self) -> String { - use self::HwServerMessage::*; - match self { - Ping => msg!["PING"], - Pong => msg!["PONG"], - Connected(protocol_version) => msg![ - "CONNECTED", - "Hedgewars server https://www.hedgewars.org/", - protocol_version - ], - Redirect(port) => msg!["REDIRECT", port], - Bye(msg) => msg!["BYE", msg], - Nick(nick) => msg!["NICK", nick], - Proto(proto) => msg!["PROTO", proto], - AskPassword(salt) => msg!["ASKPASSWORD", salt], - ServerAuth(hash) => msg!["SERVER_AUTH", hash], - LobbyLeft(nick, msg) => msg!["LOBBY:LEFT", nick, msg], - LobbyJoined(nicks) => construct_message(&["LOBBY:JOINED"], &nicks), - ClientFlags(flags, nicks) => construct_message(&["CLIENT_FLAGS", flags], &nicks), - Rooms(info) => construct_message(&["ROOMS"], &info), - RoomAdd(info) => construct_message(&["ROOM", "ADD"], &info), - RoomJoined(nicks) => construct_message(&["JOINED"], &nicks), - RoomLeft(nick, msg) => msg!["LEFT", nick, msg], - RoomRemove(name) => msg!["ROOM", "DEL", name], - RoomUpdated(name, info) => construct_message(&["ROOM", "UPD", name], &info), - Joining(name) => msg!["JOINING", name], - TeamAdd(info) => construct_message(&["ADD_TEAM"], &info), - TeamRemove(name) => msg!["REMOVE_TEAM", name], - TeamAccepted(name) => msg!["TEAM_ACCEPTED", name], - TeamColor(name, color) => msg!["TEAM_COLOR", name, color], - HedgehogsNumber(name, number) => msg!["HH_NUM", name, number], - ConfigEntry(name, values) => construct_message(&["CFG", name], &values), - Kicked => msg!["KICKED"], - RunGame => msg!["RUN_GAME"], - ForwardEngineMessage(em) => construct_message(&["EM"], &em), - RoundFinished => msg!["ROUND_FINISHED"], - ChatMsg { nick, msg } => msg!["CHAT", nick, msg], - Info(info) => construct_message(&["INFO"], &info), - ServerMessage(msg) => msg!["SERVER_MESSAGE", msg], - ServerVars(vars) => construct_message(&["SERVER_VARS"], &vars), - Notice(msg) => msg!["NOTICE", msg], - Warning(msg) => msg!["WARNING", msg], - Error(msg) => msg!["ERROR", msg], - ReplayStart => msg!["REPLAY_START"], - - LegacyReady(is_ready, nicks) => { - construct_message(&[if *is_ready { "READY" } else { "NOT_READY" }], &nicks) - } - - _ => msg!["ERROR", "UNIMPLEMENTED"], - } - } -} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/protocol/parser.rs --- a/rust/hedgewars-server/src/protocol/parser.rs Sun Mar 24 14:05:06 2024 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,576 +0,0 @@ -/** The parsers for the chat and multiplayer protocol. The main parser is `message`. - * # Protocol - * All messages consist of `\n`-separated strings. The end of a message is - * indicated by a double newline - `\n\n`. - * - * For example, a nullary command like PING will be actually sent as `PING\n\n`. - * A unary command, such as `START_GAME nick` will be actually sent as `START_GAME\nnick\n\n`. - */ -use nom::{ - branch::alt, - bytes::complete::{tag, tag_no_case, take_until, take_while}, - character::complete::{newline, not_line_ending}, - combinator::{map, peek}, - error::{ErrorKind, ParseError}, - multi::separated_list, - sequence::{delimited, pair, preceded, terminated, tuple}, - Err, IResult, -}; - -use std::{ - num::ParseIntError, - ops::Range, - str, - str::{FromStr, Utf8Error}, -}; - -use super::messages::{HwProtocolMessage, HwProtocolMessage::*}; -use crate::core::types::{ - GameCfg, HedgehogInfo, ServerVar, TeamInfo, VoteType, MAX_HEDGEHOGS_PER_TEAM, -}; - -#[derive(Debug, PartialEq)] -pub struct HwProtocolError {} - -impl HwProtocolError { - fn new() -> Self { - HwProtocolError {} - } -} - -impl ParseError for HwProtocolError { - fn from_error_kind(input: I, kind: ErrorKind) -> Self { - HwProtocolError::new() - } - - fn append(input: I, kind: ErrorKind, other: Self) -> Self { - HwProtocolError::new() - } -} - -impl From for HwProtocolError { - fn from(_: Utf8Error) -> Self { - HwProtocolError::new() - } -} - -impl From for HwProtocolError { - fn from(_: ParseIntError) -> Self { - HwProtocolError::new() - } -} - -pub type HwResult<'a, O> = IResult<&'a [u8], O, HwProtocolError>; - -fn end_of_message(input: &[u8]) -> HwResult<&[u8]> { - tag("\n\n")(input) -} - -fn convert_utf8(input: &[u8]) -> HwResult<&str> { - match str::from_utf8(input) { - Ok(str) => Ok((b"", str)), - Err(utf_err) => Result::Err(Err::Failure(utf_err.into())), - } -} - -fn convert_from_str(str: &str) -> HwResult -where - T: FromStr, -{ - match T::from_str(str) { - Ok(x) => Ok((b"", x)), - Err(format_err) => Result::Err(Err::Failure(format_err.into())), - } -} - -fn str_line(input: &[u8]) -> HwResult<&str> { - let (i, text) = not_line_ending(input.clone())?; - if i != input { - Ok((i, convert_utf8(text)?.1)) - } else { - Err(Err::Error(HwProtocolError::new())) - } -} - -fn a_line(input: &[u8]) -> HwResult { - map(str_line, String::from)(input) -} - -fn cmd_arg(input: &[u8]) -> HwResult { - let delimiters = b" \n"; - let (i, str) = take_while(move |c| !delimiters.contains(&c))(input.clone())?; - if i != input { - Ok((i, convert_utf8(str)?.1.to_string())) - } else { - Err(Err::Error(HwProtocolError::new())) - } -} - -fn u8_line(input: &[u8]) -> HwResult { - let (i, str) = str_line(input)?; - Ok((i, convert_from_str(str)?.1)) -} - -fn u16_line(input: &[u8]) -> HwResult { - let (i, str) = str_line(input)?; - Ok((i, convert_from_str(str)?.1)) -} - -fn u32_line(input: &[u8]) -> HwResult { - let (i, str) = str_line(input)?; - Ok((i, convert_from_str(str)?.1)) -} - -fn yes_no_line(input: &[u8]) -> HwResult { - alt(( - map(tag_no_case(b"YES"), |_| true), - map(tag_no_case(b"NO"), |_| false), - ))(input) -} - -fn opt_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option> { - alt(( - map(peek(end_of_message), |_| None), - map(preceded(tag("\n"), a_line), Some), - ))(input) -} - -fn spaces(input: &[u8]) -> HwResult<&[u8]> { - preceded(tag(" "), take_while(|c| c == b' '))(input) -} - -fn opt_space_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option> { - alt(( - map(peek(end_of_message), |_| None), - map(preceded(spaces, a_line), Some), - ))(input) -} - -fn hedgehog_array(input: &[u8]) -> HwResult<[HedgehogInfo; 8]> { - fn hedgehog_line(input: &[u8]) -> HwResult { - map( - tuple((terminated(a_line, newline), a_line)), - |(name, hat)| HedgehogInfo { name, hat }, - )(input) - } - - let (i, (h1, h2, h3, h4, h5, h6, h7, h8)) = tuple(( - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - terminated(hedgehog_line, newline), - hedgehog_line, - ))(input)?; - - Ok((i, [h1, h2, h3, h4, h5, h6, h7, h8])) -} - -fn voting(input: &[u8]) -> HwResult { - alt(( - map(tag_no_case("PAUSE"), |_| VoteType::Pause), - map(tag_no_case("NEWSEED"), |_| VoteType::NewSeed), - map( - preceded(pair(tag_no_case("KICK"), spaces), a_line), - VoteType::Kick, - ), - map( - preceded(pair(tag_no_case("HEDGEHOGS"), spaces), u8_line), - VoteType::HedgehogsPerTeam, - ), - map(preceded(tag_no_case("MAP"), opt_space_arg), VoteType::Map), - ))(input) -} - -fn no_arg_message(input: &[u8]) -> HwResult { - fn message<'a>( - name: &'a str, - msg: HwProtocolMessage, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { - move |i| map(tag(name), |_| msg.clone())(i) - } - - alt(( - message("PING", Ping), - message("PONG", Pong), - message("LIST", List), - message("BANLIST", BanList), - message("GET_SERVER_VAR", GetServerVar), - message("TOGGLE_READY", ToggleReady), - message("START_GAME", StartGame), - message("TOGGLE_RESTRICT_JOINS", ToggleRestrictJoin), - message("TOGGLE_RESTRICT_TEAMS", ToggleRestrictTeams), - message("TOGGLE_REGISTERED_ONLY", ToggleRegisteredOnly), - ))(input) -} - -fn single_arg_message(input: &[u8]) -> HwResult { - fn message<'a, T, F, G>( - name: &'a str, - parser: F, - constructor: G, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> - where - F: Fn(&[u8]) -> HwResult, - G: Fn(T) -> HwProtocolMessage, - { - map(preceded(tag(name), parser), constructor) - } - - alt(( - message("NICK\n", a_line, Nick), - message("INFO\n", a_line, Info), - message("CHAT\n", a_line, Chat), - message("PART", opt_arg, Part), - message("FOLLOW\n", a_line, Follow), - message("KICK\n", a_line, Kick), - message("UNBAN\n", a_line, Unban), - message("EM\n", a_line, EngineMessage), - message("TEAMCHAT\n", a_line, TeamChat), - message("ROOM_NAME\n", a_line, RoomName), - message("REMOVE_TEAM\n", a_line, RemoveTeam), - message("ROUNDFINISHED", opt_arg, |_| RoundFinished), - message("PROTO\n", u16_line, Proto), - message("QUIT", opt_arg, Quit), - ))(input) -} - -fn cmd_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { - fn cmd_no_arg<'a>( - name: &'a str, - msg: HwProtocolMessage, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { - move |i| map(tag_no_case(name), |_| msg.clone())(i) - } - - fn cmd_single_arg<'a, T, F, G>( - name: &'a str, - parser: F, - constructor: G, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> - where - F: Fn(&'a [u8]) -> HwResult<'a, T>, - G: Fn(T) -> HwProtocolMessage, - { - map( - preceded(pair(tag_no_case(name), spaces), parser), - constructor, - ) - } - - fn cmd_no_arg_message(input: &[u8]) -> HwResult { - alt(( - cmd_no_arg("STATS", Stats), - cmd_no_arg("FIX", Fix), - cmd_no_arg("UNFIX", Unfix), - cmd_no_arg("REGISTERED_ONLY", ToggleServerRegisteredOnly), - cmd_no_arg("SUPER_POWER", SuperPower), - ))(input) - } - - fn cmd_single_arg_message(input: &[u8]) -> HwResult { - alt(( - cmd_single_arg("RESTART_SERVER", |i| tag("YES")(i), |_| RestartServer), - cmd_single_arg("DELEGATE", a_line, Delegate), - cmd_single_arg("DELETE", a_line, Delete), - cmd_single_arg("SAVEROOM", a_line, SaveRoom), - cmd_single_arg("LOADROOM", a_line, LoadRoom), - cmd_single_arg("GLOBAL", a_line, Global), - cmd_single_arg("WATCH", u32_line, Watch), - cmd_single_arg("VOTE", yes_no_line, Vote), - cmd_single_arg("FORCE", yes_no_line, ForceVote), - cmd_single_arg("INFO", a_line, Info), - cmd_single_arg("MAXTEAMS", u8_line, MaxTeams), - cmd_single_arg("CALLVOTE", voting, |v| CallVote(Some(v))), - ))(input) - } - - preceded( - tag("CMD\n"), - alt(( - cmd_no_arg_message, - cmd_single_arg_message, - map(tag_no_case("CALLVOTE"), |_| CallVote(None)), - map(preceded(tag_no_case("GREETING"), opt_space_arg), Greeting), - map(preceded(tag_no_case("PART"), opt_space_arg), Part), - map(preceded(tag_no_case("QUIT"), opt_space_arg), Quit), - map( - preceded( - tag_no_case("SAVE"), - pair(preceded(spaces, cmd_arg), preceded(spaces, cmd_arg)), - ), - |(n, l)| Save(n, l), - ), - map( - preceded( - tag_no_case("RND"), - alt(( - map(peek(end_of_message), |_| vec![]), - preceded(spaces, separated_list(spaces, cmd_arg)), - )), - ), - Rnd, - ), - )), - )(input) -} - -fn config_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> { - fn cfg_single_arg<'a, T, F, G>( - name: &'a str, - parser: F, - constructor: G, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, GameCfg> - where - F: Fn(&[u8]) -> HwResult, - G: Fn(T) -> GameCfg, - { - map(preceded(pair(tag(name), newline), parser), constructor) - } - - let (i, cfg) = preceded( - tag("CFG\n"), - alt(( - cfg_single_arg("THEME", a_line, GameCfg::Theme), - cfg_single_arg("SCRIPT", a_line, GameCfg::Script), - cfg_single_arg("MAP", a_line, GameCfg::MapType), - cfg_single_arg("MAPGEN", u32_line, GameCfg::MapGenerator), - cfg_single_arg("MAZE_SIZE", u32_line, GameCfg::MazeSize), - cfg_single_arg("TEMPLATE", u32_line, GameCfg::Template), - cfg_single_arg("FEATURE_SIZE", u32_line, GameCfg::FeatureSize), - cfg_single_arg("SEED", a_line, GameCfg::Seed), - cfg_single_arg("DRAWNMAP", a_line, GameCfg::DrawnMap), - preceded(pair(tag("AMMO"), newline), |i| { - let (i, name) = a_line(i)?; - let (i, value) = opt_arg(i)?; - Ok((i, GameCfg::Ammo(name, value))) - }), - preceded( - pair(tag("SCHEME"), newline), - map( - pair( - a_line, - alt(( - map(peek(end_of_message), |_| None), - map(preceded(newline, separated_list(newline, a_line)), Some), - )), - ), - |(name, values)| GameCfg::Scheme(name, values.unwrap_or_default()), - ), - ), - )), - )(input)?; - Ok((i, Cfg(cfg))) -} - -fn server_var_message(input: &[u8]) -> HwResult { - map( - preceded( - tag("SET_SERVER_VAR\n"), - alt(( - map(preceded(tag("MOTD_NEW\n"), a_line), ServerVar::MOTDNew), - map(preceded(tag("MOTD_OLD\n"), a_line), ServerVar::MOTDOld), - map( - preceded(tag("LATEST_PROTO\n"), u16_line), - ServerVar::LatestProto, - ), - )), - ), - SetServerVar, - )(input) -} - -fn complex_message(input: &[u8]) -> HwResult { - alt(( - preceded( - pair(tag("PASSWORD"), newline), - map(pair(terminated(a_line, newline), a_line), |(pass, salt)| { - Password(pass, salt) - }), - ), - preceded( - pair(tag("CHECKER"), newline), - map( - tuple(( - terminated(u16_line, newline), - terminated(a_line, newline), - a_line, - )), - |(protocol, name, pass)| Checker(protocol, name, pass), - ), - ), - preceded( - pair(tag("CREATE_ROOM"), newline), - map(pair(a_line, opt_arg), |(name, pass)| CreateRoom(name, pass)), - ), - preceded( - pair(tag("JOIN_ROOM"), newline), - map(pair(a_line, opt_arg), |(name, pass)| JoinRoom(name, pass)), - ), - preceded( - pair(tag("ADD_TEAM"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(u8_line, newline), - terminated(a_line, newline), - terminated(a_line, newline), - terminated(a_line, newline), - terminated(a_line, newline), - terminated(u8_line, newline), - hedgehog_array, - )), - |(name, color, grave, fort, voice_pack, flag, difficulty, hedgehogs)| { - AddTeam(Box::new(TeamInfo { - owner: String::new(), - name, - color, - grave, - fort, - voice_pack, - flag, - difficulty, - hedgehogs, - hedgehogs_number: 0, - })) - }, - ), - ), - preceded( - pair(tag("HH_NUM"), newline), - map( - pair(terminated(a_line, newline), u8_line), - |(name, count)| SetHedgehogsNumber(name, count), - ), - ), - preceded( - pair(tag("TEAM_COLOR"), newline), - map( - pair(terminated(a_line, newline), u8_line), - |(name, color)| SetTeamColor(name, color), - ), - ), - preceded( - pair(tag("BAN"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(a_line, newline), - u32_line, - )), - |(name, reason, time)| Ban(name, reason, time), - ), - ), - preceded( - pair(tag("BAN_IP"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(a_line, newline), - u32_line, - )), - |(ip, reason, time)| BanIp(ip, reason, time), - ), - ), - preceded( - pair(tag("BAN_NICK"), newline), - map( - tuple(( - terminated(a_line, newline), - terminated(a_line, newline), - u32_line, - )), - |(nick, reason, time)| BanNick(nick, reason, time), - ), - ), - ))(input) -} - -pub fn malformed_message(input: &[u8]) -> HwResult<()> { - map(terminated(take_until(&b"\n\n"[..]), end_of_message), |_| ())(input) -} - -pub fn message(input: &[u8]) -> HwResult { - delimited( - take_while(|c| c == b'\n'), - alt(( - no_arg_message, - single_arg_message, - cmd_message, - config_message, - server_var_message, - complex_message, - )), - end_of_message, - )(input) -} - -#[cfg(test)] -mod test { - use super::message; - use crate::{ - core::types::GameCfg, - protocol::{messages::HwProtocolMessage::*, parser::HwProtocolError, test::gen_proto_msg}, - }; - use proptest::{proptest, proptest_helper}; - - #[cfg(test)] - proptest! { - #[test] - fn is_parser_composition_idempotent(ref msg in gen_proto_msg()) { - println!("!! Msg: {:?}, Bytes: {:?} !!", msg, msg.to_raw_protocol().as_bytes()); - assert_eq!(message(msg.to_raw_protocol().as_bytes()), Ok((&b""[..], msg.clone()))) - } - } - - #[test] - fn parse_test() { - assert_eq!(message(b"PING\n\n"), Ok((&b""[..], Ping))); - assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame))); - assert_eq!( - message(b"NICK\nit's me\n\n"), - Ok((&b""[..], Nick("it's me".to_string()))) - ); - assert_eq!(message(b"PROTO\n51\n\n"), Ok((&b""[..], Proto(51)))); - assert_eq!( - message(b"QUIT\nbye-bye\n\n"), - Ok((&b""[..], Quit(Some("bye-bye".to_string())))) - ); - assert_eq!(message(b"QUIT\n\n"), Ok((&b""[..], Quit(None)))); - assert_eq!( - message(b"CMD\nwatch 49471\n\n"), - Ok((&b""[..], Watch(49471))) - ); - assert_eq!( - message(b"BAN\nme\nbad\n77\n\n"), - Ok((&b""[..], Ban("me".to_string(), "bad".to_string(), 77))) - ); - - assert_eq!(message(b"CMD\nPART\n\n"), Ok((&b""[..], Part(None)))); - assert_eq!( - message(b"CMD\nPART _msg_\n\n"), - Ok((&b""[..], Part(Some("_msg_".to_string())))) - ); - - assert_eq!(message(b"CMD\nRND\n\n"), Ok((&b""[..], Rnd(vec![])))); - assert_eq!( - message(b"CMD\nRND A B\n\n"), - Ok((&b""[..], Rnd(vec![String::from("A"), String::from("B")]))) - ); - - assert_eq!( - message(b"CFG\nSCHEME\na\nA\n\n"), - Ok(( - &b""[..], - Cfg(GameCfg::Scheme("a".to_string(), vec!["A".to_string()])) - )) - ); - - assert_eq!( - message(b"QUIT\n1\n2\n\n"), - Err(nom::Err::Error(HwProtocolError::new())) - ); - } -} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/protocol/test.rs --- a/rust/hedgewars-server/src/protocol/test.rs Sun Mar 24 14:05:06 2024 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,252 +0,0 @@ -use proptest::{ - arbitrary::{any, any_with, Arbitrary, StrategyFor}, - strategy::{BoxedStrategy, Just, Map, Strategy}, - test_runner::{Reason, TestRunner}, -}; - -use crate::core::types::{GameCfg, HedgehogInfo, ServerVar, ServerVar::*, TeamInfo, VoteType}; - -use super::messages::{HwProtocolMessage, HwProtocolMessage::*}; - -// Due to inability to define From between Options -trait Into2: Sized { - fn into2(self) -> T; -} -impl Into2 for T { - fn into2(self) -> T { - self - } -} -impl Into2> for Vec { - fn into2(self) -> Vec { - self.into_iter().map(|x| x.0).collect() - } -} -impl Into2 for Ascii { - fn into2(self) -> String { - self.0 - } -} -impl Into2> for Option { - fn into2(self) -> Option { - self.map(|x| x.0) - } -} - -macro_rules! proto_msg_case { - ($val: ident()) => { - Just($val) - }; - ($val: ident($arg: ty)) => { - any::<$arg>().prop_map(|v| $val(v.into2())) - }; - ($val: ident($arg1: ty, $arg2: ty)) => { - any::<($arg1, $arg2)>().prop_map(|v| $val(v.0.into2(), v.1.into2())) - }; - ($val: ident($arg1: ty, $arg2: ty, $arg3: ty)) => { - any::<($arg1, $arg2, $arg3)>().prop_map(|v| $val(v.0.into2(), v.1.into2(), v.2.into2())) - }; -} - -macro_rules! proto_msg_match { - ($var: expr, def = $default: expr, $($num: expr => $constr: ident $res: tt),*) => ( - match $var { - $($num => (proto_msg_case!($constr $res)).boxed()),*, - _ => Just($default).boxed() - } - ) -} - -/// Wrapper type for generating non-empty strings -#[derive(Debug)] -struct Ascii(String); - -impl Arbitrary for Ascii { - type Parameters = ::Parameters; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - "[a-zA-Z0-9]+".prop_map(Ascii).boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for GameCfg { - type Parameters = (); - - fn arbitrary_with(_args: ::Parameters) -> ::Strategy { - use crate::core::types::GameCfg::*; - (0..10) - .no_shrink() - .prop_flat_map(|i| { - proto_msg_match!(i, def = FeatureSize(0), - 0 => FeatureSize(u32), - 1 => MapType(Ascii), - 2 => MapGenerator(u32), - 3 => MazeSize(u32), - 4 => Seed(Ascii), - 5 => Template(u32), - 6 => Ammo(Ascii, Option), - 7 => Scheme(Ascii, Vec), - 8 => Script(Ascii), - 9 => Theme(Ascii), - 10 => DrawnMap(Ascii)) - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for TeamInfo { - type Parameters = (); - - fn arbitrary_with(_args: ::Parameters) -> ::Strategy { - ( - "[a-z]+", - 0u8..127u8, - "[a-z]+", - "[a-z]+", - "[a-z]+", - "[a-z]+", - 0u8..127u8, - ) - .prop_map(|(name, color, grave, fort, voice_pack, flag, difficulty)| { - fn hog(n: u8) -> HedgehogInfo { - HedgehogInfo { - name: format!("hog{}", n), - hat: format!("hat{}", n), - } - } - let hedgehogs = [ - hog(1), - hog(2), - hog(3), - hog(4), - hog(5), - hog(6), - hog(7), - hog(8), - ]; - TeamInfo { - owner: String::new(), - name, - color, - grave, - fort, - voice_pack, - flag, - difficulty, - hedgehogs, - hedgehogs_number: 0, - } - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for ServerVar { - type Parameters = (); - - fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { - (0..=2) - .no_shrink() - .prop_flat_map(|i| { - proto_msg_match!(i, def = ServerVar::LatestProto(0), - 0 => MOTDNew(Ascii), - 1 => MOTDOld(Ascii), - 2 => LatestProto(u16) - ) - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -impl Arbitrary for VoteType { - type Parameters = (); - - fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { - use VoteType::*; - (0..=4) - .no_shrink() - .prop_flat_map(|i| { - proto_msg_match!(i, def = VoteType::Pause, - 0 => Kick(Ascii), - 1 => Map(Option), - 2 => Pause(), - 3 => NewSeed(), - 4 => HedgehogsPerTeam(u8) - ) - }) - .boxed() - } - - type Strategy = BoxedStrategy; -} - -pub fn gen_proto_msg() -> BoxedStrategy where { - let res = (0..=55).no_shrink().prop_flat_map(|i| { - proto_msg_match!(i, def = Ping, - 0 => Ping(), - 1 => Pong(), - 2 => Quit(Option), - 4 => Global(Ascii), - 5 => Watch(u32), - 6 => ToggleServerRegisteredOnly(), - 7 => SuperPower(), - 8 => Info(Ascii), - 9 => Nick(Ascii), - 10 => Proto(u16), - 11 => Password(Ascii, Ascii), - 12 => Checker(u16, Ascii, Ascii), - 13 => List(), - 14 => Chat(Ascii), - 15 => CreateRoom(Ascii, Option), - 16 => JoinRoom(Ascii, Option), - 17 => Follow(Ascii), - 18 => Rnd(Vec), - 19 => Kick(Ascii), - 20 => Ban(Ascii, Ascii, u32), - 21 => BanIp(Ascii, Ascii, u32), - 22 => BanNick(Ascii, Ascii, u32), - 23 => BanList(), - 24 => Unban(Ascii), - 25 => SetServerVar(ServerVar), - 26 => GetServerVar(), - 27 => RestartServer(), - 28 => Stats(), - 29 => Part(Option), - 30 => Cfg(GameCfg), - 31 => AddTeam(Box), - 32 => RemoveTeam(Ascii), - 33 => SetHedgehogsNumber(Ascii, u8), - 34 => SetTeamColor(Ascii, u8), - 35 => ToggleReady(), - 36 => StartGame(), - 37 => EngineMessage(Ascii), - 38 => RoundFinished(), - 39 => ToggleRestrictJoin(), - 40 => ToggleRestrictTeams(), - 41 => ToggleRegisteredOnly(), - 42 => RoomName(Ascii), - 43 => Delegate(Ascii), - 44 => TeamChat(Ascii), - 45 => MaxTeams(u8), - 46 => Fix(), - 47 => Unfix(), - 48 => Greeting(Option), - 49 => CallVote(Option), - 50 => Vote(bool), - 51 => ForceVote(bool), - 52 => Save(Ascii, Ascii), - 53 => Delete(Ascii), - 54 => SaveRoom(Ascii), - 55 => LoadRoom(Ascii) - ) - }); - res.boxed() -} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server.rs --- a/rust/hedgewars-server/src/server.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/server.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,5 +1,8 @@ #[cfg(feature = "official-server")] mod database; +pub mod demo; +mod haskell; #[cfg(feature = "official-server")] pub mod io; pub mod network; +pub mod replaystorage; diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server/database.rs --- a/rust/hedgewars-server/src/server/database.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/server/database.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,14 +1,13 @@ -use mysql; -use mysql::{error::DriverError, error::Error, from_row_opt, params}; -use openssl::sha::sha1; +use mysql_async::{self, from_row_opt, params, prelude::*, Pool}; +use sha1::{Digest, Sha1}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; use crate::handlers::{AccountInfo, Sha1Digest}; const CHECK_ACCOUNT_EXISTS_QUERY: &str = r"SELECT 1 FROM users WHERE users.name = :username LIMIT 1"; -const GET_ACCOUNT_QUERY: &str = - r"SELECT CASE WHEN users.status = 1 THEN users.pass ELSE '' END, +const GET_ACCOUNT_QUERY: &str = r"SELECT CASE WHEN users.status = 1 THEN users.pass ELSE '' END, (SELECT COUNT(users_roles.rid) FROM users_roles WHERE users.uid = users_roles.uid AND users_roles.rid = 3), (SELECT COUNT(users_roles.rid) FROM users_roles WHERE users.uid = users_roles.uid AND users_roles.rid = 13) FROM users WHERE users.name = :username"; @@ -27,104 +26,192 @@ pub struct Achievements {} +pub enum DatabaseQuery { + CheckRegistered { + nick: String, + }, + GetAccount { + nick: String, + protocol: u16, + password_hash: String, + client_salt: String, + server_salt: String, + }, + GetCheckerAccount { + nick: String, + password: String, + }, + GetReplayFilename { + id: u32, + }, +} + +pub enum DatabaseResponse { + AccountRegistered(bool), + Account(Option), + CheckerAccount { is_registered: bool }, +} + pub struct Database { - pool: Option, + pool: Pool, + query_rx: Receiver, + response_tx: Sender, } impl Database { - pub fn new() -> Self { - Self { pool: None } - } - - pub fn connect(&mut self, url: &str) -> Result<(), Error> { - self.pool = Some(mysql::Pool::new(url)?); - - Ok(()) - } - - pub fn is_registered(&mut self, nick: &str) -> Result { - if let Some(pool) = &self.pool { - let is_registered = pool - .first_exec(CHECK_ACCOUNT_EXISTS_QUERY, params! { "username" => nick })? - .is_some(); - Ok(is_registered) - } else { - Err(DriverError::SetupError.into()) + pub fn new(url: &str) -> Self { + let (query_tx, query_rx) = channel(32); + let (response_tx, response_rx) = channel(32); + Self { + pool: Pool::new(url), + query_rx, + response_tx, } } - pub fn get_account( + pub async fn run(&mut self) { + use DatabaseResponse::*; + loop { + let query = self.query_rx.recv().await; + if let Some(query) = query { + match query { + DatabaseQuery::CheckRegistered { nick } => { + let is_registered = self.get_is_registered(&nick).await.unwrap_or(false); + self.response_tx + .send(AccountRegistered(is_registered)) + .await; + } + DatabaseQuery::GetAccount { + nick, + protocol, + password_hash, + client_salt, + server_salt, + } => { + let account = self + .get_account( + &nick, + protocol, + &password_hash, + &client_salt, + &server_salt, + ) + .await + .unwrap_or(None); + self.response_tx.send(Account(account)).await; + } + DatabaseQuery::GetCheckerAccount { nick, password } => { + let is_registered = self + .get_checker_account(&nick, &password) + .await + .unwrap_or(false); + self.response_tx + .send(CheckerAccount { is_registered }) + .await; + } + DatabaseQuery::GetReplayFilename { id } => { + let filename = self.get_replay_name(id).await; + } + }; + } else { + break; + } + } + } + + pub async fn get_is_registered(&mut self, nick: &str) -> mysql_async::Result { + let mut connection = self.pool.get_conn().await?; + let result = CHECK_ACCOUNT_EXISTS_QUERY + .with(params! { "username" => nick }) + .first::(&mut connection) + .await?; + Ok(!result.is_some()) + } + + pub async fn get_account( &mut self, nick: &str, protocol: u16, password_hash: &str, client_salt: &str, server_salt: &str, - ) -> Result, Error> { - if let Some(pool) = &self.pool { - if let Some(row) = pool.first_exec(GET_ACCOUNT_QUERY, params! { "username" => nick })? { - let (mut password, is_admin, is_contributor) = - from_row_opt::<(String, i32, i32)>(row)?; - let client_hash = get_hash(protocol, &password, &client_salt, &server_salt); - let server_hash = get_hash(protocol, &password, &server_salt, &client_salt); - password.replace_range(.., "🦔🦔🦔🦔🦔🦔🦔🦔"); + ) -> mysql_async::Result> { + let mut connection = self.pool.get_conn().await?; + if let Some((mut password, is_admin, is_contributor)) = GET_ACCOUNT_QUERY + .with(params! { "username" => nick }) + .first::<(String, i32, i32), _>(&mut connection) + .await? + { + let client_hash = get_hash(protocol, &password, &client_salt, &server_salt); + let server_hash = get_hash(protocol, &password, &server_salt, &client_salt); + password.replace_range(.., "🦔🦔🦔🦔🦔🦔🦔🦔"); - if client_hash == password_hash { - Ok(Some(AccountInfo { - is_registered: true, - is_admin: is_admin == 1, - is_contributor: is_contributor == 1, - server_hash, - })) - } else { - Ok(None) - } + if client_hash == password_hash { + Ok(Some(AccountInfo { + is_registered: true, + is_admin: is_admin == 1, + is_contributor: is_contributor == 1, + server_hash, + })) } else { Ok(None) } } else { - Err(DriverError::SetupError.into()) + Ok(None) } } - pub fn store_stats(&mut self, stats: &ServerStatistics) -> Result<(), Error> { - if let Some(pool) = &self.pool { - for mut stmt in pool.prepare(STORE_STATS_QUERY).into_iter() { - stmt.execute(params! { - "players" => stats.players, - "rooms" => stats.rooms, - })?; - } - Ok(()) + pub async fn get_checker_account( + &mut self, + nick: &str, + checker_password: &str, + ) -> mysql_async::Result { + let mut connection = self.pool.get_conn().await?; + if let Some((password, _, _)) = GET_ACCOUNT_QUERY + .with(params! { "username" => nick }) + .first::<(String, i32, i32), _>(&mut connection) + .await? + { + Ok(checker_password == password) } else { - Err(DriverError::SetupError.into()) + Ok(false) } } - pub fn store_achievements(&mut self, achievements: &Achievements) -> Result<(), ()> { + pub async fn store_stats(&mut self, stats: &ServerStatistics) -> mysql_async::Result<()> { + let mut connection = self.pool.get_conn().await?; + STORE_STATS_QUERY + .with(params! { + "players" => stats.players, + "rooms" => stats.rooms, + }) + .ignore(&mut connection) + .await + } + + pub async fn store_achievements( + &mut self, + achievements: &Achievements, + ) -> mysql_async::Result<()> { Ok(()) } - pub fn get_replay_name(&mut self, replay_id: u32) -> Result, Error> { - if let Some(pool) = &self.pool { - if let Some(row) = - pool.first_exec(GET_REPLAY_NAME_QUERY, params! { "id" => replay_id })? - { - let filename = from_row_opt::<(String)>(row)?; - Ok(Some(filename)) - } else { - Ok(None) - } - } else { - Err(DriverError::SetupError.into()) - } + pub async fn get_replay_name(&mut self, replay_id: u32) -> mysql_async::Result> { + let mut connection = self.pool.get_conn().await?; + GET_REPLAY_NAME_QUERY + .with(params! { "id" => replay_id }) + .first::(&mut connection) + .await } } fn get_hash(protocol_number: u16, web_password: &str, salt1: &str, salt2: &str) -> Sha1Digest { - let s = format!( + let data = format!( "{}{}{}{}{}", salt1, salt2, web_password, protocol_number, "!hedgewars" ); - Sha1Digest::new(sha1(s.as_bytes())) + + let mut sha1 = Sha1::new(); + sha1.update(&data); + Sha1Digest::new(sha1.finalize().try_into().unwrap()) } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server/demo.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/demo.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,468 @@ +use crate::{core::types::Replay, server::haskell::HaskellValue}; +use hedgewars_network_protocol::types::{ + Ammo, GameCfg, HedgehogInfo, RoomConfig, Scheme, TeamInfo, +}; +use std::{ + collections::HashMap, + fs, + io::{self, BufReader, Read, Write}, + str::FromStr, +}; + +#[derive(PartialEq, Debug)] +pub struct Demo { + teams: Vec, + config: Vec, + messages: Vec, +} + +impl Demo { + fn load_hwd(filename: String) -> io::Result { + let file = fs::File::open(filename)?; + let mut reader = io::BufReader::new(file); + + #[inline] + fn error(cause: &str) -> io::Result { + Err(io::Error::new(io::ErrorKind::InvalidData, cause)) + } + + fn read_command<'a>( + reader: &mut BufReader, + buffer: &'a mut [u8], + ) -> io::Result> { + use io::BufRead; + + let mut size = [0u8; 1]; + if reader.read(&mut size)? == 0 { + Ok(None) + } else { + let text = &mut buffer[0..size[0] as _]; + + if reader.read(text)? < text.len() { + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Incomplete command", + )) + } else { + std::str::from_utf8(text).map(Some).map_err(|e| { + io::Error::new(io::ErrorKind::InvalidInput, "The string is not UTF8") + }) + } + } + } + + fn get_script_name(arg: &str) -> io::Result { + const PREFIX: &str = "Scripts/Multiplayer/"; + const SUFFIX: &str = ".lua"; + if arg.starts_with(PREFIX) && arg.ends_with(SUFFIX) { + let script = arg[PREFIX.len()..arg.len() - SUFFIX.len()].to_string(); + Ok(script.replace('_', " ")) + } else { + error("Script is not multiplayer") + } + } + + fn get_game_flags(arg: &str) -> io::Result> { + const FLAGS: &[u32] = &[ + 0x0000_1000, + 0x0000_0010, + 0x0000_0004, + 0x0000_0008, + 0x0000_0020, + 0x0000_0040, + 0x0000_0080, + 0x0000_0100, + 0x0000_0200, + 0x0000_0400, + 0x0000_0800, + 0x0000_2000, + 0x0000_4000, + 0x0000_8000, + 0x0001_0000, + 0x0002_0000, + 0x0004_0000, + 0x0008_0000, + 0x0010_0000, + 0x0020_0000, + 0x0040_0000, + 0x0080_0000, + 0x0100_0000, + 0x0200_0000, + 0x0400_0000, + ]; + + let flags = u32::from_str(arg).unwrap_or_default(); + let game_flags = FLAGS + .iter() + .map(|flag| (flag & flags != 0).to_string()) + .collect(); + + Ok(game_flags) + } + + let mut config = Vec::new(); + let mut buffer = [0u8; u8::max_value() as _]; + + let mut game_flags = vec![]; + let mut scheme_properties: Vec<_> = [ + "1", "1000", "100", "1", "1", "1000", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", + "1", "", + ] + .iter() + .map(|p| p.to_string()) + .collect(); + const SCHEME_PROPERTY_NAMES: &[&str] = &[ + "$damagepct", + "$turntime", + "", + "$sd_turns", + "$casefreq", + "$minestime", + "$minesnum", + "$minedudpct", + "$explosives", + "$airmines", + "$healthprob", + "$hcaseamount", + "$waterrise", + "$healthdec", + "$ropepct", + "$getawaytime", + "$worldedge", + ]; + const AMMO_PROPERTY_NAMES: &[&str] = &["eammloadt", "eammprob", "eammdelay", "eammreinf"]; + let mut ammo_settings = vec![String::new(); AMMO_PROPERTY_NAMES.len()]; + let mut teams = vec![]; + let mut hog_index = 7usize; + + //todo!("read messages from file"); + let messages = vec![]; + + while let Some(cmd) = read_command(&mut reader, &mut buffer)? { + if let Some(index) = cmd.find(' ') { + match cmd.chars().next().unwrap_or_default() { + 'T' => { + if cmd != "TD" { + let () = error("Not a demo file")?; + } + } + 'e' => { + if let Some(index) = cmd.find(' ') { + let (name, arg) = cmd.split_at(index); + match name { + "script" => config.push(GameCfg::Script(get_script_name(arg)?)), + "map" => config.push(GameCfg::MapType(arg.to_string())), + "theme" => config.push(GameCfg::Theme(arg.to_string())), + "seed" => config.push(GameCfg::Seed(arg.to_string())), + "$gmflags" => game_flags = get_game_flags(arg)?, + "$scriptparam" => { + *scheme_properties.last_mut().unwrap() = arg.to_string() + } + "$template_filter" => config.push(GameCfg::Template( + u32::from_str(arg).unwrap_or_default(), + )), + "$feature_size" => config.push(GameCfg::FeatureSize( + u32::from_str(arg).unwrap_or_default(), + )), + "$map_gen" => config.push(GameCfg::MapGenerator( + u32::from_str(arg).unwrap_or_default(), + )), + "$maze_size" => config.push(GameCfg::MazeSize( + u32::from_str(arg).unwrap_or_default(), + )), + "addteam" => { + let parts = arg.splitn(3, ' ').collect::>(); + let color = parts.get(1).unwrap_or(&"1"); + let name = parts.get(2).unwrap_or(&"Unnamed"); + teams.push(TeamInfo { + color: (u32::from_str(color).unwrap_or(2113696) / 2113696 + - 1) + as u8, + name: name.to_string(), + ..TeamInfo::default() + }); + } + "fort" => teams + .last_mut() + .iter_mut() + .for_each(|t| t.fort = arg.to_string()), + "grave" => teams + .last_mut() + .iter_mut() + .for_each(|t| t.grave = arg.to_string()), + "addhh" => { + hog_index = (hog_index + 1) % 8; + let parts = arg.splitn(3, ' ').collect::>(); + let health = parts.get(1).unwrap_or(&"100"); + teams.last_mut().iter_mut().for_each(|t| { + if let Some(difficulty) = parts.get(0) { + t.difficulty = u8::from_str(difficulty).unwrap_or(0); + } + if let Some(init_health) = parts.get(1) { + scheme_properties[2] = init_health.to_string(); + } + t.hedgehogs_number = (hog_index + 1) as u8; + t.hedgehogs[hog_index].name = + parts.get(2).unwrap_or(&"Unnamed").to_string(); + }); + } + "hat" => { + teams + .last_mut() + .iter_mut() + .for_each(|t| t.hedgehogs[hog_index].hat = arg.to_string()); + } + name => { + if let Some(index) = + SCHEME_PROPERTY_NAMES.iter().position(|n| *n == name) + { + scheme_properties[index] = arg.to_string(); + } else if let Some(index) = + AMMO_PROPERTY_NAMES.iter().position(|n| *n == name) + { + ammo_settings[index] = arg.to_string(); + } + } + } + } + } + '+' => {} + _ => (), + } + } + } + + game_flags.append(&mut scheme_properties); + config.push(GameCfg::Scheme("ADHOG_SCHEME".to_string(), game_flags)); + config.push(GameCfg::Ammo( + "ADHOG_AMMO".to_string(), + Some(ammo_settings.concat()), + )); + + Ok(Demo { + teams, + config, + messages, + }) + } +} + +fn replay_to_haskell(mut replay: Replay) -> HaskellValue { + use HaskellValue as Hs; + + let mut teams = Vec::with_capacity(replay.teams.len()); + for team in replay.teams { + let mut fields = HashMap::::new(); + + fields.insert("teamowner".to_string(), Hs::String(team.owner)); + fields.insert("teamname".to_string(), Hs::String(team.name)); + fields.insert("teamcolor".to_string(), Hs::Number(team.color)); + fields.insert("teamgrave".to_string(), Hs::String(team.grave)); + fields.insert("teamvoicepack".to_string(), Hs::String(team.voice_pack)); + fields.insert("teamflag".to_string(), Hs::String(team.flag)); + fields.insert("difficulty".to_string(), Hs::Number(team.difficulty)); + fields.insert("hhnum".to_string(), Hs::Number(team.hedgehogs_number)); + + let hogs = team + .hedgehogs + .iter() + .map(|hog| Hs::AnonStruct { + name: "HedgehogInfo".to_string(), + fields: vec![Hs::String(hog.name.clone()), Hs::String(hog.hat.clone())], + }) + .collect(); + + fields.insert("hedgehogs".to_string(), Hs::List(hogs)); + + teams.push(Hs::Struct { + name: "TeamInfo".to_string(), + fields, + }) + } + + let mut map_config = vec![]; + let mut game_config = vec![]; + + let mut save_map_config = |name: &str, value: String| { + map_config.push(Hs::Tuple(vec![ + Hs::String(name.to_string()), + Hs::String(value), + ])); + }; + + let config = replay.config; + + save_map_config("FEATURE_SIZE", config.feature_size.to_string()); + save_map_config("MAP", config.map_type); + save_map_config("MAPGEN", config.map_generator.to_string()); + save_map_config("MAZE_SIZE", config.maze_size.to_string()); + save_map_config("SEED", config.seed); + save_map_config("TEMPLATE", config.template.to_string()); + if let Some(drawn_map) = config.drawn_map { + save_map_config("DRAWNMAP", drawn_map); + } + + let mut save_game_config = |name: &str, mut value: Vec| { + game_config.push(Hs::Tuple(vec![ + Hs::String(name.to_string()), + Hs::List(value.drain(..).map(Hs::String).collect()), + ])); + }; + + match config.ammo { + Ammo { + name, + settings: Some(settings), + } => save_game_config("AMMO", vec![name, settings.clone()]), + Ammo { name, .. } => save_game_config("AMMO", vec![name.clone()]), + } + + match config.scheme { + Scheme { name, settings } => { + let mut values = vec![name]; + values.extend_from_slice(&settings); + save_game_config("SCHEME", values); + } + } + + save_game_config("SCRIPT", vec![config.script]); + save_game_config("THEME", vec![config.theme]); + + Hs::Tuple(vec![ + Hs::List(teams), + Hs::List(map_config), + Hs::List(game_config), + Hs::List(replay.message_log.drain(..).map(Hs::String).collect()), + ]) +} + +fn haskell_to_replay(value: HaskellValue) -> Option { + use HaskellValue::*; + let mut config = RoomConfig::new(); + let mut lists = value.into_tuple()?; + let mut lists_iter = lists.drain(..); + + let teams_list = lists_iter.next()?.into_list()?; + let map_config = lists_iter.next()?.into_list()?; + let game_config = lists_iter.next()?.into_list()?; + let engine_messages = lists_iter.next()?.into_list()?; + + let mut teams = Vec::with_capacity(teams_list.len()); + + for team in teams_list { + let (_, mut fields) = team.into_struct()?; + + let mut team_info = TeamInfo::default(); + for (name, value) in fields.drain() { + match &name[..] { + "teamowner" => team_info.owner = value.into_string()?, + "teamname" => team_info.name = value.into_string()?, + "teamcolor" => team_info.color = u8::from_str(&value.into_string()?).ok()?, + "teamgrave" => team_info.grave = value.into_string()?, + "teamfort" => team_info.fort = value.into_string()?, + "teamvoicepack" => team_info.voice_pack = value.into_string()?, + "teamflag" => team_info.flag = value.into_string()?, + "difficulty" => team_info.difficulty = value.into_number()?, + "hhnum" => team_info.hedgehogs_number = value.into_number()?, + "hedgehogs" => { + for (index, hog) in value + .into_list()? + .drain(..) + .enumerate() + .take(team_info.hedgehogs.len()) + { + let (_, mut fields) = hog.into_anon_struct()?; + let mut fields_iter = fields.drain(..); + team_info.hedgehogs[index] = HedgehogInfo { + name: fields_iter.next()?.into_string()?, + hat: fields_iter.next()?.into_string()?, + } + } + } + _ => (), + } + } + teams.push(team_info) + } + + for item in map_config { + let mut tuple = item.into_tuple()?; + let mut tuple_iter = tuple.drain(..); + let name = tuple_iter.next()?.into_string()?; + let value = tuple_iter.next()?.into_string()?; + + match &name[..] { + "FEATURE_SIZE" => config.feature_size = u32::from_str(&value).ok()?, + "MAP" => config.map_type = value, + "MAPGEN" => config.map_generator = u32::from_str(&value).ok()?, + "MAZE_SIZE" => config.maze_size = u32::from_str(&value).ok()?, + "SEED" => config.seed = value, + "TEMPLATE" => config.template = u32::from_str(&value).ok()?, + "DRAWNMAP" => config.drawn_map = Some(value), + _ => {} + }; + } + + for item in game_config { + let mut tuple = item.into_tuple()?; + let mut tuple_iter = tuple.drain(..); + let name = tuple_iter.next()?.into_string()?; + let mut value = tuple_iter.next()?.into_list()?; + let mut value_iter = value.drain(..); + + let config_item = match &name[..] { + "AMMO" => { + config.ammo = Ammo { + name: value_iter.next()?.into_string()?, + settings: value_iter.next().and_then(|v| v.into_string()), + } + } + "SCHEME" => { + config.scheme = Scheme { + name: value_iter.next()?.into_string()?, + settings: value_iter.filter_map(|v| v.into_string()).collect(), + } + } + "SCRIPT" => config.script = value_iter.next()?.into_string()?, + "THEME" => config.theme = value_iter.next()?.into_string()?, + _ => None?, + }; + } + + let mut messages = Vec::with_capacity(engine_messages.len()); + + for message in engine_messages { + messages.push(message.into_string()?); + } + + Some(Replay { + config, + teams, + message_log: messages, + }) +} + +impl Replay { + pub fn save(self, filename: String) -> io::Result<()> { + let text = format!("{}", replay_to_haskell(self)); + let mut file = fs::File::open(filename)?; + file.write(text.as_bytes())?; + Ok(()) + } + + pub fn load(filename: &str) -> io::Result { + let mut file = fs::File::open(filename)?; + let mut bytes = vec![]; + file.read_to_end(&mut bytes)?; + match super::haskell::parse(&bytes[..]) { + Ok((_, value)) => haskell_to_replay(value).ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid replay structure", + )), + Err(_) => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Unable to parse file", + )), + } + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server/haskell.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/haskell.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,412 @@ +use nom::{ + branch::alt, + bytes::complete::{escaped_transform, is_not, tag, take_while, take_while1}, + character::{is_alphanumeric, is_digit, is_space}, + combinator::{map, map_res}, + multi::{many0, separated_list0}, + sequence::{delimited, pair, preceded, separated_pair, terminated}, + ExtendInto, IResult, +}; +use std::{ + collections::HashMap, + fmt::{Display, Error, Formatter}, +}; + +type HaskellResult<'a, T> = IResult<&'a [u8], T>; + +#[derive(Debug, PartialEq)] +pub enum HaskellValue { + Boolean(bool), + Number(u8), + String(String), + Tuple(Vec), + List(Vec), + AnonStruct { + name: String, + fields: Vec, + }, + Struct { + name: String, + fields: HashMap, + }, +} + +impl HaskellValue { + pub fn to_number(&self) -> Option { + match self { + HaskellValue::Number(value) => Some(*value), + _ => None, + } + } + + pub fn into_number(self) -> Option { + match self { + HaskellValue::Number(value) => Some(value), + _ => None, + } + } + + pub fn to_string(&self) -> Option<&str> { + match self { + HaskellValue::String(value) => Some(value), + _ => None, + } + } + + pub fn into_string(self) -> Option { + match self { + HaskellValue::String(value) => Some(value), + _ => None, + } + } + + pub fn into_list(self) -> Option> { + match self { + HaskellValue::List(items) => Some(items), + _ => None, + } + } + + pub fn into_tuple(self) -> Option> { + match self { + HaskellValue::Tuple(items) => Some(items), + _ => None, + } + } + + pub fn into_anon_struct(self) -> Option<(String, Vec)> { + match self { + HaskellValue::AnonStruct { name, fields } => Some((name, fields)), + _ => None, + } + } + + pub fn into_struct(self) -> Option<(String, HashMap)> { + match self { + HaskellValue::Struct { name, fields } => Some((name, fields)), + _ => None, + } + } +} + +fn write_sequence( + f: &mut Formatter<'_>, + brackets: &[u8; 2], + mut items: std::slice::Iter, +) -> Result<(), Error> { + write!(f, "{}", brackets[0] as char)?; + while let Some(value) = items.next() { + write!(f, "{}", value)?; + if !items.as_slice().is_empty() { + write!(f, ", ")?; + } + } + if brackets[1] != b'\0' { + write!(f, "{}", brackets[1] as char) + } else { + Ok(()) + } +} + +fn write_text(f: &mut Formatter<'_>, text: &str) -> Result<(), Error> { + write!(f, "\"")?; + for c in text.chars() { + if c.is_ascii() && !(c as u8).is_ascii_control() { + write!(f, "{}", c)?; + } else { + let mut bytes = [0u8; 4]; + let size = c.encode_utf8(&mut bytes).len(); + for byte in &bytes[0..size] { + write!(f, "\\{:03}", byte)?; + } + } + } + write!(f, "\"") +} + +impl Display for HaskellValue { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + match self { + HaskellValue::Boolean(value) => write!(f, "{}", if *value { "True" } else { "False" }), + HaskellValue::Number(value) => write!(f, "{}", value), + HaskellValue::String(value) => write_text(f, value), + HaskellValue::Tuple(items) => write_sequence(f, b"()", items.iter()), + HaskellValue::List(items) => write_sequence(f, b"[]", items.iter()), + HaskellValue::AnonStruct { name, fields } => { + write!(f, "{} ", name)?; + write_sequence(f, b" \0", fields.iter()) + } + HaskellValue::Struct { name, fields } => { + write!(f, "{} {{", name)?; + let fields = fields.iter().collect::>(); + let mut items = fields.iter(); + while let Some((field_name, value)) = items.next() { + write!(f, "{} = {}", field_name, value)?; + if !items.as_slice().is_empty() { + write!(f, ", ")?; + } + } + write!(f, "}}") + } + } + } +} + +fn comma(input: &[u8]) -> HaskellResult<&[u8]> { + delimited(take_while(is_space), tag(","), take_while(is_space))(input) +} + +fn surrounded<'a, P, O>( + prefix: &'static str, + suffix: &'static str, + mut parser: P, +) -> impl FnMut(&'a [u8]) -> HaskellResult<'a, O> +where + P: FnMut(&'a [u8]) -> HaskellResult<'a, O>, +{ + move |input| { + delimited( + delimited(take_while(is_space), tag(prefix), take_while(is_space)), + |i| parser(i), + delimited(take_while(is_space), tag(suffix), take_while(is_space)), + )(input) + } +} + +fn boolean(input: &[u8]) -> HaskellResult { + map( + alt((map(tag("True"), |_| true), map(tag("False"), |_| false))), + HaskellValue::Boolean, + )(input) +} + +fn number_raw(input: &[u8]) -> HaskellResult { + use std::str::FromStr; + map_res(take_while(is_digit), |s| { + std::str::from_utf8(s) + .map_err(|_| ()) + .and_then(|s| u8::from_str(s).map_err(|_| ())) + })(input) +} + +fn number(input: &[u8]) -> HaskellResult { + map(number_raw, HaskellValue::Number)(input) +} + +enum Escape { + Empty, + Byte(u8), +} + +impl ExtendInto for Escape { + type Item = u8; + type Extender = Vec; + + fn new_builder(&self) -> Self::Extender { + Vec::new() + } + + fn extend_into(&self, acc: &mut Self::Extender) { + if let Escape::Byte(b) = self { + acc.push(*b); + } + } +} + +impl Extend for Vec { + fn extend>(&mut self, iter: T) { + for item in iter { + item.extend_into(self); + } + } +} + +fn string_escape(input: &[u8]) -> HaskellResult { + use Escape::*; + alt(( + map(number_raw, |n| Byte(n)), + alt(( + map(tag("\\"), |_| Byte(b'\\')), + map(tag("\""), |_| Byte(b'\"')), + map(tag("'"), |_| Byte(b'\'')), + map(tag("n"), |_| Byte(b'\n')), + map(tag("r"), |_| Byte(b'\r')), + map(tag("t"), |_| Byte(b'\t')), + map(tag("a"), |_| Byte(b'\x07')), + map(tag("b"), |_| Byte(b'\x08')), + map(tag("v"), |_| Byte(b'\x0B')), + map(tag("f"), |_| Byte(b'\x0C')), + map(tag("&"), |_| Empty), + map(tag("NUL"), |_| Byte(b'\x00')), + map(tag("SOH"), |_| Byte(b'\x01')), + map(tag("STX"), |_| Byte(b'\x02')), + map(tag("ETX"), |_| Byte(b'\x03')), + map(tag("EOT"), |_| Byte(b'\x04')), + map(tag("ENQ"), |_| Byte(b'\x05')), + map(tag("ACK"), |_| Byte(b'\x06')), + )), + alt(( + map(tag("SO"), |_| Byte(b'\x0E')), + map(tag("SI"), |_| Byte(b'\x0F')), + map(tag("DLE"), |_| Byte(b'\x10')), + map(tag("DC1"), |_| Byte(b'\x11')), + map(tag("DC2"), |_| Byte(b'\x12')), + map(tag("DC3"), |_| Byte(b'\x13')), + map(tag("DC4"), |_| Byte(b'\x14')), + map(tag("NAK"), |_| Byte(b'\x15')), + map(tag("SYN"), |_| Byte(b'\x16')), + map(tag("ETB"), |_| Byte(b'\x17')), + map(tag("CAN"), |_| Byte(b'\x18')), + map(tag("EM"), |_| Byte(b'\x19')), + map(tag("SUB"), |_| Byte(b'\x1A')), + map(tag("ESC"), |_| Byte(b'\x1B')), + map(tag("FS"), |_| Byte(b'\x1C')), + map(tag("GS"), |_| Byte(b'\x1D')), + map(tag("RS"), |_| Byte(b'\x1E')), + map(tag("US"), |_| Byte(b'\x1F')), + map(tag("DEL"), |_| Byte(b'\x7F')), + )), + ))(input) +} + +fn string_content(input: &[u8]) -> HaskellResult { + map_res( + escaped_transform(is_not("\"\\"), '\\', string_escape), + |bytes| String::from_utf8(bytes).map_err(|_| ()), + )(input) +} + +fn string(input: &[u8]) -> HaskellResult { + map( + delimited(tag("\""), string_content, tag("\"")), + HaskellValue::String, + )(input) +} + +fn tuple(input: &[u8]) -> HaskellResult { + map( + surrounded("(", ")", separated_list0(comma, value)), + HaskellValue::Tuple, + )(input) +} + +fn list(input: &[u8]) -> HaskellResult { + map( + surrounded("[", "]", separated_list0(comma, value)), + HaskellValue::List, + )(input) +} + +fn identifier(input: &[u8]) -> HaskellResult { + map_res(take_while1(is_alphanumeric), |s| { + std::str::from_utf8(s).map_err(|_| ()).map(String::from) + })(input) +} + +fn named_field(input: &[u8]) -> HaskellResult<(String, HaskellValue)> { + separated_pair( + identifier, + delimited(take_while(is_space), tag("="), take_while(is_space)), + value, + )(input) +} + +fn structure(input: &[u8]) -> HaskellResult { + alt(( + map( + pair( + identifier, + surrounded("{", "}", separated_list0(comma, named_field)), + ), + |(name, mut fields)| HaskellValue::Struct { + name, + fields: fields.drain(..).collect(), + }, + ), + map( + pair( + identifier, + preceded( + take_while(is_space), + many0(terminated(value, take_while(is_space))), + ), + ), + |(name, fields)| HaskellValue::AnonStruct { + name: name.clone(), + fields, + }, + ), + ))(input) +} + +fn value(input: &[u8]) -> HaskellResult { + alt((boolean, number, string, tuple, list, structure))(input) +} + +#[inline] +pub fn parse(input: &[u8]) -> HaskellResult { + delimited(take_while(is_space), value, take_while(is_space))(input) +} + +mod test { + use super::*; + + #[test] + fn terminals() { + use HaskellValue::*; + + matches!(number(b"127"), Ok((_, Number(127)))); + matches!(number(b"adas"), Err(nom::Err::Error(_))); + + assert_eq!( + string(b"\"Hail \\240\\159\\166\\148!\""), + Ok((&b""[..], String("Hail \u{1f994}!".to_string()))) + ); + } + + #[test] + fn sequences() { + use HaskellValue::*; + + let value = Tuple(vec![ + Number(64), + String("text\t1".to_string()), + List(vec![Number(1), Number(2), Number(3)]), + ]); + + assert_eq!( + tuple(b"(64, \"text\\t1\", [1 , 2, 3])"), + Ok((&b""[..], value)) + ); + } + + #[test] + fn structures() { + use HaskellValue::*; + + let value = Struct { + name: "Hog".to_string(), + fields: vec![ + ("name".to_string(), String("\u{1f994}".to_string())), + ("health".to_string(), Number(100)), + ] + .drain(..) + .collect(), + }; + + assert_eq!( + structure(b"Hog {name = \"\\240\\159\\166\\148\", health = 100}"), + Ok((&b""[..], value)) + ); + + let value = AnonStruct { + name: "Hog".to_string(), + fields: vec![Boolean(true), Number(100), String("\u{1f994}".to_string())], + }; + + assert_eq!( + structure(b"Hog True 100 \"\\240\\159\\166\\148\""), + Ok((&b""[..], value)) + ); + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server/io.rs --- a/rust/hedgewars-server/src/server/io.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/server/io.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,7 +1,8 @@ use std::{ fs::{File, OpenOptions}, io::{Error, ErrorKind, Read, Result, Write}, - sync::mpsc, + sync::{mpsc, Arc}, + task::Waker, thread, }; @@ -10,23 +11,22 @@ server::database::Database, }; use log::*; -use mio::{Evented, Poll, PollOpt}; -use mio_extras::channel; pub type RequestId = u32; pub struct IoThread { core_tx: mpsc::Sender<(RequestId, IoTask)>, - core_rx: channel::Receiver<(RequestId, IoResult)>, + core_rx: mpsc::Receiver<(RequestId, IoResult)>, } impl IoThread { - pub fn new() -> Self { + pub fn new(waker: Waker) -> Self { let (core_tx, io_rx) = mpsc::channel(); - let (io_tx, core_rx) = channel::channel(); + let (io_tx, core_rx) = mpsc::channel(); - let mut db = Database::new(); - db.connect("localhost"); + //todo!("convert into an IO task"); + + /*let mut db = Database::new("localhost"); thread::spawn(move || { while let Ok((request_id, task)) = io_rx.recv() { @@ -61,6 +61,18 @@ } } + IoTask::GetCheckerAccount { nick, password } => { + match db.get_checker_account(&nick, &password) { + Ok(is_registered) => IoResult::CheckerAccount { is_registered }, + Err(e) => { + warn!("Unable to get checker account data: {}", e); + IoResult::CheckerAccount { + is_registered: false, + } + } + } + } + IoTask::GetReplay { id } => { let result = match db.get_replay_name(id) { Ok(Some(filename)) => { @@ -72,11 +84,12 @@ &filename } ); - match load_file(&filename) { - Ok(contents) => Some(unimplemented!()), + + match crate::core::types::Replay::load(&filename) { + Ok(replay) => Some(replay), Err(e) => { warn!( - "Error while writing the room config file \"{}\": {}", + "Error while reading replay file \"{}\": {}", filename, e ); None @@ -125,8 +138,9 @@ } }; io_tx.send((request_id, response)); + waker.wake(); } - }); + });*/ Self { core_rx, core_tx } } @@ -142,11 +156,6 @@ Err(mpsc::TryRecvError::Disconnected) => unreachable!(), } } - - pub fn register_rx(&self, poll: &mio::Poll, token: mio::Token) -> Result<()> { - self.core_rx - .register(poll, token, mio::Ready::readable(), PollOpt::edge()) - } } fn save_file(filename: &str, contents: &str) -> Result<()> { diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server/network.rs --- a/rust/hedgewars-server/src/server/network.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/server/network.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,680 +1,395 @@ -extern crate slab; - +use bytes::{Buf, Bytes}; +use log::*; +use slab::Slab; +use std::io::Error; +use std::pin::Pin; +use std::task::{Context, Poll}; use std::{ - collections::HashSet, - io, - io::{Error, ErrorKind, Read, Write}, - mem::{replace, swap}, - net::{IpAddr, Ipv4Addr, SocketAddr}, + iter::Iterator, + net::{IpAddr, SocketAddr}, + time::Duration, }; - -use log::*; -use mio::{ +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}, net::{TcpListener, TcpStream}, - Evented, Poll, PollOpt, Ready, Token, + sync::mpsc::{channel, Receiver, Sender}, }; -use mio_extras::timer; -use netbuf; -use slab::Slab; +#[cfg(feature = "tls-connections")] +use tokio_native_tls::{TlsAcceptor, TlsStream}; use crate::{ - core::{server::HwServer, types::ClientId}, + core::types::ClientId, handlers, - handlers::{IoResult, IoTask}, - protocol::{messages::HwServerMessage::Redirect, messages::*, ProtocolDecoder}, + handlers::{IoResult, IoTask, ServerState}, + protocol::{self, ProtocolDecoder, ProtocolError}, utils, }; +use hedgewars_network_protocol::{ + messages::HwServerMessage::Redirect, messages::*, parser::server_message, +}; -#[cfg(feature = "official-server")] -use super::io::{IoThread, RequestId}; +const PING_TIMEOUT: Duration = Duration::from_secs(15); -#[cfg(feature = "tls-connections")] -use openssl::{ - error::ErrorStack, - ssl::{ - HandshakeError, MidHandshakeSslStream, Ssl, SslContext, SslContextBuilder, SslFiletype, - SslMethod, SslOptions, SslStream, SslStreamBuilder, SslVerifyMode, - }, -}; -use std::time::Duration; +#[derive(Debug)] +enum ClientUpdateData { + Message(HwProtocolMessage), + Error(String), +} -const MAX_BYTES_PER_READ: usize = 2048; -const SEND_PING_TIMEOUT: Duration = Duration::from_secs(30); -const DROP_CLIENT_TIMEOUT: Duration = Duration::from_secs(30); -const PING_PROBES_COUNT: u8 = 2; +#[derive(Debug)] +struct ClientUpdate { + client_id: ClientId, + data: ClientUpdateData, +} -#[derive(Hash, Eq, PartialEq, Copy, Clone)] -pub enum NetworkClientState { - Idle, - NeedsWrite, - NeedsRead, - Closed, - #[cfg(feature = "tls-connections")] - Connected, +struct ClientUpdateSender { + client_id: ClientId, + sender: Sender, } -type NetworkResult = io::Result<(T, NetworkClientState)>; - -pub enum ClientSocket { - Plain(TcpStream), - #[cfg(feature = "tls-connections")] - SslHandshake(Option>), - #[cfg(feature = "tls-connections")] - SslStream(SslStream), +impl ClientUpdateSender { + async fn send(&mut self, data: ClientUpdateData) -> bool { + self.sender + .send(ClientUpdate { + client_id: self.client_id, + data, + }) + .await + .is_ok() + } } -impl ClientSocket { - fn inner(&self) -> &TcpStream { - match self { - ClientSocket::Plain(stream) => stream, +enum ClientStream { + Tcp(TcpStream), + #[cfg(feature = "tls-connections")] + Tls(TlsStream), +} + +impl Unpin for ClientStream {} + +impl AsyncRead for ClientStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + use ClientStream::*; + match Pin::into_inner(self) { + Tcp(stream) => Pin::new(stream).poll_read(cx, buf), #[cfg(feature = "tls-connections")] - ClientSocket::SslHandshake(Some(builder)) => builder.get_ref(), - #[cfg(feature = "tls-connections")] - ClientSocket::SslHandshake(None) => unreachable!(), - #[cfg(feature = "tls-connections")] - ClientSocket::SslStream(ssl_stream) => ssl_stream.get_ref(), + Tls(stream) => Pin::new(stream).poll_read(cx, buf), } } } -pub struct NetworkClient { - id: ClientId, - socket: ClientSocket, - peer_addr: SocketAddr, - decoder: ProtocolDecoder, - buf_out: netbuf::Buf, - timeout: timer::Timeout, - pending_close: bool, -} +impl AsyncWrite for ClientStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + use ClientStream::*; + match Pin::into_inner(self) { + Tcp(stream) => Pin::new(stream).poll_write(cx, buf), + #[cfg(feature = "tls-connections")] + Tls(stream) => Pin::new(stream).poll_write(cx, buf), + } + } -impl NetworkClient { - pub fn new( - id: ClientId, - socket: ClientSocket, - peer_addr: SocketAddr, - timeout: timer::Timeout, - ) -> NetworkClient { - NetworkClient { - id, - socket, - peer_addr, - decoder: ProtocolDecoder::new(), - buf_out: netbuf::Buf::new(), - timeout, - pending_close: false, + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use ClientStream::*; + match Pin::into_inner(self) { + Tcp(stream) => Pin::new(stream).poll_flush(cx), + #[cfg(feature = "tls-connections")] + Tls(stream) => Pin::new(stream).poll_flush(cx), } } - #[cfg(feature = "tls-connections")] - fn handshake_impl( - &mut self, - handshake: MidHandshakeSslStream, - ) -> io::Result { - match handshake.handshake() { - Ok(stream) => { - self.socket = ClientSocket::SslStream(stream); - debug!( - "TLS handshake with {} ({}) completed", - self.id, self.peer_addr - ); - Ok(NetworkClientState::Connected) - } - 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 poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use ClientStream::*; + match Pin::into_inner(self) { + Tcp(stream) => Pin::new(stream).poll_shutdown(cx), + #[cfg(feature = "tls-connections")] + Tls(stream) => Pin::new(stream).poll_shutdown(cx), } } +} - fn read_impl( - decoder: &mut ProtocolDecoder, - source: &mut R, +struct NetworkClient { + id: ClientId, + stream: ClientStream, + receiver: Receiver, + peer_addr: SocketAddr, + decoder: ProtocolDecoder, +} + +impl NetworkClient { + fn new( id: ClientId, - addr: &SocketAddr, - ) -> NetworkResult> { - let mut bytes_read = 0; - let result = loop { - match decoder.read_from(source) { - Ok(bytes) => { - debug!("Client {}: read {} bytes", id, bytes); - bytes_read += bytes; - if bytes == 0 { - let result = if bytes_read == 0 { - info!("EOF for client {} ({})", id, addr); - (Vec::new(), NetworkClientState::Closed) - } else { - (decoder.extract_messages(), NetworkClientState::NeedsRead) - }; - break Ok(result); - } else if bytes_read >= MAX_BYTES_PER_READ { - break Ok((decoder.extract_messages(), NetworkClientState::NeedsRead)); - } - } - Err(ref error) if error.kind() == ErrorKind::WouldBlock => { - let messages = if bytes_read == 0 { - Vec::new() - } else { - decoder.extract_messages() - }; - break Ok((messages, NetworkClientState::Idle)); - } - Err(error) => break Err(error), - } - }; - result - } - - pub fn read(&mut self) -> NetworkResult> { - match self.socket { - ClientSocket::Plain(ref mut stream) => { - NetworkClient::read_impl(&mut self.decoder, stream, self.id, &self.peer_addr) - } - #[cfg(feature = "tls-connections")] - ClientSocket::SslHandshake(ref mut handshake_opt) => { - let handshake = std::mem::replace(handshake_opt, None).unwrap(); - Ok((Vec::new(), self.handshake_impl(handshake)?)) - } - #[cfg(feature = "tls-connections")] - ClientSocket::SslStream(ref mut stream) => { - NetworkClient::read_impl(&mut self.decoder, stream, self.id, &self.peer_addr) - } + stream: ClientStream, + peer_addr: SocketAddr, + receiver: Receiver, + ) -> Self { + Self { + id, + stream, + peer_addr, + receiver, + decoder: ProtocolDecoder::new(PING_TIMEOUT), } } - fn write_impl( - buf_out: &mut netbuf::Buf, - destination: &mut W, - close_on_empty: bool, - ) -> NetworkResult<()> { - let result = loop { - match buf_out.write_to(destination) { - Ok(bytes) if buf_out.is_empty() || bytes == 0 => { - let status = if buf_out.is_empty() && close_on_empty { - NetworkClientState::Closed - } else { - NetworkClientState::Idle - }; - break Ok(((), status)); - } - 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 = match self.socket { - ClientSocket::Plain(ref mut stream) => { - NetworkClient::write_impl(&mut self.buf_out, stream, self.pending_close) - } - #[cfg(feature = "tls-connections")] - ClientSocket::SslHandshake(ref mut handshake_opt) => { - let handshake = std::mem::replace(handshake_opt, None).unwrap(); - Ok(((), self.handshake_impl(handshake)?)) - } - #[cfg(feature = "tls-connections")] - ClientSocket::SslStream(ref mut stream) => { - NetworkClient::write_impl(&mut self.buf_out, stream, self.pending_close) + async fn read( + stream: &mut T, + decoder: &mut ProtocolDecoder, + ) -> protocol::Result { + let result = decoder.read_from(stream).await; + if matches!(result, Err(ProtocolError::Timeout)) { + if Self::write(stream, Bytes::from(HwServerMessage::Ping.to_raw_protocol())).await { + decoder.read_from(stream).await + } else { + Err(ProtocolError::Eof) } - }; - - 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 replace_timeout(&mut self, timeout: timer::Timeout) -> timer::Timeout { - replace(&mut self.timeout, timeout) - } - - pub fn has_pending_sends(&self) -> bool { - !self.buf_out.is_empty() - } -} - -#[cfg(feature = "tls-connections")] -struct ServerSsl { - listener: TcpListener, - context: SslContext, -} - -#[cfg(feature = "official-server")] -pub struct IoLayer { - next_request_id: RequestId, - request_queue: Vec<(RequestId, ClientId)>, - io_thread: IoThread, -} - -#[cfg(feature = "official-server")] -impl IoLayer { - fn new() -> Self { - Self { - next_request_id: 0, - request_queue: vec![], - io_thread: IoThread::new(), + } else { + result } } - fn send(&mut self, client_id: ClientId, task: IoTask) { - let request_id = self.next_request_id; - self.next_request_id += 1; - self.request_queue.push((request_id, client_id)); - self.io_thread.send(request_id, task); + async fn write(stream: &mut T, mut data: Bytes) -> bool { + !data.has_remaining() || matches!(stream.write_buf(&mut data).await, Ok(n) if n > 0) } - fn try_recv(&mut self) -> Option<(ClientId, IoResult)> { - let (request_id, result) = self.io_thread.try_recv()?; - if let Some(index) = self - .request_queue - .iter() - .position(|(id, _)| *id == request_id) - { - let (_, client_id) = self.request_queue.swap_remove(index); - Some((client_id, result)) - } else { - None - } - } + async fn run(mut self, sender: Sender) { + use ClientUpdateData::*; + let mut sender = ClientUpdateSender { + client_id: self.id, + sender, + }; - fn cancel(&mut self, client_id: ClientId) { - let mut index = 0; - while index < self.request_queue.len() { - if self.request_queue[index].1 == client_id { - self.request_queue.swap_remove(index); - } else { - index += 1; + loop { + tokio::select! { + server_message = self.receiver.recv() => { + match server_message { + Some(message) => if !Self::write(&mut self.stream, message).await { + sender.send(Error("Connection reset by peer".to_string())).await; + break; + } + None => { + break; + } + } + } + client_message = Self::read(&mut self.stream, &mut self.decoder) => { + match client_message { + Ok(message) => { + //todo!("add flood stats"); + if !sender.send(Message(message)).await { + break; + } + } + Err(e) => { + //todo!("send cmdline errors"); + //todo!("more graceful shutdown to prevent errors from explicitly closed clients") + sender.send(Error(format!("{}", e))).await; + if matches!(e, ProtocolError::Timeout) { + Self::write(&mut self.stream, Bytes::from(HwServerMessage::Bye("Ping timeout".to_string()).to_raw_protocol())).await; + } + break; + } + } + } } } } } -enum TimeoutEvent { - SendPing { probes_count: u8 }, - DropClient, +#[cfg(feature = "tls-connections")] +struct TlsListener { + listener: TcpListener, + acceptor: TlsAcceptor, } -struct TimerData(TimeoutEvent, ClientId); - pub struct NetworkLayer { listener: TcpListener, - server: HwServer, - clients: Slab, - pending: HashSet<(ClientId, NetworkClientState)>, - pending_cache: Vec<(ClientId, NetworkClientState)>, #[cfg(feature = "tls-connections")] - ssl: ServerSsl, - #[cfg(feature = "official-server")] - io: IoLayer, - timer: timer::Timer, -} - -fn register_read(poll: &Poll, evented: &E, token: mio::Token) -> io::Result<()> { - poll.register(evented, token, Ready::readable(), PollOpt::edge()) -} - -fn create_ping_timeout( - timer: &mut timer::Timer, - probes_count: u8, - client_id: ClientId, -) -> timer::Timeout { - timer.set_timeout( - SEND_PING_TIMEOUT, - TimerData(TimeoutEvent::SendPing { probes_count }, client_id), - ) -} - -fn create_drop_timeout(timer: &mut timer::Timer, client_id: ClientId) -> timer::Timeout { - timer.set_timeout( - DROP_CLIENT_TIMEOUT, - TimerData(TimeoutEvent::DropClient, client_id), - ) + tls: TlsListener, + server_state: ServerState, + clients: Slab>, + update_tx: Sender, + update_rx: Receiver } impl NetworkLayer { - pub fn register(&self, poll: &Poll) -> io::Result<()> { - register_read(poll, &self.listener, utils::SERVER_TOKEN)?; - #[cfg(feature = "tls-connections")] - register_read(poll, &self.ssl.listener, utils::SECURE_SERVER_TOKEN)?; - register_read(poll, &self.timer, utils::TIMER_TOKEN)?; - - #[cfg(feature = "official-server")] - self.io.io_thread.register_rx(poll, utils::IO_TOKEN)?; - - Ok(()) - } - - fn deregister_client(&mut self, poll: &Poll, id: ClientId, is_error: bool) { - if let Some(ref mut client) = self.clients.get_mut(id) { - poll.deregister(client.socket.inner()) - .expect("could not deregister socket"); - if client.has_pending_sends() && !is_error { - info!( - "client {} ({}) pending removal", - client.id, client.peer_addr - ); - client.pending_close = true; - poll.register( - client.socket.inner(), - Token(id), - Ready::writable(), - PollOpt::edge(), - ) - .unwrap_or_else(|_| { - self.clients.remove(id); - }); - } else { - info!("client {} ({}) removed", client.id, client.peer_addr); - self.clients.remove(id); + pub async fn run(&mut self) { + async fn accept_plain_branch( + layer: &mut NetworkLayer, + value: (TcpStream, SocketAddr), + update_tx: Sender, + ) { + let (stream, addr) = value; + if let Some(client) = layer.create_client(ClientStream::Tcp(stream), addr).await { + tokio::spawn(client.run(update_tx)); } - #[cfg(feature = "official-server")] - self.io.cancel(id); - } - } - - fn register_client( - &mut self, - poll: &Poll, - client_socket: ClientSocket, - addr: SocketAddr, - ) -> io::Result { - let entry = self.clients.vacant_entry(); - let client_id = entry.key(); - - poll.register( - client_socket.inner(), - Token(client_id), - Ready::readable() | Ready::writable(), - PollOpt::edge(), - )?; - - let client = NetworkClient::new( - client_id, - client_socket, - addr, - create_ping_timeout(&mut self.timer, PING_PROBES_COUNT - 1, client_id), - ); - info!("client {} ({}) added", client.id, client.peer_addr); - entry.insert(client); - - Ok(client_id) - } - - fn handle_response(&mut self, mut response: handlers::Response, poll: &Poll) { - if response.is_empty() { - return; } - debug!("{} pending server messages", response.len()); - let output = response.extract_messages(&mut self.server); - for (clients, message) in output { - 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)); + #[cfg(feature = "tls-connections")] + async fn accept_tls_branch( + layer: &mut NetworkLayer, + value: (TcpStream, SocketAddr), + update_tx: Sender, + ) { + let (stream, addr) = value; + match layer.tls.acceptor.accept(stream).await { + Ok(stream) => { + if let Some(client) = layer.create_client(ClientStream::Tls(stream), addr).await + { + tokio::spawn(client.run(update_tx)); + } + } + Err(e) => { + warn!("Unable to establish TLS connection: {}", e); } } } - for client_id in response.extract_removed_clients() { - self.deregister_client(poll, client_id, false); + async fn client_message_branch( + layer: &mut NetworkLayer, + client_message: Option, + ) { + use ClientUpdateData::*; + match client_message { + Some(ClientUpdate { + client_id, + data: Message(message), + }) => { + layer.handle_message(client_id, message).await; + } + Some(ClientUpdate { + client_id, + data: Error(e), + }) => { + let mut response = handlers::Response::new(client_id); + info!("Client {} error: {:?}", client_id, e); + response.remove_client(client_id); + handlers::handle_client_loss(&mut layer.server_state, client_id, &mut response); + layer.handle_response(response).await; + } + None => unreachable!(), + } } - #[cfg(feature = "official-server")] - { - let client_id = response.client_id(); - for task in response.extract_io_tasks() { - self.io.send(client_id, task); + //todo!("add the DB task"); + //todo!("add certfile watcher task"); + loop { + #[cfg(not(feature = "tls-connections"))] + tokio::select! { + Ok(value) = self.listener.accept() => accept_plain_branch(self, value, self.update_tx.clone()).await, + client_message = self.update_rx.recv(), if !self.clients.is_empty() => client_message_branch(self, client_message).await + } + + #[cfg(feature = "tls-connections")] + tokio::select! { + Ok(value) = self.listener.accept() => accept_plain_branch(self, value, self.update_tx.clone()).await, + Ok(value) = self.tls.listener.accept() => accept_tls_branch(self, value, self.update_tx.clone()).await, + client_message = self.update_rx.recv(), if !self.clients.is_empty() => client_message_branch(self, client_message).await } } } - pub fn handle_timeout(&mut self, poll: &Poll) -> io::Result<()> { - while let Some(TimerData(event, client_id)) = self.timer.poll() { - match event { - TimeoutEvent::SendPing { probes_count } => { - if let Some(ref mut client) = self.clients.get_mut(client_id) { - client.send_string(&HwServerMessage::Ping.to_raw_protocol()); - client.write()?; - let timeout = if probes_count != 0 { - create_ping_timeout(&mut self.timer, probes_count - 1, client_id) - } else { - create_drop_timeout(&mut self.timer, client_id) - }; - client.replace_timeout(timeout); - } - } - TimeoutEvent::DropClient => { - if let Some(ref mut client) = self.clients.get_mut(client_id) { - client.send_string( - &HwServerMessage::Bye("Ping timeout".to_string()).to_raw_protocol(), - ); - client.write(); - } - self.operation_failed( - poll, - client_id, - &ErrorKind::TimedOut.into(), - "No ping response", - )?; - } - } - } - Ok(()) - } + async fn create_client( + &mut self, + stream: ClientStream, + addr: SocketAddr, + ) -> Option { + let entry = self.clients.vacant_entry(); + let client_id = entry.key(); + let (tx, rx) = channel(16); + entry.insert(tx); + + let client = NetworkClient::new(client_id, stream, addr, rx); + + info!("client {} ({}) added", client.id, client.peer_addr); + + let mut response = handlers::Response::new(client_id); - #[cfg(feature = "official-server")] - pub fn handle_io_result(&mut self, poll: &Poll) -> io::Result<()> { - while let Some((client_id, result)) = self.io.try_recv() { - debug!("Handling io result {:?} for client {}", result, client_id); - let mut response = handlers::Response::new(client_id); - handlers::handle_io_result(&mut self.server, client_id, &mut response, result); - self.handle_response(response, poll); - } - Ok(()) - } + let added = if let IpAddr::V4(addr) = client.peer_addr.ip() { + handlers::handle_client_accept( + &mut self.server_state, + client_id, + &mut response, + addr.octets(), + addr.is_loopback(), + ) + } else { + todo!("implement something") + }; - fn create_client_socket(&self, socket: TcpStream) -> io::Result { - Ok(ClientSocket::Plain(socket)) - } + self.handle_response(response).await; - #[cfg(feature = "tls-connections")] - fn create_client_secure_socket(&self, socket: TcpStream) -> io::Result { - 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")) - } + if added { + Some(client) + } else { + None } } - fn init_client(&mut self, poll: &Poll, client_id: ClientId) { + async fn handle_message(&mut self, client_id: ClientId, message: HwProtocolMessage) { + debug!("Handling message {:?} for client {}", message, client_id); let mut response = handlers::Response::new(client_id); - - if let ClientSocket::Plain(_) = self.clients[client_id].socket { - #[cfg(feature = "tls-connections")] - response.add(Redirect(self.ssl.listener.local_addr().unwrap().port()).send_self()) - } - - handlers::handle_client_accept( - &mut self.server, - client_id, - &mut response, - self.clients[client_id].peer_addr.ip().is_loopback(), - ); - self.handle_response(response, poll); - } - - pub fn accept_client(&mut self, poll: &Poll, server_token: mio::Token) -> io::Result<()> { - match server_token { - utils::SERVER_TOKEN => { - let (client_socket, addr) = self.listener.accept()?; - info!("Connected(plaintext): {}", addr); - let client_id = - self.register_client(poll, self.create_client_socket(client_socket)?, addr)?; - self.init_client(poll, client_id); - } - #[cfg(feature = "tls-connections")] - utils::SECURE_SERVER_TOKEN => { - let (client_socket, addr) = self.ssl.listener.accept()?; - info!("Connected(TLS): {}", addr); - self.register_client(poll, self.create_client_secure_socket(client_socket)?, addr)?; - } - _ => unreachable!(), - } - - 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) + handlers::handle(&mut self.server_state, client_id, &mut response, message); + self.handle_response(response).await; } - 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) { - let timeout = client.replace_timeout(create_ping_timeout( - &mut self.timer, - PING_PROBES_COUNT - 1, - client_id, - )); - self.timer.cancel_timeout(&timeout); - client.read() - } else { - warn!("invalid readable client: {}", client_id); - Ok((Vec::new(), NetworkClientState::Idle)) - }; - - let mut response = handlers::Response::new(client_id); - - match messages { - Ok((messages, state)) => { - for message in messages { - debug!("Handling message {:?} for client {}", message, client_id); - handlers::handle(&mut self.server, client_id, &mut response, message); - } - match state { - NetworkClientState::NeedsRead => { - self.pending.insert((client_id, state)); - } - NetworkClientState::Closed => self.client_error(&poll, client_id)?, - #[cfg(feature = "tls-connections")] - NetworkClientState::Connected => self.init_client(poll, client_id), - _ => {} - }; - } - Err(e) => self.operation_failed( - poll, - client_id, - &e, - "Error while reading from client socket", - )?, + async fn handle_response(&mut self, mut response: handlers::Response) { + if response.is_empty() { + return; } - self.handle_response(response, poll); + for client_id in response.extract_removed_clients() { + if self.clients.contains(client_id) { + self.clients.remove(client_id); + if self.clients.is_empty() { + let (update_tx, update_rx) = channel(128); + self.update_rx = update_rx; + self.update_tx = update_tx; + } + } + info!("Client {} removed", client_id); + } - Ok(()) + debug!("{} pending server messages", response.len()); + let output = response.extract_messages(&mut self.server_state.server); + for (clients, message) in output { + debug!("Message {:?} to {:?}", message, clients); + Self::send_message(&mut self.clients, message, clients.iter().cloned()).await; + } } - 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(((), state)) if state == NetworkClientState::Closed => { - self.deregister_client(poll, client_id, false); - } - Ok(_) => (), - Err(e) => { - self.operation_failed(poll, client_id, &e, "Error while writing to client socket")? + async fn send_message( + clients: &mut Slab>, + message: HwServerMessage, + to_clients: I, + ) where + I: Iterator, + { + let msg_string = message.to_raw_protocol(); + let bytes = Bytes::copy_from_slice(msg_string.as_bytes()); + for client_id in to_clients { + if let Some(client) = clients.get_mut(client_id) { + if !client.send(bytes.clone()).await.is_ok() { + clients.remove(client_id); + } } } - - Ok(()) - } - - pub fn client_error(&mut self, poll: &Poll, client_id: ClientId) -> io::Result<()> { - let pending_close = self.clients[client_id].pending_close; - self.deregister_client(poll, client_id, true); - - if !pending_close { - let mut response = handlers::Response::new(client_id); - handlers::handle_client_loss(&mut self.server, client_id, &mut response); - self.handle_response(response, poll); - } - - 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(()) } } pub struct NetworkLayerBuilder { listener: Option, - secure_listener: Option, + #[cfg(feature = "tls-connections")] + tls_listener: Option, + #[cfg(feature = "tls-connections")] + tls_acceptor: Option, clients_capacity: usize, rooms_capacity: usize, } @@ -685,7 +400,10 @@ clients_capacity: 1024, rooms_capacity: 512, listener: None, - secure_listener: None, + #[cfg(feature = "tls-connections")] + tls_listener: None, + #[cfg(feature = "tls-connections")] + tls_acceptor: None, } } } @@ -698,52 +416,39 @@ } } - pub fn with_secure_listener(self, listener: TcpListener) -> Self { + #[cfg(feature = "tls-connections")] + pub fn with_tls_acceptor(self, listener: TlsAcceptor) -> Self { Self { - secure_listener: Some(listener), + tls_acceptor: Option::from(listener), ..self } } #[cfg(feature = "tls-connections")] - fn create_ssl_context(listener: TcpListener) -> 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) - .expect("Cannot find certificate file"); - builder - .set_private_key_file("ssl/key.pem", SslFiletype::PEM) - .expect("Cannot find private key file"); - builder.set_options(SslOptions::NO_COMPRESSION); - builder.set_cipher_list("DEFAULT:!LOW:!RC4:!EXP").unwrap(); - ServerSsl { - listener, - context: builder.build(), + pub fn with_tls_listener(self, listener: TlsAcceptor) -> Self { + Self { + tls_acceptor: Option::from(listener), + ..self } } pub fn build(self) -> NetworkLayer { - let server = HwServer::new(self.clients_capacity, self.rooms_capacity); + let server_state = ServerState::new(self.clients_capacity, self.rooms_capacity); + let clients = Slab::with_capacity(self.clients_capacity); - let pending = HashSet::with_capacity(2 * self.clients_capacity); - let pending_cache = Vec::with_capacity(2 * self.clients_capacity); - let timer = timer::Builder::default().build(); + let (update_tx, update_rx) = channel(128); NetworkLayer { listener: self.listener.expect("No listener provided"), - server, + #[cfg(feature = "tls-connections")] + tls: TlsListener { + listener: self.tls_listener.expect("No TLS listener provided"), + acceptor: self.tls_acceptor.expect("No TLS acceptor provided"), + }, + server_state, clients, - pending, - pending_cache, - #[cfg(feature = "tls-connections")] - ssl: Self::create_ssl_context( - self.secure_listener.expect("No secure listener provided"), - ), - #[cfg(feature = "official-server")] - io: IoLayer::new(), - timer, + update_tx, + update_rx } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/server/replaystorage.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/server/replaystorage.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,74 @@ +use crate::core::types::Replay; +//use super::demo::load; +use std::fs; +use std::path::PathBuf; + +pub struct ReplayStorage { + borrowed_replays: Vec, +} + +#[derive(Clone, PartialEq)] +pub struct ReplayId { + path: PathBuf, +} + +impl ReplayStorage { + pub fn new() -> Self { + ReplayStorage { + borrowed_replays: vec![], + } + } + + pub fn pick_replay(&mut self, protocol: u16) -> Option<(ReplayId, Replay)> { + let protocol_suffix = format!(".{}", protocol); + let result = fs::read_dir("replays") + .ok()? + .flat_map(|f| Some(f.ok()?.path())) + .find(|f| { + f.ends_with(&protocol_suffix) && !self.borrowed_replays.iter().any(|e| &e.path == f) + }) + .and_then(|f| { + Some(( + ReplayId { path: f.clone() }, + Replay::load(f.to_str()?).ok()?, + )) + }); + + if let Some((ref replay_id, _)) = result { + self.borrowed_replays.push((*replay_id).clone()); + } + + result + } + + pub fn move_failed_replay(&mut self, id: &ReplayId) -> std::io::Result<()> { + self.unborrow(id); + self.move_file("failed", id) + } + + pub fn move_checked_replay(&mut self, id: &ReplayId) -> std::io::Result<()> { + self.unborrow(id); + self.move_file("checked", id) + } + + pub fn requeue_replay(&mut self, id: &ReplayId) { + self.unborrow(id) + } + + fn unborrow(&mut self, id: &ReplayId) { + self.borrowed_replays.retain(|i| i != id) + } + + fn move_file(&self, dir: &str, id: &ReplayId) -> std::io::Result<()> { + let new_name = format!( + "{}/{}", + dir, + id.path + .file_name() + .and_then(|f| f.to_str()) + .expect("What's up with your file name?") + ); + + fs::rename(&id.path, new_name) + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/hedgewars-server/src/utils.rs --- a/rust/hedgewars-server/src/utils.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hedgewars-server/src/utils.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,12 +1,8 @@ use base64::encode; -use mio; use std::iter::Iterator; pub const SERVER_VERSION: u32 = 3; -pub const SERVER_TOKEN: mio::Token = mio::Token(1_000_000_000); -pub const SECURE_SERVER_TOKEN: mio::Token = mio::Token(1_000_000_001); -pub const TIMER_TOKEN: mio::Token = mio::Token(1_000_000_002); -pub const IO_TOKEN: mio::Token = mio::Token(1_000_000_003); +pub const SERVER_MESSAGE: &str = &"Hedgewars server https://www.hedgewars.org/"; pub fn is_name_illegal(name: &str) -> bool { name.len() > 40 @@ -70,7 +66,7 @@ 57 => "0.9.25", 58 => "1.0.0-dev", 59 => "1.0.0", - 60 => "1.0.1-dev", + 60 => "1.1.0-dev", _ => "Unknown", } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/Cargo.toml --- a/rust/hwphysics/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -8,3 +8,10 @@ fpnum = { path = "../fpnum" } integral-geometry = { path = "../integral-geometry" } land2d = { path = "../land2d" } + +[dev-dependencies] +criterion = "0.4.0" + +[[bench]] +name = "ecs_bench" +harness = false \ No newline at end of file diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/benches/ecs_bench.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hwphysics/benches/ecs_bench.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,119 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use hwphysics::{common::GearId, data::GearDataManager}; + +#[derive(Clone, Copy, Default)] +struct P { + position: u64, +} + +#[derive(Clone, Copy, Default)] +struct V { + velocity: u64, +} + +#[derive(Clone, Copy, Default)] +struct Pv { + position: u64, + velocity: u64, +} + +const SIZE: usize = 4 * 1024; + +pub fn array_run(c: &mut Criterion) { + let mut items = [Pv::default(); SIZE]; + + c.bench_function("array run", |b| { + b.iter(|| { + for item in &mut items { + item.velocity += black_box(item.position); + } + }) + }); +} + +pub fn component_run(c: &mut Criterion) { + let mut manager = GearDataManager::new(); + + manager.register::

(); + manager.register::(); + + for i in 1..=SIZE { + let gear_id = GearId::new(i as u16).unwrap(); + manager.add(gear_id, &P::default()); + manager.add(gear_id, &V::default()); + } + + c.bench_function("component run", |b| { + b.iter(|| { + manager + .iter() + .run(|(p, v): (&mut P, &mut V)| v.velocity += black_box(p.position)); + }) + }); +} + +pub fn component_multirun(c: &mut Criterion) { + for n in (16..=64).step_by(16) { + let mut manager = GearDataManager::new(); + + manager.register::

(); + manager.register::(); + + for i in 1..=(SIZE / n) { + let gear_id = GearId::new(i as u16).unwrap(); + manager.add(gear_id, &P::default()); + manager.add(gear_id, &V::default()); + } + + c.bench_function(&format!("component run {}", n), |b| { + b.iter(|| { + for i in 0..n { + manager + .iter() + .run(|(p, v): (&mut P, &mut V)| v.velocity += black_box(p.position)); + } + }) + }); + } +} + +pub fn component_add_remove(c: &mut Criterion) { + let mut manager = GearDataManager::new(); + let mut gears1 = vec![]; + let mut gears2 = vec![]; + + manager.register::

(); + manager.register::(); + + for i in 1..=SIZE { + let gear_id = GearId::new(i as u16).unwrap(); + manager.add(gear_id, &P::default()); + if i % 2 == 0 { + manager.add(gear_id, &V::default()); + gears1.push(gear_id); + } else { + gears2.push(gear_id); + } + } + + c.bench_function("component add/remove", |b| { + b.iter(|| { + for id in &gears2 { + manager.add(*id, &V::default()); + } + for id in &gears1 { + manager.remove::(*id); + } + std::mem::swap(&mut gears1, &mut gears2); + }) + }); +} + +criterion_group!( + benches, + array_run, + component_run, + component_multirun, + component_add_remove +); +criterion_main!(benches); diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/collision.rs --- a/rust/hwphysics/src/collision.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/src/collision.rs Sun Mar 24 14:33:57 2024 -0400 @@ -3,13 +3,9 @@ use crate::{common::GearId, data::GearDataManager, grid::Grid}; use fpnum::*; -use integral_geometry::{Point, Size}; +use integral_geometry::{Point, PotSize}; use land2d::Land2D; -pub fn fppoint_round(point: &FPPoint) -> Point { - Point::new(point.x().round(), point.y().round()) -} - #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub struct CircleBounds { pub center: FPPoint, @@ -90,7 +86,7 @@ position: &FPPoint, ) { self.pairs.push((contact_gear_id1, contact_gear_id2)); - self.positions.push(fppoint_round(&position)); + self.positions.push(Point::from_fppoint(&position)); } pub fn clear(&mut self) { @@ -105,7 +101,7 @@ data.register::(); } - pub fn new(size: Size) -> Self { + pub fn new(size: PotSize) -> Self { Self { grid: Grid::new(size), enabled_collisions: EnabledCollisionsCollection::new(), @@ -114,11 +110,11 @@ } pub fn add(&mut self, gear_id: GearId, gear_data: CollisionData) { - self.grid.insert_static(gear_id, &gear_data.bounds); + self.grid.insert(gear_id, &gear_data.bounds); } pub fn remove(&mut self, gear_id: GearId) { - self.grid.remove(gear_id); + self.grid.remove(gear_id, None); } pub fn get(&mut self, gear_id: GearId) -> Option { @@ -131,10 +127,6 @@ updates: &crate::physics::PositionUpdates, ) -> &DetectedCollisions { self.detected_collisions.clear(); - for (id, old_position, new_position) in updates.iter() { - self.grid.update_position(id, old_position, new_position) - } - self.grid.check_collisions(&mut self.detected_collisions); for (gear_id, collision) in self.enabled_collisions.iter() { diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/common.rs --- a/rust/hwphysics/src/common.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/src/common.rs Sun Mar 24 14:33:57 2024 -0400 @@ -9,12 +9,12 @@ impl Millis { #[inline] - pub fn new(value: u32) -> Self { + pub const fn new(value: u32) -> Self { Self(value) } #[inline] - pub fn get(self) -> u32 { + pub const fn get(self) -> u32 { self.0 } diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/data.rs --- a/rust/hwphysics/src/data.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/src/data.rs Sun Mar 24 14:33:57 2024 -0400 @@ -9,17 +9,28 @@ slice, }; +const MAX_TYPES: usize = 8; + pub trait TypeTuple: Sized { - fn get_types(types: &mut Vec); + fn get_types(_types: &mut [TypeId; MAX_TYPES]) -> usize; } impl TypeTuple for () { - fn get_types(_types: &mut Vec) {} + fn get_types(_types: &mut [TypeId; MAX_TYPES]) -> usize { + 0 + } } impl TypeTuple for &T { - fn get_types(types: &mut Vec) { - types.push(TypeId::of::()); + fn get_types(types: &mut [TypeId; MAX_TYPES]) -> usize { + if MAX_TYPES > 0 { + unsafe { + *types.get_unchecked_mut(0) = TypeId::of::(); + } + 1 + } else { + 0 + } } } @@ -30,13 +41,22 @@ macro_rules! type_tuple_impl { ($($n: literal: $t: ident),+) => { impl<$($t: 'static),+> TypeTuple for ($(&$t),+,) { - fn get_types(types: &mut Vec) { - $(types.push(TypeId::of::<$t>()));+ + fn get_types(types: &mut [TypeId; MAX_TYPES]) -> usize { + let mut count = 0; + $({ + if MAX_TYPES > $n { + unsafe { + *types.get_unchecked_mut($n) = TypeId::of::<$t>(); + } + count = $n + 1; + } + });+ + count } } impl<$($t: 'static),+> TypeIter for ($(&$t),+,) { - unsafe fn iter(slices: &[*mut u8], count: usize, mut f: F) { + unsafe fn iter(slices: &[*mut u8], count: usize, mut f: FI) { for i in 0..count { f(*(*slices.get_unchecked(0) as *const GearId).add(i), ($(&*(*slices.get_unchecked($n + 1) as *mut $t).add(i)),+,)); @@ -45,13 +65,22 @@ } impl<$($t: 'static),+> TypeTuple for ($(&mut $t),+,) { - fn get_types(types: &mut Vec) { - $(types.push(TypeId::of::<$t>()));+ + fn get_types(types: &mut [TypeId; MAX_TYPES]) -> usize { + let mut count = 0; + $({ + if MAX_TYPES > $n { + unsafe { + *types.get_unchecked_mut($n) = TypeId::of::<$t>(); + } + count = $n + 1; + } + });+ + count } } impl<$($t: 'static),+> TypeIter for ($(&mut $t),+,) { - unsafe fn iter(slices: &[*mut u8], count: usize, mut f: F) { + unsafe fn iter(slices: &[*mut u8], count: usize, mut f: FI) { for i in 0..count { f(*(*slices.get_unchecked(0) as *const GearId).add(i), ($(&mut *(*slices.get_unchecked($n + 1) as *mut $t).add(i)),+,)); @@ -66,6 +95,9 @@ type_tuple_impl!(0: A, 1: B, 2: C); type_tuple_impl!(0: A, 1: B, 2: C, 3: D); type_tuple_impl!(0: A, 1: B, 2: C, 3: D, 4: E); +type_tuple_impl!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F); +type_tuple_impl!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G); +type_tuple_impl!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H); const BLOCK_SIZE: usize = 32768; @@ -140,14 +172,17 @@ .add(size_of::() * max_elements as usize) }; - for i in 0..element_sizes.len() { - if mask & (1 << i as u64) != 0 { - unsafe { - address = address.add(address.align_offset(element_alignments[i] as usize)); - blocks[i] = Some(NonNull::new_unchecked(address)); - address = address.add(element_sizes[i] as usize * max_elements as usize) - }; - } + let mut mask_bits = mask; + while mask_bits != 0 { + let i = mask_bits.trailing_zeros() as usize; + + unsafe { + address = address.add(address.align_offset(element_alignments[i] as usize)); + blocks[i] = Some(NonNull::new_unchecked(address)); + address = address.add(element_sizes[i] as usize * max_elements as usize) + }; + + mask_bits &= mask_bits - 1; } Self { @@ -159,6 +194,7 @@ } } + #[inline] fn gear_ids(&self) -> &[GearId] { unsafe { slice::from_raw_parts( @@ -168,6 +204,7 @@ } } + #[inline] fn gear_ids_mut(&mut self) -> &mut [GearId] { unsafe { slice::from_raw_parts_mut( @@ -177,6 +214,7 @@ } } + #[inline] fn is_full(&self) -> bool { self.elements_count == self.max_elements } @@ -218,6 +256,11 @@ } #[inline] + fn without_type(&self, type_bit: u64) -> Self { + Self::new(self.type_mask & !type_bit, self.tag_mask) + } + + #[inline] fn with_tag(&self, tag_bit: u64) -> Self { Self::new(self.type_mask, self.tag_mask | tag_bit) } @@ -242,7 +285,7 @@ block_masks: vec![], element_sizes: Box::new([0; 64]), element_alignments: Box::new([0; 64]), - lookup: vec![LookupEntry::default(); u16::max_value() as usize].into_boxed_slice(), + lookup: vec![LookupEntry::default(); u16::MAX as usize].into_boxed_slice(), } } @@ -267,7 +310,7 @@ debug_assert!(src_block_index != dest_block_index); let src_mask = self.block_masks[src_block_index as usize]; let dest_mask = self.block_masks[dest_block_index as usize]; - debug_assert!(src_mask.type_mask & dest_mask.type_mask == src_mask.type_mask); + debug_assert!(src_mask.type_mask & dest_mask.type_mask != 0); let src_block = &self.blocks[src_block_index as usize]; let dest_block = &self.blocks[dest_block_index as usize]; @@ -275,32 +318,40 @@ debug_assert!(!dest_block.is_full()); let dest_index = dest_block.elements_count; - for i in 0..self.types.len() { - if src_mask.type_mask & (1 << i as u64) != 0 { - let size = self.element_sizes[i]; - let src_ptr = src_block.component_blocks[i].unwrap().as_ptr(); - let dest_ptr = dest_block.component_blocks[i].unwrap().as_ptr(); + + let mut type_mask = src_mask.type_mask; + while type_mask != 0 { + let i = type_mask.trailing_zeros() as usize; + + let size = self.element_sizes[i]; + let src_ptr = src_block.component_blocks[i].unwrap().as_ptr(); + if let Some(dest_ptr) = dest_block.component_blocks[i] { + let dest_ptr = dest_ptr.as_ptr(); unsafe { copy_nonoverlapping( src_ptr.add((src_index * size) as usize), dest_ptr.add((dest_index * size) as usize), size as usize, ); - if src_index < src_block.elements_count - 1 { - copy_nonoverlapping( - src_ptr.add((size * (src_block.elements_count - 1)) as usize), - src_ptr.add((size * src_index) as usize), - size as usize, - ); - } } } + unsafe { + if src_index < src_block.elements_count - 1 { + copy_nonoverlapping( + src_ptr.add((size * (src_block.elements_count - 1)) as usize), + src_ptr.add((size * src_index) as usize), + size as usize, + ); + } + } + + type_mask &= type_mask - 1; } let src_block = &mut self.blocks[src_block_index as usize]; let gear_id = src_block.gear_ids()[src_index as usize]; - if src_index < src_block.elements_count - 1 { + if src_index + 1 < src_block.elements_count { let relocated_index = src_block.elements_count as usize - 1; let gear_ids = src_block.gear_ids_mut(); let relocated_id = gear_ids[relocated_index]; @@ -460,16 +511,24 @@ pub fn remove(&mut self, gear_id: GearId) { if let Some(type_index) = self.get_type_index::() { + let type_bit = 1 << type_index as u64; let entry = self.lookup[gear_id.get() as usize - 1]; + if let Some(index) = entry.index { - let mut dest_mask = self.block_masks[entry.block_index as usize]; - dest_mask.type_mask &= !(1 << type_index as u64); + let mask = self.block_masks[entry.block_index as usize]; + let new_mask = mask.without_type(type_bit); - if dest_mask.type_mask == 0 { - self.remove_from_block(entry.block_index, index.get() - 1); - } else { - let dest_block_index = self.ensure_block(dest_mask); - self.move_between_blocks(entry.block_index, index.get() - 1, dest_block_index); + if new_mask != mask { + if new_mask.type_mask == 0 { + self.remove_from_block(entry.block_index, index.get() - 1); + } else { + let dest_block_index = self.ensure_block(new_mask); + self.move_between_blocks( + entry.block_index, + index.get() - 1, + dest_block_index, + ); + } } } } else { @@ -486,7 +545,7 @@ pub fn register(&mut self) { debug_assert!(!std::mem::needs_drop::()); - debug_assert!(size_of::() <= u16::max_value() as usize); + debug_assert!(size_of::() <= u16::MAX as usize); let id = TypeId::of::(); if size_of::() == 0 { @@ -511,7 +570,7 @@ type_indices: &[i8], mut f: F, ) { - let mut slices = vec![null_mut(); type_indices.len() + 1]; + let mut slices = [null_mut(); MAX_TYPES + 1]; for (block_index, mask) in self.block_masks.iter().enumerate() { if mask.type_mask & type_selector == type_selector @@ -527,19 +586,36 @@ } unsafe { - T::iter(&slices[..], block.elements_count as usize, |id, x| f(id, x)); + T::iter( + &slices[0..=type_indices.len()], + block.elements_count as usize, + |id, x| f(id, x), + ); } } } } + pub fn get(&self, gear_id: GearId) -> Option<&T> { + let entry = self.lookup[gear_id.get() as usize - 1]; + match (entry.index, self.get_type_index::()) { + (Some(index), Some(type_index)) => { + let block = &self.blocks[entry.block_index as usize]; + block.component_blocks[type_index].map(|ptr| unsafe { + &*(ptr.as_ptr() as *const T).add(index.get() as usize - 1) + }) + } + _ => None, + } + } + pub fn iter(&mut self) -> DataIterator { - let mut arg_types = Vec::with_capacity(64); - T::get_types(&mut arg_types); - let mut type_indices = vec![-1i8; arg_types.len()]; + let mut arg_types: [TypeId; MAX_TYPES] = unsafe { MaybeUninit::uninit().assume_init() }; + let types_count = T::get_types(&mut arg_types); + let mut type_indices = [-1; MAX_TYPES]; let mut selector = 0u64; - for (arg_index, type_id) in arg_types.iter().enumerate() { + for (arg_index, type_id) in arg_types[0..types_count].iter().enumerate() { match self.types.iter().position(|t| t == type_id) { Some(i) if selector & (1 << i as u64) != 0 => panic!("Duplicate type"), Some(i) => { @@ -556,7 +632,7 @@ pub struct DataIterator<'a, T> { data: &'a mut GearDataManager, types: u64, - type_indices: Vec, + type_indices: [i8; MAX_TYPES], tags: u64, phantom_types: PhantomData, } @@ -565,7 +641,7 @@ fn new( data: &'a mut GearDataManager, types: u64, - type_indices: Vec, + type_indices: [i8; MAX_TYPES], ) -> DataIterator<'a, T> { Self { data, @@ -577,12 +653,12 @@ } pub fn with_tags(self) -> Self { - let mut tag_types = Vec::with_capacity(64); - U::get_types(&mut tag_types); + let mut tag_types: [TypeId; MAX_TYPES] = unsafe { MaybeUninit::uninit().assume_init() }; + let tags_count = U::get_types(&mut tag_types); let mut tags = 0; for (i, tag) in self.data.tags.iter().enumerate() { - if tag_types.contains(tag) { + if tag_types[0..tags_count].contains(tag) { tags |= 1 << i as u64; } } @@ -596,8 +672,13 @@ #[inline] pub fn run_id(&mut self, f: F) { + let types_count = self + .type_indices + .iter() + .position(|i| *i == -1) + .unwrap_or(self.type_indices.len()); self.data - .run_impl(self.types, self.tags, &self.type_indices, f); + .run_impl(self.types, self.tags, &self.type_indices[0..types_count], f); } } @@ -606,7 +687,12 @@ use super::{super::common::GearId, GearDataManager}; #[derive(Clone)] - struct Datum { + struct DatumA { + value: u32, + } + + #[derive(Clone)] + struct DatumB { value: u32, } @@ -614,48 +700,93 @@ struct Tag; #[test] + fn direct_access() { + let mut manager = GearDataManager::new(); + manager.register::(); + for i in 1..=5 { + manager.add(GearId::new(i as u16).unwrap(), &DatumA { value: i * i }); + } + + for i in 1..=5 { + assert_eq!( + manager + .get::(GearId::new(i as u16).unwrap()) + .unwrap() + .value, + i * i + ); + } + } + + #[test] fn single_component_iteration() { let mut manager = GearDataManager::new(); - manager.register::(); + manager.register::(); + for i in 1..=5 { - manager.add(GearId::new(i as u16).unwrap(), &Datum { value: i }); + manager.add(GearId::new(i as u16).unwrap(), &DatumA { value: i }); } let mut sum = 0; - manager.iter().run(|(d,): (&Datum,)| sum += d.value); + manager.iter().run(|(d,): (&DatumA,)| sum += d.value); assert_eq!(sum, 15); - manager.iter().run(|(d,): (&mut Datum,)| d.value += 1); - manager.iter().run(|(d,): (&Datum,)| sum += d.value); + manager.iter().run(|(d,): (&mut DatumA,)| d.value += 1); + manager.iter().run(|(d,): (&DatumA,)| sum += d.value); assert_eq!(sum, 35); } #[test] fn tagged_component_iteration() { let mut manager = GearDataManager::new(); - manager.register::(); + manager.register::(); manager.register::(); - for i in 1..=10 { - let gear_id = GearId::new(i as u16).unwrap(); - manager.add(gear_id, &Datum { value: i }); - } for i in 1..=10 { let gear_id = GearId::new(i as u16).unwrap(); - if i & 1 == 0 { - manager.add_tag::(gear_id); - } + manager.add(gear_id, &DatumA { value: i }); + } + + for i in (2..=10).step_by(2) { + let gear_id = GearId::new(i as u16).unwrap(); + manager.add_tag::(gear_id); } let mut sum = 0; - manager.iter().run(|(d,): (&Datum,)| sum += d.value); + manager.iter().run(|(d,): (&DatumA,)| sum += d.value); assert_eq!(sum, 55); let mut tag_sum = 0; manager .iter() .with_tags::<&Tag>() - .run(|(d,): (&Datum,)| tag_sum += d.value); + .run(|(d,): (&DatumA,)| tag_sum += d.value); assert_eq!(tag_sum, 30); } + + #[test] + fn removal() { + let mut manager = GearDataManager::new(); + manager.register::(); + manager.register::(); + + for i in 1..=10 { + let gear_id = GearId::new(i as u16).unwrap(); + manager.add(gear_id, &DatumA { value: i }); + manager.add(gear_id, &DatumB { value: i }); + } + + for i in (1..=10).step_by(2) { + let gear_id = GearId::new(i as u16).unwrap(); + manager.remove::(gear_id); + } + + let mut sum_a = 0; + manager.iter().run(|(d,): (&DatumA,)| sum_a += d.value); + assert_eq!(sum_a, 30); + + let mut sum_b = 0; + manager.iter().run(|(d,): (&DatumB,)| sum_b += d.value); + assert_eq!(sum_b, 55); + } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/grid.rs --- a/rust/hwphysics/src/grid.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/src/grid.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,26 +1,36 @@ use crate::{ - collision::{fppoint_round, CircleBounds, DetectedCollisions}, + collision::{CircleBounds, DetectedCollisions}, common::GearId, }; use fpnum::FPPoint; -use integral_geometry::{GridIndex, Point, Size}; +use integral_geometry::{GridIndex, Point, PotSize}; struct GridBin { - static_refs: Vec, - static_entries: Vec, - - dynamic_refs: Vec, - dynamic_entries: Vec, + refs: Vec, + entries: Vec, } impl GridBin { fn new() -> Self { Self { - static_refs: vec![], - static_entries: vec![], - dynamic_refs: vec![], - dynamic_entries: vec![], + refs: vec![], + entries: vec![], + } + } + + fn add(&mut self, gear_id: GearId, bounds: &CircleBounds) { + self.refs.push(gear_id); + self.entries.push(*bounds); + } + + fn remove(&mut self, gear_id: GearId) -> bool { + if let Some(pos) = self.refs.iter().position(|id| *id == gear_id) { + self.refs.swap_remove(pos); + self.entries.swap_remove(pos); + true + } else { + false } } } @@ -29,114 +39,72 @@ pub struct Grid { bins: Vec, - space_size: Size, - bins_count: Size, + space_size: PotSize, + bins_count: PotSize, index: GridIndex, } impl Grid { - pub fn new(size: Size) -> Self { - assert!(size.is_power_of_two()); - let bins_count = Size::new(size.width / GRID_BIN_SIZE, size.height / GRID_BIN_SIZE); + pub fn new(size: PotSize) -> Self { + let bins_count = + PotSize::new(size.width() / GRID_BIN_SIZE, size.height() / GRID_BIN_SIZE).unwrap(); Self { bins: (0..bins_count.area()).map(|_| GridBin::new()).collect(), space_size: size, bins_count, - index: Size::square(GRID_BIN_SIZE).to_grid_index(), + index: PotSize::square(GRID_BIN_SIZE).unwrap().to_grid_index(), } } + fn linear_bin_index(&self, index: Point) -> usize { + self.bins_count + .linear_index(index.x as usize, index.y as usize) + } + fn bin_index(&self, position: &FPPoint) -> Point { - self.index.map(fppoint_round(position)) + self.index.map(Point::from_fppoint(position)) } fn get_bin(&mut self, index: Point) -> &mut GridBin { - &mut self.bins[index.x as usize * self.bins_count.width + index.y as usize] + let index = self.linear_bin_index(index); + &mut self.bins[index] + } + + fn try_get_bin(&mut self, index: Point) -> Option<&mut GridBin> { + let index = self.linear_bin_index(index); + self.bins.get_mut(index) } fn lookup_bin(&mut self, position: &FPPoint) -> &mut GridBin { self.get_bin(self.bin_index(position)) } - pub fn insert_static(&mut self, gear_id: GearId, bounds: &CircleBounds) { - let bin = self.lookup_bin(&bounds.center); - bin.static_refs.push(gear_id); - bin.static_entries.push(*bounds) - } - - pub fn insert_dynamic(&mut self, gear_id: GearId, bounds: &CircleBounds) { - let bin = self.lookup_bin(&bounds.center); - bin.dynamic_refs.push(gear_id); - bin.dynamic_entries.push(*bounds); + pub fn insert(&mut self, gear_id: GearId, bounds: &CircleBounds) { + self.lookup_bin(&bounds.center).add(gear_id, bounds); } - pub fn remove(&mut self, gear_id: GearId) {} - - pub fn update_position( - &mut self, - gear_id: GearId, - old_position: &FPPoint, - new_position: &FPPoint, - ) { - let old_bin_index = self.bin_index(old_position); - let new_bin_index = self.bin_index(new_position); - - let old_bin = self.lookup_bin(old_position); - if let Some(index) = old_bin.dynamic_refs.iter().position(|id| *id == gear_id) { - if old_bin_index == new_bin_index { - old_bin.dynamic_entries[index].center = *new_position - } else { - let bounds = old_bin.dynamic_entries.swap_remove(index); - let new_bin = self.get_bin(new_bin_index); + fn remove_all(&mut self, gear_id: GearId) { + for bin in &mut self.bins { + if bin.remove(gear_id) { + break; + } + } + } - new_bin.dynamic_refs.push(gear_id); - new_bin.dynamic_entries.push(CircleBounds { - center: *new_position, - ..bounds - }); + pub fn remove(&mut self, gear_id: GearId, bounds: Option<&CircleBounds>) { + if let Some(bounds) = bounds { + if !self.lookup_bin(&bounds.center).remove(gear_id) { + self.remove_all(gear_id); } - } else if let Some(index) = old_bin.static_refs.iter().position(|id| *id == gear_id) { - let bounds = old_bin.static_entries.swap_remove(index); - old_bin.static_refs.swap_remove(index); - - let new_bin = if old_bin_index == new_bin_index { - old_bin - } else { - self.get_bin(new_bin_index) - }; - - new_bin.dynamic_refs.push(gear_id); - new_bin.dynamic_entries.push(CircleBounds { - center: *new_position, - ..bounds - }); + } else { + self.remove_all(gear_id); } } pub fn check_collisions(&self, collisions: &mut DetectedCollisions) { for bin in &self.bins { - for (index, bounds) in bin.dynamic_entries.iter().enumerate() { - for (other_index, other) in bin.dynamic_entries.iter().enumerate().skip(index + 1) { - if bounds.intersects(other) && bounds != other { - collisions.push( - bin.dynamic_refs[index], - Some(bin.dynamic_refs[other_index]), - &((bounds.center + other.center) / 2), - ) - } - } - - for (other_index, other) in bin.static_entries.iter().enumerate() { - if bounds.intersects(other) { - collisions.push( - bin.dynamic_refs[index], - Some(bin.static_refs[other_index]), - &((bounds.center + other.center) / 2), - ) - } - } - } + for (index, bounds) in bin.entries.iter().enumerate() {} } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/lib.rs --- a/rust/hwphysics/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,19 +1,20 @@ pub mod collision; pub mod common; -mod data; +pub mod data; mod grid; pub mod physics; -pub mod time; -use integral_geometry::Size; +use integral_geometry::PotSize; use land2d::Land2D; +use std::any::{Any, TypeId}; +use crate::collision::CollisionData; +use crate::physics::VelocityData; use crate::{ collision::CollisionProcessor, common::{GearAllocator, GearId, Millis}, - data::GearDataManager, + data::{DataIterator, GearDataManager, TypeIter}, physics::PhysicsProcessor, - time::TimeProcessor, }; pub struct World { @@ -21,11 +22,10 @@ data: GearDataManager, physics: PhysicsProcessor, collision: CollisionProcessor, - time: TimeProcessor, } impl World { - pub fn new(world_size: Size) -> Self { + pub fn new(world_size: PotSize) -> Self { let mut data = GearDataManager::new(); PhysicsProcessor::register_components(&mut data); CollisionProcessor::register_components(&mut data); @@ -35,7 +35,6 @@ allocator: GearAllocator::new(), physics: PhysicsProcessor::new(), collision: CollisionProcessor::new(world_size), - time: TimeProcessor::new(), } } @@ -48,24 +47,33 @@ pub fn delete_gear(&mut self, gear_id: GearId) { self.data.remove_all(gear_id); self.collision.remove(gear_id); - self.time.cancel(gear_id); self.allocator.free(gear_id) } pub fn step(&mut self, time_step: Millis, land: &Land2D) { - let updates = if time_step == Millis::new(1) { - self.physics.process_single_tick(&mut self.data) - } else { - self.physics - .process_multiple_ticks(&mut self.data, time_step) - }; + let updates = self.physics.process(&mut self.data, time_step); let collisions = self.collision.process(land, &updates); - let events = self.time.process(time_step); + } + + pub fn add_gear_data(&mut self, gear_id: GearId, data: &T) { + self.data.add(gear_id, data); + if TypeId::of::() == TypeId::of::() { + self.collision.remove(gear_id); + } + } + + pub fn remove_gear_data(&mut self, gear_id: GearId) { + self.data.remove::(gear_id); + if TypeId::of::() == TypeId::of::() { + if let Some(collision_data) = self.data.get::(gear_id) { + self.collision.add(gear_id, *collision_data); + } + } } #[inline] - pub fn add_gear_data(&mut self, gear_id: GearId, data: &T) { - self.data.add(gear_id, data); + pub fn iter_data(&mut self) -> DataIterator { + self.data.iter() } } @@ -78,12 +86,12 @@ World, }; use fpnum::{fp, FPNum, FPPoint}; - use integral_geometry::Size; + use integral_geometry::{PotSize, Size}; use land2d::Land2D; #[test] fn data_flow() { - let world_size = Size::new(2048, 2048); + let world_size = PotSize::new(2048, 2048).unwrap(); let mut world = World::new(world_size); let gear_id = world.new_gear().unwrap(); @@ -101,7 +109,10 @@ }, ); - let land = Land2D::new(Size::new(world_size.width - 2, world_size.height - 2), 0); + let land = Land2D::new( + Size::new(world_size.width() - 2, world_size.height() - 2), + 0, + ); world.step(Millis::new(1), &land); } diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/physics.rs --- a/rust/hwphysics/src/physics.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwphysics/src/physics.rs Sun Mar 24 14:33:57 2024 -0400 @@ -67,36 +67,24 @@ } } - pub fn process_single_tick(&mut self, data: &mut GearDataManager) -> &PositionUpdates { - let gravity = FPPoint::unit_y() * self.gravity; - let wind = FPPoint::unit_x() * self.wind; - - self.position_updates.clear(); - - data.iter() - .with_tags::<&AffectedByWind>() - .run(|(vel,): (&mut VelocityData,)| { - vel.0 += wind; - }); - - data.iter().run_id( - |gear_id, (pos, vel): (&mut PositionData, &mut VelocityData)| { - let old_pos = pos.0; - vel.0 += gravity; - pos.0 += vel.0; - self.position_updates.push(gear_id, &old_pos, &pos.0) - }, - ); - - &self.position_updates + pub fn process(&mut self, data: &mut GearDataManager, time_step: Millis) -> &PositionUpdates { + if time_step == Millis::new(1) { + self.process_impl::(data, time_step) + } else { + self.process_impl::(data, time_step) + } } - pub fn process_multiple_ticks( + fn process_impl( &mut self, data: &mut GearDataManager, time_step: Millis, ) -> &PositionUpdates { - let fp_step = time_step.to_fixed(); + let fp_step = if SINGLE_TICK { + fp!(1) + } else { + time_step.to_fixed() + }; let gravity = FPPoint::unit_y() * (self.gravity * fp_step); let wind = FPPoint::unit_x() * (self.wind * fp_step); @@ -112,7 +100,7 @@ |gear_id, (pos, vel): (&mut PositionData, &mut VelocityData)| { let old_pos = pos.0; vel.0 += gravity; - pos.0 += vel.0 * fp_step; + pos.0 += if SINGLE_TICK { vel.0 } else { vel.0 * fp_step }; self.position_updates.push(gear_id, &old_pos, &pos.0) }, ); diff -r 64740eec84ad -r 4c523ed1d35c rust/hwphysics/src/time.rs --- a/rust/hwphysics/src/time.rs Sun Mar 24 14:05:06 2024 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -use crate::common::{GearId, Millis}; -use std::{ - cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}, - collections::BinaryHeap, -}; - -pub type EventId = u16; - -struct TimeEvent { - time: Millis, - gear_id: GearId, - event_id: EventId, -} - -impl PartialOrd for TimeEvent { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - self.time.partial_cmp(&other.time) - } -} - -impl PartialEq for TimeEvent { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.time.eq(&other.time) - } -} - -impl Ord for TimeEvent { - #[inline] - fn cmp(&self, other: &Self) -> Ordering { - self.time.cmp(&other.time) - } -} - -impl Eq for TimeEvent {} - -pub struct OccurredEvents { - events: Vec<(GearId, EventId)>, -} - -impl OccurredEvents { - fn new() -> Self { - Self { events: vec![] } - } - - fn clear(&mut self) { - self.events.clear() - } -} - -pub struct TimeProcessor { - current_event_id: EventId, - current_time: Millis, - events: BinaryHeap, - timeouts: OccurredEvents, -} - -impl TimeProcessor { - pub fn new() -> Self { - Self { - current_event_id: 0, - current_time: Millis::new(0), - events: BinaryHeap::with_capacity(1024), - timeouts: OccurredEvents::new(), - } - } - - pub fn register(&mut self, gear_id: GearId, timeout: Millis) -> EventId { - let event_id = self.current_event_id; - self.current_event_id.wrapping_add(1); - let event = TimeEvent { - time: self.current_time + timeout, - gear_id, - event_id, - }; - self.events.push(event); - event_id - } - - pub fn cancel(&mut self, gear_id: GearId) {} - - pub fn process(&mut self, time_step: Millis) -> &OccurredEvents { - self.timeouts.clear(); - self.current_time = self.current_time + time_step; - while self - .events - .peek() - .filter(|e| e.time <= self.current_time) - .is_some() - { - let event = self.events.pop().unwrap(); - self.timeouts.events.push((event.gear_id, event.event_id)) - } - &self.timeouts - } -} diff -r 64740eec84ad -r 4c523ed1d35c rust/hwrunner/Cargo.toml --- a/rust/hwrunner/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwrunner/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -5,8 +5,10 @@ edition = "2018" [dependencies] -glutin = "0.20" +glutin = "0.26" gl = "0.11" +futures = "0.3" +wgpu = "0.6" integral-geometry = { path = "../integral-geometry" } lib-hedgewars-engine = { path = "../lib-hedgewars-engine" } diff -r 64740eec84ad -r 4c523ed1d35c rust/hwrunner/src/main.rs --- a/rust/hwrunner/src/main.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/hwrunner/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,74 +1,231 @@ -use glutin::{ - dpi, ContextTrait, DeviceEvent, ElementState, Event, EventsLoop, GlProfile, GlRequest, - MouseButton, MouseScrollDelta, WindowEvent, WindowedContext, -}; - -use hedgewars_engine::instance::EngineInstance; - -use integral_geometry::Point; use std::time::Duration; -fn init(event_loop: &EventsLoop, size: dpi::LogicalSize) -> WindowedContext { - use glutin::{ContextBuilder, WindowBuilder}; +use futures::executor::block_on; +use glutin::event_loop::ControlFlow; +use glutin::{ + dpi, + event::{DeviceEvent, ElementState, Event, MouseButton, MouseScrollDelta, WindowEvent}, + event_loop::EventLoop, + window::{Window, WindowBuilder}, + ContextWrapper, GlProfile, GlRequest, NotCurrent, PossiblyCurrent, WindowedContext, +}; +use hedgewars_engine::instance::EngineInstance; +use integral_geometry::Point; +use std::{ + error::Error, + path::Path, +}; +use wgpu::{ + Adapter, BackendBit, Color, CommandEncoderDescriptor, Device, DeviceDescriptor, Features, + LoadOp, Operations, PowerPreference, PresentMode, Queue, RenderPassColorAttachmentDescriptor, + RenderPassDescriptor, RequestAdapterOptions, Surface, SwapChain, SwapChainDescriptor, + TextureFormat, TextureUsage, +}; + +type HwGlRendererContext = ContextWrapper; + +struct HwWgpuRenderingContext { + window: Window, + surface: Surface, + adapter: Adapter, + device: Device, + queue: Queue, + swap_chain: SwapChain, +} + +enum HwRendererContext { + Gl(HwGlRendererContext), + Wgpu(HwWgpuRenderingContext), +} + +struct ErrorStub; - let window = WindowBuilder::new() - .with_title("hwengine") - .with_dimensions(size); +impl From for ErrorStub { + fn from(_: T) -> Self { + ErrorStub + } +} + +impl HwRendererContext { + fn get_framebuffer_size(window: &Window) -> (u32, u32) { + window.inner_size().into() + } - let cxt = ContextBuilder::new() - .with_gl(GlRequest::Latest) - .with_gl_profile(GlProfile::Core) - .build_windowed(window, &event_loop) - .ok() + fn create_wpgu_swap_chain(window: &Window, surface: &Surface, device: &Device) -> SwapChain { + let (width, height) = Self::get_framebuffer_size(window); + device.create_swap_chain( + &surface, + &SwapChainDescriptor { + usage: TextureUsage::OUTPUT_ATTACHMENT, + format: TextureFormat::Bgra8Unorm, + width, + height, + present_mode: PresentMode::Fifo, + }, + ) + } + + fn init_wgpu( + event_loop: &EventLoop<()>, + size: dpi::LogicalSize, + ) -> HwWgpuRenderingContext { + let builder = WindowBuilder::new() + .with_title("hwengine") + .with_inner_size(size); + let window = builder.build(event_loop).unwrap(); + + let instance = wgpu::Instance::new(BackendBit::PRIMARY); + + let surface = unsafe { instance.create_surface(&window) }; + + let adapter = block_on(instance.request_adapter(&RequestAdapterOptions { + power_preference: PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + })) .unwrap(); - unsafe { - cxt.make_current().unwrap(); - gl::load_with(|ptr| cxt.get_proc_address(ptr) as *const _); + let (device, queue) = block_on(adapter.request_device(&Default::default(), None)).unwrap(); + + let swap_chain = Self::create_wpgu_swap_chain(&window, &surface, &device); - if let Some(sz) = cxt.get_inner_size() { - let phys = sz.to_physical(cxt.get_hidpi_factor()); - - gl::Viewport(0, 0, phys.width as i32, phys.height as i32); + HwWgpuRenderingContext { + window, + surface, + adapter, + device, + queue, + swap_chain, } } - cxt + fn init_gl(event_loop: &EventLoop<()>, size: dpi::LogicalSize) -> HwGlRendererContext { + use glutin::ContextBuilder; + + let builder = WindowBuilder::new() + .with_title("hwengine") + .with_inner_size(size); + + let context = ContextBuilder::new() + .with_gl(GlRequest::Latest) + .with_gl_profile(GlProfile::Core) + .build_windowed(builder, &event_loop) + .ok() + .unwrap(); + + unsafe { + let wrapper = context.make_current().unwrap(); + gl::load_with(|ptr| wrapper.get_proc_address(ptr) as *const _); + + let (width, height) = Self::get_framebuffer_size(wrapper.window()); + gl::Viewport(0, 0, width as i32, height as i32); + wrapper + } + } + + fn new(event_loop: &EventLoop<()>, size: dpi::LogicalSize, use_wgpu: bool) -> Self { + if use_wgpu { + Self::Wgpu(Self::init_wgpu(event_loop, size)) + } else { + Self::Gl(Self::init_gl(event_loop, size)) + } + } + + pub fn window(&self) -> &Window { + match self { + HwRendererContext::Gl(gl) => &gl.window(), + HwRendererContext::Wgpu(wgpu) => &wgpu.window, + } + } + + pub fn update(&mut self) { + match self { + HwRendererContext::Gl(context) => unsafe { + let (width, height) = Self::get_framebuffer_size(&context.window()); + gl::Viewport(0, 0, width as i32, height as i32); + }, + HwRendererContext::Wgpu(context) => { + context.swap_chain = Self::create_wpgu_swap_chain( + &context.window, + &context.surface, + &context.device, + ); + } + } + } + + pub fn present(&mut self) -> Result<(), ErrorStub> { + match self { + HwRendererContext::Gl(context) => context.swap_buffers()?, + HwRendererContext::Wgpu(context) => { + let frame_view = &context.swap_chain.get_current_frame()?.output.view; + + let mut encoder = + context + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("Main encoder"), + }); + encoder.begin_render_pass(&RenderPassDescriptor { + color_attachments: &[RenderPassColorAttachmentDescriptor { + attachment: &frame_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color { + r: 0.7, + g: 0.4, + b: 0.2, + a: 1.0, + }), + store: false, + }, + }], + depth_stencil_attachment: None, + }); + let buffer = encoder.finish(); + context.queue.submit(std::iter::once(buffer)); + } + } + Ok(()) + } } fn main() { - let mut event_loop = EventsLoop::new(); + let use_wgpu = false; + let mut event_loop = EventLoop::<()>::new(); let (w, h) = (1024.0, 768.0); - let window = init(&event_loop, dpi::LogicalSize::new(w, h)); + + let mut context = HwRendererContext::new(&event_loop, dpi::LogicalSize::new(w, h), use_wgpu); - let mut engine = EngineInstance::new(); - engine.world.create_renderer(w as u16, h as u16); + let mut engine = EngineInstance::new(Path::new("../../share/hedgewars/Data")); + if !use_wgpu { + engine.world.create_renderer(w as u16, h as u16); + } let mut dragging = false; use std::time::Instant; let mut now = Instant::now(); - let mut update = Instant::now(); + let mut update_time = Instant::now(); + let mut render_time = Instant::now(); - let mut is_running = true; - while is_running { - let curr = Instant::now(); - let delta = curr - now; - now = curr; - let ms = delta.as_secs() as f64 * 1000.0 + delta.subsec_millis() as f64; - window.set_title(&format!("hwengine {:.3}ms", ms)); + let current_time = Instant::now(); + let delta = current_time - now; + now = current_time; + let ms = delta.as_secs() as f64 * 1000.0 + delta.subsec_millis() as f64; + context.window().set_title(&format!("hwengine {:.3}ms", ms)); - if update.elapsed() > Duration::from_millis(10) { - update = curr; - engine.world.step() - } - - event_loop.poll_events(|event| match event { + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Poll; + match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => { - is_running = false; + *control_flow = ControlFlow::Exit; } + WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => { + context.update() + } + WindowEvent::MouseInput { button, state, .. } => { if let MouseButton::Right = button { dragging = state == ElementState::Pressed; @@ -78,10 +235,7 @@ WindowEvent::MouseWheel { delta, .. } => { let zoom_change = match delta { MouseScrollDelta::LineDelta(x, y) => y as f32 * 0.1f32, - MouseScrollDelta::PixelDelta(delta) => { - let physical = delta.to_physical(window.get_hidpi_factor()); - physical.y as f32 * 0.1f32 - } + MouseScrollDelta::PixelDelta(delta) => delta.y as f32 * 0.1f32, }; engine.world.move_camera(Point::ZERO, zoom_change); } @@ -97,12 +251,23 @@ } _ => {} }, + _ => (), - }); + } - unsafe { window.make_current().unwrap() }; + let current_time = Instant::now(); - engine.render(); - window.swap_buffers().unwrap(); - } + if update_time.elapsed() > Duration::from_millis(10) { + update_time = current_time; + engine.world.step() + } + + if render_time.elapsed() > Duration::from_millis(16) { + render_time = current_time; + if !use_wgpu { + engine.render(); + } + context.present().ok().unwrap(); + } + }); } diff -r 64740eec84ad -r 4c523ed1d35c rust/integral-geometry/src/lib.rs --- a/rust/integral-geometry/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/integral-geometry/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -24,12 +24,12 @@ } #[inline] - pub fn signum(self) -> Self { + pub const fn signum(self) -> Self { Self::new(self.x.signum(), self.y.signum()) } #[inline] - pub fn abs(self) -> Self { + pub const fn abs(self) -> Self { Self::new(self.x.abs(), self.y.abs()) } @@ -59,7 +59,7 @@ #[inline] pub const fn rotate90(self) -> Self { - Point::new(self.y, -self.x) + Self::new(self.y, -self.x) } #[inline] @@ -68,8 +68,8 @@ } #[inline] - pub fn clamp(self, rect: &Rect) -> Point { - Point::new(rect.x_range().clamp(self.x), rect.y_range().clamp(self.y)) + pub fn clamp(self, rect: &Rect) -> Self { + Self::new(rect.x_range().clamp(self.x), rect.y_range().clamp(self.y)) } #[inline] @@ -136,16 +136,21 @@ } #[inline] - pub fn is_power_of_two(&self) -> bool { + pub const fn is_power_of_two(&self) -> bool { self.width.is_power_of_two() && self.height.is_power_of_two() } #[inline] - pub fn next_power_of_two(&self) -> Self { - Self { - width: self.width.next_power_of_two(), - height: self.height.next_power_of_two(), - } + pub const fn as_power_of_two(&self) -> Option { + PotSize::new(self.width, self.height) + } + + #[inline] + pub const fn next_power_of_two(&self) -> PotSize { + PotSize::new_impl( + self.width.next_power_of_two(), + self.height.next_power_of_two(), + ) } #[inline] @@ -154,31 +159,107 @@ } #[inline] - pub fn to_mask(&self) -> SizeMask { - SizeMask::new(*self) - } - - #[inline] pub fn to_square(&self) -> Self { Self::square(max(self.width, self.height)) } - pub fn to_grid_index(&self) -> GridIndex { - GridIndex::new(*self) - } - #[inline] - pub fn contains(&self, other: Self) -> bool { + pub const fn contains(&self, other: Self) -> bool { self.width >= other.width && self.height >= other.height } #[inline] pub fn join(&self, other: Self) -> Self { + Self::new(max(self.width, other.width), max(self.height, other.height)) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct PotSize { + size: Size, +} + +impl PotSize { + #[inline] + const fn new_impl(width: usize, height: usize) -> Self { + debug_assert!(width.is_power_of_two() && height.is_power_of_two()); Self { - width: max(self.width, other.width), - height: max(self.height, other.height) + size: Size::new(width, height), + } + } + + #[inline] + pub const fn new(width: usize, height: usize) -> Option { + if width.is_power_of_two() && height.is_power_of_two() { + Some(Self::new_impl(width, height)) + } else { + None + } + } + + pub const fn size(&self) -> Size { + self.size + } + + pub const fn width(&self) -> usize { + self.size.width + } + + pub const fn height(&self) -> usize { + self.size.height + } + + #[inline] + pub const fn square(size: usize) -> Option { + if size.is_power_of_two() { + Some(Self::new_impl(size, size)) + } else { + None } } + + #[inline] + pub const fn area(&self) -> usize { + self.size.area() + } + + #[inline] + pub const fn linear_index(&self, x: usize, y: usize) -> usize { + self.size.linear_index(x, y) + } + + #[inline] + pub const fn transpose(&self) -> Self { + Self::new_impl(self.height(), self.width()) + } + + #[inline] + pub fn to_square(&self) -> Self { + let size = max(self.width(), self.height()); + Self::new_impl(size, size) + } + + #[inline] + pub const fn to_mask(&self) -> SizeMask { + SizeMask::new(*self) + } + + pub const fn to_grid_index(&self) -> GridIndex { + GridIndex::new(*self) + } + + #[inline] + pub const fn contains(&self, other: Self) -> bool { + self.size.contains(other.size) + } + + #[inline] + pub fn join(&self, other: Self) -> Self { + Self::new_impl( + max(self.width(), other.width()), + max(self.height(), other.height()), + ) + } } #[derive(PartialEq, Eq, Clone, Copy, Debug)] @@ -188,13 +269,10 @@ impl SizeMask { #[inline] - pub fn new(size: Size) -> Self { - debug_assert!(size.is_power_of_two()); - let size = Size { - width: !(size.width - 1), - height: !(size.height - 1), - }; - Self { size } + pub const fn new(size: PotSize) -> Self { + Self { + size: Size::new(!(size.width() - 1), !(size.height() - 1)), + } } #[inline] @@ -211,6 +289,11 @@ pub fn contains(&self, point: Point) -> bool { self.contains_x(point.x as usize) && self.contains_y(point.y as usize) } + + #[inline] + pub const fn to_size(&self) -> PotSize { + PotSize::new_impl(!self.size.width + 1, !self.size.height + 1) + } } pub struct GridIndex { @@ -218,16 +301,15 @@ } impl GridIndex { - pub fn new(size: Size) -> Self { - assert!(size.is_power_of_two()); + pub const fn new(size: PotSize) -> Self { let shift = Point::new( - size.width.trailing_zeros() as i32, - size.height.trailing_zeros() as i32, + size.width().trailing_zeros() as i32, + size.height().trailing_zeros() as i32, ); Self { shift } } - pub fn map(&self, position: Point) -> Point { + pub const fn map(&self, position: Point) -> Point { Point::new(position.x >> self.shift.x, position.y >> self.shift.y) } } @@ -324,7 +406,7 @@ }; #[inline] - pub fn new(top_left: Point, bottom_right: Point) -> Self { + pub const fn new(top_left: Point, bottom_right: Point) -> Self { debug_assert!(top_left.x <= bottom_right.x + 1); debug_assert!(top_left.y <= bottom_right.y + 1); Self { @@ -333,7 +415,7 @@ } } - pub fn from_box(left: i32, right: i32, top: i32, bottom: i32) -> Self { + pub const fn from_box(left: i32, right: i32, top: i32, bottom: i32) -> Self { Self::new(Point::new(left, top), Point::new(right, bottom)) } @@ -414,12 +496,12 @@ } #[inline] - pub fn x_range(&self) -> RangeInclusive { + pub const fn x_range(&self) -> RangeInclusive { self.left()..=self.right() } #[inline] - pub fn y_range(&self) -> RangeInclusive { + pub const fn y_range(&self) -> RangeInclusive { self.top()..=self.bottom() } @@ -429,7 +511,7 @@ } #[inline] - pub fn contains_inside(&self, point: Point) -> bool { + pub const fn contains_inside(&self, point: Point) -> bool { point.x > self.left() && point.x < self.right() && point.y > self.top() @@ -442,7 +524,7 @@ } #[inline] - pub fn intersects(&self, other: &Rect) -> bool { + pub const fn intersects(&self, other: &Rect) -> bool { self.left() <= other.right() && self.right() >= other.left() && self.top() <= other.bottom() @@ -450,8 +532,8 @@ } #[inline] - pub fn split_at(&self, point: Point) -> [Rect; 4] { - assert!(self.contains_inside(point)); + pub const fn split_at(&self, point: Point) -> [Rect; 4] { + debug_assert!(self.contains_inside(point)); [ Self::from_box(self.left(), point.x, self.top(), point.y), Self::from_box(point.x, self.right(), self.top(), point.y), @@ -461,7 +543,7 @@ } #[inline] - pub fn with_margins(&self, left: i32, right: i32, top: i32, bottom: i32) -> Self { + pub const fn with_margins(&self, left: i32, right: i32, top: i32, bottom: i32) -> Self { Self::from_box( self.left() - left, self.right() + right, diff -r 64740eec84ad -r 4c523ed1d35c rust/land2d/src/lib.rs --- a/rust/land2d/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/land2d/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,9 +1,6 @@ -use std::{ - cmp, - ops::Index -}; +use std::{cmp, ops::Index}; -use integral_geometry::{ArcPoints, EquidistantPoints, Line, Point, Rect, Size, SizeMask}; +use integral_geometry::{ArcPoints, EquidistantPoints, Line, Point, PotSize, Rect, Size, SizeMask}; pub struct Land2D { pixels: vec2d::Vec2D, @@ -15,13 +12,13 @@ pub fn new(play_size: Size, fill_value: T) -> Self { let real_size = play_size.next_power_of_two(); let top_left = Point::new( - ((real_size.width - play_size.width) / 2) as i32, - (real_size.height - play_size.height) as i32, + ((real_size.width() - play_size.width) / 2) as i32, + (real_size.height() - play_size.height) as i32, ); let play_box = Rect::from_size(top_left, play_size); Self { play_box, - pixels: vec2d::Vec2D::new(real_size, fill_value), + pixels: vec2d::Vec2D::new(real_size.size(), fill_value), mask: real_size.to_mask(), } } @@ -31,9 +28,7 @@ } pub fn raw_pixel_bytes(&self) -> &[u8] { - unsafe { - self.pixels.as_bytes() - } + unsafe { self.pixels.as_bytes() } } #[inline] @@ -47,8 +42,8 @@ } #[inline] - pub fn size(&self) -> Size { - self.pixels.size() + pub fn size(&self) -> PotSize { + self.mask.to_size() } #[inline] @@ -117,7 +112,8 @@ *v = value; 1 }) - }).count() + }) + .count() } pub fn draw_line(&mut self, line: Line, value: T) -> usize { @@ -144,36 +140,39 @@ if mask.contains_y(yd as usize) { stack.push((xl, xr, yd as usize, dir)); } - }; + } let start_x_l = (start_point.x - 1) as usize; let start_x_r = start_point.x as usize; for dir in [-1, 1].iter().cloned() { - push(mask, &mut stack, start_x_l, start_x_r, start_point.y as usize, dir); + push( + mask, + &mut stack, + start_x_l, + start_x_r, + start_point.y as usize, + dir, + ); } while let Some((mut xl, mut xr, y, dir)) = stack.pop() { let row = &mut self.pixels[y][..]; - while xl > 0 && row[xl] != border_value && row[xl] != fill_value - { + while xl > 0 && row[xl] != border_value && row[xl] != fill_value { xl -= 1; } - while xr < width - 1 && row[xr] != border_value && row[xr] != fill_value - { + while xr < width - 1 && row[xr] != border_value && row[xr] != fill_value { xr += 1; } while xl < xr { - while xl <= xr && (row[xl] == border_value || row[xl] == fill_value) - { + while xl <= xr && (row[xl] == border_value || row[xl] == fill_value) { xl += 1; } let x = xl; - while xl <= xr && row[xl] != border_value && row[xl] != fill_value - { + while xl <= xr && row[xl] != border_value && row[xl] != fill_value { row[xl] = fill_value; xl += 1; } @@ -257,7 +256,8 @@ .iter() .map(|m| self.fill_row(center, vector.transform(m), value)) .sum::() - }).sum() + }) + .sum() } pub fn draw_thick_line(&mut self, line: Line, radius: i32, value: T) -> usize { diff -r 64740eec84ad -r 4c523ed1d35c rust/land_dump/src/main.rs --- a/rust/land_dump/src/main.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/land_dump/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -2,23 +2,21 @@ use std::{ fs::File, io::{BufWriter, Read}, - path::{Path, PathBuf} + path::{Path, PathBuf}, }; use structopt::StructOpt; use integral_geometry::{Point, Rect, Size}; +use land2d::Land2D; use landgen::{ - outline_template::OutlineTemplate, - template_based::TemplatedLandGenerator, - LandGenerationParameters, - LandGenerator -}; -use mapgen::{ - MapGenerator, - theme::{Theme, slice_u32_to_u8} + outline_template::OutlineTemplate, template_based::TemplatedLandGenerator, + LandGenerationParameters, LandGenerator, }; use lfprng::LaggedFibonacciPRNG; -use land2d::Land2D; +use mapgen::{ + theme::{slice_u32_to_u8, Theme}, + MapGenerator, +}; #[derive(StructOpt, Debug)] #[structopt(name = "basic")] @@ -36,7 +34,7 @@ #[structopt(short = "t", long = "template-type")] template_type: Option, #[structopt(short = "z", long = "theme-dir")] - theme_dir: Option + theme_dir: Option, } fn template() -> OutlineTemplate { @@ -60,7 +58,8 @@ skip_bezier: bool, file_name: &Path, ) -> std::io::Result> { - let params = LandGenerationParameters::new(0 as u8, 255, distance_divisor, skip_distort, skip_bezier); + let params = + LandGenerationParameters::new(0 as u8, 255, distance_divisor, skip_distort, skip_bezier); let landgen = TemplatedLandGenerator::new(template.clone()); let mut prng = LaggedFibonacciPRNG::new(seed); let land = landgen.generate_land(¶ms, &mut prng); @@ -87,40 +86,39 @@ let ref mut w = BufWriter::new(file); let mut encoder = png::Encoder::new(w, land.width() as u32, land.height() as u32); // Width is 2 pixels and height is 1. - encoder - .set(png::ColorType::RGBA) - .set(png::BitDepth::Eight); + encoder.set(png::ColorType::RGBA).set(png::BitDepth::Eight); let mut writer = encoder.write_header().unwrap(); - writer.write_image_data(slice_u32_to_u8(texture.as_slice())).unwrap(); + writer + .write_image_data(slice_u32_to_u8(texture.as_slice())) + .unwrap(); } fn main() { let opt = Opt::from_args(); println!("{:?}", opt); - let template = - if let Some(path) = opt.templates_file { - let mut result = String::new(); - File::open(path) - .expect("Unable to read templates file") - .read_to_string(&mut result); + let template = if let Some(path) = opt.templates_file { + let mut result = String::new(); + File::open(path) + .expect("Unable to read templates file") + .read_to_string(&mut result); - let mut generator = MapGenerator::new(); + let mut generator = MapGenerator::new(); - let source = &result[..]; + let source = &result[..]; - generator.import_yaml_templates(source); + generator.import_yaml_templates(source); - let template_type = &opt.template_type - .expect("No template type specified"); - generator.get_template(template_type) - .expect(&format!("Template type {} not found", template_type)) - .clone() - } else { - template() - }; + let template_type = &opt.template_type.expect("No template type specified"); + generator + .get_template(template_type) + .expect(&format!("Template type {} not found", template_type)) + .clone() + } else { + template() + }; if opt.dump_before_distort { dump( @@ -155,10 +153,6 @@ .unwrap(); if let Some(dir) = opt.theme_dir { - texturize( - &Path::new(&dir), - &land, - &Path::new("out.texture.png") - ); + texturize(&Path::new(&dir), &land, &Path::new("out.texture.png")); } } diff -r 64740eec84ad -r 4c523ed1d35c rust/landgen/src/lib.rs --- a/rust/landgen/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/landgen/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -11,7 +11,13 @@ } impl LandGenerationParameters { - pub fn new(zero: T, basic: T, distance_divisor: u32, skip_distort: bool, skip_bezier: bool) -> Self { + pub fn new( + zero: T, + basic: T, + distance_divisor: u32, + skip_distort: bool, + skip_bezier: bool, + ) -> Self { Self { zero, basic, diff -r 64740eec84ad -r 4c523ed1d35c rust/landgen/src/outline.rs --- a/rust/landgen/src/outline.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/landgen/src/outline.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,7 +1,7 @@ use itertools::Itertools; use std::cmp::min; -use integral_geometry::{Line, Ray, Point, Polygon, Rect, Size}; +use integral_geometry::{Line, Point, Polygon, Ray, Rect, Size}; use land2d::Land2D; use crate::outline_template::OutlineTemplate; @@ -76,16 +76,14 @@ fn solve_intersection( intersections_box: &Rect, ray: &Ray, - edge: &Line - ) -> Option<(i32, u32)> - { + edge: &Line, + ) -> Option<(i32, u32)> { let edge_dir = edge.scaled_direction(); let aqpb = ray.direction.cross(edge_dir) as i64; if aqpb != 0 { - let mut iy = - ((((edge.start.x - ray.start.x) as i64 * ray.direction.y as i64 - + ray.start.y as i64 * ray.direction.x as i64) + let mut iy = ((((edge.start.x - ray.start.x) as i64 * ray.direction.y as i64 + + ray.start.y as i64 * ray.direction.x as i64) * edge_dir.y as i64 - edge.start.y as i64 * edge_dir.x as i64 * ray.direction.y as i64) / aqpb) as i32; @@ -147,7 +145,7 @@ // same for the right border let right_intersection = Point::new( map_box.right(), - mid_point.y + normal.tangent_mul(map_box.right() - mid_point.x) , + mid_point.y + normal.tangent_mul(map_box.right() - mid_point.x), ); dist_right = (mid_point - right_intersection).integral_norm(); @@ -203,7 +201,9 @@ if intersects(&pi.ray_with_dir(normal), &segment) { // ray from segment.start if let Some((t, d)) = solve_intersection( - &self.intersections_box, &normal_ray, &segment.start.line_to(pi), + &self.intersections_box, + &normal_ray, + &segment.start.line_to(pi), ) { if t > 0 { dist_right = min(dist_right, d); @@ -214,7 +214,9 @@ // ray from segment.end if let Some((t, d)) = solve_intersection( - &self.intersections_box, &normal_ray, &segment.end.line_to(pi) + &self.intersections_box, + &normal_ray, + &segment.end.line_to(pi), ) { if t > 0 { dist_right = min(dist_right, d); @@ -308,7 +310,7 @@ } } -#[test()] +#[test] fn points_test() { let size = Size::square(100); let mut points = OutlinePoints { diff -r 64740eec84ad -r 4c523ed1d35c rust/landgen/src/template_based.rs --- a/rust/landgen/src/template_based.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/landgen/src/template_based.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,11 +1,9 @@ +use crate::{ + outline::OutlinePoints, outline_template::OutlineTemplate, LandGenerationParameters, + LandGenerator, +}; use integral_geometry::{Point, Size}; use land2d::Land2D; -use crate::{ - LandGenerationParameters, - LandGenerator, - outline::OutlinePoints, - outline_template::OutlineTemplate -}; pub struct TemplatedLandGenerator { outline_template: OutlineTemplate, @@ -28,7 +26,7 @@ let mut points = OutlinePoints::from_outline_template( &self.outline_template, land.play_box(), - land.size(), + land.size().size(), random_numbers, ); diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/Cargo.toml --- a/rust/lib-hedgewars-engine/Cargo.toml Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -2,12 +2,13 @@ name = "lib-hedgewars-engine" version = "0.1.0" authors = ["Andrey Korotaev "] -edition = "2018" +edition = "2021" +build = "build.rs" [dependencies] gl = "0.11" netbuf = "0.4" -itertools = "0.8" +itertools = "0.10" png = "0.13" fpnum = { path = "../fpnum" } @@ -23,6 +24,9 @@ [dev-dependencies] proptest = "0.9.2" +[build-dependencies] +cbindgen = "0.24" + [lib] name = "hedgewars_engine" crate-type = ["dylib"] diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/build.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/lib-hedgewars-engine/build.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,32 @@ +extern crate cbindgen; + +use cbindgen::Config; +use std::env; +use std::path::PathBuf; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let package_name = env::var("CARGO_PKG_NAME").unwrap(); + let output_file = target_dir() + .join(format!("{}.hpp", package_name)) + .display() + .to_string(); + + let config = Config { + namespace: Some(String::from("hwengine")), + ..Default::default() + }; + + cbindgen::generate_with_config(&crate_dir, config) + .unwrap() + .write_to_file(&output_file); +} + +fn target_dir() -> PathBuf { + if let Ok(target) = env::var("CARGO_TARGET_DIR") { + PathBuf::from(target) + } else { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target") + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/instance.rs --- a/rust/lib-hedgewars-engine/src/instance.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/instance.rs Sun Mar 24 14:33:57 2024 -0400 @@ -7,6 +7,8 @@ use integral_geometry::{Point, Rect, Size}; use landgen::outline_template::OutlineTemplate; +use std::path::Path; + use super::{ipc::*, world::World}; pub struct EngineInstance { @@ -16,8 +18,8 @@ } impl EngineInstance { - pub fn new() -> Self { - let mut world = World::new(); + pub fn new(data_path: &Path) -> Self { + let mut world = World::new(data_path); fn template() -> OutlineTemplate { let mut template = OutlineTemplate::new(Size::new(4096 * 1, 2048 * 1)); @@ -55,6 +57,7 @@ fn process_config_message(&mut self, message: &ConfigEngineMessage) { match message { SetSeed(seed) => self.world.set_seed(seed.as_bytes()), + SetFeatureSize(feature_size) => self.world.set_feature_size(*feature_size), _ => unimplemented!(), } } diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/lib.rs --- a/rust/lib-hedgewars-engine/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,13 +1,15 @@ pub mod instance; pub mod ipc; mod render; +mod time; mod world; use std::{ - ffi::CString, + ffi::{CString, CStr}, io::{Read, Write}, mem::replace, os::raw::{c_char, c_void}, + path::Path, }; use integral_geometry::Point; @@ -78,7 +80,6 @@ x: i32, y: i32, ) { - } #[no_mangle] @@ -87,8 +88,10 @@ } #[no_mangle] -pub extern "C" fn start_engine() -> *mut EngineInstance { - let engine_state = Box::new(EngineInstance::new()); +pub extern "C" fn start_engine(data_path: *const i8) -> *mut EngineInstance { + let data_path: &str = unsafe { CStr::from_ptr(data_path) }.to_str().unwrap(); + + let engine_state = Box::new(EngineInstance::new(Path::new(&data_path))); Box::leak(engine_state) } @@ -110,7 +113,7 @@ } #[no_mangle] -pub extern "C" fn dispose_preview(engine_state: &mut EngineInstance, preview: &mut PreviewInfo) { +pub extern "C" fn dispose_preview(engine_state: &mut EngineInstance) { (*engine_state).world.dispose_preview(); } @@ -139,7 +142,7 @@ engine_state: &mut EngineInstance, width: u16, height: u16, - gl_loader: extern "C" fn(*const c_char) -> *const c_void, + gl_loader: extern "C" fn(*const c_char) -> *mut c_void, ) { gl::load_with(|name| { let c_name = CString::new(name).unwrap(); @@ -172,6 +175,6 @@ #[no_mangle] pub extern "C" fn cleanup(engine_state: *mut EngineInstance) { unsafe { - Box::from_raw(engine_state); + drop(Box::from_raw(engine_state)); } } diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/render/atlas.rs --- a/rust/lib-hedgewars-engine/src/render/atlas.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/render/atlas.rs Sun Mar 24 14:33:57 2024 -0400 @@ -14,8 +14,8 @@ impl Fit { fn new() -> Self { Self { - short_side: u32::max_value(), - long_side: u32::max_value(), + short_side: u32::MAX, + long_side: u32::MAX, } } @@ -335,7 +335,7 @@ buffer, &mut buffer_size, free_rect.with_margins(0, 0, 0, -trim), - );; + ); } if rect.bottom() < free_rect.bottom() { let trim = rect.bottom() - free_rect.top() + 1; @@ -344,7 +344,7 @@ buffer, &mut buffer_size, free_rect.with_margins(0, 0, -trim, 0), - );; + ); } } if split { diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/render/gear.rs --- a/rust/lib-hedgewars-engine/src/render/gear.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/render/gear.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,22 +1,119 @@ -use super::{atlas::AtlasCollection, gl::Texture2D}; -use crate::render::camera::Camera; +use crate::render::{ + atlas::{AtlasCollection, SpriteIndex}, + camera::Camera, + gl::{ + Buffer, BufferType, BufferUsage, InputElement, InputFormat, InputLayout, PipelineState, + Shader, Texture2D, TextureDataType, TextureFilter, TextureFormat, TextureInternalFormat, + VariableBinding, + }, +}; use integral_geometry::{Rect, Size}; -use crate::render::atlas::SpriteIndex; use png::{ColorType, Decoder, DecodingError}; -use std::path::PathBuf; + use std::{ collections::HashMap, ffi::OsString, fs::{read_dir, File}, io, io::BufReader, - path::Path, + mem::size_of, + path::{Path, PathBuf}, }; +const VERTEX_SHADER: &'static str = r#" +#version 330 core + +uniform mat4 projection; + +layout(location = 0) in vec2 position; +layout(location = 1) in vec2 texCoords; + +out vec2 varTexCoords; + +void main() { + varTexCoords = texCoords; + gl_Position = projection * vec4(position, 0.0, 1.0); +} +"#; + +const PIXEL_SHADER: &'static str = r#" +#version 330 core + +uniform sampler2D texture; + +in vec2 varTexCoords; + +out vec4 outColor; + +void main() { + outColor = texture2D(texture, varTexCoords); +} +"#; + +#[repr(C)] +#[derive(Copy, Clone)] +struct Vertex { + position: [f32; 2], + tex_coords: [f32; 2], +} + +#[derive(PartialEq, Debug, Clone, Copy)] +#[repr(u32)] +pub enum SpriteId { + Mine = 0, + Grenade, + Cheese, + Cleaver, + + MaxSprite, +} + +const SPRITE_LOAD_LIST: &[(SpriteId, &str)] = &[ + ( + SpriteId::Mine, + "Graphics/MineOn.png", + ), + ( + SpriteId::Grenade, + "Graphics/Bomb.png", + ), + ( + SpriteId::Cheese, + "Graphics/cheese.png", + ), + ( + SpriteId::Cleaver, + "Graphics/cleaver.png", + ), +]; + +const MAX_SPRITES: usize = SpriteId::MaxSprite as usize + 1; + +type SpriteTexCoords = (u32, [[f32; 2]; 4]); + +pub struct GearEntry { + position: [f32; 2], + size: Size, +} + +impl GearEntry { + pub fn new(x: f32, y: f32, size: Size) -> Self { + Self { + position: [x, y], + size, + } + } +} + pub struct GearRenderer { atlas: AtlasCollection, + texture: Texture2D, + allocation: Box<[SpriteTexCoords; MAX_SPRITES]>, + shader: Shader, + layout: InputLayout, + vertex_buffer: Buffer, } struct SpriteData { @@ -27,54 +124,147 @@ const ATLAS_SIZE: Size = Size::square(2048); impl GearRenderer { - pub fn new() -> Self { - let mut lookup = Vec::with_capacity(2048); - + pub fn new(data_path: &Path) -> Self { let mut atlas = AtlasCollection::new(ATLAS_SIZE); - let mut sprites = load_sprites(Path::new("../../share/hedgewars/Data/Graphics/")) - .expect("Unable to load Graphics"); - let max_size = sprites - .iter() - .fold(Size::EMPTY, |size, sprite| size.join(sprite.size)); - for sprite in sprites.drain(..) { - lookup.push((sprite.filename, atlas.insert_sprite(sprite.size).unwrap())); - } - println!( - "Filled atlas with {} sprites:\n{}", - sprites.len(), - atlas.used_space() + let texture = Texture2D::new( + ATLAS_SIZE, + TextureInternalFormat::Rgba8, + TextureFilter::Linear, ); - let texture = Texture2D::new(ATLAS_SIZE, gl::RGBA8, gl::LINEAR); + let mut allocation = Box::new([Default::default(); MAX_SPRITES]); - let mut pixels = vec![0; max_size.area()].into_boxed_slice(); - let mut pixels_transposed = vec![0; max_size.area()].into_boxed_slice(); + for (sprite, file) in SPRITE_LOAD_LIST { + let path = data_path.join(Path::new(file)); + let size = load_sprite_size(path.as_path()).expect(&format!("Unable to open {}", file)); + let index = atlas + .insert_sprite(size) + .expect(&format!("Could not store sprite {:?}", sprite)); + let (texture_index, rect) = atlas.get_rect(index).unwrap(); - for (path, sprite_index) in lookup.drain(..) { - if let Some((atlas_index, rect)) = atlas.get_rect(sprite_index) { - let size = load_sprite_pixels(&path, mapgen::theme::slice_u32_to_u8_mut(&mut pixels[..])).expect("Unable to load Graphics"); + let mut pixels = vec![255u8; size.area() * 4].into_boxed_slice(); + load_sprite_pixels(path.as_path(), &mut pixels).expect("Unable to load Graphics"); - let used_pixels = if size.width != rect.width() { - for y in 0..rect.height() { - for x in 0..rect.width() { - pixels_transposed[y * rect.width() + x] = pixels[x * rect.height() + y]; - } - } - &mut pixels_transposed[..] - } else { - &mut pixels[..] - }; + texture.update( + rect, + &pixels, + None, + TextureFormat::Rgba, + TextureDataType::UnsignedByte, + ); - texture.update(rect, mapgen::theme::slice_u32_to_u8_mut(used_pixels), 0, gl::RGBA, gl::UNSIGNED_BYTE); + let mut tex_coords = [ + [rect.left() as f32, rect.bottom() as f32 + 1.0], + [rect.right() as f32 + 1.0, rect.bottom() as f32 + 1.0], + [rect.left() as f32, rect.top() as f32], + [rect.right() as f32 + 1.0, rect.top() as f32], + ]; //.map(|n| n as f32 / ATLAS_SIZE as f32); + + for coords in &mut tex_coords { + coords[0] /= ATLAS_SIZE.width as f32; + coords[1] /= ATLAS_SIZE.height as f32; } + + allocation[*sprite as usize] = (texture_index, tex_coords); } - Self { atlas } + let shader = Shader::new( + VERTEX_SHADER, + Some(PIXEL_SHADER), + &[VariableBinding::Sampler("texture", 0)], + ) + .unwrap(); + + let layout = InputLayout::new(vec![ + InputElement { + shader_slot: 0, + buffer_slot: 0, + format: InputFormat::Float(gl::FLOAT, false), + components: 2, + stride: size_of::() as u32, + offset: 0, + }, + InputElement { + shader_slot: 1, + buffer_slot: 0, + format: InputFormat::Float(gl::FLOAT, false), + components: 2, + stride: size_of::() as u32, + offset: size_of::<[f32; 2]>() as u32, + }, + ]); + + let vertex_buffer = Buffer::empty(BufferType::Array, BufferUsage::DynamicDraw); + + Self { + atlas, + texture, + allocation, + shader, + layout, + vertex_buffer, + } } - pub fn render(&mut self, camera: &Camera) { + pub fn render(&mut self, camera: &Camera, entries: &[GearEntry]) { + let mut data = Vec::with_capacity(entries.len() * 6); + + for (index, entry) in entries.iter().enumerate() { + let sprite_id = match index & 0b11 { + 0 => SpriteId::Mine, + 1 => SpriteId::Grenade, + 2 => SpriteId::Cheese, + _ => SpriteId::Cleaver, + }; + let sprite_coords = &self.allocation[sprite_id as usize].1; + + let v = [ + Vertex { + position: [ + entry.position[0] - entry.size.width as f32 / 2.0, + entry.position[1] + entry.size.height as f32 / 2.0, + ], + tex_coords: sprite_coords[0], + }, + Vertex { + position: [ + entry.position[0] + entry.size.width as f32 / 2.0, + entry.position[1] + entry.size.height as f32 / 2.0, + ], + tex_coords: sprite_coords[1], + }, + Vertex { + position: [ + entry.position[0] - entry.size.width as f32 / 2.0, + entry.position[1] - entry.size.height as f32 / 2.0, + ], + tex_coords: sprite_coords[2], + }, + Vertex { + position: [ + entry.position[0] + entry.size.width as f32 / 2.0, + entry.position[1] - entry.size.height as f32 / 2.0, + ], + tex_coords: sprite_coords[3], + }, + ]; + + data.extend_from_slice(&[v[0], v[1], v[2], v[1], v[3], v[2]]); + } + let projection = camera.projection(); + self.shader.bind(); + self.shader.set_matrix("projection", projection.as_ptr()); + self.shader.bind_texture_2d(0, &self.texture); + + self.vertex_buffer.write_typed(&data); + let _buffer_bind = self.layout.bind(&[(0, &self.vertex_buffer)], None); + + let _state = PipelineState::new().with_blend(); + unsafe { + gl::DrawArrays(gl::TRIANGLES, 0, entries.len() as i32 * 6); + } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/render/gl.rs --- a/rust/lib-hedgewars-engine/src/render/gl.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/render/gl.rs Sun Mar 24 14:33:57 2024 -0400 @@ -32,7 +32,8 @@ #[derive(Debug)] pub struct Texture2D { - pub handle: Option, + handle: Option, + size: Size, } impl Drop for Texture2D { @@ -53,7 +54,7 @@ NonZeroU32::new(handle) } -fn tex_params(filter: u32) { +fn tex_params(filter: TextureFilter) { unsafe { gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32); gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32); @@ -62,8 +63,42 @@ } } +#[derive(Clone, Copy, Debug)] +pub enum TextureFormat { + Rgba = gl::RGBA as isize, +} + +#[derive(Clone, Copy, Debug)] +pub enum TextureInternalFormat { + Rgba8 = gl::RGBA as isize, +} + +#[derive(Clone, Copy, Debug)] +pub enum TextureDataType { + UnsignedByte = gl::UNSIGNED_BYTE as isize, +} + +#[derive(Clone, Copy, Debug)] +pub enum TextureFilter { + Nearest = gl::NEAREST as isize, + Linear = gl::LINEAR as isize, +} + +#[inline] +fn get_u32(value: Option) -> u32 { + value.map_or(0, |v| v.get()) +} + +fn is_out_of_bounds(data: &[u8], data_stride: Option, texture_size: Size) -> bool { + let data_stride = get_u32(data_stride); + data_stride == 0 && texture_size.area() * 4 > data.len() + || data_stride != 0 + && texture_size.width > data_stride as usize + && (texture_size.height * data_stride as usize) * 4 > data.len() +} + impl Texture2D { - pub fn new(size: Size, internal_format: u32, filter: u32) -> Self { + pub fn new(size: Size, internal_format: TextureInternalFormat, filter: TextureFilter) -> Self { if let Some(handle) = new_texture() { unsafe { gl::BindTexture(gl::TEXTURE_2D, handle.get()); @@ -74,8 +109,8 @@ size.width as i32, size.height as i32, 0, - gl::RGBA, - gl::UNSIGNED_BYTE, + TextureFormat::Rgba as u32, + TextureDataType::UnsignedByte as u32, std::ptr::null(), ) } @@ -83,25 +118,30 @@ tex_params(filter); Self { handle: Some(handle), + size, } } else { - Self { handle: None } + Self { handle: None, size } } } pub fn with_data( data: &[u8], - data_stride: u32, + data_stride: Option, size: Size, - internal_format: u32, - format: u32, - ty: u32, - filter: u32, + internal_format: TextureInternalFormat, + format: TextureFormat, + data_type: TextureDataType, + filter: TextureFilter, ) -> Self { + if is_out_of_bounds(data, data_stride, size) { + return Self { handle: None, size }; + } + if let Some(handle) = new_texture() { unsafe { gl::BindTexture(gl::TEXTURE_2D, handle.get()); - gl::PixelStorei(gl::UNPACK_ROW_LENGTH, data_stride as i32); + gl::PixelStorei(gl::UNPACK_ROW_LENGTH, get_u32(data_stride) as i32); gl::TexImage2D( gl::TEXTURE_2D, 0, @@ -110,7 +150,7 @@ size.height as i32, 0, format as u32, - ty, + data_type as u32, data.as_ptr() as *const _, ) } @@ -118,132 +158,162 @@ tex_params(filter); Self { handle: Some(handle), + size, } } else { - Self { handle: None } + Self { handle: None, size } } } - pub fn update(&self, region: Rect, data: &[u8], data_stride: u32, format: u32, ty: u32) { + pub fn update( + &self, + region: Rect, + data: &[u8], + data_stride: Option, + format: TextureFormat, + data_type: TextureDataType, + ) { if let Some(handle) = self.handle { unsafe { gl::BindTexture(gl::TEXTURE_2D, handle.get()); - gl::PixelStorei(gl::UNPACK_ROW_LENGTH, data_stride as i32); + gl::PixelStorei(gl::UNPACK_ROW_LENGTH, get_u32(data_stride) as i32); gl::TexSubImage2D( gl::TEXTURE_2D, - 0, // texture level - region.left(), // texture region + 0, + region.left(), region.top(), region.width() as i32, region.height() as i32, - format, // data format - ty, // data type - data.as_ptr() as *const _, // data ptr + format as u32, + data_type as u32, + data.as_ptr() as *const _, ); } } } pub fn retrieve(&self, data: &mut [u8]) { + if self.size.area() * 4 > data.len() { + return; + } + if let Some(handle) = self.handle { unsafe { gl::BindTexture(gl::TEXTURE_2D, handle.get()); gl::GetTexImage( gl::TEXTURE_2D, - 0, // texture level - gl::RGBA, // data format - gl::UNSIGNED_BYTE, // data type - data.as_mut_ptr() as *mut _, // data ptr + 0, + TextureFormat::Rgba as u32, + TextureDataType::UnsignedByte as u32, + data.as_mut_ptr() as *mut _, ); } } } } +#[derive(Clone, Copy, Debug)] +#[repr(u32)] +pub enum BufferType { + Array = gl::ARRAY_BUFFER, + ElementArray = gl::ELEMENT_ARRAY_BUFFER, +} + +#[derive(Clone, Copy, Debug)] +#[repr(u32)] +pub enum BufferUsage { + DynamicDraw = gl::DYNAMIC_DRAW, +} + #[derive(Debug)] pub struct Buffer { - pub handle: u32, - pub ty: u32, - pub usage: u32, + pub handle: Option, + pub buffer_type: BufferType, + pub usage: BufferUsage, } impl Buffer { - pub fn empty( - ty: u32, - usage: u32, - //size: isize - ) -> Buffer { + pub fn empty(buffer_type: BufferType, usage: BufferUsage) -> Buffer { let mut buffer = 0; unsafe { gl::GenBuffers(1, &mut buffer); - gl::BindBuffer(ty, buffer); - //gl::BufferData(ty, size, ptr::null_mut(), usage); } Buffer { - handle: buffer, - ty, + handle: NonZeroU32::new(buffer), + buffer_type: buffer_type, usage, } } - fn with_data(ty: u32, usage: u32, data: &[u8]) -> Buffer { + fn with_data(buffer_type: BufferType, usage: BufferUsage, data: &[u8]) -> Buffer { let mut buffer = 0; unsafe { gl::GenBuffers(1, &mut buffer); - gl::BindBuffer(ty, buffer); - gl::BufferData(ty, data.len() as isize, data.as_ptr() as _, usage); + if buffer != 0 { + gl::BindBuffer(buffer_type as u32, buffer); + gl::BufferData( + buffer_type as u32, + data.len() as isize, + data.as_ptr() as _, + usage as u32, + ); + } } Buffer { - handle: buffer, - ty, + handle: NonZeroU32::new(buffer), + buffer_type, usage, } } - pub fn ty(&self) -> u32 { - self.ty + pub fn ty(&self) -> BufferType { + self.buffer_type } - pub fn handle(&self) -> u32 { + pub fn handle(&self) -> Option { self.handle } pub fn write_typed(&self, data: &[T]) { - unsafe { - let data = - slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * mem::size_of::()); - - gl::BindBuffer(self.ty, self.handle); - gl::BufferData( - self.ty, - data.len() as isize, - data.as_ptr() as *const _ as *const _, - self.usage, - ); + if let Some(handle) = self.handle { + unsafe { + gl::BindBuffer(self.buffer_type as u32, handle.get()); + gl::BufferData( + self.buffer_type as u32, + (data.len() * mem::size_of::()) as isize, + data.as_ptr() as *const _, + self.usage as u32, + ); + } } } pub fn write(&self, data: &[u8]) { - unsafe { - gl::BindBuffer(self.ty, self.handle); - gl::BufferData( - self.ty, - data.len() as isize, - data.as_ptr() as *const _ as *const _, - self.usage, - ); + if let Some(handle) = self.handle { + unsafe { + gl::BindBuffer(self.buffer_type as u32, handle.get()); + gl::BufferData( + self.buffer_type as u32, + data.len() as isize, + data.as_ptr() as *const _, + self.usage as u32, + ); + } } } } impl Drop for Buffer { fn drop(&mut self) { - unsafe { - gl::DeleteBuffers(1, &self.handle); + if let Some(handle) = self.handle { + let handle = handle.get(); + unsafe { + gl::DeleteBuffers(1, &handle); + } } } } @@ -275,11 +345,11 @@ ps: Option<&str>, bindings: &[VariableBinding<'a>], ) -> Result { - unsafe fn compile_shader(ty: u32, shdr: &str) -> Result { - let shader = gl::CreateShader(ty); - let len = shdr.len() as i32; - let shdr = shdr.as_ptr() as *const i8; - gl::ShaderSource(shader, 1, &shdr, &len); + unsafe fn compile_shader(shader_type: u32, shader_code: &str) -> Result { + let shader = gl::CreateShader(shader_type); + let len = shader_code.len() as i32; + let code_strings = shader_code.as_ptr() as *const i8; + gl::ShaderSource(shader, 1, &code_strings, &len); gl::CompileShader(shader); let mut success = 0i32; @@ -343,14 +413,8 @@ return Err(String::from_utf8_unchecked(log)); } - //gl::DetachShader(program, vs); - if let Some(ps) = ps { - //gl::DetachShader(program, ps); - } - gl::UseProgram(program); - // after linking we setup sampler bindings as specified in the shader for bind in bindings { match bind { VariableBinding::Uniform(name, id) => { @@ -468,8 +532,10 @@ } for &(slot, ref buffer) in buffers { - unsafe { - gl::BindBuffer(buffer.ty(), buffer.handle()); + if let Some(handle) = buffer.handle() { + unsafe { + gl::BindBuffer(buffer.ty() as u32, handle.get()); + } } for attr in self.elements.iter().filter(|a| a.buffer_slot == slot) { @@ -501,8 +567,10 @@ } if let Some(buf) = index_buffer { - unsafe { - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, buf.handle()); + if let Some(handle) = buf.handle() { + unsafe { + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, handle.get()); + } } } diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/render/map.rs --- a/rust/lib-hedgewars-engine/src/render/map.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/render/map.rs Sun Mar 24 14:33:57 2024 -0400 @@ -5,12 +5,14 @@ use super::{ camera::Camera, gl::{ - Buffer, InputElement, InputFormat, InputLayout, PipelineState, Shader, Texture2D, + Buffer, BufferType, BufferUsage, InputElement, InputFormat, InputLayout, PipelineState, + Shader, Texture2D, TextureDataType, TextureFilter, TextureFormat, TextureInternalFormat, VariableBinding, }, }; -// TODO: temp +use std::num::NonZeroU32; + const VERTEX_SHADER: &'static str = r#" #version 150 @@ -45,7 +47,7 @@ } "#; -pub struct MapTile { +struct MapTile { // either index into GL texture array or emulated [Texture; N] texture_index: u32, @@ -55,13 +57,13 @@ #[repr(C)] #[derive(Copy, Clone)] -pub struct TileVertex { +struct TileVertex { pos: [f32; 2], // doesn't hurt to include another float, just in case.. uv: [f32; 3], } -pub struct DrawTile { +struct DrawTile { texture_index: u32, index_len: u32, } @@ -124,8 +126,8 @@ tiles: Vec::new(), textures: Vec::new(), - tile_vertex_buffer: Buffer::empty(gl::ARRAY_BUFFER, gl::DYNAMIC_DRAW), - tile_index_buffer: Buffer::empty(gl::ELEMENT_ARRAY_BUFFER, gl::DYNAMIC_DRAW), + tile_vertex_buffer: Buffer::empty(BufferType::Array, BufferUsage::DynamicDraw), + tile_index_buffer: Buffer::empty(BufferType::ElementArray, BufferUsage::DynamicDraw), tile_vertices: Vec::new(), tile_indices: Vec::new(), index_offset: 0, @@ -164,7 +166,7 @@ let data = unsafe { &land.as_bytes()[offset..] }; let stride = land.width(); - (data, stride as u32) + (data, NonZeroU32::new(stride as u32)) }; let texture_index = if idx >= self.textures.len() { @@ -172,10 +174,10 @@ data, stride, self.tile_size, - gl::RGBA8, - gl::RGBA, - gl::UNSIGNED_BYTE, - gl::NEAREST, + TextureInternalFormat::Rgba8, + TextureFormat::Rgba, + TextureDataType::UnsignedByte, + TextureFilter::Nearest, ); let texture_index = self.textures.len(); @@ -189,8 +191,8 @@ texture_region, data, stride, - gl::RGBA, - gl::UNSIGNED_BYTE, + TextureFormat::Rgba, + TextureDataType::UnsignedByte, ); idx }; diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/time.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/lib-hedgewars-engine/src/time.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,113 @@ +use hwphysics::common::{GearId, Millis}; +use std::{ + cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}, + collections::BinaryHeap, +}; + +pub type EventId = u16; + +struct TimeEvent { + time: Millis, + gear_id: GearId, + event_id: EventId, +} + +impl PartialOrd for TimeEvent { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.time.partial_cmp(&other.time) + } +} + +impl PartialEq for TimeEvent { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.time.eq(&other.time) + } +} + +impl Ord for TimeEvent { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.time.cmp(&other.time) + } +} + +impl Eq for TimeEvent {} + +pub struct OccurredEvents { + events: Vec<(GearId, EventId)>, +} + +impl OccurredEvents { + fn new() -> Self { + Self { events: vec![] } + } + + fn clear(&mut self) { + self.events.clear() + } +} + +pub struct TimeProcessor { + current_event_id: EventId, + current_time: Millis, + events: BinaryHeap, + timeouts: OccurredEvents, +} + +impl TimeProcessor { + pub fn new() -> Self { + Self { + current_event_id: 0, + current_time: Millis::new(0), + events: BinaryHeap::with_capacity(1024), + timeouts: OccurredEvents::new(), + } + } + + pub fn register(&mut self, gear_id: GearId, timeout: Millis) -> EventId { + let event_id = self.current_event_id; + self.current_event_id = self.current_event_id.wrapping_add(1); + let event = TimeEvent { + time: self.current_time + timeout, + gear_id, + event_id, + }; + self.events.push(event); + event_id + } + + fn retain_events

(&mut self, predicate: P) + where + P: Fn(&TimeEvent) -> bool, + { + let events = self.events.drain().filter(predicate).collect::>(); + self.events.extend(events); + } + + pub fn cancel(&mut self, event_id: EventId) { + //self.events.retain(|event| event.event_id != event_id) + self.retain_events(|event| event.event_id != event_id) + } + + pub fn cancel_all(&mut self, gear_id: GearId) { + //self.events.retain(|event| event.gear_id != gear_id) + self.retain_events(|event| event.gear_id != gear_id) + } + + pub fn process(&mut self, time_step: Millis) -> &OccurredEvents { + self.timeouts.clear(); + self.current_time = self.current_time + time_step; + while self + .events + .peek() + .filter(|e| e.time <= self.current_time) + .is_some() + { + let event = self.events.pop().unwrap(); + self.timeouts.events.push((event.gear_id, event.event_id)) + } + &self.timeouts + } +} diff -r 64740eec84ad -r 4c523ed1d35c rust/lib-hedgewars-engine/src/world.rs --- a/rust/lib-hedgewars-engine/src/world.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/lib-hedgewars-engine/src/world.rs Sun Mar 24 14:33:57 2024 -0400 @@ -2,6 +2,7 @@ use hwphysics::{ self as hwp, common::{GearId, Millis}, + physics::{PositionData, VelocityData}, }; use integral_geometry::{Point, Rect, Size}; use land2d::Land2D; @@ -10,8 +11,9 @@ LandGenerationParameters, LandGenerator, }; use lfprng::LaggedFibonacciPRNG; +use std::path::{Path, PathBuf}; -use crate::render::{camera::Camera, GearRenderer, MapRenderer}; +use crate::render::{camera::Camera, GearEntry, GearRenderer, MapRenderer}; struct GameState { land: Land2D, @@ -26,39 +28,44 @@ pub struct World { random_numbers_gen: LaggedFibonacciPRNG, + feature_size: u8, preview: Option>, game_state: Option, map_renderer: Option, gear_renderer: Option, camera: Camera, + gear_entries: Vec, + data_path: PathBuf, } impl World { - pub fn new() -> Self { + pub fn new(data_path: &Path) -> Self { Self { random_numbers_gen: LaggedFibonacciPRNG::new(&[]), + feature_size: 5, preview: None, game_state: None, map_renderer: None, gear_renderer: None, camera: Camera::new(), + gear_entries: vec![], + data_path: data_path.to_owned(), } } pub fn create_renderer(&mut self, width: u16, height: u16) { let land_tile_size = Size::square(512); self.map_renderer = Some(MapRenderer::new(land_tile_size)); - self.gear_renderer = Some(GearRenderer::new()); + self.gear_renderer = Some(GearRenderer::new(&self.data_path.as_path())); self.camera = Camera::with_size(Size::new(width as usize, height as usize)); use mapgen::{theme::Theme, MapGenerator}; - use std::path::Path; if let Some(ref state) = self.game_state { self.camera.position = state.land.play_box().center(); let theme = - Theme::load(Path::new("../../share/hedgewars/Data/Themes/Cheese/")).unwrap(); + Theme::load(self.data_path.join(Path::new("Themes/Cheese/")).as_path()).unwrap(); let texture = MapGenerator::new().make_texture(&state.land, &theme); if let Some(ref mut renderer) = self.map_renderer { renderer.init(&texture); @@ -70,6 +77,10 @@ self.random_numbers_gen = LaggedFibonacciPRNG::new(seed); } + pub fn set_feature_size(&mut self, feature_size: u8) { + self.feature_size = feature_size; + } + pub fn preview(&self) -> &Option> { &self.preview } @@ -88,7 +99,10 @@ template } - let params = LandGenerationParameters::new(0u8, u8::max_value(), 5, false, false); + // based on old engine min_distance... dunno if this is the correct place tho + let distance_divisor = (self.feature_size as u32).pow(2) / 8 + 10; + + let params = LandGenerationParameters::new(0u8, u8::MAX, distance_divisor, false, false); let landgen = TemplatedLandGenerator::new(template()); self.preview = Some(landgen.generate_land(¶ms, &mut self.random_numbers_gen)); } @@ -98,12 +112,12 @@ } pub fn init(&mut self, template: OutlineTemplate) { - let physics = hwp::World::new(template.size); - - let params = LandGenerationParameters::new(0u32, u32::max_value(), 5, false, false); + let params = LandGenerationParameters::new(0u32, u32::MAX, 5, false, false); let landgen = TemplatedLandGenerator::new(template); let land = landgen.generate_land(¶ms, &mut self.random_numbers_gen); + let physics = hwp::World::new(land.size()); + self.game_state = Some(GameState::new(land, physics)); } @@ -124,19 +138,36 @@ renderer.render(&self.camera); } + + self.gear_entries.clear(); + let mut gear_entries = std::mem::take(&mut self.gear_entries); + if let Some(ref mut renderer) = self.gear_renderer { - renderer.render(&self.camera) + if let Some(ref mut state) = self.game_state { + state + .physics + .iter_data() + .run(|(pos,): (&mut PositionData,)| { + gear_entries.push(GearEntry::new( + f64::from(pos.0.x()) as f32, + f64::from(pos.0.y()) as f32, + Size::square(256), + )) + }); + } + renderer.render(&self.camera, &gear_entries); } + self.gear_entries = gear_entries; } fn create_gear(&mut self, position: Point) { if let Some(ref mut state) = self.game_state { let id = state.physics.new_gear().unwrap(); let fp_position = FPPoint::new(position.x.into(), position.y.into()); - state.physics.add_gear_data( - id, - hwp::physics::PhysicsData::new(fp_position, FPPoint::zero()), - ) + state.physics.add_gear_data(id, &PositionData(fp_position)); + state + .physics + .add_gear_data(id, &VelocityData(FPPoint::zero())) } } diff -r 64740eec84ad -r 4c523ed1d35c rust/mapgen/src/lib.rs --- a/rust/mapgen/src/lib.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/mapgen/src/lib.rs Sun Mar 24 14:33:57 2024 -0400 @@ -121,7 +121,7 @@ where LandT: Copy + Default + PartialEq, { - let mut texture = Vec2D::new(land.size(), 0); + let mut texture = Vec2D::new(land.size().size(), 0); if let Some(land_sprite) = theme.land_texture() { for (row_index, (land_row, tex_row)) in land.rows().zip(texture.rows_mut()).enumerate() diff -r 64740eec84ad -r 4c523ed1d35c rust/mapgen/src/theme.rs --- a/rust/mapgen/src/theme.rs Sun Mar 24 14:05:06 2024 -0400 +++ b/rust/mapgen/src/theme.rs Sun Mar 24 14:33:57 2024 -0400 @@ -1,3 +1,4 @@ +use integral_geometry::{Point, Rect}; use png::{ColorType, Decoder, DecodingError}; use std::{ fs::{read_dir, File}, @@ -109,9 +110,70 @@ } } +#[derive(Default)] +struct Color(u8, u8, u8, u8); + +pub struct LandObjectOverlay { + texture: ThemeSprite, + offset: Point, +} + +pub struct LandObject { + texture: ThemeSprite, + inland_rects: Vec, + outland_rects: Vec, + anchors: Vec, + overlays: Vec, +} + +pub struct LandSpray { + texture: ThemeSprite, + count: u16, +} + +#[derive(Default)] +pub struct ThemeColors { + border: Color, +} + +pub struct Flakes { + texture: ThemeSprite, + frames_count: u16, + frame_ticks: u16, + velocity: u16, + fall_speed: u16, +} + +#[derive(Default)] +pub struct Water { + top_color: Color, + bottom_color: Color, + opacity: u8, +} + +#[derive(Default)] +pub struct ThemeParts { + water: Water, + flakes: Option, + music: String, + sky: Color, + tint: Color, +} + +#[derive(Default)] pub struct Theme { + border_color: Color, + clouds_count: u16, + flatten_flakes: bool, land_texture: Option, border_texture: Option, + land_objects: Vec, + spays: Vec, + use_ice: bool, + use_snow: bool, + music: String, + normal_parts: ThemeParts, + sd_parts: ThemeParts, } impl Theme { @@ -145,10 +207,7 @@ impl Theme { pub fn new() -> Self { - Theme { - land_texture: None, - border_texture: None, - } + Default::default() } pub fn load(path: &Path) -> Result { diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/AmmoMenu/Ammos_ExtraDamage_comma.png Binary file share/hedgewars/Data/Graphics/AmmoMenu/Ammos_ExtraDamage_comma.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/AmmoMenu/Ammos_base.png Binary file share/hedgewars/Data/Graphics/AmmoMenu/Ammos_base.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/AmmoMenu/Ammos_bw_ExtraDamage_comma.png Binary file share/hedgewars/Data/Graphics/AmmoMenu/Ammos_bw_ExtraDamage_comma.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/AmmoMenu/Ammos_bw_base.png Binary file share/hedgewars/Data/Graphics/AmmoMenu/Ammos_bw_base.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/AmmoMenu/TurnsLeft.png Binary file share/hedgewars/Data/Graphics/AmmoMenu/TurnsLeft.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/cm_anarcho_capitalism.png Binary file share/hedgewars/Data/Graphics/Flags/cm_anarcho_capitalism.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/cm_anarcho_communism.png Binary file share/hedgewars/Data/Graphics/Flags/cm_anarcho_communism.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/cm_anarcho_individualism.png Binary file share/hedgewars/Data/Graphics/Flags/cm_anarcho_individualism.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/cm_anarcho_primitivism.png Binary file share/hedgewars/Data/Graphics/Flags/cm_anarcho_primitivism.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/cm_black.png Binary file share/hedgewars/Data/Graphics/Flags/cm_black.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/montenegro.png Binary file share/hedgewars/Data/Graphics/Flags/montenegro.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Flags/serbia.png Binary file share/hedgewars/Data/Graphics/Flags/serbia.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Graves/Mushroom.png Binary file share/hedgewars/Data/Graphics/Graves/Mushroom.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Graves/Mushroom.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Graves/Mushroom.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Graves/Teapot.png Binary file share/hedgewars/Data/Graphics/Graves/Teapot.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Graves/Teapot.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Graves/Teapot.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/Dauber.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/Dauber.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1634 @@ + + + + + dauber + + + + + + + + + + image/svg+xml + + dauber + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat dauber + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/DayAndNight.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/DayAndNight.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1218 @@ + + + + + star and moon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + star and moon + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat star and moon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 4 + 3 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/Dragon.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/Dragon.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,8195 @@ + + + + + dragon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + dragon + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat dragon and laminaria hat + + + + A moustache without a dragon +is the same as a moustache with a dragon, +only without a dragon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + +  2 + + + +   3 + + + +    4  + + + +     5  + + + +      6  + + + +       7  + + + +        8  + + + +         9  + + + +          10  + + + +           11  + + + +            12  + + + +             13  + + + +              14  + + + +               15  + + + +                16  + + + +                 17   + + + +                  18  + + + +                   19 + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/Pantsu.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/Pantsu.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,367 @@ + + + + + pantsu hat + + + + + + + + + + + + + image/svg+xml + + pantsu hat + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat pantsu hat + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/Plunger.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/Plunger.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,400 @@ + + + + + plunger + + + + + + image/svg+xml + + plunger + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat plunger + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/ShaggyYeti.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/ShaggyYeti.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,13216 @@ + + + + + shaggy yeti + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + shaggy yeti + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat shaggy yeti and chicken + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/Sleepwalker.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/Sleepwalker.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,895 @@ + + + + + sleep walker + + + + + + + + + + + + image/svg+xml + + sleep walker + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat sleep walker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/SunWukong.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/SunWukong.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1167 @@ + + + + + Sun Wukong monkey king + + + + + + + + + + + + + + + + + image/svg+xml + + Sun Wukong monkey king + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat Sun Wukong monkey king + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/Zombi.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/Zombi.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2560 @@ + + + + + zombie + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + zombie + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat zombie + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + repeat + + + + + + + + + + + repeat + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/bubble.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/bubble.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2450 @@ + + + + + bubble + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + bubble + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat bubble + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/car.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/car.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,4792 @@ + + + + + car + + + + + + + + + + image/svg+xml + + car + + + hedgewars hat car + + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 01 + + + + + +  02 + + + + + +   03 + + + + + +    04 + + + + + +     05 + + + + + +      06 + + + + + +       07 + + + + + +        08 + + + + + +         09 + + + + + +          10 + + + + + +           11 + + + + + +            12 + + + + + +             13 + + + + + +              14 + + + + + +               15 + + + + + +                16 + + + + + +                 17 + + + + + +                  18 + + + + + +                   19 + + + + + + + + + + + + + + + + + + + + + + + + + current + + + + + + + + + + + + + + + + + + + + + + previous + + + + + + + + + + + + + + + + + + + + + + next + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/dish_Ladle.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/dish_Ladle.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1011 @@ + + + + + ladle + + + + + + + + + + image/svg+xml + + ladle + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat ladle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/dish_SauceBoatTemplate.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/dish_SauceBoatTemplate.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,791 @@ + + + + + sauce boat + + + + + + image/svg+xml + + sauce boat + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat sauce boat + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/dish_Teacup.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/dish_Teacup.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,416 @@ + + + + + teacup + + + + + + image/svg+xml + + teacup + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat teacup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/dish_Teapot.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/dish_Teapot.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2144 @@ + + + + + teapot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + teapot + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat teapot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/lamp.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/lamp.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2010 @@ + + + + + incandescent light bulb (lamp) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + incandescent light bulb (lamp) + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat incandescent light bulb (lamp) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 11 + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/mechanicaltoy.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/mechanicaltoy.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,5451 @@ + + + + + mechanical toy with key + + + + + + + + + + + + + + + + + + + image/svg+xml + + mechanical toy with key + + + hedgewars hat mechanical toy with key + + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + + + + + + + + + + + + + + + + + + + 01 + + + + + + + + + + + + +  02 + + + + + + + + + + + + +   03 + + + + + + + + + + + + +    04 + + + + + + + + + + + + +     05 + + + + + + + + + + + + +      06 + + + + + + + + + + + + +       07 + + + + + + + + + + + + +        08 + + + + + + + + + + + + +         09 + + + + + + + + + + + + +          10 + + + + + + + + + + + + +           11 + + + + + + + + + + + + +            12 + + + + + + + + + + + + +             13 + + + + + + + + + + + + +              14 + + + + + + + + + + + + +               15 + + + + + + + + + + + + +                16 + + + + + + + + + + + + +                 17 + + + + + + + + + + + + +                  18 + + + + + + + + + + + + +                   19 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + current + + + + + + + + + + + + + + + + + + + + + + previous + + + + + + + + + + + + + + + + + + + + + + next + + + + + + + + + (let ((pi 3.1415926) (start-width 13.019)) (do ((i 1 (+ i 1))) ((> i 19)) (format #t "~d\t~1,2f\n" i (* start-width (cos (* i (/ pi 19))))))) + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/noface.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/noface.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1627 @@ + + + + + No Face + + + + + + + + + + + + + + + image/svg+xml + + No Face + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat No Face + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/scif_BrainSlugTemplate.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/scif_BrainSlugTemplate.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1782 @@ + + + + + brain slug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + brain slug + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat brain slug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/scif_cosmonaut.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/scif_cosmonaut.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2037 @@ + + + + + cosmonaut + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + cosmonaut + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat cosmonaut + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + clones disconnected from parent object :( for illuminator + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_Pig.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/zoo_Pig.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,878 @@ + + + + + pig + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + pig + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat pig + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_elephant.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/zoo_elephant.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2022 @@ + + + + + Ganesha elephant + + + + + + + + + + + + + + + image/svg+xml + + Ganesha elephant + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat Ganesha elephant + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_fish.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/zoo_fish.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,3995 @@ + + + + + fish + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + fish + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat fish + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_frog.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/zoo_frog.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1863 @@ + + + + + frog + + + + + + + + + + image/svg+xml + + frog + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat frog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_panda.png Binary file share/hedgewars/Data/Graphics/Hats/zoo_panda.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_snail.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/zoo_snail.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,2321 @@ + + + + + snail + + + + + + + + + + + + + image/svg+xml + + snail + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat snail + + + this is definitely not "Leucochloridium paradoxum" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hats/zoo_turtle.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Graphics/Hats/zoo_turtle.svg Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1790 @@ + + + + + turtle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + turtle + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + Roman V. Prikhodchenko (chujoii@gmail.com) + + + + + hedgewars hat turtle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hedgehog/Bubble.png Binary file share/hedgewars/Data/Graphics/Hedgehog/Bubble.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Hedgehog/Happy.png Binary file share/hedgewars/Data/Graphics/Hedgehog/Happy.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/Missions/Scenario/Big_Armory@2x.png Binary file share/hedgewars/Data/Graphics/Missions/Scenario/Big_Armory@2x.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/dynamiteDefused.png Binary file share/hedgewars/Data/Graphics/dynamiteDefused.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Graphics/slider.png diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/campaigns_de.txt --- a/share/hedgewars/Data/Locale/campaigns_de.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/campaigns_de.txt Sun Mar 24 14:33:57 2024 -0400 @@ -54,7 +54,7 @@ A_Space_Adventure-desert02.desc="Unser Held suchte nach dem Teil in diesem Tunnel, als er unerwarteterweise anfing, geflutet zu werden! Komm so schnell wie möglich zur Oberfläche und pass auf, keine Mine auszulösen." A_Space_Adventure-desert03.name="Nebenmission: Präzisionsfliegen" -A_Space_Adventure-desert03.desc="Unser Held hat etwas Zeit, um mit Funkflugzeugen zu spielen und etwas Spaß zu haben. Flieg das Funkflugzeug und triff alle Ziele!" +A_Space_Adventure-desert03.desc="Unser Held hat etwas Zeit, um mit Funkflugzeugen zu spielen und etwas Spaß zu haben. Flieg das Funkflugzeug und triff alle Ziele! Wenn du es schaffst, erhältst du zusätzliche Munition für die Hauptmission." A_Space_Adventure-fruit01.name="Hauptmission: Schlechtes Timing" A_Space_Adventure-fruit01.desc="Auf dem Obstplaneten laufen die Dinge nicht so gut. Igel sammeln kein Obst, sondern sie bereiten sich auf den Kampf vor. Du musst dich entscheiden, ob du kämpfen oder fliehen wirst." @@ -69,7 +69,7 @@ A_Space_Adventure-death01.desc="Auf dem Todesplaneten, dem sterilsten Planeten in der Gegend, ist unser Held ganz kurz davor, das letzte Teil des Geräts zu holen! Allerdings erwartet ihn eine unangenehme Überraschung." A_Space_Adventure-death02.name="Nebenmission: Die Spezialisten töten" -A_Space_Adventure-death02.desc="Unser Held ist wieder in eine schwierige Situation geraten. Besiege die »5 tödlichen Igel« in ihrem eigenem Spiel!" +A_Space_Adventure-death02.desc="Unser Held ist wieder in eine schwierige Situation geraten. Besiege die »5 tödlichen Igel« in ihrem eigenem Spiel! Wenn du gewinnst, erhältst du ein paar Extras für die Hauptmission." A_Space_Adventure-final.name="Hauptmission: Der große Knall" A_Space_Adventure-final.desc="Unser Held muss ein paar Sprengkörper, die auf dem Meteoriten platziert wurden, detonieren. Beende diese Mission, ohne verletzt zu werden!" diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/campaigns_en.txt --- a/share/hedgewars/Data/Locale/campaigns_en.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/campaigns_en.txt Sun Mar 24 14:33:57 2024 -0400 @@ -46,7 +46,7 @@ A_Space_Adventure-desert02.name="Side Mission: Running for survival" A_Space_Adventure-desert02.desc="Our hero was searching for the part in this tunnel when it unexpectedly start getting flooded! Get to the surface as soon as possible and be careful not to trigger a mine." A_Space_Adventure-desert03.name="Side Mission: Precise flying" -A_Space_Adventure-desert03.desc="Our hero has some time to play with RC planes and have some fun. Fly the RC plane and hit all the targets!" +A_Space_Adventure-desert03.desc="Our hero has some time to play with RC planes and have some fun. Fly the RC plane and hit all the targets! If you win, you will gain some bonus ammo for the main mission." A_Space_Adventure-fruit01.name="Main Mission: Bad timing" A_Space_Adventure-fruit01.desc="On the fruit planet things aren't going so well. Hogs aren't collecting fruits but they are preparing for battle. You'll have to choose if you'll fight or if you'll flee." A_Space_Adventure-fruit02.name="Main Mission: Getting to the device" @@ -56,6 +56,6 @@ A_Space_Adventure-death01.name="Main Mission: The last encounter" A_Space_Adventure-death01.desc="On the Death Planet, the most infertile planet around, our hero is very close to get the last part of the device! However, an unpleasant surprise awaits ..." A_Space_Adventure-death02.name="Side Mission: Killing the specialists" -A_Space_Adventure-death02.desc="Again our hero has gotten in a difficult situation. Defeat the “5 Deadly Hogs“ in their own game!" +A_Space_Adventure-death02.desc="Again our hero has gotten in a difficult situation. Defeat the “5 Deadly Hogs“ in their own game! If you win, you will gain bonuses for the main mission." A_Space_Adventure-final.name="Main Mission: The big bang" A_Space_Adventure-final.desc="Our hero has to detonate some explosives that have been placed on the meteorite. Complete this mission without getting hurt!" diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/campaigns_zh_CN.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Locale/campaigns_zh_CN.txt Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,61 @@ +A_Classic_Fairytale.name="经典童话" + +A_Classic_Fairytale-first_blood.name="任务 1: 第一滴血" +A_Classic_Fairytale-first_blood.desc="帮助 Leaks a Lot 完成他的训练,成为一个合格的刺猬战士。你将训练使用绳索、降落伞、升龙拳和沙漠之鹰" + +A_Classic_Fairytale-shadow.name="任务 2: 暗影降临" +A_Classic_Fairytale-shadow.desc="Leaks a Lot 和 Dense Cloud 外出打猎。准备好应对在森林等待你的危险。记住,做出明智的选择" + +A_Classic_Fairytale-journey.name="任务 3: 归途" +A_Classic_Fairytale-journey.desc="Leaks a Lot 必须去岛屿的另一边。要快且谨慎." + +A_Classic_Fairytale-united.name="任务 4: 团结则存" +A_Classic_Fairytale-united.desc="经过漫长的旅途,Leaks a Lot 终于回到村庄。然而,没有时间休息。你必须从食人族的愤怒中保卫村庄" + +A_Classic_Fairytale-backstab.name="任务 5: 背刺" +A_Classic_Fairytale-backstab.desc="可怕的食人族正在追杀 Leaks a Lot 和他的朋友们。再次打败他们,保护你的盟友。 相应地使用你的资源打败来袭的敌人!" + +A_Classic_Fairytale-dragon.name="任务 6: 龙的巢穴" +A_Classic_Fairytale-dragon.desc="我们的英雄必须到湖的另一边。成为绳索大师,避免被敌人击中" + +A_Classic_Fairytale-family.name="任务 7: 家人团聚" +A_Classic_Fairytale-family.desc="我们的英雄必须再次拯救部落。消灭敌人,解救战友。小心使用你的资源,因为它们是有限的。在正确的位置钻一些洞并靠近公主。" + +A_Classic_Fairytale-queen.name="任务 8: 女王万岁" +A_Classic_Fairytale-queen.desc="部落必须再次战斗。为了胜利,他们必须和叛徒战斗,并使用所有可用的资源。打败敌人!" + +A_Classic_Fairytale-enemy.name="任务 9: 敌人的敌人" +A_Classic_Fairytale-enemy.desc="多棒的转折!Leaks a Lot 必须和食人族并肩作战,对抗共同的敌人:邪恶机器人!" + +A_Classic_Fairytale-epil.name="任务 10: 后记" +A_Classic_Fairytale-epil.desc="恭喜!Leaks a Lot 终于能在和平中离开,并受到新朋友和部落的赞扬。为你的成功感到自豪!你可以重玩之前的任务,并查看其他可能的结局" + +A_Space_Adventure.name="太空冒险" +A_Space_Adventure-cosmos.name="菜单: 太空旅行" +A_Space_Adventure-cosmos.desc="Hogera,刺猬的星球,快被巨大的陨石击中. 在这场求生的竞赛中,你必须带领受刺猬星球协会(PAotH)委托的勇敢刺猬,在邻星周围太空旅行,收集所有四个失落已久的反重力设备部件" +A_Space_Adventure-moon01.name="主要任务: 第一站" +A_Space_Adventure-moon01.desc="我们的英雄要在月球着陆,为飞碟加满燃料,但 Hogevil 教授先到那里并设好埋伏!救出 PAotH 研究员并赶走 Hogevil 教授!" +A_Space_Adventure-moon02.name="支线任务: 追逐蓝刺猬" +A_Space_Adventure-moon02.desc="我们的英雄拜访了一位隐士,住在月球上的 PAotH 老兵。然而,为了收集一些关于 Hogevil 教授的情报,你必须先在追逐游戏中打败隐士 Crazy Runner!" +A_Space_Adventure-ice01.name="主要任务: 冰冻冒险" +A_Space_Adventure-ice01.desc="欢迎来到冰雪星球,这里太冷了导致大多数武器不能用。你必须使用在那里找到的武器,从强盗首领 Thanta 手中得到丢失的部件!" +A_Space_Adventure-ice02.name="支线任务: 艰难飞行" +A_Space_Adventure-ice02.desc="我们的英雄来到冰雪星球,不可能不参观飞碟的奥林匹克体育场。在这个任务,你可以证明自己飞行技能,并宣称你是最棒的!" +A_Space_Adventure-desert01.name="主要任务: 尘中搜索" +A_Space_Adventure-desert01.desc="你在沙漠星球着陆!我们的英雄必须在地下隧道找到丢失的部件。请小心,恶毒的走私者在等着,攻击并抢劫你!" +A_Space_Adventure-desert02.name="支线任务: 逃命" +A_Space_Adventure-desert02.desc="我们的英雄在隧道中搜索部件时,隧道意外开始被淹没!尽快回到地面,请小心不要触发地雷。" +A_Space_Adventure-desert03.name="支线任务: 精确飞行" +A_Space_Adventure-desert03.desc="我们的英雄有时喜欢玩遥控飞机。控制遥控飞机,击中所有目标!如果你赢了会在主要任务得到奖励" +A_Space_Adventure-fruit01.name="主要任务: 坏时机" +A_Space_Adventure-fruit01.desc="在水果星球上,事情进行得不那么顺利。刺猬们没在收集水果,而是准备战斗。你必须选择打或跑。" +A_Space_Adventure-fruit02.name="主要任务: 取得设备" +A_Space_Adventure-fruit02.desc="我们的英雄在水果星球上接近了丢失的部件,Captain Lime 会帮你获得这个部件吗?" +A_Space_Adventure-fruit03.name="主要任务: 精确射击" +A_Space_Adventure-fruit03.desc="我们的英雄在红色草莓中迷路并被埋伏。消灭他们并为“取得设备”任务赢得额外子弹。" +A_Space_Adventure-death01.name="主要任务: 最后的遭遇战" +A_Space_Adventure-death01.desc="在死亡星球,周围最贫瘠的星球,我们的英雄非常接近最后的部件。然而,一个不愉快的惊喜在等着……" +A_Space_Adventure-death02.name="支线任务: 杀死专家" +A_Space_Adventure-death02.desc="我们的英雄再次陷入困境。在“致命五刺猬”自己的游戏中打败他们!如果你赢了会在主要任务得到奖励" +A_Space_Adventure-final.name="主要任务: 大爆炸" +A_Space_Adventure-final.desc="我们的英雄必须引爆一些放置在陨石上的爆炸物。无伤完成这个任务!" diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/cs.lua --- a/share/hedgewars/Data/Locale/cs.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/cs.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/cs.txt --- a/share/hedgewars/Data/Locale/cs.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/cs.txt Sun Mar 24 14:33:57 2024 -0400 @@ -304,7 +304,7 @@ 02:07=Ou, tahle bedna je těžká 02:07=Možná bys mohl potřebovat tohle -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 je nudný... 02:08=%1 se nemusel obtěžovat 02:08=%1 je líný ježek @@ -342,7 +342,7 @@ 02:08=%1 je slušně vystrašený 02:08=%1 usnul -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 by měl trénovat míření! 02:09=%1 se asi nenávidí 02:09=%1 je na špatné straně! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/da.lua --- a/share/hedgewars/Data/Locale/da.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/da.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/da.txt --- a/share/hedgewars/Data/Locale/da.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/da.txt Sun Mar 24 14:33:57 2024 -0400 @@ -304,7 +304,7 @@ 02:07=Åh, den her er tung 02:07=Måske får du brug for den her -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 er sååå kedelig... 02:08=%1 gad ikke lige 02:08=%1 er et dovent pindsvin @@ -342,7 +342,7 @@ 02:08=%1 er stiv af skræk 02:08=%1 er vist faldet i søvn -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 burde øve sig i at sigte! 02:09=%1 hader vist sig selv 02:09=%1 står på den forkerte side! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/de.lua --- a/share/hedgewars/Data/Locale/de.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/de.lua Sun Mar 24 14:33:57 2024 -0400 @@ -499,8 +499,8 @@ ["Destroy him, Leaks A Lot! He is responsible for the deaths of many of us!"]="Zerstöre ihn, Undichte Stelle! Er ist verantwortlich für viele Tote auf unserer Seite!", ["Destroy invaders and collect bonuses to score points."] = "Zerstöre Invasoren und sammle Boni auf, um zu punkten.", -- Space_Invasion ["- Destroy the enemy"] = "- Vernichte den Feind", -- HedgeEditor +["- Destroy the red targets"] = "- Zerstöre die roten Ziele", -- HedgeEditor ["- Destroy the red target"] = "- Zerstöre das rote Ziel", -- HedgeEditor -["- Destroy the red targets"] = "- Zerstöre die roten Ziele", -- HedgeEditor ["Destroy the targets!"] = "Zerstöre die Zielscheiben!", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade ["+%d flamer fuel!"] = "+%d Flammenwerfertreibstoff", -- Tumbler ["%d-Hit Combo! +%d points!"] = "%d-Treffer-Kombi! +%d Punkte!", -- Space_Invasion @@ -692,6 +692,7 @@ ["Flawless victory!"]="Perfekter Sieg!", ["Flee: Press [Jump]"] = "Fliehen: Drücke [Springen]", -- A_Space_Adventure:fruit01 ["Flesh for Brainz"]="Fleisch gegen Hirn", +["Flower Power"] = "Flower-Power", -- Basic_Training_-_Rope ["Fly around and hurl explosives to your enemies."] = "Flieg herum und wirf Sprengkörper auf deine Gegner.", -- Tumbler ["Flying Saucer Training"] = "Grundausbildung: Fliegende Untertasse", -- Basic_Training_-_Flying_Saucer ["Fly into space to fight off the invaders with barrels!"] = "Flieg in den Weltraum, um die Invasoren mit Fässern abzuwehren!", -- Space_Invasion @@ -699,6 +700,7 @@ ["Fly to the moon"]="Flieg zum Mond.", ["Fly to the moon."] = "Flieg zum Mond.", -- A_Space_Adventure:cosmos ["Follow the path and destroy the next target."] = "Folge dem Pfad und zerstöre die nächste Zielscheibe.", -- Basic_Training_-_Rope +["For each kill you win %d seconds."] = "Für jeden toten Igel erhältst du %d Sekunden.", -- RopeKnocking ["Forgetfulness: You will lose all your weapons each turn."] = "Vergesslichkeit: Du wirst jeden Zug alle Waffen verlieren.", -- Continental_supplies ["For the next crate, you have to do back jumps."] = "Für die nächste Kiste brauchst du Rückwärtssprünge.", -- Basic_Training_-_Movement ["Four Eyes"] = "Vier Augen", -- @@ -1097,7 +1099,7 @@ ["I love you."] = "Ich liebe dich.", -- A_Classic_Fairytale:epil ["I'm afraid I can't let you proceed!"] = "Ich fürchte, ich kann euch nicht weitergehen lassen.", -- A_Classic_Fairytale:queen ["I'm afraid we cannot afford that."] = "Ich fürchte, wir können uns das nicht leisten.", -- A_Classic_Fairytale:queen -["Imagine those targets are the wolves that killed your parents! Take your anger out on them!"]="Stell dir vor, diese Zielscheiben sind die Wölfe, die eine Eltern getötet haben! Lass deine Wut an ihnen aus!", +["Imagine those targets are the wolves that killed your parents! Take your anger out on them!"]="Stell dir vor, diese Zielscheiben sind die Wölfe, die deine Eltern getötet haben! Lass deine Wut an ihnen aus!", ["I'm...alive? How? Why?"]="Ich lebe? Wie? Warum?", ["I'm a ninja."]="Ich bin ein Ninja.", ["I marked the place of their arrival. You're welcome!"]="Ich habe ihren Ankunftsort markiert. Gern geschehen!", @@ -1545,6 +1547,7 @@ ["One tribe was peaceful, spending their time hunting and training, enjoying the small pleasures of life..."]="Ein Stamm war friedlich und verbrachte die Zeit mit der Jagd, Übungen und den kleinen Freuden des Lebens.", ["Oneye"] = "Einauge", -- portal ["Only one hog per team allowed! Excess hogs will be removed."] = "Nur ein Igel pro Team erlaubt! Überschüssige Igel werden entfernt.", -- Mutant +["Only one team per clan allowed! Excess teams will be removed."] = "Nur ein Team pro Klan erlaubt! Überschüssige Teams werden entfernt.", -- Mutant ["Only %s can be trusted with the crate."] = "Die Kiste kann nur %s anvertraut werden.", -- A_Space_Adventure:fruit02 ["Only the best pilots can master the following stunts."] = "Nur die besten Piloten können die folgenden Stunts meistern.", -- Basic_Training_-_Flying_Saucer ["Only two clans allowed! Excess hedgehogs will be removed."] = "Nur zwei Klans erlaubt! Überschüssige Igel werden entfernt.", -- CTF_Blizzard @@ -2485,8 +2488,8 @@ ["Use it wisely!"]="Benutze sie weise!", ["Use it with precaution!"]="Benutze sie weise.", ["User Challenge"]="Benutzerherausforderung", +["User Mission"] = "Benutzermission", -- HedgeEditor ["!"] = "!", -- User_Mission_-_Dangerous_Ducklings -["User Mission"] = "Benutzermission", -- HedgeEditor ["Use space button twice to change flying saucer while being in air."]="Drücke die Angriffstaste 2 mal, um die fliegende Untertasse im Flug zu wechseln", ["Use space button twice to change flying saucer while floating in mid-air."]="Drücke die Angriffstaste 2 mal, um die fliegende Untertasse im Flug zu wechseln.", ["Use the attack key twice to change the flying saucer while being in air."] = "Benutze die Angriffstaste 2 mal, um die fliegende Untertasse in der Luft zu wechseln.", -- A_Space_Adventure:ice02 @@ -2838,6 +2841,7 @@ ["You have kidnapped our whole tribe!"]="Ihr habt unseren ganzen Stamm entführt!", ["You have killed an innocent hedgehog!"]="Du hast einen unschuldigen Igel getötet!", ["You have killed %d of 16 hedgehogs (+%d points)."] = "Du hast %d von 16 Igeln getötet (+%d Punkte).", -- User_Mission_-_Rope_Knock_Challenge +["You have killed %d of %d hedgehogs (+%d points)."] = "Du hast %d von %d Igeln getötet (+%d Punkte).", -- RopeKnocking ["You have launched %d bazookas."]="Du hast %d Bazookas abgefeuert.", ["You have launched %d homing bees."]="Du hast %d zielsuchende Bienen abgefeuert.", ["You have made %d shots."]="Du hast %d Schüsse abgegeben.", diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/de.txt --- a/share/hedgewars/Data/Locale/de.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/de.txt Sun Mar 24 14:33:57 2024 -0400 @@ -61,6 +61,7 @@ 00:58=Luftmine 00:59=Creeper 00:60=Minigun +00:61=Wachroboter 01:00=Laden … 01:01=Unentschieden @@ -111,6 +112,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 @@ -269,7 +271,7 @@ 02:01=%1 wird für immer Blasen machen 02:01=%1 war ganz, ganz knapp vor einem Floß 02:01=%1 dachte, Salzwasser sei gut für die Haut -02:01=%1 bekommt Salzwasser in seine Wunden +02:01=%1 bekommt Salzwasser in die Wunden 02:01=%1 ging über die Planke 02:01=%1 geht baden 02:01=%1 ist nass, nass, nass @@ -483,7 +485,7 @@ 02:05=Notvorräte! 02:05=Hartnäckige Igel sind gute Igel 02:05=Vergiss nicht deine Vitamine! -02:05=Jemand hat wohl seine Pillen vergessen +02:05=Jemand hat wohl die Pillen vergessen 02:05=Wir denken an euch! 02:05=Hilfe für die Verletzten 02:05=Lasst uns deine Hartnäckigkeit erhöhen @@ -753,7 +755,7 @@ 02:07=Direkt aus dem Baumarkt 02:07=Erwecke den Erfinder in dir -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 ist so ein Langeweiler … 02:08=%1 will nicht gestört werden 02:08=%1 denkt weiter nach … @@ -823,7 +825,7 @@ 02:08=%1 will nicht gestört werden -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 sollte lieber zielen üben! 02:09=%1 scheint sich zu hassen 02:09=%1 steht auf der falschen Seite! @@ -844,7 +846,7 @@ 02:09=%1 verletzt sich selbst 02:09=%1 blamiert sich! 02:09=%1 ist ungeschickt -02:09=%1 zeigt dem Feind, wozu er fähig ist +02:09=%1 zeigt dem Feind, wo der Hammer hängt 02:09=%1 ist nicht perfekt 02:09=Mach dir keine Sorgen, %1, piemand ist nerfekt 02:09=%1 hat das bestimmt mit Absicht getan @@ -931,7 +933,7 @@ ; Hog (%1) has to leave (team is gone) 02:11=%1 muss ins Bett! 02:11=%1 scheint zu beschäftigt zu sein -02:11=Beam ihn hoch, Scotty! +02:11=Beam sie hoch, Scotty! 02:11=%1 muss weg 02:11=%1 verkrümelt sich 02:11=%1 macht sich vom Acker @@ -1302,6 +1304,7 @@ 03:58=Schwebende Annäherungsmine 03:59=Unfertige Waffe 03:60=Die ultimative Feuerwaffe +03:61=Unfertige Waffe ; Weapon Descriptions (use | as line breaks) 04:00=Greife deine Feinde mit einfachen Granaten an.|Der Zeitzünder steuert den Explosionszeitpunkt.|1–5: Zeitzünder einstellen|Genaues Zielen + 1-5: Sprungkraft einstellen|Angriff: Halten, um mit mehr Kraft zu werfen @@ -1363,8 +1366,9 @@ 04:56=Du kannst zwei Hackebeile auf deinen Feind schleudern,|Passagen und Tunnel blockieren, und sie sogar zum Klettern|benutzen! Der Schaden erhöht sich mit der Geschwindigkeit.|Aber sei vorsichtig! Es ist gefährlich, mit Messern zu spielen.|Angriff: Gedrückt halten, um mit mehr Schwung zu werfen (zwei mal) 04:57=Bau einen SEHR elastischen Balken aus Gummi,|von dem Igel und andere Sachen abprallen,|ohne Fallschaden zu nehmen.|Links/Rechts: Ausrichtung des Gummis wählen|Cursor: Gummi platzieren 04:58=Diese Annäherungsmine wird frei in der Luft schweben und|verfolgt törichte Igel, die dumm genug sind, ihr zu nahe zu|kommen. Allerdings ist ihre Explosion schwächer als|die der Landmine.|Angriff: Halten, um mit mehr Kraft zu werfen -04:59=Diese Waffe ist unfertig und experimentell.|Benutze sie auf eigene Gefahr! +04:59=Diese Waffe ist unfertig und experimentell.|Benutze sie auf eigene Gefahr!|Angriff: Einsetzen 04:60=Lass es Kugeln auf deine Gegner hageln!|Und sie dachten wirklich, sie seien hinter|drei Trägerschichten sicher.|Angriff: Mit voller Kraft feuern|Hoch/Runter: Weiterzielen +04:61=Diese Waffe ist unfertig und experimentell.|Benutze sie auf eigene Gefahr!|Angriff: Roboter platzieren ; Game goal strings 05:00=Spielmodifikationen @@ -1421,3 +1425,6 @@ 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! +06:29=/bubble: Igel die Luft anhalten lassen +06:30=/happy: Igel glücklich aussehen lassen +06:31=/sad: Igel unglücklich aussehen lassen diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/el.txt --- a/share/hedgewars/Data/Locale/el.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/el.txt Sun Mar 24 14:33:57 2024 -0400 @@ -286,7 +286,7 @@ 02:07=Ωωωχ! Αυτό το κουτί είναι βαρύ! 02:07=Μπορεί να σου χρησιμεύσει σε κάτι! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=Ο %1 είναι τόσο βαρετός... 02:08=Ο %1 βαριέται ασυστόλως! 02:08=Ο %1 είναι ένας τεμπέλης σκαντζόχοιρος! @@ -326,7 +326,7 @@ 02:08=Ο %1 έχει κλάσει μέντες! 02:08=Ο %1 αποκοιμήθηκε! -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=Ο %1 πρέπει να εξασκηθεί στην σκοποβολή! 02:09=Ο %1 φαίνεται να έχει αυτοκτονικές τάσεις! 02:09=Ο %1 πολεμάει στην λάθος πλευρά! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/en.txt --- a/share/hedgewars/Data/Locale/en.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/en.txt Sun Mar 24 14:33:57 2024 -0400 @@ -63,6 +63,7 @@ 00:58=Air Mine 00:59=Creeper 00:60=Minigun +00:61=Sentry Bot ; Game messages and HUD texts 01:00=Loading … @@ -121,6 +122,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) @@ -129,24 +131,24 @@ 02:00=%1 never saw that coming! 02:00=%1 waves goodbye! 02:00=%1 has gone to a better place! -02:00=%1 meets his maker! +02:00=%1 meets their maker! 02:00=%1 can hang on no longer! -02:00=%1 has done his duty! +02:00=%1 has done their duty! 02:00=%1 makes the ultimate sacrifice! 02:00=%1 departs this mortal coil! 02:00=%1 makes like a tree and leaves! 02:00=%1 has timed out! 02:00=%1 says peace out! 02:00=%1 will be fondly remembered! -02:00=%1 leaves behind a wife and child -02:00=%1 has launched his last bazooka -02:00=%1 has tossed his last grenade -02:00=%1 has baked his last cake -02:00=%1 has swung on his last rope -02:00=%1 has called his last airstrike -02:00=%1 has pumped his last shotgun -02:00=%1 has thrown his last melon -02:00=%1 has drawn his last deagle +02:00=%1 leaves behind a sad family +02:00=%1 has launched their last bazooka +02:00=%1 has tossed their last grenade +02:00=%1 has baked their last cake +02:00=%1 has swung on their last rope +02:00=%1 has called their last airstrike +02:00=%1 has pumped their last shotgun +02:00=%1 has thrown their last melon +02:00=%1 has drawn their last deagle 02:00=%1 took one shot too many 02:00=%1 could really have used a health crate 02:00=%1 has gone to play a better game @@ -154,9 +156,9 @@ 02:00=%1 fails 02:00=Poor, poor %1 ... 02:00=%1 prefers WarMUX -02:00=%1 has been blocking shots with his face +02:00=%1 has been blocking shots with their face 02:00=%1 is a hero amongst me...err...hogs -02:00=%1 finds his place in Valhalla +02:00=%1 finds their place in Valhalla 02:00=%1 has left the building 02:00=%1 goes the way of the dinosaurs 02:00=%1 brings hedgehogs one step closer to extinction @@ -230,22 +232,22 @@ 02:01=%1 checks out the deep end 02:01=%1 goes glug glug glug 02:01=%1 goes splash -02:01=%1 forgot his armbands +02:01=%1 forgot their armbands 02:01=%1 really should have taken swimming lessons -02:01=%1 left his surfboard at home +02:01=%1 left their surfboard at home 02:01=%1 is washed up 02:01=%1 is one soggy hog -02:01=%1 forgot to bring his life jacket +02:01=%1 forgot to bring their life jacket 02:01=%1 goes splish splash splish 02:01=%1 is sleeping with the fishes 02:01=%1 thinks the water physics suck in this game 02:01=%1 looks thirsty 02:01=The sea claims %1 02:01=%1 is lost at sea -02:01=%1 should have brought his scuba gear +02:01=%1 should have brought their scuba gear 02:01=%1 gets a burial at sea 02:01=%1 has that sinking feeling -02:01=%1 is practicing his backstroke +02:01=%1 is practicing their backstroke 02:01=%1 goes in search of the Titanic 02:01=%1 is not Jesus 02:01=%1 is finding Nemo @@ -253,7 +255,7 @@ 02:01=You've gotta wonder how many hogs are down there 02:01=%1 makes the ocean slightly higher 02:01=%1 didn't enlist in the Navy -02:01=%1 is doing his impersonation of a dead fish +02:01=%1 is doing their impersonation of a dead fish 02:01=At least you didn't go down the toilet, %1 02:01=Sonic couldn't swim and neither can %1 02:01=%1 wants to play Ecco the dolphin @@ -265,11 +267,11 @@ 02:01=%1 is forever blowing bubbles 02:01=%1 is short of a raft 02:01=%1 thinks salt water is good for the skin -02:01=%1 gets salt water in his wounds +02:01=%1 gets salt water in their wounds 02:01=%1 has walked the plank 02:01=%1 has a bath 02:01=%1 is wet wet wet -02:01=%1 gets his quills wet +02:01=%1 gets their quills wet 02:01=It's Davy Jones' locker for %1 02:01=%1 explores the Sea 02:01=Hedgehogs have quills, not gills, %1! @@ -466,7 +468,7 @@ 02:05=Emergency supplies! 02:05=Strong hedgehogs are good hedgehogs 02:05=Don't forget to take your vitamins! -02:05=Somebody must have forgotten his daily pills +02:05=Somebody must have forgotten their daily pills 02:05=Adds to your total health 02:05=We got you covered! 02:05=Hurt hedgehogs have help @@ -745,7 +747,7 @@ 02:07=Collect all the tools! 02:07=Tools! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 is sooo boring ... 02:08=%1 couldn't be bothered 02:08=%1 is one lazy hog @@ -759,20 +761,20 @@ 02:08=%1 has a breather 02:08=%1 has a rest 02:08=%1 chills out -02:08=%1 has no faith in his own abilities +02:08=%1 has no faith in their own abilities 02:08=%1 decides to do nothing at all 02:08=%1 lets the enemy destroy itself 02:08=%1 would be terrible at parties 02:08=%1 hides out 02:08=%1 has decided to pass on this opportunity -02:08=%1 decides the best thing he can do is...nothing +02:08=%1 decides the best thing to do is...nothing 02:08=%1 is a big wuss 02:08=Buck Buck Buck, %1 is a chicken 02:08=%1 is looking a little yellow 02:08=%1 is a coward! 02:08=%1 is waiting for sudden death 02:08=%1 is not the fighting type -02:08=%1 is reconsidering his purpose in life +02:08=%1 is reconsidering their purpose in life 02:08=%1 was never much of a good shot anyway 02:08=%1 didn't want to join the army in the first place 02:08=Stop wasting our time, %1 @@ -800,12 +802,12 @@ 02:08=%1 attacked with nothing 02:08=%1 is passive -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 should practice aiming! -02:09=%1 seems to hate himself +02:09=%1 seems to hate themselves 02:09=%1 is standing on the wrong side! 02:09=%1 makes like an emo -02:09=%1 was holding his weapon the wrong way around +02:09=%1 was holding their weapon the wrong way around 02:09=%1 is a little sadistic 02:09=%1 is a masochist 02:09=%1 has no instinct of self-preservation @@ -823,17 +825,17 @@ 02:09=%1 lives by the mantra of "no pain, no gain" 02:09=%1 is confused 02:09=%1 hurts itself in its confusion -02:09=%1 has a knack for embarrassing himself +02:09=%1 has a knack for embarrassing themselves 02:09=%1 is a klutz! 02:09=%1 is clumsy -02:09=%1 shows the enemy what he's capable of +02:09=%1 shows the enemy what they're capable of 02:09=%1 can't be expected to be perfect all the time 02:09=Don't worry %1, pobody's nerfect 02:09=%1 totally did that on purpose 02:09=I won't tell anyone if you don't, %1 02:09=How embarrassing! 02:09=I'm sure nobody saw that, %1 -02:09=%1 needs to review his field manual +02:09=%1 needs to review their field manual 02:09=%1's weapon clearly malfunctioned 02:09=%1 is still practicing 02:09=%1's shot backfired @@ -850,7 +852,7 @@ 02:09=%1 confuses the enemy 02:09=Something is wrong with %1 02:09=Better luck next time, %1! -02:09=%1 is his worst enemy +02:09=%1 is their worst enemy 02:09=Let's call that a technical difficulty, %1? 02:09=%1 is drunk 02:09=%1 says “Ouchywawa!” @@ -894,7 +896,7 @@ ; Hog (%1) has to leave (team is gone) 02:11=%1 has to go to bed! 02:11=%1 seems too busy to play -02:11=Beam him up, Scotty! +02:11=Beam 'em up, Scotty! 02:11=%1 has to go 02:11=%1 teleports into a parallel universe 02:11=%1 vanishes into thin air @@ -1214,6 +1216,7 @@ 03:58=Floating proximity bomb 03:59=Incomplete weapon 03:60=The ultimate firearm +03:61=Incomplete weapon ; Weapon descriptions (use | as line breaks) 04:00=Attack your enemies using a simple grenade.|It will explode once its timer reaches zero.|1-5: Set grenade's timer|Precise + 1-5: Set bounce strength|Attack: Hold to throw with more power @@ -1239,8 +1242,8 @@ 04:20=Allows you to play the current turn with|a different hog.|Attack: Enable switching hogs|Switch: Select next hog|Precise + Switch: Select previous hog 04:21=Shoot a projectile that will release|multiple clusters upon impact. The|clusters are hurled backwards and are|more dangerous than the main projectile.|Attack: Shoot at full power 04:22=Not just for Indiana Jones! The whip is a|useful weapon in many situations. Especially|when you'd like to shove someone off a cliff.|Attack: Strike everything in front of you -04:23=If you have nothing to lose, this might be|quite handy. Sacrifice your hog by launching|it into a specific direction hurting everything|on his way and exploding at the end.|Attack: Launch the devastating and deadly attack -04:24=Happy Birthday! Launch this cake, let it walk right|next to your enemies and let them have an explosive|party. The cake is able to pass almost all terrain|but he might detonate earlier this way.|Attack: Start the cake or let it stop and explode +04:23=If you have nothing to lose, this might be|quite handy. Sacrifice your hog by launching|it into a specific direction hurting everything|on the way and exploding at the end.|Attack: Launch the devastating and deadly attack +04:24=Happy Birthday! Launch this cake, let it walk right|next to your enemies and let them have an explosive|party. The cake is able to pass almost all terrain|but it might detonate earlier this way.|Attack: Start the cake or let it stop and explode 04:25=With this costume kit your hog becomes irresistibly|attractive and makes nearby hogs jump in blind love|towards it (and into some gap or hole). The seduction is|so heartwarming, it even breaks the ice of frozen hogs.|Attack: Use the irresistible seduction 04:26=Throw this juicy (and bouncy) watermelon at|your enemies. Once the timer expires, it will|split into several explosive pieces.|1-5: Set watermelon's timer|Attack: Hold to shoot with more power 04:27=Let hellfire rain onto your opponents by using|this fiendish explosive. Don't get too close to|the explosion as smaller fires might last longer.|Attack: Hold to shoot with more power @@ -1275,8 +1278,9 @@ 04:56=You can throw two cleavers at your enemy, block|passages and tunnels and even use them for|climbing! Its damage increases with its speed.|But be careful! Playing with knives is dangerous.|Attack: Hold to shoot with more power (twice) 04:57=Build a VERY elastic rubber band, from which|hedgehogs and other things bounce off|without taking fall damage.|Left/Right: Change rubber band orientation|Cursor: Place rubber band in a valid position 04:58=This proximity bomb will float freely in the air and follow|hedgehogs careless enough to come too close to it.|Its explosion is weaker than that of the land mine, however.|Attack: Hold to shoot with more power -04:59=This weapon is unfinished and experimental.|Use at your own risk! +04:59=This weapon is unfinished and experimental.|Use at your own risk!|Attack: Deploy 04:60=Unleash a rain of bullets upon your foes!|And they thought they were safe|behind a triple layer of girders.|Attack: Shoot at full power|Up/Down: Continue aiming +04:61=This weapon is unfinished and experimental.|Use at your own risk!|Attack: Deploy the bot ; Game goal strings 05:00=Game Modes @@ -1333,3 +1337,6 @@ 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! +06:29=/bubble: Make hedgehog hold its breath +06:30=/happy: Make hedgehog look happy +06:31=/sad: Make hedgehog look sad diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/es.lua --- a/share/hedgewars/Data/Locale/es.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/es.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/es.txt --- a/share/hedgewars/Data/Locale/es.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/es.txt Sun Mar 24 14:33:57 2024 -0400 @@ -1,528 +1,538 @@ -; Spanish locale -; Revision 4632 - -00:00=Granada -00:01=Granada de frag. -00:02=Bazuca -00:03=Abejorro -00:04=Escopeta -00:05=Taladro -00:06=Pasar -00:07=Cuerda -00:08=Mina -00:09=Desert Eagle -00:10=Dinamita -00:11=Bate de béisbol -00:12=Shoryuken -00:13=seg. -00:14=Paracaídas -00:15=Bombardeo aéreo -00:16=Minado aéreo -00:17=Soplete -00:18=Construcción -00:19=Teletransporte -00:20=Cambiar erizo -00:21=Mortero -00:22=Látigo -00:23=Kamikaze -00:24=Tarta -00:25=Seducción -00:26=Sandía bomba -00:27=Granada infernal -00:28=Misil perforador -00:29=Lanzapelotas -00:30=Napalm -00:31=Avión teledirigido -00:32=Baja gravedad -00:33=Daño extra -00:34=Invulnerabilidad -00:35=Tiempo extra -00:36=Mira láser -00:37=Vampirismo -00:38=Rifle francotirador -00:39=Platillo volante -00:40=Cóctel molotov -00:41=Birdy -00:42=Dispositivo portátil de portales -00:43=Piano -00:44=Limbuger añejo -00:45=Rifle sinusoidal (beta) -00:46=Lanzallamas -00:47=Bomba lapa -00:48=Mazo -00:49=Resurrección -00:50=Bombardeo perforador aéreo -00:51=Bola de barro -00:52=No hay arma seleccionada -00:53=Cabina del tiempo -00:54=Pistola de barro - -; 01:00=Loading … -01:01=Empate -01:02=¡%1 venció! -01:03=Volumen %1% -01:04=Pausa -01:05=¿Seguro que quieres salir (%1 / %2)? -01:06=¡Muerte súbita! -01:07=%1 restante -01:08=Combustible: %1% -01:09=Sincronizando... -01:10=Usar esta herramienta no hará que acabe tu turno. -01:11=Esta herramienta o arma todavía no está disponible. -01:12=¡Última ronda antes de la muerte súbita! -01:13=¡%1 rondas hasta la muerte súbita! -01:14=¡Prepárate, %1! -01:15=mínimo -01:16=bajo -01:17=normal -01:18=alto -01:19=extremo -01:20=Nivel de elasticidad: %1 - -; Eventos -; El erizo (%1) ha muerto -02:00=¡%1 ha estirado la pata! -02:00=¡%1 ha visto la luz! -02:00=¡%1 no lo vio venir! -02:00=¡%1 se despide! -02:00=¡%1 ahora está en un lugar mejor! -02:00=¡%1 pasea por verdes praderas! -02:00=¡%1 acaba de conocer a su Creador! -02:00=¡%1 no pudo aguantar más! -02:00=¡%1 ha cumplido con su deber! -02:00=¡%1 hizo el sacrificio supremo! -02:00=¡%1 deja atrás este mundo mortal! -02:00=¡%1 ha expirado! -02:00=¡%1 será recordado con cariño! -02:00=¡%1 ha tenido un aneurisma! -02:00=%1 deja atrás una mujer y tres niños -02:00=%1 ha disparado su última bazuca -02:00=%1 ha lanzado su última granada -02:00=%1 ha cocinado su última tarta -02:00=%1 se ha columpiado de su última cuerda -02:00=%1 ha solicitado su último bombardeo aéreo -02:00=%1 ha disparado su última escopeta -02:00=%1 ha lanzado su último melón -02:00=%1 ha apuntado su última pistola -02:00=%1 pensó que aguantaría una más -02:00=%1 habría agradecido un botiquín más -02:00=%1 se ha ido a jugar a un juego mejor -02:00=%1 se ha picado -02:00=%1 falló -02:00=Pobrecito %1... -02:00=%1 prefiere Warmux -02:00=%1 intentó parar las balas con su cara -02:00=%1 es un héroe entre los hom... digo.. erizos -02:00=%1 encontró el camino al Valhala -02:00=%1 has left the building -02:00=%1 siguió la misma suerte que los dinosaurios -02:00=%1 acerca los erizos un poco más a la extinción -02:00=%1, haces que se me humedezcan los ojos -02:00=%1 es un ex-erizo -02:00=%1 se fue a criar malvas -02:00=%1 ha dejado de ser -02:00=Despedíos de %1 -02:00=No hay esperanza para %1 -02:00=%1 recorrió la última milla -02:00=%1 sufrió un Error Fatal -02:00=%1 está frío como una piedra -02:00=%1 ha expirado -02:00=%1 se une al coro celestial -02:00=¡Cuídate, %1, ojalá nos hubiéramos llegado a conocer mejor! -02:00=%1 tenía intolerancia a las balas -02:00=%1 habría necesitado una vida extra -02:00=¿Hay algún médico en la sala? -02:00=¡Zas! ¡En toda la boca! - -; El erizo (%1) se ha ahogado -02:01=¡%1 hace el submarino! -02:01=¡%1 imita al Titanic! -02:01=¡%1 nada como una piedra! -02:01=¡%1 flota como un ladrillo! -02:01=¡%1 flota como el plomo! -02:01=%1 investiga a fondo -02:01=%1 hizo "glu, glu, glu" -02:01=%1 hizo "splash" -02:01=%1 olvidó sus brazaletes -02:01=A %1 le habrían venido realmente bien aquellas clases de natación -02:01=%1 olvidó su tabla de surf -02:01=%1 tiene los dedos arrugados -02:01=%1 está chorreando -02:01=%1 olvidó su salvavidas -02:01=%1 está durmiendo con los peces -02:01=%1 piensa que la simulación de fluidos de este juego apesta -02:01=%1 tenía sed, MUCHA sed -02:01=El océano reclamó a %1 -02:01=%1 está perdido en el mar -02:01=%1 debería haber traído sus gafas de bucear -02:01=%1 ha sido enterrado en el mar -02:01=%1 tuvo una sensación de pesadez -02:01=%1 está practicando su zambullida -02:01=%1 se fue a buscar el Titanic -02:01=%1 no es como Jesús -02:01=%1 está buscando a Nemo -02:01=Te asombraría saber cuántos erizos hay ahí abajo -02:01=%1 hizo que el nivel del mar subiera un pelín -02:01=%1 no se alistó a la marina -02:01=%1 hace su imitación del pez muerto -02:01=Al menos no te tiraron por el váter, %1 -02:01=Sonic no podía nadar y tú tampoco, %1 -02:01=%1 prefiere jugar a Ecco the dolphin -02:01=%1 ha ido a visitar Aquaria -02:01=%1 ha encontrado la ciudad perdida de la Atlántida -02:01=Necesitas practicar más tu estilo perrito, %1 -02:01=Necesitas practicar más tu brazada, %1 -02:01=Necesitas practicar más tu estilo mariposa, %1 -02:01=%1 debería haber traído sus esquís acuáticos -02:01=A %1 no le gustan los deportes acuáticos -02:01=%1 estará haciendo burbujas para siempre -02:01=%1 no pensó que fuera tan profundo -02:01=%1 cree que el agua salada es buena para la piel -02:01=El agua salada cura las heridas, %1 -02:01=%1 paseó por la tabla -02:01=%1 se bañó -02:01=%1 se remojó -02:01=%1 está mojado, mojado, mojado -02:01=No olvides el jabón, %1 -02:01=¡No salpiques, %1! -02:01=¿Estaba fría el agua? - -; El combate empieza -02:02=¡Luchad! -02:02=¡Armado y listo! -02:02=Vamos a montar una buena fiesta -02:02=El último erizo en pie gana -02:02=¡Vamos! -02:02=¡Let's rock! -02:02=¡Al lío! -02:02=En el comienzo... -02:02=Este es el principio de algo grande -02:02=Bienvenidos a Hedgewars -02:02=Bienvenido al frente, soldado -02:02=¡Machaca al enemigo! -02:02=Que gane el mejor erizo -02:02=Victoria o muerte -02:02=Hasta la victoria, siempre -02:02=Perder no es una opción -02:02=¡Soltad los erizos de la guerra! -02:02=Hedgewars, presentado por Hedgewars.org -02:02=Tienes suerte si no juegas contra Tiyuri -02:02=Tienes suerte si no juegas contra unC0Rr -02:02=Tienes suerte si no juegas contra Nemo -02:02=Tienes suerte si no juegas contra Smaxx -02:02=Tienes suerte si no juegas contra Jessor -02:02=¡Da lo mejor! -02:02=¡El que pierda, paga! -02:02=Que empiece la batalla del milenio -02:02=Que empiece la batalla del siglo -02:02=Que empiece la batalla de la década -02:02=Que empiece la batalla del año -02:02=Que empiece la batalla del mes -02:02=Que empiece la batalla de la semana -02:02=Que empiece la batalla del día -02:02=Que empiece la batalla de la hora -02:02=¡Hazlo lo mejor que puedas! -02:02=¡Destruye al enemigo! -02:02=Buena suerte -02:02=Diviértete -02:02=Lucha limpiamente -02:02=Lucha suciamente -02:02=Lucha con honore -02:02=Si haces trampas, procura que no te pillen -02:02=Nunca abandones -02:02=Nunca te rindas -02:02=¡Que empiece la marcha! -02:02=¡Espero que estés listo para el meneo! -02:02=¡Vamos, vamos, vamos! -02:02=Tropas, ¡avanzad! -02:02=¡Dadles caña! -02:02=¡No temáis! - -; Round ends and team/clan (%1) wins -02:03=¡%1 venció! - -; Round ends in a draw -02:04=Empate - -; Botiquín -02:05=¡Ayuda en camino! -02:05=¡Médico! -02:05=¡Primeros auxilios desde el cielo! -02:05=Un buen lote de medicamentos para ti -02:05=¡Buena salud... en forma de caja! -02:05=La llamada del doctor -02:05=¡Tiritas frescas! -02:05=Vendas limpias -02:05=Esto te hará sentir mejor -02:05=¡Una poción para ti! Ups, juego equivocado -02:05=¡Un paquete para recoger! -02:05=Cógelo -02:05=Una barrita saludable -02:05=Una cura para el dolor -02:05=Posología: ¡tantos como puedas conseguir! -02:05=Envío urgente -02:05=¡Víveres! - -; Caja de armamento -02:06=¡Más armas! -02:06=¡Refuerzos! -02:06=¡Armado y listo! -02:06=Me pregunto qué arma habrá ahí dentro... -02:06=¡Víveres! -02:06=¿Qué habrá dentro? -02:06=La navidad llega antes a Hedgewars -02:06=¡Un regalito! -02:06=¡Envío especial! -02:06=No sabes qué pesadilla ha sido atravesar la aduana con esto -02:06=Juguetes destructivos del Cielo -02:06=¡Cuidado! Volátil -02:06=¡Cuidado! Inflamable -02:06=Cógelo o reviéntalo, la elección es tuya -02:06=¡Mmmmm, armas! -02:06=Una caja de poder destructivo -02:06=¡Correo aéreo! -02:06=Contenga lo que contenga esa caja, seguro que no es pizza -02:06=¡Cógelo! -02:06=Envío de armas en camino -02:06=Refuerzos en camino -02:06=¡No dejes que el enemigo te lo quite! -02:06=¡Nuevos juguetitos! -02:06=¡Una caja misteriosa! - -; Caja de herramientas -02:07=¡La hora de la herramienta! -02:07=Esto podría ser útil... -02:07=¡Herramientas! -02:07=Usa esta caja -02:07=Cuidado los de abajo -02:07=¡Más herramientas! -02:07=¡Herramientas para ti! -02:07=¡Esto te vendrá bien! -02:07=Úsalo correctamente -02:07=Guau, esta caja es pesada -02:07=Podrías necesitarlo - -; El erizo %1 pasa su turno -02:08=%1 es un muermo... -02:08=%1 ni se molesta -02:08=%1 es un erizo perezoso -02:08=%1 tiene la mente en blanco -02:08=%1 abandona -02:08=El que quiera peces debe mojarse el culo, %1 -02:08=%1 abandona vergonzosamente el frente -02:08=%1 es muy muy vago -02:08=%1 necesita un poco más de motivación -02:08=%1 es un pacifista -02:08=%1 necesita su inhalador -02:08=%1 echa una cabezada -02:08=%1 se relaja -02:08=%1 se tumba a la bartola -02:08=Ommmmmm... -02:08=%1 no tiene confianza en sí mismo -02:08=%1 decide no hacer nada en absoluto -02:08=%1 deja que el enemigo se destruya a sí mismo -02:08=%1 debe ser un muermo en las fiestas -02:08=%1 se esconde -02:08=%1 ha dejado pasar esta oportunidad -02:08=%1 ha decidido que lo mejor que puede hacer es... nada -02:08=%1 es un cobardica -02:08=Co-Co-Cococó, %1 es un gallina -02:08=¡%1 es un cobarde! -02:08=%1 está esperando a la muerte súbita -02:08=%1 no se encuentra en forma -02:08=%1 está reconsiderando el sentido de su vida -02:08=%1 nunca tuvo mucha puntería, de todas formas -02:08=%1 nunca quiso alistarse en el ejército en realidad -02:08=No nos hagas perder el tiempo, %1 -02:08=Me has decepcionado, %1 -02:08=Vamos, %1, eres capaz de hacerlo mejor -02:08=La voluntad de %1 se quebró -02:08=Por lo visto %1 tiene mejores cosas que hacer -02:08=%1 está paralizado de terror -02:08=%1 se ha dormido - -; El erizo %1 se daña únicamente a sí mismo -02:09=¡%1 debería ir al campo de tiro a practicar! -02:09=%1 se odia a sí mismo -02:09=¡%1 estaba en el lado equivocado! -02:09=%1 es un poco emo -02:09=%1 tenía el arma del revés -02:09=%1 es un poco sádico -02:09=%1 es un masoquista -02:09=%1 no tiene instinto de supervivencia -02:09=%1 la pifió -02:09=%1 la fastidió -02:09=Ese fue un tiro pésimo, %1 -02:09=%1 es demasiado descuidado como para usar armas peligrosas -02:09=%1, deberías considerar un cambio de profesión -02:09=¡Peor! ¡Tiro! ¡Historia! -02:09=¡No, no, no, %1, debes disparar AL ENEMIGO! -02:09=%1 debería estar destruyendo enemigos -02:09=%1 se acerca un poco más al suicidio -02:09=%1 le echa una mano al enemigo -02:09=Eso fue una estupidez, %1 -02:09=%1 vive con la máxima "sin dolor no hay honor" -02:09=%1 está confuso -02:09=%1 se dispara a sí mismo en su confusión -02:09=¡%1 tiene un don para hacerse daño! -02:09=¡%1 es un patoso! -02:09=%1 es torpe -02:09=%1 le acaba de demostrar al enemigo de lo que es capaz -02:09=No se puede esperar que %1 sea perfecto todo el tiempo -02:09=No te preocupes, %1, nabie es ferpecto -02:09=¡Pues claro que %1 hizo eso a propósito! -02:09=No se lo diré a nadie si tú tampoco lo haces, %1 -02:09=¡Qué vergüenza! -02:09=Seguro que nadie te ha visto, %1 -02:09=%1 necesita revisar el manual -02:09=Las armas de %1 eran obviamente defectuosas - -; Home run (usando el bate de béisbol) -02:10=¡Home Run! -02:10=Es un pájaro, es un avión... -02:10=¡Eliminado! - -; El erizo (%1) abandona (el equipo ha salido de la partida) -02:11=¡%1 tiene que irse a mimir! -02:11=¡%1 tiene que irse a la cama! -02:11=Parece que %1 está demasiado ocupado para seguir jugando -02:11=¡Teletranspórtame, Scotty! -02:11=%1 tiene que irse - -; Categorías de armamento -03:00=Arma arrojadiza -03:01=Arma arrojadiza -03:02=Artillería -03:03=Misil -03:04=Arma de fuego (múltiples disparos) -03:05=Herramienta de excavación -03:06=Acción -03:07=Herramienta de transporte -03:08=Bomba de proximidad -03:09=Arma de fuego (disparo único) -03:10=¡BUM! -03:11=¡Bonk! -03:12=Artes marciales -03:13=SIN USAR -03:14=Herramienta de transporte -03:15=Ataque por aire -03:16=Ataque por aire -03:17=Herramienta de excavación -03:18=Herramienta -03:19=Herramienta de transporte -03:20=Acción -03:21=Arma balística -03:22=¡Llámame Indiana! -03:23=Artes marciales (en serio) -03:24=¡La tarta NO ES una mentira! -03:25=Disfraz -03:26=Arma arrojadiza jugosa -03:27=Arma arrojadiza fogosa -03:28=Artillería -03:29=Artillería -03:30=Ataque por aire -03:31=Bomba radiocontrolada -03:32=Efecto temporal -03:33=Efecto temporal -03:34=Efecto temporal -03:35=Efecto temporal -03:36=Efecto temporal -03:37=Efecto temporal -03:38=Arma de fuego (disparo único) -03:39=Herramienta de transporte -03:40=Bomba incendiaria -03:41=Amigo chillón -03:42=Creo que voy a tomar una nota... -03:43=E interpretando el Cascanueces tenemos a... -03:44=Consumir preferentemente antes de 1923 -03:45=¡El poder de la ciencia! -03:46=¡Caliente caliente caliente! -03:47=¡Pégalo en un buen sitio! -03:48=Pablo clavó un clavito -03:49=Hace exactamente lo que dice -03:50=Para los amantes de los topos -03:51=Me la encontré por el suelo -03:52=SIN USAR -03:53=Tipo 40 -03:54=Herramienta - -; Descripciones de armamento ( líneas delimitadas con | ) -04:00=Ataca a tus enemigos usando una sencilla granada.|Explotará una vez el temporizador llegue a cero.|1-5: ajustar temporizador.|Atacar: mantener presionado para lanzar más lejos. -04:01=Ataca a tus enemigos usando una granada de fragmentación.|Se fragmentará en metralla explosiva|una vez el temporizador llegue a cero.|1-5: ajustar temporizador.|Atacar: mantener presionado para lanzar más lejos. -04:02=Ataca a tus enemigos usando un proyectil balístico.|¡Atención al viento, modificará su trayectoria!|Atacar: mantener presionado para lanzar más lejos. -04:03=Lanza un abejorro explosivo que buscará el objetivo marcado.|No dispares a máxima potencia para mejorar su precisión.|Ratón: seleccionar objetivo.|Atacar: mantener presionado para lanzar más lejos. -04:04=Ataca a tus enemigos usando una escopeta de dos cañones.|Las balas se dispersan, así que no necesitarás|un tiro directo para herir a tus oponentes.|Atacar: abrir fuego (dos tiros). -04:05=¡Entiérrate! Usa el martillo neumático para excavar|un pozo en el suelo y alcanzar otras áreas.|Atacar: empezar o terminar de cavar. -04:06=¿Aburrido? ¿Sin posibilidad de atacar? ¿Racionas tu munición?|¡No hay problema! ¡Adelante, pasa esta turno, gallina!|Atacar: pasa este turno sin hacer nada. -04:07=Cubre grandes distancias usando hábilmente la cuerda.|Gana inercia para empujar a otros erizos|o deja caer granadas u otras armas sobre ellos.|Atacar: lanza o suelta la cuerda.|Salto: deja caer el arma seleccionada. -04:08=Mantén alejados a tus enemigos desplegando minas|en pasadizos estrechos o justo bajo sus pies.|¡Asegúrate de alejarte rápidamente para no activarla tú mismo!|Atacar: deposita una mina ante ti. -04:09=¿No confías en tu puntería? Con la desert eagle|tienes 4 disparos para conseguir alcanzar a tu enemigo.|Atacar: abrir fuego (hasta 4 veces). -04:10=La fuerza bruta siempre es una opción. Coloca este clásico|explosivo cerca de tus enemigos y huye.|Atacar: deposita la dinamita ante ti. -04:11=¡Manda a tus enemigos lejos de ti de un buen batazo!|Acaba con ellos lanzándolos fuera del mapa o al agua.|¿O qué tal lanzarles algunas minas?|Atacar: batear cualquier cosa delante de ti. -04:12=Enfréntate cara a cara con tus enemigos|y libera el poder de tus puños sobre ellos.|Útil para lanzarlos fuera del mapa o al agua.|Atacar: ejecutar el puño de fuego. -04:13=SIN USAR -04:14=¿Te dan miedo las alturas? Nunca más con un buen paracaídas.|Se desplegará automáticamente cuando caigas suficientemente lejos.|Atacar: desplegar/replegar el paracaídas.|Cursores: controlar el descenso. -04:15=Haz llover bombas sobre tus enemigos solicitando un bombardeo aéreo.|Derecha/izquierda: determinar dirección del ataque.|Ratón: seleccionar objetivo. -04:16=Haz llover minas sobre tus enemigos solicitando un minado aéreo.|Derecha/izquierda: determinar dirección del ataque.|Ratón: seleccionar objetivo. -04:17=¿Buscas refugio? ¿Necesitas salir de una cueva?|Usa el soplete para cavar un túnel a través del terreno.|Atacar: encender/apagar el soplete. -04:18=¿Necesitas protección adicional o quieres atravesar algún abismo?|Coloca tantas vigas como quieras/puedas.|Derecha/izquierda: seleccionar tipo de viga.|Ratón: colocar viga. -04:19=Usado en el momento adecuado, el teletransporte puede ser|tu mayor aliado, ayudándote a escapar de situaciones mortales|o alcanzar víveres valiosos.|Ratón: seleccionar objetivo. -04:20=Te permite jugar este turno con otro de tus erizos.|Atacar: activar.|Tabulador: cambiar entre erizos una vez activada. -04:21=Lanza un proyectil que se fragmentará al impactar, enviando|una lluvia explosiva sobre tus enemigos.|Atacar: lanzar a máxima potencia. -04:22=¡Siéntete como Indiana Jones! El látigo es un arma muy útil|en ciertas situaciones, especialmente para deshacerte de|erizos enemigos enviándolos fuera del mapa o al agua.|Atacar: golpear cualquier cosa delante de ti. -04:23=Si no tienes nada que perder, esto puede serte útil.|Sacrifica a tu erizo lanzándolo como un cohete que despejará|cualquier cosa que encuentre en su camino, detonando al final.|Atacar: manda a tu erizo a la perdición. -04:24=¡Feliz cumpleaños! Esta tarta bípeda caminará|hasta tus enemigos para darles una fiesta sorpresa explosiva.|La tarta es capaz de atravesar casi cualquier tipo de terreno,|pero evita que se quede atascada.|Atacar: enviar la tarta de camino o hacerla detonar. -04:25=Utiliza este disfraz para seducir a tus enemigos,|haciéndoles perder la cabeza y saltar como locos hacia ti|(y de paso hacia el agua o una mina).|Atacar: disfrazarte y lanzar un beso a tus enemigos. -04:26=Lanza esta jugosa sandía a tus enemigos.|Una vez el temporizador llegue a cero se fragmentará|en rodajas deliciosamente explosivas sobre tus enemigos.|Atacar: mantener presionado para lanzar más lejos. -04:27=Haz que el fuego del averno chamusque a tus enemigos|usando esta granada infernal.|¡Aléjate todo lo que puedas de la explosión,|la onda expansiva y el fuego tienen gran alcance!|Atacar: mantener presionado para lanzar más lejos. -04:28=Una vez lanzado, este proyectil comenzará a cavar tan pronto toque tierra|y explotará al volver a salir a la superficie o al encontrar algún obstáculo,|como un erizo enemigo o una caja.|Atacar: mantener presionado para lanzar más lejos. -04:29=¡Puede parecer un juguete, pero no lo es!|El lanzapelotas dispara montones de pelotas multicolores|llenas de explosivos que rebotarán hasta tus enemigos.|Atacar: lanzar a máxima potencia.|Arriba/abajo: modificar ángulo de disparo. -04:30=Haz llover fuego sobre tus enemigos solicitando un ataque aéreo.|Con la destreza adecuada, este ataque puede erradicar grandes áreas de tierra.|Derecha/izquierda: determinar dirección del ataque.|Ratón: seleccionar objetivo. -04:31=El avión teledirigido es el arma ideal para recoger cajas|o atacar enemigos lejanos.|Cargado con 3 bombas, el avión explotará|si choca contra algo.|Atacar: lanzar el avión o dejar caer las bombas.|Cursores: controlar el avión. -04:32=¡Mucho mejor que cualquier dieta! Salta más alto y más lejos|o haz que tus enemigos vuelen incluso más lejos.|Atacar: activar. -04:33=A veces uno necesita una pequeña ayuda para acabar con sus enemigos.|Atacar: activar. -04:34=¡Na, na, na, no me tocas!|Atacar: activar. -04:35=A veces el reloj corre demasiado deprisa. Consigue un poco|de tiempo extra para finalizar tu ataque.|Atacar: activar. -04:36=Vaya, parece que tu puntería apesta. Por suerte para ti|la tecnología moderna está de tu lado.|Atacar: activar. -04:37=No le temas a la luz del día. Sólo durará un turno, pero|te permitirá absorber la fuerza vital de tus enemigos|cuando les ataques.|Atacar: activar. -04:38=El rifle de francotirador puede ser el arma más destructiva|de todo tu arsenal, pero es muy inefectiva en distancias cortas.|El daño infligido es proporcional a la distancia respecto del objetivo.|Atacar: abrir fuego (un disparo). -04:39=Vuela hasta otras partes del mapa usando un platillo volante.|Puede ser complicado de controlar, pero conseguirás llegar|a sitios que nunca hubieras imaginado accesibles.|Atacar: activar.|Cursores: acelerar en esa dirección (un golpe cada vez). -04:40=Alza un muro de fuego usando esta botella|llena de (en breve, ardiendo) líquido inflamable.|Atacar: mantener presionado para lanzar más lejos. -04:41=¡Demostrando que lo natural puede ser mejor|que lo artificial, Birdy puede no sólo|transportar tu erizo como el platillo volante,|sino también lanzar huevos envenenados a tus enemigos!|Atacar: activar/lanzar huevos.|Cursores: aletear en esa dirección. -04:42=El dispositivo portátil de portales es capaz de|transportar instantáneamente minas, armas o ¡incluso erizos!|Úsalo adecuadamente y tu campaña será un... |¡ÉXITO ALUCINANTE!|Atacar: disparar un portal.|Cambiar: alternar el color a disparar. -04:43=¡Haz un debut explosivo en el mundo del espectáculo!|Lanza un piano desde lo más alto del firmamento, pero ten cuidado...|¡alguien debe tocarlo, y eso puede costarte la vida!|Ratón: seleccionar objetivo.|F1-F9: tocar el piano. -04:44=¡No es simplemente queso, es un arma biológica!|No causará mucho daño al detonar, pero ten por seguro|que cualquiera que se acerque demasiado|a su oloroso rastro quedará gravemente intoxicado.|1-5: ajustar temporizador.|Atacar: mantener presionado para lanzar más lejos. -04:45=Al fin una utilidad para todas esas clases de física.|Dispara una devastadora onda sinusoidal que mandará|a tus enemigos al infierno matemático.|Ten cuidado, el retroceso de este arma es considerable.|Atacar: disparar. -04:46=Envuelve a tus enemigos en siseante fuego líquido.|¡Se derretirán de placer!|Atacar: activar.|Arriba/abajo: modificar trayectoria.|Izquierda/derecha: modificar potencia de fuego. -04:47=¡Dos bombas lapa, doble diversión!|Útiles para planear reacciones en cadena, atrincherarte...|¡o las dos cosas!.|Atacar: mantener presionado para lanzar más lejos (dos disparos). -04:48=¿Por qué la gente siempre la toma con los topos?|¡Golpear erizos es aún más divertido!|Un buen mazazo puede reducir en un tercio la|vida de cualquier erizo y enterrarlo completamente.|Atacar: activar. -04:49=¡Resucita a tus aliados!|Pero ten cuidado, también resucitarás a tus enemigos.|Atacar: mantener presionado para resucitar lentamente.|Arriba: acelerar resurrección. -04:50=¿Alguien está oculto bajo tierra?|¡Desentiérralos con un bombardeo perforador!|El temporizador controla la profundidad a alcanzar. -04:51=¿Qué hay más barato que el barro?|Un tiro gratis gracias a la bola de barro.|Hará que el enemigo salga volando|y escuece un poco si te entra en los ojos. -04:52=SIN USAR -04:53=Vive una trepidante aventura a través del|espacio y el tiempo mientras tus compañeros|siguen luchando en tu lugar.|Estate preparado para volver en cualquier momento,|o al llegar la Muerte súbita si te has quedado solo.|Aviso: no funciona durante la Muerte súbita,|si estás solo o si eres el rey. -04:54=Esparce un chorro de pegajoso barro.|Construye puentes, entierra enemigos o cierra túneles.|¡Ten especial cuidado de no mancharte! - -; Game goal strings -05:00=Modos de juego -05:01=Las siguientes reglas están activas: -05:02=Posicionar el rey: elige un buen cobijo para tu rey -05:03=Baja gravedad: mira bien dónde pisas -05:04=Invulnerabilidad: todos los erizos tienen un campo de fuerza personal que los protege -05:05=Vampirismo: dañar a tus enemigos te curará a ti -05:06=Karma: compartirás parte del daño que inflijas -05:07=Rey: ¡no permitas que tu rey muera! -05:08=Posicionar erizos: los jugadores posicionan a mano su erizos por turnos antes de empezar a jugar -05:09=Artillería: afina tu puntería, los erizos no pueden moverse -05:10=Terreno indestructible: la mayoría de armas no pueden dañar el terreno de juego -05:11=Munición compartida: los equipos del mismo color comparten la munición -05:12=Minas: las minas detonarán a cabo de %1 segundo(s) -05:13=Minas: las minas detonarán al instante -05:14=Minas: las minas detonarán aleatoriamente al cabo de 0 - 3 segundos -05:15=Modificador al daño: las armas harán un %1% de su daño habitual -05:16=La salud de todos los erizos se restaura al final de cada turno -05:17=La computadora resucita al morir -05:18=Sin límite de ataques por turno -05:19=El arsenal se restaura al final de cada turno -05:20=Los erizos no comparten arsenal -05:21=Tag Team: los equipos del mismo clan se van turnando entre ellos.|Turno compartido: los equipos del mismo clan comparten la duración del turno. +; Spanish locale +; Revision 4632 + +00:00=Granada +00:01=Granada de frag. +00:02=Bazuca +00:03=Abejorro +00:04=Escopeta +00:05=Taladro +00:06=Pasar +00:07=Cuerda +00:08=Mina +00:09=Desert Eagle +00:10=Dinamita +00:11=Bate de béisbol +00:12=Shoryuken +00:13=seg. +00:14=Paracaídas +00:15=Bombardeo aéreo +00:16=Minado aéreo +00:17=Soplete +00:18=Construcción +00:19=Teletransporte +00:20=Cambiar erizo +00:21=Mortero +00:22=Látigo +00:23=Kamikaze +00:24=Tarta +00:25=Seducción +00:26=Sandía bomba +00:27=Granada infernal +00:28=Misil perforador +00:29=Lanzapelotas +00:30=Napalm +00:31=Avión teledirigido +00:32=Baja gravedad +00:33=Daño extra +00:34=Invulnerabilidad +00:35=Tiempo extra +00:36=Mira láser +00:37=Vampirismo +00:38=Rifle francotirador +00:39=Platillo volante +00:40=Cóctel molotov +00:41=Birdy +00:42=Dispositivo portátil de portales +00:43=Piano +00:44=Limbuger añejo +00:45=Rifle sinusoidal (beta) +00:46=Lanzallamas +00:47=Bomba lapa +00:48=Mazo +00:49=Resurrección +00:50=Bombardeo perforador aéreo +00:51=Bola de barro +00:52=No hay arma seleccionada +00:53=Cabina del tiempo +00:54=Pistola de barro + +; 01:00=Loading … +01:01=Empate +01:02=¡%1 venció! +01:03=Volumen %1% +01:04=Pausa +01:05=¿Seguro que quieres salir (%1 / %2)? +01:06=¡Muerte súbita! +01:07=%1 restante +01:08=Combustible: %1% +01:09=Sincronizando... +01:10=Usar esta herramienta no hará que acabe tu turno. +01:11=Esta herramienta o arma todavía no está disponible. +01:12=¡Última ronda antes de la muerte súbita! +01:13=¡%1 rondas hasta la muerte súbita! +01:14=¡Prepárate, %1! +01:15=mínimo +01:16=bajo +01:17=normal +01:18=alto +01:19=extremo +01:20=Nivel de elasticidad: %1 + +; Eventos +; El erizo (%1) ha muerto +02:00=¡%1 ha estirado la pata! +02:00=¡%1 ha visto la luz! +02:00=¡%1 no lo vio venir! +02:00=¡%1 se despide! +02:00=¡%1 ahora está en un lugar mejor! +02:00=¡%1 pasea por verdes praderas! +02:00=¡%1 acaba de conocer a su Creador! +02:00=¡%1 no pudo aguantar más! +02:00=¡%1 ha cumplido con su deber! +02:00=¡%1 hizo el sacrificio supremo! +02:00=¡%1 deja atrás este mundo mortal! +02:00=¡%1 ha expirado! +02:00=¡%1 será recordado con cariño! +02:00=¡%1 ha tenido un aneurisma! +02:00=%1 deja atrás una mujer y tres niños +02:00=%1 ha disparado su última bazuca +02:00=%1 ha lanzado su última granada +02:00=%1 ha cocinado su última tarta +02:00=%1 se ha columpiado de su última cuerda +02:00=%1 ha solicitado su último bombardeo aéreo +02:00=%1 ha disparado su última escopeta +02:00=%1 ha lanzado su último melón +02:00=%1 ha apuntado su última pistola +02:00=%1 pensó que aguantaría una más +02:00=%1 habría agradecido un botiquín más +02:00=%1 se ha ido a jugar a un juego mejor +02:00=%1 se ha picado +02:00=%1 falló +02:00=Pobrecito %1... +02:00=%1 prefiere Warmux +02:00=%1 intentó parar las balas con su cara +02:00=%1 es un héroe entre los hom... digo.. erizos +02:00=%1 encontró el camino al Valhala +02:00=%1 has left the building +02:00=%1 siguió la misma suerte que los dinosaurios +02:00=%1 acerca los erizos un poco más a la extinción +02:00=%1, haces que se me humedezcan los ojos +02:00=%1 es un ex-erizo +02:00=%1 se fue a criar malvas +02:00=%1 ha dejado de ser +02:00=Despedíos de %1 +02:00=No hay esperanza para %1 +02:00=%1 recorrió la última milla +02:00=%1 sufrió un Error Fatal +02:00=%1 está frío como una piedra +02:00=%1 ha expirado +02:00=%1 se une al coro celestial +02:00=¡Cuídate, %1, ojalá nos hubiéramos llegado a conocer mejor! +02:00=%1 tenía intolerancia a las balas +02:00=%1 habría necesitado una vida extra +02:00=¿Hay algún médico en la sala? +02:00=¡Zas! ¡En toda la boca! + +; El erizo (%1) se ha ahogado +02:01=¡%1 hace el submarino! +02:01=¡%1 imita al Titanic! +02:01=¡%1 nada como una piedra! +02:01=¡%1 flota como un ladrillo! +02:01=¡%1 flota como el plomo! +02:01=%1 investiga a fondo +02:01=%1 hizo "glu, glu, glu" +02:01=%1 hizo "splash" +02:01=%1 olvidó sus brazaletes +02:01=A %1 le habrían venido realmente bien aquellas clases de natación +02:01=%1 olvidó su tabla de surf +02:01=%1 tiene los dedos arrugados +02:01=%1 está chorreando +02:01=%1 olvidó su salvavidas +02:01=%1 está durmiendo con los peces +02:01=%1 piensa que la simulación de fluidos de este juego apesta +02:01=%1 tenía sed, MUCHA sed +02:01=El océano reclamó a %1 +02:01=%1 está perdido en el mar +02:01=%1 debería haber traído sus gafas de bucear +02:01=%1 ha sido enterrado en el mar +02:01=%1 tuvo una sensación de pesadez +02:01=%1 está practicando su zambullida +02:01=%1 se fue a buscar el Titanic +02:01=%1 no es como Jesús +02:01=%1 está buscando a Nemo +02:01=Te asombraría saber cuántos erizos hay ahí abajo +02:01=%1 hizo que el nivel del mar subiera un pelín +02:01=%1 no se alistó a la marina +02:01=%1 hace su imitación del pez muerto +02:01=Al menos no te tiraron por el váter, %1 +02:01=Sonic no podía nadar y tú tampoco, %1 +02:01=%1 prefiere jugar a Ecco the dolphin +02:01=%1 ha ido a visitar Aquaria +02:01=%1 ha encontrado la ciudad perdida de la Atlántida +02:01=Necesitas practicar más tu estilo perrito, %1 +02:01=Necesitas practicar más tu brazada, %1 +02:01=Necesitas practicar más tu estilo mariposa, %1 +02:01=%1 debería haber traído sus esquís acuáticos +02:01=A %1 no le gustan los deportes acuáticos +02:01=%1 estará haciendo burbujas para siempre +02:01=%1 no pensó que fuera tan profundo +02:01=%1 cree que el agua salada es buena para la piel +02:01=El agua salada cura las heridas, %1 +02:01=%1 paseó por la tabla +02:01=%1 se bañó +02:01=%1 se remojó +02:01=%1 está mojado, mojado, mojado +02:01=No olvides el jabón, %1 +02:01=¡No salpiques, %1! +02:01=¿Estaba fría el agua? + +; El combate empieza +02:02=¡Luchad! +02:02=¡Armado y listo! +02:02=Vamos a montar una buena fiesta +02:02=El último erizo en pie gana +02:02=¡Vamos! +02:02=¡Let's rock! +02:02=¡Al lío! +02:02=En el comienzo... +02:02=Este es el principio de algo grande +02:02=Bienvenidos a Hedgewars +02:02=Bienvenido al frente, soldado +02:02=¡Machaca al enemigo! +02:02=Que gane el mejor erizo +02:02=Victoria o muerte +02:02=Hasta la victoria, siempre +02:02=Perder no es una opción +02:02=¡Soltad los erizos de la guerra! +02:02=Hedgewars, presentado por Hedgewars.org +02:02=Tienes suerte si no juegas contra Tiyuri +02:02=Tienes suerte si no juegas contra unC0Rr +02:02=Tienes suerte si no juegas contra Nemo +02:02=Tienes suerte si no juegas contra Smaxx +02:02=Tienes suerte si no juegas contra Jessor +02:02=¡Da lo mejor! +02:02=¡El que pierda, paga! +02:02=Que empiece la batalla del milenio +02:02=Que empiece la batalla del siglo +02:02=Que empiece la batalla de la década +02:02=Que empiece la batalla del año +02:02=Que empiece la batalla del mes +02:02=Que empiece la batalla de la semana +02:02=Que empiece la batalla del día +02:02=Que empiece la batalla de la hora +02:02=¡Hazlo lo mejor que puedas! +02:02=¡Destruye al enemigo! +02:02=Buena suerte +02:02=Diviértete +02:02=Lucha limpiamente +02:02=Lucha suciamente +02:02=Lucha con honore +02:02=Si haces trampas, procura que no te pillen +02:02=Nunca abandones +02:02=Nunca te rindas +02:02=¡Que empiece la marcha! +02:02=¡Espero que estés listo para el meneo! +02:02=¡Vamos, vamos, vamos! +02:02=Tropas, ¡avanzad! +02:02=¡Dadles caña! +02:02=¡No temáis! + +; Round ends and team/clan (%1) wins +02:03=¡%1 venció! + +; Round ends in a draw +02:04=Empate + +; Botiquín +02:05=¡Ayuda en camino! +02:05=¡Médico! +02:05=¡Primeros auxilios desde el cielo! +02:05=Un buen lote de medicamentos para ti +02:05=¡Buena salud... en forma de caja! +02:05=La llamada del doctor +02:05=¡Tiritas frescas! +02:05=Vendas limpias +02:05=Esto te hará sentir mejor +02:05=¡Una poción para ti! Ups, juego equivocado +02:05=¡Un paquete para recoger! +02:05=Cógelo +02:05=Una barrita saludable +02:05=Una cura para el dolor +02:05=Posología: ¡tantos como puedas conseguir! +02:05=Envío urgente +02:05=¡Víveres! + +; Caja de armamento +02:06=¡Más armas! +02:06=¡Refuerzos! +02:06=¡Armado y listo! +02:06=Me pregunto qué arma habrá ahí dentro... +02:06=¡Víveres! +02:06=¿Qué habrá dentro? +02:06=La navidad llega antes a Hedgewars +02:06=¡Un regalito! +02:06=¡Envío especial! +02:06=No sabes qué pesadilla ha sido atravesar la aduana con esto +02:06=Juguetes destructivos del Cielo +02:06=¡Cuidado! Volátil +02:06=¡Cuidado! Inflamable +02:06=Cógelo o reviéntalo, la elección es tuya +02:06=¡Mmmmm, armas! +02:06=Una caja de poder destructivo +02:06=¡Correo aéreo! +02:06=Contenga lo que contenga esa caja, seguro que no es pizza +02:06=¡Cógelo! +02:06=Envío de armas en camino +02:06=Refuerzos en camino +02:06=¡No dejes que el enemigo te lo quite! +02:06=¡Nuevos juguetitos! +02:06=¡Una caja misteriosa! + +; Caja de herramientas +02:07=¡La hora de la herramienta! +02:07=Esto podría ser útil... +02:07=¡Herramientas! +02:07=Usa esta caja +02:07=Cuidado los de abajo +02:07=¡Más herramientas! +02:07=¡Herramientas para ti! +02:07=¡Esto te vendrá bien! +02:07=Úsalo correctamente +02:07=Guau, esta caja es pesada +02:07=Podrías necesitarlo + +; El erizo %1 pasa su turno +02:08=%1 es un muermo... +02:08=%1 ni se molesta +02:08=%1 es un erizo perezoso +02:08=%1 tiene la mente en blanco +02:08=%1 abandona +02:08=El que quiera peces debe mojarse el culo, %1 +02:08=%1 abandona vergonzosamente el frente +02:08=%1 es muy muy vago +02:08=%1 necesita un poco más de motivación +02:08=%1 es un pacifista +02:08=%1 necesita su inhalador +02:08=%1 echa una cabezada +02:08=%1 se relaja +02:08=%1 se tumba a la bartola +02:08=Ommmmmm... +02:08=%1 no tiene confianza en sí mismo +02:08=%1 decide no hacer nada en absoluto +02:08=%1 deja que el enemigo se destruya a sí mismo +02:08=%1 debe ser un muermo en las fiestas +02:08=%1 se esconde +02:08=%1 ha dejado pasar esta oportunidad +02:08=%1 ha decidido que lo mejor que puede hacer es... nada +02:08=%1 es un cobardica +02:08=Co-Co-Cococó, %1 es un gallina +02:08=¡%1 es un cobarde! +02:08=%1 está esperando a la muerte súbita +02:08=%1 no se encuentra en forma +02:08=%1 está reconsiderando el sentido de su vida +02:08=%1 nunca tuvo mucha puntería, de todas formas +02:08=%1 nunca quiso alistarse en el ejército en realidad +02:08=No nos hagas perder el tiempo, %1 +02:08=Me has decepcionado, %1 +02:08=Vamos, %1, eres capaz de hacerlo mejor +02:08=La voluntad de %1 se quebró +02:08=Por lo visto %1 tiene mejores cosas que hacer +02:08=%1 está paralizado de terror +02:08=%1 se ha dormido + +; El erizo %1 se daña únicamente a sí mismo +02:09=¡%1 debería ir al campo de tiro a practicar! +02:09=%1 se odia a sí mismo +02:09=¡%1 estaba en el lado equivocado! +02:09=%1 es un poco emo +02:09=%1 tenía el arma del revés +02:09=%1 es un poco sádico +02:09=%1 es un masoquista +02:09=%1 no tiene instinto de supervivencia +02:09=%1 la pifió +02:09=%1 la fastidió +02:09=Ese fue un tiro pésimo, %1 +02:09=%1 es demasiado descuidado como para usar armas peligrosas +02:09=%1, deberías considerar un cambio de profesión +02:09=¡Peor! ¡Tiro! ¡Historia! +02:09=¡No, no, no, %1, debes disparar AL ENEMIGO! +02:09=%1 debería estar destruyendo enemigos +02:09=%1 se acerca un poco más al suicidio +02:09=%1 le echa una mano al enemigo +02:09=Eso fue una estupidez, %1 +02:09=%1 vive con la máxima "sin dolor no hay honor" +02:09=%1 está confuso +02:09=%1 se dispara a sí mismo en su confusión +02:09=¡%1 tiene un don para hacerse daño! +02:09=¡%1 es un patoso! +02:09=%1 es torpe +02:09=%1 le acaba de demostrar al enemigo de lo que es capaz +02:09=No se puede esperar que %1 sea perfecto todo el tiempo +02:09=No te preocupes, %1, nabie es ferpecto +02:09=¡Pues claro que %1 hizo eso a propósito! +02:09=No se lo diré a nadie si tú tampoco lo haces, %1 +02:09=¡Qué vergüenza! +02:09=Seguro que nadie te ha visto, %1 +02:09=%1 necesita revisar el manual +02:09=Las armas de %1 eran obviamente defectuosas + +; Home run (usando el bate de béisbol) +02:10=¡Home Run! +02:10=Es un pájaro, es un avión... +02:10=¡Eliminado! + +; El erizo (%1) abandona (el equipo ha salido de la partida) +02:11=¡%1 tiene que irse a mimir! +02:11=¡%1 tiene que irse a la cama! +02:11=Parece que %1 está demasiado ocupado para seguir jugando +02:11=¡Teletranspórtame, Scotty! +02:11=%1 tiene que irse + +; Categorías de armamento +03:00=Arma arrojadiza +03:01=Arma arrojadiza +03:02=Artillería +03:03=Misil +03:04=Arma de fuego (múltiples disparos) +03:05=Herramienta de excavación +03:06=Acción +03:07=Herramienta de transporte +03:08=Bomba de proximidad +03:09=Arma de fuego (disparo único) +03:10=¡BUM! +03:11=¡Bonk! +03:12=Artes marciales +03:13=SIN USAR +03:14=Herramienta de transporte +03:15=Ataque por aire +03:16=Ataque por aire +03:17=Herramienta de excavación +03:18=Herramienta +03:19=Herramienta de transporte +03:20=Acción +03:21=Arma balística +03:22=¡Llámame Indiana! +03:23=Artes marciales (en serio) +03:24=¡La tarta NO ES una mentira! +03:25=Disfraz +03:26=Arma arrojadiza jugosa +03:27=Arma arrojadiza fogosa +03:28=Artillería +03:29=Artillería +03:30=Ataque por aire +03:31=Bomba radiocontrolada +03:32=Efecto temporal +03:33=Efecto temporal +03:34=Efecto temporal +03:35=Efecto temporal +03:36=Efecto temporal +03:37=Efecto temporal +03:38=Arma de fuego (disparo único) +03:39=Herramienta de transporte +03:40=Bomba incendiaria +03:41=Amigo chillón +03:42=Creo que voy a tomar una nota... +03:43=E interpretando el Cascanueces tenemos a... +03:44=Consumir preferentemente antes de 1923 +03:45=¡El poder de la ciencia! +03:46=¡Caliente caliente caliente! +03:47=¡Pégalo en un buen sitio! +03:48=Pablo clavó un clavito +03:49=Hace exactamente lo que dice +03:50=Para los amantes de los topos +03:51=Me la encontré por el suelo +03:52=SIN USAR +03:53=Tipo 40 +03:54=Herramienta +03:55=No se ve genial con esto! +03:56=Porfavor usalo o dejalo +03:57=Utilidad +03:58=Bomba de proximidad flotante. +03:59=El ultimo poder +; Descripciones de armamento ( líneas delimitadas con | ) +04:00=Ataca a tus enemigos usando una sencilla granada.|Explotará una vez el temporizador llegue a cero.|1-5: ajustar temporizador.|Atacar: mantener presionado para lanzar más lejos. +04:01=Ataca a tus enemigos usando una granada de fragmentación.|Se fragmentará en metralla explosiva|una vez el temporizador llegue a cero.|1-5: ajustar temporizador.|Atacar: mantener presionado para lanzar más lejos. +04:02=Ataca a tus enemigos usando un proyectil balístico.|¡Atención al viento, modificará su trayectoria!|Atacar: mantener presionado para lanzar más lejos. +04:03=Lanza un abejorro explosivo que buscará el objetivo marcado.|No dispares a máxima potencia para mejorar su precisión.|Ratón: seleccionar objetivo.|Atacar: mantener presionado para lanzar más lejos. +04:04=Ataca a tus enemigos usando una escopeta de dos cañones.|Las balas se dispersan, así que no necesitarás|un tiro directo para herir a tus oponentes.|Atacar: abrir fuego (dos tiros). +04:05=¡Entiérrate! Usa el martillo neumático para excavar|un pozo en el suelo y alcanzar otras áreas.|Atacar: empezar o terminar de cavar. +04:06=¿Aburrido? ¿Sin posibilidad de atacar? ¿Racionas tu munición?|¡No hay problema! ¡Adelante, pasa esta turno, gallina!|Atacar: pasa este turno sin hacer nada. +04:07=Cubre grandes distancias usando hábilmente la cuerda.|Gana inercia para empujar a otros erizos|o deja caer granadas u otras armas sobre ellos.|Atacar: lanza o suelta la cuerda.|Salto: deja caer el arma seleccionada. +04:08=Mantén alejados a tus enemigos desplegando minas|en pasadizos estrechos o justo bajo sus pies.|¡Asegúrate de alejarte rápidamente para no activarla tú mismo!|Atacar: deposita una mina ante ti. +04:09=¿No confías en tu puntería? Con la desert eagle|tienes 4 disparos para conseguir alcanzar a tu enemigo.|Atacar: abrir fuego (hasta 4 veces). +04:10=La fuerza bruta siempre es una opción. Coloca este clásico|explosivo cerca de tus enemigos y huye.|Atacar: deposita la dinamita ante ti. +04:11=¡Manda a tus enemigos lejos de ti de un buen batazo!|Acaba con ellos lanzándolos fuera del mapa o al agua.|¿O qué tal lanzarles algunas minas?|Atacar: batear cualquier cosa delante de ti. +04:12=Enfréntate cara a cara con tus enemigos|y libera el poder de tus puños sobre ellos.|Útil para lanzarlos fuera del mapa o al agua.|Atacar: ejecutar el puño de fuego. +04:13=SIN USAR +04:14=¿Te dan miedo las alturas? Nunca más con un buen paracaídas.|Se desplegará automáticamente cuando caigas suficientemente lejos.|Atacar: desplegar/replegar el paracaídas.|Cursores: controlar el descenso. +04:15=Haz llover bombas sobre tus enemigos solicitando un bombardeo aéreo.|Derecha/izquierda: determinar dirección del ataque.|Ratón: seleccionar objetivo. +04:16=Haz llover minas sobre tus enemigos solicitando un minado aéreo.|Derecha/izquierda: determinar dirección del ataque.|Ratón: seleccionar objetivo. +04:17=¿Buscas refugio? ¿Necesitas salir de una cueva?|Usa el soplete para cavar un túnel a través del terreno.|Atacar: encender/apagar el soplete. +04:18=¿Necesitas protección adicional o quieres atravesar algún abismo?|Coloca tantas vigas como quieras/puedas.|Derecha/izquierda: seleccionar tipo de viga.|Ratón: colocar viga. +04:19=Usado en el momento adecuado, el teletransporte puede ser|tu mayor aliado, ayudándote a escapar de situaciones mortales|o alcanzar víveres valiosos.|Ratón: seleccionar objetivo. +04:20=Te permite jugar este turno con otro de tus erizos.|Atacar: activar.|Tabulador: cambiar entre erizos una vez activada. +04:21=Lanza un proyectil que se fragmentará al impactar, enviando|una lluvia explosiva sobre tus enemigos.|Atacar: lanzar a máxima potencia. +04:22=¡Siéntete como Indiana Jones! El látigo es un arma muy útil|en ciertas situaciones, especialmente para deshacerte de|erizos enemigos enviándolos fuera del mapa o al agua.|Atacar: golpear cualquier cosa delante de ti. +04:23=Si no tienes nada que perder, esto puede serte útil.|Sacrifica a tu erizo lanzándolo como un cohete que despejará|cualquier cosa que encuentre en su camino, detonando al final.|Atacar: manda a tu erizo a la perdición. +04:24=¡Feliz cumpleaños! Esta tarta bípeda caminará|hasta tus enemigos para darles una fiesta sorpresa explosiva.|La tarta es capaz de atravesar casi cualquier tipo de terreno,|pero evita que se quede atascada.|Atacar: enviar la tarta de camino o hacerla detonar. +04:25=Utiliza este disfraz para seducir a tus enemigos,|haciéndoles perder la cabeza y saltar como locos hacia ti|(y de paso hacia el agua o una mina).|Atacar: disfrazarte y lanzar un beso a tus enemigos. +04:26=Lanza esta jugosa sandía a tus enemigos.|Una vez el temporizador llegue a cero se fragmentará|en rodajas deliciosamente explosivas sobre tus enemigos.|Atacar: mantener presionado para lanzar más lejos. +04:27=Haz que el fuego del averno chamusque a tus enemigos|usando esta granada infernal.|¡Aléjate todo lo que puedas de la explosión,|la onda expansiva y el fuego tienen gran alcance!|Atacar: mantener presionado para lanzar más lejos. +04:28=Una vez lanzado, este proyectil comenzará a cavar tan pronto toque tierra|y explotará al volver a salir a la superficie o al encontrar algún obstáculo,|como un erizo enemigo o una caja.|Atacar: mantener presionado para lanzar más lejos. +04:29=¡Puede parecer un juguete, pero no lo es!|El lanzapelotas dispara montones de pelotas multicolores|llenas de explosivos que rebotarán hasta tus enemigos.|Atacar: lanzar a máxima potencia.|Arriba/abajo: modificar ángulo de disparo. +04:30=Haz llover fuego sobre tus enemigos solicitando un ataque aéreo.|Con la destreza adecuada, este ataque puede erradicar grandes áreas de tierra.|Derecha/izquierda: determinar dirección del ataque.|Ratón: seleccionar objetivo. +04:31=El avión teledirigido es el arma ideal para recoger cajas|o atacar enemigos lejanos.|Cargado con 3 bombas, el avión explotará|si choca contra algo.|Atacar: lanzar el avión o dejar caer las bombas.|Cursores: controlar el avión. +04:32=¡Mucho mejor que cualquier dieta! Salta más alto y más lejos|o haz que tus enemigos vuelen incluso más lejos.|Atacar: activar. +04:33=A veces uno necesita una pequeña ayuda para acabar con sus enemigos.|Atacar: activar. +04:34=¡Na, na, na, no me tocas!|Atacar: activar. +04:35=A veces el reloj corre demasiado deprisa. Consigue un poco|de tiempo extra para finalizar tu ataque.|Atacar: activar. +04:36=Vaya, parece que tu puntería apesta. Por suerte para ti|la tecnología moderna está de tu lado.|Atacar: activar. +04:37=No le temas a la luz del día. Sólo durará un turno, pero|te permitirá absorber la fuerza vital de tus enemigos|cuando les ataques.|Atacar: activar. +04:38=El rifle de francotirador puede ser el arma más destructiva|de todo tu arsenal, pero es muy inefectiva en distancias cortas.|El daño infligido es proporcional a la distancia respecto del objetivo.|Atacar: abrir fuego (un disparo). +04:39=Vuela hasta otras partes del mapa usando un platillo volante.|Puede ser complicado de controlar, pero conseguirás llegar|a sitios que nunca hubieras imaginado accesibles.|Atacar: activar.|Cursores: acelerar en esa dirección (un golpe cada vez). +04:40=Alza un muro de fuego usando esta botella|llena de (en breve, ardiendo) líquido inflamable.|Atacar: mantener presionado para lanzar más lejos. +04:41=¡Demostrando que lo natural puede ser mejor|que lo artificial, Birdy puede no sólo|transportar tu erizo como el platillo volante,|sino también lanzar huevos envenenados a tus enemigos!|Atacar: activar/lanzar huevos.|Cursores: aletear en esa dirección. +04:42=El dispositivo portátil de portales es capaz de|transportar instantáneamente minas, armas o ¡incluso erizos!|Úsalo adecuadamente y tu campaña será un... |¡ÉXITO ALUCINANTE!|Atacar: disparar un portal.|Cambiar: alternar el color a disparar. +04:43=¡Haz un debut explosivo en el mundo del espectáculo!|Lanza un piano desde lo más alto del firmamento, pero ten cuidado...|¡alguien debe tocarlo, y eso puede costarte la vida!|Ratón: seleccionar objetivo.|F1-F9: tocar el piano. +04:44=¡No es simplemente queso, es un arma biológica!|No causará mucho daño al detonar, pero ten por seguro|que cualquiera que se acerque demasiado|a su oloroso rastro quedará gravemente intoxicado.|1-5: ajustar temporizador.|Atacar: mantener presionado para lanzar más lejos. +04:45=Al fin una utilidad para todas esas clases de física.|Dispara una devastadora onda sinusoidal que mandará|a tus enemigos al infierno matemático.|Ten cuidado, el retroceso de este arma es considerable.|Atacar: disparar. +04:46=Envuelve a tus enemigos en siseante fuego líquido.|¡Se derretirán de placer!|Atacar: activar.|Arriba/abajo: modificar trayectoria.|Izquierda/derecha: modificar potencia de fuego. +04:47=¡Dos bombas lapa, doble diversión!|Útiles para planear reacciones en cadena, atrincherarte...|¡o las dos cosas!.|Atacar: mantener presionado para lanzar más lejos (dos disparos). +04:48=¿Por qué la gente siempre la toma con los topos?|¡Golpear erizos es aún más divertido!|Un buen mazazo puede reducir en un tercio la|vida de cualquier erizo y enterrarlo completamente.|Atacar: activar. +04:49=¡Resucita a tus aliados!|Pero ten cuidado, también resucitarás a tus enemigos.|Atacar: mantener presionado para resucitar lentamente.|Arriba: acelerar resurrección. +04:50=¿Alguien está oculto bajo tierra?|¡Desentiérralos con un bombardeo perforador!|El temporizador controla la profundidad a alcanzar. +04:51=¿Qué hay más barato que el barro?|Un tiro gratis gracias a la bola de barro.|Hará que el enemigo salga volando|y escuece un poco si te entra en los ojos. +04:52=SIN USAR +04:53=Vive una trepidante aventura a través del|espacio y el tiempo mientras tus compañeros|siguen luchando en tu lugar.|Estate preparado para volver en cualquier momento,|o al llegar la Muerte súbita si te has quedado solo.|Aviso: no funciona durante la Muerte súbita,|si estás solo o si eres el rey. +04:54=Esparce un chorro de pegajoso barro.|Construye puentes, entierra enemigos o cierra túneles.|¡Ten especial cuidado de no mancharte! +04:55=Retorna a la Edad de Hielo!|Congela los erizos y vuelve el piso mas resbaloso|Salvate congelando el agua.|Ataque: activa o desactiva rayo congelador |Arriba o abajo: Continua apuntando +04:56=Puedes tirar 2 cuchillos al enemigo y bloques|pazadisos y tuneles en vez usarlos|Escalarlos! Su daño incrementa con su velocidad.|Pero se cauteloso, jugar con cuchillos es peligroso.|Attaque: Presiona para disparar con mas poder. +04:57=Construye una banda elastica muy resistente, desde donde|los erizos y otros rebotan|sin tomar daño de caida.|Izquierda/Derecha: cambiar la orientacion de la banda|Cursor: Coloca la banda en una posicion adecuada. +04:58=Este bomba de promixidad flota en el aire y sigue|los erizos que se acerquen a esto.|Su explosion es mas debil que una mina.|Ataque: Presiona para obtener mas poder. +04:59=Esta arma no esta terminada y es experimental.|Usar a su propio riesgo! +04:60=Libera una lluvia de balas encima de tu amigo!|Y pensaban que estaban a salvo|Detras de una capa triple de vigas.|Ataque: Dispara a poder maximo|Arriba/Abajo: Continua disparando. +; Game goal strings +05:00=Modos de juego +05:01=Las siguientes reglas están activas: +05:02=Posicionar el rey: elige un buen cobijo para tu rey +05:03=Baja gravedad: mira bien dónde pisas +05:04=Invulnerabilidad: todos los erizos tienen un campo de fuerza personal que los protege +05:05=Vampirismo: dañar a tus enemigos te curará a ti +05:06=Karma: compartirás parte del daño que inflijas +05:07=Rey: ¡no permitas que tu rey muera! +05:08=Posicionar erizos: los jugadores posicionan a mano su erizos por turnos antes de empezar a jugar +05:09=Artillería: afina tu puntería, los erizos no pueden moverse +05:10=Terreno indestructible: la mayoría de armas no pueden dañar el terreno de juego +05:11=Munición compartida: los equipos del mismo color comparten la munición +05:12=Minas: las minas detonarán a cabo de %1 segundo(s) +05:13=Minas: las minas detonarán al instante +05:14=Minas: las minas detonarán aleatoriamente al cabo de 0 - 3 segundos +05:15=Modificador al daño: las armas harán un %1% de su daño habitual +05:16=La salud de todos los erizos se restaura al final de cada turno +05:17=La computadora resucita al morir +05:18=Sin límite de ataques por turno +05:19=El arsenal se restaura al final de cada turno +05:20=Los erizos no comparten arsenal +05:21=Tag Team: los equipos del mismo clan se van turnando entre ellos.|Turno compartido: los equipos del mismo clan comparten la duración del turno. +05:22=Viento pesado: el viento afecta casi todo. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/fi.txt --- a/share/hedgewars/Data/Locale/fi.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/fi.txt Sun Mar 24 14:33:57 2024 -0400 @@ -299,7 +299,7 @@ 02:07=Saatat tarvita tätä 02:07=Vasara ja nauloja -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 on niin tylsä... 02:08=%1:ää ei voisi vähempää kiinnostaa 02:08=%1 on laiska siili @@ -337,7 +337,7 @@ 02:08=%1 on kangistunut pelosta 02:08=%1 on nukahtanut -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1:n pitäisi harjoitella tähtäämistä 02:09=%1 vihaa itseään 02:09=%1 on väärällä puolella! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/fr.lua --- a/share/hedgewars/Data/Locale/fr.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/fr.lua Sun Mar 24 14:33:57 2024 -0400 @@ -685,11 +685,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 ["Flesh for Brainz"] = "Flesh for Brainz", +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1553,6 +1555,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2864,6 +2867,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory ["You have killed an innocent hedgehog!"] = "Tu as tué un innocent !", -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/fr.txt --- a/share/hedgewars/Data/Locale/fr.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/fr.txt Sun Mar 24 14:33:57 2024 -0400 @@ -281,7 +281,7 @@ 02:07=Bob le bricoleur sait être généreux ! 02:07=Le moment donné par le hasard vaut mieux que le moment choisi ! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 est une lopette... 02:08=%1 est trooooop rasant... 02:08=%1 est un hérisson flemmard @@ -323,7 +323,7 @@ 02:08=Ne crains pas d'avancer lentement, crains seulement de t'arrêter %1 02:08=Patience ! Avec le temps, l'herbe devient du lait -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 devrait apprendre à viser ! 02:09=%1 s'en veut 02:09=%1 joue contre son camp ! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/gd.txt --- a/share/hedgewars/Data/Locale/gd.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/gd.txt Sun Mar 24 14:33:57 2024 -0400 @@ -435,7 +435,7 @@ 02:07=Cruinnich na h-acainnean uile! 02:07=Seo acainnean dhut! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=Abair thusa gu bheil %1 ràsanach… 02:08=Tha %1 coma 02:08=Tha %1 ’na ghraineag leisg @@ -473,7 +473,7 @@ 02:08=Tha %1 chun a bhith gòrach leis an eagal 02:08=Thuit %1 ’na chadal -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=Chan eil amas math aig %1! 02:09=Tha coltas gur lugha air %1 e fhèin 02:09=Sheas %1 air an taobh chearr! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/gl.txt --- a/share/hedgewars/Data/Locale/gl.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/gl.txt Sun Mar 24 14:33:57 2024 -0400 @@ -188,7 +188,7 @@ 02:07=Sexa o que sexa, seguro que lle sacamos partido! 02:07=Ferramentas?! Queremos armas! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 case mexa por riba! 02:08=%1 está feito un preguiceiro... 02:08=%1 non dá diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_bg.ts --- a/share/hedgewars/Data/Locale/hedgewars_bg.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_bg.ts Sun Mar 24 14:33:57 2024 -0400 @@ -340,6 +340,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -544,12 +545,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -615,6 +610,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1391,7 +1393,7 @@ Save - Запазване + Запазване (%1 %2) @@ -1442,6 +1444,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2093,6 +2103,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2853,6 +2867,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3753,10 +3775,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3764,6 +3782,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4648,6 +4670,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_cs.ts --- a/share/hedgewars/Data/Locale/hedgewars_cs.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_cs.ts Sun Mar 24 14:33:57 2024 -0400 @@ -346,6 +346,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -550,12 +551,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -621,6 +616,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1419,7 +1421,7 @@ Save - Uložit + Uložit (%1 %2) @@ -1476,6 +1478,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2148,6 +2158,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2909,6 +2923,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3810,10 +3832,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3821,6 +3839,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4845,6 +4867,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_da.ts --- a/share/hedgewars/Data/Locale/hedgewars_da.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_da.ts Sun Mar 24 14:33:57 2024 -0400 @@ -344,6 +344,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -548,12 +549,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -619,6 +614,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1413,7 +1415,7 @@ Save - Gem + Gem (%1 %2) @@ -1464,6 +1466,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2135,6 +2145,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2903,6 +2917,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3803,10 +3825,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3814,6 +3832,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4842,6 +4864,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_de.ts --- a/share/hedgewars/Data/Locale/hedgewars_de.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_de.ts Sun Mar 24 14:33:57 2024 -0400 @@ -380,6 +380,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Das Schema »%1« wird nicht unterstützt @@ -598,7 +599,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Dein Spitzname ist nicht registriert. + Dein Spitzname ist nicht registriert. Um Andere von der Benutzung abzuhalten, registrier ihn bitte auf www.hedgewars.org @@ -680,6 +681,16 @@ Internal error: Reply object is invalid. Interner Fehler: Reply-Objekt ist ungültig. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + Dein Spitzname ist nicht registriert. +Um laufenden Spielen wieder beitreten zu können +und Andere von der Benutzung deines Spitznamens +abzuhalten, registriere ihn bitte auf www.hedgewars.org. + HWGame @@ -1604,7 +1615,7 @@ Save - Speichern + Speichern (%1 %2) @@ -1655,6 +1666,14 @@ (%1 Kisten) + + Save demo + Wiederholung speichern + + + Save demo (unavailable because the /lua command was used) + Wiederholung speichern (nicht möglich, da der /lua-Befehl benutzt wurde) + PageInGame @@ -2115,7 +2134,7 @@ Share your opponents pain, share their damage - Teile den Schmerz deines Gegners, teile seinen erleideten Schaden + Teile den Schmerz deines Gegners, teile seinen erlittenen Schaden Your hogs are unable to move, put your artillery skills to the test @@ -2330,6 +2349,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + Durchschnittliche Anzahl der Wachroboter, die auf mittelgroßen Inselkarten platziert werden. Dieser Wert wird für andere Karten skaliert. + PageSelectWeapon @@ -3171,6 +3194,14 @@ Zoom (%) Zoom (%) + + Chat size (%) + Chatgröße (%) + + + Sentry Bots + Wachroboter + QLineEdit @@ -4204,7 +4235,7 @@ precise + switch + toggle hedgehog tags - Genaues Zielen + wechseln + Igelschilder umschalten + Genaues Zielen + wechseln + Igelschilder umschalten high jump (twice) @@ -4214,6 +4245,10 @@ precise + screenshot Genaues Zielen + Bildschirmfoto + + precise + switch + toggle team bars + Genaues Zielen + Wechseln + Teamleisten umschalten + binds (descriptions) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_el.ts --- a/share/hedgewars/Data/Locale/hedgewars_el.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_el.ts Sun Mar 24 14:33:57 2024 -0400 @@ -336,6 +336,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -542,12 +543,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -613,6 +608,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1401,10 +1403,6 @@ Play again - - Save - - (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1454,6 +1452,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2125,6 +2131,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2881,6 +2891,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3777,10 +3795,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3788,6 +3802,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4816,6 +4834,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_en.ts --- a/share/hedgewars/Data/Locale/hedgewars_en.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_en.ts Sun Mar 24 14:33:57 2024 -0400 @@ -344,6 +344,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Scheme ‘%1’ not supported @@ -563,7 +564,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Your nickname is not registered. + Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org @@ -657,6 +658,13 @@ Internal error: Reply object is invalid. Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1515,7 +1523,7 @@ Save - Save + Save (%1 %2) @@ -1566,6 +1574,14 @@ (%1 crates) + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2217,6 +2233,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -3004,6 +3024,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3985,10 +4013,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3996,6 +4020,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -5024,6 +5052,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_es.ts --- a/share/hedgewars/Data/Locale/hedgewars_es.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_es.ts Sun Mar 24 14:33:57 2024 -0400 @@ -14,7 +14,7 @@ Revision %1 (%2) - + Revision %1 (%2) Visit our homepage: %1 @@ -22,7 +22,7 @@ This program is distributed under the %1. - + Este programa es distribuido bajo el 1%. GNU GPL v2 @@ -80,11 +80,11 @@ Credits - + Créditos Other people - + Otras personas %1 (alias %2) @@ -107,11 +107,11 @@ Extended Credits - + Créditos extendidos An extended credits list can be found in the CREDITS text file. - + Una lista de créditos extendidos puede encontrarse en el archivo de texto CREDITS. <a href="https://visualstudio.microsoft.com">VC++</a>: %1 @@ -119,14 +119,14 @@ Unknown Compiler: %1 - + Compilador Desconocido: %1 AbstractPage Go back - + Atrás @@ -137,23 +137,23 @@ Nick - + Apodo IP/Nick - + IP/Apodo Reason - + Razón Duration - + Duración Ok - + Ok Cancel @@ -165,15 +165,15 @@ Warning - + Alerta permanent - + permanente Ban player - + Banear Jugador Please specify an IP address. @@ -188,14 +188,14 @@ DataManager Use Default - + Usar Default FeedbackDialog View - + Ver Cancel @@ -203,15 +203,15 @@ Send Feedback - + Enviar Feedback We are always happy about suggestions, ideas, or bug reports. - + Sugerencias, ideas y reportes de fallos son bienvenidos. Send us feedback! - + Envíe sus comentarios! If you found a bug, you can see if it's already been reported here: @@ -344,6 +344,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -548,12 +549,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -619,6 +614,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1409,7 +1411,7 @@ Save - Guardar + Guardar (%1 %2) @@ -1460,6 +1462,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2131,6 +2141,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2895,6 +2909,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3795,10 +3817,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3806,6 +3824,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4834,6 +4856,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_fi.ts --- a/share/hedgewars/Data/Locale/hedgewars_fi.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_fi.ts Sun Mar 24 14:33:57 2024 -0400 @@ -340,6 +340,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Säännöt '%1' ei ole tuettu @@ -555,7 +556,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Nimimerkkiäsi ei ole rekisteröity. + Nimimerkkiäsi ei ole rekisteröity. Estääksesi muita käyttämästä sitä, voit rekisteröidä sen osoitteessa hedgewars.org @@ -637,6 +638,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1468,7 +1476,7 @@ Save - Tallenna + Tallenna (%1 %2) @@ -1519,6 +1527,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2142,6 +2158,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2924,6 +2944,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3867,10 +3895,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3878,6 +3902,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4902,6 +4930,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_fr.ts --- a/share/hedgewars/Data/Locale/hedgewars_fr.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_fr.ts Sun Mar 24 14:33:57 2024 -0400 @@ -360,6 +360,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Règle %1 incomprise @@ -575,7 +576,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Votre pseudo n'est pas enregistré. + Votre pseudo n'est pas enregistré. Pour éviter que d'autre joueurs l'utilisent, veuillez l'enregistrer sur www.hedgewars.org @@ -657,6 +658,13 @@ Internal error: Reply object is invalid. Erreur interne : L'objet de réponse est invalide. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1493,7 +1501,7 @@ Save - Enregistrer + Enregistrer (%1 %2) @@ -1544,6 +1552,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2220,6 +2236,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -3052,6 +3072,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -4045,10 +4073,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -4056,6 +4080,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -5084,6 +5112,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_gd.ts --- a/share/hedgewars/Data/Locale/hedgewars_gd.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_gd.ts Sun Mar 24 14:33:57 2024 -0400 @@ -352,6 +352,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Cha chuir sinn taic ris ann sgeama “%1” @@ -571,7 +572,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Chan eil d’ fhar-ainm clàraichte. + Chan eil d’ fhar-ainm clàraichte. ’S urrainn dhut a chlàradh air www.hedgewars.org ach nach cleachd duine eile e. @@ -665,6 +666,13 @@ Internal error: Reply object is invalid. Mearachd taobh a-staigh: Chan eil oibseact na freagairt dligheach. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1535,7 +1543,7 @@ Save - Sàbhail + Sàbhail (%1 %2) @@ -1598,6 +1606,14 @@ (%1 creat) + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2227,6 +2243,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2996,6 +3016,14 @@ Zoom (%) Sùm (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3961,7 +3989,7 @@ precise + switch + toggle hedgehog tags - amas pongail + dèan suidse + toglaich thagaichean gràineige + amas pongail + dèan suidse + toglaich thagaichean gràineige high jump (twice) @@ -3971,6 +3999,10 @@ precise + screenshot amas pongail + glacadh-sgrìn + + precise + switch + toggle team bars + + binds (descriptions) @@ -4995,6 +5027,10 @@ Project founder Stèidheadair a’ phròiseict + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_gl.ts --- a/share/hedgewars/Data/Locale/hedgewars_gl.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_gl.ts Sun Mar 24 14:33:57 2024 -0400 @@ -336,6 +336,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -536,12 +537,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -607,6 +602,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1381,10 +1383,6 @@ Play again - - Save - - (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1434,6 +1432,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2093,6 +2099,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2825,6 +2835,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3720,10 +3738,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3731,6 +3745,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4759,6 +4777,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_hu.ts --- a/share/hedgewars/Data/Locale/hedgewars_hu.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_hu.ts Sun Mar 24 14:33:57 2024 -0400 @@ -1,6 +1,6 @@ - + About @@ -330,6 +330,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” A(z) %1 séma nem támogatott @@ -541,7 +542,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - A beceneved nincs regisztrálva. + A beceneved nincs regisztrálva. Nehogy más is használja, kérjük, regisztráld a www.hedgewars.org címen @@ -615,6 +616,13 @@ Internal error: Reply object is invalid. Belső hiba: Érvénytelen válaszobjektum. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -874,7 +882,7 @@ Connection refused - Kapcsolat visszautasítva + Kapcsolat visszautasítva Room destroyed @@ -882,7 +890,7 @@ Quit reason: - Kilépés oka: + Kilépés oka: You got kicked @@ -1407,7 +1415,7 @@ Save - Mentés + Mentés (%1 %2) @@ -1452,6 +1460,14 @@ (%1 csomag) + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -1891,7 +1907,7 @@ Land can not be destroyed! - A talajt nem lehet elpusztítani! + A talajt nem lehet elpusztítani! Lower gravity @@ -2110,6 +2126,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2851,6 +2871,14 @@ Zoom (%) Nagyítás (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -2886,7 +2914,7 @@ QMessageBox Connection to server is lost - A kapcsolat a szerverrel megszakadt + A kapcsolat a szerverrel megszakadt Error @@ -3160,7 +3188,7 @@ Specify - Beállítás + Beállítás Start @@ -3560,7 +3588,7 @@ pause - szünet + szünet confirmation @@ -3580,7 +3608,7 @@ capture - elfogás + elfogás quit @@ -3596,7 +3624,7 @@ reset zoom - nagyítás visszaállítása + nagyítás visszaállítása long jump @@ -3759,7 +3787,7 @@ precise + switch + toggle hedgehog tags - pontos célzás + váltás + süncímkék kapcsolása + pontos célzás + váltás + süncímkék kapcsolása high jump (twice) @@ -3769,6 +3797,10 @@ precise + screenshot pontos célzás + képernyőkép készítése + + precise + switch + toggle team bars + + binds (descriptions) @@ -3810,7 +3842,7 @@ Talk to your team or all participants: - Beszélgetés a csapattal vagy minden résztvevővel: + Beszélgetés a csapattal vagy minden résztvevővel: Pause, continue or leave your game: @@ -3830,7 +3862,7 @@ Toggle labels above hedgehogs: - Sünik feletti címkék beállítása: + Sünik feletti címkék beállítása: Record video: @@ -3861,31 +3893,31 @@ binds (keys) Axis - Tengely + Tengely (Up) - (Fel) + (Fel) (Down) - (Le) + (Le) Hat - Fejfedő + Fejfedő (Left) - (Balra) + (Balra) (Right) - (Jobbra) + (Jobbra) Button - Gomb + Gomb Keyboard @@ -3945,71 +3977,71 @@ Numpad 0 - Numerikus 0 + Numerikus 0 Numpad 1 - Numerikus 1 + Numerikus 1 Numpad 2 - Numerikus 2 + Numerikus 2 Numpad 3 - Numerikus 3 + Numerikus 3 Numpad 4 - Numerikus 4 + Numerikus 4 Numpad 5 - Numerikus 5 + Numerikus 5 Numpad 6 - Numerikus 6 + Numerikus 6 Numpad 7 - Numerikus 7 + Numerikus 7 Numpad 8 - Numerikus 8 + Numerikus 8 Numpad 9 - Numerikus 9 + Numerikus 9 Numpad . - Numerikus . + Numerikus . Numpad / - Numerikus / + Numerikus / Numpad * - Numerikus * + Numerikus * Numpad - - Numerikus - + Numerikus - Numpad + - Numerikus + + Numerikus + Enter - Enter + Enter Equals - Egyenlő + Egyenlő Up @@ -4041,55 +4073,55 @@ Page up - Page Up + Page Up Page down - Page Down + Page Down Num lock - Num Lock + Num Lock Caps lock - Caps Lock + Caps Lock Scroll lock - Scroll Lock + Scroll Lock Right shift - Jobb oldali Shift + Jobb oldali Shift Left shift - Bal oldali Shift + Bal oldali Shift Right ctrl - Jobb oldali Ctrl + Jobb oldali Ctrl Left ctrl - Bal oldali Ctrl + Bal oldali Ctrl Right alt - Jobb oldali Alt + Jobb oldali Alt Left alt - Bal oldali Alt + Bal oldali Alt Right meta - Jobb oldali Meta + Jobb oldali Meta Left meta - Bal oldali Meta + Bal oldali Meta A button @@ -4173,7 +4205,7 @@ DPad - DPad + DPad D-pad @@ -4797,6 +4829,10 @@ Project founder Projektalapító + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_it.ts --- a/share/hedgewars/Data/Locale/hedgewars_it.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_it.ts Sun Mar 24 14:33:57 2024 -0400 @@ -348,6 +348,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Schema '%1' non supportato @@ -563,7 +564,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Il tuo nome non è registrato. + Il tuo nome non è registrato. Per evitare che qualcun altro lo usi, per favore registralo su www.hedgewars.org @@ -645,6 +646,13 @@ Internal error: Reply object is invalid. Errore interno: Oggetto di risposta non valido. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1495,7 +1503,7 @@ Save - Salva + Salva (%1 %2) @@ -1546,6 +1554,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2185,6 +2201,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2983,6 +3003,14 @@ Zoom (%) Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3940,7 +3968,7 @@ precise + switch + toggle hedgehog tags - Shift + cambia riccio + attiva tags del riccio + Shift + cambia riccio + attiva tags del riccio high jump (twice) @@ -3950,6 +3978,10 @@ precise + screenshot Shift + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4974,6 +5006,10 @@ Project founder Fondatore del progetto + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_ja.ts --- a/share/hedgewars/Data/Locale/hedgewars_ja.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_ja.ts Sun Mar 24 14:33:57 2024 -0400 @@ -326,6 +326,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” スキーム「%1」はサポートされていません @@ -525,7 +526,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - 指定されたニックネームは登録されていません。 + 指定されたニックネームは登録されていません。 他のプレーヤーからの使用を防ぐためには, 「www.hedgewars.org」をアクセスして登録してください。 @@ -615,6 +616,13 @@ Internal error: Reply object is invalid. 内部エラー:返事オブジェクトは無効です。 + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1394,7 +1402,7 @@ Save - セーブ + セーブ (%1 %2) @@ -1439,6 +1447,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2062,6 +2078,10 @@ %1 (%2) %1(%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2785,6 +2805,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3674,10 +3702,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3685,6 +3709,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4705,6 +4733,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_ko.ts --- a/share/hedgewars/Data/Locale/hedgewars_ko.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_ko.ts Sun Mar 24 14:33:57 2024 -0400 @@ -326,6 +326,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -526,12 +527,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -597,6 +592,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1346,10 +1348,6 @@ Play again - - Save - - (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1393,6 +1391,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2011,6 +2017,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2730,6 +2740,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3580,10 +3598,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3591,6 +3605,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4455,6 +4473,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_lt.ts --- a/share/hedgewars/Data/Locale/hedgewars_lt.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_lt.ts Sun Mar 24 14:33:57 2024 -0400 @@ -330,22 +330,22 @@ GameSchemeModel - + New - + New (%1) - + Copy of %1 - + Copy of %1 (%2) @@ -353,7 +353,7 @@ GameUIConfig - + Guest @@ -411,8 +411,9 @@ - + Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -505,82 +506,82 @@ HWChatWidget - + Chat log - + Enter chat messages here and send them with [Enter] - + List of players - + %1 has joined - + %1 has left - + %1 has left (%2) - + %1 has been removed from your ignore list - + %1 has been added to your ignore list - + %1 has been removed from your friends list - + %1 has been added to your friends list + + Stylesheet imported from %1 + + + - Stylesheet imported from %1 - - - - Enter %1 if you want to use the current StyleSheet in future, enter %2 to reset! - + Couldn't read %1 - + StyleSheet discarded - + StyleSheet saved to %1 - + Failed to save StyleSheet to %1 @@ -588,56 +589,64 @@ HWForm - + Game aborted - + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + + + Nickname - - + + No nickname supplied. - + Someone already uses your nickname %1 on the server. Please pick another nickname: - + Team 1 - + %1's Team - + Team %1 Default team name - + Computer %1 Default computer team name - + Hedgewars - Nick registered - + This nick is registered, and you haven't specified a password. If this nick isn't yours, please register your own nick at www.hedgewars.org @@ -646,102 +655,95 @@ - - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - - + Your password wasn't saved either. - - + + Hedgewars - Empty nickname - + Hedgewars - Wrong password - + You entered a wrong password. - + Room password - + The room is protected with password. Please, enter the password: - + Try Again - + Hedgewars - Connection error - + You reconnected too fast. Please wait a few seconds and try again. - - + + Cannot save record to file %1 - + Hedgewars Demo File File Types - + Hedgewars Save File File Types - + Demo name - + Demo name: - + Unknown network error (possibly missing SSL library). - + This feature requires an Internet connection, but you don't appear to be online (error code: %1). - + Internal error: Reply object is invalid. @@ -749,7 +751,7 @@ HWGame - + A fatal ERROR occured! The game engine had to stop. We are very sorry for the inconvenience. :-( @@ -761,14 +763,14 @@ - + en.txt IMPORTANT: This text has a special meaning, do not translate it directly. This is the file name of translation files for the game engine, found in Data/Locale/. Usually, you replace “en” with the ISO-639-1 language code of your language. lt.txt - + Cannot open demofile %1 @@ -1110,7 +1112,7 @@ - + Reason: @@ -1346,17 +1348,17 @@ - + Refresh - + Add - + Remove @@ -1564,43 +1566,43 @@ - + Randomize the team name - + Randomize the grave - + Randomize the flag - + Play a random example of this voice - + Randomize the voice - + Randomize the fort - + CPU %1 Name of a flag for computer-controlled enemies. %1 is replaced with the computer level - + %1 (%2) @@ -1608,33 +1610,38 @@ PageGameStats - + Details - - + + Health graph - + Ranking - + Play again - - Save + + Save demo + + + + + Save demo (unavailable because the /lua command was used) - + The best shot award was won by <b>%1</b> with <b>%2</b> pts. @@ -1643,7 +1650,7 @@ - + The best killer is <b>%1</b> with <b>%2</b> kills in a turn. @@ -1652,7 +1659,7 @@ - + A total of <b>%1</b> hedgehog(s) were killed during this round. @@ -1661,7 +1668,7 @@ - + (%1 kill) Number of kills in stats screen, written after the team name @@ -1671,7 +1678,7 @@ - + (%1 point(s)) Number of points in stats screen, written after the team name @@ -1681,8 +1688,8 @@ - - + + (%L1 second(s)) Time in seconds @@ -1692,7 +1699,7 @@ - + (%1 crate(s)) @@ -1701,7 +1708,7 @@ - + (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1711,7 +1718,7 @@ - + <b>%1</b> thought it's good to shoot their own hedgehogs for <b>%2</b> pts. @@ -1720,7 +1727,7 @@ - + <b>%1</b> killed <b>%2</b> of their own hedgehogs. @@ -1729,7 +1736,7 @@ - + <b>%1</b> was scared and skipped turn <b>%2</b> times. @@ -1738,7 +1745,7 @@ - + With everyone having the same clan color, there was no reason to fight. And so the hedgehogs happily lived in peace ever after. @@ -1798,6 +1805,7 @@ + Feedback @@ -2054,125 +2062,125 @@ - - + + x Multiplication sign, to be used between two numbers. Note the “x” is only a dummy character, we recommend to use “×” if your language permits it - + Frontend - + Custom colors - + Reset to default colors - + Game audio - + Frontend audio - + Account - + Proxy settings - + Proxy host - + Proxy port - + Proxy login - + Proxy password - + No proxy - + System proxy settings - + Socks5 proxy - + HTTP proxy - + Miscellaneous - + MISSING LANGUAGE NAME [%1] In the case of an error, this is shown in the language selection for a language with unknown name. %1 = language code - - Updates - - - + Updates + + + + Check for updates - - Check now - - - + Check now + + + + Video recording options - + Can't delete last team - + You can't delete the last team! @@ -2452,61 +2460,66 @@ - Affects the left and right boundaries of the map + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. - Time you get after an attack + Affects the left and right boundaries of the map + Time you get after an attack + + + + Additional parameter to configure game styles. The meaning depends on the used style, refer to the documentation. When in doubt, leave it empty. - + None (Default) - + Wrap (World wraps) - + Bounce (Edges reflect) - + Sea (Edges connect to sea) - + Name of this scheme - + Copy - + New - + Delete - + %1 (%2) @@ -2570,12 +2583,12 @@ PageTraining - + Pick the training to play - + Pick the challenge to play @@ -2585,66 +2598,66 @@ - + Trainings - - Challenges - - - + Challenges + + + + Scenarios - + Team - - + + Start fighting - + No description available - + Team highscore: %1 Highest score of a team - + Team lowscore: %1 Lowest score of a team - + Team's top accuracy: %1% Best accuracy of a team (in a challenge) - + Team's best time: %L1 s - + Team's longest time: %L1 s - + Select a mission! @@ -2662,7 +2675,7 @@ - + %1 bytes @@ -2671,34 +2684,34 @@ - + %1% Video encoding progress. %1 = number - + (in progress...) - + Date: %1 - + Size: %1 - + %1 (%2%) - %3 Video encoding list entry. %1 = file name, %2 = percent complete, %3 = video operation type (e.g. “encoding”) - + encoding @@ -2706,49 +2719,49 @@ QAction - + Info - + Kick - + Ban - + Delegate room control - + Follow - - + + Ignore - - + + Add friend - + Unignore - + Remove friend @@ -2791,146 +2804,146 @@ QCheckBox - + Show ammo menu tooltips - + Alternative damage show - - Team - - - - - Enable team tags by default - - - - - Hog - - - - - Enable hedgehog tags by default - - - - - Health - - - - - Enable health tags by default - - - - Translucent + Team + Enable team tags by default + + + + + Hog + + + + + Enable hedgehog tags by default + + + + + Health + + + + + Enable health tags by default + + + + + Translucent + + + + Enable translucent tags by default - + Visual effects - + Enable visual effects such as animated menu transitions and falling stars - - + + Sound - + In-game sound effects - - + + Music - + In-game music - + Dampen when losing focus Checkbox text. If checked, the in-game audio volume is reduced (=dampened) when the game window loses its focus - + Reduce the game audio volume if the game window has lost its focus - + Frontend sound effects - + Frontend music - + Append date and time to record file name - + If enabled, Hedgewars adds the date and time in the form "YYYY-MM-DD_hh-mm" for automatically created demos. - + Check for updates at startup - + Fullscreen - + Show FPS - + Save password - + Record audio - + Use game resolution @@ -2948,117 +2961,117 @@ - + Community - + (System default) + + Disabled + + + + + Stereoscopy creates an illusion of depth when you wear 3D glasses. + + + - Disabled - - - - - Stereoscopy creates an illusion of depth when you wear 3D glasses. + Red/Cyan - Red/Cyan + Cyan/Red - Cyan/Red + Red/Blue - Red/Blue + Blue/Red - Blue/Red + Red/Green - Red/Green - - - - Green/Red + + Side-by-side + + + - Side-by-side - - - - Top-Bottom - + 24 FPS - + 25 FPS - + 30 FPS - + 50 FPS - + 60 FPS + + Red/Cyan grayscale + + + - Red/Cyan grayscale + Cyan/Red grayscale - Cyan/Red grayscale + Red/Blue grayscale - Red/Blue grayscale + Blue/Red grayscale - Blue/Red grayscale + Red/Green grayscale - Red/Green grayscale - - - - Green/Red grayscale @@ -3076,7 +3089,7 @@ - + Fort @@ -3106,7 +3119,7 @@ - + Description @@ -3185,38 +3198,38 @@ - + Locale - + Nickname - + Stereoscopy - + This setting will be effective at next restart. - + Resolution - + Bitrate (Kibit/s) “Kibit/s” is the symbol for 1024 bits per second - + Quality @@ -3236,118 +3249,128 @@ - + Zoom (%) - + + Chat size (%) + + + + Displayed tags above hogs and translucent tags - + Initial sound volume - + FPS limit - + Damage Modifier - + Turn Time - + Initial Health - + Sudden Death Timeout - + Sudden Death Water Rise - + Sudden Death Health Decrease - + % Rope Length - + Crate Drops - + % Health Crates - + Health in Crates - + Mines Time - + Mines - + % Dud Mines - + Barrels + Sentry Bots + + + + % Retreat Time Label of game scheme setting for the time you get after an attack - + Air Mines - + World Edge - + Script parameter - + Scheme Name: @@ -3389,22 +3412,22 @@ - + Format - + Audio codec - + Video codec - + Framerate @@ -3412,22 +3435,22 @@ QLineEdit - + unnamed - + unnamed (%1) - + hedgehog %1 - + anonymous @@ -3448,69 +3471,69 @@ QMessageBox - + Teams - Are you sure? - + Do you really want to delete the team '%1'? - - Teams - Name already taken - - - + Teams - Name already taken + + + + The team name '%1' is already taken, so your team has been renamed to '%2'. - - + + Cannot delete default scheme '%1'! - + Please select a record from the list - + Hedgewars - Nick not registered - + Unable to start server - + The connection to the server is lost. - + Server redirection - + This server supports secure connections on port %1. Would you like to reconnect securely? - + Not all players are ready - + Are you sure you want to start this game? Not all players are ready. @@ -3543,18 +3566,18 @@ - + Hedgewars - Success - + All file associations have been set - + File association failed. @@ -3563,7 +3586,7 @@ - + Error @@ -3621,43 +3644,43 @@ - + Schemes - Warning - + Schemes - Are you sure? - + Do you really want to delete the game scheme '%1'? - + Schemes - Name already taken - + A scheme with the name '%1' already exists. Your scheme has been renamed to '%2'. - - + + Videos - Are you sure? - + Do you really want to delete the video '%1'? - + Do you really want to remove %1 file(s)? @@ -3684,28 +3707,28 @@ - - + + Weapons - Warning - + A weapon scheme with the name '%1' already exists. Changes made to the weapon scheme have been discarded. - + Cannot delete default weapon set '%1'! - + Weapons - Are you sure? - + Do you really want to delete the weapon set '%1'? @@ -3732,7 +3755,7 @@ - + Cannot use the weapon scheme '%1'! @@ -3740,8 +3763,8 @@ QObject - - + + No description available @@ -3760,7 +3783,7 @@ - + Cancel @@ -3807,7 +3830,7 @@ - + Start @@ -3817,7 +3840,7 @@ - + Associate file extensions @@ -3833,8 +3856,8 @@ - - + + Delete @@ -3849,37 +3872,37 @@ - + Set default options - + Restore default coding parameters - + Open videos directory - + Open the video directory in your system - - Play - - - + Play + + + + Play this video - + Delete this video @@ -3887,7 +3910,7 @@ QSpinBox - + Specify the bitrate of recorded videos as a multiple of 1024 bits per second @@ -4023,42 +4046,42 @@ SelWeaponWidget - + Weapon set - + Probabilities - + Ammo in boxes - + Delays - + New - + New (%1) - + Copy of %1 - + Copy of %1 (%2) @@ -4493,7 +4516,7 @@ - precise + switch + toggle hedgehog tags + precise + switch + toggle team bars @@ -5508,71 +5531,76 @@ - Italian + Hungarian - Japanese + Italian - Korean + Japanese - Lithuanian + Korean - Polish + Lithuanian - Portuguese + Polish - Russian + Portuguese - Scottish Gaelic + Russian - Slovak + Scottish Gaelic - Spanish + Slovak - Swedish + Spanish - Ukrainian + Swedish - Special thanks + Ukrainian + Special thanks + + + + Project founder @@ -5616,7 +5644,7 @@ - + Unknown command or invalid parameters. Say '/help' in chat for a list of commands. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_ms.ts --- a/share/hedgewars/Data/Locale/hedgewars_ms.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_ms.ts Sun Mar 24 14:33:57 2024 -0400 @@ -328,22 +328,22 @@ GameSchemeModel - + New - + New (%1) - + Copy of %1 - + Copy of %1 (%2) @@ -351,7 +351,7 @@ GameUIConfig - + Guest @@ -399,8 +399,9 @@ - + Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -493,82 +494,82 @@ HWChatWidget - + Chat log - + Enter chat messages here and send them with [Enter] - + List of players - + %1 has joined - + %1 has left - + %1 has left (%2) - + %1 has been removed from your ignore list - + %1 has been added to your ignore list - + %1 has been removed from your friends list - + %1 has been added to your friends list + + Stylesheet imported from %1 + + + - Stylesheet imported from %1 - - - - Enter %1 if you want to use the current StyleSheet in future, enter %2 to reset! - + Couldn't read %1 - + StyleSheet discarded - + StyleSheet saved to %1 - + Failed to save StyleSheet to %1 @@ -576,39 +577,39 @@ HWForm - + Team 1 - + %1's Team - + Team %1 Default team name - + Computer %1 Default computer team name - + Game aborted - + Hedgewars - Nick registered - + This nick is registered, and you haven't specified a password. If this nick isn't yours, please register your own nick at www.hedgewars.org @@ -617,119 +618,120 @@ - + Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - - +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + + + Your password wasn't saved either. - + Nickname - + Someone already uses your nickname %1 on the server. Please pick another nickname: - - + + No nickname supplied. - - + + Hedgewars - Empty nickname - + Hedgewars - Wrong password - + You entered a wrong password. - + Room password - + The room is protected with password. Please, enter the password: - + Try Again - + Hedgewars - Connection error - + You reconnected too fast. Please wait a few seconds and try again. - - + + Cannot save record to file %1 - + Hedgewars Demo File File Types - + Hedgewars Save File File Types - + Demo name - + Demo name: - + Unknown network error (possibly missing SSL library). - + This feature requires an Internet connection, but you don't appear to be online (error code: %1). - + Internal error: Reply object is invalid. @@ -737,7 +739,7 @@ HWGame - + A fatal ERROR occured! The game engine had to stop. We are very sorry for the inconvenience. :-( @@ -749,14 +751,14 @@ - + en.txt IMPORTANT: This text has a special meaning, do not translate it directly. This is the file name of translation files for the game engine, found in Data/Locale/. Usually, you replace “en” with the ISO-639-1 language code of your language. ms.txt - + Cannot open demofile %1 @@ -1098,7 +1100,7 @@ - + Reason: @@ -1332,17 +1334,17 @@ - + Refresh - + Add - + Remove @@ -1550,43 +1552,43 @@ - + Randomize the team name - + Randomize the grave - + Randomize the flag - + Play a random example of this voice - + Randomize the voice - + Randomize the fort - + CPU %1 Name of a flag for computer-controlled enemies. %1 is replaced with the computer level - + %1 (%2) @@ -1594,54 +1596,59 @@ PageGameStats - + Details - - + + Health graph - + Ranking - + Play again - - Save + + Save demo + + + + + Save demo (unavailable because the /lua command was used) - + The best shot award was won by <b>%1</b> with <b>%2</b> pts. - + The best killer is <b>%1</b> with <b>%2</b> kills in a turn. - + A total of <b>%1</b> hedgehog(s) were killed during this round. - + (%1 kill) Number of kills in stats screen, written after the team name @@ -1649,7 +1656,7 @@ - + (%1 point(s)) Number of points in stats screen, written after the team name @@ -1657,8 +1664,8 @@ - - + + (%L1 second(s)) Time in seconds @@ -1666,14 +1673,14 @@ - + (%1 crate(s)) - + (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1681,28 +1688,28 @@ - + <b>%1</b> thought it's good to shoot their own hedgehogs for <b>%2</b> pts. - + <b>%1</b> killed <b>%2</b> of their own hedgehogs. - + <b>%1</b> was scared and skipped turn <b>%2</b> times. - + With everyone having the same clan color, there was no reason to fight. And so the hedgehogs happily lived in peace ever after. @@ -1762,6 +1769,7 @@ + Feedback @@ -2018,125 +2026,125 @@ - - + + x Multiplication sign, to be used between two numbers. Note the “x” is only a dummy character, we recommend to use “×” if your language permits it - + Frontend - + Custom colors - + Reset to default colors - + Game audio - + Frontend audio - + Account - + Proxy settings - + Proxy host - + Proxy port - + Proxy login - + Proxy password - + No proxy - + System proxy settings - + Socks5 proxy - + HTTP proxy - + Miscellaneous - + MISSING LANGUAGE NAME [%1] In the case of an error, this is shown in the language selection for a language with unknown name. %1 = language code - - Updates - - - + Updates + + + + Check for updates - - Check now - - - + Check now + + + + Video recording options - + Can't delete last team - + You can't delete the last team! @@ -2414,61 +2422,66 @@ - Affects the left and right boundaries of the map + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. - Time you get after an attack + Affects the left and right boundaries of the map + Time you get after an attack + + + + Additional parameter to configure game styles. The meaning depends on the used style, refer to the documentation. When in doubt, leave it empty. - + None (Default) - + Wrap (World wraps) - + Bounce (Edges reflect) - + Sea (Edges connect to sea) - + Name of this scheme - + Copy - + New - + Delete - + %1 (%2) @@ -2532,12 +2545,12 @@ PageTraining - + Pick the training to play - + Pick the challenge to play @@ -2547,66 +2560,66 @@ - + Trainings - - Challenges - - - + Challenges + + + + Scenarios - + Team - - + + Start fighting - + No description available - + Team highscore: %1 Highest score of a team - + Team lowscore: %1 Lowest score of a team - + Team's top accuracy: %1% Best accuracy of a team (in a challenge) - + Team's best time: %L1 s - + Team's longest time: %L1 s - + Select a mission! @@ -2624,41 +2637,41 @@ - + %1 bytes - + %1% Video encoding progress. %1 = number - + (in progress...) - + Date: %1 - + Size: %1 - + %1 (%2%) - %3 Video encoding list entry. %1 = file name, %2 = percent complete, %3 = video operation type (e.g. “encoding”) - + encoding @@ -2681,49 +2694,49 @@ - + Info - + Kick - + Ban - + Delegate room control - + Follow - - + + Ignore - - + + Add friend - + Unignore - + Remove friend @@ -2752,145 +2765,145 @@ QCheckBox - + Save password - + Check for updates at startup - + Fullscreen - + Alternative damage show - + Show FPS - + Show ammo menu tooltips - - Team - - - - - Enable team tags by default - - - - - Hog - - - - - Enable hedgehog tags by default - - - - - Health - - - - - Enable health tags by default - - - - Translucent + Team + Enable team tags by default + + + + + Hog + + + + + Enable hedgehog tags by default + + + + + Health + + + + + Enable health tags by default + + + + + Translucent + + + + Enable translucent tags by default - + Visual effects - + Enable visual effects such as animated menu transitions and falling stars - - + + Sound - + In-game sound effects - - + + Music - + In-game music - + Dampen when losing focus Checkbox text. If checked, the in-game audio volume is reduced (=dampened) when the game window loses its focus - + Reduce the game audio volume if the game window has lost its focus - + Frontend sound effects - + Frontend music - + Append date and time to record file name - + If enabled, Hedgewars adds the date and time in the form "YYYY-MM-DD_hh-mm" for automatically created demos. - + Record audio - + Use game resolution @@ -2908,117 +2921,117 @@ - + Community - + (System default) + + Disabled + + + + + Stereoscopy creates an illusion of depth when you wear 3D glasses. + + + - Disabled - - - - - Stereoscopy creates an illusion of depth when you wear 3D glasses. + Red/Cyan - Red/Cyan + Cyan/Red - Cyan/Red + Red/Blue - Red/Blue + Blue/Red - Blue/Red + Red/Green - Red/Green - - - - Green/Red + + Side-by-side + + + - Side-by-side - - - - Top-Bottom - + 24 FPS - + 25 FPS - + 30 FPS - + 50 FPS - + 60 FPS + + Red/Cyan grayscale + + + - Red/Cyan grayscale + Cyan/Red grayscale - Cyan/Red grayscale + Red/Blue grayscale - Red/Blue grayscale + Blue/Red grayscale - Blue/Red grayscale + Red/Green grayscale - Red/Green grayscale - - - - Green/Red grayscale @@ -3036,7 +3049,7 @@ - + Fort @@ -3061,7 +3074,7 @@ - + Description @@ -3172,38 +3185,38 @@ - + Locale - + Nickname - + Stereoscopy - + This setting will be effective at next restart. - + Resolution - + Bitrate (Kibit/s) “Kibit/s” is the symbol for 1024 bits per second - + Quality @@ -3223,138 +3236,148 @@ - + Zoom (%) - + + Chat size (%) + + + + Displayed tags above hogs and translucent tags - + Initial sound volume - + FPS limit - + Damage Modifier - + Turn Time - + Initial Health - + Sudden Death Timeout - + Sudden Death Water Rise - + Sudden Death Health Decrease - + % Rope Length - + Crate Drops - + % Health Crates - + Health in Crates - + Mines Time - + Mines - + % Dud Mines - + Barrels + Sentry Bots + + + + % Retreat Time Label of game scheme setting for the time you get after an attack - + Air Mines - + World Edge - + Script parameter - + Scheme Name: - + Format - + Audio codec - + Video codec - + Framerate @@ -3372,22 +3395,22 @@ QLineEdit - + unnamed - + unnamed (%1) - + hedgehog %1 - + anonymous @@ -3408,69 +3431,69 @@ QMessageBox - + Teams - Are you sure? - + Do you really want to delete the team '%1'? - - Teams - Name already taken - - - + Teams - Name already taken + + + + The team name '%1' is already taken, so your team has been renamed to '%2'. - - + + Cannot delete default scheme '%1'! - + Please select a record from the list - + Hedgewars - Nick not registered - + Unable to start server - + The connection to the server is lost. - + Server redirection - + This server supports secure connections on port %1. Would you like to reconnect securely? - + Not all players are ready - + Are you sure you want to start this game? Not all players are ready. @@ -3503,18 +3526,18 @@ - + Hedgewars - Success - + All file associations have been set - + File association failed. @@ -3556,43 +3579,43 @@ - + Schemes - Warning - + Schemes - Are you sure? - + Do you really want to delete the game scheme '%1'? - + Schemes - Name already taken - + A scheme with the name '%1' already exists. Your scheme has been renamed to '%2'. - - + + Videos - Are you sure? - + Do you really want to delete the video '%1'? - + Do you really want to remove %1 file(s)? @@ -3621,7 +3644,7 @@ - + Error @@ -3642,28 +3665,28 @@ - - + + Weapons - Warning - + A weapon scheme with the name '%1' already exists. Changes made to the weapon scheme have been discarded. - + Cannot delete default weapon set '%1'! - + Weapons - Are you sure? - + Do you really want to delete the weapon set '%1'? @@ -3690,7 +3713,7 @@ - + Cannot use the weapon scheme '%1'! @@ -3698,8 +3721,8 @@ QObject - - + + No description available @@ -3723,7 +3746,7 @@ - + Cancel @@ -3770,7 +3793,7 @@ - + Start @@ -3780,7 +3803,7 @@ - + Associate file extensions @@ -3796,8 +3819,8 @@ - - + + Delete @@ -3807,37 +3830,37 @@ - + Set default options - + Restore default coding parameters - + Open videos directory - + Open the video directory in your system - - Play - - - + Play + + + + Play this video - + Delete this video @@ -3845,7 +3868,7 @@ QSpinBox - + Specify the bitrate of recorded videos as a multiple of 1024 bits per second @@ -3981,42 +4004,42 @@ SelWeaponWidget - + Weapon set - + Probabilities - + Ammo in boxes - + Delays - + New - + New (%1) - + Copy of %1 - + Copy of %1 (%2) @@ -4451,7 +4474,7 @@ - precise + switch + toggle hedgehog tags + precise + switch + toggle team bars @@ -5466,71 +5489,76 @@ - Italian + Hungarian - Japanese + Italian - Korean + Japanese - Lithuanian + Korean - Polish + Lithuanian - Portuguese + Polish - Russian + Portuguese - Scottish Gaelic + Russian - Slovak + Scottish Gaelic - Spanish + Slovak - Swedish + Spanish - Ukrainian + Swedish - Special thanks + Ukrainian + Special thanks + + + + Project founder @@ -5574,7 +5602,7 @@ - + Unknown command or invalid parameters. Say '/help' in chat for a list of commands. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_nl.ts --- a/share/hedgewars/Data/Locale/hedgewars_nl.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_nl.ts Sun Mar 24 14:33:57 2024 -0400 @@ -332,6 +332,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -532,12 +533,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -603,6 +598,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1358,10 +1360,6 @@ Play again - - Save - - (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1411,6 +1409,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2030,6 +2036,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2750,6 +2760,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3601,10 +3619,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3612,6 +3626,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4476,6 +4494,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_pl.ts --- a/share/hedgewars/Data/Locale/hedgewars_pl.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_pl.ts Sun Mar 24 14:33:57 2024 -0400 @@ -362,6 +362,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Shemat '%1' nie jest wspierany @@ -581,7 +582,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Twój nick nie jest zarejestrowany. + Twój nick nie jest zarejestrowany. By zapobiec używania go przez kogoś innego zarejestruj go na www.hedgewars.org @@ -681,6 +682,13 @@ Twoje hasło nie zostało zapisane. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1576,7 +1584,7 @@ Save - Zapisz + Zapisz (%1 %2) @@ -1633,6 +1641,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2309,6 +2325,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -3148,6 +3168,14 @@ Zoom (%) Przybliżenie (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -4187,7 +4215,7 @@ precise + switch + toggle hedgehog tags - precyzja + zmiana + przełącz tagi jeży + precyzja + zmiana + przełącz tagi jeży high jump (twice) @@ -4197,6 +4225,10 @@ precise + screenshot precyzja + zrzut ekranu + + precise + switch + toggle team bars + + binds (descriptions) @@ -5225,6 +5257,10 @@ Project founder Fundator projektu + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_pt_BR.ts --- a/share/hedgewars/Data/Locale/hedgewars_pt_BR.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_pt_BR.ts Sun Mar 24 14:33:57 2024 -0400 @@ -348,6 +348,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” O esquema "%1" não é suportado @@ -563,7 +564,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Seu apelido não está registrado. + Seu apelido não está registrado. Para evitar de outra pessoa usá-lo, registre-o em www.hedgewars.org @@ -640,6 +641,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1466,7 +1474,7 @@ Save - Salvar + Salvar (%1 %2) @@ -1517,6 +1525,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2192,6 +2208,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -3024,6 +3044,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3995,10 +4023,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -4006,6 +4030,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -5036,6 +5064,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_pt_PT.ts --- a/share/hedgewars/Data/Locale/hedgewars_pt_PT.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_pt_PT.ts Sun Mar 24 14:33:57 2024 -0400 @@ -352,6 +352,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Esquema '%1' não suportado @@ -567,7 +568,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - O teu nome de utilizador não está registado. + O teu nome de utilizador não está registado. De forma a prevenir que alguém o utilize, por favor regista-o em www.hedgewars.org @@ -644,6 +645,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1474,7 +1482,7 @@ Save - Gravar + Gravar (%1 %2) @@ -1525,6 +1533,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2176,6 +2192,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2996,6 +3016,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3999,10 +4027,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -4010,6 +4034,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -5038,6 +5066,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_ro.ts --- a/share/hedgewars/Data/Locale/hedgewars_ro.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_ro.ts Sun Mar 24 14:33:57 2024 -0400 @@ -342,6 +342,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -546,12 +547,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -617,6 +612,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1397,10 +1399,6 @@ Play again - - Save - - (%1 %2) For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” @@ -1456,6 +1454,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2112,6 +2118,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2853,6 +2863,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3750,10 +3768,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3761,6 +3775,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4637,6 +4655,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_ru.ts --- a/share/hedgewars/Data/Locale/hedgewars_ru.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_ru.ts Sun Mar 24 14:33:57 2024 -0400 @@ -354,6 +354,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Схема "%1" не поддерживается @@ -565,7 +566,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Ваше имя пользователя не зарегистрировано. + Ваше имя пользователя не зарегистрировано. Чтобы никто другой не воспользовался им, зарегистрируйте его на www.hedgewars.org @@ -637,6 +638,13 @@ Internal error: Reply object is invalid. Внутренняя ошибка: невалидный ответ + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1446,7 +1454,7 @@ Save - Сохранить + Сохранить (%1 %2) @@ -1475,7 +1483,7 @@ With everyone having the same clan color, there was no reason to fight. And so the hedgehogs happily lived in peace ever after. - Когда все имеют один и тот же цвет клана, нет причины воевать. И так ёжики жили долго и и счастливо в мире и согласии. + Когда все имеют один и тот же цвет союза, нет причины воевать. И так ёжики жили долго и и счастливо в мире и согласии. (%1 point(s)) @@ -1503,6 +1511,14 @@ (%1 ящиков) + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2056,7 +2072,7 @@ Teams in each clan take successive turns sharing their turn time. - Команды в каждом клане будут последовательно получать право хода, имея общее время на ход. + Команды в каждом союзе будут последовательно получать право хода, имея общее время на ход. Add an indestructible border around the terrain @@ -2084,7 +2100,7 @@ Each clan starts in its own part of the terrain. - Каждый клан стартует в своей части карты. + Каждый союз стартует в своей части карты. Overall damage and knockback in percent @@ -2175,6 +2191,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2956,6 +2976,14 @@ Zoom (%) Масштаб (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3745,7 +3773,7 @@ clan chat - чат клана + чат союза unselect weapon @@ -3855,7 +3883,7 @@ precise + switch + toggle hedgehog tags - точность + переключить + вкл ярлыки над ёжиками + точность + переключить + вкл ярлыки над ёжиками high jump (twice) @@ -3865,6 +3893,10 @@ precise + screenshot точность + снимок экрана + + precise + switch + toggle team bars + + binds (descriptions) @@ -3942,7 +3974,7 @@ Talk to your clan or all participants: - Общение с кланом или другими игроками: + Общение с союзом или другими игроками: @@ -4733,6 +4765,10 @@ Project founder Основатель проекта + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_sk.ts --- a/share/hedgewars/Data/Locale/hedgewars_sk.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_sk.ts Sun Mar 24 14:33:57 2024 -0400 @@ -350,6 +350,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Schéma '%1' nie je podporovaná @@ -561,12 +562,6 @@ Heslo: - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -637,6 +632,13 @@ Internal error: Reply object is invalid. Interná chyba: Objekt odpovede nie je platný. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1463,7 +1465,7 @@ Save - Uložiť + Uložiť (%1 %2) @@ -1520,6 +1522,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2192,6 +2202,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -3009,6 +3023,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3990,10 +4012,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -4001,6 +4019,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -5029,6 +5051,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_sv.ts --- a/share/hedgewars/Data/Locale/hedgewars_sv.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_sv.ts Sun Mar 24 14:33:57 2024 -0400 @@ -344,6 +344,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” @@ -548,12 +549,6 @@ - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - Your password wasn't saved either. @@ -619,6 +614,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1409,7 +1411,7 @@ Save - Spara + Spara (%1 %2) @@ -1460,6 +1462,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2131,6 +2141,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2895,6 +2909,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3795,10 +3817,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -3806,6 +3824,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -4834,6 +4856,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_tr_TR.ts --- a/share/hedgewars/Data/Locale/hedgewars_tr_TR.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_tr_TR.ts Sun Mar 24 14:33:57 2024 -0400 @@ -354,6 +354,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” '%1' planı desteklenmiyor @@ -572,7 +573,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Takma adın kayıtlı değil. + Takma adın kayıtlı değil. Başkasının kullanmaması için lütfen, www.hedgewars.org sitesinden kaydet. @@ -645,6 +646,13 @@ Internal error: Reply object is invalid. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1463,7 +1471,7 @@ Save - Kaydet + Kaydet (%1 %2) @@ -1508,6 +1516,14 @@ + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2170,6 +2186,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2997,6 +3017,14 @@ Zoom (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3997,10 +4025,6 @@ - precise + switch + toggle hedgehog tags - - - high jump (twice) @@ -4008,6 +4032,10 @@ precise + screenshot + + precise + switch + toggle team bars + + binds (descriptions) @@ -5036,6 +5064,10 @@ Project founder + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_uk.ts --- a/share/hedgewars/Data/Locale/hedgewars_uk.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_uk.ts Sun Mar 24 14:33:57 2024 -0400 @@ -358,6 +358,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” Схема '%1' не підтримується @@ -573,7 +574,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - Ваш нікнейм не зареєстрований. + Ваш нікнейм не зареєстрований. Щоб ніхто інший ним не користувався, зареєструйте його на www.hedgewars.org @@ -655,6 +656,13 @@ Internal error: Reply object is invalid. Внутрішня помилка: об'єкт відповіді недійсний. + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1506,7 +1514,7 @@ Save - Зберегти + Зберегти (%1 %2) @@ -1563,6 +1571,14 @@ (%1 ящиків) + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2235,6 +2251,10 @@ %1 (%2) %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -3058,6 +3078,14 @@ Zoom (%) Масштаб (%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -4030,7 +4058,7 @@ precise + switch + toggle hedgehog tags - приціл + переключення + перемкнути теги їжаків + приціл + переключення + перемкнути теги їжаків high jump (twice) @@ -4040,6 +4068,10 @@ precise + screenshot приціл + знімок + + precise + switch + toggle team bars + + binds (descriptions) @@ -5060,6 +5092,10 @@ Project founder Засновник проекту + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_zh_CN.ts --- a/share/hedgewars/Data/Locale/hedgewars_zh_CN.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_zh_CN.ts Sun Mar 24 14:33:57 2024 -0400 @@ -2,1154 +2,4416 @@ - About - - - Credits - - - - - Other people - - - - - %1 (alias %2) - - - - - %1 &lt;%2&gt; - Part of credits. %1: Contributor name. %2: E-mail address - - - - - %1: %2 - Part of credits. %1: Description of contribution. %2: Contributor name - - - - - %1: %2 &lt;%3&gt; - Part of credits. %1: Description of contribution. %2: Contributor name. %3: E-mail address - - - - - Hedgewars %1 - %1 contains Hedgewars' version number - 刺猬大作战 %1 - - - - Revision %1 (%2) - - - - - Visit our homepage: %1 - - - - - This program is distributed under the %1. - - - - - GNU GPL v2 - Short for “GNU General Public License version 2” - - - - - Extended Credits - - - - - An extended credits list can be found in the CREDITS text file. - - - - - Dependency versions: - For the version numbers of Hedgewars' software dependencies - - - - - <a href="https://gcc.gnu.org">GCC</a>: %1 - - - - - <a href="https://visualstudio.microsoft.com">VC++</a>: %1 - - - - - Unknown Compiler: %1 - - - - - Unknown Compiler - - - - - <a href="https://www.libsdl.org/">SDL2</a>: %1.%2.%3 - - - - - <a href="https://www.libsdl.org/">SDL2_mixer</a>: %1.%2.%3 - - - - - <a href="https://www.libsdl.org/">SDL2_net</a>: %1.%2.%3 - - - - - <a href="https://www.libsdl.org/">SDL2_image</a>: %1.%2.%3 - - - - - <a href="https://www.libsdl.org/">SDL2_ttf</a>: %1.%2.%3 - - - - - <a href="https://www.qt.io/developers/">Qt</a>: %1 - - - - - <a href="https://libav.org">libavcodec</a>: %1.%2.%3 - - - - - <a href="https://libav.org">libavformat</a>: %1.%2.%3 - - - - - <a href="https://libav.org">libavutil</a>: %1.%2.%3 - - - - - <a href="https://icculus.org/physfs/">PhysFS</a>: %1.%2.%3 - + RoomsListModel + + C + 人数 + + + T + 队伍 + + + Map + 地图 + + + Forts + 堡垒 + + + Owner + 房主 + + + Rules + 规则 + + + Script + 脚本 + + + Random Maze + 随机迷宫 + + + Room Name + 房间名 + + + Hand-drawn + 手绘 + + + In progress + 进行中 + + + Random Map + 随机地图 + + + Weapons + 武器 + + + Random Perlin + 随机 Perlin - AbstractPage - - - Go back - + PageOptions + + x + × + + + Game + 游戏 + + + Audio + 声音 + + + Teams + 队伍 + + + Edit team + 编辑队伍 + + + Check for updates + 检查更新 + + + No proxy + 无代理 + + + Proxy password + 代理密码 + + + HTTP proxy + HTTP 代理 + + + Socks5 proxy + Socks5 代理 + + + You can't edit teams from team selection. Go back to main menu to add, edit or delete teams. + 你不能从队伍选择编辑队伍,回到主菜单来添加、编辑、删除队伍。 + + + Video Recording + 录制视频 + + + Select an action to change what key controls it + 选择一个动作来改变控制它的按键 + + + Network + 网络 + + + Proxy settings + 代理设置 + + + Controls + 控制 + + + Miscellaneous + 杂项 + + + Reset to default + 重设为默认 + + + Edit weapon set + 编辑武器集 + + + Game audio + 游戏声音 + + + Account + 账号 + + + New scheme + 新方案 + + + Reset all binds + 重设所有绑定按键 + + + Edit scheme + 编辑方案 + + + Graphics + 图形 + + + Can't delete last team + 不能删除最后一个队伍 + + + Frontend + 前端 + + + Schemes + 方案 + + + Delete team + 删除队伍 + + + You can't delete the last team! + 你不能删除最后一个队伍! + + + Proxy login + 代理登录 + + + Reset to default colors + 重设为默认颜色 + + + Check now + 现在检查 + + + Advanced + 高级 + + + New weapon set + 新武器集 + + + Updates + 更新 + + + Custom colors + 自定义颜色 + + + New team + 新队伍 + + + Delete scheme + 删除方案 + + + Delete weapon set + 删除武器集 + + + Weapons + 武器 + + + Frontend audio + 前端声音 + + + MISSING LANGUAGE NAME [%1] + 缺少语言名字[%1] + + + System proxy settings + 系统代理设置 + + + Proxy port + 代理端口 + + + Proxy host + 代理 host + + + Video recording options + 录制视频选项 BanDialog - - permanent - - - - IP - IP - - - - Nick - - - - - IP/Nick - - - - - Reason - - - - - Duration - - - - + IP + + Ok - - - - + 确定 + + + Nick + 昵称 + + Cancel - 取消 - - - + 取消 + + + Reason + 理由 + + Ban player - - - - + 封禁玩家 + + + permanent + 永久 + + + Please specify an IP address. + 请指定一个 IP 地址。 + + + Please specify a nickname. + 请指定一个昵称。 + + + Duration + 时长 + + you know why - - - - - Please specify an IP address. - - - - - Please specify a nickname. - - - - + 你知道为什么 + + Warning - + 警告 + + + IP/Nick + IP/昵称 + + + + HWNetServersModel + + IP + IP + + + Port + 端口 + + + Title + 标题 + + + + QPushButton + + OK + 确定 + + + Load + 加载 + + + Play + 播放 + + + Reset + 重置 + + + Start + 开始 + + + Open videos directory + 打开视频目录 + + + Invite your friends to your server in just 1 click! + 一键邀请你的朋友到你的服务器! + + + Start server + 启动服务器 + + + Set default options + 设置默认选项 + + + Click to copy your unique server URL to your clipboard. Send this link to your friends and they will be able to join you. + 点击以复制你的唯一服务器网址到剪贴板,发送这个链接给你的朋友,他们就能加入你。 + + + Start private server + 启动私人服务器 + + + Cancel + 取消 + + + Delete + 删除 + + + Rename + 重命名 + + + Update + 更新 + + + Restore default coding parameters + 还原默认编码参数 + + + More info + 更多信息 + + + Play demo + 播放 demo + + + Associate file extensions + 关联文件扩展名 + + + Connect + 连接 + + + default + 默认 + + + Specify address + 指定地址 + + + Play this video + 播放这个视频 + + + Set the default server port for Hedgewars + 为刺猬战争设置默认服务器端口 + + + Delete this video + 删除这个视频 + + + Open the video directory in your system + 在你的系统打开视频目录 + + + + binds (keys) + + Up + + + + End + End + + + Tab + Tab + + + Down + + + + Home + Home + + + Left + + + + Menu + + + + D-pad + D-pad + + + Clear + Clear + + + Pause + Pause + + + Right + + + + Space + 空格 + + + Right stick (Down) + Right stick (Down) + + + Right stick (Left) + Right stick (Left) + + + Mouse: X1 button + 鼠标: 后退 + + + Left stick (Right) + Left stick (Right) + + + Mouse: Wheel up + 鼠标: 滚轮上 + + + Start button + Start button + + + Keypad Enter + + + + Right trigger + Right trigger + + + Mouse: Wheel down + 鼠标: 滚轮下 + + + Mouse: Middle button + 鼠标: 中键 + + + Delete + Delete + + + Left stick (Up) + Left stick (Up) + + + Escape + Escape + + + Insert + Insert + + + Axis %1 %2 + Axis %1 %2 + + + Left stick (Down) + Left stick (Down) + + + Left stick (Left) + Left stick (Left) + + + PageUp + Page Up + + + Return + 回车键 + + + Numlock + Numlock + + + Right Shift + 右 Shift + + + Right stick + Right stick + + + ScrollLock + Scroll Lock + + + Left Shift + 左 Shift + + + D-pad %1 %2 + D-pad %1 %2 + + + Left stick + Left stick + + + B button + B button + + + A button + A button + + + Y button + Y button + + + X button + X button + + + LB button + LB button + + + RB button + RB button + + + Right stick (Up) + Right stick (Up) + + + (QWERTY) + + + + Back button + Back button + + + Mouse: Right button + 鼠标: 右键 + + + PageDown + Page Down + + + CapsLock + Caps Lock + + + Backspace + Backspace + + + Button %1 + Button %1 + + + Left Alt + 左 Alt + + + Left GUI + + + + Keyboard + 键盘 + + + Keypad 0 + + + + Keypad 1 + + + + Keypad 2 + + + + Keypad 3 + + + + Keypad 4 + + + + Keypad 5 + + + + Keypad 6 + + + + Keypad 7 + + + + Keypad 8 + + + + Keypad 9 + + + + Keypad * + + + + Keypad + + + + + Keypad - + + + + Keypad . + + + + Keypad / + + + + Left Ctrl + 左 Ctrl + + + Right GUI + + + + Right Alt + 右 Alt + + + (Don't use) + (不使用) + + + Left trigger + Left trigger + + + Mouse: X2 button + 鼠标: 前进 + + + Right Ctrl + 右 Ctrl + + + Mouse: Left button + 鼠标: 左键 + + + Right stick (Right) + Right stick (Right) + + + + binds + + up + + + + put + + + + chat + 聊天 + + + down + + + + left + + + + quit + 退出 + + + right + + + + precise aim + 精确瞄准 + + + zoom in + 放大 + + + change timer + 改变定时器 + + + save map as image + 保存地图为图片 + + + set zoom to 100% + 缩放 100% + + + volume down + 音量减 + + + volume up + 音量加 + + + ammo menu + 弹药菜单 + + + change direction without moving + 不移动改变方向 + + + timer 1 sec + 定时 1 秒 + + + timer 5 sec + 定时 5 秒 + + + timer 4 sec + 定时 4 秒 + + + timer 3 sec + 定时 3 秒 + + + timer 2 sec + 定时 2 秒 + + + long jump + 远跳 + + + switch backwards + 向后切换 + + + zoom out + 缩小 + + + attack + 攻击 + + + mute audio + 静音 + + + record + 录制 + + + slot 1 + 槽位 1 + + + slot 2 + 槽位 2 + + + slot 3 + 槽位 3 + + + slot 4 + 槽位 4 + + + slot 5 + 槽位 5 + + + slot 6 + 槽位 6 + + + slot 7 + 槽位 7 + + + slot 8 + 槽位 8 + + + slot 9 + 槽位 9 + + + switch + 切换 + + + clan chat + 战队聊天 + + + stand still on slippery land + 在光滑地面上站稳 + + + change bounciness + 改变弹力 + + + chat history + 聊天历史记录 + + + toggle team bars + 切换队伍栏 + + + slot 10 + 槽位 10 + + + show object information + 显示对象信息 + + + toggle hedgehog tag translucency + 切换刺猬标签透明度 + + + change hedgehog tag types + 改变刺猬标签类型 + + + screenshot + 截图 + + + speed up replay + 加速重播 + + + backwards jump + 后跳 + + + change mode + 改变模式 + + + high jump + 高跳 + + + unselect weapon + 取消选择武器 + + + toggle HUD + 切换 HUD + + + autocam / find hedgehog + 自动镜头/ 查找 刺猬 + + + pause / auto skip + 暂停/自动跳过 + + + reset zoom to start value + 重置缩放到初始值 + + + show mission information + 显示任务信息 + + + toggle hedgehog tags + 切换刺猬标签 + + + confirmation + 确认 + + + + PageVideos + + %1% + %1% + + + Name + 名字 + + + Size + 大小 + + + Size: %1 + 大小: %1 + + + %1 bytes + %1 字节 + + + encoding + 编码 + + + Date: %1 + 日期: %1 + + + (in progress...) + (进行中…) + + + %1 (%2%) - %3 + %1 (%2%)—%3 + + + + PageAdmin + + Add + 添加 + + + Bans + 封禁 + + + Expiration + 到期 + + + Reason + 理由 + + + Remove + 移除 + + + Server message for previous versions: + 给之前版本的服务器消息: + + + Refresh + 刷新 + + + Fetch data + 获取数据 + + + Latest version protocol number: + 最新版本协议编号: + + + MOTD preview: + 今日消息预览: + + + Set data + 设置数据 + + + Server message for latest version: + 给最新版本的服务器消息: + + + General + 一般 + + + IP/Nick + IP/昵称 + + + Clear Accounts Cache + 清除账户缓存 + + + + HWMapContainer + + All + 所有 + + + Edit + 编辑 + + + Load + 加载 + + + Map: + 地图: + + + Seed + 种子 + + + Mission map + 任务地图 + + + Medium tunnels + 中等隧道 + + + Forts + 堡垒 + + + Large + + + + Small + + + + Wacky + 古怪 + + + Mission: + 任务: + + + View and edit the seed, the source of randomness in the game + 查看并编辑种子,游戏中随机性的来源 + + + Click to randomize the theme and seed + 点击以随机主题和种子 + + + Click to edit + 点击以编辑 + + + Small tunnels + 小隧道 + + + Large islands + 大岛屿 + + + Map size: + 地图大小: + + + Map type: + 地图类型: + + + Randomly generated + 随机生成 + + + Cavern + 洞穴 + + + Medium + 中等 + + + Random + 随机 + + + Style: + 风格: + + + Random maze + 随机迷宫 + + + Randomize the theme and seed + 随机主题和种子 + + + Hand-drawn + 手绘 + + + Image map + 图片地图 + + + Maze style: + 迷宫风格: + + + Large tunnels + 大隧道 + + + Randomize the seed + 随机种子 + + + Click to randomize the map, theme and seed + 点击以随机地图、主题和种子 + + + Scale size of the drawn map + 绘画地图的比例大小 + + + Small islands + 小岛屿 + + + Medium islands + 中等岛屿 + + + Randomize the map, theme and seed + 随机地图、主题和种子 + + + Adjust the complexity of the generated map + 调整生成地图的复杂度 + + + Choose a theme + 选择一个主题 + + + Theme: %1 + 主题: %1 + + + Adjust the distance between forts + 调整堡垒之间距离 + + + Load drawn map + 加载绘画地图 + + + Map preview: + 地图预览: + + + Load map drawing + 加载绘画地图 + + + Edit map drawing + 编辑绘画地图 + + + Drawn Maps + 绘画地图 + + + All files + 所有文件 + + + Randomize the theme + 随机主题 + + + Random perlin + 随机 perlin + + + + QAction + + Ban + 封禁 + + + Info + 信息 + + + Kick + 踢出 + + + Follow + 跟随 + + + Unignore + 取消忽略 + + + Ignore + 忽略 + + + Show games in-progress + 显示进行中的游戏 + + + Add friend + 添加朋友 + + + Show password protected + 显示受密码保护的 + + + Restrict Unregistered Players Join + 限制未注册的玩家加入 + + + Restrict Team Additions + 限制队伍添加 + + + Show join restricted + 显示限制加入的 + + + Restrict Joins + 限制加入 + + + Delegate room control + 转授房间控制 + + + Remove friend + 删除朋友 + + + Show games in lobby + 显示在大厅的游戏 - DataManager - - - Use Default - + credits + + Art + + + + Hell + + + + Maps + 地图 + + + Tank + + + + Some Pas2C and GLES2 work + + + + ClimbHome + + + + Beach + + + + Brick + + + + Czech + + + + Forts + 堡垒 + + + Greek + + + + Music + 音乐 + + + Ports + 移植 + + + Ruler + + + + Sheep + + + + Snail + + + + Maze maps + + + + Italian + + + + Special thanks + 特别感谢 + + + Frontend / main menu + 前端/主菜单 + + + Drill rocket, ballgun, RC plane + + + + Lonely_Island + + + + Basketball, BasketballField, Bath, Bubbleflow, Hammock, Hedgelove, Hedgewars, Hydrant, Mushrooms, Plane, Ropes, Tree + + + + macOS/iPhone port, OpenGL-ES conversion + + + + Portal Mind Challenge + + + + Air mine, rubber, others + + + + iPhone/iPad ports + + + + Scottish Gaelic + + + + sdmusic (Hitman [sheepluva edit]) + + + + Bulgarian + + + + Battlefield + + + + Climb Home + + + + Created Capture the Flag, Construction Mode, Control, HedgeEditor, Highlander, Racer, TechRacer, The Specialists, WxW + + + + EarthRise, oriental, Pirate, snow + + + + Cheese + + + + Video recording + + + + Map generation + 地图生成 + + + Hungarian + + + + French + + + + German + + + + Teamwork 2 + + + + Jungle + + + + Korean + + + + Many engine improvements + + + + Keybinds, feedback, maps and hats interfaces + + + + Nature + + + + Polish + + + + Slovak + + + + Sounds + 声音 + + + Sticks + + + + Themes + 主题 + + + Many frontend improvements + + + + Olympic + + + + olympics_sd + + + + Fruit, Cake + + + + See CREDITS text file + + + + Missions and styles + 任务和风格 + + + Game engine + 游戏引擎 + + + Login dialogs, other improvements + + + + Translations + 翻译 + + + portal + + + + CTF_Blizzard + + + + Game server + 游戏服务器 + + + Programming + 编程 + + + City, Rock, others + + + + Japanese + + + + Gamepad and Lua integration + + + + ShoppaKing, TrophyRace + + + + Ukrainian + + + + Graphics + 图形 + + + Theme customization improvements + + + + Perlin maps and other improvements + + + + EvilChicken + + + + Golf, Hoggywood, Stage + + + + Russian + + + + Some styles and missions + + + + Chinese + 中文 + + + A Classic Fairytale + + + + Compost + + + + Spanish + + + + Fruit, Jungle + + + + Creator + + + + Octorama + + + + Default_pl, Russian_pl voices + + + + Swedish + + + + Brazilian Portuguese + + + + Cave, Olympics + + + + Project founder + 项目发起人 + + + Most core weapons + + + + A Space Adventure + + + + Bamboo, EarthRise, BambooPlinko + + + + Battalion + + + + Portuguese + + + + Castle, PirateFlag + + + + WebGL port + + + + Android port + + + + SteelTower + + + + Campaign support + + + + Bamboo, Blox, Cake, Cogs, EarthRise, Freeway + + + + Finnish + + + + Mine number and time game settings + + + + Various authors from www.freesound.org (see CREDITS text file) + + + + Freezer + + + + Weapons + 武器 + + + General + 一般 + + + SB_Bones, SB_Crystal, SB_Grassy, SB_Grove, SB_Haunty, SB_Oaks, SB_Shrooms, SB_Tentacle + + + + Hoggywood + + + + Hats, graves, other + 帽子、墓碑、其他 + + + Core map generators + + + + Android netplay, portability abstraction + + + + Other improvements + + + + Training, time-trial and target practice challenges, Bazooka Battlefield, Tentacle Terror, Big Armory, bugfixes and maintenance + + + + Nature, Snow, City, Castle, Halloween, Island + + + + Continental supplies + + + + Hedgehogs voice + + + + Lithuanian + + + + + PageEditTeam + + Hat + 帽子 + + + Name + 名字 + + + Custom Controls + 自定义控制 + + + Use my default + 使用我的默认 + + + CPU %1 + CPU %1 + + + Random Hats + 随机帽子 + + + Random Team + 随机队伍 + + + Reset all binds + 重置所有绑定按键 + + + %1 (%2) + %1 (%2) + + + Randomize the fort + 随机堡垒 + + + Randomize the flag + 随机旗帜 + + + Select an action to choose a custom key bind for this team + 选择一个动作为这个队伍自定义按键绑定 + + + Random Names + 随机名字 + + + Randomize the team name + 随机队伍名 + + + Play a random example of this voice + 播放一个随机语音示例 + + + This hedgehog's name + 这个刺猬的名字 + + + General + 一般 + + + Randomize this hedgehog's name + 随机这个刺猬的名字 + + + Randomize the grave + 随机墓碑 + + + Randomize the voice + 随机语音 + + + + QCheckBox + + Hog + 刺猬 + + + Team + 队伍 + + + Music + 音乐 + + + Sound + 声音 + + + In-game sound effects + 游戏中音效 + + + Enable hedgehog tags by default + 默认启用刺猬标签 + + + Save password + 保存密码 + + + Enable translucent tags by default + 默认启用透明标签 + + + Show ammo menu tooltips + 显示弹药菜单提示 + + + Alternative damage show + 备选伤害显示 + + + Reduce the game audio volume if the game window has lost its focus + 游戏窗口失去焦点时降低游戏音量 + + + Fullscreen + 全屏 + + + Record audio + 录制音频 + + + Health + 血量 + + + Enable health tags by default + 默认启用血量标签 + + + Frontend sound effects + 前端音效 + + + In-game music + 游戏中音乐 + + + Enable team tags by default + 默认启用队伍标签 + + + Append date and time to record file name + 附加日期和时间到录制文件名 + + + Enable visual effects such as animated menu transitions and falling stars + 启用视觉效果,如动态菜单过渡和掉落星星 + + + Check for updates at startup + 启动时检查更新 + + + If enabled, Hedgewars adds the date and time in the form "YYYY-MM-DD_hh-mm" for automatically created demos. + 如果启用,刺猬战争会添加日期和时间(YYYY-MM-DD_hh-mm)到自动创建的demo。 + + + Translucent + 透明度 + + + Visual effects + 视觉效果 + + + Use game resolution + 使用游戏分辨率 + + + Dampen when losing focus + 抑制当失去焦点时 + + + Frontend music + 前端音乐 + + + Show FPS + 显示帧数 + + + + GameCFGWidget + + Map + 地图 + + + Game scheme will auto-select a weapon + 游戏方案会自动选择一个武器 + + + Game options + 游戏选项 + + + Edit weapons + 编辑武器 + + + Edit schemes + 编辑方案 + + + + GameSchemeModel + + New + 新建 + + + Copy of %1 + %1 的复制 + + + New (%1) + 新建 (%1) + + + Copy of %1 (%2) + %1 的复制 (%2) + + + + PageScheme + + New + 新建 + + + Copy + 复制 + + + Select a hedgehog at the beginning of a turn + 回合开始时选择一个刺猬 + + + How much the water rises per turn while in Sudden Death. Set to 0 along with Sudden Death Health Decrease to disable Sudden Death. + 在突然死亡中的每个回合,水面升起多少。和减少血量一起设为0以禁用突然死亡。 + + + Add an indestructible border along the bottom + 沿底部添加坚不可摧的边框 + + + All (living) hedgehogs are fully restored at the end of turn + 所有(活着的)刺猬在回合结束时完全恢复 + + + Disable land objects when generating random maps. + 生成随机地图时禁用地面物体。 + + + Wind will affect almost everything. + 风力会影响几乎所有东西。 + + + Gain 80% of the damage you do back in health + 获得血量为造成伤害的 80% + + + Order of play is random instead of in room order. + 游玩顺序随机而不是房间顺序。 + + + How much health hedgehogs lose per turn while in Sudden Death, down to 1 health. Set to 0 along with Sudden Death Water Rise to disable Sudden Death. + 在突然死亡中的每个回合,刺猬失去多少血量,直到1点血。和水面升起设为0以禁用突然死亡。 + + + Ammo is shared between all teams that share a colour. + 所有相同颜色的队伍共享弹药。 + + + Weapons are reset to starting values each turn. + 武器在每个回合重置初始值。 + + + Share your opponents pain, share their damage + 分享你的对手的痛苦,分享他们的伤害 + + + Initial health of hedgehogs + 刺猬的初始血量 + + + Likelihood of a mine being a dud. Does not affect mines placed by hedgehogs. + 地雷是哑弹的概率。不影响由刺猬放置的地雷。 + + + Likelihood of a crate dropping before a turn + 回合开始前掉落箱子的概率 + + + Average number of mines to be placed a medium-sized island map. This number will be scaled for other maps. + 放置在中等岛屿地图的地雷平均数。这个数值会被缩放于其他地图。 + + + Land can not be destroyed by most weapons. + 地面不会被多数武器破坏。 + + + Delete + 删除 + + + How many rounds have to be played before Sudden Death begins + 在突然死亡开始前有多少个回合可以玩 + + + Average number of air mines to be placed a medium-sized island map. This number will be scaled for other maps. + 放置在中等岛屿地图的浮空雷平均数。这个数值会被缩放于其他地图。 + + + Bounce (Edges reflect) + 弹力 (边缘反弹) + + + Health bonus for collecting a health crate + 收集一个医疗箱的血量奖励 + + + Maximum rope length in percent + 绳子最大长度(百分比) + + + Assisted aiming with laser sight + 激光辅助瞄准 + + + Turn time in seconds + 回合时间(秒) + + + Take turns placing your hedgehogs before the start of play. + 游戏开始前放置你的刺猬。 + + + Attacking does not end your turn. + 攻击不会终止你的回合。 + + + Additional parameter to configure game styles. The meaning depends on the used style, refer to the documentation. When in doubt, leave it empty. + 额外参数以配置游戏风格。意味着取决于使用的风格,请参阅文档。有疑问时请留空。 + + + Time you get after an attack + 攻击后你得到的时间 + + + Play with a King. If he dies, your side dies. + 和国王一起玩,如果他死了,你这边也死。 + + + %1 (%2) + %1 (%2) + + + Each clan starts in its own part of the terrain. + 每个战队开始在他的所属地形部分。 + + + Average number of barrels to be placed a medium-sized island map. This number will be scaled for other maps. + 放置在中等岛屿地图的油桶平均数。这个数值会被缩放于其他地图。 + + + You will not have to worry about wind anymore. + 你再也不用担心风了。 + + + Detonation timer of mines. The random timer lies between 0 and 5 seconds. The timer of air mines will be a quarter of the mines timer. + 地雷的爆炸定时。随机定时器在0-5秒之间。浮空雷的定时只有地雷定时的四分之一。 + + + Overall damage and knockback in percent + 整体伤害和击退(百分比) + + + Teams in each clan take successive turns sharing their turn time. + 每个战队的队伍在共享的回合时间里可以连续行动。 + + + Sea (Edges connect to sea) + 海 (边缘连接到海) + + + Name of this scheme + 这个方案的名字 + + + Each hedgehog has its own ammo. It does not share with the team. + 每个刺猬有自己的弹药,不和队伍共享。 + + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + 放置在中等岛屿地图的哨兵机器人平均数。这个数值会被缩放于其他地图。 + + + Add an indestructible border around the terrain + 在地形周围添加坚不可摧的边框 + + + AI respawns on death. + AI 死后重生。 + + + None (Default) + 无(默认) + + + Your hogs are unable to move, put your artillery skills to the test + 你的刺猬不能移动,测试你的炮兵技能 + + + Lower gravity + 较低重力 + + + Likelihood of a dropped crate being a health crate. All other crates will be weapon or utility crates. + 空投箱是医疗箱的概率。 + + + Disable girders when generating random maps. + 生成随机地图时禁用大梁。 + + + All hogs have a personal forcefield + 所有刺猬有一个个人力场 + + + Affects the left and right boundaries of the map + 影响地图的左右边界 + + + Wrap (World wraps) + Wrap (边缘互通) + + + + PageSelectWeapon + + New + 新建 + + + Copy + 复制 + + + Delete + 删除 + + + Default + 默认 + + + + SelWeaponWidget + + New + 新建 + + + Delays + 推迟 + + + Weapon set + 武器集 + + + Ammo in boxes + 箱中弹药 + + + Probabilities + 概率 + + + Copy of %1 + %1 的复制 + + + New (%1) + 新建 (%1) + + + Copy of %1 (%2) + %1 的复制 (%2) + + + + server + + map + 地图 + + + kick + 踢出 + + + room + 房间 + + + Access denied. This room is for registered users only. + 访问被拒绝。这个房间仅限注册用户。 + + + Please confirm server restart with '/restart_server yes'. + 请确认服务器以“/restart_server yes”重启。 + + + /vote: Please use 'yes' or 'no'. + /vote: 请使用 ‘yes’ 或 ‘no’. + + + heads + + + + lobby + 大厅 + + + pause + 暂停 + + + tails + + + + Empty config entry. + 空配置条目。 + + + Voting closed. + 投票已关闭。 + + + /delete <config ID>: Delete a votable room configuration + /delete <config ID>: 删除一个可投票的房间配置 + + + Super power activated. + 超级力量已激活。 + + + This command is only available in rooms. + 这个命令只在房间可用。 + + + /rnd: Flip a virtual coin and reply with 'heads' or 'tails' + /rnd: 翻转一个虚拟硬币并回复‘正’ 或 ‘反’ + + + You can't kick yourself! + 你不能踢自己! + + + Protocol already known. + 协议已知。 + + + Warning! Room name change flood protection activated + 警告!房间名改变滥用保护已激活 + + + You can't kick the only other player! + 你不能踢仅剩的其他玩家! + + + Excess flood + 过量滥发 + + + Greeting message cleared. + 问候语已清除。 + + + 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: $()*+?[]^{|} + 非法房间名!房间名必须在1-40字符之间,不得以空格开头或结尾,且不包含这些字符:$()*+?[]^{|} + + + /vote <yes/no>: Vote 'yes' or 'no' for active vote + /vote <yes/no>: 投票 ‘yes’ 或 ‘no’ 以激活投票 + + + /rnd [A] [B] [C] [...]: Reply with a random word from the given list + /rnd [A] [B] [C] [...]: 从给出列表中回复一个随机词语 + + + hedgehogs per team: + 每个队伍的刺猬: + + + This server does not support replays! + 这个服务器不支持重玩! + + + (playing) + (正在玩) + + + /fix: Force this room to stay open when it is empty + /fix: 当这个房间空的时候强制保持开启 + + + Access denied. + 访问被拒绝。 + + + /loadroom <file name>: Load votable room configurations (and greeting) from a file + /loadroom <file name>: 从文件加载可投票的房间配置(和问候) + + + Too many hedgehogs! + 刺猬太多了! + + + Greeting message set. + 设置问候语。 + + + /registered_only: Toggle 'registered only' state. If enabled, only registered players can join server + /registered_only: 切换 ‘registered only’ 状态。如果启用,只有注册玩家能加入服务器 + + + Room version incompatible to your Hedgewars version! + 房间版本跟你的刺猬战争版本不兼容! + + + /greeting [message]: Set or clear greeting message to be shown to players who join the room + /greeting [message]: 设置或清除问候语,显示给加入房间的玩家 + + + 60 seconds cooldown after kick + 踢出后60秒冷却时间 + + + Warning! Chat flood protection activated + 警告!聊天滥发保护已激活 + + + Available callvote commands: hedgehogs <number>, pause, newseed, map <name>, kick <player> + 可用的发起投票命令:hedgehogs <number>, pause, newseed, map <name>, kick <player> + + + There's already a team with same name in the list. + 列表中已有相同名字的队伍。 + + + Voting expired. + 投票已过期。 + + + /unfix: Undo the /fix command + /unfix: 撤销 /fix 命令 + + + /maxteams: specify number from 2 to 8 + /maxteams: 从2到8指定数字 + + + /global <message>: Send global chat message which can be seen by everyone on the server + /global <message>: 发送全局聊天消息,服务器中所有人可见 + + + Joining not possible: Round is in progress. + 无法加入:回合进行中。 + + + You already have voted. + 你已投票。 + + + Reconnected too fast + 重连太快 + + + Kicked + 已踢 + + + Your vote has been counted. + 你的投票已计数。 + + + /callvote map: No maps available. + /callvote map: 无可用地图。 + + + /watch <id>: Watch a demo stored on the server with the given ID + /watch <id>: 观看存储在服务器上给定ID的demo + + + You're not the room master! + 你不是房主! + + + /callvote map: No such map! + /callvote map: 没有这个地图! + + + Authentication failed + 验证失败 + + + /saveroom <file name>: Save all votable room configurations (and the greeting) of this room into a file + /saveroom <file name>: 保存这个房间所有可投票的房间配置(和问候)到文件 + + + Bad number. + 错误的数字。 + + + /maxteams <N>: Limit maximum number of teams to N + /maxteams <N>: 限制最大队伍数为N + + + Warning! Joins flood protection activated + 警告!加入滥用保护已激活 + + + Error: The team you tried to remove does not exist. + 错误:你试图删除的队伍不存在。 + + + This server no longer allows unregistered players to join. + 这个服务器不再允许未注册玩家加入。 + + + A room with the same name already exists. + 已存在相同名字的房间。 + + + Too many teams! + 队伍太多了! + + + kicked + 已踢 + + + /quit: Quit the server + /quit: 退出服务器 + + + Unknown command or invalid parameters. Say '/help' in chat for a list of commands. + 未知命令或无效参数。 在聊天说‘/help’查看命令列表。 + + + Game messages flood detected - 1 + 检测到游戏消息滥发 - 1 + + + (spectating) + (正在旁观) + + + /help: Show chat command help + /help: 显示聊天命令帮助 + + + /callvote kick: No such user! + /callvote kick: 没有这个用户! + + + 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: $()*+?[]^{|} + 非法房间名!房间名必须在1-40字符之间,不得以空格开头或结尾,且不包含这些字符:$()*+?[]^{|} + + + New voting started + 已开始新的投票 + + + No checker rights + 无checker权限 + + + There's no voting going on. + 没有进行中的投票。 + + + You're already the room master. + 你已经是房主。 + + + Nickname already provided. + 昵称已提供。 + + + Commands for server admins only: + 仅限服务器管理员的命令: + + + /save <config ID> <config name>: Add current room configuration as votable choice for /callvote map + /save <config ID> <config name>: 为 /callvote map 添加当前房间配置作为可投票选择 + + + /callvote kick: You need to specify a nickname. + /callvote kick:你需要指定一个昵称。 + + + Player is not online. + 玩家不在线。 + + + You are banned from this room. + 你被房间封禁。 + + + You're not the room master or a server admin! + 你不是房主或服务器管理员! + + + This command is only available in the lobby. + 这个命令仅在大厅可用。 + + + This server only allows registered users to join. + 这个服务器只允许注册用户加入。 + + + 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: $()*+?[]^{|} + 非法昵称!昵称必须在1-40字符之间,不得以空格开头或结尾,且不包含这些字符:$()*+?[]^{|} + + + /callvote [arguments]: Start a vote + /callvote [arguments]: 开始一个投票 + + + You can't remove a team you don't own. + 你不能移除你不拥有的队伍。 + + + Access denied. This room currently doesn't allow joining. + 访问被拒绝。这个房间当前不允许加入。 + + + new seed + 新种子 + + + /stats: Query server stats + /stats: 查询服务器状态 + + + This player is protected from being kicked. + 这个玩家受保护不能踢。 + + + Pause toggled. + 已切换暂停。 + + + /super_power: Activate your super power. With it you can enter any room and are protected from kicking. Expires when you leave server + /super_power: 激活你的超级力量。这样你可以进入任何房间且不能踢出。当你离开服务器就过期 + + + /force <yes/no>: Force vote result for active vote + /force <yes/no>: 为激活的投票强制投票结果 + + + /delegate <player>: Surrender room control to player + /delegate <player>: 转移房间控制给玩家 + + + /force: Please use 'yes' or 'no'. + /force: 请使用‘yes’ 或 ‘no’. + + + You're the new room master! + 你是新的房主! + + + /callvote pause: No game in progress! + /callvote pause: 没有进行中的游戏! + + + /me <message>: Chat action, e.g. '/me eats pizza' becomes '* Player eats pizza' + /me <message>: 聊天动作,如‘/me eats pizza’变成‘* Player eats pizza’ + + + Nickname is already in use + 昵称已使用 + + + The player is not in your room. + 这个玩家不在你的房间。 + + + This server now allows unregistered players to join. + 这个服务器现在允许未注册玩家加入。 + + + /callvote hedgehogs: Specify number from 1 to 8. + /callvote hedgehogs: 从1到8指定数字。 + + + /info <player>: Show info about player + /info <player>: 显示关于玩家的信息 + + + /callvote kick: This is only allowed in rooms without a room master. + /callvote kick: 仅允许没有房主的房间。 + + + List of lobby chat commands: + 大厅聊天命令列表: + + + Corrupted hedgehogs info! + 损坏的刺猬信息! + + + No such room. + 没有这个房间。 + + + This room currently does not allow adding new teams. + 这个房间当前不允许添加新队伍。 + + + Ping timeout + Ping 超时 + + + The game can't be started with less than two clans! + 游戏少于两个战队不能开始! + + + List of room chat commands: + 房间聊天命令列表: + + + + QLabel + + Flag + 旗帜 + + + Name + 名字 + + + There are videos that are currently being processed. +Exiting now will abort them. +Do you really want to quit? + 当前正在处理视频。 +现在退出会中止它们。 +你真的要退出? + + + Grave + 墓碑 + + + Host: + 主机: + + + Mines + 地雷 + + + Port: + 端口: + + + Style + 风格 + + + Voice + 语音 + + + Send system information + 发送系统信息 + + + This development build is 'work in progress' and may not be compatible with other versions of the game, while some features might be broken or incomplete! + 这个开发版还没完成,可能和其他版本的游戏不兼容,有些功能可能损坏或未实现! + + + Audio codec + 音频编码 + + + Video codec + 视频编码 + + + Windowed Resolution + 窗口分辨率 + + + Initial Health + 初始血量 + + + Server port: + 服务器端口: + + + Server name: + 服务器名称: + + + World Edge + 世界边缘 + + + Scheme Name: + 方案名: + + + Fullscreen + 全屏 + + + Your Email + 你的邮箱 + + + Air Mines + 浮空雷 + + + % Rope Length + % 绳子长度 + + + Format + 格式 + + + Locale + 语言 + + + Player + 玩家 + + + FPS limit + 帧数限制 + + + Initial sound volume + 初始音量 + + + Scheme + 方案 + + + This setting will be effective at next restart. + 这个设置会在下次启动时生效。 + + + Resolution + 分辨率 + + + Zoom (%) + 缩放 (%) + + + % Dud Mines + % 哑弹地雷 + + + Damage Modifier + 伤害修改器 + + + Crate Drops + 箱子掉落 + + + Health in Crates + 医疗箱回血 + + + Type the security code: + 输入安全码: + + + Bitrate (Kibit/s) + 比特率(Kibit/s) + + + Script parameter + 脚本参数 + + + Framerate + 帧率 + + + Barrels + 油桶 + + + Quality + 质量 + + + Loading<br>CAPTCHA ... + 正在加载<br>验证码… + + + Turn Time + 回合时间 + + + Description + 描述 + + + Sentry Bots + 哨兵机器人 + + + % Health Crates + % 医疗箱 + + + Sudden Death Water Rise + 突然死亡水面上升 + + + Summary + 总结 + + + Chat size (%) + 聊天大小 (%) + + + Tip: %1 + 提示: %1 + + + Sudden Death Timeout + 突然死亡超时 + + + Stereoscopy + 立体视觉 + + + Mines Time + 地雷时间 + + + Fullscreen Resolution + 全屏分辨率 + + + Weapons + 武器 + + + % Retreat Time + % 撤退时间 + + + Sudden Death Health Decrease + 突然死亡减少血量 + + + Displayed tags above hogs and translucent tags + 刺猬上面显示的标签和透明标签 + + + Nickname + 昵称 + + + + QGroupBox + + Fort + 堡垒 + + + Game Modifiers + 游戏修改器 + + + Basic Settings + 基本设置 + + + Videos + 视频 + + + Team Settings + 队伍设置 + + + Team Members + 队伍数量 + + + Description + 描述 + + + Net game + 网络游戏 + + + Playing teams + 正在玩的队伍 + + + + PageDrawMap + + Load + 加载 + + + Save + 保存 + + + Undo + 撤销 + + + Save drawn map + 保存绘画地图 + + + Clear + 清除 + + + Eraser + 橡皮擦 + + + Polyline + 线条 + + + Optimize + 优化 + + + Brush size + 笔刷大小 + + + Rectangle + 长方形 + + + Ellipse + 椭圆 + + + Load drawn map + 加载绘画地图 + + + Drawn Maps + 绘画地图 + + + All files + 所有文件 + + + + SeedPrompt + + Seed + 种子 + + + Close + 关闭 + + + Cancel + 取消 + + + The map seed is the basis for all random values generated by the game. + 地图种子是游戏生成的所有随机数值的偏移。 + + + Set seed + 设置种子 + + + + PageCampaign + + Team + 队伍 + + + Mission + 任务 + + + Start fighting + 开始战斗 + + + Campaign + 战役 + + + + PageTraining + + Team + 队伍 + + + Team's longest time: %L1 s + 队伍最长时间: %L1 秒 + + + Team lowscore: %1 + 队伍最低分: %1 + + + Pick the challenge to play + 选择挑战开玩 + + + Challenges + 挑战 + + + No description available + 无可用描述 + + + Pick the training to play + 选择训练开玩 + + + Team's best time: %L1 s + 队伍最佳时间: %L1 秒 + + + Start fighting + 开始战斗 + + + Pick the scenario to play + 选择场景开玩 + + + Trainings + 训练 + + + Select a mission! + 选择一个任务! + + + Scenarios + 场景 + + + Team's top accuracy: %1% + 队伍最高准确度: %1% + + + Team highscore: %1 + 队伍最高分: %1 FeedbackDialog - + View + 查看 + + + Send Feedback + 发送反馈 + + + Cancel + 取消 + + We are always happy about suggestions, ideas, or bug reports. - - - - - Send us feedback! - - - - + 我们很高兴收到建议、想法或错误报告。 + + Feedback - - - - + 反馈 + + + Send us feedback! + 给我们发送反馈! + + If you found a bug, you can see if it's already been reported here: - - - - + 如果你发现一个错误,且它已经被报告,你可以在这里看到: + + Your email address is optional, but necessary if you want us to get back at you. - - - - + 你的邮箱地址是可选的,如果你想要收到回复。 + + This is optional, but this information might help us to resolve bugs and other technical problems. - - - - - View - - - - - Cancel - 取消 - - - - Send Feedback - - - - - FreqSpinBox - - - Never - 从不 - - - - Every %1 turn - - 每隔 %1 回合 - + 这是可选的,但这个信息可能帮助我们解决错误和其他技术问题。 - GameCFGWidget - - - Map - 地图 - - - - Game options - - - - - Edit weapons - - - - - Game scheme will auto-select a weapon - - - - - Edit schemes - 修改游戏设置 - - - - GameSchemeModel - - new - - - - - New - 新游戏 - - - - New (%1) - - - - - Copy of %1 - - - - - Copy of %1 (%2) - - - - - GameUIConfig - - - Guest - + PageRoomsList + + Join room + 加入房间 + + + Admin features + 管理员功能 + + + Open server administration page + 打开服务器管理员页面 + + + %1 players online + %1 玩家在线 + + + Create room + 创建房间 + + + Search for a room: + 搜索房间: + + + Room state + 房间状态 HWApplication - - - - %1 minutes - - - - - - - %1 hour - - - - - - - - - %1 hours - - - - - - - %1 day - - - - - - - - - %1 days - - - - - - - Scheme '%1' not supported - - - - - Cannot create directory %1 - - - - + + Custom path to the game data folder + 游戏数据的自定义路径 + + Usage - command-line - “Usage” as in “how the command-line syntax works”. Shown when running “hedgewars --help” in command-line - - - - - OPTION - command-line - Name of a command-line argument, shown when running “hedgewars --help” in command-line. “OPTION” as in “command-line option” - - - - - + 使用情况 + + CONNECTSTRING - command-line - Name of a command-line argument, shown when running “hedgewars --help” in command-line - - - - - Options - command-line - “Options” as in “command-line options” - - - - + CONNECTSTRING + + + %1 day + %1 天 + + Display this help - command-line - - - - - Custom path for configuration data and user data - command-line - - - - - Custom path to the game data folder - command-line - - - - + 显示这个帮助 + + + %1 hours + %1 时 + + + %1 minutes + %1 分 + + + OPTION + 选项 + + Hedgewars can use a %1 (e.g. "%2") to connect on start. - command-line - - - - - Malformed option argument: %1 - command-line - - - - - Unknown option argument: %1 - command-line - - - - + 刺猬在开始时可以用 %1 (如 "%2") 来连接。 + + + Options + 选项 + + + Scheme '%1' not supported + 不支持方案‘%1’ + + Failed to open data directory: %1 Please check your installation! - - - - - HWAskQuitDialog - - - Do you really want to quit? - - - - - HWChatWidget - - - Chat log - - - - - Enter chat messages here and send them with [Enter] - - - - - List of players - - - - - %1 has joined - - - - - %1 has left - - - - - %1 has left (%2) - - - - - %1 has been removed from your ignore list - - - - - %1 has been added to your ignore list - - - - - %1 has been removed from your friends list - - - - - %1 has been added to your friends list - - - - - Stylesheet imported from %1 - - - - - Enter %1 if you want to use the current StyleSheet in future, enter %2 to reset! - - - - - Couldn't read %1 - - - - - StyleSheet discarded - - - - - StyleSheet saved to %1 - - - - - Failed to save StyleSheet to %1 - + 无法打开数据目录: +%1 + +请检查你的安装! + + + %1 days + %1 天 + + + %1 hour + %1 时 + + + Unknown option argument: %1 + 未知的选项参数: %1 + + + Custom path for configuration data and user data + 配置和用户数据的自定义路径 + + + Malformed option argument: %1 + 畸形选项参数: %1 + + + Cannot create directory %1 + 不能创建目录 %1 - HWForm - - - Team 1 - - - - - %1's Team - - - - - Team %1 - Default team name - - - - - Computer %1 - Default computer team name - - - - - Game aborted - - - - - Hedgewars - Nick registered - - - - - This nick is registered, and you haven't specified a password. - -If this nick isn't yours, please register your own nick at www.hedgewars.org - -Password: - - - - - Your nickname is not registered. -To prevent someone else from using it, -please register it at www.hedgewars.org - - - - - - -Your password wasn't saved either. - - - - - Nickname - - - - - Someone already uses your nickname %1 on the server. -Please pick another nickname: - - - - - - No nickname supplied. - - - - - - Hedgewars - Empty nickname - - - - - Hedgewars - Wrong password - - - - - You entered a wrong password. - - - - - Room password - - - - - The room is protected with password. -Please, enter the password: - - - - - Try Again - - - - - Hedgewars - Connection error - - - - - You reconnected too fast. -Please wait a few seconds and try again. - - - - - Hedgewars Demo File - File Types - - - - - Hedgewars Save File - File Types - - - - - Demo name - - - - - Demo name: - - - - - Unknown network error (possibly missing SSL library). - - - - - This feature requires an Internet connection, but you don't appear to be online (error code: %1). - - - - - Internal error: Reply object is invalid. - - - - - - Cannot save record to file %1 - 无法录入文件 %1 + About + + <a href="https://www.libsdl.org/">SDL2_ttf</a>: %1.%2.%3 + <a href="https://www.libsdl.org/">SDL2_ttf</a>: %1.%2.%3 + + + <a href="https://www.libsdl.org/">SDL2_net</a>: %1.%2.%3 + <a href="https://www.libsdl.org/">SDL2_net</a>: %1.%2.%3 + + + This program is distributed under the %1. + 这个程序在 %1 下发布。 + + + Unknown Compiler + 未知编译器 + + + Visit our homepage: %1 + 访问我们的主页: %1 + + + Dependency versions: + 依赖版本: + + + <a href="https://www.qt.io/developers/">Qt</a>: %1 + <a href="https://www.qt.io/developers/">Qt</a>: %1 + + + Other people + 其他人 + + + %1: %2 + + + + <a href="https://gcc.gnu.org">GCC</a>: %1 + <a href="https://gcc.gnu.org">GCC</a>: %1 + + + Unknown Compiler: %1 + 未知编译器: %1 + + + GNU GPL v2 + GNU GPL v2 + + + %1 &lt;%2&gt; + + + + %1: %2 &lt;%3&gt; + + + + Extended Credits + 扩展名单 + + + %1 (alias %2) + + + + <a href="https://libav.org">libavformat</a>: %1.%2.%3 + <a href="https://libav.org">libavformat</a>: %1.%2.%3 + + + <a href="https://www.libsdl.org/">SDL2_mixer</a>: %1.%2.%3 + <a href="https://www.libsdl.org/">SDL2_mixer</a>: %1.%2.%3 + + + <a href="https://www.libsdl.org/">SDL2</a>: %1.%2.%3 + <a href="https://www.libsdl.org/">SDL2</a>: %1.%2.%3 + + + Revision %1 (%2) + 修订 %1 (%2) + + + Hedgewars %1 + 刺猬战争 %1 + + + <a href="https://visualstudio.microsoft.com">VC++</a>: %1 + + + + An extended credits list can be found in the CREDITS text file. + 一个扩展名单列表可以在 CREDITS 文本文件里找到。 + + + Credits + 名单 + + + <a href="https://icculus.org/physfs/">PhysFS</a>: %1.%2.%3 + <a href="https://icculus.org/physfs/">PhysFS</a>: %1.%2.%3 + + + <a href="https://libav.org">libavutil</a>: %1.%2.%3 + <a href="https://libav.org">libavutil</a>: %1.%2.%3 + + + <a href="https://libav.org">libavcodec</a>: %1.%2.%3 + <a href="https://libav.org">libavcodec</a>: %1.%2.%3 + + + <a href="https://www.libsdl.org/">SDL2_image</a>: %1.%2.%3 + <a href="https://www.libsdl.org/">SDL2_image</a>: %1.%2.%3 + + + + HatButton + + Change hat (%1) + 改变帽子 (%1) - HWGame - - - A fatal ERROR occured! The game engine had to stop. - -We are very sorry for the inconvenience. :-( - -If this keeps happening, please click the 'Feedback' button in the main menu! + QMessageBox + + Error + 错误 + + + Do you really want to remove %1 file(s)? + 你确定要删除 %1 个文件? + + + Room Name - Are you sure? + 房间名 — 你确定? + + + Please select a file from the list. + 请从列表选择一个文件。 + + + Not all players are ready + 还有玩家没准备好 + + + Welcome to Hedgewars + 欢迎来到刺猬战争 + + + Failed to download captcha + 无法下载验证码 + + + Teams - Name already taken + 队伍 - 已取名 + + + Videos - Are you sure? + 视频 - 你确定? + + + Do you really want to delete the team '%1'? + 你确定要删除队伍 ‘%1’? + + + Hedgewars - Error + 刺猬战争 - 错误 + + + Hedgewars - Nick not registered + 刺猬战争 - 昵称未注册 + + + Schemes - Name already taken + 方案 - 已取名 + + + Cannot delete default weapon set '%1'! + 不能删除默认武器集 ‘%1’! + + + This server supports secure connections on port %1. +Would you like to reconnect securely? + 这个服务器支持安全连接在端口 %1. +你想要重新安全连接吗? + + + Netgame - Error + 网络游戏 - 错误 + + + Failed to generate captcha + 无法生成验证码 + + + File error + 文件错误 + + + Please enter room name + 请输入房间名 + + + Teams - Are you sure? + 队伍 - 你确定? + + + Please select a record from the list + 请从列表选择一份录制 + + + Please select a server from the list + 请从列表选择一个服务器 + + + Cannot open '%1' for writing + 不能打开‘%1’ 以写入 + + + File association failed. + 文件关联失败。 + + + Please fill out all fields. Email is optional. + 请填写所有字段,邮箱是可选的。 + + + Unable to start server + 无法启动服务器 + + + Server redirection + 服务器重定向 + + + Cannot delete file %1. + 不能删除文件 %1. + + + All file associations have been set + 所有文件关联已设置 + + + Weapons - Are you sure? + 武器 - 你确定? + + + Room Name - Error + 房间名 - 错误 + + + A weapon scheme with the name '%1' already exists. Changes made to the weapon scheme have been discarded. + 已存在名为‘%1’的武器方案。对武器方案的改变已放弃。 + + + Are you sure you want to start this game? +Not all players are ready. + 你确定要开始游戏? +还有玩家没准备好。 + + + Weapons - Warning + 武器 - 警告 + + + Hedgewars - Warning + 刺猬战争 - 警告 + + + Schemes - Are you sure? + 方案 - 你确定? + + + Hedgewars - Success + 刺猬战争 - 成功 + + + Cannot delete default scheme '%1'! + 不能删除默认方案‘%1’! + + + Hedgewars - Information + 刺猬战争 - 信息 + + + The game you are trying to join has started. +Do you still want to join the room? + 你试图加入的游戏已经开始。 +你还想加入房间吗? + + + Welcome to Hedgewars! -Last engine message: -%1 - - - - - - en.txt - IMPORTANT: This text has a special meaning, do not translate it directly. This is the file name of translation files for the game engine, found in Data/Locale/. Usually, you replace “en” with the ISO-639-1 language code of your language. - zh_CN.txt - - - - Cannot open demofile %1 - DEMO %1 打不开 - - - - HWHostPortDialog - - - Connect to server - +You seem to be new around here. Would you like to play some training missions first to learn the basics of Hedgewars? + 欢迎来到刺猬战争! + +你看起来是新来的,想要玩新手训练学习基本操作吗? + + + The team name '%1' is already taken, so your team has been renamed to '%2'. + 已存在名为‘%1’的队伍, 所以你的队伍名被改为 ‘%2’. + + + A scheme with the name '%1' already exists. Your scheme has been renamed to '%2'. + 已存在名为‘%1’的方案,你的方案名被改为‘%2’. + + + Do you really want to delete the weapon set '%1'? + 你确定要删除武器集 ‘%1’? + + + The connection to the server is lost. + 与服务器的连接丢失。 + + + Schemes - Warning + 方案 - 警告 + + + Do you really want to delete the game scheme '%1'? + 你确定要删除游戏方案‘%1’? + + + Cannot open '%1' for reading + 不能打开 ‘%1’ 以读取 + + + Cannot rename file to %1. + 不能重命名文件为 %1. + + + Cannot use the weapon scheme '%1'! + 不能使用武器集‘%1’! + + + Do you really want to delete the video '%1'? + 你确定要删除视频 ‘%1’? + + + Please select room from the list + 请从列表选择房间 + + + System Information Preview + 系统信息预览 - HWMapContainer - - - Small tunnels - - - - - Medium tunnels - - - - - Seed - Refers to the "random seed"; the source of randomness in the game - - - - - Map type: - - - - - Image map - - - - - Mission map - - - - - Hand-drawn - - - - - Randomly generated - - - - - Random maze - - - - - Random perlin - - - - - Forts - - - - - Random - - - - - View and edit the seed, the source of randomness in the game - - - - - Map preview: - - - - - Load - 读取 - - - - Load map drawing - - - - - Edit - - - - - Edit map drawing - - - - - All - 全部 - - - - Small - 小型 - - - - Medium - 中型 - - - - Large - 大型 - - - - Cavern - 洞穴 - - - - Wacky - 曲折 - - - - Large tunnels - - - - - Small islands - - - - - Medium islands - - - - - Large islands - - - - - Randomize the theme - - - - - Choose a theme - - - - - Randomize the map, theme and seed - - - - - Randomize the theme and seed - - - - - Randomize the seed - - - - - Click to randomize the map, theme and seed - - - - - Click to randomize the theme and seed - - - - - Adjust the complexity of the generated map - - - - - Adjust the distance between forts - - - - - Scale size of the drawn map - - - - - Click to edit - - - - - Map size: - - - - - Maze style: - - - - - Style: - - - - - Mission: - - - - - Map: - - - - - - Theme: %1 - - - - - Load drawn map - - - - - Drawn Maps - - - - - All files - + GameUIConfig + + Guest + 游客 - HWNetServersModel - - - Title - 标题 - - - - IP - short for "IP address" (Internet Protocol), part of server address - IP - - - - Port - short for "port number", part of server address - 端口 + QComboBox + + Human + 人类 + + + Disabled + 禁用 + + + (System default) + (系统默认) + + + Top-Bottom + 顶部按钮 + + + 24 FPS + 24 FPS + + + 25 FPS + 25 FPS + + + 30 FPS + 30 FPS + + + 50 FPS + 50 FPS + + + 60 FPS + 60 FPS + + + Blue/Red + 蓝/红 + + + Stereoscopy creates an illusion of depth when you wear 3D glasses. + 当你戴着3D眼镜时,立体视觉创造深度的错觉。 + + + Red/Cyan grayscale + 红/青 灰度 + + + Community + 社区 + + + Cyan/Red grayscale + 青/红 灰度 + + + Blue/Red grayscale + 蓝/红 灰度 + + + Red/Green + 红/绿 + + + Computer (Level %1) + 计算机 (等级 %1) + + + Red/Blue + 红/蓝 + + + Red/Cyan + 红/青 + + + Green/Red + 绿/红 + + + Red/Green grayscale + 红/绿 灰度 + + + Side-by-side + 并排 + + + Green/Red grayscale + 绿/红 灰度 + + + Red/Blue grayscale + 红/蓝 灰度 + + + Cyan/Red + 青/红 HWNewNet - - Remote host has closed connection - - - - + %1 *** %2 has joined the room + %1 *** %2 已加入房间 + + + You got kicked + 你被踢了 + + The host was not found. Please check the host name and port settings. - 错误没找到这个主机。请检查主机名和端口设置。 - - - Connection refused - 连接被拒绝 - - - - The connection was refused by the official server or timed out. Something seems to be wrong with the official server at the moment. This might be a temporary problem. Please try again later. - - - - + 未找到主机,请检查主机和端口设置。 + + + %1 *** %2 has left + %1 *** %2 已离开 + + The connection was refused by the host or timed out. This might have one of the following reasons: - The Hedgewars Server program does currently not run on the host - The specified port number is incorrect - There is a temporary network problem Please check the host name and port settings and/or try again later. - - - - + 连接被主机拒绝或超时,理由可能如下: +- 刺猬战争服务器程序当前没有运行在主机上 +- 指定的端口号不正确 +- 临时网络问题 + +请检查主机名和端口设置,并/或稍后再试。 + + The server is too old. Disconnecting now. - - - - + 服务器太旧了,现在断开连接。 + + + Remote host has closed connection + 远程主机已关闭连接 + + + Reason: + 理由: + + Server authentication error - - - - - %1 *** %2 has left - - - - + 服务器验证出错 + + + Room destroyed + 房间已销毁 + + %1 *** %2 has left (%3) - - - - - - %1 *** %2 has joined the room - - - - Quit reason: - 退出原因: - - - - Room destroyed - 房间损坏 - - - - You got kicked - 被踢出 - - - - Reason: - + %1 *** %2 已离开 (%3) + + + The connection was refused by the official server or timed out. Something seems to be wrong with the official server at the moment. This might be a temporary problem. Please try again later. + 连接被官方服务器拒绝或超时,目前官方服务器似乎出了问题,这可能是临时问题,请稍后再试。 HWPasswordDialog - Login - - - - + 登录 + + + New Account + 新账号 + + + Nickname: + 昵称: + + + Password: + 密码: + + To connect to the server, please log in. If you don't have an account on www.hedgewars.org, just enter your nickname. - - - - - Nickname: - - - - - Password: - - - - - New Account - + 要连接到服务器,请登录。 + +如果你还没有www.hedgewars.org的账号, +只需输入你的昵称。 + + + + FreqSpinBox + + Never + 从不 + + + Every %1 turn + 每 %1 回合 + + + + PageMultiplayer + + Start + 开始 + + + Start fighting (requires at least 2 teams) + 开始战斗(需要至少两个队伍) + + + Edit game preferences + 编辑游戏首选项 + + + + PageNetGame + + Start + 开始 + + + Turn on the lightbulb to show the other players when you're ready to fight + 点亮灯泡让其他玩家知道你准备好战斗 + + + Update + 更新 + + + Room name + 房间名 + + + Start fighting (requires at least 2 teams) + 开始战斗(需要至少两个队伍) + + + Update the room name + 更新房间名 + + + Edit game preferences + 编辑游戏首选项 + + + Room controls + 房间控制 + + + + PageMain + + Exit game + 退出游戏 + + + Play official network game + 玩官方网络游戏 + + + Play a game on a single computer + 单机 + + + Play a game across a local area network + 局域网 + + + Downloadable Content + 可下载内容 + + + Access the user created content downloadable from our website + 从我们的网站下载用户创建的内容 + + + Leave a feedback here reporting issues, suggesting features or just saying how you like Hedgewars + 在这里留下反馈,报告问题,建议功能,或告诉我们你多喜欢刺猬战争 + + + Play local network game + 玩本地网络游戏 + + + Play a game across a network + 互联网 + + + Open the Hedgewars online game manual in your web browser + 在你的浏览器打开刺猬战争游戏的在线手册 + + + Edit game preferences + 编辑游戏首选项 + + + Feedback + 反馈 + + + Read about who is behind the Hedgewars Project + 了解刺猬战争项目后面有谁 + + + Play a game on an official server + 官方服务器 + + + Manage videos recorded from game + 管理游戏录制视频 + + + + PageConnecting + + Connecting... + 连接中... + + + + binds (descriptions) + + Switch your currently active hog (if possible): + 切换你当前活动的刺猬 (如果能行): + + + Set the timer on bombs and timed weapons: + 设置炸弹和武器的定时器: + + + Toggle fullscreen mode: + 切换全屏模式: + + + Toggle automatic camera / refocus on active hedgehog: + 切换自动镜头/重新聚焦在活动的刺猬: + + + Pick a weapon or a target location under the cursor: + 选择武器或目标位置: + + + Pause, continue or leave your game: + 暂停,继续,或离开游戏: + + + Pick a weapon or utility item: + 选择武器或工具: + + + Talk to your clan or all participants: + 和你的战队或所有参赛者聊天: + + + Heads-up display: + 抬头显示(HUD): + + + Modify the camera's zoom level: + 修改镜头缩放级别: + + + Move the cursor or camera without using the mouse: + 不使用鼠标移动光标或镜头: + + + Fire your selected weapon or trigger an utility item: + 开火你选择的武器,或触发工具: + + + Demo replay: + 重播 Demo: + + + Modify the game's volume while playing: + 玩游戏时修改音量: + + + Hedgehog movement + 刺猬移动 + + + Take a screenshot: + 截图: + + + Record video: + 录制视频: + + + Traverse gaps and obstacles by jumping: + 穿越裂隙和跳过障碍: + + + + binds (combination) + + precise + toggle hedgehog tags + 精确 + 切换刺猬标签 + + + precise + switch + toggle team bars + 精确 + 切换 + 切换队伍栏 + + + hold down precise + 长按精确 + + + precise + screenshot + 精确 + 截图 + + + precise + reset zoom + 精确 + 重置缩放 + + + precise + timer + 精确 + 定时器 + + + switch + toggle hedgehog tags + 切换 + 切换刺猬标签 + + + high jump (twice) + 高跳(两次) + + + precise + left/right + 精确 + 左/右 + + + precise + switch + 精确 + 切换 + + + + PageNet + + Connect to the selected server + 连接到选择的服务器 + + + Start private server + 启动私人服务器 + + + Specify the address and port number of a known server and connect to it directly + 指定已知服务器的地址和端口并直连 + + + Update the list of servers + 更新服务器列表 + + + + PageGameStats + + A total of <b>%1</b> hedgehog(s) were killed during this round. + 这一局共杀死了<b>%1</b>个刺猬。 + + + The best killer is <b>%1</b> with <b>%2</b> kills in a turn. + 最优秀的杀手是 <b>%1</b>,一个回合 <b>%2</b> 杀。 + + + (%1 crate(s)) + (%1 箱子) + + + The best shot award was won by <b>%1</b> with <b>%2</b> pts. + <b>%1</b> 以 <b>%2</b> 分获得最佳射击奖。 + + + (%1 kill) + (%1 杀) + + + (%1 point(s)) + (%1 分) + + + Save demo (unavailable because the /lua command was used) + 保存 demo (不可用,因为使用了/lua命令) + + + Ranking + 排名 + + + (%1 %2) + (%1 %2) + + + With everyone having the same clan color, there was no reason to fight. And so the hedgehogs happily lived in peace ever after. + 有着相同战队颜色的各位,没有理由去战斗了,刺猬们从此过上幸福的生活。 + + + Details + 细节 + + + <b>%1</b> thought it's good to shoot their own hedgehogs for <b>%2</b> pts. + <b>%1</b> 误伤队友获得 <b>%2</b> 分。 + + + <b>%1</b> killed <b>%2</b> of their own hedgehogs. + <b>%1</b> 杀死 <b>%2</b> 个队友。 + + + Play again + 再玩一次 + + + (%L1 second(s)) + (%L1 秒) + + + <b>%1</b> was scared and skipped turn <b>%2</b> times. + <b>%1</b> 很害怕并跳过 <b>%2</b> 个回合。 + + + Save demo + 保存 demo + + + Health graph + 血量图表 + + + + PageInfo + + Open the snapshot folder + 打开截图文件夹 + + + + HWForm + + Demo name: + Demo 名字: + + + + +Your password wasn't saved either. + + +你的密码也没有保存。 + + + Room password + 房间密码 + + + Hedgewars - Wrong password + 刺猬战争 - 密码错误 + + + Game aborted + 游戏中止 + + + Demo name + Demo 名字 + + + Someone already uses your nickname %1 on the server. +Please pick another nickname: + 已经有人在服务器上使用你的昵称 %1 。 +请另选一个昵称: + + + Team 1 + 队伍 1 + + + The room is protected with password. +Please, enter the password: + 房间受密码保护。 +请输入密码: + + + Cannot save record to file %1 + 不能保存录制到文件 %1 + + + No nickname supplied. + 没提供昵称。 + + + Hedgewars Demo File + 刺猬战争 Demo 文件 + + + Hedgewars - Nick registered + 刺猬战争 - 昵称已注册 + + + Internal error: Reply object is invalid. + 内部错误 - 回复对象无效。 + + + You reconnected too fast. +Please wait a few seconds and try again. + 你重连太快了。 +请等一下再试。 + + + This feature requires an Internet connection, but you don't appear to be online (error code: %1). + 这个功能需要网络连接,但你似乎不在线 (error code: %1). + + + Hedgewars - Empty nickname + 刺猬战争 - 空昵称 + + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + 你的昵称没注册。 +为防止其他人使用它, +请在 www.hedgewars.org 注册。 + + + Hedgewars Save File + 刺猬战争保存文件 + + + This nick is registered, and you haven't specified a password. + +If this nick isn't yours, please register your own nick at www.hedgewars.org + +Password: + 这个昵称已注册,而且你没有设置密码。 + +如果这个昵称不是你的,请在 www.hedgewars.org 注册自己的昵称 + +密码: + + + Team %1 + 队伍 %1 + + + You entered a wrong password. + 你输入了错误的密码。 + + + Try Again + 再试一次 + + + Hedgewars - Connection error + 刺猬战争 - 连接出错 + + + Unknown network error (possibly missing SSL library). + 未知网络错误(可能缺少SSL库)。 + + + Computer %1 + 计算机 %1 + + + %1's Team + %1 的队伍 + + + Nickname + 昵称 HWRecorder - A fatal ERROR occured while processing the video recording! The video could not be saved. As a workaround, you could try to reset the Hedgewars video recorder settings to the defaults. @@ -1158,5011 +4420,455 @@ Last engine message: %1 - - - - - HatButton - - - Change hat (%1) - - - - - HatPrompt - - - Choose a hat - - - - - Search for a hat: - - - - - Cancel - 取消 - - - - Use selected hat - - - - - KB - - SDL_ttf returned error while rendering text, most propably it is related to the bug in freetype2. It's recommended to update your freetype lib. - SDL_ttf 返回错误-渲染文字失败,可能有关freetype2的bug。建议升级 freetype。 - - - - KeyBinder - - - Warning: The same key is assigned multiple times! - - - - - Category - - - - - LibavInteraction - - - Duration: %1min %2s - Duration in minutes and seconds (SI units) - - - - - Video: %1x%2, %3 FPS, %4 - Video metadata. %1 = video width, %2 = video height, %3 = frames per second = %4 = decoder name - - - - - Video: %1x%2, %3 - Video metadata. %1 = video width, %2 = video height, %3 = decoder name - - - - - Player: %1 - - - - - Theme: %1 - - - - - Map: %1 - - - - - Record: %1 - As in ‘recording’ - - - - - Audio: - - - - - unknown - - - - - MapModel - - - No description available. - - - - - MinesTimeSpinBox - - - Random - - - - - %1 seconds - - - - - - - PageAdmin - - - General - 常规 - - - - Bans - - - - - Fetch data - - - - - Server message for latest version: - - - - - Server message for previous versions: - - - - - Latest version protocol number: - - - - - MOTD preview: - MOTD = Message Of The Day, the message which is shown to players joining the server - - - - - Clear Accounts Cache - - - - - Set data - - - - - IP/Nick - - - - - Expiration - - - - - Reason - - - - - Refresh - - - - - Add - - - - - Remove - - - - - PageCampaign - - - - Start fighting - - - - - Team - - - - - Campaign - - - - - Mission - - - - - PageConnecting - - - Connecting... - 连接中... + 在录制视频时发生了一个致命错误!视频不能保存。 + +一个解决方案是尝试把视频录制器的设置重设为默认。 + +要报告这个错误,请点击主菜单的“反馈”按钮。 + +最后的引擎消息: +%1 PageDataDownload - + Open packages directory + 打开下载内容目录 + + Load the start page - - - - - Open packages directory - - - - + 加载开始页面 + + + Internal error: Reply object is invalid. + 内部错误:回复对象无效。 + + Loading, please wait. - - - - + 加载中,请等待。 + + + This feature requires an Internet connection, but you don't appear to be online (error code: %1). + 这个功能需要网络连接,但你似乎不在线 (error code: %1). + + Unknown network error (possibly missing SSL library). - - - - - This feature requires an Internet connection, but you don't appear to be online (error code: %1). - - - - - Internal error: Reply object is invalid. - + 未知网络错误(可能缺少SSL库)。 - PageDrawMap - - - Eraser - - - - - Undo - - - - - Polyline - - - - - Rectangle - - - - - Ellipse - - - - - Brush size - - - - - Clear - - - - - Optimize - - - - - Load - 读取 - - - - Save - 保存 - - - - Load drawn map - - - - - - Drawn Maps - - - - - - All files - - - - - Save drawn map - - - - - PageEditTeam - - - Select an action to choose a custom key bind for this team - - - - - Use my default - - - - - Reset all binds - - - - - General - 常规 - - - - Custom Controls - - - - - Hat - - - - - Name - - - - - This hedgehog's name - - - - - Randomize this hedgehog's name - - - - - Random Hats - - - - - Random Names - - - - - Random Team - - - - - Randomize the team name - - - - - Randomize the grave - - - - - Randomize the flag - - - - - Play a random example of this voice - - - - - Randomize the voice - - - - - Randomize the fort - - - - - CPU %1 - Name of a flag for computer-controlled enemies. %1 is replaced with the computer level - - - - - %1 (%2) - + PageSinglePlayer + + Load a previously saved game + 加载之前保存的游戏 + + + Watch recorded demos + 观看 demo + + + Campaign Mode + 战役模式 + + + Play a quick game against the computer with random settings + 玩快速单机游戏(随机设置) + + + Singleplayer missions: Learn how to play in the training, practice your skills in challenges or try to complete goals in scenarios. + 单人任务:在训练中学习,在挑战中实践,或试着完成场景目标。 + + + Play a hotseat game against your friends, or AI teams + 在一个座位上对抗你的朋友或AI - PageGameStats - - - Details - - - - - - Health graph - - - - - Ranking - - - - - Play again - - - - - Save - 保存 - - - - The best shot award was won by <b>%1</b> with <b>%2</b> pts. - - - - - - - The best killer is <b>%1</b> with <b>%2</b> kills in a turn. - - - - - - - A total of <b>%1</b> hedgehog(s) were killed during this round. - - - - - - - (%1 kill) - Number of kills in stats screen, written after the team name - - - - - - - (%1 point(s)) - Number of points in stats screen, written after the team name - - - - - - - - (%L1 second(s)) - Time in seconds - - - - - - - (%1 crate(s)) - - - - - - - (%1 %2) - For custom number of points in the stats screen, written after the team name. %1 is the number, %2 is the word. Example: “4 points” - - - - - - - <b>%1</b> thought it's good to shoot their own hedgehogs for <b>%2</b> pts. - - - - - - - <b>%1</b> killed <b>%2</b> of their own hedgehogs. - - - - - - - <b>%1</b> was scared and skipped turn <b>%2</b> times. - - - - - - - With everyone having the same clan color, there was no reason to fight. And so the hedgehogs happily lived in peace ever after. - - - - - PageInGame - - - In game... - - - - - PageInfo - - - Open the snapshot folder - + HWChatWidget + + StyleSheet saved to %1 + 样式表保存到 %1 + + + Failed to save StyleSheet to %1 + 无法保存样式表到 %1 + + + %1 has left (%2) + %1 离开了 (%2) + + + Stylesheet imported from %1 + 从 %1 导入样式表 + + + Enter %1 if you want to use the current StyleSheet in future, enter %2 to reset! + 如果你想在以后使用当前样式表,输入 %1 。重置输入 %2 ! + + + %1 has been added to your ignore list + %1 已添加你的忽略列表 + + + %1 has left + %1 离开了 + + + %1 has been removed from your friends list + %1 已从你的朋友列表移除 + + + Couldn't read %1 + 无法读取 %1 + + + List of players + 玩家列表 + + + %1 has been removed from your ignore list + %1 已从你的忽略列表移除 + + + %1 has been added to your friends list + %1 已添加到你的朋友列表 + + + StyleSheet discarded + 已放弃样式表 + + + Chat log + 聊天记录 + + + %1 has joined + %1 已加入 + + + Enter chat messages here and send them with [Enter] + 输入聊天消息并按回车键发送 - PageMain - - - Play a game on a single computer - - - - - Play a game across a network - - - - - Play local network game - - - - - Play a game across a local area network - - - - - Play official network game - - - - - Play a game on an official server - - - - - - Read about who is behind the Hedgewars Project - - - - - Feedback - - - - - Leave a feedback here reporting issues, suggesting features or just saying how you like Hedgewars - - - - - Downloadable Content - - - - - Access the user created content downloadable from our website - - - - - Exit game - - - - - Manage videos recorded from game - - - - - Open the Hedgewars online game manual in your web browser - - - - - Edit game preferences - + QObject + + No description available + 无可用描述 - PageMultiplayer - - - Edit game preferences - - - - - Start - 开始 - - - - Start fighting (requires at least 2 teams) - - - - - PageNet - - - Connect to the selected server - - - - - Update the list of servers - - - - - Specify the address and port number of a known server and connect to it directly - - - - - Start private server - + LibavInteraction + + Map: %1 + 地图: %1 + + + Video: %1x%2, %3 FPS, %4 + 视频: %1×%2, %3 FPS, %4 + + + Record: %1 + 录制: %1 + + + Video: %1x%2, %3 + 视频: %1×%2, %3 + + + Audio: + 音频: + + + Duration: %1min %2s + 时长: %1 分 %2 秒 + + + Theme: %1 + 主题: %1 + + + unknown + 未知 + + + Player: %1 + 玩家: %1 - PageNetGame - - - Room name - - - - - Update the room name - - - - - Update - 更新 - - - - Room controls - - - - - Edit game preferences - - - - - Turn on the lightbulb to show the other players when you're ready to fight - - - - - Start fighting (requires at least 2 teams) - - - - Control - Ctrl - - - - Start - 开始 + HatPrompt + + Choose a hat + 选择帽子 + + + Cancel + 取消 + + + Use selected hat + 使用选择的帽子 + + + Search for a hat: + 搜索帽子: PageNetServer - Click here for details - - - - + 点击这里了解详情 + + Insert your address here - + 在这里输入你的地址 + + + + binds (categories) + + Camera + 镜头 + + + Miscellaneous + 杂项 + + + Movement + 移动 + + + Weapons + 武器 - PageOptions - - - Select an action to change what key controls it - - - - - Reset to default - - - - - Reset all binds - - - - - - Game - - - - - Graphics - - - - - Audio - - - - - Controls - - - - - Video Recording - - - - - Network - - - - - Advanced - 进阶 - - - - Teams - 队伍 - - - - New team - 新队伍 - - - - Edit team - 修改队伍设定 - - - - Delete team - - - - - You can't edit teams from team selection. Go back to main menu to add, edit or delete teams. - - - - - Schemes - - - - - New scheme - - - - - Edit scheme - - - - - Delete scheme - - - - - Weapons - 武器 - - - - New weapon set - - - - - Edit weapon set - - - - - Delete weapon set - - - - - - x - Multiplication sign, to be used between two numbers. Note the “x” is only a dummy character, we recommend to use “×” if your language permits it - - - - - Frontend - - - - - Custom colors - - - - - Reset to default colors - - - - - Game audio - - - - - Frontend audio - - - - - Account - - - - - Proxy settings - - - - - Proxy host - - - - - Proxy port - - - - - Proxy login - - - - - Proxy password - - - - - No proxy - - - - - System proxy settings - - - - - Socks5 proxy - - - - - HTTP proxy - - - - - Miscellaneous - - - - - MISSING LANGUAGE NAME [%1] - In the case of an error, this is shown in the language selection for a language with unknown name. %1 = language code - - - - - Updates - - - - - Check for updates - - - - - Check now - - - - - Video recording options - - - - - Can't delete last team - - - - - You can't delete the last team! - + RoomNamePrompt + + Cancel + 取消 + + + Create room + 创建房间 + + + Enter a name for your room. + 命名房间。 + + + set password + 设置密码 + + + + ThemePrompt + + Cancel + 取消 + + + Search for a theme: + 搜索主题: + + + Use selected theme + 使用选择的主题 + + + Choose a theme + 选择主题 PagePlayDemo - + Load the selected game + 加载选择的游戏 + + Play demo - 播放 demo - - - + 播放 demo + + Play the selected demo - - - - - Load the selected game - - - - + 播放选择的 demo + + Rename dialog 重命名对话框 - Enter new file name: 输入新的文件名: - PageRoomsList - - - Search for a room: - - - - - Create room - - - - - Join room - - - - - Room state - - - - - Open server administration page - - - - Create - 建立 - - - Join - 加入 - - - - %1 players online - - - - - - - Admin features - 管理员功能 - - - - PageScheme - - - Add an indestructible border around the terrain - - - - - Lower gravity - - - - - Assisted aiming with laser sight - - - - - All hogs have a personal forcefield - - - - - All (living) hedgehogs are fully restored at the end of turn - - - - - Gain 80% of the damage you do back in health - - - - - Share your opponents pain, share their damage - - - - - Your hogs are unable to move, put your artillery skills to the test - - - - - Order of play is random instead of in room order. - - - - - Play with a King. If he dies, your side dies. - - - - - Take turns placing your hedgehogs before the start of play. - - - - - Ammo is shared between all teams that share a colour. - - - - - Disable girders when generating random maps. - - - - - Disable land objects when generating random maps. - - - - - AI respawns on death. - - - - - Attacking does not end your turn. - - - - - Weapons are reset to starting values each turn. - - - - - Each hedgehog has its own ammo. It does not share with the team. - - - - - You will not have to worry about wind anymore. - - - - - Wind will affect almost everything. - - - - - Teams in each clan take successive turns sharing their turn time. - - - - - Add an indestructible border along the bottom - - - - - Select a hedgehog at the beginning of a turn - - - - - Land can not be destroyed by most weapons. - - - - - Each clan starts in its own part of the terrain. - - - - - Overall damage and knockback in percent - Description of the game scheme setting “Damage Modifier”. “Knockback” means how much hedgehogs and objects get pushed by explosions and other forces - - - - - Turn time in seconds - - - - - Initial health of hedgehogs - - - - - How many rounds have to be played before Sudden Death begins - - - - - How much the water rises per turn while in Sudden Death. Set to 0 along with Sudden Death Health Decrease to disable Sudden Death. - - - - - How much health hedgehogs lose per turn while in Sudden Death, down to 1 health. Set to 0 along with Sudden Death Water Rise to disable Sudden Death. - - - - - Maximum rope length in percent - - - - - Likelihood of a dropped crate being a health crate. All other crates will be weapon or utility crates. - - - - - Likelihood of a crate dropping before a turn - - - - - Health bonus for collecting a health crate - - - - - Detonation timer of mines. The random timer lies between 0 and 5 seconds. The timer of air mines will be a quarter of the mines timer. - - - - - Average number of mines to be placed a medium-sized island map. This number will be scaled for other maps. - - - - - Likelihood of a mine being a dud. Does not affect mines placed by hedgehogs. - - - - - Average number of barrels to be placed a medium-sized island map. This number will be scaled for other maps. - - - - - Average number of air mines to be placed a medium-sized island map. This number will be scaled for other maps. - - - - - Affects the left and right boundaries of the map - - - - - Time you get after an attack - - - - - Additional parameter to configure game styles. The meaning depends on the used style, refer to the documentation. When in doubt, leave it empty. - - - - - None (Default) - - - - - Wrap (World wraps) - - - - - Bounce (Edges reflect) - - - - - Sea (Edges connect to sea) - - - - - Name of this scheme - - - - - Copy - - - - - New - 新游戏 - - - - Delete - 删除 - - - - %1 (%2) - - - - - PageSelectWeapon - - - New - 新游戏 - - - - Default - 默认 - - - - Copy - - - - - Delete - 删除 - - - - PageSinglePlayer - - - Play a quick game against the computer with random settings - - - - - Play a hotseat game against your friends, or AI teams - - - - - Campaign Mode - - - - - Singleplayer missions: Learn how to play in the training, practice your skills in challenges or try to complete goals in scenarios. - - - - - Watch recorded demos - - - - - Load a previously saved game - - - - - PageTraining - - - Pick the training to play - - - - - Pick the challenge to play - - - - - Pick the scenario to play - - - - - Trainings - - - - - Challenges - - - - - Scenarios - - - - - Team - - - - - - Start fighting - - - - - No description available - - - - - Team highscore: %1 - Highest score of a team - - - - - Team lowscore: %1 - Lowest score of a team - - - - - Team's top accuracy: %1% - Best accuracy of a team (in a challenge) - - - - - Team's best time: %L1 s - - - - - Team's longest time: %L1 s - - - - - Select a mission! - - - - - PageVideos - - - Name - - - - - Size - - - - - %1 bytes - - - - - - - %1% - Video encoding progress. %1 = number - - - - - (in progress...) - - - - - Date: %1 - - - - - Size: %1 - - - - - %1 (%2%) - %3 - Video encoding list entry. %1 = file name, %2 = percent complete, %3 = video operation type (e.g. “encoding”) - - - - - encoding - - - - - QAction - - - Kick - - - - Update - 更新 - - - - Restrict Joins - 限制参与 - - - - Restrict Team Additions - 限制团队插件 - - - - Restrict Unregistered Players Join - - - - - Info - 信息 - - - - Ban - 屏蔽 - - - - Delegate room control - - - - - Follow - - - - - - Ignore - - - - - - Add friend - - - - - Unignore - - - - - Remove friend - - - - - Show games in lobby - - - - - Show games in-progress - - - - - Show password protected - - - - - Show join restricted - + MapModel + + No description available. + 无可用描述。 - QCheckBox - - - Fullscreen - 游戏全屏幕 - - - - Show FPS - 显示帧率 (FPS) - - - - Alternative damage show - 另一种伤害显示方式 - - - - Team - - - - - Enable team tags by default - - - - - Hog - - - - - Enable hedgehog tags by default - - - - - Health - - - - - Enable health tags by default - - - - - Translucent - - - - - Enable translucent tags by default - - - - - Visual effects - - - - - Enable visual effects such as animated menu transitions and falling stars - - - - - - Sound - - - - - In-game sound effects - - - - - - Music - - - - - In-game music - - - - - Dampen when losing focus - Checkbox text. If checked, the in-game audio volume is reduced (=dampened) when the game window loses its focus - - - - - Reduce the game audio volume if the game window has lost its focus - - - - - Frontend sound effects - - - - - Frontend music - - - - - If enabled, Hedgewars adds the date and time in the form "YYYY-MM-DD_hh-mm" for automatically created demos. - - - - - Check for updates at startup - - - - - Show ammo menu tooltips - - - - - Append date and time to record file name - 记录名称中包含具体时间日期 - - - - - Save password - - - - - Record audio - - - - - Use game resolution - - - - - QComboBox - - - Human - 玩家 - - - - Computer (Level %1) - - - - - Community - - - - Level - Lv 级别 - - - - (System default) - - - - - Disabled - - - - - Stereoscopy creates an illusion of depth when you wear 3D glasses. - - - - - Red/Cyan - - - - - Cyan/Red - - - - - Red/Blue - - - - - Blue/Red - - - - - Red/Green - - - - - Green/Red - - - - - Side-by-side - - - - - Top-Bottom - - - - - 24 FPS - - - - - 25 FPS - - - - - 30 FPS - - - - - 50 FPS - - - - - 60 FPS - - - - - Red/Cyan grayscale - - - - - Cyan/Red grayscale - - - - - Red/Blue grayscale - - - - - Blue/Red grayscale - - - - - Red/Green grayscale - - - - - Green/Red grayscale - - - - - QGroupBox - - - Team Members - 成员 - - - - Team Settings - - - - - Fort - 城堡模式 - - - - Playing teams - 玩家队伍 - - - - Net game - 网络游戏 - - - - Game Modifiers - 游戏修改 - - - - Basic Settings - 基本设置 - - - - Videos - - - - - Description - - - - - QLabel - - - Locale - - - - - Nickname - - - - - Zoom (%) - - - - - Stereoscopy - - - - - Displayed tags above hogs and translucent tags - - - - - This setting will be effective at next restart. - - - - - Resolution - 分辨率 - - - - Bitrate (Kibit/s) - “Kibit/s” is the symbol for 1024 bits per second - - - - - Quality - - - - - Fullscreen - 游戏全屏幕 - - - - Fullscreen Resolution - - - - - Windowed Resolution - - - - - FPS limit - FPS 上限 - - - - Server name: - 服务器名: - - - - Server port: - 服务器端口: - - - - Host: - 主机: - - - - Port: - 端口: - - - - Weapons - 武器 - - - Version - 版本 - - - - Initial sound volume - 初始音量 - - - - Damage Modifier - 伤害修改 - - - - Turn Time - 回合时间 - - - - Initial Health - 初始生命值 - - - - Sudden Death Timeout - 死亡模式倒计时 - - - - Sudden Death Water Rise - - - - - Sudden Death Health Decrease - - - - - % Rope Length - - - - - % Health Crates - - - - - Health in Crates - - - - - Mines Time - - - - - Mines - - - - - % Dud Mines - - - - - Barrels - - - - - % Retreat Time - Label of game scheme setting for the time you get after an attack - - - - - Air Mines - - - - - World Edge - - - - - Script parameter - - - - - Scheme Name: - 设置名称: - - - - Crate Drops - 箱子降落 - - - - There are videos that are currently being processed. -Exiting now will abort them. -Do you really want to quit? - - - - - Name - - - - - Player - - - - - Grave - - - - - Flag - - - - - Voice - - - - - Your Email - - - - - Summary - - - - - Send system information - - - - - Description - - - - - Loading<br>CAPTCHA ... - - - - - Type the security code: - - - - - This development build is 'work in progress' and may not be compatible with other versions of the game, while some features might be broken or incomplete! - - - - - - Tip: %1 - - - - - Format - - - - - Audio codec - - - - - Video codec - - - - - Framerate - - - - - Style - - - - - Scheme - + MinesTimeSpinBox + + Random + 随机 + + + %1 seconds + %1 秒 QLineEdit - - unnamed - 无名 - - - - unnamed (%1) - - - - + anonymous + 匿名 + + hedgehog %1 - - - - - anonymous - - - - + 刺猬 %1 + + Hedgehog %1 - - - - - QMainWindow - - - Hedgewars %1 - 刺猬大作战 %1 - - - - QMessageBox - - - - - - - Error - 错误 - - - - - Please select a file from the list. - - - - - Cannot rename file to %1. - - - - - Cannot delete file %1. - - - - - Teams - Are you sure? - - - - - Do you really want to delete the team '%1'? - - - - - Teams - Name already taken - - - - - The team name '%1' is already taken, so your team has been renamed to '%2'. - - - - - - Cannot delete default scheme '%1'! - - - - - Please select a record from the list - - - - - Hedgewars - Nick not registered - - - - - Unable to start server - - - - - The connection to the server is lost. - - - - - Server redirection - - - - - This server supports secure connections on port %1. -Would you like to reconnect securely? - - - - Connection to server is lost - 服务器连接丢失 - - - - Not all players are ready - - - - - Are you sure you want to start this game? -Not all players are ready. - - - - - - Hedgewars - Error - - - - - System Information Preview - - - - - - Failed to generate captcha - - - - - Failed to download captcha - - - - - Please fill out all fields. Email is optional. - - - - - - Hedgewars - Success - - - - - All file associations have been set - - - - - File association failed. - - - - - - Netgame - Error - - - - - Please select a server from the list - - - - - Please enter room name - - - - - Room Name - Error - - - - - Please select room from the list - - - - - Room Name - Are you sure? - - - - - The game you are trying to join has started. -Do you still want to join the room? - - - - - Schemes - Warning - - - - - Schemes - Are you sure? - - - - - Do you really want to delete the game scheme '%1'? - - - - - Schemes - Name already taken - - - - - A scheme with the name '%1' already exists. Your scheme has been renamed to '%2'. - - - - - - Videos - Are you sure? - - - - - Do you really want to delete the video '%1'? - - - - - Do you really want to remove %1 file(s)? - - - - - - - - - File error - - - - - Cannot open '%1' for writing - - - - - - Cannot open '%1' for reading - - - - - - Weapons - Warning - - - - - A weapon scheme with the name '%1' already exists. Changes made to the weapon scheme have been discarded. - - - - - Cannot delete default weapon set '%1'! - - - - - Weapons - Are you sure? - - - - - Do you really want to delete the weapon set '%1'? - - - - - Hedgewars - Warning - - - - - Hedgewars - Information - - - - - Welcome to Hedgewars - - - - - Welcome to Hedgewars! - -You seem to be new around here. Would you like to play some training missions first to learn the basics of Hedgewars? - - - - - Cannot use the weapon scheme '%1'! - + 刺猬 %1 + + + unnamed + 未命名 + + + unnamed (%1) + 未命名 (%1) - QObject - - - - No description available - - - - - QPushButton - - - Play demo - 播放 demo - - - - Connect - 连接 - - - - Specify address - - - - Go! - 上场! - - - - Reset - - - - - Set the default server port for Hedgewars - - - - - Invite your friends to your server in just 1 click! - - - - - Click to copy your unique server URL to your clipboard. Send this link to your friends and they will be able to join you. - - - - - - - Start - 开始 - - - - Start private server - - - - - Start server - 开始服务端 - - - - Update - 更新 - - - - Load - 读取 - - - Specify - 指定 - - - - default - 默认 - - - - Rename - 重命名 - - - - OK - 确定 - - - - - Cancel - 取消 - - - - - - Delete - 删除 - - - - More info - - - - - Associate file extensions - - - - - Set default options - - - - - Restore default coding parameters - - - - - Open videos directory - - - - - Open the video directory in your system - - - - - Play - - - - - Play this video - - - - - Delete this video - - - - - QSpinBox - - - Specify the bitrate of recorded videos as a multiple of 1024 bits per second - + HWGame + + en.txt + zh_CN.txt + + + A fatal ERROR occured! The game engine had to stop. + +We are very sorry for the inconvenience. :-( + +If this keeps happening, please click the 'Feedback' button in the main menu! + +Last engine message: +%1 + 发生了致命错误!游戏引擎已停止。 + +很抱歉造成不便 _(:з」∠)_ + +如果继续发生,请点击主菜单的“反馈”按钮! + +最后的引擎消息: +%1 + + + Cannot open demofile %1 + 不能打开demo文件 %1 - RoomNamePrompt - - - Enter a name for your room. - - - - - set password - - - - - Cancel - 取消 - - - - - Create room - - - - - RoomsListModel - - - In progress - - - - - Room Name - - - - - C - Caption of the column for the number of connected clients in the list of rooms - - - - - T - Caption of the column for the number of teams in the list of rooms - - - - - Owner - - - - - Map - 地图 - - - - Script - - - - - Rules - - - - - Weapons - 武器 - - - - Random Map - - - - - Random Maze - - - - - Random Perlin - - - - - Hand-drawn - - - - - Forts - - - - - SeedPrompt - - - Seed - Refers to the "random seed"; the source of randomness in the game - - - - - The map seed is the basis for all random values generated by the game. - - - - - Cancel - 取消 - - - - Set seed - - - - - Close - - - - - SelWeaponWidget - - - Weapon set - - - - - Probabilities - - - - - Ammo in boxes - - - - - Delays - - - - - New - 新游戏 - - - - New (%1) - - - - - Copy of %1 - - - - - Copy of %1 (%2) - - - - new - + KeyBinder + + Warning: The same key is assigned multiple times! + 警告:相同按键分配多次! + + + Category + 类别 TCPBase - Unable to start server at %1. - - - - + 无法启动服务器在 %1. + + Unable to run engine at %1 Error code: %2 - - - - + 无法运行引擎在 %1 +Error code: %2 + + The game engine died unexpectedly! (exit code %1) We are very sorry for the inconvenience :( If this keeps happening, please click the '%2' button in the main menu! - + 游戏引擎意外死亡! +(exit code %1) + +很抱歉造成不便 _(:з」∠)_ + +如果继续发生,请点击主菜单的 ‘%2’ 按钮! + + + + QMainWindow + + Hedgewars %1 + 刺猬战争 %1 TeamSelWidget - At least two teams are required to play! - - - - - ThemePrompt - - - Choose a theme - - - - - Search for a theme: - - - - - Cancel - 取消 - - - - Use selected theme - - - - - binds - - - - up - - - - - - left - - - - - - right - - - - - - down - - - - - attack - 攻击 - - - - put - - - - - switch - 切换 - - - - stand still on slippery land - - - - - change direction without moving - - - - - backwards jump - - - - - switch backwards - - - - - slot 1 - slot 1 - - - - slot 2 - slot 2 - - - - slot 3 - slot 3 - - - - slot 4 - slot 4 - - - - slot 5 - slot 5 - - - - slot 6 - slot 6 - - - - slot 7 - slot 7 - - - - slot 8 - slot 8 - - - - slot 10 - slot 10 - - - - unselect weapon - - - - - timer 1 sec - 定时1秒 - - - - timer 2 sec - 定时2秒 - - - - timer 3 sec - 定时3秒 - - - - timer 4 sec - 定时4秒 - - - - timer 5 sec - 定时5秒 - - - - change timer - - - - - change bounciness - - - - - autocam / find hedgehog - - - - - zoom in - - - - - zoom out - - - - - pause / auto skip - - - - - mute audio - - - - - screenshot - - - - capture - 夺取 - - - - save map as image - - - - - speed up replay - - - - - show mission information - - - - - show object information - - - - - toggle team bars - This refers to the team info bars (name/flag/health) of all teams. These are shown at the bottom center of the screen - - - - - toggle hedgehog tags - - - - - change hedgehog tag types - - - - - toggle hedgehog tag translucency - - - - - toggle HUD - - - - - record - - - - - quit - 退出 - - - find hedgehog - 找到 刺猬 - - - - ammo menu - 弹药菜单 - - - - long jump - - - - - high jump - - - - - reset zoom to start value - - - - - set zoom to 100% - - - - - clan chat - - - - - volume down - 降低音量 - - - - volume up - 提高音量 - - - - change mode - 改变模式 - - - pause - 暂停 - - - - slot 9 - slot 9 - - - - chat - 聊天 - - - - chat history - 聊天记录 - - - - confirmation - 确认 - - - - precise aim - 练习瞄准 - - - - binds (categories) - - - Movement - - - - - Weapons - 武器 - - - - Camera - - - - - Miscellaneous - - - - - binds (combination) - - - hold down precise - - - - - precise + left/right - - - - - high jump (twice) - - - - - precise + switch - - - - - precise + timer - - - - - precise + reset zoom - - - - - precise + screenshot - - - - - precise + toggle hedgehog tags - - - - - switch + toggle hedgehog tags - - - - - precise + switch + toggle hedgehog tags - + 需要至少两个队伍才能玩! - binds (descriptions) - - - Traverse gaps and obstacles by jumping: - - - - - Fire your selected weapon or trigger an utility item: - - - - - Pick a weapon or a target location under the cursor: - - - - - Switch your currently active hog (if possible): - - - - - Hedgehog movement - - - - - Pick a weapon or utility item: - - - - - Set the timer on bombs and timed weapons: - - - - - Toggle automatic camera / refocus on active hedgehog: - - - - - Move the cursor or camera without using the mouse: - - - - - Modify the camera's zoom level: - - - - - Talk to your clan or all participants: - - - - - Pause, continue or leave your game: - - - - - Modify the game's volume while playing: - - - - - Toggle fullscreen mode: - - - - - Take a screenshot: - - - - - Demo replay: - - - - - Heads-up display: - - - - - Record video: - + DataManager + + Use Default + 使用默认 - binds (keys) - - - Mouse: Left button - - - - - Mouse: Middle button - - - - - Mouse: Right button - - - - - Mouse: X1 button - - - - - Mouse: X2 button - - - - - Mouse: Wheel up - - - - - Mouse: Wheel down - - - - - Backspace - - - - - Tab - - - - - Clear - - - - - Return - - - - - Pause - - - - - Escape - - - - - Space - - - - - Delete - 删除 - - - - - Up - - - - - - Down - - - - - - Right - - - - - - Left - - - - - Insert - - - - - Home - - - - - End - - - - - Keypad 0 - - - - - Keypad 1 - - - - - Keypad 2 - - - - - Keypad 3 - - - - - Keypad 4 - - - - - Keypad 5 - - - - - Keypad 6 - - - - - Keypad 7 - - - - - Keypad 8 - - - - - Keypad 9 - - - - - Keypad . - - - - - Keypad / - - - - - Keypad * - - - - - Keypad - - - - - - Keypad + - - - - - Keypad Enter - - - - - PageUp - - - - - PageDown - - - - - Numlock - - - - - CapsLock - - - - - ScrollLock - - - - - Menu - - - - - Right Shift - - - - - Left Shift - - - - - Right Ctrl - - - - - Left Ctrl - - - - - Right Alt - - - - - Left Alt - - - - - Right GUI - Windows key / Command key / Meta key /Super key (right) - - - - - Left GUI - Windows key / Command key / Meta key /Super key (left) - - - - - A button - - - - - B button - - - - - X button - - - - - Y button - - - - - LB button - - - - - RB button - - - - - Back button - - - - - Start button - - - - - Left stick - - - - - Right stick - - - - - Left stick (Right) - - - - - Left stick (Left) - - - - - Left stick (Down) - - - - - Left stick (Up) - - - - - Left trigger - - - - - Right trigger - - - - - Right stick (Down) - - - - - Right stick (Up) - - - - - Right stick (Right) - - - - - Right stick (Left) - - - - - D-pad - - - - - Axis %1 %2 - Game controller axis direction. %1 = axis number, %2 = direction - - - - - Button %1 - Game controller button. %1 = button number - - - - - D-pad %1 %2 - Game controller D-pad button. %1 = D-pad number, %2 = direction - - - - - (Don't use) - Special entry in key selection when an action has no control assigned - - - - - (QWERTY) - Name of QWERTY US keyboard layout - - - - - Keyboard - + QSpinBox + + Specify the bitrate of recorded videos as a multiple of 1024 bits per second + 将录制视频的比特率指定为每秒 1024 位的倍数 - credits - - - Programming - - - - - Game engine - - - - - Creator - - - - - Many engine improvements - - - - - Gamepad and Lua integration - - - - - Campaign support - - - - - Theme customization improvements - - - - - Some Pas2C and GLES2 work - - - - - Video recording - - - - - Other improvements - - - - - Map generation - - - - - Core map generators - - - - - Perlin maps and other improvements - - - - - Maze maps - - - - - Weapons - 武器 - - - - Most core weapons - - - - - Air mine, rubber, others - - - - - Drill rocket, ballgun, RC plane - - - - - Freezer - - - - - Mine number and time game settings - - - - - Frontend / main menu - - - - - Many frontend improvements - - - - - Keybinds, feedback, maps and hats interfaces - - - - - Login dialogs, other improvements - - - - - Missions and styles - - - - - A Classic Fairytale - - - - - A Space Adventure - - - - - Created Capture the Flag, Construction Mode, Control, HedgeEditor, Highlander, Racer, TechRacer, The Specialists, WxW - - - - - Training, time-trial and target practice challenges, Bazooka Battlefield, Tentacle Terror, Big Armory, bugfixes and maintenance - - - - - Some styles and missions - - - - - Battalion - - - - - Continental supplies - - - - - Teamwork 2 - - - - - Climb Home - - - - - Portal Mind Challenge - - - - - Game server - - - - - Ports - - - - - macOS/iPhone port, OpenGL-ES conversion - - - - - Android port - - - - - Android netplay, portability abstraction - - - - - WebGL port - - - - - iPhone/iPad ports - - - - - Graphics - - - - - General - 常规 - - - - Themes - - - - - Nature, Snow, City, Castle, Halloween, Island - - - - - Bamboo, EarthRise, BambooPlinko - - - - - Golf, Hoggywood, Stage - - - - - Hoggywood - - - - - Cave, Olympics - - - - - Fruit, Cake - - - - - Art - - - - - Beach - - - - - Brick - - - - - Hell - - - - - Jungle - - - - - Sheep - - - - - Maps - - - - - Basketball, BasketballField, Bath, Bubbleflow, Hammock, Hedgelove, Hedgewars, Hydrant, Mushrooms, Plane, Ropes, Tree - - - - - SB_Bones, SB_Crystal, SB_Grassy, SB_Grove, SB_Haunty, SB_Oaks, SB_Shrooms, SB_Tentacle - - - - - Bamboo, Blox, Cake, Cogs, EarthRise, Freeway - - - - - Castle, PirateFlag - - - - - ShoppaKing, TrophyRace - - - - - Battlefield - - - - - CTF_Blizzard - - - - - Cheese - - - - - ClimbHome - - - - - Lonely_Island - - - - - Octorama - - - - - portal - - - - - Ruler - - - - - Sticks - - - - - Forts - - - - - EvilChicken - - - - - Olympic - - - - - Tank - - - - - Snail - - - - - SteelTower - - - - - Hats, graves, other - - - - - See CREDITS text file - - - - - Sounds - - - - - Hedgehogs voice - - - - - Default_pl, Russian_pl voices - - - - - Various authors from www.freesound.org (see CREDITS text file) - - - - - Music - - - - - City, Rock, others - - - - - Compost - - - - - EarthRise, oriental, Pirate, snow - - - - - Fruit, Jungle - - - - - Nature - - - - - olympics_sd - - - - - sdmusic (Hitman [sheepluva edit]) - - - - - Translations - - - - - Brazilian Portuguese - - - - - Bulgarian - - - - - Czech - - - - - Chinese - - - - - Finnish - - - - - French - - - - - German - - - - - Greek - - - - - Italian - - - - - Japanese - - - - - Korean - - - - - Lithuanian - - - - - Polish - - - - - Portuguese - - - - - Russian - - - - - Scottish Gaelic - - - - - Slovak - - - - - Spanish - - - - - Swedish - - - - - Ukrainian - - - - - Special thanks - - - - - Project founder - + HWHostPortDialog + + Connect to server + 连接到服务器 + + + + PageInGame + + In game... + 游戏中... - server - - - New voting started - - - - - kick - - - - - map - - - - - pause - 暂停 - - - - new seed - - - - - /maxteams: specify number from 2 to 8 - - - - - Super power activated. - - - - - - Unknown command or invalid parameters. Say '/help' in chat for a list of commands. - - - - - Nickname is already in use - - - - - This server only allows registered users to join. - - - - - No checker rights - - - - - Authentication failed - - - - - 60 seconds cooldown after kick - - - - - Kicked - - - - - kicked - - - - - Reconnected too fast - - - - - Ping timeout - - - - - heads - - - - - tails - - - - - This server does not support replays! - - - - - This server no longer allows unregistered players to join. - - - - - This server now allows unregistered players to join. - - - - - /info <player>: Show info about player - - - - - /me <message>: Chat action, e.g. '/me eats pizza' becomes '* Player eats pizza' - - - - - /rnd: Flip a virtual coin and reply with 'heads' or 'tails' - - - - - /rnd [A] [B] [C] [...]: Reply with a random word from the given list - - - - - /watch <id>: Watch a demo stored on the server with the given ID - - - - - /quit: Quit the server - - - - - /help: Show chat command help - - - - - /callvote [arguments]: Start a vote - - - - - /vote <yes/no>: Vote 'yes' or 'no' for active vote - - - - - /delegate <player>: Surrender room control to player - - - - - /maxteams <N>: Limit maximum number of teams to N - - - - - /global <message>: Send global chat message which can be seen by everyone on the server - - - - - /registered_only: Toggle 'registered only' state. If enabled, only registered players can join server - - - - - /super_power: Activate your super power. With it you can enter any room and are protected from kicking. Expires when you leave server - - - - - /stats: Query server stats - - - - - /force <yes/no>: Force vote result for active vote - - - - - /fix: Force this room to stay open when it is empty - - - - - /unfix: Undo the /fix command - - - - - List of lobby chat commands: - - - - - List of room chat commands: - - - - - Commands for server admins only: - - - - - Warning! Room name change flood protection activated - - - - - This command is only available in the lobby. - - - - - This command is only available in rooms. - - - - - room - - - - - lobby - - - - - (playing) - - - - - (spectating) - - - - - Player is not online. - - - - - The game can't be started with less than two clans! - - - - - Empty config entry. - - - - - Access denied. - - - - - You're not the room master! - - - - - Corrupted hedgehogs info! - - - - - Too many teams! - - - - - Too many hedgehogs! - - - - - There's already a team with same name in the list. - - - - - Joining not possible: Round is in progress. - - - - - This room currently does not allow adding new teams. - - - - - Error: The team you tried to remove does not exist. - - - - - You can't remove a team you don't own. - - - - - 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: $()*+?[]^{|} - - - - - A room with the same name already exists. - - - - - You can't kick yourself! - - - - - You can't kick the only other player! - - - - - The player is not in your room. - - - - - This player is protected from being kicked. - - - - - You're not the room master or a server admin! - - - - - You're already the room master. - - - - - Greeting message cleared. - - - - - Greeting message set. - - - - - Available callvote commands: hedgehogs <number>, pause, newseed, map <name>, kick <player> - - - - - /callvote kick: You need to specify a nickname. - - - - - /callvote kick: This is only allowed in rooms without a room master. - - - - - /callvote kick: No such user! - - - - - /callvote map: No maps available. - - - - - /callvote map: No such map! - - - - - /callvote pause: No game in progress! - - - - - /callvote hedgehogs: Specify number from 1 to 8. - - - - - /force: Please use 'yes' or 'no'. - - - - - /vote: Please use 'yes' or 'no'. - - - - - 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: $()*+?[]^{|} - - - - - No such room. - - - - - Room version incompatible to your Hedgewars version! - - - - - Access denied. This room currently doesn't allow joining. - - - - - Access denied. This room is for registered users only. - - - - - You are banned from this room. - - - - - Please confirm server restart with '/restart_server yes'. - - - - - Nickname already provided. - - - - - 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: $()*+?[]^{|} - - - - - Protocol already known. - - - - - Bad number. - - - - - There's no voting going on. - - - - - You already have voted. - - - - - Your vote has been counted. - - - - - Voting closed. - - - - - Pause toggled. - - - - - Voting expired. - - - - - hedgehogs per team: - - - - - You're the new room master! - - - - - Warning! Chat flood protection activated - - - - - /greeting [message]: Set or clear greeting message to be shown to players who join the room - - - - - /save <config ID> <config name>: Add current room configuration as votable choice for /callvote map - - - - - /delete <config ID>: Delete a votable room configuration - - - - - /saveroom <file name>: Save all votable room configurations (and the greeting) of this room into a file - - - - - /loadroom <file name>: Load votable room configurations (and greeting) from a file - - - - - Excess flood - - - - - Game messages flood detected - 1 - - - - - Warning! Joins flood protection activated - + AbstractPage + + Go back + 返回 + + + + HWAskQuitDialog + + Do you really want to quit? + 你确定要退出? diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hedgewars_zh_TW.ts --- a/share/hedgewars/Data/Locale/hedgewars_zh_TW.ts Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hedgewars_zh_TW.ts Sun Mar 24 14:33:57 2024 -0400 @@ -342,6 +342,7 @@ Scheme '%1' not supported + Here, “scheme” refers to the scheme of a Uniform Resource Identifier” 不支持遊戲方案“%1” @@ -553,7 +554,7 @@ Your nickname is not registered. To prevent someone else from using it, please register it at www.hedgewars.org - 你的暱稱未註冊。 + 你的暱稱未註冊。 要防止其他人使用它, 請上www.hedgewars.org進行註冊 @@ -633,6 +634,13 @@ Internal error: Reply object is invalid. 內部錯誤: 回覆的對象是無效的 + + Your nickname is not registered. +To be able to rejoin games in progress and +prevent someone else from using your nickname, +please register it at www.hedgewars.org. + + HWGame @@ -1483,7 +1491,7 @@ Save - 存檔 + 存檔 (%1 %2) @@ -1528,6 +1536,14 @@ (%1 箱子) + + Save demo + + + + Save demo (unavailable because the /lua command was used) + + PageInGame @@ -2186,6 +2202,10 @@ %1 (%2) + + Average number of sentry bots to be placed on a medium-sized island map. This number will be scaled for other maps. + + PageSelectWeapon @@ -2991,6 +3011,14 @@ Zoom (%) 畫面縮放(%) + + Chat size (%) + + + + Sentry Bots + + QLineEdit @@ -3960,7 +3988,7 @@ precise + switch + toggle hedgehog tags - 精細瞄準 + 切換 + 隊伍資訊欄開關 + 精細瞄準 + 切換 + 隊伍資訊欄開關 high jump (twice) @@ -3970,6 +3998,10 @@ precise + screenshot 精細瞄準 + 擷圖 + + precise + switch + toggle team bars + + binds (descriptions) @@ -4982,6 +5014,10 @@ Project founder 項目創始人 + + Hungarian + + server diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/hu.txt --- a/share/hedgewars/Data/Locale/hu.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/hu.txt Sun Mar 24 14:33:57 2024 -0400 @@ -363,7 +363,7 @@ 02:07=Svájci bicska doboz formában 02:07=Csináld magad! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 unalmas… 02:08=%1 hiába fáradt 02:08=%1 eléggé lusta @@ -398,7 +398,7 @@ 02:08=%1 épp mással foglalkozik 02:08=%1 elaludt -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 gyakorolhatna kicsit! 02:09=%1 látszólag utálja magát 02:09=%1 rossz oldalra állt! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/it.lua --- a/share/hedgewars/Data/Locale/it.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/it.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 ["Flesh for Brainz"] = "Carne per Brainz", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory ["You have killed an innocent hedgehog!"] = "Hai ucciso un riccio innocente!", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/it.txt --- a/share/hedgewars/Data/Locale/it.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/it.txt Sun Mar 24 14:33:57 2024 -0400 @@ -299,7 +299,7 @@ 02:07=Potrebbe servirti 02:07=Utilissime queste utilità! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 è così noioso... 02:08=%1 deve riordinare le idee 02:08=%1, se non volevi giocare lo potevi anche dire prima! @@ -339,7 +339,7 @@ 02:08=%1 desidera lasciare che il nemico lo sconfigga 02:08=%1, domande, dubbi, perplessità? -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 dovrebbe migliorare la sua mira! 02:09=%1 sembra odiarsi... 02:09=%1 ha manie suicide... diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/ja.txt --- a/share/hedgewars/Data/Locale/ja.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/ja.txt Sun Mar 24 14:33:57 2024 -0400 @@ -209,7 +209,7 @@ 02:07=道具の時間です! 02:07=この中には、一体何があるのでしょうか? -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1が退屈そうです。 02:08=%1が寝ているようです。 02:08=%1が休憩をとります。 @@ -221,7 +221,7 @@ 02:08=%1が息抜きをしています。 02:08=%1が眠っています。 -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1の行動は、もちろんわざとでしたね。 ; The real saying has けってん = shortcoming, whereas てっけん = clenched fist 02:09=大丈夫ですよ、%1。誰にでも「鉄拳」があります! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/ko.lua --- a/share/hedgewars/Data/Locale/ko.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/ko.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/ko.txt --- a/share/hedgewars/Data/Locale/ko.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/ko.txt Sun Mar 24 14:33:57 2024 -0400 @@ -92,10 +92,10 @@ ; New utility crate 02:07=장치상자! -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 통과했다. -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 피학대 성애자인 것 캍아... ; Hog shot an home run (using the bat and another hog) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/lt.lua --- a/share/hedgewars/Data/Locale/lt.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/lt.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/lt.txt --- a/share/hedgewars/Data/Locale/lt.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/lt.txt Sun Mar 24 14:33:57 2024 -0400 @@ -309,7 +309,7 @@ 02:07=Ooo sunki dėžė 02:07=Tau gali šito prireikti -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 yra toks nuobodus... 02:08=%1 net nesumirksėjo 02:08=%1 yra vienas tingus ežys @@ -347,7 +347,7 @@ 02:08=%1 persigando 02:08=%1 užmigo -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 turėtų treniruoti taiklumą! 02:09=%1 nekenčia savęs 02:09=%1 stovi ne toje pusėje! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/missions_pt.txt diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/missions_zh_CN.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Locale/missions_zh_CN.txt Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,98 @@ +Basic_Training_-_Movement.name=基础移动训练 +Basic_Training_-_Movement.desc="新手在这里开始!学会在不同地形移动和切换刺猬" + +Basic_Training_-_Bazooka.name=基础火箭筒训练 +Basic_Training_-_Bazooka.desc="学会使用火箭筒, 了解风,并摧毁所有目标" + +Basic_Training_-_Grenade.name=基础手榴弹训练 +Basic_Training_-_Grenade.desc="学会使用手榴弹,并摧毁所有目标" + +Basic_Training_-_Rope.name=基础绳索训练 +Basic_Training_-_Rope.desc="绳索是你能得到的最实用的工具之一,但这需要大量练习" + +Basic_Training_-_Flying_Saucer.name=基础飞碟训练 +Basic_Training_-_Flying_Saucer.desc="学会操控飞碟飞行,在飞行中射击,甚至表演特技" + +User_Mission_-_Dangerous_Ducklings.name=危险的小鸭子 +User_Mission_-_Dangerous_Ducklings.desc="是时候进行实战训练了" + +User_Mission_-_Diver.name=潜水员 +User_Mission_-_Diver.desc="这个水陆两用的东西,看着简单,用起来难" + +User_Mission_-_Teamwork.name=团队合作 +User_Mission_-_Teamwork.desc="一个故障的机器人保护着有价值的军事机密,为了获得机密,你需要带领两个刺猬组成的特别行动队伍打败敌人" + +User_Mission_-_Teamwork_2.name=团队合作 2 +User_Mission_-_Teamwork_2.desc="我们找到了机器帝国的秘密前哨,它只由无害的监视机器人守卫。带领你的特别行动队伍摧毁监视机器人,这样我们就能宣称这个基地是我们的" + +User_Mission_-_Spooky_Tree.name=怪树 +User_Mission_-_Spooky_Tree.desc="这里有很多空投箱。我希望那些鸟不是很饿" + +User_Mission_-_Bamboo_Thicket.name=竹林 +User_Mission_-_Bamboo_Thicket.desc="一个机器人正在威胁竹林,用几乎完美的准确度攻击进入视线的任何人。提前计划, 快速行动除掉敌人" + +User_Mission_-_That_Sinking_Feeling.name=淹没的感觉 +User_Mission_-_That_Sinking_Feeling.desc="水涨得很快,时间有限,很多人尝试过但失败了,你能拯救它们所有人吗?" + +User_Mission_-_Newton_and_the_Hammock.name=牛顿和吊床 +User_Mission_-_Newton_and_the_Hammock.desc="小刺猬们记住:除非受到外力作用,身体的速度保持不变" + +User_Mission_-_The_Great_Escape.name=大逃离 +User_Mission_-_The_Great_Escape.desc="逃离监狱并复仇" + +User_Mission_-_Rope_Knock_Challenge.name=绳索撞击 +User_Mission_-_Rope_Knock_Challenge.desc="使用你的绳索把敌人撞下去" + +User_Mission_-_Nobody_Laugh.name=没人笑 +User_Mission_-_Nobody_Laugh.desc="Oh, 这些小丑认为它们很搞笑!它们数量超过我们,但我们有很多回合时间。除掉所有喜剧演员,直到这里没有人笑" + +User_Mission_-_RCPlane_Challenge.name=遥控飞机挑战 +User_Mission_-_RCPlane_Challenge.desc="使用遥控飞机获得所有箱子。尽可能使用更少的遥控飞机提高你的排名" + +Big_Armory.name=大武器库 +Big_Armory.desc="你独自作战,拥有完整的武器库,且必须在时间结束前打败所有八个刺猬" + +Bazooka_Battlefield.name=火箭筒战场 +Bazooka_Battlefield.desc="你忠诚的刺猬埋伏了敌人。只用火箭筒打败它们,但不要浪费时间,水面会很快上升" + +Tentacle_Terror.name=恐怖触手 +Tentacle_Terror.desc="恐怖的怪物之下,你的敌人像懦夫一样躲起来,当你失去掩护就用空袭攻击你。你需要神乎其技的绳索技术才有机会" + +ClimbHome.name=爬回家 +ClimbHome.desc="你离家很远,水面在上升,尽可能地爬到高处" + +portal.name=传送门思维挑战 +portal.desc="使用传送门快速移动、到达远处、杀死刺猬,请小心使用" + +Target_Practice_-_Bazooka_easy.name=目标训练: 火箭筒(简单) +Target_Practice_-_Bazooka_easy.desc="尽快打中这些目标" + +Target_Practice_-_Bazooka_hard.name=目标训练: 火箭筒(困难) +Target_Practice_-_Bazooka_hard.desc="你能打中很远的目标吗?" + +Target_Practice_-_Cluster_Bomb.name=目标训练: 集束炸弹 +Target_Practice_-_Cluster_Bomb.desc="有人需要用集束炸弹洗个热水澡" + +Target_Practice_-_Shotgun.name=目标训练: 霰弹枪 +Target_Practice_-_Shotgun.desc="先射击,再问问题" + +Basic_Training_-_Sniper_Rifle.name=目标训练: 狙击枪 +Basic_Training_-_Sniper_Rifle.desc="对于狙击手来说这是完美的射击范围,快速准确摧毁所有目标" + +Target_Practice_-_Homing_Bee.name=目标训练: 蜜蜂枪 +Target_Practice_-_Homing_Bee.desc="使用蜜蜂枪比看起来要难" + +Target_Practice_-_Grenade_easy.name=目标训练: 手榴弹(简单) +Target_Practice_-_Grenade_easy.desc="给有抱负的榴弹手的热身训练" + +Target_Practice_-_Grenade_hard.name=目标训练: 手榴弹(困难) +Target_Practice_-_Grenade_hard.desc="这不是给新手的,我们会把目标放在难打的位置" + +Challenge_-_Speed_Shoppa_-_Hedgelove.name=时间试炼: Shoppa Love +Challenge_-_Speed_Shoppa_-_Hedgelove.desc="在这个小地图收集一些箱子" + +Challenge_-_Speed_Shoppa_-_Ropes.name=时间试炼: Ropes and Crates +Challenge_-_Speed_Shoppa_-_Ropes.desc="在这个中等地图收集所有箱子" + +Challenge_-_Speed_Shoppa_-_ShoppaKing.name=时间试炼: The Customer is King +Challenge_-_Speed_Shoppa_-_ShoppaKing.desc="在这个大地图尽快收集所有箱子" diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/nl.txt --- a/share/hedgewars/Data/Locale/nl.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/nl.txt Sun Mar 24 14:33:57 2024 -0400 @@ -294,7 +294,7 @@ 02:07=Ooo this box is heavy 02:07=You might need this -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 is sooo boring... 02:08=%1 couldn't be bothered 02:08=%1 is one lazy hog @@ -332,7 +332,7 @@ 02:08=%1 is scared stiff 02:08=%1 has fallen asleep -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 should practice aiming! 02:09=%1 seems to hate himself 02:09=%1 is standing on the wrong side! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/pl.lua --- a/share/hedgewars/Data/Locale/pl.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/pl.lua Sun Mar 24 14:33:57 2024 -0400 @@ -462,8 +462,8 @@ ["Defeat all enemies!"] = "Pokonaj wszystkich wrogów!", -- portal ["Defeat!"] = "Porażka!", -- HedgeEditor ["Defeat Professor Hogevil!"] = "Pokonaj Profesora Jeżozło!", -- A_Space_Adventure:death01 + ["Defeat the cannibals!|Grenade hint: set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"] = "Pokonaj kanibali!|Porada do granatów: ustaw zapalnik używając [1-5], celuj [Góra]/[Dół] i przytrzymaj [Spację], by ustawić moc", -- A_Classic_Fairytale:shadow ["Defeat the cannibals!"] = "Pokonaj kanibali!", -- A_Classic_Fairytale:shadow - ["Defeat the cannibals!|Grenade hint: set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"] = "Pokonaj kanibali!|Porada do granatów: ustaw zapalnik używając [1-5], celuj [Góra]/[Dół] i przytrzymaj [Spację], by ustawić moc", -- A_Classic_Fairytale:shadow ["Defeat the cyborgs!"] = "Pokonaj cyborgów!", -- A_Classic_Fairytale:enemy ["Defeat the enemy!"] = "Pokonaj wroga!", -- A_Classic_Fairytale:queen ["Delete Waypoint"] = "Usuń punkt kontrolny", -- HedgeEditor @@ -485,8 +485,8 @@ ["Destroy him, Leaks A Lot! He is responsible for the deaths of many of us!"] = "Zniszcz go, Spory Przecieku! On jest odpowiedzalny za śmierć wielu z nas!", -- A_Classic_Fairytale:first_blood ["Destroy invaders and collect bonuses to score points."] = "Niszcz najeźdźców i zbieraj bonusy, by zaliczać punkty.", -- Space_Invasion ["- Destroy the enemy"] = "- Zniszcz wroga", -- HedgeEditor + ["- Destroy the red targets"] = "- Zniszcz czerwone cele", -- HedgeEditor ["- Destroy the red target"] = "- Zniszcz czerwony cel", -- HedgeEditor - ["- Destroy the red targets"] = "- Zniszcz czerwone cele", -- HedgeEditor ["Destroy the targets!"] = "Zniszcz cele!", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade ["+%d flamer fuel!"] = "+%d paliwa miotacza ognia!", -- Tumbler ["+%d health"] = "+%d zdrowia", -- Mutant @@ -677,11 +677,13 @@ ["Flawless victory!"] = "Bezbłędne zwycięstwo!", -- User_Mission_-_RCPlane_Challenge ["Flee: Press [Jump]"] = "Ucieknij: Wciśnij [Skok]", -- A_Space_Adventure:fruit01 ["Flesh for Brainz"] = "Mięso dla Mózgów", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope ["Fly around and hurl explosives to your enemies."] = "Lataj dookoła i ciskaj ładunki wybuchowe na swoich wrogów.", -- Tumbler ["Flying Saucer Training"] = "Trening latającego talerza", -- Basic_Training_-_Flying_Saucer ["Fly into space to fight off the invaders with barrels!"] = "Leć w kosmos, by odeprzeć najeźdźców beczkami!", -- Space_Invasion ["Fly to the meteorite and detonate the explosives"] = "Poleć do meteorytu i zdetonuj ładunki", -- A_Space_Adventure:cosmos ["Follow the path and destroy the next target."] = "Podążaj ściezką i zniszcz następny cel.", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking ["Forgetfulness: You will lose all your weapons each turn."] = "Zapominalstwo: W każdej turze stracisz wszystkie swoje bronie.", -- Continental_supplies ["For the next crate, you have to do back jumps."] = "Do następnej skrzyni musisz zrobić skoki w tył.", -- Basic_Training_-_Movement ["Four Eyes"] = "Czterooki", @@ -1378,8 +1380,8 @@ ["Minions"] = "Sługusy", -- A_Space_Adventure:moon01 ["Mission failed!"] = "Misja zakończona niepowodzeniem!", -- Big_Armory ["Mission failure in %d s"] = "Porażka misji w %d s", -- Big_Armory + ["Mission lost!"] = "Misja stracona!", -- Basic_Training_-_Grenade ["Mission"] = "Misja", -- HedgeEditor - ["Mission lost!"] = "Misja stracona!", -- Basic_Training_-_Grenade ["Mission panel: [M]"] = "Panel misji: [M]", -- Basic_Training_-_Movement ["Mission Panel"] = "Panel misji", -- Basic_Training_-_Movement ["Mission succeeded!"] = "Misja ukończona!", -- portal, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, User_Mission_-_Diver, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork_2, User_Mission_-_Teamwork, SimpleMission, HedgeEditor @@ -1541,6 +1543,7 @@ ["Oneye"] = "Jednooki", -- portal ["Only one hog per team allowed! Excess hogs will be removed"] = "Dozwolony tylko jeden jeż na drużynę! Nadmiarowe jeże będą usunięte", -- Mutant ["Only one hog per team allowed! Excess hogs will be removed."] = "Dozwolony tylko jeden jeż na drużynę! Nadmiarowe jeże będą usunięte.", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant ["Only %s can be trusted with the crate."] = "Tylko jeżowi %s można ufać ze skrzynią.", -- A_Space_Adventure:fruit02 ["Only the best pilots can master the following stunts."] = "Tylko najlepsi piloci mogą opanować następujące wyczyny.", -- Basic_Training_-_Flying_Saucer ["Only two clans allowed! Excess hedgehogs will be removed."] = "Tylko dwa klany dozwolone! Nadmiarowe jeże będą usunięte.", -- CTF_Blizzard @@ -2851,6 +2854,7 @@ ["You have killed all enemies."] = "Zabiłeś wszystkich wrogów.", -- Big_Armory ["You have killed an innocent hedgehog!"] = "Zabiłeś niewinnego jeża!", -- A_Classic_Fairytale:backstab ["You have killed %d of 16 hedgehogs (+%d points)."] = "Zabiłeś %d z 16 jeży (+%d punktów)", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking ["You have launched %d bazookas."] = "Wystrzeliłeś %d bazook.", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka ["You have launched %d homing bees."] = "Wystrzeliłeś %d pszczół.", -- Target_Practice_-_Homing_Bee ["You have made %d shots."] = "Wykonałeś %d strzałów.", -- Basic_Training_-_Sniper_Rifle @@ -2985,6 +2989,6 @@ ["Zombi"] = "Zombi", -- portal ["'Zooka Team"] = "Bazookinierzy", ["Zoom: [Pinch] with 2 fingers"] = "Przybliż: [Uszczypnij] dwoma palcami", -- Basic_Training_-_Movement - ["Zoom: [Rotate mouse wheel]"] = "Przyybliż: [Obróć kółkiem myszy]", -- Basic_Training_-_Movement + ["Zoom: [Rotate mouse wheel]"] = "Przybliż: [Obróć kółkiem myszy]", -- Basic_Training_-_Movement ["Zork"] = "Zork", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen } diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/pl.txt --- a/share/hedgewars/Data/Locale/pl.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/pl.txt Sun Mar 24 14:33:57 2024 -0400 @@ -670,7 +670,7 @@ 04:50=Wezwij szwadron śmiercionośnych wierteł|by wykurzyć kogoś z kryjówki. Po użyciu|zostanie zrzuconych 6 wiertniczych bomb|wkręcających się w podłoże.|Lewo/Prawo: Określ kierunek ataku|Kursor: Wybierz miejsce zrzutu 04:51=Obrzuć kogoś błotem! Broń ta nie zadaje dużych|obrażeń ale potrafi zepchnąć z krawędzi!|Atak: Przytrzymaj by strzelić z większą siłą 04:52=UNUSED -04:53=Wybierz się w podróż w czasie i przestrzeni|zostawiając swoich kompanów samych na|polu walki. Bądź gotowy na powrót w każdej|chwili. Wrócisz też gdy rozpocznie się Nagła|Śmierć lub jeże zostaną pokonane.|Uwaga: nie zadziała podczas Nagłej Śmierci,|gdy jesteś sam lub jesteś Królem.|Atak: Aktywuj +04:53=Wybierz się w podróż w czasie i przestrzeni|zostawiając swoich kompanów samych na|polu walki. Bądź gotowy na powrót w każdej|chwili. Wrócisz też gdy rozpocznie się Nagła|Śmierć lub jeże zostaną pokonane.|Uwaga\: nie zadziała podczas Nagłej Śmierci,|gdy jesteś sam lub jesteś Królem.|Atak: Aktywuj 04:54=Wystrzel strumień kleistej mazi.|Buduj mosty, zasypuj wrogów, zatykaj tunele.|Uważaj by nie zasypać samego siebie! 04:55=Epoka lodowcowa powraca!!|Zamroź jeże, uczyń podłoże śliskim lub zapobiegnij|utonięciu zamrażając wodę.|Atak: Strzał 04:56=Rzuć w przeciwnika dwoma tasakami i zablokuj mu |drogę lub użyj ich do wspinaczki! Jednak uważaj! |Te tasaki są naprawdę ostre!|Atak: Przytrzymaj by rzucić z większą siłą (dwukrotnie) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/pt_BR.lua --- a/share/hedgewars/Data/Locale/pt_BR.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/pt_BR.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/pt_BR.txt --- a/share/hedgewars/Data/Locale/pt_BR.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/pt_BR.txt Sun Mar 24 14:33:57 2024 -0400 @@ -205,7 +205,7 @@ 02:07=A cegonha chegou 02:07=Não esqueça o seu cinto de utilidades -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 está entediado 02:08=%1 não sabe o que fazer 02:08=%1 não tem criatividade @@ -232,7 +232,7 @@ 02:08=%1 está paralizado de terror 02:08=%1 está pedindo para sair -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 se machucou 02:09=%1 fez dodói 02:09=%1 é uma anta diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/pt_PT.lua --- a/share/hedgewars/Data/Locale/pt_PT.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/pt_PT.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2850,6 +2853,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/pt_PT.txt --- a/share/hedgewars/Data/Locale/pt_PT.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/pt_PT.txt Sun Mar 24 14:33:57 2024 -0400 @@ -307,7 +307,7 @@ 02:07=Ooo esta caixa é pesada 02:07=Podes precisar disto -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 é tão aborrecido... 02:08=%1 não se quis incomodar 02:08=%1 é um ouriço tão preguiçoso @@ -345,7 +345,7 @@ 02:08=%1 está cheio de medo 02:08=%1 adormeceu -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 devia praticar a pontaria! 02:09=Parece que %1 se odeia a si mesmo 02:09=%1 está do lado errado! @@ -504,7 +504,7 @@ 04:50=Está alguém escondido num túnel subterrâneo?|Escava-os de lá para fora com um Ataque Perfurador!|O temporizador controla quão longe os misseis irão escavar. 04:51=Empurra um ouriço sem perder o turno|criando uma bola de lama tu mesmo! 04:52=UNUSED -04:53=Parte numa aventura pelo tempo e espaço,|deixando os teus colegas para se defenderem sozinhos.|Está preparado para regressar a qualquer altura,|para Morte Súbita ou se todos forem derrotados.|Atenção: Não funciona em Morte Súbita,|se estiveres sozinho, ou se fores o Rei. +04:53=Parte numa aventura pelo tempo e espaço,|deixando os teus colegas para se defenderem sozinhos.|Está preparado para regressar a qualquer altura,|para Morte Súbita ou se todos forem derrotados.|Atenção\: Não funciona em Morte Súbita,|se estiveres sozinho, ou se fores o Rei. 04:54=Aplica estas particulas de terreno em spray onde quiseres.|Constroi pontes, enterra inimigos ou fecha túneis.|Tem apenas cuidado, não o uses em ti proprio! ; Game goal strings diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/ro.txt --- a/share/hedgewars/Data/Locale/ro.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/ro.txt Sun Mar 24 14:33:57 2024 -0400 @@ -304,7 +304,7 @@ 02:07=Ooo this box is heavy 02:07=You might need this -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 is sooo boring... 02:08=%1 couldn't be bothered 02:08=%1 is one lazy hog @@ -342,7 +342,7 @@ 02:08=%1 is scared stiff 02:08=%1 has fallen asleep -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 should practice aiming! 02:09=%1 seems to hate himself 02:09=%1 is standing on the wrong side! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/ru.lua --- a/share/hedgewars/Data/Locale/ru.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/ru.lua Sun Mar 24 14:33:57 2024 -0400 @@ -676,11 +676,13 @@ ["Flawless victory!"] = "Безупречная победа!", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1538,6 +1540,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2844,6 +2847,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking ["You have launched %d bazookas."] = "Вы запустили %d базук.", -- Basic_Training_-_Bazooka ["You have launched %d homing bees."] = "Вы запустили %d пчёлок.", -- Target_Practice_-_Homing_Bee ["You have made %d shots."] = "Вы сделали %d выстрелов.", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/ru.txt --- a/share/hedgewars/Data/Locale/ru.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/ru.txt Sun Mar 24 14:33:57 2024 -0400 @@ -451,7 +451,7 @@ 02:07=О! Это тяжёлый ящик 02:07=Тебе это может понадобиться -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 такой скучный... 02:08=%1 ни о чем не беспокоится 02:08=%1 ленивый ёжик @@ -489,7 +489,7 @@ 02:08=%1 оцепенел от страха 02:08=%1 уснул -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 должен практиковаться в прицеливании! 02:09=%1 ненавидит себя 02:09=%1 перешел на сторону врага! @@ -677,7 +677,7 @@ 02:17=%1 больше не с нами 02:17=Наш любимый король %1 покинул нас -; Weapon Categories +; Weapon categories/subcaptions 03:00=Граната с таймером 03:01=Бомба с таймером 03:02=Баллистическое оружие @@ -741,7 +741,7 @@ 03:59=Незавершённое оружие 03:60=Наимощнейшая пушка -; Weapon Descriptions (use | as line breaks) +; Weapon descriptions (use | as line breaks) 04:00=Атакуй своих врагов обычной гранатой.|Она взорвется сразу, как только таймер достигнет нуля.|1-5: Установить таймер гранаты|Точность + 1-5: Установить силу отскока|Атака: Удерживай для более дальнего броска 04:01=Атакуй своих врагов касетной бомбой.|Она разорвётся на несколько меньших бомб,|когда таймер достигнет нуля.|1-5: Установить таймер бомбы|Точность + 1-5: Установить силу отскока|Атака: Удерживай для более дальнего броска 04:02=Атакуй своих врагов баллистическим снарядом,|на который может повлиять направление ветра.|Атака: Удерживай для выстрела с большей силой @@ -795,7 +795,7 @@ 04:50=Кто-то скрывается под землёй?|Достань их сверлящим ударом!|Таймер контролирует глубину бурения.|Влево/Вправо: Определить направление атаки|1-5: Установить таймер|Курсор: Выбрать бомбардируемую область 04:51=Швырни в противника комок грязи задаром!|Не наносит урона, но сталкивает|ёжиков и другие объекты назад.|Атака: Удерживай для более дальнего броска 04:52=Не используется -04:53=Проделайте путь сквозь время и пространство,|пока ваши соратники борятся в одиночестве.|Будьте готовы вернуться в любое время,|при Внезапной Смерти или когда все союзники повержены.|Предупреждение: Не работает во время Внезапной Смерти,|если вы один или если вы Король.|Атака: Активировать +04:53=Проделайте путь сквозь время и пространство,|пока ваши соратники борятся в одиночестве.|Будьте готовы вернуться в любое время,|при Внезапной Смерти или когда все союзники повержены.|Предупреждение\: Не работает во время Внезапной Смерти,|если вы один или если вы Король.|Атака: Активировать 04:54=Распыляет поток липких хлопьев.|Строит мосты, хоронит врагов, перекрывает туннели.|Будьте осторожны - не попадите на себя!|Атака: Включить/Выключить поток|Вверх/Вниз: Продолжать прицельную стрельбу|Влево/Вправо: Изменить силу (дальность) распыления 04:55=Bерните ледниковый период!|Замораживает ёжиков или объекты, делает пол скользким|или спасает вас от утопления, замораживая воду.|Атака: Включить/Выключить замораживатель|Вверх/Вниз: Продолжать прицельную стрельбу 04:56=Вы можете бросить два секача во врага,|заблокировать проходы и туннели|и даже использовать их для восхождения!|Урон зависит от их скорости.|Острожно! Игры с ножами опасны.|Атака: Удерживай для выстрела с большей силой (дважды) @@ -826,13 +826,13 @@ 05:18=Неограниченные атаки: Ход не заканчивается после атаки 05:19=Постоянное вооружение: Оружие поменяется в конце хода 05:20=Личное оружие: Ежи не имеют общего оружия -05:21=Признак команды: Команды в клане ходят последовательно|Общее время: Команды в клане имеют общее время хода +05:21=Признак команды: Команды в союзе ходят последовательно|Общее время: Команды в союзе имеют общее время хода 05:22=Сильный ветер: Ветер влияет почти на всё ; Chat command help 06:00=Список основных команд чата: 06:01=/togglechat: Отобразить чат -06:02=/clan <сообщение>: Отправить сообщение только участникам клана +06:02=/clan <сообщение>: Отправить сообщение только участникам союза 06:03=/me <сообщение>: Действие в чате, например "/me ест пиццу" становится "* Игрок ест пиццу" 06:04=/pause: Включить паузу 06:05=/pause: Включить автопропуск хода diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/sk.lua --- a/share/hedgewars/Data/Locale/sk.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/sk.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2853,6 +2856,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/sk.txt --- a/share/hedgewars/Data/Locale/sk.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/sk.txt Sun Mar 24 14:33:57 2024 -0400 @@ -439,7 +439,7 @@ 04:50=Skrýva sa pred vami nepriateľ pod zemou? Vykopte|ho pomocou raketovej vŕtačky!|Časovač nastavuje počet sekúnd|do detonácie. 04:51=Nemíňajte muníciu - hod blatom je zdarma.|Trošku štípe a dokáže ježka zhodiť. 04:52=NEPOUŽITÉ -04:53=Vyberte sa na cestu časom a priestorom|a nechajte vašich priateľov bojovať bez vás.|Buďte pripravený vrátiť sa kedykoľvek,|buď pri Náhlej smrti alebo keď sú všetci porazení.|Poznámka: Nefunguje počas Náhlej smrti,|ak ste sám alebo ak ste kráľom. +04:53=Vyberte sa na cestu časom a priestorom|a nechajte vašich priateľov bojovať bez vás.|Buďte pripravený vrátiť sa kedykoľvek,|buď pri Náhlej smrti alebo keď sú všetci porazení.|Poznámka\: Nefunguje počas Náhlej smrti,|ak ste sám alebo ak ste kráľom. 04:54=Rozprášte prúd lepkavej hliny.|Postavte mosty, pochovajte nepriateľov,|zapečaťte tunely. Buďte|však opatrný a nezašpinte sa|od nej aj vy. 04:55=Vráťme sa do doby ľadovej!|Zamrazte ježkov, urobte podlahu šmykľavú alebo|sa poistite pred utopením zmrazením vody.|Útok: Aktivuje/Deaktivuje ľadový lúč|Hore/Dole: Pokračuj v mierení 04:56=Po nepriateľovi môžete hodiť dva sekáčiky,|zablokovať priechody a tunely a dokonca ich použiť pri šplhaní!|Buďte opatrný! Nože nie su na hranie.|Útok: Podržte pre hod väčšou silou (dvakrát) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/stub.lua --- a/share/hedgewars/Data/Locale/stub.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/stub.lua Sun Mar 24 14:33:57 2024 -0400 @@ -636,11 +636,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1446,6 +1448,7 @@ -- ["One tribe was peaceful, spending their time hunting and training, enjoying the small pleasures of life..."] = "", -- A_Classic_Fairytale:first_blood -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2661,6 +2664,7 @@ -- ["You have kidnapped our whole tribe!"] = "", -- A_Classic_Fairytale:enemy -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/sv.lua --- a/share/hedgewars/Data/Locale/sv.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/sv.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1540,6 +1542,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2849,6 +2852,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/sv.txt --- a/share/hedgewars/Data/Locale/sv.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/sv.txt Sun Mar 24 14:33:57 2024 -0400 @@ -321,7 +321,7 @@ 02:07=Ooh, den här lådan är tung 02:07=Du kanske behöver det här -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 är såååå tråkig... 02:08=%1 brydde sig inte 02:08=%1 är en lat igelkott @@ -365,7 +365,7 @@ 02:08=Du borde byta karriär om du ska hålla på så, %1 02:08=Var det allt? -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 borde öva sitt sikte! 02:09=%1 verka hata sig själv 02:09=%1 står på fel sida! diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_de.xml --- a/share/hedgewars/Data/Locale/tips_de.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_de.xml Sun Mar 24 14:33:57 2024 -0400 @@ -13,7 +13,7 @@ Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit erstellen. Falls du Probleme hast, frag uns in unseren Foren oder besuch unseren IRC-Channel! Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit erstellen. Wenn es dir gefällt, hilf uns mit einer kleinen Spende oder steuere deine eigenen Werke bei! Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit erstellen. Teile es mit deiner Familie und deinen Freunden, wie es dir gefällt! - Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit nur so zum Spaß erstellen. Triff die Entwickler auf #hedgewars! + Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit nur so zum Spaß erstellen. Triff die Entwickler auf #hedgewars! Von Zeit zu Zeit wird es offizielle Turniere geben. Bevorstehende Ereignisse werden auf https://www.hedgewars.org/ ein paar Tage im Voraus angekündigt. Hedgewars ist in vielen Sprachen verfügbar. Wenn die Übersetzung deiner Sprache zu fehlen oder veraltet zu sein scheint, nimm ruhig mit uns Kontakt auf! Hedgewars läuft auf vielen verschiedenen Betriebssystemen, unter anderem Microsoft Windows, macOS und GNU/Linux. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_en.xml --- a/share/hedgewars/Data/Locale/tips_en.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_en.xml Sun Mar 24 14:33:57 2024 -0400 @@ -3,7 +3,7 @@ Tips between the platform specific tags are shown only on those platforms. Do not escape characters or use the CDATA tag. --> - Simply pick the same color as a friend to play together as a clan. Each of you will still control his or her own hedgehogs but they’ll win or lose together. + Simply pick the same color as a friend to play together as a clan. Each of you will still control their own hedgehogs but they’ll win or lose together. Some weapons might do only low damage but they can be a lot more devastating in the right situation. Try to use the Desert Eagle to knock multiple hedgehogs into the water. If you’re unsure what to do and don’t want to waste ammo, skip one round. But don’t let too much time pass as there will be Sudden Death! Want to save ropes? Release the rope in mid air and then shoot again. As long as you don’t touch the ground or miss a shot you’ll reuse your rope without wasting ammo! @@ -13,7 +13,7 @@ Hedgewars is free software (Open Source) we create in our spare time. If you’ve got problems, ask on our forums or visit our IRC room! Hedgewars is free software (Open Source) we create in our spare time. If you like it, feel free to help us with a small donation or contribute your own work! Hedgewars is free software (Open Source) we create in our spare time. Share it with your family and friends as you like! - Hedgewars is free software (Open Source) we create in our spare time, just for fun! Meet the developers in #hedgewars! + Hedgewars is free software (Open Source) we create in our spare time, just for fun! Meet the developers in #hedgewars! From time to time there will be official tournaments. Upcoming events will be announced at https://www.hedgewars.org/ some days in advance. Hedgewars is available in many languages. If the translation in your language seems to be missing or outdated, feel free to contact us! Hedgewars can be run on lots of different operating systems including Microsoft Windows, macOS and GNU/Linux. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_gd.xml --- a/share/hedgewars/Data/Locale/tips_gd.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_gd.xml Sun Mar 24 14:33:57 2024 -0400 @@ -13,7 +13,7 @@ ’S e bathar-bog saor (Open Source) a th’ ann an Hedgewars a tha sinn a’ cruthachadh gu saor-thoileach. Ma tha duilgheadas agad, faighnich air a’ bhòrd-bhrath no tadhail air an t-seòmar IRC againn! ’S e bathar-bog saor (Open Source) a th’ ann an Hedgewars a tha sinn a’ cruthachadh gu saor-thoileach. Ma tha e a’ còrdadh riut, nach doir thu tabhartas airgid no obrach dhuinn? ’S e bathar-bog saor (Open Source) a th’ ann an Hedgewars a tha sinn a’ cruthachadh gu saor-thoileach. Co-roinn e le do theaghlach is caraidean mar a thogras tu! - ’S e bathar-bog saor (Open Source) a th’ ann an Hedgewars a tha sinn a’ cruthachadh gu saor-thoileach a cum tlachd! Coinnich ris an luchd-leasachaidh ann an #hedgewars! + ’S e bathar-bog saor (Open Source) a th’ ann an Hedgewars a tha sinn a’ cruthachadh gu saor-thoileach a cum tlachd! Coinnich ris an luchd-leasachaidh ann an #hedgewars! Bi fèill-chluiche oifigeil againn o àm gu àm. Sgaoilidh sinn brathan-naidheachd mu na tachartasan air https://www.hedgewars.org/ beagan làithean ro làimh. Tha Hedgewars ri fhaighinn ann an iomadh cànan. Ma tha an cànan agad a dhìth no an t-eadar-theangachadh ro shean, nach cuir thu fios thugainn? Gabhaidh Hedgewars a ruith air iomadh siostam-obrachaidh, a’ gabhail a-steach Microsoft Windows, MacOS agus GNU/Linux. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_hu.xml --- a/share/hedgewars/Data/Locale/tips_hu.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_hu.xml Sun Mar 24 14:33:57 2024 -0400 @@ -13,7 +13,7 @@ A Hedgewars egy szabad szoftver (nyílt forráskódú), amit a szabadidőnkben fejlesztünk. Probléma esetén kérdezz a fórumokon, vagy látogasd meg IRC szobánkat! A Hedgewars egy szabad szoftver (nyílt forráskódú), amit a szabadidőnkben fejlesztünk. Ha tetszik, nyugodtan küldj egy kis adományt, vagy add hozzá te is a munkádat! A Hedgewars egy szabad szoftver (nyílt forráskódú), amit a szabadidőnkben fejlesztünk. Nyugodtan oszd meg családoddal, barátaiddal is! - A Hedgewars egy szabad szoftver (nyílt forráskódú), amit a szabadidőnkben fejlesztünk, csak a móka kedvéért! A fejlesztőkkel a #hedgewars csatornán találkozhatsz. + A Hedgewars egy szabad szoftver (nyílt forráskódú), amit a szabadidőnkben fejlesztünk, csak a móka kedvéért! A fejlesztőkkel a #hedgewars csatornán találkozhatsz. Időről időre hivatalos bajnokságok indulnak. A közelgő eseményeket pár nappal előre bejelentjük a https://www.hedgewars.org/ webhelyen. A Hedgewars sok nyelven elérhető. Ha a te nyelved fordítása hiányzik vagy elavult, nyugodtan keress minket! A Hedgewars számos operációs rendszerre elérhető, többek között Microsoft Windowsra, macOS-re és GNU/Linuxra is. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_it.xml --- a/share/hedgewars/Data/Locale/tips_it.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_it.xml Sun Mar 24 14:33:57 2024 -0400 @@ -13,7 +13,7 @@ Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Se hai problemi, chiedi nei nostri forum oppure visita il nostro canale IRC! Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Se ti piace, aiutaci con una piccola donazione o contribuisci con il tuo lavoro! Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Condividilo con tutta la famiglia e con gli amici come più ti piace! - Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Incontra gli sviluppatori sul canale #hedgewars! + Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Incontra gli sviluppatori sul canale #hedgewars! Di tanto in tanto ci saranno tornei ufficiali. Gli eventi saranno annunciati su https://www.hedgewars.org/ con qualche giorno di anticipo. Hedgewars è disponibile in molte lingue. Se la traduzione nella tua lingua sembra mancante o non aggiornata, sentiti libero di contattaci! Hedgewars può essere usato su molti sistemi operativi differenti come Microsoft Windows, Mac OS X e GNU/Linux. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_pl.xml --- a/share/hedgewars/Data/Locale/tips_pl.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_pl.xml Sun Mar 24 14:33:57 2024 -0400 @@ -11,7 +11,7 @@ Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie. Jeśli masz jakiś problem, zapytaj na naszym forum lub odwiedź nasz kanał IRC! Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie. Jeśli ją lubisz, wspomóż nas małą wpłatą lub wnieś w nią trochę własnej pracy! Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie. Jeśli tylko chcesz, rozdaj ją swojej rodzinie i kolegom! - Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie, tylko dla zabawy! Poznaj twórców na #hedgewars! + Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie, tylko dla zabawy! Poznaj twórców na #hedgewars! Od czasu do czasu będą organizowane mistrzostwa. Będą one ogłaszane z wyprzedzeniem na http://www.hedgewars.org/. Hedgewars jest dostępne w wielu językach. Jeśli brakuje tłumaczenia w twoim języku bądź jest ono niekompletne, nie bój się z nami skontaktować! Hedgewars może być uruchomione na różnych systemach operacyjnych, takich jak Microsoft Windows, Mac OS X, oraz GNU/Linux. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_ru.xml --- a/share/hedgewars/Data/Locale/tips_ru.xml Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tips_ru.xml Sun Mar 24 14:33:57 2024 -0400 @@ -13,7 +13,7 @@ Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время. Если у вас возникают вопросы, задайте их на нашем форуме или посетите наш IRC канал! Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время. Если вам понравилась игра, помогите нам денежным вознаграждением или вкладом в виде вашей работы! Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время. Распространяйте его среди друзей и членов семьи! - Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время в своё удовольствие! Встретиться с разработчиками можно тут #hedgewars! + Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время в своё удовольствие! Встретиться с разработчиками можно тут #hedgewars! Время от времени проводятся официальные турниры. Предстоящие события анонсируются на https://www.hedgewars.org/ за несколько дней. Hedgewars доступен на многих языках. Если русский перевод устарел или содержит ошибки, сообщите нам или последнему переводчику в списке! Hedgewars запускается на множестве различных операционных систем, включая Microsoft Windows, Mac OS и Linux. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tips_zh_CN.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Locale/tips_zh_CN.xml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,91 @@ + + + 选择相同颜色作为一个战队,你们仍然控制自己的刺猬,但会一起输赢 + 一些武器伤害很低,在合适的情况下,它们会更具破坏性。试着用沙漠之鹰把几个刺猬打下水。 + 如果你不确定要怎么做,并且不想浪费武器,跳过这个回合。但别花掉太多时间,因为会进入突然死亡! + 想要节省绳索吗?在空中放手并再次发射,只要你没接触地面或射空,就能重复利用绳索! + 如果你不想让别人在官方服务器上使用你喜欢的昵称,在官网注册一个账号 https://www.hedgewars.org/. + 如果你对默认玩法感到无聊,试试任务模式,它们会给你提供不一样的玩法 + 游戏会默认录制最后一场比赛作为 demo,选择“观看 demo”进行播放或管理 + 刺猬战争是我们在空余时间做出的开源免费软件。如果你有问题,在我们的论坛询问,或访问IRC房间! + 刺猬战争是我们在空余时间做出的开源免费软件。如果你喜欢它,可以给我们捐赠,或贡献自己的工作! + 刺猬战争是我们在空余时间做出的开源免费软件。如果你喜欢就和家人朋友分享它! + 刺猬战争是我们在空余时间做出的开源免费软件,只为好玩!在这里与开发人员会面 #hedgewars! + 游戏不时会举办赛事,官网会提前公布即将举办的活动 https://www.hedgewars.org/. + 刺猬战争提供多种语言,如果你使用的语言翻译缺失或过时,请联系我们! + 刺猬战争能在多个操作系统上运行,包括 Microsoft Windows, macOS and GNU/Linux. + 永远记住你可以在本地或网络游戏建立自己的游戏。你不会受限于“简单游戏”选项 + 在游戏开始前连接一个或多个手柄,就能分配控制到你的队伍 + 适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。 + 如果你的显卡不能提供硬件加速 OpenGL,尝试启用低质量模式来提升表现 + 如果你的显卡不能提供硬件加速 OpenGL,尝试更新相关的驱动 + 我们乐于接受建议和建设性反馈,如果你不喜欢某样东西,或有很棒的想法,让我们知道! + 为了您自己的利益,我们希望你在服务器上玩的时候保持礼貌和友好,另外请记住有些玩家未成年! + 特殊游戏模式如“吸血”或 “报应” 能让你开发全新的战术,在自定义游戏里尝试他们! + 请不要在不属于你的电脑上安装刺猬战争(学校、大学、工作等),除非你有权限。我们不想让你惹上麻烦 + 刺猬战争很适合在休息时玩一局,只要确保你没添加太多刺猬,或使用超大地图。减少时间和血量会很有帮助 + 做这个游戏的时候没伤害任何刺猬 + 有三种不同的跳跃方式。连按两次[高跳]来跳高/向后跳 + 害怕掉下悬崖?长按[精确]+[左][右]转向且不移动 + 一些武器需要特殊策略或大量练习,所以当你射空敌人时不要放弃特定的工具 + 大多数武器接触到水就失效,蜜蜂和蛋糕是例外 + 旧林堡干酪只能造成小爆炸,然而风会影响臭云,使很多刺猬中毒 + 钢琴空袭是最具伤害的空袭,但有个巨大缺点,你会失去执行的刺猬 + 蜜蜂枪有点难用,它的转弯半径取决于加速度,所以不要尝试全力发射 + 黏性地雷可以创造小型连锁反应,把刺猬打入绝境或水里 + 锤子在桥梁或大梁上使用最有效,打中刺猬会穿透地面 + 如果你卡在敌方刺猬后面,可以用锤子或其他近战武器解救自己 + 蛋糕的最大行走距离取决于它走过的地面,使用[攻击]来提前引爆 + 火焰喷射器可以用来挖隧道 + 使用燃烧瓶或火焰喷射器,暂时阻止刺猬通过地形 + 喜欢刺猬战争?在 FacebookTwitter 关注我们 + 你可以画出属于自己的墓碑、帽子、旗帜、地图和主题,但别人需要下载你的作品,才能在网络对战看到你使用的东西(没有则显示默认) + 保持你的显卡驱动为最新版本,避免在玩游戏时出问题 + 正还是反?在聊天中输入“/rnd”得到答案,还可以 “/rnd 石头 剪刀 布” + 你可以关联刺猬战争相关文件到游戏,从你最喜欢的文件或网页浏览器启动文件 + 哑弹地雷并非无害:虽然它们的定时器坏了,但是受到一定伤害还是会爆炸! + 爱是火热的!使用诱惑来解冻冰冻的刺猬 + 油桶冒烟说明它“血量”很低,只要一点伤害就爆炸 + 油桶初始血量60,会像刺猬一样受伤,所以需要一点“帮助”才会爆炸 + 想要弹得更高?长按[精确]+定时 改变手榴弹和地雷的弹性 + 在游戏里忘了目标或修改器?按暂停或退出就能看到它们 + 你能拳击、鞭打、锤爆的不只是刺猬 + 如果没有另行提及,地雷通常在触发后三秒爆炸 + 在国王模式,国王比喽啰更强,更多血量和伤害抗性 + 在国王模式,队伍中没有手下时,国王每个回合受到伤害 + 鞭子能打到薄墙后面的刺猬和物体 + 菜刀的速度越快,伤害越高 + 你可以在悬崖丢蛋糕,但必须非常靠近边缘,所以小心点 + 使用绳索并放手,在地面滑动推开其他刺猬,这个技术被称为“绳索撞击” + 不要站在结冰的斜坡上,不然你会滑下去。 或者,你可以长按[精确]站着 + 在下雪或圣诞的地面, 雪随着时间的推移堆积起来, 除非地面是不可破坏 + 在下雪或圣诞的地面要小心,因为大梁是用很滑的冰做的 + 撤退时间取决于你用过的武器,有些武器没有撤退时间,立刻终止回合 + 用一点技巧,在刺猬下面跳出时,可以击中它 + 如果你要从高处跳下,可以在落地前使用升龙拳、冲击钻或正弦枪,取消坠落伤害 + 你的刺猬在空中不会跟炮弹发生碰撞 + 钻地火箭能钻出一个洞,让狙击枪和沙漠之鹰打进里面,只要你瞄准足够精确 + 收集医疗包治疗中毒的刺猬 + 你可以用遥控飞机、升龙拳、冲击钻和神风特攻队收集箱子 + 使用焊枪时要小心,碰到箱子会爆炸 + 浮空雷在回合的前五秒保持被动,然后开始寻找刺猬 + 和浮空雷保持距离,一旦它开始追你,没有工具很难逃离 + 浮空雷的爆炸定时只有常规地雷的四分之一 + 地形上的菜刀受到一次30或更多伤害,有几率掉下去,几率随伤害增加 + 留意风向,因为火焰和一些炮弹受风力影响 + 使用焊枪推开刺猬和定时器长的地雷 + 开着焊枪冲向油桶,是灾难的爆炸性配方 + 在聊天中输入 “/help” 获得命令列表,聊天命令能让你使用特殊功能,如投票和嘲讽 + + 你可以在“My Documents\Hedgewars”找到你的游戏配置文件,备份或随身携带文件,但不要随意手动编辑它们 + + + 你可以在主目录“Library/Application Support/Hedgewars”找到你的游戏配置文件,备份或随身携带文件,但不要随意手动编辑它们 + + + 你可以在主目录“.Hedgewars”找到你的游戏配置文件,备份或随身携带文件,但不要随意手动编辑它们 + + diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tr.lua --- a/share/hedgewars/Data/Locale/tr.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tr.lua Sun Mar 24 14:33:57 2024 -0400 @@ -677,11 +677,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1541,6 +1543,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2851,6 +2854,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/tr.txt --- a/share/hedgewars/Data/Locale/tr.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/tr.txt Sun Mar 24 14:33:57 2024 -0400 @@ -309,7 +309,7 @@ 02:07=Off bu kutu da ağırmış 02:07=Buna ihtiyacın olabilir -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 çoook sıkıcı... 02:08=%1 rahatsız olamazdı 02:08=%1 tembel bir kirpi @@ -347,7 +347,7 @@ 02:08=%1 korkak bir ölü 02:08=%1 uyuya kaldı -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 atış çalışmalı! 02:09=%1 kendinden nefret ediyor gibi görünüyor 02:09=%1 yanlış tarafta duruyor! @@ -508,7 +508,7 @@ 04:50=Biri altta mı saklanıyor?|Matkap saldırısı ile kaz!|Zamanlayıcı ne kadar kazılacağını denetler. 04:51=Bir çamur topu fırlatarak ücretsiz bir atış kap.|Biraz kokar ancak kirpileri geri sektirir. 04:52=KULLANILMIYOR -04:53=Arkadaşlarını savaşta yalnız bırakarak|zaman ve uzaya seyahat et.|Herhangi bir an, Ani Ölüm veya tümü|ölmüşse geri gelmeye hazır ol.|Yadsıma: Ani Ölüm kipinde, tek isen veya|Kralsan çalışmaz. +04:53=Arkadaşlarını savaşta yalnız bırakarak|zaman ve uzaya seyahat et.|Herhangi bir an, Ani Ölüm veya tümü|ölmüşse geri gelmeye hazır ol.|Yadsıma\: Ani Ölüm kipinde, tek isen veya|Kralsan çalışmaz. 04:54=TAM DEĞİL 04:55=Yapışkan tanecikler püskürt.|Köprü yap, düşmanı göm, tünelleri kapat.|Dikkatli ol sana gelmesin! 04:56=İki satırı düşmanına atabilir, geçişleri|ve tünelleri kapatabilir,|hatta tırmanmak için bile|kullanabilirsin!|Dikkatli ol! Bıçakla oynamak tehlikeli!|Saldır: Daha yüksek hızda atmak için basılı tut (iki kez) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/uk.lua --- a/share/hedgewars/Data/Locale/uk.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/uk.lua Sun Mar 24 14:33:57 2024 -0400 @@ -676,11 +676,13 @@ -- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge -- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 -- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey +-- ["Flower Power"] = "", -- Basic_Training_-_Rope -- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler -- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer -- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion -- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos -- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope +-- ["For each kill you win %d seconds."] = "", -- RopeKnocking -- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies -- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement -- ["Four Eyes"] = "", -- @@ -1539,6 +1541,7 @@ -- ["Oneye"] = "", -- portal -- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant -- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant +-- ["Only one team per clan allowed! Excess teams will be removed."] = "", -- Mutant -- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 -- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer -- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard @@ -2848,6 +2851,7 @@ -- ["You have killed all enemies."] = "", -- Big_Armory -- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab -- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge +-- ["You have killed %d of %d hedgehogs (+%d points)."] = "", -- RopeKnocking -- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka -- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee -- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/uk.txt --- a/share/hedgewars/Data/Locale/uk.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/uk.txt Sun Mar 24 14:33:57 2024 -0400 @@ -192,7 +192,7 @@ 02:07=Треба використати з розумом 02:07=Тобі це знадобиться -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 такий нудний... 02:08=%1 ледачий їжак 02:08=%1 безтурботний @@ -213,7 +213,7 @@ 02:08=%1 все одно не вміє стріляти 02:08=Я розчарований тобою, %1 -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 повинен практикуватися в прицілюванні! 02:09=%1 ненавидить себе 02:09=%1 не тим боком взявся за зброю diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/zh_CN.lua --- a/share/hedgewars/Data/Locale/zh_CN.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/zh_CN.lua Sun Mar 24 14:33:57 2024 -0400 @@ -1,2989 +1,2993 @@ locale = { - ["!!!"] = "!!!", --- ["..."] = "", --- ["011101000"] = "", -- A_Classic_Fairytale:dragon --- ["011101001"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:family, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow, A_Classic_Fairytale:united --- ["10 weapon schemes"] = "", -- Continental_supplies --- ["15+%d damage, %d invulnerable left"] = "", -- Continental_supplies --- ["1-5, Precise + 1-4: Choose structure type"] = "", -- Construction_Mode --- ["+1 barrel!"] = "", -- Tumbler --- ["%.1f seconds were remaining."] = "", -- Basic_Training_-_Bazooka --- ["%.1fs"] = "", -- Racer, TechRacer --- ["+1 Grenade"] = "", -- Basic_Training_-_Flying_Saucer --- ["+1 mine!"] = "", -- Tumbler --- ["+1 point"] = "", -- Mutant --- ["-1 point"] = "", -- Mutant --- ["-1 to anyone for a suicide"] = "", -- Mutant --- ["+1 to the Bottom Feeder for killing anyone"] = "", -- Mutant --- ["+1 to the Mutant for killing anyone"] = "", -- Mutant --- ["+2 for becoming the Mutant"] = "", -- Mutant --- ["30 minutes later..."] = "", -- A_Classic_Fairytale:shadow --- ["%.3fs"] = "", -- A_Space_Adventure:ice02 --- ["5 additional enemies will be spawned during the game."] = "", -- A_Space_Adventure:fruit01 --- ["5 Deadly Hogs"] = "", -- A_Space_Adventure:death02 --- ["6 more seconds added to the clock"] = "", -- A_Space_Adventure:ice02 --- ["About a month ago, a cyborg came and told us that you're the cannibals!"] = "", -- A_Classic_Fairytale:enemy --- ["Above-average pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Accuracy Bonus! +15 points"] = "", -- Space_Invasion --- ["Accuracy bonus: +%d points"] = "", -- Basic_Training_-_Sniper_Rifle --- ["Achievement gotten: %s"] = "", -- User_Mission_-_RCPlane_Challenge, User_Mission_-_That_Sinking_Feeling, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, Basic_Training_-_Rope, Tumbler --- ["A Classic Fairytale"] = "", -- A_Classic_Fairytale:first_blood --- ["A crate critical to this mission has been destroyed."] = "", -- SimpleMission --- ["Actually, you aren't worthy of life! Take this..."] = "", -- A_Classic_Fairytale:shadow --- ["A cy-what?"] = "", -- A_Classic_Fairytale:enemy --- ["Add %d"] = "", -- HedgeEditor --- ["Admit what?"] = "", -- A_Classic_Fairytale:queen --- ["Adventurous"] = "", -- A_Classic_Fairytale:journey --- ["A frenetic Hedgewars mini-game"] = "", -- Frenzy --- ["Africa"] = "", -- Continental_supplies --- ["A frozen adventure"] = "", -- A_Space_Adventure:ice01 --- ["After Leaks A Lot betrayed his tribe, he joined the cannibals..."] = "", -- A_Classic_Fairytale:first_blood --- ["After that incident he went underground and started working on his plan to steal the device."] = "", -- A_Space_Adventure:moon02 --- ["After the shock caused by the enemy spy, Leaks A Lot and Dense Cloud went hunting to relax."] = "", -- A_Classic_Fairytale:shadow --- ["After you killed an enemy, you'll lose the weapon that he is named after."] = "", -- A_Space_Adventure:death02 --- ["After you left the moon, my other loyal minions came and resurrected me so I could complete my master plan."] = "", -- A_Space_Adventure:death01 --- ["Again with the 'cannibals' thing!"] = "", -- A_Classic_Fairytale:enemy --- ["A Hedgewars minigame"] = "", -- Capture_the_Flag --- ["A Hedgewars mini-game"] = "", -- Racer, Space_Invasion, TechRacer, Tumbler --- ["A Hedgewars tag game"] = "", -- Mutant --- ["Ahhh, home, sweet home. Made it in %d seconds."] = "", -- ClimbHome --- ["Aim at the ceiling and hold [Attack] pressed until the rope attaches."] = "", -- Basic_Training_-_Rope --- ["Aiming practice"] = "", -- TargetPractice - ["Aiming Practice"] = "瞄准练习", --火箭筒、霰弹枪、狙击枪 --- ["Aim: [Up]/[Down]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Rope --- ["Air Attack"] = "", -- Construction_Mode --- ["Air General"] = "", -- Battalion --- ["Air Mine Placement Mode"] = "", -- HedgeEditor --- ["AIR MINE PLACEMENT MODE"] = "", -- HedgeEditor --- ["A leap in a leap"] = "", -- A_Classic_Fairytale:first_blood --- ["Alex"] = "", -- --- ["Alien! I wish to be moved!"] = "", -- A_Classic_Fairytale:queen --- ["A little gift from the cyborgs"] = "", -- A_Classic_Fairytale:shadow --- ["Al.Kaholic"] = "", -- --- ["All But Last"] = "", -- WxW --- ["All But Last: You must not solely attack the team with the least health"] = "", -- WxW --- ["All gone...everything!"] = "", -- A_Classic_Fairytale:enemy --- ["Allies"] = "", -- A_Space_Adventure:ice01, A_Space_Adventure:ice02 --- ["All right, I'll admit it!"] = "", -- A_Classic_Fairytale:queen --- ["All right, we just need to get to the other side of the island!"] = "", -- A_Classic_Fairytale:journey --- ["All right, you got me!"] = "", -- A_Classic_Fairytale:queen --- ["All the other places are protected by our flight-inhibiting weapons."] = "", -- A_Space_Adventure:fruit01 --- ["All the saucer pilots dream to come here one day in order to compete with the best!"] = "", -- A_Space_Adventure:ice02 --- ["All they do is sit around and judge us!"] = "", -- A_Classic_Fairytale:queen --- ["All this to please our beloved “elders” … hick …"] = "", -- A_Classic_Fairytale:queen --- ["All walls touched!"] = "", -- WxW --- ["All you do is take long walks when everyone else works."] = "", -- A_Classic_Fairytale:queen --- ["All your hedgehogs must be above the marked height!"] = "", -- A_Classic_Fairytale:family --- ["Also, you should know that the only place where you can fly is the left-most part of this area."] = "", -- A_Space_Adventure:fruit01 --- ["Always being considered weak and fragile."] = "", -- A_Classic_Fairytale:queen --- ["Amazing! I was never beaten in a race before!"] = "", -- A_Space_Adventure:moon02 --- ["Ammo depleted!"] = "", -- Space_Invasion --- ["Ammo: %d"] = "", -- Tumbler --- ["Ammo is reset at the end of your turn."] = "", --- ["Ammo Limit: Hogs can’t have more than 1 ammo per type"] = "", -- Highlander --- ["Ammo Maniac! +5 points!"] = "", -- Space_Invasion --- ["A mysterious Box"] = "", -- Basic_Training_-_Movement --- ["And how am I alive?!"] = "", -- A_Classic_Fairytale:enemy --- ["And I just forgot the checkpoint of my main mission. Great, just great!"] = "", -- A_Space_Adventure:cosmos --- ["… and I think they are up to something. Something bad!"] = "", -- A_Classic_Fairytale:epil --- ["Andrey"] = "", -- --- ["And so happened that Leaks A Lot failed to complete the challenge! He landed, pressured by shame ..."] = "", -- A_Classic_Fairytale:first_blood --- ["And so it began..."] = "", -- A_Classic_Fairytale:first_blood --- ["And so the cyborgs took over the island."] = "", -- A_Classic_Fairytale:queen --- ["...and so the cyborgs took over the world..."] = "", -- A_Classic_Fairytale:shadow --- ["And so they discovered that cyborgs weren't invulnerable..."] = "", -- A_Classic_Fairytale:journey --- ["… and then I took a stroll …"] = "", -- A_Classic_Fairytale:epil --- ["And what do they do in the meantime? Nothing!"] = "", -- A_Classic_Fairytale:queen --- ["And where's all the weed?"] = "", -- A_Classic_Fairytale:dragon --- ["And you believed me? Oh, god, that's cute!"] = "", -- A_Classic_Fairytale:journey --- ["And you need to move to the top!"] = "", -- Basic_Training_-_Movement --- ["An experimental editing tool for missions and more"] = "", -- HedgeEditor --- ["Anno 1032"] = "", -- Continental_supplies --- ["Anno 1032: [The explosion will make a strong push ~ Wide range, wont affect hogs close to the target]"] = "", -- Continental_supplies --- ["An object has been destroyed before it took enough damage."] = "", -- SimpleMission --- ["Antarctica"] = "", -- Continental_supplies --- ["Antarctic summer: Every 4th turn you get 1 girder, 1 mudball, 2 sine guns and 1 portable portal device."] = "", -- Continental_supplies --- ["Antarctic summer: - Will give you one girder/mudball and two sineguns/portals every fourth turn."] = "", -- Continental_supplies --- ["Anti-Gravity Device Part (+1)"] = "", -- A_Space_Adventure:desert01, A_Space_Adventure:fruit02, A_Space_Adventure:ice01 --- ["Anton"] = "", -- --- ["An unexpected event!"] = "", -- A_Space_Adventure:cosmos --- ["Anyway, the aliens accept me for who I am."] = "", -- A_Classic_Fairytale:queen --- ["A random hedgehog will inherit the weapons of his deceased team-mates."] = "", -- A_Space_Adventure:death02 --- ["Arashi"] = "", -- --- ["Area"] = "", -- Continental_supplies --- ["Areas surrounded by a green dashed outline are portal-proof and repel portals."] = "", -- A_Space_Adventure:final --- ["Areas surrounded by a security border are indestructible."] = "", -- A_Space_Adventure:final --- ["Areas with a green dashed outline are portal-proof."] = "", -- A_Space_Adventure:final --- ["Areas with a security outline are indestructible."] = "", -- A_Space_Adventure:final --- ["Are we there yet?"] = "", -- A_Classic_Fairytale:shadow --- ["Are you accusing me of something?"] = "", -- A_Classic_Fairytale:backstab --- ["Are you helping the aliens?"] = "", -- A_Classic_Fairytale:queen --- ["Are you saying that many of us have died for your entertainment?"] = "", -- A_Classic_Fairytale:enemy --- ["Argh, the boredom!"] = "", -- A_Classic_Fairytale:queen --- ["Artur Detour"] = "", -- A_Classic_Fairytale:queen --- ["As a reward for your performance, here's some new technology!"] = "", -- A_Classic_Fairytale:dragon --- ["Ash"] = "", -- --- ["A Shoppa minigame"] = "", -- WxW --- ["Asia"] = "", -- Continental_supplies --- ["As long you don't touch the ground, you can|re-use the same rope as often as you like."] = "", -- Basic_Training_-_Rope --- ["A smuggler! Prepare for battle"] = "", -- A_Space_Adventure:desert01 --- ["A Space Adventure"] = "", -- A_Space_Adventure:desert01, A_Space_Adventure:moon01 --- ["Assault Team"] = "", -- A_Classic_Fairytale:backstab --- ["Asteroid"] = "", -- Big_Armory --- ["As the ammo is sparse, you might want to reuse ropes while mid-air."] = "", -- A_Classic_Fairytale:dragon --- ["As the challenge was completed, Leaks A Lot set foot on the ground..."] = "", -- A_Classic_Fairytale:first_blood --- ["As you are more experienced, I want you to lead them to battle."] = "", -- A_Space_Adventure:fruit01 --- ["As you can see I have survived our last encounter and I had time to plot my master plan!"] = "", -- A_Space_Adventure:death01 --- ["As you can see, there is no way to get on the other side!"] = "", -- A_Classic_Fairytale:dragon --- ["As you probably noticed, these rubber bands|are VERY elastic. Hedgehogs and many other|things will bounce off without taking any damage."] = "", -- Basic_Training_-_Movement --- ["As you've seen, the dropped grenade roughly fell into your flying direction."] = "", -- Basic_Training_-_Flying_Saucer --- ["Athlete"] = "", -- Battalion --- ["Attack: Activate"] = "", -- Racer --- ["Attack Captain Lime before he attacks back."] = "", -- A_Space_Adventure:fruit02 --- ["Attack From Rope: %s"] = "", -- WxW --- ["Attack From Rope: You may only attack from a rope."] = "", -- WxW --- ["Attack rule: %s"] = "", -- WxW --- ["Attack: Select this continent"] = "", -- Continental_supplies --- ["Attack: [Space]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope --- ["Attack: Tap the [Bomb]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:shadow --- ["Attack the assassins before they attack back."] = "", -- A_Space_Adventure:fruit02 --- ["Attack: Throw ball"] = "", -- Knockball --- ["At the end of the game your health was %d."] = "", -- A_Space_Adventure:ice01 --- ["At the start of the game each enemy hog has only the weapon that he is named after."] = "", -- A_Space_Adventure:death02 --- ["Australia"] = "", -- Continental_supplies --- ["Available weapon specials:"] = "", -- Continental_supplies --- ["Average pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Avoid bazookas, red and blue invaders."] = "", -- Space_Invasion --- ["Axes"] = "", -- Bazooka_Battlefield --- ["Aye! Fellow! Let me exit this chamber of doom!"] = "", -- A_Classic_Fairytale:epil --- ["Back Breaker"] = "", -- A_Classic_Fairytale:backstab --- ["Back in the village, after telling the villagers about the threat..."] = "", -- A_Classic_Fairytale:united --- ["Back in the village, the two tribes finally started to live in harmony."] = "", -- A_Classic_Fairytale:epil --- ["Back Jump: [Backspace] ×2"] = "", -- Basic_Training_-_Movement --- ["Back Jump: Double-tap the [Curvy Arrow]"] = "", -- Basic_Training_-_Movement --- ["Back Jumping (1/2)"] = "", -- Basic_Training_-_Movement --- ["Back Jumping (2/2)"] = "", -- Basic_Training_-_Movement --- ["Backstab"] = "", -- A_Classic_Fairytale:backstab --- ["Backwards jump: Press [Backspace] twice"] = "", -- A_Classic_Fairytale:first_blood --- ["Backwards jump: Tap the [Curvy Arrow] twice"] = "", -- A_Classic_Fairytale:first_blood --- ["Bacon"] = "", -- --- ["Bad Guy"] = "", -- User_Mission_-_The_Great_Escape --- ["Badmad"] = "", -- portal --- ["Bad Team"] = "", -- User_Mission_-_The_Great_Escape --- ["Bad timing"] = "", -- A_Space_Adventure:fruit01 --- ["Baggy"] = "", -- --- ["Balrog"] = "", -- --- ["Bamboo Thicket"] = "", --- ["Barrel Launcher"] = "", --- ["Barrel Placement Mode"] = "", -- Construction_Mode --- ["BARREL PLACEMENT MODE"] = "", -- HedgeEditor --- ["Barrier unlocked!"] = "", -- Basic_Training_-_Rope --- ["Baseballbat"] = "", -- Continental_supplies --- ["Baseball bat specials cannot be used close to other hogs."] = "", -- Continental_supplies --- ["Baseball Bat with Ball"] = "", -- Knockball --- ["Base damage has been modified to 12 per shot."] = "", -- Battalion --- ["Based on what you've learned, destroy the target on the girder and as always, land safely!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Basically this is a combination of diving and launching."] = "", -- Basic_Training_-_Flying_Saucer --- ["Basic Bazooka Training"] = "", -- Basic_Training_-_Bazooka --- ["Basic Grenade Training"] = "", -- Basic_Training_-_Grenade --- ["Basic Movement Training"] = "", -- Basic_Training_-_Movement --- ["Basic Rope Training"] = "", -- Basic_Training_-_Rope --- ["Basic Training"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope --- ["Basketball"] = "", -- Basketball - ["Bat balls at your enemies and|push them into the sea!"] = "发射棒球将敌人击打入水", --- ["Battalion"] = "", -- Battalion --- ["Battle Starts Now!"] = "", -- A_Space_Adventure:fruit01 --- ["Batty"] = "", -- - ["Bat your opponents through the|baskets and out of the map!"] = "把敌人击出场地——对准栏框", --- ["Bazooka Battlefield"] = "", -- Bazooka_Battlefield --- ["Bazooka Master"] = "", -- Basic_Training_-_Bazooka --- ["Bazookas are influenced by wind."] = "", -- Basic_Training_-_Bazooka - ["Bazooka Training"] = "火箭筒训练", --- ["Bearded Beast"] = "", -- --- ["Be careful, the future of Hogera is in your hands!"] = "", -- A_Space_Adventure:cosmos --- ["Be careful, your fuel is limited from now on!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Be careful, your gadgets won't work in the bandit area. You should get an ice gun."] = "", -- A_Space_Adventure:ice01 --- ["Beep Loopers"] = "", -- A_Classic_Fairytale:queen --- ["Beginner"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Behind these trees on the east side there is Secret Base 17."] = "", -- A_Space_Adventure:cosmos --- ["Below-average pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Besides, why would I choose certain death?"] = "", -- A_Classic_Fairytale:queen - ["Best laps per team: "] = "每一队最佳速度:", --- ["Best team times: "] = "", -- Racer, TechRacer --- ["Better get yourself another health crate to heal your wounds."] = "", -- Basic_Training_-_Movement --- ["Better luck next time!"] = "", -- ClimbHome --- ["Better Safe Than Sorry"] = "", -- A_Space_Adventure:desert02 --- ["Beware, any damage taken will stay until you complete the moon's main mission"] = "", -- A_Space_Adventure:cosmos --- ["Beware of mines: They explode after 3 seconds."] = "", -- A_Classic_Fairytale:journey --- ["Beware of mines: They explode after 5 seconds."] = "", -- A_Classic_Fairytale:journey --- ["Beware, though! If you are slow, you die!"] = "", -- A_Classic_Fairytale:dragon --- ["Beware, though! Many smugglers come often to explore these tunnels and scavenge whatever valuable items they can find."] = "", -- A_Space_Adventure:desert01 --- ["Beware, though, you will only be able to move slowly through the water."] = "", -- Basic_Training_-_Flying_Saucer --- ["Big Armory"] = "", -- Big_Armory --- ["Billy Frost"] = "", -- A_Space_Adventure:ice01 --- ["Bingo"] = "", -- --- ["Bio-Filter: Aggressively removes enemies."] = "", -- Construction_Mode --- ["Bio-Filter"] = "", -- Construction_Mode --- ["Biomechanic Team"] = "", -- A_Classic_Fairytale:family --- ["Bitter"] = "", -- --- ["Blanka"] = "", -- --- ["Blender"] = "", -- A_Classic_Fairytale:family --- ["Bloodpie"] = "", -- A_Classic_Fairytale:backstab --- ["Bloodrocutor"] = "", -- A_Classic_Fairytale:shadow --- ["Bloodsucker"] = "", -- A_Classic_Fairytale:shadow --- ["Blue"] = "", -- --- ["Blue Team"] = "", -- User_Mission_-_Dangerous_Ducklings --- ["Bob"] = "", -- A_Space_Adventure:cosmos --- ["Bobo"] = "", -- User_Mission_-_Nobody_Laugh --- ["Bone Jackson"] = "", -- A_Classic_Fairytale:backstab --- ["Bonely"] = "", -- A_Classic_Fairytale:shadow --- ["Bones"] = "", -- --- ["Boom!"] = --- ["BOOM! BOOM! BOOM! %s are the masters of destruction with %d destroyed invaders."] = "", -- Space_Invasion --- ["Boom! %s has destroyed %d invaders."] = "", -- Space_Invasion --- ["BOOM! %s really didn't like the invaders, so they decided to destroy as much as %d of them."] = "", -- Space_Invasion --- ["Boris"] = "", -- A_Space_Adventure:moon01 --- ["Boss defeated! +30 points!"] = "", -- Space_Invasion --- ["Boss Slayer! +25 points!"] = "", -- Space_Invasion --- ["Both Barrels"] = "", -- --- ["Both your hedgehogs must survive."] = "", -- User_Mission_-_Teamwork_2, User_Mission_-_Teamwork --- ["Bottom Feeder"] = "", -- Mutant --- ["Bounciness"] = "", -- Basic_Training_-_Grenade --- ["Bouncing Bomb"] = "", -- Basic_Training_-_Bazooka --- ["Bouncy Boomerang"] = "", -- Continental_supplies --- ["Bouncy Girder: [4]"] = "", -- HedgeEditor --- ["Bouncy Land: [4]"] = "", -- HedgeEditor --- ["Bouncy Land"] = "", -- HedgeEditor --- ["Bounty: Get 6 weapons for each kill (even on own hogs)."] = "", -- Continental_supplies --- ["Bozo"] = "", -- --- ["Brain Blower"] = "", -- A_Classic_Fairytale:journey --- ["Brainiac"] = "", -- A_Classic_Fairytale:epil, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:shadow --- ["Brainila"] = "", -- A_Classic_Fairytale:united --- ["Brain Stu"] = "", -- A_Classic_Fairytale:united --- ["Brain Teaser"] = "", -- A_Classic_Fairytale:backstab --- ["Brigadier Briggs"] = "", -- --- ["Bruce"] = "", -- A_Space_Adventure:moon01 --- ["Brutal Lily"] = "", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil --- ["Brutus"] = "", -- A_Classic_Fairytale:backstab --- ["Build a fortress and destroy your enemy."] = "", -- Construction_Mode --- ["Build an awesome race track by placing|waypoints which the hedgehogs have to|touch in any order to finish a round."] = "", -- Racer --- ["Build a track and race."] = "", --- ["Builder"] = "", -- Battalion --- ["Build one of multiple different structures|to aid you in victory, at the cost of energy."] = "", -- Construction_Mode --- ["Bullseye"] = "", -- A_Classic_Fairytale:dragon --- ["Bunny"] = "", -- --- ["burp"] = "", -- --- ["Bushes"] = "", -- Bazooka_Battlefield --- ["Bushi"] = "", -- --- ["Buster"] = "", -- --- ["But it proved to be no easy task!"] = "", -- A_Classic_Fairytale:dragon --- ["But I want my sandals!"] = "", -- A_Classic_Fairytale:queen --- ["But one thing's for sure:"] = "", -- A_Space_Adventure:final --- ["But that's impossible!"] = "", -- A_Classic_Fairytale:backstab --- ["But the ones alive are stronger in their heart!"] = "", -- A_Classic_Fairytale:enemy --- ["But … they kidnapped you!"] = "", -- A_Classic_Fairytale:queen --- ["But...we died!"] = "", -- A_Classic_Fairytale:backstab --- ["But where can we go?"] = "", -- A_Classic_Fairytale:united --- ["But why did you betray us?!"] = "", -- A_Classic_Fairytale:queen --- ["But why would they help us?"] = "", -- A_Classic_Fairytale:backstab --- ["But you're cannibals. It's what you do."] = "", -- A_Classic_Fairytale:enemy --- ["But you said you'd let her go!"] = "", -- A_Classic_Fairytale:journey --- ["But you saved me!"] = "", -- A_Classic_Fairytale:queen --- ["By the way, not only bazookas will bounce on water, but also greandes and many other things."] = "", -- Basic_Training_-_Bazooka --- ["By the way, not only bazookas will bounce on water, but also grenades and many other things."] = "", -- Basic_Training_-_Bazooka --- ["By the way, you can turn around without walking|by holding down Precise when you hit a walk control."] = "", -- Basic_Training_-_Movement --- ["C-1"] = "", -- portal --- ["C-2"] = "", -- portal --- ["Callahan"] = "", -- --- ["Call me Beep! Well, 'cause I'm such a nice...person!"] = "", -- A_Classic_Fairytale:family --- ["Cannibals"] = "", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:first_blood --- ["Cannibal Sentry"] = "", -- A_Classic_Fairytale:journey --- ["Cannibals?! You're the cannibals!"] = "", -- A_Classic_Fairytale:enemy --- ["Can you do it?"] = "", -- A_Space_Adventure:ice02 --- ["Cappy"] = "", -- Basic_Training_-_Movement --- ["Captain Lime"] = "", -- A_Space_Adventure:fruit01, A_Space_Adventure:fruit02 --- ["Captain Lime offered his help if you assist him in battle."] = "", -- A_Space_Adventure:fruit01 --- ["Capture The Flag"] = "", -- Capture_the_Flag, CTF_Blizzard --- ["Careful, hedgehogs can't swim!"] = "", -- Basic_Training_-_Movement --- ["Careless"] = "", --- ["Carol"] = "", -- A_Classic_Fairytale:family --- ["Challenge completed!"] = "", -- User_Mission_-_Rope_Knock_Challenge, User_Mission_-_That_Sinking_Feeling, SpeedShoppa --- ["CHALLENGE COMPLETE"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Challenge failed!"] = "", -- SpeedShoppa --- ["Challenge objectives"] = "", -- A_Space_Adventure:death02, A_Space_Adventure:desert03, A_Space_Adventure:final, A_Space_Adventure:fruit03, A_Space_Adventure:moon02 --- ["Challenge over!"] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Challenge"] = "", -- User_Mission_-_RCPlane_Challenge, User_Mission_-_Rope_Knock_Challenge, User_Mission_-_That_Sinking_Feeling, SpeedShoppa, ClimbHome --- ["Change bounciness: Tap [B]"] = "", -- Basic_Training_-_Grenade --- ["Change Content: [Left], [Right]"] = "", -- HedgeEditor --- ["Change detonation timer: Tap the [Clock]"] = "", -- Basic_Training_-_Grenade, A_Classic_Fairytale:shadow --- ["Change direction: [Left]/[Right]"] = "", -- Basic_Training_-_Grenade --- ["Change Health Boost: [Left], [Right]"] = "", -- HedgeEditor --- ["Change Health: [Left], [Right]"] = "", -- HedgeEditor --- ["Change modification mode: [Left], [Right]"] = "", -- HedgeEditor --- ["Change Placement Mode: [Up], [Down]"] = "", -- HedgeEditor --- ["Change Rotation: [Left], [Right]"] = "", -- HedgeEditor --- ["Change Sprite Frame: [Precise]+[Left], [Precise]+[Right]"] = "", -- HedgeEditor --- ["Change Sprite: [Left], [Right]"] = "", -- HedgeEditor --- ["Change Timer: [Left], [Right]"] = "", -- HedgeEditor --- ["Change weapon: [Long jump] or [Slot 1]-[Slot 3]"] = "", -- Tumbler --- ["Charmander"] = "", -- --- ["Chasing the blue hog"] = "", -- A_Space_Adventure:moon02 --- ["Cheater"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Checkpoint reached!"] = "", -- A_Space_Adventure:cosmos, A_Space_Adventure:ice01, A_Space_Adventure:moon01 --- ["Chef"] = "", -- Battalion, HedgeEditor --- ["Chester"] = "", -- --- ["Chicken"] = "", -- --- ["Chief Sandologist"] = "", -- A_Space_Adventure:desert01 --- ["Chikorita"] = "", -- --- ["Choose location: Left click"] = "", -- A_Classic_Fairytale:shadow --- ["Choose location: Tap the [Target] button, then tap on the spot you want to choose"] = "", -- A_Classic_Fairytale:shadow --- ["Choose Selection/Placement/Deletion: [Left], [Right]"] = "", -- HedgeEditor --- ["Choose your continent wisely, as your decision will be permanent."] = "", -- Continental_supplies --- ["Choose your side! If you want to join the strange man, walk up to him.|Otherwise, walk away from him. If you decide to att...nevermind..."] = "", -- A_Classic_Fairytale:shadow --- ["Chunli"] = "", -- --- ["Clark Kent"] = "", -- --- ["Cleaver"] = "", -- Construction_Mode --- ["Cleaver Placement Mode"] = "", -- Construction_Mode --- ["CLEAVER PLACEMENT MODE"] = "", -- HedgeEditor --- ["Climb Home"] = "", -- ClimbHome --- ["Closing in"] = "", -- A_Classic_Fairytale:queen --- ["Clown"] = "", -- HedgeEditor --- ["Clowns"] = "", -- User_Mission_-_Nobody_Laugh --- ["Cluck-cluck time: [Fire an egg ~ Sabotages and cures poison ~ Cannot be fired close to another hog]"] = "", -- Continental_supplies --- ["Clumsy"] = "", --- ["Cluster Bomb Training"] = "", -- Basic_Training_-_Cluster_Bomb --- ["Collateral Damage"] = "", -- A_Classic_Fairytale:journey --- ["Collateral Damage II"] = "", -- A_Classic_Fairytale:journey --- ["- Collect all the blue crates"] = "", -- HedgeEditor --- ["Collect all the crates, but remember, our time in this life is limited!"] = "", -- A_Classic_Fairytale:first_blood --- ["Collect or destroy all the health crates."] = "", -- User_Mission_-_RCPlane_Challenge --- ["Collect or destroy the final crate to finish the training."] = "", -- Basic_Training_-_Flying_Saucer --- ["- Collect the blue crate"] = "", -- HedgeEditor --- ["Collect the crate and attack!"] = "", -- WxW --- ["Collect the crate on the right."] = "", -- A_Classic_Fairytale:first_blood --- ["Collect the crates within the time limit!|If you fail, you'll have to try again."] = "", -- A_Classic_Fairytale:first_blood --- ["Collect the first crate to begin!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Collect the freezer and get the device part from Thanta."] = "", -- A_Space_Adventure:ice01 --- ["Collect the green and purple invaders."] = "", -- Space_Invasion --- ["Collect the remaining crates to complete the training."] = "", -- Basic_Training_-_Movement --- ["Collect the weapon crate and drop|a grenade from rope to destroy the barrels."] = "", -- Basic_Training_-_Rope --- ["Collect the weapon crate at the left coast!"] = "", -- A_Classic_Fairytale:journey --- ["Color Squad"] = "", -- --- ["Come closer and die! … burp …"] = "", -- A_Classic_Fairytale:queen --- ["Come closer, so that your training may continue!"] = "", -- A_Classic_Fairytale:first_blood --- ["Comet"] = "", -- Big_Armory --- ["Commander"] = "", -- HedgeEditor --- ["Compete to use as few planes as possible!"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Complete all main and side missions to complete the spacetrip mission."] = "", -- A_Space_Adventure:cosmos --- ["Complete the obstacle course."] = "", -- Basic_Training_-_Movement --- ["Complete the remaining side missions to complete this mission."] = "", -- A_Space_Adventure:cosmos --- ["Complete the track as fast as you can!"] = "", --- ["Completion time: %.2fs"] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Comrades! Sail me away!"] = "", -- A_Classic_Fairytale:queen --- ["Configuration accepted."] = "", -- WxW --- ["Configuration phase"] = "", -- WxW --- ["Congrats! You won!"] = "", -- A_Space_Adventure:moon01 --- ["Congratulations"] = "", -- Basic_Training_-_Rope --- ["Congratulations, you acquired the device part!"] = "", -- A_Space_Adventure:ice01 --- ["Congratulations, you are the best!"] = "", -- A_Space_Adventure:desert03 --- ["Congratulations, you are the fastest!"] = "", -- A_Space_Adventure:moon02 --- ["Congratulations, you collected the device part!"] = "", -- A_Space_Adventure:ice01 --- ["Congratulations! You have completed the obstacle course!"] = "", -- Basic_Training_-_Movement --- ["Congratulations! You have destroyed all targets within the time."] = "", -- TargetPractice --- ["Congratulations, you have saved Hogera!"] = "", -- A_Space_Adventure:final --- ["Congratulations! You have truly mastered this challenge! Don't forget to save the demo."] = "", -- User_Mission_-_RCPlane_Challenge --- ["Congratulations! You've completed the Basic Rope Training!"] = "", -- Basic_Training_-_Rope - ["Congratulations! You've eliminated all targets|within the allowed time frame."] = "恭喜!你在规定时限内清零全部目标。", --Bazooka, Shotgun, SniperRifle --- ["Congratulations! You win."] = "", -- Big_Armory --- ["Congratulations, you won!"] = "", -- A_Space_Adventure:death01, A_Space_Adventure:death02, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:fruit02, A_Space_Adventure:fruit03, A_Space_Adventure:ice02 - ["Congratulations!"] = "恭喜", --- ["Conquering the galaxy"] = "", -- A_Space_Adventure:cosmos --- ["CONSTRUCTION MODE"] = "", -- Construction_Mode --- ["Construction Mode tool"] = "", -- Construction_Mode --- ["Construction Station: Allows placement of| girders, rubber, mines, sticky mines| and barrels."] = "", -- Construction_Mode --- ["Construction Station"] = "", -- Construction_Mode --- ["Continental supplies"] = "", -- Continental_supplies --- ["Continent selection"] = "", -- Continental_supplies --- ["Continents: Select a continent at the beginning."] = "", -- Continental_supplies --- ["Control"] = "", -- Control - ["Control pillars to score points."] = "控制支柱得分", --- ["Controls: Hold the Attack key (space by default) to|fire the rope, then, once attached use:|Left and Right to swing the rope;|Up and Down to contract and expand."] = "", -- Basic_Training_-_Rope --- ["Copper"] = "", -- User_Mission_-_Nobody_Laugh --- ["Corn"] = "", -- A_Space_Adventure:fruit01 --- ["Corporal Calvin"] = "", -- --- ["Corporationals"] = "", -- A_Classic_Fairytale:queen --- ["Corpsemonger"] = "", -- A_Classic_Fairytale:shadow --- ["Corpse Thrower"] = "", -- A_Classic_Fairytale:epil --- ["Cost"] = "", -- Construction_Mode --- ["Cost: %d"] = "", -- Construction_Mode --- ["Cotton Needer"] = "", -- Mutant --- ["Count Hogula"] = "", -- --- ["Coward"] = "", -- A_Classic_Fairytale:queen --- ["Crate Before Attack: %s"] = "", -- WxW --- ["Crate Before Attack: You must collect a crate before you can attack."] = "", -- WxW --- ["Crate Placer"] = "", -- Construction_Mode --- ["Crates: Crates drop more often with a higher chance of bonus ammo"] = "", -- Battalion --- ["Crates: Crates drop randomly and may be empty"] = "", -- Battalion --- ["Crates: Crates drop randomly with chance of being empty"] = "", -- Battalion --- ["Crates left: %d"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Crates Left:"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Crates per turn: %d"] = "", -- WxW --- ["Crazy Gravity: Gravity randomly changes within a range from %i%% to %i%% with a period of %s"] = "", -- Gravity --- ["Crazy Runner"] = "", -- A_Space_Adventure:moon02 --- ["Cricket Time"] = "", -- Continental_supplies --- ["Cricket time: [Fire away a 1 sec mine! ~ Cannot be fired close to another hog]"] = "", -- Continental_supplies --- ["CTF_Blizzard"] = "", -- CTF_Blizzard --- ["Cursor: Build structure"] = "", -- Construction_Mode --- ["Cursor: Mode action"] = "", -- HedgeEditor --- ["|Cursor: Place crate"] = "", -- Construction_Mode --- ["Cursor: Place waypoint"] = "", -- Racer --- ["Cutlass Cain"] = "", -- - ["Cybernetic Empire"] = "自动化帝国", --- ["Cyborg. It's what the aliens call themselves."] = "", -- A_Classic_Fairytale:enemy --- ["Dahmer"] = "", -- A_Classic_Fairytale:backstab --- ["Daisy"] = "", -- - ["DAMMIT, ROOKIE! GET OFF MY HEAD!"] = "新人,别让我看到", - ["DAMMIT, ROOKIE!"] = "新人", --- ["+%d ammo"] = "", -- Battalion --- ["+%d Ammo"] = "", -- Space_Invasion - ["Dangerous Ducklings"] = "危险的小鸭子", --- ["Dark Strawberry"] = "", -- A_Space_Adventure:fruit02 --- ["+%d"] = "", -- Battalion --- ["%d crate(s) remaining"] = "", -- SpeedShoppa --- ["%d damage was dealt in this game."] = "", -- Mutant --- ["%d / %d"] = "", -- Battalion --- ["%d | %d"] = "", -- Mutant --- ["%d/%d"] = "", -- SpeedShoppa --- ["Deadly Grape"] = "", -- A_Space_Adventure:fruit02 --- ["Deadweight"] = "", --- ["Deal 15 damage + 10% of your hog’s health to all hogs around you and get 2/3 back."] = "", -- Continental_supplies --- ["Deals 15 damage to all enemies in the circle."] = "", -- Continental_supplies --- ["Deer"] = "", -- --- ["Defeat all enemies!"] = "", -- portal --- ["Defeat!"] = "", -- HedgeEditor --- ["Defeat Professor Hogevil!"] = "", -- A_Space_Adventure:death01 --- ["Defeat the cannibals!"] = "", -- A_Classic_Fairytale:shadow --- ["Defeat the cannibals!|Grenade hint: set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"] = "", -- A_Classic_Fairytale:shadow --- ["Defeat the cyborgs!"] = "", -- A_Classic_Fairytale:enemy --- ["Defeat the enemy!"] = "", -- A_Classic_Fairytale:queen --- ["Delete Waypoint"] = "", -- HedgeEditor --- ["Deletion Mode: [5]"] = "", -- HedgeEditor --- ["Deletion Mode"] = "", -- HedgeEditor --- ["Deletition Mode"] = "", -- HedgeEditor --- ["Demolition is fun!"] = "", --- ["Demo"] = "", -- The_Specialists --- ["Dense Cloud"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow, A_Classic_Fairytale:united --- ["Dense Cloud must have already told them everything..."] = "", -- A_Classic_Fairytale:shadow --- ["Dense Cloud?! What are you doing?!"] = "", -- A_Classic_Fairytale:queen --- ["Depleted Kamikaze! +5 points!"] = "", -- Space_Invasion --- ["Derp"] = "", -- User_Mission_-_Nobody_Laugh --- ["Desert Storm"] = "", -- --- ["Destroy all targets with no more than 10 bazookas."] = "", -- Basic_Training_-_Bazooka --- ["Destroy all targets with no more than 5 bazookas."] = "", -- Basic_Training_-_Bazooka --- ["Destroy all the targets!"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["Destroyer of planes"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Destroy him, Leaks A Lot! He is responsible for the deaths of many of us!"] = "", -- A_Classic_Fairytale:first_blood --- ["Destroy invaders and collect bonuses to score points."] = "", -- Space_Invasion --- ["- Destroy the enemy"] = "", -- HedgeEditor --- ["- Destroy the red target"] = "", -- HedgeEditor --- ["- Destroy the red targets"] = "", -- HedgeEditor --- ["Destroy the targets!"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["+%d flamer fuel!"] = "", -- Tumbler --- ["+%d health"] = "", -- Mutant --- ["%d-Hit Combo! +%d points!"] = "", -- Space_Invasion --- ["Did anyone follow you?"] = "", -- A_Classic_Fairytale:united --- ["Did I miss something?"] = "", -- Space_Invasion --- ["Did not finish"] = "", -- Racer, TechRacer --- ["Did you really think I've changed?"] = "", -- A_Classic_Fairytale:queen --- ["Did you really think that I've changed?"] = "", -- A_Classic_Fairytale:queen --- ["Did you really think that we needed the help of one of you?"] = "", -- A_Classic_Fairytale:queen --- ["Did you see him coming?"] = "", -- A_Classic_Fairytale:shadow --- ["Did you warn the village?"] = "", -- A_Classic_Fairytale:shadow --- ["Die, die, die!"] = "", -- A_Classic_Fairytale:dragon --- ["Difficulty: "] = "", -- Continental_supplies --- ["Difficulty: Easy"] = "", -- A_Classic_Fairytale:first_blood --- ["Difficulty: Hard"] = "", -- A_Classic_Fairytale:first_blood --- ["Dimitry"] = "", -- --- ["%d invaders have been destroyed in this game."] = "", -- Space_Invasion --- ["Disabled"] = "", -- WxW --- ["Disguise as a Rockhopper Penguin"] = "", -- Continental_supplies --- ["Disguise as a Rockhopper Penguin: [Swap place with a random enemy hog in the circle]"] = "", -- Continental_supplies --- ["Displacer"] = "", -- --- ["Diver"] = "", -- User_Mission_-_Diver --- ["%d ms"] = "", -- HedgeEditor --- ["Doing stuff a monkey could do."] = "", -- A_Classic_Fairytale:queen --- ["Domination game"] = "", -- Control --- ["Donald"] = "", -- --- ["Do not destroy the crates!"] = "", -- A_Space_Adventure:fruit02 --- ["Do not laugh, inexperienced one, for he speaks the truth!"] = "", -- A_Classic_Fairytale:backstab --- ["Do not let his words fool you, young one! He will stab you in the back as soon as you turn away!"] = "", -- A_Classic_Fairytale:first_blood --- ["Don't be foolish, son, there will be more."] = "", -- A_Space_Adventure:fruit01 --- ["Don't blow up the crate."] = "", -- A_Classic_Fairytale:journey --- ["Don't destroy the device crate!"] = "", -- A_Space_Adventure:desert01 --- ["Don't eliminate Captain Lime before collecting the last crate!"] = "", -- A_Space_Adventure:fruit02 --- ["Don't hit me, you fools!"] = "", -- A_Space_Adventure:moon01 --- ["Don't hit yourself!"] = "", -- Basic_Training_-_Bazooka --- ["Don't touch the flames!"] = "", -- ClimbHome --- ["Don't you dare harming our tribe!"] = "", -- A_Classic_Fairytale:queen --- ["Double Kill!"] = "", --- ["Double kill!"] = "", -- Mutant --- ["Do you have any idea how bad an exploding arrow hurts?"] = "", -- A_Classic_Fairytale:queen --- ["Do you have any idea how valuable grass is?"] = "", -- A_Classic_Fairytale:enemy --- ["Do you have any idea what it's like in the village for a woman?"] = "", -- A_Classic_Fairytale:queen --- ["Do you know where they are?"] = "", -- A_Classic_Fairytale:queen --- ["Do you think you're some kind of god?"] = "", -- A_Classic_Fairytale:enemy --- ["Dragon's Lair"] = "", -- A_Classic_Fairytale:dragon --- ["Dr. Banting"] = "", -- --- ["Dr. Barnard"] = "", -- --- ["Dr. Blackwell"] = "", -- --- ["Dr. Cornelius"] = "", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01 --- ["Dr. Crushing"] = "", -- --- ["Dr. Drew"] = "", -- --- ["Dr. Harvey"] = "", -- --- ["Dr. Hollows"] = "", -- --- ["Dr. Horace"] = "", -- --- ["Drills"] = "", -- A_Classic_Fairytale:backstab --- ["Drill Strike"] = "", -- Construction_Mode --- ["Dr. Jenner"] = "", -- --- ["Dr. Jung"] = "", -- --- ["Drone Hunter! +10 points!"] = "", -- Space_Invasion --- ["Drop a ball of dirt which turns into a|cluster on impact. Doesn’t end turn."] = "", -- Continental_supplies --- ["Drop a bomb: [Drop some heroic wind that will turn into a bomb on impact]"] = "", -- Continental_supplies --- ["- Dropped flags may be returned or recaptured"] = "", -- Capture_the_Flag --- ["Dropping a weapon while in water would just drown it, but launching one would work."] = "", -- Basic_Training_-_Flying_Saucer --- ["Drop weapon (while on rope): [Long Jump]"] = "", -- Basic_Training_-_Rope --- ["Drowner"] = "", --- ["Dr. Parkinson"] = "", -- --- ["Drunk greenhorn"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Drunk with power, perhaps!"] = "", -- A_Classic_Fairytale:queen --- ["%d sec"] = "", -- Construction_Mode --- ["+%d seconds!"] = "", -- Tumbler --- ["Dubloon Devil"] = "", -- --- ["Dude, all the plants are gone!"] = "", -- A_Classic_Fairytale:family --- ["Dude, can you see Ramon and Spiky?"] = "", -- A_Classic_Fairytale:journey --- ["Dude, it's unbearable!"] = "", -- A_Classic_Fairytale:queen --- ["Dude, let me out!"] = "", -- A_Classic_Fairytale:epil --- ["Dude, that outfit is so cool!"] = "", -- A_Classic_Fairytale:epil --- ["Dude, that's so cool!"] = "", -- A_Classic_Fairytale:backstab --- ["Dude, this is boring!"] = "", -- A_Classic_Fairytale:queen --- ["Dude, we really need a new shaman..."] = "", -- A_Classic_Fairytale:shadow --- ["Dude, what's this place?!"] = "", -- A_Classic_Fairytale:dragon --- ["Dude, where are we?"] = "", -- A_Classic_Fairytale:backstab --- ["Dude, wow! I just had the weirdest high!"] = "", -- A_Classic_Fairytale:backstab --- ["Dude, wow, you're so cute!"] = "", -- A_Classic_Fairytale:queen --- ["Dud Mine Placement Mode"] = "", -- HedgeEditor --- ["DUD MINE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Duration"] = "", -- Continental_supplies --- ["During the final testing of the device an accident happened."] = "", -- A_Space_Adventure:moon02 --- ["During the game you can get new RC planes by collecting the weapon crates."] = "", -- A_Space_Adventure:desert03 --- ["Dust Storm"] = "", -- Continental_supplies --- ["Dust storm: [Deals 15 damage to all enemies in the circle]"] = "", -- Continental_supplies --- ["Each time you destroy all the targets on your current level you'll get teleported to the next level."] = "", -- A_Space_Adventure:desert03 --- ["Each time you play this missions enemy hogs will play in a random order."] = "", -- A_Space_Adventure:death02 --- ["Each turn is only ONE SECOND!"] = "", -- Frenzy --- ["Each turn you get 1-3 random weapons"] = "", --- ["Each turn you get one random weapon"] = "", --- ["Each turn you'll have only one rope to use."] = "", -- A_Space_Adventure:moon02 --- ["Eagle Eye"] = "", -- A_Classic_Fairytale:backstab --- ["Eagle Eye: [Blink to the impact ~ One shot]"] = "", -- Continental_supplies --- ["Ear Sniffer"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:epil --- ["EASY"] = "", -- Continental_supplies --- ["Eckles"] = "", -- User_Mission_-_Nobody_Laugh --- ["Eclipse"] = "", -- Big_Armory --- ["Editing Commands: (Use while no weapon is selected)"] = "", -- HedgeEditor --- ["Ehm, okay ..."] = "", -- A_Space_Adventure:moon01 --- ["Elderbot"] = "", -- A_Classic_Fairytale:family --- ["Elimate your captor."] = "", -- User_Mission_-_The_Great_Escape - ["Eliminate all targets before your time runs out.|You have unlimited ammo for this mission."] = "时间限制内清除全部目标。弹药无限。", --Bazooka, Shotgun, SniperRifle --- ["Eliminate the enemy before the time runs out."] = "", -- User_Mission_-_Diver, User_Mission_-_Spooky_Tree --- ["Eliminate the enemy hogs to win."] = "", --- ["Eliminate the enemy."] = "", -- User_Mission_-_Bamboo_Thicket, User_Mission_-_Newton_and_the_Hammock, User_Mission_-_Nobody_Laugh --- ["Eliminate Unit 3378."] = "", -- User_Mission_-_Teamwork --- ["Eliminate WatchBot 4000."] = "", -- User_Mission_-_Teamwork_2 --- ["Eliminate your captor."] = "", -- User_Mission_-_The_Great_Escape --- ["Elite pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Elmo"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Enabled"] = "", -- WxW --- ["Enemy kills: Collect victim's weapons and +%d%% of its base health"] = "", -- Battalion --- ["Energetic Engineer"] = "", --- ["Engineer"] = "", -- HedgeEditor, The_Specialists - ["Enjoy the swim..."] = "游水愉快", --- ["Entered boredom phase! Discrepancies detected …"] = "", -- A_Classic_Fairytale:queen --- ["Epilogue"] = "", -- A_Classic_Fairytale:epil --- ["ERROR [getHogInfo]: Hog is nil!"] = "", -- Battalion --- ["Eugene"] = "", -- --- ["Europe"] = "", -- Continental_supplies --- ["Everyone knows this."] = "", -- A_Classic_Fairytale:enemy --- ["Every single time!"] = "", -- A_Classic_Fairytale:dragon --- ["Everything looks OK..."] = "", -- A_Classic_Fairytale:enemy --- ["Every time you kill an enemy hog your ammo will get reset next turn."] = "", -- A_Space_Adventure:death02 --- ["Everywhere I look, I see hogs walking around …"] = "", -- A_Classic_Fairytale:epil --- ["Exactly, man! That was my dream."] = "", -- A_Classic_Fairytale:backstab --- ["Except me, of course! I just saved a whole planet!"] = "", -- A_Space_Adventure:final --- ["Experienced beginner"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Explore the tunnel with the other hedgehogs and search for the device."] = "", -- A_Space_Adventure:fruit02 --- ["Exploring the tunnel"] = "", -- A_Space_Adventure:fruit02 --- ["Eye Chewer"] = "", -- A_Classic_Fairytale:journey --- ["Fair Wind"] = "", -- --- ["Fall Damage"] = "", -- Basic_Training_-_Movement --- ["Fallen Angel"] = "", -- Tentacle_Terror --- ["Family Reunion"] = "", -- A_Classic_Fairytale:family --- ["Fastest escape: %d turns"] = "", -- A_Space_Adventure:desert02 --- ["Fastest lap: %.3fs by %s"] = "", -- TrophyRace - ["Fastest lap: "] = "最快记录:", - ["Feeble Resistance"] = "反抗者", --- ["Fell From Grace"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Fell From Heaven"] = "", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen --- ["Fell From Heaven is the best! Fell From Heaven is the greatest!"] = "", -- A_Classic_Fairytale:family --- ["Femur Lover"] = "", -- A_Classic_Fairytale:shadow --- ["Fierce Competition! +8 points!"] = "", -- Space_Invasion --- ["Fiery Water"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:united --- ["Fiery Water?! Are you drunk again?"] = "", -- A_Classic_Fairytale:queen --- ["Fighting instead of cultivating a beautiful friendship."] = "", -- A_Classic_Fairytale:epil --- ["Fight: Press [Attack]"] = "", -- A_Space_Adventure:fruit01 --- ["Filthy Blue"] = "", -- User_Mission_-_Dangerous_Ducklings --- ["Final Challenge:"] = "", -- Basic_Training_-_Rope --- ["Finally! We're out of this hellhole. Now go save the princess, %s!"] = "", -- A_Classic_Fairytale:family --- ["Finally you are here!"] = "", -- A_Space_Adventure:desert01, A_Space_Adventure:ice01 --- ["Final result"] = "", -- Mutant --- ["Final Targets"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["Final team scores:"] = "", -- Space_Invasion --- ["Find all the parts of the anti-gravity device."] = "", -- A_Space_Adventure:cosmos --- ["Find a way to detonate all the explosives and stay alive!"] = "", -- A_Space_Adventure:final --- ["Find your tribe!|Cross the lake!"] = "", -- A_Classic_Fairytale:dragon --- ["Finish this challenge as fast as possible to earn bonus points."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Finish waypoint placement"] = "", -- Racer --- ["Finish your training."] = "", -- A_Classic_Fairytale:first_blood --- ["Finite Ropes"] = "", -- Basic_Training_-_Rope --- ["Fire a rocket with napalm."] = "", -- Continental_supplies --- ["Fire: [Precise]"] = "", -- Space_Invasion, Tumbler --- ["Fire some exploding medicine that will heal 15 health to all hogs in its effect radius."] = "", -- Continental_supplies --- ["Fire your hedgehog like a sticky mine."] = "", -- Continental_supplies --- ["First aid kits?!"] = "", -- A_Classic_Fairytale:united --- ["First Blood"] = "", -- A_Classic_Fairytale:first_blood --- ["- First clan to capture the flag wins"] = "", -- Capture_the_Flag --- ["- First clan to score %d captures wins"] = "", -- Capture_the_Flag --- ["First killer will mutate"] = "", -- Mutant --- ["First Steps"] = "", -- A_Classic_Fairytale:first_blood --- ["- First team to capture the flag wins"] = "", -- Capture_the_Flag --- ["- First team to score %d captures wins"] = "", -- Capture_the_Flag --- ["Fishy"] = "", -- - ["Flag captured!"] = "夺旗得分!", - ["Flag respawned!"] = "旗帜重生!", - ["Flag returned!"] = "旗帜归还!", --- ["Flamer"] = "", --- ["Flaming Worm"] = "", -- A_Classic_Fairytale:backstab --- ["Flare"] = "", -- Continental_supplies --- ["Flawless victory!"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Flee: Press [Jump]"] = "", -- A_Space_Adventure:fruit01 --- ["Flesh for Brainz"] = "", -- A_Classic_Fairytale:journey --- ["Fly around and hurl explosives to your enemies."] = "", -- Tumbler --- ["Flying Saucer Training"] = "", -- Basic_Training_-_Flying_Saucer --- ["Fly into space to fight off the invaders with barrels!"] = "", -- Space_Invasion --- ["Fly to the meteorite and detonate the explosives"] = "", -- A_Space_Adventure:cosmos --- ["Follow the path and destroy the next target."] = "", -- Basic_Training_-_Rope --- ["Forgetfulness: You will lose all your weapons each turn."] = "", -- Continental_supplies --- ["For the next crate, you have to do back jumps."] = "", -- Basic_Training_-_Movement --- ["Four Eyes"] = "", -- --- ["Frankie"] = "", -- --- ["Frank"] = "", -- User_Mission_-_Nobody_Laugh --- ["Free Dense Cloud and continue the mission!"] = "", -- A_Classic_Fairytale:journey --- ["FRENZY"] = "", -- Frenzy --- ["Friendly Fire!"] = "", --- ["Friendly kills: Clear killer's pool and -%d%% of its base health"] = "", -- Battalion --- ["From the second turn and beyond the water rises."] = "", -- A_Space_Adventure:desert02 --- ["Frozen Bandits"] = "", -- A_Space_Adventure:ice01 --- ["Fruit"] = "", -- --- ["Fruit Assassins"] = "", -- A_Space_Adventure:fruit02 --- ["Fruity"] = "", -- --- ["Fuel: %d"] = "", -- Tumbler --- ["Fuzzy Beard"] = "", -- --- ["“g=150”, where 150 is 150% of normal gravity."] = "", -- Gravity --- ["“g=50, g2=150, period=4000” for gravity changing|from 50 to 150 and back with period of 4000 ms."] = "", -- Gravity --- ["Galaxy Guardians"] = "", -- Big_Armory --- ["Game over!"] = "", -- Space_Invasion - ["Game Started!"] = "开始", --- ["Game? Was this a game to you?!"] = "", -- A_Classic_Fairytale:enemy --- ["Gangsters"] = "", -- --- ["GasBomb"] = "", -- Continental_supplies --- ["Gas Gargler"] = "", -- A_Classic_Fairytale:queen --- ["Gasp! A smuggler!"] = "", -- A_Space_Adventure:desert01 --- ["Gasp!"] = "", -- A_Space_Adventure:desert01 --- ["Gathering fruits all day long."] = "", -- A_Classic_Fairytale:queen --- ["Gear Placement Tool"] = "", -- HedgeEditor --- ["General information"] = "", -- Continental_supplies --- ["General information:"] = "", -- Continental_supplies --- ["General Lemon"] = "", -- A_Space_Adventure:fruit01 --- ["Generator"] = "", -- Construction_Mode --- ["Generator: Generates energy."] = "", -- Construction_Mode --- ["Get Dense Cloud out of the pit!"] = "", -- A_Classic_Fairytale:journey --- ["Get him, Spike!"] = "", -- A_Space_Adventure:desert01 - ["Get on over there and take him out!"] = "上去把它拉下来!", --- ["Get on the head of the mole."] = "", -- A_Classic_Fairytale:first_blood --- ["Get past the flower."] = "", -- A_Classic_Fairytale:journey --- ["Get ready to fight!"] = "", -- A_Space_Adventure:moon01 --- ["Get that crate!"] = "", -- A_Classic_Fairytale:first_blood --- ["Get the crate on the other side of the island!|"] = "", -- A_Classic_Fairytale:journey --- ["Get the crate on the other side of the island."] = "", -- A_Classic_Fairytale:journey --- ["Get the final crate to the right to complete the training."] = "", -- Basic_Training_-_Movement --- ["Get the highest score to win."] = "", -- Space_Invasion --- ["Get the next crate by jumping over the abyss."] = "", -- Basic_Training_-_Movement --- ["Getting ready"] = "", -- A_Space_Adventure:cosmos, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:ice01, A_Space_Adventure:ice02, A_Space_Adventure:moon01 --- ["Getting Started"] = "", -- Basic_Training_-_Rope --- ["Getting to the device"] = "", -- A_Space_Adventure:fruit02 --- ["Get to the crate using your flying saucer!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Get to the target using your rope!"] = "", -- Basic_Training_-_Rope --- ["Get your teammates out of their natural prison and save the princess!"] = "", -- A_Classic_Fairytale:family --- ["Get your teammates out of their natural prison and save the princess!|Hint: Drilling holes should solve everything.|Hint: It might be a good idea to place a girder before starting to drill. Just saying.|Hint: All your hedgehogs need to be above the marked height!|Hint: Leaks A Lot needs to get really close to the princess!"] = "", -- A_Classic_Fairytale:family --- ["Giggles"] = "", -- --- ["Gimme Bones"] = "", -- A_Classic_Fairytale:backstab --- ["Girder"] = "", -- Construction_Mode --- ["Girder Placement Mode"] = "", -- Construction_Mode --- ["GIRDER PLACEMENT MODE"] = "", -- HedgeEditor --- ["Give a hog a preset identity and weapons"] = "", -- HedgeEditor --- ["Give an entire team themed hats and names"] = "", -- HedgeEditor --- ["Glark"] = "", -- A_Classic_Fairytale:shadow --- ["Glasses"] = "", -- --- ["Glassy"] = "", -- --- ["Goal Definition Mode"] = "", -- HedgeEditor --- ["GOAL DEFINITION MODE"] = "", -- HedgeEditor --- ["Goal: Score %d points or more to win!"] = "", -- Mutant --- ["Go and collect the crate"] = "", -- A_Space_Adventure:cosmos --- ["Godai"] = "", -- --- ["Go down and save these PAotH hogs!"] = "", -- A_Space_Adventure:moon01 --- ["Go, get him again!"] = "", -- A_Space_Adventure:moon02 --- ["Goggles"] = "", -- --- ["Goggs"] = "", -- - ["GO! GO! GO!"] = "上!", - ["Good birdy......"] = "乖鸟儿", --- ["Good bye!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Good idea, they'll never find us there!"] = "", -- A_Classic_Fairytale:united --- ["Good job!"] = "", -- Basic_Training_-_Flying_Saucer, Basic_Training_-_Rope --- ["Good job! Defeat the rest of the aliens!"] = "", -- A_Classic_Fairytale:queen --- ["Good job! Now destroy the final targets to finish the training."] = "", -- Basic_Training_-_Grenade --- ["Good luck!"] = "", -- A_Space_Adventure:desert01, A_Space_Adventure:fruit02 --- ["Good luck...or else!"] = "", -- A_Classic_Fairytale:journey - ["Good luck out there!"] = "祝好运", --- ["Good so far!"] = "", --- ["Good to go!"] = "", --- ["Good! You now control Cappy."] = "", -- Basic_Training_-_Movement --- ["Go on top of the flower."] = "", -- A_Classic_Fairytale:first_blood --- ["Go, quick!"] = "", -- A_Classic_Fairytale:backstab --- ["Gorkij"] = "", -- A_Classic_Fairytale:journey --- ["Go surf!"] = "", -- WxW --- ["Got 1 more saucer and 8 more seconds added to the clock"] = "", -- A_Space_Adventure:ice02 --- ["Got 1 more saucer"] = "", -- A_Space_Adventure:ice02 --- ["GOTCHA!"] = "", --- ["Go to Thanta and get the device part!"] = "", -- A_Space_Adventure:ice01 --- ["Go to the surface!"] = "", -- A_Space_Adventure:fruit02 --- ["Go to the target."] = "", -- Basic_Training_-_Rope --- ["Go to the upper platform and get the weapons in the crates!"] = "", -- A_Space_Adventure:moon01 --- ["Got the saucer!"] = "", -- A_Space_Adventure:cosmos --- ["Got to go back."] = "", -- A_Space_Adventure:cosmos --- ["Got you? You're acting weird."] = "", -- A_Classic_Fairytale:queen --- ["Grab mines/barrels: [High jump]"] = "", -- Tumbler --- ["Gravity: 100%"] = "", -- Gravity --- ["Great!"] = "", -- Basic_Training_-_Rope --- ["Great choice, Steve! Mind if I call you that?"] = "", -- A_Classic_Fairytale:shadow --- ["Great! Let’s kill all these enemies, using portals."] = "", -- portal --- ["Great work! Now hit it with your Baseball Bat! |Tip: You can change weapon with 'Right Click'!"] = "", -- Basic_Training_-_Rope --- ["Great! You will be contacted soon for assistance."] = "", -- A_Classic_Fairytale:shadow --- ["Green"] = "", -- --- ["Green Bananas"] = "", -- A_Space_Adventure:fruit01 --- ["Green double rings also give you a new flying saucer."] = "", -- A_Space_Adventure:ice02 --- ["Green Hog Grape"] = "", -- A_Space_Adventure:fruit01 --- ["Green hogs won't intentionally hurt you."] = "", -- A_Space_Adventure:fruit01 --- ["Greenhorn"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Green Lipstick Bullet"] = "", -- Continental_supplies --- ["Green lipstick bullet: [Poisonous, deals no damage]"] = "", -- Continental_supplies --- ["Greetings, cloudy one!"] = "", -- A_Classic_Fairytale:shadow --- ["Greetings from the Navy, %s (%s), for being a distance of %d away from the mainland!"] = "", -- ClimbHome --- ["Greetings, %s!"] = "", -- A_Classic_Fairytale:dragon --- ["Grenades explode after 1 to 5 seconds (you decide)."] = "", -- Basic_Training_-_Grenade --- ["Grenades with high bounciness bounce a lot and behave chaotic."] = "", -- Basic_Training_-_Grenade --- ["Grenade Training"] = "", -- Basic_Training_-_Grenade --- ["Grenadiers"] = "", -- Basic_Training_-_Grenade --- ["Grenadier"] = "", -- Target_Practice_-_Grenade_easy, Target_Practice_-_Grenade_hard, HedgeEditor --- ["Grey"] = "", -- --- ["Guards"] = "", -- A_Space_Adventure:cosmos --- ["Guile"] = "", -- --- ["Guys, do you think there's more of them?"] = "", -- A_Classic_Fairytale:backstab --- ["HAHA!"] = "", -- A_Classic_Fairytale:enemy --- ["Haha!"] = "", -- A_Classic_Fairytale:united --- ["Haha! Come!"] = "", -- A_Classic_Fairytale:queen --- ["Hahahaha!"] = "", --- ["Haha, I love the look on your face!"] = "", -- A_Classic_Fairytale:queen --- ["Haha, now THAT would be something!"] = "", --- ["Haha, that was just a coincidence!"] = "", -- A_Classic_Fairytale:queen --- ["Hammer"] = "", -- Construction_Mode, Continental_supplies --- ["Hannibal"] = "", -- A_Classic_Fairytale:epil --- ["Hapless Hogs"] = "", --- [" Hapless Hogs left!"] = "", --- ["Happy with your race track?|Then stop building and start racing!"] = "", -- Racer --- ["HARD"] = "", -- Continental_supplies --- ["Hard flying"] = "", -- A_Space_Adventure:ice02 --- ["Harris"] = "", -- --- ["Harry Potter"] = "", -- --- ["Harry"] = "", -- User_Mission_-_Nobody_Laugh --- ["H"] = "", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01 --- ["Hatless Jerry"] = "", -- A_Classic_Fairytale:queen --- ["Have no illusions, your tribe is dead, indifferent of your choice."] = "", -- A_Classic_Fairytale:shadow --- ["Haven't found it yet ..."] = "", -- A_Space_Adventure:desert01 --- ["Have we ever attacked you first?"] = "", -- A_Classic_Fairytale:enemy --- ["H confirmed that there isn't such a PAotH activity logged."] = "", -- A_Space_Adventure:desert01 --- ["Healing Station"] = "", -- Construction_Mode --- ["Healing Station: Heals nearby hogs."] = "", -- Construction_Mode --- ["Health and Mission Panel"] = "", -- Basic_Training_-_Movement --- ["Health"] = "", -- Basic_Training_-_Movement --- ["Health Crate Placement Mode"] = "", -- Construction_Mode --- ["HEALTH CRATE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Health: %d"] = "", -- HedgeEditor --- ["Health: Hogs lose up to 7% base health per turn"] = "", -- Battalion --- ["Health Modification Mode"] = "", -- HedgeEditor --- ["HEALTH MODIFICATION MODE"] = "", -- HedgeEditor --- ["Heavenly Defense"] = "", -- Tentacle_Terror --- ["Heavy"] = "", --- ["Heavy Cannfantry"] = "", -- A_Classic_Fairytale:united --- ["Heckles"] = "", -- --- ["Heck, you even executed one of your own!"] = "", -- A_Classic_Fairytale:queen --- ["Hedge-cogs"] = "", -- A_Classic_Fairytale:enemy --- ["HEDGEEDITOR"] = "", -- HedgeEditor --- ["HedgeEditor tool"] = "", -- HedgeEditor --- ["Hedgehog"] = "", -- --- ["Hedgehog Projectile"] = "", -- Continental_supplies --- ["Hedgehog projectile: [Fire your hog like a Sticky Bomb]"] = "", -- Continental_supplies --- ["Hedgehogs can not be deleted."] = "", -- HedgeEditor --- ["Hedgehogs left: %d"] = "", -- User_Mission_-_That_Sinking_Feeling --- ["Hedgehogs will be revived after their death."] = "", -- Mutant --- ["Hedgehogs will start in the first waypoint."] = "", -- Racer --- ["Hedgibal Lecter"] = "", -- A_Classic_Fairytale:backstab --- ["He doesn't know it but this device is a part of the anti-gravity device."] = "", -- A_Space_Adventure:ice01 --- ["He has captured the rest of the PAotH team and awaits to capture you!"] = "", -- A_Space_Adventure:moon01 --- ["Heh, it's not that bad."] = "", --- ["Height over time"] = "", -- ClimbHome --- ["He is a very tough and very determined hedgehog. I would be extremely careful if I were you."] = "", -- A_Space_Adventure:moon02 --- ["Helena"] = "", -- A_Space_Adventure:moon01 --- ["Hell Army"] = "", -- portal --- ["Hello again, %s!"] = "", -- A_Classic_Fairytale:family --- ["Help Disabled"] = "", -- HedgeEditor --- ["Help Enabled"] = "", -- HedgeEditor --- ["Helpers: Each team starts with %d helper points"] = "", -- Battalion --- ["Helpers: Hogs will get 1 out of 2 helpers randomly each turn"] = "", -- Battalion --- ["Help me, Leaks!"] = "", -- A_Classic_Fairytale:journey --- ["Help me, please!!!"] = "", -- A_Classic_Fairytale:journey --- ["Help me, please!"] = "", -- A_Classic_Fairytale:journey --- ["He moves like an eagle in the sky."] = "", -- A_Classic_Fairytale:first_blood --- ["He must be in the village already."] = "", -- A_Classic_Fairytale:journey --- ["HeneK"] = "", -- --- ["Here, let me help you!"] = "", -- A_Classic_Fairytale:backstab --- ["Here, let me help you save her!"] = "", -- A_Classic_Fairytale:family --- ["Here...pick your weapon!"] = "", -- A_Classic_Fairytale:first_blood --- ["Here! Take it!"] = "", -- A_Space_Adventure:ice01 --- ["Here we go!"] = "", -- A_Space_Adventure:moon01 --- ["Here you will find the current mission instructions."] = "", -- Basic_Training_-_Movement --- ["Here you will learn how to fly the flying saucer|and get so learn some cool tricks."] = "", -- Basic_Training_-_Flying_Saucer --- ["Heroic Wind"] = "", -- Continental_supplies --- ["He's so brave..."] = "", -- A_Classic_Fairytale:first_blood --- ["He was the lab assistant of Dr. Goodhogan, the inventor of the anti-gravity device."] = "", -- A_Space_Adventure:moon02 --- ["He won't be selling us out anymore!"] = "", -- A_Classic_Fairytale:backstab --- ["Hey, don't forget us! We still need to climb up!"] = "", -- A_Classic_Fairytale:family --- ["Hey, guys!"] = "", -- A_Classic_Fairytale:backstab --- ["Hey guys!"] = "", -- A_Classic_Fairytale:united --- ["Hey! I was supposed to collect it!"] = "", -- A_Space_Adventure:fruit02 --- ["Hey, %s! Finally you have come!"] = "", -- A_Space_Adventure:moon01 --- ["Hey, %s! Look, someone is stealing the saucer!"] = "", -- A_Space_Adventure:cosmos --- ["Hey! This is cheating!"] = "", -- A_Classic_Fairytale:journey --- ["Hidden"] = "", -- portal --- ["High Gravity: Gravity is %i%%"] = "", -- Gravity --- ["High Jump: [Backspace]"] = "", -- Basic_Training_-_Movement --- ["High Jump: Tap the [Curvy Arrow] shortly"] = "", -- Basic_Training_-_Movement --- ["--- Highland ---"] = "", -- Battalion --- ["Highlander: Eliminate hogs to take their weapons"] = "", -- Highlander --- ["Highland: Hogs get %d random weapons from their pool"] = "", -- Battalion --- ["--- Highland Mode ---"] = "", -- Battalion --- ["High Target"] = "", -- Basic_Training_-_Bazooka --- ["Hightime"] = "", -- A_Classic_Fairytale:first_blood --- ["Hightower"] = "", -- --- ["Hill Guard"] = "", -- Bazooka_Battlefield --- ["Hi! Nice to meet you."] = "", -- A_Space_Adventure:ice01 --- ["--- Hint ---"] = "", -- Battalion --- ["Hint: Cinematics can be skipped with the [Precise] key."] = "", -- A_Classic_Fairytale:first_blood, A_Classic_Fairytale:shadow --- ["Hint: Drilling holes should solve everything."] = "", -- A_Classic_Fairytale:family --- ["Hint: Hold down [M] to review the mission texts."] = "", -- A_Classic_Fairytale:first_blood --- ["Hint: If this mission panel disappears, you can|see it again by hitting the Pause or Quit key."] = "", -- Basic_Training_-_Movement --- ["Hint: It might be a good idea to place a girder before starting to drill. Just saying."] = "", -- A_Classic_Fairytale:family --- ["Hint: It might be easier if you vary the angle only slightly."] = "", -- Basic_Training_-_Bazooka --- ["Hint: Just select the parachute, it opens automatically when you fall."] = "", -- A_Classic_Fairytale:first_blood --- ["Hint: Kills won't transfer a hog's pool to the killer's pool"] = "", -- Battalion --- ["Hint: Launch the bazooka horizontally at full power."] = "", -- Basic_Training_-_Bazooka --- ["Hint: Pause the game to review the mission texts."] = "", -- A_Classic_Fairytale:first_blood --- ["Hint: Select the blow torch, aim and press [Fire]. Press [Fire] again to stop."] = "", -- A_Classic_Fairytale:journey --- ["Hint: Select the low gravity and press [Fire]."] = "", -- A_Classic_Fairytale:journey --- ["Hint: Select the rope, [Up] or [Down] to aim, [Attack] to fire, directional keys to move."] = "", -- A_Classic_Fairytale:first_blood --- ["Hint: Select the Shoryuken and hit [Attack].|P.S.: You can use it mid-air."] = "", -- A_Classic_Fairytale:first_blood --- ["Hint: %s needs to get really close to the princess!"] = "", -- A_Classic_Fairytale:family --- ["Hint: The rope only bends around objects.|When it doesn't hit anything, it's always straight."] = "", -- Basic_Training_-_Rope --- ["Hint: To jump higher, wait a bit before you hit “High Jump” a second time."] = "", -- Basic_Training_-_Movement --- ["Hint: To place a girder, select it,|then use [Left] and [Right] to select angle and length,|then choose a location for the girder."] = "", -- A_Classic_Fairytale:shadow --- ["Hint: Use the quit key to see the team’s continent."] = "", -- Continental_supplies --- ["Hint: When you shorten the rope, you move faster!|And when you lengthen it, you move slower."] = "", -- Basic_Training_-_Rope --- ["Hint: you might want to stay out of sight and take all the crates...|"] = "", -- A_Classic_Fairytale:journey --- ["Hint: You might want to stay out of sight and take all the crates ..."] = "", -- A_Classic_Fairytale:journey --- ["His arms are so strong!"] = "", -- A_Classic_Fairytale:first_blood --- ["hits"] = "", -- Basic_Training_-_Bazooka --- ["Hit the “Switch Hedgehog” key until you have|selected Cappy, the hedgehog with the cap!"] = "", -- Basic_Training_-_Movement --- ["Hmm … it's going slower than expected."] = "", -- A_Classic_Fairytale:queen --- ["Hmmm...actually...I didn't either."] = "", -- A_Classic_Fairytale:enemy --- ["Hmmm, I’ll have to find some way of moving him off this anti-portal surface."] = "", -- portal --- ["Hmmm...it's a draw. How unfortunate!"] = "", -- A_Classic_Fairytale:enemy --- ["Hmmm...perhaps a little more time will help."] = "", -- A_Classic_Fairytale:first_blood - ["Hmmm..."] = "呃...", --- ["Hm ... Now I ran out of fuel."] = "", -- A_Space_Adventure:cosmos --- ["Hog 100"] = "", -- A_Space_Adventure:fruit03 --- ["Hog 1"] = "", -- A_Space_Adventure:fruit03 --- ["Hog 3x5"] = "", -- A_Space_Adventure:fruit03 --- ["Hog 7+7"] = "", -- A_Space_Adventure:fruit03 --- ["Hog D"] = "", -- A_Space_Adventure:fruit03 --- ["Hog decar"] = "", -- A_Space_Adventure:fruit03 --- ["Hog dertien"] = "", -- A_Space_Adventure:fruit03 --- ["Hog %d"] = "", -- SimpleMission --- ["Hog EOF"] = "", -- A_Space_Adventure:fruit03 --- ["Hogera is definitely the last planet I saved!"] = "", -- A_Space_Adventure:final --- ["Hogera is safe!"] = "", -- A_Space_Adventure:final --- ["Hog exi"] = "", -- A_Space_Adventure:fruit03 --- ["Hog Hephaestus"] = "", -- A_Space_Adventure:fruit03 --- ["Hog Identity Mode"] = "", -- HedgeEditor --- ["HOG IDENTITY MODE"] = "", -- HedgeEditor --- ["Hog III"] = "", -- A_Space_Adventure:fruit03 --- ["Hogminator"] = "", -- A_Classic_Fairytale:family --- ["Hog nueve"] = "", -- A_Space_Adventure:fruit03 --- ["Hog octo"] = "", -- A_Space_Adventure:fruit03 --- ["Hog onze"] = "", -- A_Space_Adventure:fruit03 --- ["Hog Saturn"] = "", -- A_Space_Adventure:fruit03 --- ["Hogs in sight!"] = "", -- Continental_supplies --- ["Hog Solo and GB"] = "", -- A_Space_Adventure:fruit02 --- ["Hog Solo"] = "", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01, A_Space_Adventure:death02, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:desert03, A_Space_Adventure:final, A_Space_Adventure:fruit01, A_Space_Adventure:fruit02, A_Space_Adventure:fruit03, A_Space_Adventure:ice01, A_Space_Adventure:ice02, A_Space_Adventure:moon01, A_Space_Adventure:moon02 --- ["- Hogs will be revived"] = "", -- Capture_the_Flag --- ["- Hogs will drop the flag when killed"] = "", -- Capture_the_Flag --- ["Hog two"] = "", -- A_Space_Adventure:fruit03 --- ["Hold [Attack] to attach the rope."] = "", -- Basic_Training_-_Rope --- ["Hold the Attack key pressed for more power."] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["Holy shit!"] = "", -- Mutant --- ["Homing Bee"] = "", -- Construction_Mode --- ["Honda"] = "", -- --- ["Honest Lee"] = "", -- A_Classic_Fairytale:enemy --- ["Hooks"] = "", -- --- ["Hooray! I actually did it! Hogera is safe!"] = "", -- A_Space_Adventure:final --- ["Hooray! I've found it, now I have to get back to Captain Lime!"] = "", -- A_Space_Adventure:fruit02 --- ["Hooray! You are a champion!"] = "", -- A_Space_Adventure:ice02 --- ["Hopeless case"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Hop on top of the next flower and advance to the left coast."] = "", -- A_Classic_Fairytale:journey --- ["Horns"] = "", -- --- ["Hostage Situation"] = "", -- A_Classic_Fairytale:family --- ["How can I ever repay you for saving my life?"] = "", -- A_Classic_Fairytale:journey --- ["How come in a village full of warriors, it's up to me to save it?"] = "", -- A_Classic_Fairytale:dragon --- ["How could you betray us?"] = "", -- A_Classic_Fairytale:queen --- ["How difficult would you like it to be?"] = "", -- A_Classic_Fairytale:first_blood --- ["HOW DO THEY KNOW WHERE WE ARE?"] = "", -- A_Classic_Fairytale:united --- ["However, if you fail to do so, she dies a most violent death, just like your friend! Muahahaha!"] = "", -- A_Classic_Fairytale:journey --- ["However, if you fail to do so, she dies a most violent death! Muahahaha!"] = "", -- A_Classic_Fairytale:journey --- ["However, my mates don't agree with me on letting you go..."] = "", -- A_Classic_Fairytale:dragon --- ["However, the army of %s is about to attack any moment now."] = "", -- A_Space_Adventure:fruit01 --- ["How to Rope"] = "", -- Basic_Training_-_Rope --- ["How would you like being discriminated against?"] = "", -- A_Classic_Fairytale:queen --- ["Huh?"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:queen - ["Hunter"] = "猎人", --Bazooka, Shotgun, SniperRifle --- ["I ain't gonna sit around no more!"] = "", -- A_Classic_Fairytale:queen --- ["I already said I'm sorry!"] = "", -- A_Classic_Fairytale:epil --- ["I always suspected him!"] = "", -- A_Classic_Fairytale:epil --- ["I am going to leave the kids play by themselves."] = "", -- A_Classic_Fairytale:queen --- ["I am not ready for this planet yet. I should visit it when I have found all the other device parts."] = "", -- A_Space_Adventure:cosmos --- ["I am sorry but I was looking for a device that may be hidden somewhere around here."] = "", -- A_Space_Adventure:fruit02 --- ["I believe there's more of them."] = "", -- A_Classic_Fairytale:backstab --- ["I cannot let you go any further! … burp …"] = "", -- A_Classic_Fairytale:queen --- ["I can see you have been training diligently."] = "", -- A_Classic_Fairytale:first_blood --- ["I can't believe how blind we were."] = "", -- A_Classic_Fairytale:epil --- ["I can't believe it worked!"] = "", -- A_Classic_Fairytale:shadow --- ["I can't believe this!"] = "", -- A_Classic_Fairytale:enemy --- ["I can't believe what I'm hearing!"] = "", -- A_Classic_Fairytale:backstab --- ["I can't let you go further because …"] = "", -- A_Classic_Fairytale:queen --- ["I can't wait any more, I have to save myself!"] = "", -- A_Classic_Fairytale:shadow --- ["Ice Jake"] = "", -- A_Space_Adventure:ice01 --- ["I could just teleport myself there..."] = "", -- A_Classic_Fairytale:family --- ["Icy Girder: [3]"] = "", -- HedgeEditor --- ["Icy Land: [3]"] = "", -- HedgeEditor --- ["Icy Land"] = "", -- HedgeEditor --- ["I'd better get going myself."] = "", -- A_Classic_Fairytale:journey --- ["Identity Thief"] = "", -- Mutant --- ["I didn't until about a month ago."] = "", -- A_Classic_Fairytale:enemy --- ["I don't care. It's worth a fortune! Good bye, you idiot!"] = "", -- A_Space_Adventure:fruit02 --- ["I don't know how you did that. But good work!|The next one should be easy as cake for you!"] = "", -- Basic_Training_-_Rope --- ["I don't know if I can forget what you've done!"] = "", -- A_Classic_Fairytale:epil --- ["I don't know who I can trust anymore."] = "", -- A_Classic_Fairytale:epil --- ["I don't like your tone! You're hurting me!"] = "", -- A_Classic_Fairytale:queen --- ["I feel something...a place! They will arrive near the circles!"] = "", -- A_Classic_Fairytale:backstab --- ["If only I had a way..."] = "", -- A_Classic_Fairytale:backstab --- ["If only I were given a chance to explain my being here..."] = "", -- A_Classic_Fairytale:first_blood --- ["If only one enemy is left, you'll get bonus ammo."] = "", -- A_Space_Adventure:death02 --- ["I forgot that she's the daughter of the chief, too..."] = "", -- A_Classic_Fairytale:backstab --- ["I found it! Hooray!"] = "", -- A_Space_Adventure:desert01 --- ["If some good old explosives were enough to save Hogera …"] = "", -- A_Space_Adventure:final --- ["If they try coming here, they can have a taste of my delicious knuckles!"] = "", -- A_Classic_Fairytale:united --- ["If you agree to provide the information we need, you will be spared!"] = "", -- A_Classic_Fairytale:shadow --- ["If you can get that crate fast enough, your beloved \"princess\" may go free."] = "", -- A_Classic_Fairytale:journey --- ["If you decide to help us, though, we will no longer need to find a new governor for the island."] = "", -- A_Classic_Fairytale:shadow --- ["If you don't want to slip away, you have to keep moving!"] = "", -- Basic_Training_-_Movement --- ["If you get stuck, use your Desert Eagle or restart the mission!"] = "", -- A_Classic_Fairytale:journey --- ["If you get stuck, use your Desert Eagle or restart the mission!|"] = "", -- A_Classic_Fairytale:journey --- ["If you help us you can keep the device if you find it but we'll keep everything else."] = "", -- A_Space_Adventure:fruit02 --- ["If you hurt an enemy, you'll get one third of the damage dealt."] = "", -- A_Space_Adventure:death02 --- ["If you injure a hedgehog you'll get 35% of the damage dealt."] = "", -- A_Space_Adventure:death02 --- ["If you just don’t care …"] = "", -- Continental_supplies --- ["If you kill a hedgehog with the respective weapon your health points will be set to 100."] = "", -- A_Space_Adventure:death02 --- ["If you kill an enemy, your health will be set to 100."] = "", -- A_Space_Adventure:death02 --- ["If you know what I mean..."] = "", -- A_Classic_Fairytale:shadow --- ["If you miss a shot while trying to|re-attach, your rope is gone, too!"] = "", -- Basic_Training_-_Rope --- ["If you say so..."] = "", -- A_Classic_Fairytale:shadow --- ["If you skip a turn then the turn time left will be added to your next turn."] = "", -- A_Space_Adventure:fruit03 --- ["If you wish to replay, there are other possible endings, too!"] = "", -- A_Classic_Fairytale:epil --- ["Igmund"] = "", -- User_Mission_-_Nobody_Laugh --- ["I grew sick of the oppression! I broke free!"] = "", -- A_Classic_Fairytale:queen --- ["I guess I can't go far without fuel!"] = "", -- A_Space_Adventure:cosmos --- ["I guess we lost him!"] = "", -- A_Space_Adventure:cosmos --- ["I guess you'll have to kill them."] = "", -- A_Classic_Fairytale:dragon --- ["I have come to make you an offering..."] = "", -- A_Classic_Fairytale:shadow --- ["I have heard that the local tribes say that many years ago some PAotH scientists were dumping their waste here."] = "", -- A_Space_Adventure:desert01 --- ["I have more important things to do!"] = "", -- A_Classic_Fairytale:queen --- ["I have no idea where that mole disappeared...Can you see it?"] = "", -- A_Classic_Fairytale:shadow --- ["I have only 3 hogs available and they are all cadets."] = "", -- A_Space_Adventure:fruit01 --- ["I have to follow that alien."] = "", -- A_Classic_Fairytale:backstab --- ["I have to get back to the village!"] = "", -- A_Classic_Fairytale:shadow --- ["I have to reach the surface as quickly as I can."] = "", -- A_Space_Adventure:desert02 --- ["I hope you are prepared for a small challenge, young one."] = "", -- A_Classic_Fairytale:first_blood --- ["I just don't want to sink to your level."] = "", -- A_Classic_Fairytale:backstab --- ["I just forgot all checkpoints of incomplete missions."] = "", -- A_Space_Adventure:cosmos --- ["I just found out that they have captured your princess!"] = "", -- A_Classic_Fairytale:family --- ["I just want the strange device you found!"] = "", -- A_Space_Adventure:ice01 --- ["I just wonder where Ramon and Spiky disappeared..."] = "", -- A_Classic_Fairytale:journey --- ["I know and I'm terribly sorry!"] = "", -- A_Classic_Fairytale:epil --- ["I know, my hero!"] = "", -- A_Classic_Fairytale:epil --- ["I know that your resources are low due to the battle but I'll send two of my best hogs to assist you."] = "", -- A_Space_Adventure:fruit02 --- ["I … like being with you, too."] = "", -- A_Classic_Fairytale:epil --- ["I'll get him!"] = "", -- A_Space_Adventure:cosmos --- ["I'll hold them off while you return to the village!"] = "", -- A_Classic_Fairytale:shadow --- ["I'll let you know whatever I know about him if you manage to catch me 3 times."] = "", -- A_Space_Adventure:moon02 --- ["I'll make good use of it."] = "", -- A_Space_Adventure:cosmos --- ["I'll protect you!"] = "", -- A_Classic_Fairytale:epil --- ["I love Dense Cloud now!"] = "", -- A_Classic_Fairytale:epil --- ["I love you."] = "", -- A_Classic_Fairytale:epil --- ["I'm afraid I can't let you proceed!"] = "", -- A_Classic_Fairytale:queen --- ["I'm afraid we cannot afford that."] = "", -- A_Classic_Fairytale:queen --- ["Imagine those targets are the wolves that killed your parents! Take your anger out on them!"] = "", -- A_Classic_Fairytale:first_blood --- ["I'm...alive? How? Why?"] = "", -- A_Classic_Fairytale:backstab --- ["I'm a ninja."] = "", -- A_Classic_Fairytale:dragon --- ["I marked the place of their arrival. You're welcome!"] = "", -- A_Classic_Fairytale:backstab --- ["I may lost this battle, but I haven't lost the war yet!"] = "", -- A_Space_Adventure:moon01 --- ["I'm certain that this is a misunderstanding, fellow hedgehogs!"] = "", -- A_Classic_Fairytale:first_blood --- ["I mean, none of you ceased to live."] = "", -- A_Classic_Fairytale:enemy --- ["I'm getting old for this!"] = "", -- A_Classic_Fairytale:family --- ["I'm getting thirsty..."] = "", -- A_Classic_Fairytale:family --- ["I'm glad this is over!"] = "", -- A_Classic_Fairytale:epil --- ["I'm here to help you rescue her."] = "", -- A_Classic_Fairytale:family --- ["I'm living a dream!"] = "", -- A_Classic_Fairytale:queen --- ["I'm not sure about that!"] = "", -- A_Classic_Fairytale:united --- ["IMPORTANT: To see the mission panel again, hold the mission panel key."] = "", -- Basic_Training_-_Movement --- ["IMPORTANT: To see the mission panel again, pause the game."] = "", -- Basic_Training_-_Movement --- ["Impressive...you are still dry as the corpse of a hawk after a week in the desert..."] = "", -- A_Classic_Fairytale:first_blood --- ["%i ms"] = "", -- Gravity --- ["I'm so glad this is finally over!"] = "", -- A_Space_Adventure:final --- ["I'm so scared!"] = "", -- A_Classic_Fairytale:united --- ["I'm still low on hogs. If you are not afraid I could use a set of extra hands."] = "", -- A_Space_Adventure:fruit02 --- ["I'm still with the aliens."] = "", -- A_Classic_Fairytale:queen --- ["I'm terribly sorry!"] = "", -- A_Classic_Fairytale:queen --- ["I'm the spy! I've been giving you out!"] = "", -- A_Classic_Fairytale:queen --- ["In am also entrusting you with some rope."] = "", -- A_Space_Adventure:cosmos --- ["In case you haven't noticed, I'm a woman, too!"] = "", -- A_Classic_Fairytale:queen --- ["Increase the dust storm damage by sacrificing|your invulnerable ammo."] = "", -- Continental_supplies --- ["Incredible..."] = "", -- A_Classic_Fairytale:shadow --- ["Indestructible Girder: [2]"] = "", -- HedgeEditor --- ["Indestructible Land: [2]"] = "", -- HedgeEditor --- ["Indestructible Land"] = "", -- HedgeEditor --- ["In each round, the worst hedgehog of the round is eliminated."] = "", -- TrophyRace --- ["I need to find the others!"] = "", -- A_Classic_Fairytale:backstab --- ["I need to get to the other side of this island, fast!"] = "", -- A_Classic_Fairytale:journey --- ["I need to move the tribe!"] = "", -- A_Classic_Fairytale:united --- ["I need to prevent their arrival!"] = "", -- A_Classic_Fairytale:backstab --- ["I need to warn the others."] = "", -- A_Classic_Fairytale:backstab --- ["In fact, you are the only one that's been acting strangely."] = "", -- A_Classic_Fairytale:backstab --- ["Initial health: %d"] = "", -- Continental_supplies --- ["Initiate escape wish!"] = "", -- A_Classic_Fairytale:queen --- ["In order to get to the other side, you need to get rid of the crates first."] = "", -- A_Classic_Fairytale:dragon --- ["Insanity!"] = "", -- Mutant --- ["Inside %d"] = "", -- WxW --- ["Inside"] = "", -- WxW --- ["Instructions"] = "", -- Basic_Training_-_Flying_Saucer - ["Instructor"] = "引导员", -- 01#Boot_Camp, User_Mission_-_Dangerous_Ducklings --- ["Insufficient Power"] = "", -- Construction_Mode --- ["Interesting idea, haha!"] = "", -- A_Classic_Fairytale:enemy --- ["Interesting! Last time you said you killed a cannibal!"] = "", -- A_Classic_Fairytale:backstab --- ["In the Ice Planet Flying Saucer Stadium ..."] = "", -- A_Space_Adventure:ice02 --- ["In the meantime, take these and return to your \"friend\"!"] = "", -- A_Classic_Fairytale:shadow --- ["In the stadium, where the best pilots compete ..."] = "", -- A_Space_Adventure:ice02 --- ["In this accident, Professor Hogevil lost all his spines on his head!"] = "", -- A_Space_Adventure:moon02 --- ["In this mission you get %d%% fuel."] = "", -- User_Mission_-_Diver --- ["In this mission you have infinite time."] = "", -- portal --- ["Invalid Placement"] = "", -- Construction_Mode --- ["Invasion"] = "", -- A_Classic_Fairytale:united --- ["In your best (and only) flight you took out %d crates with one RC plane!"] = "", -- User_Mission_-_RCPlane_Challenge --- ["In your best flight you took out %d crates with one RC plane."] = "", -- User_Mission_-_RCPlane_Challenge --- ["I regret to end your little odyssey."] = "", -- A_Classic_Fairytale:queen --- ["I saw it with my own eyes!"] = "", -- A_Classic_Fairytale:shadow --- ["I see..."] = "", -- A_Classic_Fairytale:shadow --- ["I see you already took care of your enemies."] = "", -- A_Classic_Fairytale:dragon --- ["I see you have already taken the leap of faith."] = "", -- A_Classic_Fairytale:first_blood --- ["I see you would like his punishment to be more...personal..."] = "", -- A_Classic_Fairytale:first_blood --- ["I sense another wave of cannibals heading my way!"] = "", -- A_Classic_Fairytale:backstab --- ["I sense another wave of cannibals heading our way!"] = "", -- A_Classic_Fairytale:backstab --- ["%i s"] = "", -- Gravity --- ["I should get myself a portal device, maybe this crate has one."] = "", -- portal --- ["I should go now, goodbye!"] = "", -- A_Space_Adventure:moon02 --- ["I shouldn't have drunk that last pint."] = "", -- A_Classic_Fairytale:dragon --- ["Is this place in my head?"] = "", -- A_Classic_Fairytale:dragon --- ["I still can't believe he sold us out like that."] = "", -- A_Classic_Fairytale:epil --- ["I still can't believe you forgave her!"] = "", -- A_Classic_Fairytale:epil --- ["I still have to get rid of the crates."] = "", -- A_Classic_Fairytale:dragon --- ["Itami"] = "", -- --- ["It doesn't matter. I won't let that alien hurt my daughter!"] = "", -- A_Classic_Fairytale:dragon --- ["I think I love you!"] = "", -- A_Classic_Fairytale:epil --- ["I think we are safe here."] = "", -- A_Classic_Fairytale:backstab --- ["I thought their shaman died when he tried our medicine!"] = "", -- A_Classic_Fairytale:shadow --- ["It is called 'Hogs of Steel'."] = "", -- A_Classic_Fairytale:enemy --- ["It is time to practice your fighting skills."] = "", -- A_Classic_Fairytale:first_blood --- ["It must be a childhood trauma..."] = "", -- A_Classic_Fairytale:family --- ["It must be the aliens!"] = "", -- A_Classic_Fairytale:backstab --- ["It must be the aliens' deed."] = "", -- A_Classic_Fairytale:backstab --- ["It must be the cyborgs again!"] = "", -- A_Classic_Fairytale:enemy --- ["It needs some practice, but you have infinite lives."] = "", -- Basic_Training_-_Rope --- ["I told you, I just found them."] = "", -- A_Classic_Fairytale:backstab --- ["It only works in teleportation nodes of your own clan."] = "", -- Construction_Mode --- ["It's a good thing SUDDEN DEATH is 99 turns away..."] = "", --- ["It's all about the right carrots, you know."] = "", -- A_Classic_Fairytale:epil --- ["It's always up to women to clear up the mess men created!"] = "", -- A_Classic_Fairytale:dragon --- ["It's amazing how quickly our lives can change."] = "", -- A_Classic_Fairytale:epil --- ["It's an ancient ritual of theirs."] = "", -- A_Classic_Fairytale:queen --- ["IT'S A SERIOUS MEDICAL CONDITION!"] = "", -- A_Classic_Fairytale:queen --- ["It's a shame, I forgot how to do that!"] = "", -- A_Classic_Fairytale:family --- ["It's a shame, really!"] = "", -- A_Classic_Fairytale:queen --- ["It seems that Professor Hogevil has prepared for your arrival!"] = "", -- A_Space_Adventure:moon01 --- ["It's empty!"] = "", -- Battalion --- ["It's impossible to communicate with the spirits without a shaman."] = "", -- A_Classic_Fairytale:shadow --- ["It's not that easy, so listen carefully:"] = "", -- Basic_Training_-_Flying_Saucer --- ["It's over..."] = "", -- A_Classic_Fairytale:shadow --- ["It's precious to me!"] = "", -- A_Classic_Fairytale:queen --- ["It's time you learned that your actions have consequences!"] = "", -- A_Classic_Fairytale:journey --- ["It's worth more than wood!"] = "", -- A_Classic_Fairytale:enemy --- ["It's your fault you're there!"] = "", -- A_Classic_Fairytale:epil --- ["It wants our brains!"] = "", -- A_Classic_Fairytale:shadow --- ["It was all a trick?!"] = "", -- A_Classic_Fairytale:queen --- ["It was all just bad luck!"] = "", -- ClimbHome --- ["It was completely useless!"] = "", -- A_Space_Adventure:final --- ["It was fun to watch."] = "", -- A_Classic_Fairytale:queen --- ["It was fun to watch, though."] = "", -- A_Classic_Fairytale:queen --- ["It was not a dream, unwise one!"] = "", -- A_Classic_Fairytale:backstab --- ["It wasn't her fault!"] = "", -- A_Classic_Fairytale:epil --- ["It would be wiser to steal the space ship while the PAotH guards are taking a brake!"] = "", -- A_Space_Adventure:cosmos --- ["Ivan"] = "", -- --- ["I've made it! Yeah!"] = "", -- A_Space_Adventure:moon01 --- ["I've seen this before. They just appear out of thin air."] = "", -- A_Classic_Fairytale:united --- ["I've thought that the best way to get the device is to let you collect most of the parts for me!"] = "", -- A_Space_Adventure:death01 --- ["I want to play a game..."] = "", -- A_Classic_Fairytale:journey --- ["I want to see how it handles this!"] = "", -- A_Classic_Fairytale:backstab --- ["I was heading home, you see!"] = "", -- A_Classic_Fairytale:queen --- ["I was so scared."] = "", -- A_Classic_Fairytale:epil --- ["I was told that as the leader of the king's guard, no one knows this world better than you!"] = "", -- A_Space_Adventure:fruit01 --- ["I will never hand you the parts!"] = "", -- A_Space_Adventure:death01 --- ["I wish to help you, %s!"] = "", -- A_Classic_Fairytale:dragon --- ["I wonder where Dense Cloud is..."] = "", -- A_Classic_Fairytale:journey, A_Classic_Fairytale:shadow --- ["I wonder why I'm so angry all the time..."] = "", -- A_Classic_Fairytale:family --- ["I won't let you kill her!"] = "", -- A_Classic_Fairytale:journey --- ["I won't let you kill the tribe!"] = "", -- A_Classic_Fairytale:queen --- ["I would gladly help you if we won this battle but under these circumstances I'll only help you if you fight for our side."] = "", -- A_Space_Adventure:fruit01 --- ["Jack"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Jason"] = "", -- --- ["Jeremiah"] = "", -- A_Classic_Fairytale:dragon --- ["Jetpack"] = "", -- Big_Armory --- ["Jigglypuff"] = "", -- --- ["Jim Morgan"] = "", -- --- ["Jimmy"] = "", -- --- ["Jingo"] = "", -- --- ["Joe"] = "", -- A_Space_Adventure:moon01 --- ["John"] = "", -- A_Classic_Fairytale:journey --- ["John Snow"] = "", -- A_Space_Adventure:ice01 --- ["Jolly Roger"] = "", -- --- ["Jones"] = "", -- --- ["Judas"] = "", -- A_Classic_Fairytale:backstab --- ["Juicy"] = "", -- --- ["Jumping"] = "", -- Basic_Training_-_Movement --- ["Jumping is disabled"] = "", --- ["Just kidding, none of you have died!"] = "", -- A_Classic_Fairytale:enemy --- ["Just look at Leaks, may he rest in peace!"] = "", -- A_Classic_Fairytale:queen --- ["Just on a walk."] = "", -- A_Classic_Fairytale:united --- ["Just wait till I get my hands on that trauma! ARGH!"] = "", -- A_Classic_Fairytale:family --- ["Kaboom!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Kaboom! Hahahaha! Take this, stupid meteorite!"] = "", -- A_Space_Adventure:final --- ["Kamikaze"] = "", -- Construction_Mode --- ["Kamikaze Expert! +15 points!"] = "", -- Space_Invasion --- ["Keep it up!"] = "", --- ["Ken"] = "", -- --- ["Kenshi"] = "", -- --- ["Kerguelen"] = "", -- Continental_supplies --- ["key."] = "", -- Continental_supplies --- ["Kill all enemy hedgehogs in a single turn."] = "", -- Big_Armory --- ["Kill him or skip your turn."] = "", -- A_Classic_Fairytale:backstab --- ["Killing spree!"] = "", --- ["Killing the specialists"] = "", -- A_Space_Adventure:death02 --- ["KILL IT!"] = "", -- A_Classic_Fairytale:first_blood --- ["Kills: %d"] = "", -- Space_Invasion --- ["Kill the aliens!"] = "", -- A_Classic_Fairytale:dragon --- ["Kill the cannibal!"] = "", -- A_Classic_Fairytale:first_blood --- ["Kill The Leader"] = "", -- WxW --- ["Kill The Leader: You must also hit the team with the most health."] = "", -- WxW --- ["Kill the traitor, %s, or spare his life!"] = "", -- A_Classic_Fairytale:backstab --- ["--- King ---"] = "", -- Battalion --- ["King"] = "", -- Battalion --- ["--- King Mode ---"] = "", -- Battalion --- ["Knight"] = "", -- Battalion --- ["Knives"] = "", -- --- ["Knockball"] = "", -- Knockball --- ["Knockball weapon"] = "", -- Knockball --- ["Knock off the enemies from the left-most place of the map!"] = "", -- A_Space_Adventure:fruit01 --- ["koda"] = "", -- --- ["Kostya"] = "", -- --- ["Lady Mango"] = "", -- A_Space_Adventure:fruit01, A_Space_Adventure:fruit02 --- ["LandFlag Modification Mode"] = "", -- HedgeEditor --- ["Land mines explode instantly."] = "", -- User_Mission_-_Teamwork_2 --- ["Lassard"] = "", -- --- ["Last Resort: Having less than 25% base health gives kamikaze"] = "", -- Battalion --- ["Last Target!"] = "", --- ["Launch a bouncy ball which explodes into a crate."] = "", -- Continental_supplies --- ["Launch some bazookas to destroy the targets!"] = "", -- Basic_Training_-_Bazooka --- ["Leader"] = "", -- A_Classic_Fairytale:enemy --- ["Leaderbot"] = "", -- A_Classic_Fairytale:queen --- ["Lead your allies to battle and eliminate all the enemies!"] = "", -- A_Space_Adventure:fruit01 --- ["Leaks A Lot"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow, A_Classic_Fairytale:united --- ["Leaks A Lot, depressed for killing his loved one, failed to save the village..."] = "", -- A_Classic_Fairytale:journey --- ["Leaks A Lot gave his life for his tribe! He should have survived!"] = "", -- A_Classic_Fairytale:first_blood --- ["Leaks A Lot must survive!"] = "", -- A_Classic_Fairytale:journey --- ["Leap of Faith"] = "", -- Basic_Training_-_Movement --- ["Led Heart"] = "", -- A_Classic_Fairytale:queen --- ["Lee"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Left and right"] = "", -- WxW --- ["Left, right and roof"] = "", -- WxW --- ["[Left], [Right]: Change between identities."] = "", -- HedgeEditor --- ["[Left], [Right]: Change health value."] = "", -- HedgeEditor --- ["Left/right: Choose crate contents"] = "", -- Construction_Mode --- ["Left/right: Choose structure type"] = "", -- Construction_Mode --- ["Left/right: Choose structure type|Cursor: Build structure"] = "", -- Construction_Mode --- ["Legs"] = "", -- --- ["Less tools, more fun"] = "", -- Battalion --- ["Lestat"] = "", -- portal --- ["Let a continent provide your weapons!"] = "", -- Continental_supplies --- ["Let me test your skills a little, will you?"] = "", -- A_Classic_Fairytale:journey --- ["Let's get started!"] = "", -- Basic_Training_-_Bazooka --- ["Let's go!"] = "", -- A_Space_Adventure:moon02 --- ["Let's go home!"] = "", -- A_Classic_Fairytale:journey --- ["Let's go, %s!"] = "", -- WxW --- ["Let's head back to the village!"] = "", -- A_Classic_Fairytale:shadow --- ["Let's see what your comrade does now!"] = "", -- A_Classic_Fairytale:journey --- ["Let's show those cannibals what we're made of!"] = "", -- A_Classic_Fairytale:backstab --- ["Let them have a taste of my fury!"] = "", -- A_Classic_Fairytale:backstab --- ["Let us help, too!"] = "", -- A_Classic_Fairytale:backstab --- ["Level 1 clear!"] = "", -- A_Space_Adventure:desert03 --- ["Level 2 clear!"] = "", -- A_Space_Adventure:desert03 --- ["Level Data Saved!"] = "", -- HedgeEditor --- ["Lightbender"] = "", -- --- ["Light Cannfantry"] = "", -- A_Classic_Fairytale:united --- ["Limited Ammo"] = "", -- Basic_Training_-_Bazooka --- ["Listen carefully! The bandit leader, Thanta, has recently found a very strange device."] = "", -- A_Space_Adventure:ice01 --- ["Listen up, maggot!"] = "", -- User_Mission_-_Dangerous_Ducklings - ["Listen up, maggot!!"] = "听好,小子!!", --- ["Little did they know that this hunt will mark them forever..."] = "", -- A_Classic_Fairytale:shadow --- ["Little Obstacle Course"] = "", -- Basic_Training_-_Rope --- ["Lively Lifeguard"] = "", --- ["Lonely Cries"] = "", -- Continental_supplies --- ["Lonely Cries: [Rise the water if no hog is in the circle and deal 6 damage to all enemy hogs.]"] = "", -- Continental_supplies --- ["Long Jump: [Enter]"] = "", -- Basic_Training_-_Movement --- ["Long Jump: Tap the [Curvy Arrow] button for long"] = "", -- Basic_Training_-_Movement, A_Classic_Fairytale:first_blood --- ["Long Live The Queen"] = "", -- A_Classic_Fairytale:queen --- ["Look around: [Mouse movement]"] = "", -- Basic_Training_-_Movement --- ["Look around: [Tap or swipe on the screen]"] = "", -- Basic_Training_-_Movement --- ["Look, boss! There is the target!"] = "", -- A_Space_Adventure:moon01 --- ["Look, I had no choice!"] = "", -- A_Classic_Fairytale:backstab --- ["Look out! There's more of them!"] = "", -- A_Classic_Fairytale:backstab --- ["Look out! We're surrounded by cannibals!"] = "", -- A_Classic_Fairytale:enemy --- ["Looks like the whole world is falling apart!"] = "", -- A_Classic_Fairytale:enemy --- ["Look to the left and do a backwards jump towards the mushroom."] = "", -- A_Classic_Fairytale:first_blood --- ["Loon"] = "", -- The_Specialists --- ["Loopy"] = "", -- --- ["Losing Condition: Destroy"] = "", -- HedgeEditor --- ["Low Gravity: Gravity is %i%%"] = "", -- Gravity --- ["Loyal Highlander: Eliminate enemy hogs to take their weapons"] = "", -- Highlander --- ["Lt. Luke"] = "", -- --- ["Lucifer"] = "", -- portal --- ["Luck: %d%% (modifier for crates)"] = "", -- Battalion --- ["Luckily, I've managed to snatch some of them."] = "", -- A_Classic_Fairytale:united --- ["Ludicrous kill!"] = "", -- Mutant --- ["Lugia"] = "", -- --- ["Luigi"] = "", -- --- ["Made it!"] = "", -- ClimbHome --- ["Mahoney"] = "", -- --- ["Make fun of me when I fart …"] = "", -- A_Classic_Fairytale:queen --- ["Manual: https://hedgewars.org/hedgeeditor"] = "", -- HedgeEditor --- ["Many long forgotten things can be found in the same tunnels that we are about to explore!"] = "", -- A_Space_Adventure:fruit02 --- ["Many meters below the surface ..."] = "", -- A_Space_Adventure:desert02 --- ["Mario"] = "", -- --- ["Mark gears for win/lose conditions"] = "", -- HedgeEditor --- ["Mark/unmark gear: [Left Click]"] = "", -- HedgeEditor --- ["- Massive weapon bonus on first turn"] = "", -- Continental_supplies --- ["Max Citrus"] = "", -- A_Space_Adventure:fruit01 --- ["Maybe you should try an easier map next time."] = "", -- Racer --- ["Maybe you should try an easier TechRacer map."] = "", -- TechRacer --- ["Maybe you should try easier waypoints next time."] = "", -- Racer --- ["May the spirits aid you in all your quests!"] = "", -- A_Classic_Fairytale:backstab --- ["Meals"] = "", -- --- ["Medic"] = "", -- Battalion --- ["Medicine"] = "", -- Continental_supplies --- ["Medicine: [Fire some exploding medicine that will heal all hogs effected by the explosion]"] = "", -- Continental_supplies --- ["MEDIUM"] = "", -- Continental_supplies --- ["Mega kill!"] = "", -- Mutant --- ["Meiwes"] = "", -- A_Classic_Fairytale:backstab --- ["mikade"] = "", -- --- ["Mindy"] = "", -- A_Classic_Fairytale:united --- ["Mine Deployer"] = "", --- ["Mine Placement Mode"] = "", -- Construction_Mode --- ["MINE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Mines explode after %d s."] = "", -- Mutant --- ["Mines time: 0s-5s"] = "", -- SimpleMission --- ["Mines time: 0 seconds"] = "", -- portal, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork, User_Mission_-_The_Great_Escape, A_Space_Adventure:desert01, A_Space_Adventure:final, A_Space_Adventure:fruit02, A_Space_Adventure:ice01 --- ["Mines time: 1.5 seconds"] = "", -- A_Space_Adventure:death01 --- ["Mines time: %.1fs"] = "", -- SimpleMission --- ["Mines time: 1 second"] = "", -- User_Mission_-_Diver, User_Mission_-_Newton_and_the_Hammock, A_Space_Adventure:desert02 --- ["Mines time: %.2fs"] = "", -- SimpleMission --- ["Mines time: 3 seconds"] = "", -- A_Classic_Fairytale:journey --- ["Mines time: 5 seconds"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:journey --- ["Mines time: %ds"] = "", -- SimpleMission --- ["Mine Strike"] = "", -- Construction_Mode --- ["Minion"] = "", -- A_Space_Adventure:moon01 --- ["Minions"] = "", -- A_Space_Adventure:moon01 --- ["Mission failed!"] = "", -- Big_Armory --- ["Mission failure in %d s"] = "", -- Big_Armory --- ["Mission"] = "", -- HedgeEditor --- ["Mission lost!"] = "", -- Basic_Training_-_Grenade --- ["Mission Panel"] = "", -- Basic_Training_-_Movement --- ["Mission panel: [M]"] = "", -- Basic_Training_-_Movement --- ["Mission succeeded!"] = "", -- portal, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, User_Mission_-_Diver, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork_2, User_Mission_-_Teamwork, SimpleMission, HedgeEditor --- ["Mission won!"] = "", -- Basic_Training_-_Grenade --- ["Mister Pear"] = "", -- A_Space_Adventure:fruit01, A_Space_Adventure:fruit02 --- ["Mixed %d"] = "", -- WxW --- ["Mixed"] = "", -- WxW --- ["Modes: Activate “highland”, “king” or “points” mode by putting mode=|into the script parameter"] = "", -- Battalion --- ["Modifiers: Unlimited ammo, per-hog ammo"] = "", -- Battalion --- ["Modifiers: Unlimited ammo, shared clan ammo"] = "", -- Battalion --- ["Modifiers: Unlimited attacks, per-hog ammo"] = "", -- Battalion --- ["Modifiers: Unlimited attacks, shared clan ammo"] = "", -- Battalion --- ["Modify Sprite under Cursor: [Left Click]"] = "", -- HedgeEditor --- ["Molly"] = "", -- --- ["Molotov"] = "", -- Continental_supplies --- ["Monster kill!"] = "", -- Mutant --- ["Monsters"] = "", -- --- ["Mooney"] = "", -- --- ["Morris"] = "", -- --- ["Most mines are not active."] = "", -- A_Space_Adventure:desert02 --- ["Most of the destructible terrain in marked with blue color"] = "", -- A_Space_Adventure:desert01 --- ["Most of the destructible terrain is marked with dashed lines."] = "", -- A_Space_Adventure:desert01 --- ["Most of the time you'll be able to use the freezer only."] = "", -- A_Space_Adventure:ice01 --- ["Movement: [Up], [Down], [Left], [Right]"] = "", --- ["Mr Mango"] = "", -- A_Space_Adventure:fruit01 --- ["Mudkip"] = "", -- --- ["Multi-shot! +15 points!"] = "", -- Space_Invasion --- ["Multi-Use: You can take and use the same ammo type multiple times in a turn"] = "", -- Highlander --- ["Muriel"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Muscle Dissolver"] = "", -- A_Classic_Fairytale:shadow --- ["Mushroom Kingdom"] = "", -- --- ["Mutant"] = "", -- Mutant --- ["My First Bazooka"] = "", -- Basic_Training_-_Bazooka --- ["My flying saucer stopped working!"] = "", -- A_Space_Adventure:ice01 --- ["Nade Boy"] = "", -- Basic_Training_-_Grenade --- ["Nah, probably everyone was just stupid."] = "", -- A_Space_Adventure:final --- ["Name"] = "", -- A_Classic_Fairytale:queen --- ["Nancy Screw"] = "", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:queen --- ["Napalm"] = "", -- Construction_Mode --- ["Napalm Rocket"] = "", -- Continental_supplies --- ["Napalm rocket: [Fire a bomb with napalm!]"] = "", -- Continental_supplies --- ["Naranja Jed"] = "", -- A_Space_Adventure:fruit01 --- ["Naughty Ninja"] = "", -- User_Mission_-_Dangerous_Ducklings --- ["Near a PAotH base on the moon ..."] = "", -- A_Space_Adventure:moon01 --- ["Near Secret Base 17 of PAotH in the rural Hogland ..."] = "", -- A_Space_Adventure:cosmos --- ["nemo"] = "", -- --- ["Neutralize your enemies and be careful!"] = "", -- A_Space_Adventure:moon01 --- ["New barrels per turn: %d"] = "", -- Tumbler --- ["New clan record: %.1fs"] = "", -- Racer, TechRacer - ["NEW fastest lap: "] = "新记录", --- ["New mines per turn: %d"] = "", -- Tumbler --- ["New race record: %.1fs"] = "", -- Racer, TechRacer --- ["Newton and the Hammock"] = "", -- User_Mission_-_Newton_and_the_Hammock --- ["Next target is ready!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Next time you play \"Searching in the dust\" you'll have an RC plane available."] = "", -- A_Space_Adventure:desert03 --- ["Nice!"] = "", -- A_Space_Adventure:cosmos --- ["Nicely done, meatbags!"] = "", -- A_Classic_Fairytale:enemy --- ["Nice! Now hurry and get down! You have to rescue my friends!"] = "", -- A_Space_Adventure:moon01 --- ["Nice, then I should get the part as soon as possible!"] = "", -- A_Space_Adventure:ice01 --- ["Nice work!"] = "", -- A_Classic_Fairytale:enemy --- ["Nice work, meatbags!"] = "", -- A_Classic_Fairytale:queen --- ["Nice work, %s!"] = "", -- A_Classic_Fairytale:dragon --- ["Nilarian"] = "", -- A_Classic_Fairytale:queen --- ["Ninja"] = "", -- Battalion, HedgeEditor, The_Specialists --- ["Ninpo"] = "", -- --- ["Nobody Laugh"] = "", -- User_Mission_-_Nobody_Laugh --- ["Nobody managed to finish the race. What a shame!"] = "", -- Racer, TechRacer --- ["Nobody takes walks every day!"] = "", -- A_Classic_Fairytale:epil --- ["No continent selected"] = "", -- Continental_supplies --- ["No, I am afraid I had to travel light."] = "", -- A_Space_Adventure:moon01 --- ["No, I came back to help you out..."] = "", -- A_Classic_Fairytale:shadow --- ["No...I wonder where they disappeared?!"] = "", -- A_Classic_Fairytale:journey --- ["Nom-Nom"] = "", -- A_Classic_Fairytale:journey --- ["NomNom"] = "", -- A_Classic_Fairytale:united --- ["No Multi-Use: Once you used an ammo, you can’t take it again in this turn"] = "", -- Highlander --- ["Noo, Thanta has to stay alive!"] = "", -- A_Space_Adventure:ice01 --- ["Nope. It was one fast mole, that's for sure."] = "", -- A_Classic_Fairytale:shadow --- ["No! Please, help me!"] = "", -- A_Classic_Fairytale:journey --- ["No problem, Captain!"] = "", -- A_Space_Adventure:fruit01 --- ["No problem, I would do anything for H!"] = "", -- A_Space_Adventure:desert01 --- ["No radar pings left!"] = "", -- Space_Invasion --- ["NORMAL"] = "", -- Continental_supplies --- ["Normal Girder: [1]"] = "", -- HedgeEditor --- ["Normal Land: [1]"] = "", -- HedgeEditor --- ["Normal Land"] = "", -- HedgeEditor --- ["Normally, the mission panel disappears after a few seconds."] = "", -- Basic_Training_-_Movement --- ["Normal Rubber: [1]"] = "", -- HedgeEditor --- ["North America"] = "", -- Continental_supplies --- ["Not being able to fight or hunt."] = "", -- A_Classic_Fairytale:queen --- ["Note: Some weapons have a second option (See continent information). Find and use them with the \""] = "", -- Continental_supplies --- ["Note: Some weapons have a second option (See continent information). Find and use them with the \"%s\" key."] = "", -- Continental_supplies --- ["Note: This basic training assumes default controls."] = "", -- Basic_Training_-_Movement --- ["Note: Walking is disabled in this mission."] = "", -- Basic_Training_-_Grenade --- ["Note: We only give you grenades if you stay in your flying saucer."] = "", -- Basic_Training_-_Flying_Saucer --- ["Nothing of interest has happened."] = "", -- Space_Invasion --- ["Not now, Fiery Water!"] = "", -- A_Classic_Fairytale:backstab - ["Not So Friendly Match"] = "非友善对抗", -- Basketball, Knockball --- ["Not you again! My head still hurts from last time!"] = "", -- A_Classic_Fairytale:shadow --- ["No waypoint to be removed!"] = "", -- Racer --- ["Now collect the 2 crates to the far left and right."] = "", -- Basic_Training_-_Flying_Saucer --- ["Now collect the next crate!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Now dive just one more time and collect the next crate."] = "", -- Basic_Training_-_Flying_Saucer --- ["No, we made sure of that!"] = "", -- A_Classic_Fairytale:united --- ["Now find the next target! |Tip: Normally you lose health by falling down, so be careful!"] = "", -- Basic_Training_-_Rope --- ["Now for the supreme discipline of saucer flying, the underwater attack."] = "", -- Basic_Training_-_Flying_Saucer --- ["Now go and don't waste more of my time, you coward!"] = "", -- A_Space_Adventure:fruit01 --- ["Now go and play the menu mission to complete the campaign."] = "", -- A_Space_Adventure:death01 --- ["Now go to the next crate."] = "", -- Basic_Training_-_Movement --- ["No! What have I done?! What have YOU done?!"] = "", -- A_Classic_Fairytale:journey --- ["No. Where did he come from?"] = "", -- A_Classic_Fairytale:shadow --- ["Now how do I get on the other side?!"] = "", -- A_Classic_Fairytale:dragon --- ["Now I have to climb these trees"] = "", -- A_Space_Adventure:cosmos --- ["No Wind Influcence"] = "", -- Basic_Training_-_Grenade --- ["No Wind Influence"] = "", -- Basic_Training_-_Grenade --- ["Now let's try to drop weapons while flying!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Now listen carefully! Below us there are tunnels that have been created naturally over the years"] = "", -- A_Space_Adventure:desert01 --- ["Now try to get out of this bounce house|and take the next crate."] = "", -- Basic_Training_-_Movement --- ["Now use it and go to the moon PAotH station to get more fuel!"] = "", -- A_Space_Adventure:cosmos --- ["Now you have the chance to try and claim the place that you deserve among the best."] = "", -- A_Space_Adventure:ice02 --- ["No. You and the rest of the tribe are safer there!"] = "", -- A_Classic_Fairytale:backstab --- ["Objective completed! Now land safely."] = "", -- Basic_Training_-_Flying_Saucer --- ["Objectives"] = "", -- A_Space_Adventure:ice01 --- ["Object Placer"] = "", -- Construction_Mode --- ["Obliterate them!|Hint: You might want to take cover..."] = "", -- A_Classic_Fairytale:shadow --- ["Obstacle"] = "", -- Basic_Training_-_Rope --- ["Obstacle course"] = "", -- A_Classic_Fairytale:dragon --- ["Of course, but you're … special."] = "", -- A_Classic_Fairytale:epil --- ["Of course I am!"] = "", -- A_Classic_Fairytale:queen --- ["Of course I have to save her. What did I expect?!"] = "", -- A_Classic_Fairytale:family --- ["Of course! It's all obvious now!"] = "", -- A_Classic_Fairytale:epil --- ["Of course, I will observe the battle and intervene if necessary."] = "", -- A_Space_Adventure:fruit01 --- ["OH, COME ON!"] = "", -- A_Classic_Fairytale:journey --- ["Oh man! Learn how to fly!"] = "", -- A_Space_Adventure:ice02 --- ["Oh, my!"] = "", -- A_Classic_Fairytale:first_blood --- ["Oh, my! I forgot something!"] = "", -- A_Classic_Fairytale:queen --- ["Oh, my! This is even more entertaining than I've expected!"] = "", -- A_Classic_Fairytale:backstab --- ["Oh no, not %s!"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united --- ["Oh no, the companions have betrayed %s and stole the anti-gravity device part!"] = "", -- A_Space_Adventure:fruit02 --- ["Oh no! You have died. Try again!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Oh! Please spare me. You can take all my treasures!"] = "", -- A_Space_Adventure:ice01 --- ["Oh, silly me! I forgot that I'm the shaman."] = "", -- A_Classic_Fairytale:backstab --- ["Oh, that. We were just having fun!"] = "", -- A_Classic_Fairytale:queen --- ["Oh yeah! You sure know how to rope!"] = "", -- Basic_Training_-_Rope --- ["Oh yes! I got the device part! Now it belongs to me alone."] = "", -- A_Space_Adventure:fruit02 --- ["Okay, I'll be extra careful!"] = "", -- A_Space_Adventure:desert01 --- ["Okay, now destroy the target|using the baseball bat."] = "", -- Basic_Training_-_Rope --- ["Okay then!"] = "", -- A_Space_Adventure:fruit02 --- ["Okay, then you have to go and take some of the weapons we have hidden in case of an emergency!"] = "", -- A_Space_Adventure:moon01 --- ["Old One Eye"] = "", -- --- ["Oleg"] = "", -- --- ["Olive"] = "", -- A_Classic_Fairytale:united --- ["Omnivore"] = "", -- A_Classic_Fairytale:first_blood --- ["Once upon a time, on an island with great natural resources, lived two tribes in heated conflict..."] = "", -- A_Classic_Fairytale:first_blood --- ["Once you set off the proximity trigger, Mr. Mine is not your friend"] = "", -- ClimbHome --- ["One does not simply rope to the moon!"] = "", -- A_Space_Adventure:cosmos --- ["One flower: Incomplete side missions"] = "", -- A_Space_Adventure:cosmos --- ["One shall not judge one by one's appearance!"] = "", -- A_Classic_Fairytale:epil --- ["One tribe was peaceful, spending their time hunting and training, enjoying the small pleasures of life..."] = "", -- A_Classic_Fairytale:first_blood --- ["Oneye"] = "", -- portal --- ["Only one hog per team allowed! Excess hogs will be removed"] = "", -- Mutant --- ["Only one hog per team allowed! Excess hogs will be removed."] = "", -- Mutant --- ["Only %s can be trusted with the crate."] = "", -- A_Space_Adventure:fruit02 --- ["Only the best pilots can master the following stunts."] = "", -- Basic_Training_-_Flying_Saucer --- ["Only two clans allowed! Excess hedgehogs will be removed."] = "", -- CTF_Blizzard --- ["On the Ice Planet, where ice rules ..."] = "", -- A_Space_Adventure:ice01 --- ["On the other side of the moon ..."] = "", -- A_Space_Adventure:moon02 --- ["On the Planet of Sand, you have to double check your moves ..."] = "", -- A_Space_Adventure:desert01 --- ["On this map you get %d%% fuel."] = "", -- TechRacer --- ["On this map you get infinite fuel."] = "", -- TechRacer --- ["Oops...I dropped them."] = "", -- A_Classic_Fairytale:united --- ["Oops, I've been spotted and I have no weapons! I am doomed!"] = "", -- A_Space_Adventure:moon01 --- ["Oops! You have selected the wrong hedgehog! Just try again."] = "", -- Basic_Training_-_Movement --- ["Open ammo menu: [Right click]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope --- ["Open ammo menu: Tap the [Suitcase]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope --- ["Open that crate and we will continue!"] = "", -- A_Classic_Fairytale:first_blood - ["Opposing Team: "] = "对方队伍", --- ["Orange"] = "", -- --- ["Orlando Boom!"] = "", -- A_Classic_Fairytale:queen --- ["Or let the next player place waypoints|if less than 2 waypoints have been placed."] = "", -- Racer --- ["Or maybe this was all part of an evil plan, so evil that even Prof. Hogevil can't think of it!"] = "", -- A_Space_Adventure:final --- ["Oscillating Gravity: Gravity periodically changes within a range from %i%% to %i%% with a period of %s"] = "", -- Gravity --- ["Other kills don't give you points."] = "", -- Mutant --- ["Ouch! That must have hurt. %s (%s) hit the ground with %d damage points."] = "", -- ClimbHome --- ["Ouch! That must have hurt. You mutilated your poor hedgehog hog with %d damage."] = "", -- ClimbHome --- ["Ouch! You just took fall damage."] = "", -- Basic_Training_-_Movement --- ["Our tribe, our beautiful island!"] = "", -- A_Classic_Fairytale:enemy --- ["Out of ammo!"] = "", -- A_Space_Adventure:desert03, Tumbler --- ["Out of ammo! Try again!"] = "", -- Basic_Training_-_Bazooka --- ["Over the Water"] = "", -- Basic_Training_-_Rope --- ["PAotH"] = "", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01, A_Space_Adventure:desert01, A_Space_Adventure:moon01 --- ["PAotH has sent explosives but unfortunately the trigger mechanism seems to be faulty!"] = "", -- A_Space_Adventure:cosmos --- ["Parachute"] = "", -- Continental_supplies --- ["Patches"] = "", -- - ["Pathetic Hog #1"] = "可怜刺猬一号", - ["Pathetic Hog #2"] = "可怜刺猬二号", --- ["Paul McHoggy"] = "", -- A_Space_Adventure:ice01, A_Space_Adventure:ice02 --- ["Pause: [P]"] = "", -- Basic_Training_-_Movement --- ["Pause: Tap the [Pause] button"] = "", -- Basic_Training_-_Movement --- ["Penalty: If you violate above rule, you have to skip in the next turn."] = "", -- WxW --- ["Penguin Roar"] = "", -- Continental_supplies --- ["Penguin roar: [Deal 15 damage + 10% of your hog’s health to all hogs around you and get 2/3 back]"] = "", -- Continental_supplies --- ["Penguin roar: [Deal 15 damage + 10% of your hogs health to all hogs around you and get 2/3 back]"] = "", -- Continental_supplies --- ["Perfect! Now try to get the next crate without hurting yourself!"] = "", -- A_Classic_Fairytale:first_blood --- ["Per-hog Ammo: Weapons are not shared between hogs"] = "", -- User_Mission_-_Nobody_Laugh --- ["Personal best: %.3f seconds"] = "", -- A_Space_Adventure:ice02 --- ["Per team weapons"] = "", -- Continental_supplies --- ["Pfew! That was close!"] = "", -- A_Classic_Fairytale:shadow --- ["Phosphat"] = "", -- portal --- ["Physicist"] = "", -- HedgeEditor --- ["Piano Strike"] = "", -- Construction_Mode --- ["Pikachu"] = "", -- --- ["Pings left: %d"] = "", -- Space_Invasion --- ["Pink"] = "", -- --- ["Pirates"] = "", -- --- ["Place 2-%d waypoints using the waypoint placement tool."] = "", -- Racer --- ["Place 2 waypoints using the waypoint placement tool."] = "", -- Racer --- ["Place air mines"] = "", -- HedgeEditor --- ["Place barrels"] = "", -- HedgeEditor --- ["Place cleavers"] = "", -- HedgeEditor --- ["Place/Delete Waypoint: [Left Click]"] = "", -- HedgeEditor --- ["Place dud mines"] = "", -- HedgeEditor --- ["Place Gears (and more): Gear Placement Tool"] = "", -- HedgeEditor --- ["Place Girder: Girder"] = "", -- HedgeEditor --- ["Place Girder: [Left Click]"] = "", -- HedgeEditor --- ["Place girders"] = "", -- HedgeEditor --- ["Place health crates"] = "", -- HedgeEditor --- ["Place hedgehogs: Place your hedgehogs at the start of the game."] = "", -- WxW --- ["Placement Mode"] = "", -- HedgeEditor --- ["Place mines"] = "", -- HedgeEditor --- ["Place, modify and delete gears (e.g. objects)|and waypoints, edit hedgehog settings, values,|victory conditions, and more."] = "", -- HedgeEditor --- ["Place Object: [Left Click]"] = "", -- HedgeEditor --- ["Place or delete waypoints"] = "", -- HedgeEditor --- ["Place rubber"] = "", -- HedgeEditor --- ["Place Rubber: Rubber"] = "", -- HedgeEditor --- ["Place Sprite: [Left Click]"] = "", -- HedgeEditor --- ["Place sprites to build land"] = "", -- HedgeEditor --- ["Place sticky mines"] = "", -- HedgeEditor --- ["Place targets"] = "", -- HedgeEditor --- ["Place utility crates"] = "", -- HedgeEditor --- ["Place Waypoint"] = "", -- HedgeEditor --- ["Place waypoint"] = "", -- Racer --- ["Place weapon crates"] = "", -- HedgeEditor --- ["- Place your clan flag at the end of your first turn"] = "", -- Capture_the_Flag --- ["- Place your team flag at the end of your first turn"] = "", -- Capture_the_Flag --- ["Planes used: %d"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Planes Used"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Planes Used:"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Planets with all missions completed will be marked with two flowers."] = "", -- A_Space_Adventure:cosmos --- ["Planets with completed main missions will be marked with a flower."] = "", -- A_Space_Adventure:cosmos --- ["Play with me!"] = "", -- A_Classic_Fairytale:shadow --- ["Please click on a crate."] = "", -- HedgeEditor --- ["Please click on a gear."] = "", -- HedgeEditor --- ["Please click on a hedgehog, barrel, health crate or dud mine."] = "", -- HedgeEditor --- ["Please click on a hedgehog."] = "", -- HedgeEditor --- ["Please place the waypoint further away from the waterline"] = "", -- Racer, TechRacer --- ["Please place the waypoint in the air and within the map boundaries"] = "", -- TechRacer --- ["Please place the waypoint in the air, within the map boundaries"] = "", -- Racer --- ["Please place your hedgehog first!"] = "", -- WxW --- ["Please, stop releasing your \"smoke signals\"!"] = "", -- A_Classic_Fairytale:shadow --- ["Please wait …"] = "", -- WxW --- ["Point Blank Combo! +5 points!"] = "", -- Space_Invasion --- ["--- Points ---"] = "", -- Battalion --- ["--- Points Mode ---"] = "", -- Battalion --- ["Poison"] = --- ["Poisonous Apple"] = "", -- A_Space_Adventure:fruit02 --- ["Poisonous, deals no damage."] = "", -- Continental_supplies --- ["Pokémon"] = "", -- --- ["Poor %s (%s) died %d times."] = "", -- Mutant --- ["Population"] = "", -- Continental_supplies --- ["Porkey"] = "", -- --- ["Portal hint: one goes to the destination, and one is the entrance.|"] = "", -- A_Classic_Fairytale:dragon --- ["Portal hint: One goes to the destination, the other one is the entrance.|"] = "", -- A_Classic_Fairytale:dragon --- ["Portal Mind Challenge"] = "", -- portal --- ["Precise Aim: [Left Shift]"] = "", -- Basic_Training_-_Movement --- ["Precise Aim: [Left Shift] + [Up]/[Down]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["Precise flying"] = "", -- A_Space_Adventure:desert03 --- ["Precise: Remove previous waypoint"] = "", -- Racer --- ["Precise shooting"] = "", -- A_Space_Adventure:fruit03 --- ["Predator"] = "", -- portal --- ["Prepare for battle!"] = "", -- A_Space_Adventure:moon01 --- ["Prepare to fight"] = "", -- A_Space_Adventure:moon01 --- ["Prepare to flee!"] = "", -- A_Space_Adventure:cosmos --- ["Prepare yourself, %s!"] = "", -- The_Specialists --- ["Press [Attack] (space bar by default) to start,|repeadedly tap the up, left and right movement keys to accelerate."] = "", -- Basic_Training_-_Flying_Saucer --- ["Press [Attack] (space bar by default) to start,|repeatedly tap the up, left and right movement keys to accelerate."] = "", -- Basic_Training_-_Flying_Saucer --- ["Press [Attack] to begin."] = "", -- A_Classic_Fairytale:first_blood --- ["Press [Attack] to confirm."] = "", -- Continental_supplies --- ["Press [Attack] to select this continent!"] = "", -- Continental_supplies --- ["Press [Left] and [Right] to change the difficulty."] = "", -- A_Classic_Fairytale:first_blood --- ["Press [Left] or [Right] to move around, [Long Jump] to jump forwards."] = "", -- A_Classic_Fairytale:first_blood --- ["Press [Long jump] to accept this configuration and begin placing hedgehogs."] = "", -- WxW --- ["Press [Long jump] to accept this configuration and start the game."] = "", -- WxW --- ["Press [M] to see the mission texts"] = "", -- Basic_Training_-_Movement --- ["Press [Precise] to skip intro"] = "", --- ["Press [Up] and [Down] to move between menu items.|Press [Attack], [Left], or [Right] to toggle."] = "", -- WxW --- ["Prestigious Pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Princess"] = "", -- A_Classic_Fairytale:family, A_Classic_Fairytale:journey --- ["Princess Peach"] = "", -- --- ["Problems, dude? Chillax!"] = "", -- A_Classic_Fairytale:epil --- ["Professional pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Professional stunt pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Professor"] = "", -- A_Space_Adventure:death01, A_Space_Adventure:moon01 --- ["Professor Hogevil, then known as James Hogus, worked for PAotH back in my time."] = "", -- A_Space_Adventure:moon02 --- ["Professor's Team"] = "", -- A_Space_Adventure:death01 --- ["Prof. Hogevil"] = "", -- A_Space_Adventure:death01, A_Space_Adventure:moon01 --- ["Protect the King: When the king dies, so does the team"] = "", -- Battalion --- ["Protect yourselves!|Grenade hint: set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"] = "", -- A_Classic_Fairytale:shadow --- ["Purple"] = "", -- --- ["Pyro"] = "", -- HedgeEditor, The_Specialists --- ["Pyromancer"] = "", -- Battalion --- ["Quit: [Esc]"] = "", -- Basic_Training_-_Movement --- ["Race complexity limit reached"] = "", -- Racer, TechRacer --- ["Race failed!"] = "", -- A_Space_Adventure:moon02 --- ["Racer"] = "", -- Racer --- ["Racer tool"] = "", -- Racer --- ["Race"] = "", -- TrophyRace --- ["Rachel"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Radar: Off"] = "", -- WxW --- ["Radar: On"] = "", -- WxW --- ["Radar Ping: [High jump]"] = "", -- Space_Invasion --- ["Radar: Show after crate drop"] = "", -- WxW --- ["Raging Buffalo"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:united --- ["Ramesses"] = "", -- --- ["Ramon"] = "", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow --- ["Random continent"] = "", -- Continental_supplies --- ["Rank: %s"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Razac"] = "", -- portal --- ["RC Plane Challenge"] = "", -- User_Mission_-_RCPlane_Challenge --- ["RC Plane"] = "", -- Construction_Mode --- ["Reach and destroy the final target to win."] = "", -- Basic_Training_-_Rope --- ["Read the challenge objectives from within the mission for more details."] = "", -- A_Space_Adventure:death02, A_Space_Adventure:desert03, A_Space_Adventure:fruit03 --- ["Ready for Battle?"] = "", -- A_Space_Adventure:fruit01 --- ["Really?! You thought you could harm me with your little toys?"] = "", -- A_Classic_Fairytale:shadow --- ["Red"] = "", -- --- ["Reflector Shield"] = "", -- Construction_Mode --- ["Reflector Shield: Reflects enemy projectiles."] = "", -- Construction_Mode --- ["Regurgitator"] = "", -- A_Classic_Fairytale:backstab --- ["Reinforcements! +2 of each weapon!"] = "", -- A_Space_Adventure:death02 --- ["Reinforcements"] = "", -- A_Classic_Fairytale:backstab --- ["Release rope: [Attack]"] = "", -- Basic_Training_-_Rope --- ["Remember: Hold down [Left Shift] to prevent slipping"] = "", -- Basic_Training_-_Movement --- ["Remember! Many will seek the anti-gravity device! Now go, hurry up!"] = "", -- A_Space_Adventure:cosmos --- ["Remember: The rope only bend around objects, |if it doesn't hit anything it's always stright!"] = "", -- Basic_Training_-_Rope --- ["Remember this, pathetic animal: when the day comes, you will regret your blind loyalty!"] = "", -- A_Classic_Fairytale:shadow --- ["Remember this, pathetic animal: When the day comes, you will regret your blind loyalty!"] = "", -- A_Classic_Fairytale:shadow --- ["Replenishment: Weapons are restocked on turn start of a new hog"] = "", -- Highlander --- ["Repositioning Mode"] = "", -- HedgeEditor --- ["REPOSITIONING MODE"] = "", -- HedgeEditor --- ["Rescue the imprisoned PAotH team and get the fuel!"] = "", -- A_Space_Adventure:moon01 --- ["Respawner"] = "", -- Construction_Mode --- ["Respawner: Resurrects dead hogs."] = "", -- Construction_Mode --- ["Resurrector"] = "", -- Construction_Mode --- ["Retract/Extend rope: [Up]/[Down]"] = "", -- Basic_Training_-_Rope --- ["- Return the enemy flag to your base to score"] = "", -- Capture_the_Flag --- ["Return to Leaks A Lot!"] = "", -- A_Classic_Fairytale:shadow --- ["Return to the mission menu by pressing the \"Go back\" button."] = "", -- A_Space_Adventure:cosmos --- ["Return to the Surface"] = "", -- A_Space_Adventure:fruit02 --- ["Return to the training menu by pressing the “Go back” button."] = "", -- Basic_Training_-_Movement --- ["Rider"] = "", -- portal --- ["Rifleman"] = "", -- Battalion --- ["Righteous Beard"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:queen, A_Classic_Fairytale:united --- ["Ripe"] = "", -- --- ["Rise the water if nobody else is in the circle and deal 6 damage to all enemy hogs."] = "", -- Continental_supplies --- ["Robert Yellow Apple"] = "", -- A_Space_Adventure:fruit01 --- ["Rocket"] = "", -- Big_Armory --- ["Ronald"] = "", -- portal --- ["Roof"] = "", -- WxW --- ["Rope-knocking Challenge"] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Rope Master"] = "", -- Basic_Training_-_Rope --- ["Ropes and Crates"] = "", -- Challenge_-_Speed_Shoppa_-_Ropes --- ["Ropes can be fired again in the air!"] = "", -- A_Classic_Fairytale:first_blood --- ["Rope Team"] = "", -- Basic_Training_-_Rope --- ["Rope Training"] = "", -- Basic_Training_-_Rope --- ["Rope Weapons"] = "", -- Basic_Training_-_Rope --- ["Roshi"] = "", -- --- ["Rot Molester"] = "", -- A_Classic_Fairytale:shadow --- ["Rotten"] = "", -- --- ["Round draw"] = "", -- Racer, TechRacer --- ["Round %d (Sudden Death in round %d)"] = "", -- Battalion --- ["Round limit: %d"] = "", -- Racer --- ["Round Limit: %d"] = "", -- Space_Invasion --- ["Round limit:"] = "", -- TechRacer --- ["Rounds complete: %d/%d"] = "", -- Racer, Space_Invasion, TechRacer --- ["Round's slowest lap: %.3fs by %s"] = "", -- TrophyRace --- ["RS1"] = "", -- A_Space_Adventure:fruit03 --- ["RS2"] = "", -- A_Space_Adventure:fruit03 --- ["Rubber"] = "", -- Construction_Mode, HedgeEditor --- ["Rubber Placement Mode"] = "", -- Construction_Mode --- ["RUBBER PLACEMENT MODE"] = "", -- HedgeEditor --- ["Rules:"] = "", -- Capture_the_Flag --- ["RULES:"] = "", -- Frenzy --- ["Rules: "] = "", -- Mutant --- ["Run away, you coward!"] = "", -- A_Space_Adventure:desert01 --- ["Running displacement algorithm …"] = "", -- A_Classic_Fairytale:queen --- ["Running for survival"] = "", -- A_Space_Adventure:desert02 --- ["Rusted Diego"] = "", -- --- ["Rusty Joe"] = "", -- A_Classic_Fairytale:queen --- ["Ryu"] = "", -- --- ["%s (+1)"] = "", -- A_Space_Adventure:fruit03 --- ["%s: %.1fs"] = "", -- Racer, TechRacer --- ["Sabotage all hogs in the circle and fire a cluster above you.|Sabotaged hogs lose health and have to deal with a very high gravity during their turn."] = "", -- Continental_supplies --- ["Sabotage/Flare: [Sabotage all hogs in the circle and deal ~1 dmg OR Fire a cluster up into the air]"] = "", -- Continental_supplies --- ["Saint"] = "", -- HedgeEditor, The_Specialists --- ["Salivaslurper"] = "", -- A_Classic_Fairytale:united --- ["Salty Dog"] = "", -- --- ["Salvation"] = "", -- A_Classic_Fairytale:family --- ["Salvation was one step closer now..."] = "", -- A_Classic_Fairytale:dragon --- ["Sam"] = "", -- A_Space_Adventure:cosmos --- ["Sandals?! I thought you left your ring!"] = "", -- A_Classic_Fairytale:queen --- ["%s and GB"] = "", -- A_Space_Adventure:fruit02 --- ["%s and %s enter the battlefield"] = "", -- A_Space_Adventure:fruit01 --- ["Sandstorm"] = "", -- A_Space_Adventure:desert01 --- ["Sandy"] = "", -- A_Space_Adventure:desert01 --- ["%s arrived at the Desert Planet!"] = "", -- A_Space_Adventure:cosmos --- ["%s arrived at the Fruit Planet!"] = "", -- A_Space_Adventure:cosmos --- ["%s arrived at the Ice Planet!"] = "", -- A_Space_Adventure:cosmos --- ["%s arrived at the meteorite!"] = "", -- A_Space_Adventure:cosmos --- ["%s arrived at the moon!"] = "", -- A_Space_Adventure:cosmos --- ["%s arrived at the Planet of Death!"] = "", -- A_Space_Adventure:cosmos --- ["Save as many hogs as possible!"] = "", -- User_Mission_-_That_Sinking_Feeling --- ["Save Fell From Heaven!"] = "", -- A_Classic_Fairytale:journey --- ["Save Leaks A Lot!|Hint: The switch hedgehog utility might be of help to you."] = "", -- A_Classic_Fairytale:shadow --- ["Save Level: [Precise]+[4]"] = "", -- HedgeEditor --- ["Save the princess! All your hogs must survive!|Hint: Kill the cyborgs first! Use the ammo very carefully!|Hint: You might want to spare a girder for cover!"] = "", -- A_Classic_Fairytale:family --- ["Save the princess by collecting the crate in under 12 turns!"] = "", -- A_Classic_Fairytale:journey --- ["Saving Hogera"] = "", -- A_Space_Adventure:cosmos --- ["%s barely made it past the hogosphere."] = "", -- ClimbHome --- ["%s bravely climbed up to a dizzy height of %d to reach home."] = "", -- ClimbHome --- ["Scallywag"] = "", -- --- ["Scalp Muncher"] = "", -- A_Classic_Fairytale:backstab --- ["Scenario"] = "", -- Big_Armory, portal, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, User_Mission_-_Diver, User_Mission_-_Newton_and_the_Hammock, User_Mission_-_Nobody_Laugh, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork_2, User_Mission_-_Teamwork, User_Mission_-_The_Great_Escape --- ["Scientist"] = "", -- Battalion --- ["%s climbed home in %d seconds!"] = "", -- ClimbHome --- ["%s (contd.)"] = "", -- A_Classic_Fairytale:epil --- ["Score: %d"] = "", -- Space_Invasion --- ["Score goal: %d"] = "", -- Control --- ["Score graph"] = "", -- Mutant, Space_Invasion --- ["Score points by killing other hedgehogs."] = "", -- Mutant --- ["Score points by killing other hedgehogs (see below)."] = "", -- Mutant --- ["Scores: "] = "", -- Capture_the_Flag --- ["Scores"] = "", -- Mutant --- ["Scores:"] = "", -- Mutant --- ["Scoring: "] = "", -- Mutant --- ["%s couldn't escape, try again!"] = "", -- A_Space_Adventure:fruit01 --- ["Script parameter examples:"] = "", -- Gravity --- ["%s (+%d)"] = "", -- Battalion --- ["%s: %d"] = "", -- Capture_the_Flag, Control --- ["%s (%d)"] = "", -- Continental_supplies --- ["%s: %d (deaths: %d)"] = "", -- Mutant --- ["%s (%d), %d sec"] = "", -- Continental_supplies --- ["%s: Did not finish"] = "", -- Racer, TechRacer --- ["%s did not finish the race."] = "", -- Racer, TechRacer --- ["%s didn't expect that."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s died … and lives again!"] = "", -- Construction_Mode --- ["%s doesn’t really know how to handle a rope properly."] = "", -- ClimbHome --- ["%s, %d sec"] = "", -- Continental_supplies --- ["Search for the device with the help of the other hedgehogs."] = "", -- A_Space_Adventure:fruit02 --- ["Searching in the dust"] = "", -- A_Space_Adventure:desert01 --- ["Searching the stars!"] = "", -- A_Space_Adventure:cosmos --- ["Seduction"] = "", -- Continental_supplies --- ["Seems like every time you take a \"walk\", the enemy finds us!"] = "", -- A_Classic_Fairytale:backstab --- ["See that crate farther on the right?"] = "", -- A_Classic_Fairytale:first_blood - ["See ya!"] = "再见!", --- ["Segmentation Paul"] = "", -- A_Classic_Fairytale:dragon --- ["Select a placement mode and read the infos|in the mission panel to learn how to use it."] = "", -- HedgeEditor --- ["Select continent!"] = "", -- Continental_supplies --- ["Select continent"] = "", -- Continental_supplies --- ["Selection Mode"] = "", -- HedgeEditor --- ["Select, modify, or delete girders, rubbers and sprites"] = "", -- HedgeEditor --- ["Select/Place/Delete Gear: [Left Click]"] = "", -- HedgeEditor --- ["Select, reposition and delete gears"] = "", -- HedgeEditor --- ["Select Rope"] = "", -- Basic_Training_-_Rope --- ["Select “Switch Hedgehog” from the ammo menu and|hit the “Attack” key."] = "", -- Basic_Training_-_Movement --- ["Select “Switch Hedgehog” from the ammo menu and|hit the “Attack” key to proceed."] = "", -- Basic_Training_-_Movement --- ["Select the current continent."] = "", -- Continental_supplies --- ["Select the rope to begin!"] = "", -- Basic_Training_-_Rope --- ["Select this item for a random continent."] = "", -- Continental_supplies --- ["Select Weapon"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["Select weapon: [Left click]"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade --- ["Select win/lose condition: [Left], [Right]"] = "", -- HedgeEditor --- ["Select your continent/weaponset: with the \"Up\" or \"Down\" keys. You can also select one with the weapons menu."] = "", -- Continental_supplies --- ["Select your continent/weaponset: With the \"Up\" or \"Down\" keys. You can also select one with the weapons menu."] = "", -- Continental_supplies --- ["Select your continent with [Up]/[Down] or by selecting a representative weapon."] = "", -- Continental_supplies --- ["%s enters the battlefield"] = "", -- A_Space_Adventure:fruit01 --- ["Sergey"] = "", -- --- ["%s escaped successfully!"] = "", -- A_Space_Adventure:fruit01 --- ["Set bounciness: [Left Shift] + [1]-[5]"] = "", -- Basic_Training_-_Grenade --- ["Set detonation timer: [1]-[5]"] = "", -- Basic_Training_-_Grenade --- ["Set Health: [Left Click]"] = "", -- HedgeEditor --- ["Set Identity: [Left Click]"] = "", -- HedgeEditor --- ["Set period to negative value for random gravity."] = "", -- Gravity --- ["Set the health of hogs, health crates, barrels and duds"] = "", -- HedgeEditor --- ["Set to %d"] = "", -- HedgeEditor --- ["%s exploded."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s fell from a high cliff."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s fell too fast."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s fell victim to a weapon filter"] = "", -- Construction_Mode --- ["%s felt unstable."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s felt victim to rope-knocking."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s flew like a rock."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s gets an extra life"] = "", -- Construction_Mode --- ["%s goes the way of the lemming."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Sgt. Smith"] = "", -- --- ["%s had it coming."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s had no chance."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["... share your beauty with the world every morning, my princess!"] = "", -- A_Classic_Fairytale:journey --- ["%s has been killed before taking enough damage first."] = "", -- SimpleMission --- ["%s has been knocked out."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s has been rescued from death"] = "", -- Construction_Mode --- ["%s has dropped the flag!"] = "", -- CTF_Blizzard --- ["%s has fallen victim to gravity."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s has mutated! +2 points"] = "", -- Mutant --- ["%s has passed the best height of %s!"] = "", -- ClimbHome --- ["%s has scored!"] = "", -- Capture_the_Flag --- ["%s has to refuel the saucer."] = "", -- A_Space_Adventure:moon01 --- ["%s hates Newton."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["She endangered the whole tribe!"] = "", -- A_Classic_Fairytale:epil --- ["sheepluva"] = "", -- --- ["Sheepy"] = "", -- --- ["She's behind that tall thingy."] = "", -- A_Classic_Fairytale:family --- ["Shield boosted! +%d power"] = "", -- Space_Invasion --- ["Shield depleted"] = "", -- Space_Invasion --- ["Shield is fully recharged!"] = "", --- ["Shield Master! +10 points!"] = "", -- Space_Invasion --- ["Shield Miser! +%d points!"] = "", -- Space_Invasion --- ["Shield OFF: %d power remaining"] = "", -- Space_Invasion --- ["Shield ON: %d power remaining"] = "", -- Space_Invasion --- ["Shield Seeker! +10 points!"] = "", -- Space_Invasion --- ["Shinobi"] = "", -- --- ["%s hit the ground."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Shoppa Love"] = "", -- Challenge_-_Speed_Shoppa_-_Hedgelove --- ["Shotgun"] = "", -- Continental_supplies --- ["Sigh."] = "", -- A_Classic_Fairytale:epil --- ["Silly"] = "", --- ["Silver"] = "", -- --- ["Sine Gun"] = "", -- Construction_Mode --- ["Sinky"] = "", --- ["Sirius Lee"] = "", -- A_Classic_Fairytale:enemy --- ["%s is dead, who was critical to this mission!"] = "", -- SimpleMission --- ["%s is eliminated!"] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s is now as poor as a church mouse"] = "", -- Construction_Mode --- ["%s is now a zombie hedgehog"] = "", -- Construction_Mode --- ["%s is suddenly low on ammo"] = "", -- Construction_Mode --- ["Skulls"] = "", -- Bazooka_Battlefield --- ["Slimer"] = "", -- --- ["Slippery"] = "", -- A_Classic_Fairytale:journey --- ["%s lost all the weapons"] = "", -- Construction_Mode --- ["%s lost, try again!"] = "", -- A_Space_Adventure:death01, A_Space_Adventure:death02, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:desert03, A_Space_Adventure:final, A_Space_Adventure:fruit01, A_Space_Adventure:fruit02, A_Space_Adventure:fruit03, A_Space_Adventure:ice01, A_Space_Adventure:moon01 --- ["Slot %d: %s"] = "", -- Frenzy --- ["Slot keys save time! (F1-F10 by default)"] = "", -- Frenzy --- ["Slowpoke"] = "", -- --- ["%s made it past the hogosphere."] = "", -- ClimbHome --- ["%s managed to pass half of the distance towards home."] = "", -- ClimbHome --- ["%s may choose the rules."] = "", -- WxW --- ["Smith 0.97"] = "", -- A_Classic_Fairytale:enemy --- ["Smith 0.98"] = "", -- A_Classic_Fairytale:enemy --- ["Smith 0.99a"] = "", -- A_Classic_Fairytale:enemy --- ["Smith 0.99b"] = "", -- A_Classic_Fairytale:enemy --- ["Smith 0.99f"] = "", -- A_Classic_Fairytale:enemy --- ["Smith 1.0"] = "", -- A_Classic_Fairytale:enemy --- ["Smugglers"] = "", -- A_Space_Adventure:desert01 --- ["%s must collect the final crates."] = "", -- A_Space_Adventure:fruit02 --- ["%s must skip this turn for rule violation."] = "", -- WxW --- ["Sneaks"] = "", -- Bazooka_Battlefield --- ["%s never got the ninja diploma."] = "", -- ClimbHome --- ["%s never wanted to reach for the sky in the first place."] = "", -- ClimbHome --- ["Sniper! +8 points!"] = "", -- Space_Invasion --- ["Sniper"] = "", -- HedgeEditor, The_Specialists --- ["Sniper Rifle"] = "", -- Continental_supplies - ["Sniper Training"] = "狙击训练", --- ["So, as promised I have brought you where I think that the device you are looking for is hidden."] = "", -- A_Space_Adventure:fruit02 --- ["So far, you had infinite ropes, but in the|real world, ropes are usually limited."] = "", -- Basic_Training_-_Rope --- ["So humiliating..."] = "", -- A_Classic_Fairytale:first_blood --- ["So, I believe that it's a good place to start."] = "", -- A_Space_Adventure:desert01 --- ["So, I kindly ask for your help."] = "", -- A_Space_Adventure:fruit01 --- ["So I shook my fist in the air!"] = "", -- A_Classic_Fairytale:epil --- ["Soldier"] = "", -- HedgeEditor, The_Specialists --- ["So, let me tell you what I know about Professor Hogevil."] = "", -- A_Space_Adventure:moon02 --- ["Some parts of the land are indestructible."] = "", -- A_Space_Adventure:fruit03 --- ["Some sick game of yours?!"] = "", -- A_Classic_Fairytale:queen --- ["Some weapons can be dropped from the rope."] = "", -- Basic_Training_-_Rope --- ["Somewhere else on the planet of fruits, Captain Lime helps %s"] = "", -- A_Space_Adventure:fruit02 --- ["Somewhere else on the planet of fruits, %s gets closer to the device"] = "", -- A_Space_Adventure:fruit02 --- ["Somewhere on the Planet of Fruits a terrible war is about to begin ..."] = "", -- A_Space_Adventure:fruit01 --- ["Somewhere on the uninhabitable Death Planet ..."] = "", -- A_Space_Adventure:death01 --- ["So, now I got the last part and I have your friends captured."] = "", -- A_Space_Adventure:death01 --- ["So, %s, here we are ..."] = "", -- A_Space_Adventure:cosmos --- ["So the princess was never heard of again ..."] = "", -- A_Classic_Fairytale:family --- ["So, uhmm, how did you manage to teleport them so far?"] = "", -- A_Classic_Fairytale:epil --- ["Sour"] = "", -- --- ["South America"] = "", -- Continental_supplies --- ["So? What will it be?"] = "", -- A_Classic_Fairytale:shadow --- ["So you are able to launch projectiles into your aiming direction, always at full power."] = "", -- Basic_Training_-_Flying_Saucer --- ["So you are interested in Professor Hogevil, huh?"] = "", -- A_Space_Adventure:moon02 --- ["So you basically did the dirty work for us."] = "", -- A_Classic_Fairytale:dragon --- ["Space Invasion"] = "", -- Space_Invasion --- ["SPACE INVASION"] = "", -- Space_Invasion --- ["Spacetrip"] = "", -- A_Space_Adventure:cosmos --- ["Spawn the crate and attack!"] = "", -- WxW --- ["Specials: Kings and air generals drop helpers, not weapons"] = "", -- Battalion --- ["Special weapons:"] = "", -- Continental_supplies --- ["Special Weapons:"] = "", -- Continental_supplies --- ["Speckles"] = "", -- --- ["Specs"] = "", -- --- ["Specs Appeal"] = "", -- --- ["Spectator"] = "", -- --- ["Speed Roping"] = "", -- Basic_Training_-_Rope --- ["Speed Shoppa"] = "", -- SpeedShoppa --- ["Spike"] = "", -- A_Space_Adventure:desert01 --- ["Spikes"] = "", -- --- ["Spiky Cheese"] = "", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow --- ["%s, place the first hedgehog!"] = "", -- WxW --- ["Spleenlover"] = "", -- A_Classic_Fairytale:united --- ["Sponge"] = "", - ["Spooky Tree"] = "怪树", --- ["Sprite Erasure Mode"] = "", -- HedgeEditor --- ["Sprite Modification Mode"] = "", -- HedgeEditor --- ["SPRITE MODIFICATION MODE"] = "", -- HedgeEditor --- ["Sprite Placement Mode"] = "", -- Construction_Mode --- ["SPRITE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Sprite Testing Mode"] = "", -- Construction_Mode --- ["Squirtle"] = "", -- --- ["Squishy"] = "", -- --- ["%s reached home in %.3f seconds. Congratulations!"] = "", -- ClimbHome --- ["%s: %s"] = "", -- Continental_supplies --- ["%s (%s) destroyed %d invaders in one round."] = "", -- Space_Invasion --- ["%s (%s) does not have to feel ashamed for their best height of %d."] = "", -- ClimbHome --- ["%s, select your continent!"] = "", -- Continental_supplies --- ["%s (%s) gave short shrift to the invaders: Longest combo of %d!"] = "", -- Space_Invasion --- ["%s (%s) has been invited to join the Planetary Association of the Hedgehogs, it destroyed a staggering %d invaders in just one round!"] = "", -- Space_Invasion --- ["%s (%s) has captured the flag %d times."] = "", -- Capture_the_Flag --- ["%s (%s) hate life and suicided %d times."] = "", -- Mutant --- ["%s should try the rope training mission first."] = "", -- ClimbHome --- ["%s (%s) is addicted to killing: %d invaders destroyed in one round."] = "", -- Space_Invasion --- ["%s (%s) is a hardened hunter: No misses and %d hits in its best round!"] = "", -- Space_Invasion --- ["%s (%s) is a tumbleweed: %d points in one round."] = "", -- Space_Invasion --- ["%s (%s) is good at this: %d points in only one round!"] = "", -- Space_Invasion --- ["%s (%s) is Rambo in a hedgehog costume! He destroyed %d invaders in one round."] = "", -- Space_Invasion --- ["%s skipped ninja classes."] = "", -- ClimbHome --- ["%s spawned at a really bad position."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s splatted."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s (%s) reached a decent peak height of %d."] = "", -- ClimbHome --- ["%s (%s) reached a peak height of %d."] = "", -- ClimbHome --- ["%s (%s) reached for the sky and beyond with a height of %d!"] = "", -- ClimbHome --- ["%s (%s) reached home in %.3f seconds."] = "", -- ClimbHome --- ["%s (%s) shot %d invaders and never missed in the best round!"] = "", -- Space_Invasion --- ["%s (%s) struck like a meteor: %d points in only one round!"] = "", -- Space_Invasion --- ["%s still had a long way to go."] = "", -- ClimbHome --- ["%s stumbled."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s (%s) tumbles like no other: %d points in one round."] = "", -- Space_Invasion --- ["%s stumpled."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s (%s) was certainly not afraid of heights: Peak height of %d!"] = "", -- ClimbHome --- ["%s (%s) was lightning-fast! Longest combo of %d, absolutely insane!"] = "", -- Space_Invasion --- ["%s (%s) was on fire: Longest combo of %d."] = "", -- Space_Invasion --- ["%s (%s) was panicly afraid of the water and decided to get in a safe distance of %d from it."] = "", -- ClimbHome --- ["%s (%s) was the best baby tumbler: %d points in one round."] = "", -- Space_Invasion --- ["%s (%s) was the greediest hedgehog and collected %d crates."] = "", -- Mutant --- ["%s (%s) was undoubtedly the very best professional tumbler in this game: %d points in one round!"] = "", -- Space_Invasion --- ["Star"] = "", -- Big_Armory --- ["Status update"] = "", -- Racer, TechRacer --- ["Status Update"] = "", -- Space_Invasion --- ["Stay away from our weapons!"] = "", -- A_Classic_Fairytale:queen --- ["Stay there, comrades!"] = "", -- A_Classic_Fairytale:queen --- ["Stay there to flee!"] = "", -- A_Space_Adventure:fruit01 --- ["Steel Eye"] = "", -- A_Classic_Fairytale:queen --- ["Step 1: Activate your flying saucer but do NOT move yet!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Step 1: Start roping"] = "", -- Basic_Training_-_Rope --- ["Step 2: Select grenade"] = "", -- Basic_Training_-_Rope --- ["Step 2: Select your grenade."] = "", -- Basic_Training_-_Flying_Saucer --- ["Step 3: Drop the grenade"] = "", -- Basic_Training_-_Rope --- ["Step 3: Start flying and get yourself right above the target."] = "", -- Basic_Training_-_Flying_Saucer --- ["Step 4: Drop your grenade by pressing the [Long jump] key."] = "", -- Basic_Training_-_Flying_Saucer --- ["Step 5: Get away quickly and land safely anywhere."] = "", -- Basic_Training_-_Flying_Saucer --- ["Step By Step"] = "", -- A_Classic_Fairytale:first_blood --- ["Steve"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["Sticky Mine"] = "", -- Continental_supplies --- ["Sticky Mine Placement Mode"] = "", -- Construction_Mode --- ["STICKY MINE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Stop, comrades!"] = "", -- A_Classic_Fairytale:queen --- ["Stop right there, puny worms!"] = "", -- A_Classic_Fairytale:queen --- ["Street Fighters"] = "", -- --- ["Strength: %d (multiplier for ammo)"] = "", -- Battalion --- ["Strong knockback, but no poison."] = "", -- Continental_supplies --- ["Stronglings"] = "", -- A_Classic_Fairytale:shadow --- ["Structure Placement Mode"] = "", -- Construction_Mode --- ["Structure Placer"] = "", -- Construction_Mode --- ["Stupid, stupid Hogerians!"] = "", -- A_Space_Adventure:final --- ["Subtract %d"] = "", -- HedgeEditor --- ["--- Sudden Death ---"] = "", -- Battalion --- ["Summer Squash"] = "", -- A_Space_Adventure:fruit01 --- ["Sundaland"] = "", -- Continental_supplies --- ["Sunflame"] = "", -- Big_Armory --- ["Super weapons: A few crates contain very powerful weapons."] = "", -- WxW --- ["Super weapons: %s"] = "", -- WxW --- ["Supplies: Each continent gives you unique weapons, specials and health."] = "", -- Continental_supplies --- ["Support Station: Allows placement of crates."] = "", -- Construction_Mode --- ["Support Station"] = "", -- Construction_Mode --- ["Sure!"] = "", -- A_Classic_Fairytale:epil --- ["Surf Before Crate: %s"] = "", -- WxW --- ["Surf Before Crate: You must bounce off the water once before you can get crates."] = "", -- WxW --- ["Surfer! +15 points!"] = "", -- Space_Invasion --- ["Surfer!"] = "", -- WxW --- ["Surprise supplies: Get 1-3 random weapons each turn."] = "", -- Continental_supplies --- ["Survive!"] = "", -- A_Classic_Fairytale:shadow --- ["%s violated the “All But Last” rule and will be penalized."] = "", -- WxW --- ["%s violated the “Kill The Leader” rule and will be penalized."] = "", -- WxW --- ["Swap place with a random enemy in the circle."] = "", -- Continental_supplies --- ["%s was a good target."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s was close to home."] = "", -- ClimbHome --- ["%s was damn close to home."] = "", -- ClimbHome --- ["%s was doomed from the beginning."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s was extracted from the scheme"] = "", -- Continental_supplies --- ["%s was good, but not good enough."] = "", -- ClimbHome --- ["%s was knocked away."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s was really unlucky."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s was shoved away."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["%s was smashed."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Sweet"] = "", -- --- ["%s went over a quarter of the way towards home."] = "", -- ClimbHome --- ["%s! Why?!"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united --- ["Swing, Leaks A Lot, on the wings of the wind!"] = "", -- A_Classic_Fairytale:first_blood --- ["Swing: [Left]/[Right]"] = "", -- Basic_Training_-_Rope --- ["%s wins, congratulations!"] = "", -- A_Space_Adventure:moon01 --- ["%s wins!"] = "", -- Racer, Space_Invasion, TechRacer, ClimbHome --- ["%s wins with a best time of %.1fs."] = "", -- Racer, TechRacer --- ["switch"] = "", -- Continental_supplies --- ["Switch: Drop ball of dirt from parachute (once)"] = "", -- Continental_supplies --- ["Switched to "] = "", --- ["Switch Hedgehog (1/3)"] = "", -- Basic_Training_-_Movement --- ["Switch Hedgehog (2/3)"] = "", -- Basic_Training_-_Movement --- ["Switch Hedgehog (3/3)"] = "", -- Basic_Training_-_Movement --- ["Switch Hedgehog (Failed!)"] = "", -- Basic_Training_-_Movement --- ["Switch hedgehog: [Tabulator]"] = "", -- Basic_Training_-_Movement --- ["Switch Hog"] = "", -- Construction_Mode --- ["Switch: Select weapon special"] = "", -- Continental_supplies --- ["Switch: Toggle crate radar"] = "", -- WxW --- ["%s won!"] = "", -- A_Space_Adventure:fruit01 --- ["Swords"] = "", -- Bazooka_Battlefield --- ["Syntax Errol"] = "", -- A_Classic_Fairytale:dragon --- ["%s, you may choose the rules."] = "", -- WxW --- ["szczur"] = "", -- --- ["Tackleberry"] = "", -- --- ["Tails"] = "", -- --- ["Talk about mixed signals..."] = "", -- A_Classic_Fairytale:dragon --- ["Tall Potato"] = "", -- A_Space_Adventure:fruit01 --- ["Tap [Pause] to see the mission texts"] = "", -- Basic_Training_-_Movement --- ["Tap the “rotating arrow” button on the left|until you have selected Cappy, the hedgehog with the cap!"] = "", -- Basic_Training_-_Movement --- ["Target"] = "", -- HedgeEditor --- ["Target Placement Mode"] = "", -- Construction_Mode --- ["TARGET PLACEMENT MODE"] = "", -- HedgeEditor --- ["Target Practice: Bazooka (easy)"] = "", -- Target_Practice_-_Bazooka_easy --- ["Target Practice: Bazooka (hard)"] = "", -- Target_Practice_-_Bazooka_hard --- ["Target Practice: Grenade (easy)"] = "", -- Target_Practice_-_Grenade_easy --- ["Target Practice: Grenade (hard)"] = "", -- Target_Practice_-_Grenade_hard --- ["Target Practice: Homing Bee"] = "", -- Target_Practice_-_Homing_Bee --- ["Target Practice: Shotgun"] = "", -- Target_Practice_-_Shotgun --- ["Target Puncher"] = "", -- Basic_Training_-_Rope --- ["Targets left: %d"] = "", -- TargetPractice --- ["Tatsujin"] = "", -- --- ["Tatters"] = "", -- --- ["Team %d"] = "", -- SimpleMission - ["Team %d: "] = "队伍 %d", --- ["Team highscore: %d"] = "", -- Utils --- ["Team Identity Mode"] = "", -- HedgeEditor --- ["TEAM IDENTITY MODE"] = "", -- HedgeEditor --- ["Team lowscore: %d"] = "", -- Utils --- ["Teams are tied! Continue playing rounds until we have a winner!"] = "", -- Space_Invasion --- ["Team's best time: %.3fs"] = "", -- Utils --- ["Team Scores:"] = "", -- Control --- ["Team scores:"] = "", -- Space_Invasion --- ["Team's longest time: %.3fs"] = "", -- Utils --- ["Team's top accuracy: %d%"] = "", -- Utils --- ["Teamwork 2"] = "", -- User_Mission_-_Teamwork_2 --- ["Teamwork"] = "", -- User_Mission_-_Teamwork --- ["TechRacer"] = "", -- TechRacer --- ["Teleporation Node"] = "", -- Construction_Mode --- ["Teleportation Mode"] = "", -- Construction_Mode --- ["Teleportation Node: Allows teleportation| between other nodes."] = "", -- Construction_Mode --- ["Teleportation Node"] = "", -- Construction_Mode --- ["Teleport"] = "", -- Construction_Mode, Frenzy --- ["Teleport hint: just use the mouse to select the destination!"] = "", -- A_Classic_Fairytale:dragon --- ["Teleport hint: Just use the mouse to select the destination!"] = "", -- A_Classic_Fairytale:dragon --- ["Teleport to the impact location."] = "", -- Continental_supplies --- ["Teleport to the top of the map, expect fall damage!"] = "", -- Continental_supplies --- ["Teleport unsuccessful. Please teleport within a clan teleporter's sphere of influence."] = "", -- Construction_Mode --- ["Teleport Unsuccessful. Please teleport within a clan teleporter's sphere of influence."] = "", -- Construction_Mode --- ["Tentacle Terror"] = "", -- Tentacle_Terror --- ["Textile industry: Will give you a parachute every second turn."] = "", -- Continental_supplies --- ["Thanks!"] = "", -- A_Classic_Fairytale:family --- ["Thanks, dude! It really means a lot to me."] = "", -- A_Classic_Fairytale:epil --- ["Thanks, man! It really means a lot to me."] = "", -- A_Classic_Fairytale:epil --- ["Thank you, Dr. Cornelius."] = "", -- A_Space_Adventure:cosmos --- ["Thank you for meeting me on such a short notice!"] = "", -- A_Space_Adventure:desert01 --- ["Thank you, my hero!"] = "", -- A_Classic_Fairytale:family --- ["Thank you, oh, thank you, Leaks A Lot!"] = "", -- A_Classic_Fairytale:journey --- ["Thank you, oh, thank you, my heroes!"] = "", -- A_Classic_Fairytale:journey --- ["Thanta"] = "", -- A_Space_Adventure:ice01 --- ["That is, indeed, very weird..."] = "", -- A_Classic_Fairytale:united --- ["That makes it almost invaluable!"] = "", -- A_Classic_Fairytale:enemy --- ["That ought to show them!"] = "", -- A_Classic_Fairytale:backstab --- ["That's all, folks!"] = "", -- A_Classic_Fairytale:epil --- ["That's for my father!"] = "", -- A_Classic_Fairytale:backstab --- ["That shaman sure knows what he's doing!"] = "", -- A_Classic_Fairytale:shadow --- ["That Sinking Feeling"] = "", --- ["That's just the way it works, you know."] = "", -- A_Classic_Fairytale:queen --- ["That's not our problem!"] = "", -- A_Classic_Fairytale:enemy --- ["That's typical of you!"] = "", -- A_Classic_Fairytale:family --- ["That's why he always wears a hat since then."] = "", -- A_Space_Adventure:moon02 --- ["That traitor won't be killing us anymore!"] = "", -- A_Classic_Fairytale:queen --- ["That was just mean!"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united --- ["That was pointless. The flag will respawn next round."] = "", -- CTF_Blizzard --- ["The adventure begins!"] = "", -- A_Space_Adventure:cosmos --- ["The air bombs are weaker than usual."] = "", -- Battalion --- ["The aliens respect me, even worship me!"] = "", -- A_Classic_Fairytale:queen --- ["The ally units share their ammo."] = "", -- A_Space_Adventure:fruit01 --- ["The ammo of %s has been vaporized"] = "", -- Construction_Mode --- ["The answer is ... entertainment. You'll see what I mean."] = "", -- A_Classic_Fairytale:backstab --- ["The answer is...entertaintment. You'll see what I mean."] = "", -- A_Classic_Fairytale:backstab --- ["The anti-portal surface is all over the floor, and I have nothing to kill him. Dropping something could hurt him enough to kill him."] = "", -- portal --- ["The big bang"] = "", -- A_Space_Adventure:final --- ["The Boss"] = "", -- --- ["The boss has fallen! Retreat!"] = "", -- A_Space_Adventure:moon01 --- ["The Bull's Eye"] = "", -- A_Classic_Fairytale:first_blood --- ["The caves are well hidden, they won't find us there!"] = "", -- A_Classic_Fairytale:united --- ["The clan of the Red Strawberry wants to take over the dominion and overthrow King Pineapple."] = "", -- A_Space_Adventure:fruit01 --- ["The continent of cowards"] = "", -- Continental_supplies --- ["The continent of dust"] = "", -- Continental_supplies --- ["The continent of firearms"] = "", -- Continental_supplies --- ["The continent of greed"] = "", -- Continental_supplies --- ["The continent of guerilla tactics"] = "", -- Continental_supplies --- ["The continent of ice and science"] = "", -- Continental_supplies --- ["The continent of medicine"] = "", -- Continental_supplies --- ["The continent of ninjas"] = "", -- Continental_supplies --- ["The continent of sports"] = "", -- Continental_supplies --- ["The Crate Frenzy"] = "", -- A_Classic_Fairytale:first_blood --- ["The Customer is King"] = "", -- Challenge_-_Speed_Shoppa_-_ShoppaKing --- ["The device part has been stolen!"] = "", -- A_Space_Adventure:fruit02 --- ["The device part is hidden in one of the crates! Go and get it!"] = "", -- A_Space_Adventure:desert01 --- ["The Devs"] = "", -- --- ["The Dilemma"] = "", -- A_Classic_Fairytale:shadow --- ["The editor weapons and tools have been added!"] = "", -- HedgeEditor --- ["The editor weapons and tools have been removed!"] = "", -- HedgeEditor --- ["The enemies aren't many anyway, it is going to be easy!"] = "", -- A_Space_Adventure:fruit01 --- ["The enemy can't move but it might be a good idea to stay out of sight!"] = "", -- A_Classic_Fairytale:dragon --- ["The enemy has taken a crate which we really needed!"] = "", -- SimpleMission --- ["The enemy hogs play in a random order."] = "", -- A_Space_Adventure:death02 - ["The enemy is hiding out on yonder ducky!"] = "敌人藏在那边!", --- ["The Enemy Of My Enemy"] = "", -- A_Classic_Fairytale:enemy --- ["The explosion is weaker than usual."] = "", -- Battalion --- ["The fastest hedgehog was %s from %s with a time of %.3fs."] = "", -- TrophyRace --- ["The fight begins!"] = "", -- A_Space_Adventure:moon01 --- ["The final part"] = "", -- A_Space_Adventure:death01 --- ["The final targets are quite tricky. You need to aim well."] = "", -- Basic_Training_-_Bazooka --- ["The First Blood"] = "", -- A_Classic_Fairytale:first_blood --- ["The First Encounter"] = "", -- A_Classic_Fairytale:shadow --- ["The first hedgehog to kill someone becomes the Mutant."] = "", -- Mutant --- ["The first hedgehog which scores %d or more wins the game."] = "", -- Mutant --- ["The first stop"] = "", -- A_Space_Adventure:moon01 --- ["The first turn will last 25 sec and every other turn 15 sec."] = "", -- A_Space_Adventure:fruit03 --- ["The flag will respawn next round."] = --- ["The flood has stopped! Challenge over."] = "", -- User_Mission_-_That_Sinking_Feeling --- ["The food bites back"] = "", -- A_Classic_Fairytale:backstab --- ["The forgotten continent"] = "", -- Continental_supplies --- ["The giant umbrella from the last crate should help break the fall."] = "", -- A_Classic_Fairytale:first_blood --- ["The Great Escape"] = "", -- User_Mission_-_The_Great_Escape --- ["- The green target must survive"] = "", -- HedgeEditor --- ["- The green targets must survive"] = "", -- HedgeEditor --- ["The guardian"] = "", -- A_Classic_Fairytale:shadow --- ["The hardships of the war turned %s (%s) into a killing machine: %d invaders destroyed in one round!"] = "", -- Space_Invasion --- ["The health of your current hedgehog|is shown at the top right corner."] = "", -- Basic_Training_-_Movement --- ["The hedgehog with least points (or most deaths) becomes the Bottom Feeder."] = "", -- Mutant --- ["The Hospital"] = "", -- --- ["The Individualist"] = "", -- A_Classic_Fairytale:shadow --- ["Their buildings were very primitive back then, even for an uncivilised island."] = "", -- A_Classic_Fairytale:united --- ["The Iron Curtain"] = "", -- --- ["The Journey Back"] = "", -- A_Classic_Fairytale:journey --- ["The king of %s has died!"] = "", -- Battalion --- ["The last encounter"] = "", -- A_Space_Adventure:death01 --- ["The last surviving clan wins."] = "", -- TrophyRace --- ["The leader escaped. Defeat the rest of the aliens!"] = "", -- A_Classic_Fairytale:queen --- ["The leader seems scared, he will probably flee."] = "", -- A_Classic_Fairytale:queen --- ["The Leap of Faith"] = "", -- A_Classic_Fairytale:first_blood --- ["The meteorite has come too close and the anti-gravity device isn't powerful enough to stop it now."] = "", -- A_Space_Adventure:cosmos --- ["The Moonwalk"] = "", -- A_Classic_Fairytale:journey --- ["The Mutant has super weapons and a lot of health."] = "", -- Mutant --- ["The Mutant has super-weapons and a lot of health."] = "", -- Mutant --- ["The Mutant loses health quickly, but gains health by killing."] = "", -- Mutant --- ["The Mutant loses health quickly if he doesn't keep scoring kills."] = "", -- Mutant --- ["The Navy greets %s for managing to get in a distance of %d away from the mainland!"] = "", -- ClimbHome --- ["The next 4 times you play the \"The last encounter\" mission you'll get 20 more hit points and a laser sight."] = "", -- A_Space_Adventure:death02 --- ["The next crate is an utility crate."] = "", -- Basic_Training_-_Movement --- ["The next one is pretty hard! |Tip: You have to do multiple swings!"] = "", -- Basic_Training_-_Rope --- ["The next target can only be reached by something called “bouncing bomb”."] = "", -- Basic_Training_-_Bazooka --- ["The next target is high in the sky."] = "", -- Basic_Training_-_Bazooka --- ["Then how do they keep appearing?"] = "", -- A_Classic_Fairytale:shadow --- ["The Ninja-Samurai Alliance"] = "", -- --- ["Then prepare for battle!"] = "", -- A_Space_Adventure:death01 --- ["Then what am I?"] = "", -- A_Classic_Fairytale:epil --- ["The only woman, huh?"] = "", -- A_Classic_Fairytale:epil --- ["The oppression of the elders, of course!"] = "", -- A_Classic_Fairytale:queen --- ["The opression of the elders, of course!"] = "", -- A_Classic_Fairytale:queen --- ["The other hog has died, he should have survived!"] = "", -- A_Space_Adventure:moon02 --- ["The other one were all cannibals, spending their time eating the organs of fellow hedgehogs..."] = "", -- A_Classic_Fairytale:first_blood --- ["The Police"] = "", -- --- ["The power of love! No, wait, the power of the aliens!"] = "", -- A_Classic_Fairytale:queen --- ["The RC plane only carries 2 weak bombs."] = "", -- Battalion --- ["There are a variety of structures available to aid you."] = "", -- Construction_Mode --- ["There are no snarky comments this time."] = "", -- Space_Invasion --- ["There is one below us!"] = "", -- A_Space_Adventure:ice01 --- ["There must be a spy among us!"] = "", -- A_Classic_Fairytale:backstab --- ["There's more of them? When did they become so hungry?"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united --- ["There's nothing more satisfying for me than seeing you share your beauty with the world every morning, my princess!"] = "", -- A_Classic_Fairytale:journey --- ["There's nothing more satisfying to us than seeing you share your beauty..."] = "", -- A_Classic_Fairytale:journey --- ["There's nothing more satisfying to us than seeing you share your beauty with the world every morning, my princess!"] = "", -- A_Classic_Fairytale:journey --- ["The respawner respawns %s"] = "", -- Construction_Mode --- ["The Rising"] = "", -- A_Classic_Fairytale:first_blood --- ["The rope won't get reset."] = "", -- A_Space_Adventure:death02 --- ["The Savior"] = "", -- A_Classic_Fairytale:journey --- ["The score and deaths are shown next to the team bar."] = "", -- Mutant --- ["These girders are slippery, like ice."] = "", -- Basic_Training_-_Movement --- ["These primitive people are so funny!"] = "", -- A_Classic_Fairytale:backstab --- ["These weapon specials cannot be used close to other hogs."] = "", -- Continental_supplies --- ["The Shadow Falls"] = "", -- A_Classic_Fairytale:shadow --- ["The Showdown"] = "", -- A_Classic_Fairytale:shadow --- ["The Slaughter"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:first_blood --- ["The Society of Perfectionists greets %s (%s): No misses and %d hits in its best round."] = "", -- Space_Invasion --- ["The Specialists: Each hedgehog starts with its own weapon set"] = "", -- The_Specialists --- ["The spinning arrows above your hedgehog show|which hedgehog is selected right now."] = "", -- Basic_Training_-_Movement --- ["The spirits of the ancerstors are surely pleased, Leaks A Lot."] = "", -- A_Classic_Fairytale:first_blood --- ["The spirits of the ancestors are surely pleased, Leaks A Lot."] = "", -- A_Classic_Fairytale:first_blood --- ["The targets will guide you through the training."] = "", -- Basic_Training_-_Rope --- ["The team continued their quest of finding the rest of the tribe."] = "", -- A_Classic_Fairytale:queen --- ["The teams are tied for the fastest time."] = "", -- Racer, TechRacer --- ["The teams were tied, so an additional round has been played to determine the winner."] = "", -- Space_Invasion --- ["The teams were tied, so %d additional rounds have been played to determine the winner."] = "", -- Space_Invasion --- ["The time that you have left when you reach the blue hedgehog will be added to the next turn."] = "", -- A_Space_Adventure:moon02 --- ["The Torment"] = "", -- A_Classic_Fairytale:first_blood --- ["The truth about Professor Hogevil"] = "", -- A_Space_Adventure:moon02 --- ["The tunnel entrance is over there."] = "", -- A_Space_Adventure:desert01 --- ["The tunnel is about to get flooded!"] = "", -- A_Space_Adventure:desert02 --- ["The Tunnel Maker"] = "", -- A_Classic_Fairytale:journey --- ["The Ultimate Weapon"] = "", -- A_Classic_Fairytale:first_blood --- ["The Union"] = "", -- A_Classic_Fairytale:enemy --- ["The Union: You can select a hedgehog at the start of your turns."] = "", -- Continental_supplies --- ["The village, unprepared, was destroyed by the cyborgs..."] = "", -- A_Classic_Fairytale:journey --- ["The walk of Fame"] = "", -- A_Classic_Fairytale:shadow --- ["The wasted youth"] = "", -- A_Classic_Fairytale:first_blood --- ["The way you handled your little internal conflicts …"] = "", -- A_Classic_Fairytale:queen --- ["The weapon in that last crate was bestowed upon us by the ancients!"] = "", -- A_Classic_Fairytale:first_blood --- ["The what?!"] = "", -- A_Classic_Fairytale:dragon --- ["The wind whispers that you are ready to become familiar with tools, now..."] = "", -- A_Classic_Fairytale:first_blood --- ["The wrong hedgehog has taken the crate."] = "", -- SimpleMission --- ["They are all waiting back in the village, haha."] = "", -- A_Classic_Fairytale:enemy --- ["They are up there! Take this rope and hurry!"] = "", -- A_Space_Adventure:moon01 --- ["They Call Me Bullseye! +16 points!"] = "", -- Space_Invasion --- ["They have weapons we've never seen before!"] = "", -- A_Classic_Fairytale:united --- ["They keep appearing like this. It's weird!"] = "", -- A_Classic_Fairytale:united --- ["They killed %s! You bastards!"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united --- ["They must be trying to weaken us!"] = "", -- A_Classic_Fairytale:enemy --- ["They never learn"] = "", -- A_Classic_Fairytale:journey --- ["They stumbled upon a pile of weapons, they seemed to be getting closer."] = "", -- A_Classic_Fairytale:queen --- ["They told us to wear these clothes. They said that this is the newest trend."] = "", -- A_Classic_Fairytale:enemy --- ["They've been manipulating us all this time!"] = "", -- A_Classic_Fairytale:enemy --- ["They won't hesitate to attack you in order to rob you!"] = "", -- A_Space_Adventure:desert01 --- ["The Zoo"] = "", -- --- ["Thighlicker"] = "", -- A_Classic_Fairytale:united --- ["Things are going to get messy around here."] = "", -- A_Space_Adventure:fruit01 --- ["This allows to select any hedgehog in your team!"] = "", -- Basic_Training_-_Movement --- ["This allows you to create a crate anywhere|within your clan's area of influence,|at the cost of energy."] = "", -- Construction_Mode --- ["This allows you to create and place mines,|sticky mines and barrels anywhere within your|clan's area of influence at the cost of energy."] = "", -- Construction_Mode --- ["This allows you to create and place mines,|sticky mines and barrels anywhere within your|clan's area of influence at the cost of energy.|Up/down: Choose object type|Left/right: Choose timer (for mines)|Cursor: Place object"] = "", -- Construction_Mode --- ["This almost concludes our tutorial."] = "", -- Basic_Training_-_Flying_Saucer --- ["This also increases the effectiveness of Medicine."] = "", -- Continental_supplies --- ["This game wasn’t really exciting."] = "", -- Space_Invasion --- ["This is a new personal best, congratulations!"] = "", -- A_Space_Adventure:death02, A_Space_Adventure:desert02, A_Space_Adventure:fruit03 --- ["This is a new personal best time, congratulations!"] = "", -- A_Space_Adventure:ice02, A_Space_Adventure:moon02 --- ["This is Cappy."] = "", -- Basic_Training_-_Movement --- ["This is it! It's time to make Fell From Heaven fall for me..."] = "", -- A_Classic_Fairytale:first_blood --- ["This island is the only place left on Earth with grass on it!"] = "", -- A_Classic_Fairytale:enemy --- ["This is seems like a wealthy hedgehog, nice ..."] = "", -- A_Space_Adventure:desert01 --- ["This is the mission panel."] = "", -- Basic_Training_-_Movement --- ["This is the Olympic stadium of saucer flying."] = "", -- A_Space_Adventure:ice02 --- ["This is the Olympic Stadium of Saucer Flying."] = "", -- A_Space_Adventure:ice02 --- ["This is typical!"] = "", -- A_Classic_Fairytale:dragon --- ["This must be some kind of sorcery!"] = "", -- A_Classic_Fairytale:shadow --- ["This must be the caves!"] = "", -- A_Classic_Fairytale:backstab --- ["This one's tricky."] = "", --- ["This planet seems dangerous!"] = "", -- A_Space_Adventure:cosmos --- ["This rain is really something..."] = "", --- ["This round’s award for ultimate disappointment goes to: Everyone!"] = "", -- ClimbHome --- ["This seems like a wealthy hedgehog, nice ..."] = "", -- A_Space_Adventure:desert01 --- ["This %s is so naive! I'm going to shoot this fool so I can keep that device for myself!"] = "", -- A_Space_Adventure:fruit02 --- ["This was an awesome performance! But this challenge can be finished with even just one RC plane. Can you figure out how?"] = "", -- User_Mission_-_RCPlane_Challenge --- ["This will be fun!"] = "", -- A_Classic_Fairytale:enemy --- ["This will be useful when I need a new platform or if I want to rise."] = "", -- portal --- ["This will certainly come in handy."] = "", -- User_Mission_-_Teamwork_2 --- ["This will certianly come in handy."] = "", -- User_Mission_-_Teamwork_2 --- ["Thompson"] = "", -- --- ["Those aliens are destroying the island!"] = "", -- A_Classic_Fairytale:family --- ["Those were scheduled for disposal anyway."] = "", -- A_Classic_Fairytale:dragon --- ["Throw a 1 second mine!"] = "", -- Continental_supplies --- ["Throw a baseball at your foes|and send them flying!"] = "", -- Knockball --- ["Throw a grenade to destroy the target!"] = "", -- Basic_Training_-_Grenade --- ["Throw some grenades to destroy the targets!"] = "", -- Basic_Training_-_Grenade --- ["Thug #%d"] = "", -- A_Space_Adventure:death01 --- ["Tie-breaking round %d"] = "", -- Space_Invasion --- ["Timbers"] = "", -- --- ["Time: %.1fs"] = "", -- Racer, TechRacer --- ["Time: %.3fs by %s"] = "", -- TrophyRace --- ["Time: %.3fs"] = "", -- TrophyRace --- ["Time Box"] = "", -- Construction_Mode --- ["Timed Kamikaze! +10 points!"] = "", -- Space_Invasion --- ["Time extended! +%dsec"] = "", -- Space_Invasion --- ["Time extension: %ds"] = "", -- Tumbler --- ["Time for a more interesting stunt, but first just collect the next crate!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Timer"] = "", -- Basic_Training_-_Grenade --- ["Time's up!"] = "", -- Basic_Training_-_Sniper_Rifle, SpeedShoppa, Space_Invasion --- ["Time’s up!"] = "", -- TargetPractice --- ["Time to run!"] = "", -- A_Space_Adventure:fruit01 --- ["Tip: Changing your aim while flying is very difficult, so adjust it before you take off."] = "", -- Basic_Training_-_Flying_Saucer --- ["Tip: Don't remain for too long in the water, or you won't make it."] = "", -- Basic_Training_-_Flying_Saucer --- ["Tip: If you get stuck in this training, use \"Skip turn\" to restart the current objective."] = "", -- Basic_Training_-_Flying_Saucer --- ["Tip: See the \"esc\" key (this menu) if you want to see the currently playing teams continent, or that continents specials."] = "", -- Continental_supplies --- ["Tip: See the \"Esc\" key (this menu) if you want to see the currently playing teams continent, or that continents specials."] = "", -- Continental_supplies --- ["Tip: The rope physics are different than in the real world, |use it to your advantage!"] = "", -- Basic_Training_-_Rope --- ["Tip: You can change your flying saucer|in mid-flight by hitting the [Attack] key twice."] = "", -- Basic_Training_-_Flying_Saucer --- ["Tiyuri"] = "", -- --- ["Toad"] = "", -- --- ["To begin, walk to the crate to the right."] = "", -- Basic_Training_-_Movement --- ["To begin with the training, hit the attack key!"] = "", -- Basic_Training_-_Movement --- ["To begin with the training, select the bazooka from the ammo menu!"] = "", -- Basic_Training_-_Bazooka --- ["To begin with the training, select the grenade from the ammo menu!"] = "", -- Basic_Training_-_Grenade --- ["To begin with the training, tap the attack button!"] = "", -- Basic_Training_-_Movement --- ["To finish hedgehog selection, just do anything|with him, like walking."] = "", -- Basic_Training_-_Movement --- ["To get over the next obstacles, keep some distance from the wall before you back jump."] = "", -- Basic_Training_-_Movement --- ["To get over the water, you have to do multiple|rope shots and swings."] = "", -- Basic_Training_-_Rope --- ["Toggle Editing Weapons and Tools: [Precise]+[2]"] = "", -- HedgeEditor --- ["Toggle Help: [Precise]+[1]"] = "", -- HedgeEditor --- ["Toggle Placement/Deletion: [Left], [Right]"] = "", -- HedgeEditor --- ["Toggle Shield: [Long jump]"] = "", -- Space_Invasion --- ["To help you, of course!"] = "", -- A_Classic_Fairytale:journey --- ["To launch a projectile in mid-flight, hold [Precise] and press [Long jump]."] = "", -- Basic_Training_-_Flying_Saucer --- ["Tony"] = "", -- --- ["Too bad! Then you should really leave!"] = "", -- A_Space_Adventure:fruit01 --- ["Too slow! Try again ..."] = "", -- A_Space_Adventure:moon02 --- ["Top-class elite pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["To reach higher ground, walk to a ledge, look to the left, then do a back jump."] = "", -- Basic_Training_-_Movement --- ["Torn Muscle"] = "", -- A_Classic_Fairytale:journey --- ["To the caves..."] = "", -- A_Classic_Fairytale:united --- ["Touch all waypoints as fast as you can!"] = "", -- Racer --- ["- Touch the sparkles near your base to teleport"] = "", -- CTF_Blizzard --- ["To win the game, %s has to get the bottom crates and come back to the surface."] = "", -- A_Space_Adventure:fruit02 --- ["To win the game you had to collect the 2 crates with no specific order."] = "", -- A_Space_Adventure:desert01 --- ["To win the game you have to eliminate Professor Hogevil."] = "", -- A_Space_Adventure:death01 --- ["To win the game you have to find the right crate."] = "", -- A_Space_Adventure:desert01 --- ["To win the game you have to go next to Thanta."] = "", -- A_Space_Adventure:ice01 --- ["To win the game you have to go to the surface."] = "", -- A_Space_Adventure:desert02 --- ["To win the game you have to pass into the rings in time."] = "", -- A_Space_Adventure:ice02 --- ["To win the game you have to stand next to Thanta."] = "", -- A_Space_Adventure:ice01 - ["Toxic Team"] = "腐坏的队伍", -- User_Mission_-_Diver, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork --- ["Track completed!"] = "", -- Racer, TechRacer --- ["Training"] = "", -- Basic_Training_-_Flying_Saucer, Basic_Training_-_Rope --- ["Training complete!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Traitors"] = "", -- A_Classic_Fairytale:epil --- ["Traitors don't get to shout around here!"] = "", -- A_Classic_Fairytale:epil --- ["Trapper"] = "", -- HedgeEditor --- ["Travel carefully as your fuel is limited"] = "", -- A_Space_Adventure:cosmos --- ["Travel to all the neighbor planets and collect all the pieces"] = "", -- A_Space_Adventure:cosmos --- ["Treasure: Massive weapon bonus in first turn."] = "", -- Continental_supplies --- ["Tribe"] = "", -- A_Classic_Fairytale:backstab - ["TrophyRace"] = "竞速", --- ["Trunks"] = "", -- --- ["Try again!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Try it now and dive here to collect the crate on the right girder."] = "", -- Basic_Training_-_Flying_Saucer --- ["Try not to get spotted by the guards!"] = "", -- A_Space_Adventure:cosmos --- ["Try out different bounciness levels to reach difficult targets."] = "", -- Basic_Training_-_Grenade --- ["Try to be smart and eliminate them quickly. This way you might scare off the rest!"] = "", -- A_Space_Adventure:fruit01 --- ["Try to keep as many allies alive as possible."] = "", -- A_Space_Adventure:fruit01 --- ["Try to land softly, as you can still take fall damage!"] = "", -- Basic_Training_-_Flying_Saucer --- ["Try to protect the chief! You won't lose if he dies, but it is advised that he survives."] = "", -- A_Classic_Fairytale:united --- ["Try to reach and destroy the next target quickly."] = "", -- Basic_Training_-_Rope --- ["Tumbler"] = "", -- Tumbler --- ["Turn around: [Left Shift] + [Left]/[Right]"] = "", -- Basic_Training_-_Movement --- ["Turning Around"] = "", -- Basic_Training_-_Movement --- ["Turns: Hogs get %d random weapon(s) from their pool"] = "", -- Battalion --- ["Turns: King's health is set to %d%% of the team health"] = "", -- Battalion --- ["Turns left: %d"] = "", -- A_Classic_Fairytale:journey --- ["Turns: Refill %d weapon and %d helper points|and randomize weapons and helpers based on team points"] = "", -- Battalion --- ["Turns until arrival: %d"] = "", -- A_Classic_Fairytale:backstab --- ["Turn Time: %dsec"] = "", -- Space_Invasion --- ["Twenty-Twenty"] = "", -- --- ["Two flowers: All missions complete"] = "", -- A_Space_Adventure:cosmos --- ["Two little hogs cooperating, getting past obstacles..."] = "", -- A_Classic_Fairytale:journey --- ["Ugly Mug"] = "", -- --- ["Uhm...I met one of them and took his weapons."] = "", -- A_Classic_Fairytale:shadow --- ["Uhmm, it's … uhm … my ring!"] = "", -- A_Classic_Fairytale:queen --- ["Uhmm...ok no."] = "", -- A_Classic_Fairytale:enemy --- ["Ukemi"] = "", -- --- ["Ultra kill!"] = "", -- Mutant --- ["unC0Rr"] = "", -- --- ["Under Construction"] = "", -- A_Classic_Fairytale:shadow --- ["Under normal circumstances we could easily defeat them but we have kindly sent most of our men to the Kingdom of Sand to help with the annual dusting of the king's palace."] = "", -- A_Space_Adventure:fruit01 --- ["Under the meteorite’s shadow ..."] = "", -- A_Space_Adventure:cosmos --- ["Under the meteorites shadow ..."] = "", -- A_Space_Adventure:cosmos --- ["Unexpected Igor"] = "", -- A_Classic_Fairytale:dragon --- ["Unique new weapons"] = "", -- Continental_supplies --- ["Unit"] = "", --- ["Unit 0x0007"] = "", -- A_Classic_Fairytale:family --- ["Unit 189"] = "", -- --- ["Unit 234"] = "", -- --- ["Unit 333"] = "", -- --- ["Unit 334a$7%;.*"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:united - ["Unit 3378"] = "3378", --- ["Unit 485"] = "", -- --- ["Unit 527"] = "", -- --- ["Unit 638"] = "", -- --- ["Unit 709"] = "", -- --- ["Unit 835"] = "", --- ["Unit 881"] = "", -- User_Mission_-_Newton_and_the_Hammock --- ["Unit 883"] = "", -- --- ["United We Stand"] = "", -- A_Classic_Fairytale:united --- ["Unlike bazookas, grenades are not influenced by wind."] = "", -- Basic_Training_-_Grenade --- ["Unlimited Attacks: Attacks don't end your turn"] = "", -- User_Mission_-_Diver, User_Mission_-_Nobody_Laugh, User_Mission_-_Spooky_Tree --- ["Unlucky Sods"] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Unstoppable!"] = "", --- ["Unsuspecting Louts"] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Up/Down: Adjust dust storm damage"] = "", -- Continental_supplies --- ["Up/Down: Browse through continents"] = "", -- Continental_supplies --- ["Up/Down: Change placement mode"] = "", -- HedgeEditor --- ["Up/down: Choose crate type"] = "", -- Construction_Mode --- ["Up/down: Choose object type|1-5/Switch/Left/Right: Choose mine timer|Cursor: Place object"] = "", -- Construction_Mode --- ["Upper-class elite pilot"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Upside-Down World"] = "", -- Continental_supplies --- ["Use it wisely!"] = "", -- A_Classic_Fairytale:dragon --- ["Use it with precaution!"] = "", -- A_Classic_Fairytale:first_blood --- ["User Challenge"] = "", --- ["!"] = "", -- User_Mission_-_Dangerous_Ducklings --- ["User Mission"] = "", -- HedgeEditor --- ["Use the attack key twice to change the flying saucer while being in air."] = "", -- A_Space_Adventure:ice02 --- ["Use the attack key twice to change the flying saucer while floating in mid-air."] = "", -- A_Space_Adventure:ice02 --- ["Use the bazooka and the flying saucer to get the freezer."] = "", -- A_Space_Adventure:ice01 --- ["Use the flying saucer from the crate to fly to the moon."] = "", -- A_Space_Adventure:cosmos --- ["Use the flying saucer to fly the other planets."] = "", -- A_Space_Adventure:cosmos --- ["Use the flying saucer to fly to the other planets."] = "", -- A_Space_Adventure:cosmos --- ["Use the parachute to get the next crate."] = "", -- A_Classic_Fairytale:first_blood --- ["Use the portal gun to get to the next crate, then use the new gun to get to the final destination!|"] = "", -- A_Classic_Fairytale:dragon --- ["Use the RC plane and destroy the all the targets."] = "", -- A_Space_Adventure:desert03 --- ["Use the rope in order to catch the blue hedgehog"] = "", -- A_Space_Adventure:moon02 --- ["Use the rope to complete the obstacle course!"] = "", -- Basic_Training_-_Rope --- ["Use the rope to get on the head of the mole, young one!"] = "", -- A_Classic_Fairytale:first_blood --- ["Use the rope to get to the crate"] = "", -- A_Space_Adventure:cosmos --- ["Use the rope to get to the target!"] = "", -- Basic_Training_-_Rope --- ["Use the rope to knock your enemies to their doom."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["Use the rope to quickly get to the surface!"] = "", -- A_Space_Adventure:desert02 --- ["Use the saucer and fly away"] = "", -- A_Space_Adventure:cosmos --- ["Use the saucer and fly to the moon"] = "", -- A_Space_Adventure:cosmos --- ["Use the shield to protect yourself from bazookas."] = "", -- Space_Invasion --- ["Use the structure placer to place structures."] = "", -- Construction_Mode --- ["Use your ammo wisely."] = "", -- A_Space_Adventure:desert01 --- ["Use your available weapons in order to eliminate the enemies."] = "", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 --- ["Use your ready time to think."] = "", -- Frenzy --- ["Use your rope to collect all crates as fast as possible."] = "", -- SpeedShoppa - ["Use your rope to get from start to finish as fast as you can!"] = "抓起绳子飞向目的地,越快越好。", --- ["Use your rope to get to the next target, then destroy it!"] = "", -- Basic_Training_-_Rope --- ["Utility Crate Placement Mode"] = "", -- Construction_Mode --- ["UTILITY CRATE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Utility crates extend your time."] = "", -- Tumbler --- ["Variants: Hogs will be randomized from 12 different variants"] = "", -- Battalion --- ["Variants: Kings and air generals are disabled"] = "", -- Battalion --- ["Variants: The last hog of each team will be a king"] = "", -- Battalion --- ["Vedgies"] = "", -- A_Classic_Fairytale:journey --- ["Vega"] = "", -- --- ["Vegan Jack"] = "", -- A_Classic_Fairytale:enemy --- ["Very valuable, haha!"] = "", -- A_Classic_Fairytale:queen --- ["Victory!"] = "", -- Basic_Training_-_Rope --- ["Victory Condition: Collect"] = "", -- HedgeEditor --- ["Victory Condition: Destroy"] = "", -- HedgeEditor --- ["Victory for %s!"] = "", -- Capture_the_Flag --- ["Violence is not the answer to your problems!"] = "", -- A_Classic_Fairytale:first_blood --- ["Visit the planets of Ice, Desert and Fruit before you proceed to the Death Planet"] = "", -- A_Space_Adventure:cosmos --- ["Vladimir"] = "", -- --- ["Void"] = "", -- Big_Armory --- ["Voldemort"] = "", -- portal --- ["Voltorb"] = "", -- --- ["Wait a moment …"] = "", -- A_Space_Adventure:final --- ["Walking on Ice"] = "", -- Basic_Training_-_Movement --- ["Walk: [Left] and [Right]"] = "", -- Basic_Training_-_Movement --- ["Walk: [Left]/[Right]"] = "", -- Basic_Training_-_Bazooka --- ["Wall Before Crate: You must touch the marked wall before you can get crates."] = "", -- WxW --- ["Walls Before Crate: You must touch the %d marked walls before you can get crates."] = "", -- WxW --- ["Wall set: No walls"] = "", -- WxW --- ["Wall set: %s (%d walls)"] = "", -- WxW --- ["Wall set: %s"] = "", -- WxW --- ["Walls left: %d"] = "", -- WxW --- ["Wall to wall"] = "", -- WxW --- ["Waluigi"] = "", -- --- ["Wario"] = "", -- --- ["Warming Up"] = "", -- Basic_Training_-_Grenade --- ["Warning: Fire cake detected"] = "", -- ClimbHome --- ["Warning: Never ever leave the flying saucer while in water!"] = "", -- Basic_Training_-_Flying_Saucer --- ["WARNING: Sabotage detected!"] = "", -- Continental_supplies --- ["Warrior"] = "", -- Battalion --- [" was extracted from the scheme|- This continent will be able to use the specials from the other continents!"] = "", -- Continental_supplies --- ["WatchBot 4000"] = "", -- User_Mission_-_Teamwork_2 --- ["Watch your steps, young one!"] = "", -- A_Classic_Fairytale:first_blood --- ["Watermelon Heart"] = "", -- A_Space_Adventure:fruit02 --- ["Water: Rises by 37 per turn"] = "", -- Battalion --- ["Waypoint Editing Mode"] = "", -- HedgeEditor --- ["WAYPOINT EDITING MODE"] = "", -- HedgeEditor --- ["Waypoint placed. Available points remaining: %d"] = "", -- Racer --- ["Waypoint placement phase"] = "", -- Racer --- ["Waypoint removed. Available points: %d"] = "", -- Racer --- ["Waypoints remaining: %d"] = "", -- Racer, TechRacer --- ["Weaklings"] = "", -- A_Classic_Fairytale:shadow --- ["We all know what happens when you get frightened..."] = "", -- A_Classic_Fairytale:first_blood --- ["Weapon Crate Placement Mode"] = "", -- Construction_Mode --- ["WEAPON CRATE PLACEMENT MODE"] = "", -- HedgeEditor --- ["Weapon Filter"] = "", -- Construction_Mode --- ["Weapon Filter: Dematerializes all ammo| carried by enemies entering it."] = "", -- Construction_Mode --- ["weaponschemes"] = "", -- Continental_supplies --- ["Weapons: Each team starts with %d weapon points"] = "", -- Battalion --- ["Weapons: Hogs will get 1 out of 3 weapons randomly each turn"] = "", -- Battalion --- ["Weapons: Nearly every hog variant gets 1 kamikaze"] = "", -- Battalion --- ["Weapon specials: Some weapons have special modes (see weapon description)."] = "", -- Continental_supplies --- ["Weapons reset: The weapons are reset after each turn."] = "", -- WxW --- ["We are indeed."] = "", -- A_Classic_Fairytale:backstab --- ["We can't defeat them!"] = "", -- A_Classic_Fairytale:shadow --- ["We can't hold them up much longer!"] = "", -- A_Classic_Fairytale:united --- ["We can't let them take over our little island!"] = "", -- A_Classic_Fairytale:enemy --- ["We come in peace! Just let our friends go!"] = "", -- A_Classic_Fairytale:queen --- ["We could just have blown up the meteorite from the the beginning!"] = "", -- A_Space_Adventure:final --- ["We don't have time for that now!"] = "", -- A_Classic_Fairytale:queen --- ["We have lost an object which was critical to this mission."] = "", -- SimpleMission --- ["We have no time to waste..."] = "", -- A_Classic_Fairytale:journey --- ["We have nowhere else to live!"] = "", -- A_Classic_Fairytale:enemy --- ["We have spotted the enemy! We'll attack when the enemies start gathering!"] = "", -- A_Space_Adventure:fruit02 --- ["We have to find our folk!"] = "", -- A_Classic_Fairytale:queen --- ["We have to hurry! Are you armed?"] = "", -- A_Space_Adventure:moon01 --- ["We have to protect the village!"] = "", -- A_Classic_Fairytale:united --- ["We have to unite and defeat those cylergs!"] = "", -- A_Classic_Fairytale:enemy --- ["Welcome home! Please take a seat"] = "", -- ClimbHome --- ["Welcome, Leaks A Lot!"] = "", -- A_Classic_Fairytale:journey --- ["Welcome, %s, surprised to see me?"] = "", -- A_Space_Adventure:death01 --- ["Welcome to the Death Planet!"] = "", -- A_Space_Adventure:cosmos --- ["Welcome to the Desert Planet!"] = "", -- A_Space_Adventure:cosmos --- ["Welcome to the Fruit Planet!"] = "", -- A_Space_Adventure:cosmos --- ["Welcome to the meteorite!"] = "", -- A_Space_Adventure:cosmos --- ["Welcome to the moon!"] = "", -- A_Space_Adventure:cosmos --- ["Welcome to the Planet of Ice!"] = "", -- A_Space_Adventure:cosmos --- ["Well done."] = "", --- ["Well done! Let's destroy the next target!"] = "", -- Basic_Training_-_Rope --- ["Well done! The next target awaits."] = "", -- Basic_Training_-_Rope --- ["We'll give you a problem then!"] = "", -- A_Classic_Fairytale:enemy --- ["We'll play a game first."] = "", -- A_Space_Adventure:moon02 --- ["We'll spare your life for now!"] = "", -- A_Classic_Fairytale:backstab --- ["Well, that escalated quickly!"] = "", -- ClimbHome --- ["Well that was an unnecessary act of violence."] = "", -- A_Classic_Fairytale:epil --- ["Well, that was an unnecessary act of violence."] = "", -- A_Classic_Fairytale:epil --- ["Well, that was a waste of time."] = "", -- A_Classic_Fairytale:dragon --- ["We'll use our communicators to contact you."] = "", -- A_Space_Adventure:cosmos --- ["Well, well! Isn't that the cutest thing you've ever seen?"] = "", -- A_Classic_Fairytale:journey --- ["Well, yes. This was a cyborg television show."] = "", -- A_Classic_Fairytale:enemy --- ["Well, you're about to wake up!"] = "", -- A_Classic_Fairytale:queen --- ["We made sure noone followed us!"] = "", -- A_Classic_Fairytale:backstab --- ["We need it to get split into at least two parts."] = "", -- A_Space_Adventure:cosmos --- ["We need to go back!"] = "", -- A_Classic_Fairytale:queen --- ["We need to hurry!"] = "", -- A_Classic_Fairytale:queen --- ["We need to move!"] = "", -- A_Classic_Fairytale:united --- ["We need to prevent their arrival!"] = "", -- A_Classic_Fairytale:backstab --- ["We need to warn the village."] = "", -- A_Classic_Fairytale:shadow --- ["We need you to go there and detonate them yourself! Good luck!"] = "", -- A_Space_Adventure:cosmos --- ["We oppressed her, the only woman in the tribe!"] = "", -- A_Classic_Fairytale:epil --- ["We're terribly sorry!"] = "", -- A_Classic_Fairytale:epil --- ["We risk our lives going through challenges."] = "", -- A_Classic_Fairytale:queen --- ["We should better report this and continue our watch!"] = "", -- A_Space_Adventure:cosmos --- ["We should head back to the village now."] = "", -- A_Classic_Fairytale:shadow --- ["We, the youth, have to constantly prove our value."] = "", -- A_Classic_Fairytale:queen --- ["We trusted you, you fool!"] = "", -- A_Classic_Fairytale:queen --- ["We were trying to save her and we got lost."] = "", -- A_Classic_Fairytale:family --- ["We were your home! Your family!"] = "", -- A_Classic_Fairytale:queen --- ["We won't accept you destroying our village!"] = "", -- A_Classic_Fairytale:queen --- ["We won't let you hurt any more of us!"] = "", -- A_Classic_Fairytale:queen --- ["We won't let you hurt her!"] = "", -- A_Classic_Fairytale:journey --- ["We work and work until we sweat blood."] = "", -- A_Classic_Fairytale:queen --- ["What?! A cannibal? Here? There is no time to waste! Come, you are prepared."] = "", -- A_Classic_Fairytale:first_blood --- ["What?!"] = "", -- A_Classic_Fairytale:queen --- ["What a douche!"] = "", -- A_Classic_Fairytale:enemy --- ["What am I gonna...eat, yo?"] = "", -- A_Classic_Fairytale:family --- ["What are you doing at a distance so great, young one?"] = "", -- A_Classic_Fairytale:first_blood --- ["What are you doing? Let her go!"] = "", -- A_Classic_Fairytale:journey --- ["What a ride!"] = "", -- A_Classic_Fairytale:shadow --- ["What a strange cave!"] = "", -- A_Classic_Fairytale:dragon --- ["What a strange feeling!"] = "", -- A_Classic_Fairytale:backstab --- ["What could you possibly forget in that cage?"] = "", -- A_Classic_Fairytale:queen --- ["What does it look like?"] = "", -- A_Classic_Fairytale:queen --- ["What do my faulty eyes observe? A spy!"] = "", -- A_Classic_Fairytale:first_blood --- ["What do you say? Are you in?"] = "", -- A_Space_Adventure:fruit02 --- ["What do you say? Will you fight for us?"] = "", -- A_Space_Adventure:fruit01 --- ["What do you want to do?"] = "", -- A_Space_Adventure:fruit01 --- ["Whatever floats your boat..."] = "", -- A_Classic_Fairytale:shadow --- ["What?! For all this struggle I just win some ... time? Oh dear!"] = "", -- portal --- ["What has %s ever done to you?"] = "", -- A_Classic_Fairytale:backstab --- ["What? Here? How did they find us?!"] = "", -- A_Classic_Fairytale:backstab --- ["What? Is it over already?"] = "", -- ClimbHome --- ["What is it that you forgot?"] = "", -- A_Classic_Fairytale:queen --- ["What is this place?"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy --- ["What oppression? You were the most unoppressed member of the tribe!"] = "", -- A_Classic_Fairytale:queen --- ["What shall we do with the traitor?"] = "", -- A_Classic_Fairytale:backstab --- ["What's in the box, you ask? Let's find out!"] = "", -- Basic_Training_-_Movement --- ["What the?"] = "", -- A_Classic_Fairytale:queen --- ["WHAT?! You're the ones attacking us!"] = "", -- A_Classic_Fairytale:enemy --- ["When?"] = "", -- A_Classic_Fairytale:enemy --- ["When I find it..."] = "", -- A_Classic_Fairytale:dragon --- ["When you're in mid-air, you can continue to aim|and fire another rope if you're not attached."] = "", -- Basic_Training_-_Rope --- ["Where are all these crates coming from?!"] = "", -- A_Classic_Fairytale:shadow --- ["Where are they?!"] = "", -- A_Classic_Fairytale:backstab --- ["Where did that alien run?"] = "", -- A_Classic_Fairytale:dragon --- ["Where did you get the exploding apples?"] = "", -- A_Classic_Fairytale:shadow --- ["Where did you get the exploding apples and the magic bow that shoots many arrows?"] = "", -- A_Classic_Fairytale:shadow --- ["Where did you get the magic bow that shoots many arrows?"] = "", -- A_Classic_Fairytale:shadow --- ["Where did you get the weapons in the forest, Dense Cloud?"] = "", -- A_Classic_Fairytale:backstab --- ["Where do you get that?!"] = "", -- A_Classic_Fairytale:enemy --- ["Where have you been?!"] = "", -- A_Classic_Fairytale:backstab --- ["Where have you been?"] = "", -- A_Classic_Fairytale:united --- ["While in modification mode, you can change|the LandFlag by clicking on an object."] = "", -- HedgeEditor --- ["White Tee"] = "", -- A_Space_Adventure:ice01 --- ["Who's there?! I'll get you!"] = "", -- A_Space_Adventure:desert01 --- ["Why?"] = "", -- A_Classic_Fairytale:queen --- ["Why are you doing this?"] = "", -- A_Classic_Fairytale:journey --- ["Why are you helping us, uhm...?"] = "", -- A_Classic_Fairytale:family --- ["Why can't he just let her go?!"] = "", -- A_Classic_Fairytale:family --- ["… why did I risk my life to collect all the parts of the anti-gravity device?"] = "", -- A_Space_Adventure:final --- ["Why did you do this?"] = "", -- A_Classic_Fairytale:queen --- ["Why did you kill your father?"] = "", -- A_Classic_Fairytale:queen --- ["Why do men keep hurting me?"] = "", -- A_Classic_Fairytale:first_blood --- ["Why do you always have to call me names?"] = "", -- A_Classic_Fairytale:queen --- ["Why do you keep betraying us?"] = "", -- A_Classic_Fairytale:queen --- ["Why do you not like me?"] = "", -- A_Classic_Fairytale:shadow --- ["Why do you want to take over our island?"] = "", -- A_Classic_Fairytale:enemy --- ["Why me?!"] = "", -- A_Classic_Fairytale:backstab --- ["Why %s? Why?"] = "", -- A_Classic_Fairytale:backstab --- ["Why, why, why, why!"] = "", -- A_Classic_Fairytale:queen --- ["Why would they do this?"] = "", -- A_Classic_Fairytale:backstab --- ["- Will get 1-3 random weapons"] = "", -- Continental_supplies --- ["- Will Get 1-3 random weapons"] = "", -- Continental_supplies --- ["- Will give you a parachute every second turn."] = "", -- Continental_supplies --- ["Will this ever end?"] = "", --- ["Will you give me the other parts?"] = "", -- A_Space_Adventure:death01 --- ["Win"] = "", -- A_Space_Adventure:ice01 --- ["Wind"] = "", -- Basic_Training_-_Bazooka --- ["Winner: %s"] = "", -- Mutant --- ["Winning time: %s"] = "", -- Racer, TechRacer --- ["Wise Oak"] = "", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen --- ["With Dense Cloud on the land of shadows, I'm the village's only hope..."] = "", -- A_Classic_Fairytale:journey --- ["With low bounciness, it barely bounces at all, but it is much more predictable."] = "", -- Basic_Training_-_Grenade --- ["With the rest of the tribe gone, it was up to %s to save the village."] = "", -- A_Classic_Fairytale:dragon --- ["Worry not, for it is a peaceful animal! There is no reason to be afraid..."] = "", -- A_Classic_Fairytale:first_blood --- ["Wow, what a dream!"] = "", -- A_Classic_Fairytale:backstab --- ["Xeli"] = "", -- --- ["Xerxes"] = "", -- --- ["Y3K1337"] = "", -- A_Classic_Fairytale:journey, A_Classic_Fairytale:shadow --- ["Yay, we won!"] = "", -- A_Classic_Fairytale:enemy --- ["Y Chwiliad"] = "", -- A_Classic_Fairytale:dragon --- ["Yeah...I think it's a 'he', lol."] = "", -- A_Classic_Fairytale:shadow --- ["Yeah, sure! I died. Hilarious!"] = "", -- A_Classic_Fairytale:backstab --- ["Yeah, sure! I died. Hillarious!"] = "", -- A_Classic_Fairytale:backstab --- ["Yeah, take that!"] = "", -- A_Classic_Fairytale:dragon --- ["Yeah? Watcha gonna do? Cry?"] = "", -- A_Classic_Fairytale:journey --- ["Yeah, well, for some dude to be happy, some other dude has to suffer."] = "", -- A_Classic_Fairytale:queen --- ["Yellow"] = "", -- --- ["Yellow Pepper"] = "", -- A_Space_Adventure:fruit01 --- ["Yellow Watermelons"] = "", -- A_Space_Adventure:fruit01 --- ["Yes!"] = "", -- A_Classic_Fairytale:enemy --- ["Yes, but you're … different!"] = "", -- A_Classic_Fairytale:queen --- ["Yes, yeees! You are now ready to enter the real world!"] = "", -- A_Classic_Fairytale:first_blood --- ["Yeti"] = "", -- --- ["Yikes!"] = "", -- A_Space_Adventure:desert01 --- ["Yo, dude! Get away from our weapons!"] = "", -- A_Classic_Fairytale:queen --- ["Yo, dude, we're here, too!"] = "", -- A_Classic_Fairytale:family --- ["Yo, escort my buttocks!"] = "", -- A_Classic_Fairytale:queen --- ["Yoshi"] = "", -- --- ["Yo, the aliens gave me plants. Medicinal plants. Lots of it."] = "", -- A_Classic_Fairytale:queen --- ["You are far from home, and the water is rising, climb up as high as you can!|Your score will be based on your height."] = "", -- ClimbHome --- ["You are given the chance to turn your life around..."] = "", -- A_Classic_Fairytale:shadow --- ["You are in control of all the active ally units."] = "", -- A_Space_Adventure:fruit01 --- ["You are indeed the best PAotH pilot."] = "", -- A_Space_Adventure:desert03 --- ["You are out of danger, time to go to the moon!"] = "", -- A_Space_Adventure:cosmos --- ["You are playing with our lives here!"] = "", -- A_Classic_Fairytale:enemy --- ["You are sabotaged, RUN!"] = "", -- Continental_supplies --- ["You are the one who fled! So, you are alive."] = "", -- A_Space_Adventure:fruit02 --- ["You bear impressive skills, %s!"] = "", -- A_Classic_Fairytale:dragon --- ["You can also hold down the key for “Precise Aim” to prevent slipping."] = "", -- Basic_Training_-_Movement --- ["You can always trust me!"] = "", -- A_Classic_Fairytale:epil --- ["You can always trust me! I love you!"] = "", -- A_Classic_Fairytale:epil --- ["You can avoid some battles."] = "", -- A_Space_Adventure:desert01 --- ["You can change the detonation timer of grenades."] = "", -- Basic_Training_-_Grenade --- ["You can choose another planet by replaying this mission."] = "", -- A_Space_Adventure:cosmos --- ["You can dive with your flying saucer!"] = "", -- Basic_Training_-_Flying_Saucer --- ["You can even change your aiming direction in mid-flight if you first hold [Precice] and then press [Up] or [Down]."] = "", -- Basic_Training_-_Flying_Saucer --- ["You can even change your aiming direction in mid-flight if you first hold [Precise] and then press [Up] or [Down]."] = "", -- Basic_Training_-_Flying_Saucer --- ["You can further customize the race by changing the scheme script paramater."] = "", -- TechRacer --- ["You can further customize the race by changing the scheme script parameter."] = "", -- TechRacer --- ["You can only use the sniper rifle or the watermelon bomb."] = "", -- A_Space_Adventure:fruit03 --- ["You can practice moving around and using utilities in this mission.|However, it will never end!"] = "", -- A_Classic_Fairytale:epil --- ["You can set the bounciness of grenades (and grenade-like weapons)."] = "", -- Basic_Training_-_Grenade --- ["- You can switch between hogs at the start of your turns. (Not first one)"] = "", -- Continental_supplies --- ["You can’t open a portal on the blue surface."] = "", -- portal --- ["You can use the other 2 hogs to assist you."] = "", -- A_Space_Adventure:fruit02 --- ["You can use the rope to reach new places."] = "", -- Basic_Training_-_Rope --- ["You choose well, %s!"] = "", -- A_Space_Adventure:fruit01 --- ["You completed the mission in %.3f seconds."] = "", -- A_Space_Adventure:ice02 --- ["You completed the mission in %d rounds."] = "", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 --- ["You couldn't have come to a worse time, %s!"] = "", -- A_Space_Adventure:fruit01 --- ["You couldn't possibly believe that after refusing my offer I'd just let you go!"] = "", -- A_Classic_Fairytale:journey --- ["You'd almost swear the water was rising!"] = "", --- ["You'd better watch your steps..."] = "", -- A_Classic_Fairytale:journey --- ["You defended yourself against Captain Lime."] = "", -- A_Space_Adventure:fruit02 --- ["You defended yourself against %s."] = "", -- A_Space_Adventure:fruit02 --- ["You did great, %s! However, we aren't out of danger yet!"] = "", -- A_Space_Adventure:cosmos --- ["You did not make it in time, try again!"] = "", -- Basic_Training_-_Rope --- ["You don't deserve my sacrifice!"] = "", -- A_Classic_Fairytale:queen --- ["You drove Professor Hogevil away."] = "", -- A_Space_Adventure:moon01 --- ["You drove the minions away."] = "", -- A_Space_Adventure:moon01 --- ["You earned the \"Rope Master\" achievement for finishing in under 50 seconds."] = "", -- Basic_Training_-_Rope --- ["You endangered your whole tribe, you bastard!"] = "", -- A_Classic_Fairytale:queen --- ["You failed!"] = "", -- Basic_Training_-_Rope --- ["You failed to kill all enemies in a single turn."] = "", -- Big_Armory --- ["You failed to kill all enemies in this turn."] = "", -- Big_Armory --- ["You fought bravely and you helped us win this battle!"] = "", -- A_Space_Adventure:fruit02 --- ["You give me no choice!"] = "", -- A_Classic_Fairytale:queen --- ["You got a killer mask there, amigo!"] = "", -- A_Classic_Fairytale:epil --- ["You got me!"] = "", -- A_Space_Adventure:moon02 --- ["You had %.1fs remaining on the clock (+%d points)."] = "", -- TargetPractice --- ["You had %.2fs remaining on the clock (+%d points)."] = "", -- Basic_Training_-_Sniper_Rifle --- ["You have 7 turns until the next wave arrives.|Make sure the arriving cannibals are greeted appropriately!|If the hog dies, the cause is lost.|Hint: you might want to use some mines..."] = "", -- A_Classic_Fairytale:backstab --- ["You have 7 turns until the next wave arrives.|Make sure the arriving cannibals are greeted appropriately!|If the hog dies, the cause is lost.|Hint: You might want to use some mines ..."] = "", -- A_Classic_Fairytale:backstab --- ["You have acquired the last device part."] = "", -- A_Space_Adventure:death01 --- ["You have activated Switch Hedgehog!"] = "", -- Basic_Training_-_Movement --- ["You have beaten the challenge!"] = "", -- ClimbHome --- ["You have beaten the team record, congratulations!"] = "", -- Utils --- ["You have been giving us out to the enemy, haven't you!"] = "", -- A_Classic_Fairytale:backstab --- ["You have chosen the perfect moment to leave."] = "", -- A_Classic_Fairytale:united --- ["You have chosen to fight!"] = "", -- A_Space_Adventure:fruit01 --- ["You have chosen to flee."] = "", -- A_Space_Adventure:fruit01 --- ["You have collected %d out of %d crate(s)."] = "", -- SpeedShoppa --- ["You have collected the “Switch Hedgehog” utility!"] = "", -- Basic_Training_-_Movement --- ["You have completed the Basic Bazooka Training!"] = "", -- Basic_Training_-_Bazooka --- ["You have completed the Basic Grenade Training!"] = "", -- Basic_Training_-_Grenade --- ["You have completed the Basic Movement Training!"] = "", -- Basic_Training_-_Movement --- ["You have completed this challenge in %.2f s (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["You have destroyed all targets!"] = "", -- TargetPractice --- ["You have destroyed all the targets."] = "", -- A_Space_Adventure:desert03 --- ["You have destroyed %d of %d targets."] = "", -- Basic_Training_-_Bazooka --- ["You have destroyed %d of %d targets (+%d points)."] = "", -- Basic_Training_-_Sniper_Rifle, TargetPractice --- ["You have dropped %d missiles."] = "", -- User_Mission_-_RCPlane_Challenge --- ["You have eliminated all visible enemy hedgehogs!"] = "", -- A_Space_Adventure:fruit01 --- ["You have eliminated Professor Hogevil."] = "", -- A_Space_Adventure:moon01 --- ["You have eliminated the evil minions."] = "", -- A_Space_Adventure:moon01 --- ["You have escaped successfully."] = "", -- A_Space_Adventure:desert02 --- ["You have failed to complete your task, young one!"] = "", -- A_Classic_Fairytale:journey --- ["You have failed to save the tribe!"] = "", -- A_Classic_Fairytale:backstab --- ["You have finally figured it out!"] = "", -- A_Classic_Fairytale:enemy --- ["You have finished the Basic Rope Training!"] = "", -- Basic_Training_-_Rope --- ["You have finished the bazooka training!"] = "", -- Basic_Training_-_Bazooka --- ["You have finished the challenge in %.3f s."] = "", -- SpeedShoppa --- ["You have finished the challenge!"] = "", -- User_Mission_-_RCPlane_Challenge --- ["You have finished the Flying Saucer Training!"] = "", -- Basic_Training_-_Flying_Saucer --- ["You have finished the target practice!"] = "", -- TargetPractice --- ["You have kidnapped our whole tribe!"] = "", -- A_Classic_Fairytale:enemy --- ["You have killed all enemies."] = "", -- Big_Armory --- ["You have killed an innocent hedgehog!"] = "", -- A_Classic_Fairytale:backstab --- ["You have killed %d of 16 hedgehogs (+%d points)."] = "", -- User_Mission_-_Rope_Knock_Challenge --- ["You have launched %d bazookas."] = "", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka --- ["You have launched %d homing bees."] = "", -- Target_Practice_-_Homing_Bee --- ["You have made %d shots."] = "", -- Basic_Training_-_Sniper_Rifle --- ["You have managed to catch the blue hedgehog in %.3f seconds."] = "", -- A_Space_Adventure:moon02 --- ["You have never worked a bit in your life!"] = "", -- A_Classic_Fairytale:queen --- ["You have nothing to be afraid of now."] = "", -- A_Classic_Fairytale:epil --- ["You haven't rescued anyone."] = "", -- User_Mission_-_That_Sinking_Feeling --- ["You have perfectly beaten the challenge!"] = "", -- User_Mission_-_RCPlane_Challenge --- ["You have proven yourself worthy to see our most ancient secret!"] = "", -- A_Classic_Fairytale:first_blood --- ["You have proven yourselves worthy!"] = "", -- A_Classic_Fairytale:enemy --- ["You have reached the take-off area successfully!"] = "", -- A_Space_Adventure:fruit01 --- ["You have rescued H and Dr. Cornelius."] = "", -- A_Space_Adventure:death01 - ["You have SCORED!!"] = "得分", --- ["You have shot %d times."] = "", -- TargetPractice --- ["You have successfully eliminated Professor Hogevil."] = "", -- A_Space_Adventure:death01 --- ["You have successfully finished the campaign!"] = "", -- A_Classic_Fairytale:epil --- ["You have successfully finished the sniper rifle training!"] = "", -- Basic_Training_-_Sniper_Rifle --- ["You have thrown %d cluster bombs."] = "", -- Target_Practice_-_Cluster_Bomb --- ["You have thrown %d grenades."] = "", -- Target_Practice_-_Grenade_easy, Target_Practice_-_Grenade_hard --- ["You have to be careful and must not die!"] = "", -- A_Space_Adventure:cosmos --- ["You have to catch the other hog 3 times."] = "", -- A_Space_Adventure:moon02 --- ["You have to complete the main mission on moon in order to travel to other planets."] = "", -- A_Space_Adventure:cosmos --- ["You have to continue alone from now on."] = "", -- A_Space_Adventure:cosmos --- ["You have to destroy all the explosives without dying!"] = "", -- A_Space_Adventure:final --- ["You have to destroy all the targets."] = "", -- A_Space_Adventure:desert03 --- ["You have to destroy the target above by dropping a grenade on it from your flying saucer."] = "", -- Basic_Training_-_Flying_Saucer --- ["You have to destroy two targets, but the previous technique would be very difficult or dangerous to use."] = "", -- Basic_Training_-_Flying_Saucer --- ["You have to drop the grenade from rope!"] = "", -- Basic_Training_-_Rope --- ["You have to eliminate all the enemies."] = "", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 --- ["You have to eliminate all the visible enemies."] = "", -- A_Space_Adventure:fruit01 --- ["You have to get the weapons and rescue the PAotH researchers."] = "", -- A_Space_Adventure:moon01 --- ["You have to get to the left-most land and remove any enemy hog from there."] = "", -- A_Space_Adventure:fruit01 --- ["You have to go back to the moon!"] = "", -- A_Space_Adventure:cosmos --- ["You have to move upwards, not downwards, %s!"] = "", -- ClimbHome --- ["You have to reach the left-most place on the map."] = "", -- A_Space_Adventure:fruit01 --- ["You have to stand very close to him"] = "", -- A_Space_Adventure:moon02 --- ["You have to travel again"] = "", -- A_Space_Adventure:cosmos --- ["You have to try again!"] = "", -- A_Space_Adventure:cosmos --- ["You have triggered the secret Do-Not-Rope-to-the-Moon Defense System."] = "", -- A_Space_Adventure:cosmos --- ["You have unlocked the target radar!"] = "", -- TargetPractice --- ["You have used %d flying saucers."] = "", -- A_Space_Adventure:ice02 --- ["You have used %d RC planes."] = "", -- User_Mission_-_RCPlane_Challenge --- ["You have used only 1 RC plane. Outstanding!"] = "", -- User_Mission_-_RCPlane_Challenge --- ["You have violated PAotH regulations!"] = "", -- A_Space_Adventure:cosmos --- ["You have won the game by proving true cooperative skills!"] = "", -- A_Classic_Fairytale:enemy --- ["You just appeared out of thin air!"] = "", -- A_Classic_Fairytale:backstab --- ["You just can't let it go, can you!"] = "", -- A_Classic_Fairytale:queen --- ["You just committed suicide..."] = "", -- A_Classic_Fairytale:shadow --- ["You just got yourself some extra health.|The more health your hedgehogs have, the better!"] = "", -- Basic_Training_-_Movement --- ["You killed my father, you monster!"] = "", -- A_Classic_Fairytale:backstab --- ["You know...taking a stroll."] = "", -- A_Classic_Fairytale:backstab --- ["You know what? I don't even regret anything!"] = "", -- A_Classic_Fairytale:backstab --- ["You'll get an extra sniper rifle every time you kill an enemy hog with a limit of max 4 rifles."] = "", -- A_Space_Adventure:fruit03 --- ["You'll get an extra teleport every time you kill an enemy hog with a limit of max 2 teleports."] = "", -- A_Space_Adventure:fruit03 --- ["You'll get extra time in case you need it when you pass a ring."] = "", -- A_Space_Adventure:ice02 --- ["You'll have only 2 watermelon bombs during the game."] = "", -- A_Space_Adventure:fruit03 --- ["You'll have only one RC plane at the start of the mission."] = "", -- A_Space_Adventure:desert03 --- ["You'll have to eliminate Captain Lime at the end."] = "", -- A_Space_Adventure:fruit02 --- ["You'll have to eliminate %s at the end."] = "", -- A_Space_Adventure:fruit02 --- ["You'll lose if you die or if your time is up."] = "", -- A_Space_Adventure:moon02 --- ["You'll see what I mean!"] = "", -- A_Classic_Fairytale:enemy --- ["You lost your target, try again!"] = "", -- TargetPractice --- ["You may find it handy."] = "", -- A_Space_Adventure:cosmos --- ["You may only attack from a rope!"] = "", -- WxW --- ["You may only place 1 Extra Time crate per turn."] = "", -- Construction_Mode --- ["You may only place %d crates per round."] = "", -- Construction_Mode --- ["- You may only score when your flag is in your base"] = "", -- Capture_the_Flag --- ["You meatbags are pretty slow, you know!"] = "", -- A_Classic_Fairytale:enemy --- ["You might want to find a way to instantly kill arriving cannibals!"] = "", -- A_Classic_Fairytale:backstab --- ["You must attack from a rope, after you collected a crate!"] = "", -- WxW --- ["You must first collect a crate before you attack!"] = "", -- WxW --- ["You must survive the flood in order to score."] = "", -- User_Mission_-_That_Sinking_Feeling --- ["You never give me plants!"] = "", -- A_Classic_Fairytale:queen --- ["Young one, you are telling us that they can instantly change location without a shaman?"] = "", -- A_Classic_Fairytale:united --- ["You now have infinite fuel, grenades and bazookas for fun."] = "", -- Basic_Training_-_Flying_Saucer --- ["You only get 1 rope this time, don't waste it!"] = "", -- Basic_Training_-_Rope --- ["You only have 2 flying saucers this time."] = "", -- Basic_Training_-_Flying_Saucer --- ["You only have one flying saucer this time."] = "", -- Basic_Training_-_Flying_Saucer --- ["You probably know what to do next..."] = "", -- A_Classic_Fairytale:first_blood --- ["Your accuracy was %.1f%%."] = "", -- Basic_Training_-_Bazooka, TargetPractice --- ["Your accuracy was %.1f%% (+%d points)."] = "", -- TargetPractice --- ["Your ammo is limited this time."] = "", -- Basic_Training_-_Bazooka --- ["Your deaths will be avenged, %s!"] = "", -- A_Classic_Fairytale:enemy --- ["Your death will not be in vain, Dense Cloud!"] = "", -- A_Classic_Fairytale:shadow --- ["You're a coward!"] = "", -- A_Classic_Fairytale:queen --- ["You're...alive!? But we saw you die!"] = "", -- A_Classic_Fairytale:backstab --- ["You're a pathetic liar!"] = "", -- A_Classic_Fairytale:backstab --- ["You're funny!"] = "", -- A_Classic_Fairytale:journey --- ["You're getting pretty good! |Tip: When you shorten you rope, you move faster!|And when you lengthen it, you move slower."] = "", -- Basic_Training_-_Rope --- ["You're on your way to freeing your tribe!"] = "", -- A_Classic_Fairytale:queen --- ["You're pathetic! You are not worthy of my attention..."] = "", -- A_Classic_Fairytale:shadow --- ["You're probably wondering why I bought you back..."] = "", -- A_Classic_Fairytale:backstab --- ["You're probably wondering why I brought you back ..."] = "", -- A_Classic_Fairytale:backstab --- ["Your escape took you %d turns."] = "", -- A_Space_Adventure:desert02 --- ["You're so brave! I feel safe with you."] = "", -- A_Classic_Fairytale:epil --- ["You're some piece of hypocrite junkie!"] = "", -- A_Classic_Fairytale:queen --- ["You're terrorizing the forest...We won't catch anything like this!"] = "", -- A_Classic_Fairytale:shadow --- ["You retrieved the lost part."] = "", -- A_Space_Adventure:fruit02 --- ["Your fastest escape so far: %d turns"] = "", -- A_Space_Adventure:desert02 --- ["Your fastest victory so far: %d rounds"] = "", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 --- ["Your first destination is the moon in order to get more fuel."] = "", -- A_Space_Adventure:cosmos --- ["Your hedgehog died!"] = "", -- User_Mission_-_That_Sinking_Feeling --- ["Your hedgehog has been revived!"] = "", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope --- ["Your hedgehog was panicly afraid of the water and decided to go in a safe distance of %d from it."] = "", -- ClimbHome --- ["Your height over time"] = "", -- ClimbHome --- ["Your hogs must survive!"] = "", -- A_Classic_Fairytale:journey --- ["Your movement skills will be evaluated now."] = "", -- A_Classic_Fairytale:first_blood --- ["Your next task is to collect some crates by using the rope!"] = "", -- A_Classic_Fairytale:first_blood --- ["Your personal best time so far: %.3f seconds"] = "", -- A_Space_Adventure:ice02, A_Space_Adventure:moon02 --- ["Your rank: %s"] = "", -- User_Mission_-_RCPlane_Challenge --- ["Your rope is gone! Try again!"] = "", -- Basic_Training_-_Rope --- ["You saved %d of 8 hegehogs."] = "", -- User_Mission_-_That_Sinking_Feeling --- ["You see, hedgehog spikes are very, very valuable."] = "", -- A_Classic_Fairytale:queen --- ["You see the wind strength at the bottom right corner."] = "", -- Basic_Training_-_Bazooka --- ["You see the wind strength at the top."] = "", -- Basic_Training_-_Bazooka --- ["You should have known that we don't rely on meatbags!"] = "", -- A_Classic_Fairytale:queen --- ["You should know this more than anyone, Leaks!"] = "", -- A_Classic_Fairytale:queen --- ["You speak great truth, Hannibal. Here, take a sip!"] = "", -- A_Classic_Fairytale:epil --- ["You've been assaulting us, we have been just defending ourselves!"] = "", -- A_Classic_Fairytale:enemy - ["You've reached the goal!| |Time: "] = "目标达成| |时间:", --- ["You will be avenged!"] = "", -- A_Classic_Fairytale:shadow --- ["You will fail if you run out of ammo and there are still targets available."] = "", -- A_Space_Adventure:desert03 --- ["You will gain some extra ammo from the crates the next time you play the \"Getting to the device\" mission."] = "", -- A_Space_Adventure:fruit03 --- ["You will play every 3 turns."] = "", -- A_Space_Adventure:fruit01 --- ["- You will recieve 2-4 weapons on each kill! (Even on own hogs)"] = "", -- Continental_supplies --- ["You won't believe what happened to me!"] = "", -- A_Classic_Fairytale:backstab --- ["Yuck! I bet they'll keep worshipping her even after I save the village!"] = "", -- A_Classic_Fairytale:family --- ["Yumme Gunpowder"] = "", -- --- ["Zealandia"] = "", -- Continental_supplies --- ["Zombie"] = "", -- --- ["Zombi"] = "", -- portal - ["'Zooka Team"] = "火箭队", --- ["Zoom: [Pinch] with 2 fingers"] = "", -- Basic_Training_-_Movement --- ["Zoom: [Rotate mouse wheel]"] = "", -- Basic_Training_-_Movement --- ["Zork"] = "", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["!!!"] = "!!!", +["..."] = "...", +["011101000"] = "011101000", -- A_Classic_Fairytale:dragon +["011101001"] = "011101001", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:family, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow, A_Classic_Fairytale:united +["10 weapon schemes"] = "10武器方案", -- Continental_supplies +["15+%d damage, %d invulnerable left"] = "15+%d伤害,无敌还有%d", -- Continental_supplies +["1-5, Precise + 1-4: Choose structure type"] = "1-5,精确+ 1-4: 选择结构类型", -- Construction_Mode +["+1 barrel!"] = "+1油桶", -- Tumbler +["%.1f seconds were remaining."] = "还有%.1f秒", -- Basic_Training_-_Bazooka +["%.1fs"] = "%.1f秒", -- Racer, TechRacer +["+1 Grenade"] = "+1手榴弹", -- Basic_Training_-_Flying_Saucer +["+1 mine!"] = "+1地雷", -- Tumbler +["+1 point"] = "+1分", -- Mutant +["-1 point"] = "-1分", -- Mutant +["-1 to anyone for a suicide"] = "自杀-1分", -- Mutant +["+1 to the Bottom Feeder for killing anyone"] = "让对手喂鱼+1分", -- Mutant +["+1 to the Mutant for killing anyone"] = "变种人杀死对手+1分", -- Mutant +["+2 for becoming the Mutant"] = "成为变种人+2分", -- Mutant +["30 minutes later..."] = "30分钟后", -- A_Classic_Fairytale:shadow +["%.3fs"] = "%.3f秒", -- A_Space_Adventure:ice02 +["5 additional enemies will be spawned during the game."] = "五个额外的敌人会在游戏中出现", -- A_Space_Adventure:fruit01 +["5 Deadly Hogs"] = "致命五刺猬", -- A_Space_Adventure:death02 +["6 more seconds added to the clock"] = "加6秒", -- A_Space_Adventure:ice02 +["About a month ago, a cyborg came and told us that you're the cannibals!"] = "大约一个月前,机器人来到,并说你们是食人族", -- A_Classic_Fairytale:enemy +["Above-average pilot"] = "平均水平之上的飞行员", -- User_Mission_-_RCPlane_Challenge +["Accuracy Bonus! +15 points"] = "准确奖励!+15分", -- Space_Invasion +["Accuracy bonus: +%d points"] = "准确奖励: +%d分", -- Basic_Training_-_Sniper_Rifle +["Achievement gotten: %s"] = "获得成就: %s", -- User_Mission_-_RCPlane_Challenge, User_Mission_-_That_Sinking_Feeling, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, Basic_Training_-_Rope, Tumbler +["A Classic Fairytale"] = "经典童话故事", -- A_Classic_Fairytale:first_blood +["A crate critical to this mission has been destroyed."] = "这个任务的一个关键箱子被破坏", -- SimpleMission +["Actually, you aren't worthy of life! Take this..."] = "实际上,你不值得活着!拿着这个……", -- A_Classic_Fairytale:shadow +["A cy-what?"] = "一个机器-什么?", -- A_Classic_Fairytale:enemy +["Add %d"] = "添加%d", -- HedgeEditor +["Admit what?"] = "承认什么?", -- A_Classic_Fairytale:queen +["Adventurous"] = "Adventurous", -- A_Classic_Fairytale:journey +["A frenetic Hedgewars mini-game"] = "狂乱的迷你游戏", -- Frenzy +["Africa"] = "非洲", -- Continental_supplies +["A frozen adventure"] = "一个冰冻的冒险", -- A_Space_Adventure:ice01 +["After Leaks A Lot betrayed his tribe, he joined the cannibals..."] = "Leaks A Lot背叛了他的部落后加入了食人族", -- A_Classic_Fairytale:first_blood +["After that incident he went underground and started working on his plan to steal the device."] = "在那次严重事件后,他进入地下开始偷走设备的计划", -- A_Space_Adventure:moon02 +["After the shock caused by the enemy spy, Leaks A Lot and Dense Cloud went hunting to relax."] = "被敌人间谍吓到后,Leaks A Lot 和 Dense Cloud 去打猎放松一下", -- A_Classic_Fairytale:shadow +["After you killed an enemy, you'll lose the weapon that he is named after."] = "杀死敌人后,会失去跟敌人同样名字的武器", -- A_Space_Adventure:death02 +["After you left the moon, my other loyal minions came and resurrected me so I could complete my master plan."] = "你离开月球后,我的其他忠诚部下来到并复活我,这样我就能完成我的伟大计划", -- A_Space_Adventure:death01 +["Again with the 'cannibals' thing!"] = "和食人族的又一件事情", -- A_Classic_Fairytale:enemy +["A Hedgewars minigame"] = "迷你游戏", -- Capture_the_Flag +["A Hedgewars mini-game"] = "迷你游戏", -- Racer, Space_Invasion, TechRacer, Tumbler +["A Hedgewars tag game"] = "迷你游戏", -- Mutant +["Ahhh, home, sweet home. Made it in %d seconds."] = "回到家了,用时%d秒", -- ClimbHome +["Aim at the ceiling and hold [Attack] pressed until the rope attaches."] = "瞄准天花板,长按[攻击]直到绳索连接", -- Basic_Training_-_Rope +["Aiming practice"] = "瞄准练习", -- TargetPractice +["Aiming Practice"] = "瞄准练习", --火箭筒、霰弹枪、狙击枪 +["Aim: [Up]/[Down]"] = "瞄准: [上]/[下]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Rope +["Air Attack"] = "空袭", -- Construction_Mode +["Air General"] = "空军将军", -- Battalion +["Air Mine Placement Mode"] = "浮空雷放置模式", -- HedgeEditor +["AIR MINE PLACEMENT MODE"] = "浮空雷放置模式", -- HedgeEditor +["A leap in a leap"] = "一跳一跳", -- A_Classic_Fairytale:first_blood +["Alex"] = "Alex", -- +["Alien! I wish to be moved!"] = "外星人,我希望能转移", -- A_Classic_Fairytale:queen +["A little gift from the cyborgs"] = "机器人的一个小礼物", -- A_Classic_Fairytale:shadow +["Al.Kaholic"] = "Al.Kaholic", -- +["All But Last"] = "除了最后一个", -- WxW +["All But Last: You must not solely attack the team with the least health"] = "除了最后一个: 你不能攻击血量最少的队伍", -- WxW +["All gone...everything!"] = "一切都消失了…一切!", -- A_Classic_Fairytale:enemy +["Allies"] = "Allies", -- A_Space_Adventure:ice01, A_Space_Adventure:ice02 +["All right, I'll admit it!"] = "好吧,我承认了!", -- A_Classic_Fairytale:queen +["All right, we just need to get to the other side of the island!"] = "好吧,我们只需要到达岛屿的另一边", -- A_Classic_Fairytale:journey +["All right, you got me!"] = "好吧,你懂我的!", -- A_Classic_Fairytale:queen +["All the other places are protected by our flight-inhibiting weapons."] = "所有其他地方都受到我们的飞行抑制武器保护", -- A_Space_Adventure:fruit01 +["All the saucer pilots dream to come here one day in order to compete with the best!"] = "所有飞碟飞行员都梦想有一天来到这里,和最优秀的人比赛", -- A_Space_Adventure:ice02 +["All they do is sit around and judge us!"] = "他们做的只有坐下和审判我们", -- A_Classic_Fairytale:queen +["All this to please our beloved “elders” … hick …"] = "这些都是为了取悦我们心爱的“长辈”", -- A_Classic_Fairytale:queen +["All walls touched!"] = "碰到所有墙壁了", -- WxW +["All you do is take long walks when everyone else works."] = "其他人工作的时候你在散步", -- A_Classic_Fairytale:queen +["All your hedgehogs must be above the marked height!"] = "你的所有刺猬都要在标记的高度之上", -- A_Classic_Fairytale:family +["Also, you should know that the only place where you can fly is the left-most part of this area."] = "并且,你应该知道这个地区的最左边是唯一能飞的地方", -- A_Space_Adventure:fruit01 +["Always being considered weak and fragile."] = "一直被当做美丽的花瓶", -- A_Classic_Fairytale:queen +["Amazing! I was never beaten in a race before!"] = "令人惊叹!之前我没有在竞赛中输过!", -- A_Space_Adventure:moon02 +["Ammo depleted!"] = "弹药耗尽!", -- Space_Invasion +["Ammo: %d"] = "弹药: %d", -- Tumbler +["Ammo is reset at the end of your turn."] = "你的回合结束后弹药重置", +["Ammo Limit: Hogs can’t have more than 1 ammo per type"] = "弹药限制: 每个类型武器的数量不能超过一", -- Highlander +["Ammo Maniac! +5 points!"] = "弹药狂人!+5分", -- Space_Invasion +["A mysterious Box"] = "一个神秘的箱子", -- Basic_Training_-_Movement +["And how am I alive?!"] = "我怎么还活着?!", -- A_Classic_Fairytale:enemy +["And I just forgot the checkpoint of my main mission. Great, just great!"] = "我忘了主要任务的检查点,好,太好了!", -- A_Space_Adventure:cosmos +["… and I think they are up to something. Something bad!"] = "… 而且我认为他们要搞事,搞坏事", -- A_Classic_Fairytale:epil +["Andrey"] = "Andrey", -- +["And so happened that Leaks A Lot failed to complete the challenge! He landed, pressured by shame ..."] = "Leaks A Lot 没能完成挑战", -- A_Classic_Fairytale:first_blood +["And so it began..."] = "就这样开始了……", -- A_Classic_Fairytale:first_blood +["And so the cyborgs took over the island."] = "就这样机器人控制了这个岛屿", -- A_Classic_Fairytale:queen +["...and so the cyborgs took over the world..."] = "……就这样机器人控制了这个世界……", -- A_Classic_Fairytale:shadow +["And so they discovered that cyborgs weren't invulnerable..."] = "就这样他们发现机器人不是无敌的……", -- A_Classic_Fairytale:journey +["… and then I took a stroll …"] = "……然后我去闲逛了……", -- A_Classic_Fairytale:epil +["And what do they do in the meantime? Nothing!"] = "在同一时间他们做了什么?什么都没做!", -- A_Classic_Fairytale:queen +["And where's all the weed?"] = "所有杂草在哪里?", -- A_Classic_Fairytale:dragon +["And you believed me? Oh, god, that's cute!"] = "你相信我?哦,太可爱了!", -- A_Classic_Fairytale:journey +["And you need to move to the top!"] = "你需要走到上面!", -- Basic_Training_-_Movement +["An experimental editing tool for missions and more"] = "一个实验性的编辑工具用于任务和更多", -- HedgeEditor +["Anno 1032"] = "Anno 1032", -- Continental_supplies +["Anno 1032: [The explosion will make a strong push ~ Wide range, wont affect hogs close to the target]"] = "Anno 1032: [爆炸会造成大范围的强烈冲击,不会影响靠近目标的刺猬]", -- Continental_supplies +["An object has been destroyed before it took enough damage."] = "一个对象在它造成足够伤害前被破坏了", -- SimpleMission +["Antarctica"] = "南极洲", -- Continental_supplies +["Antarctic summer: Every 4th turn you get 1 girder, 1 mudball, 2 sine guns and 1 portable portal device."] = "南极的夏天: 每四个回合,你会得到1大梁、1泥球、2正弦枪、1便携传送设备", -- Continental_supplies +["Antarctic summer: - Will give you one girder/mudball and two sineguns/portals every fourth turn."] = "南极的夏天: 每四个回合给你1大梁/泥球、2正弦枪/传送门", -- Continental_supplies +["Anti-Gravity Device Part (+1)"] = "反重力设备部件(+1)", -- A_Space_Adventure:desert01, A_Space_Adventure:fruit02, A_Space_Adventure:ice01 +["Anton"] = "Anton", -- +["An unexpected event!"] = "一个意外事件!", -- A_Space_Adventure:cosmos +["Anyway, the aliens accept me for who I am."] = "不管怎样,外星人接受了我是谁", -- A_Classic_Fairytale:queen +["A random hedgehog will inherit the weapons of his deceased team-mates."] = "一个随机刺猬会继承死了的队友的武器", -- A_Space_Adventure:death02 +["Arashi"] = "Arashi", -- +["Area"] = "地区", -- Continental_supplies +["Areas surrounded by a green dashed outline are portal-proof and repel portals."] = "绿色轮廓包围的地区防传送门和排斥传送门", -- A_Space_Adventure:final +["Areas surrounded by a security border are indestructible."] = "安全边界包围的地区不可破坏", -- A_Space_Adventure:final +["Areas with a green dashed outline are portal-proof."] = "有绿色轮廓的地区防传送门", -- A_Space_Adventure:final +["Areas with a security outline are indestructible."] = "有安全轮廓的地区不可破坏", -- A_Space_Adventure:final +["Are we there yet?"] = "我们到了吗?", -- A_Classic_Fairytale:shadow +["Are you accusing me of something?"] = "你因某事指责我?", -- A_Classic_Fairytale:backstab +["Are you helping the aliens?"] = "你在帮助外星人?", -- A_Classic_Fairytale:queen +["Are you saying that many of us have died for your entertainment?"] = "你说我们会为你的娱乐死掉很多人?", -- A_Classic_Fairytale:enemy +["Argh, the boredom!"] = "啊,无聊啊!", -- A_Classic_Fairytale:queen +["Artur Detour"] = "Artur Detour", -- A_Classic_Fairytale:queen +["As a reward for your performance, here's some new technology!"] = "作为你的表现的奖励,这是一些新科技!", -- A_Classic_Fairytale:dragon +["Ash"] = "Ash", -- +["A Shoppa minigame"] = "一个 Shoppa 迷你游戏", -- WxW +["Asia"] = "亚洲", -- Continental_supplies +["As long you don't touch the ground, you can|re-use the same rope as often as you like."] = "只要你还没碰到地面,就能重新使用绳索", -- Basic_Training_-_Rope +["A smuggler! Prepare for battle"] = "一个走私者!准备战斗", -- A_Space_Adventure:desert01 +["A Space Adventure"] = "一个太空冒险", -- A_Space_Adventure:desert01, A_Space_Adventure:moon01 +["Assault Team"] = "突击小队", -- A_Classic_Fairytale:backstab +["Asteroid"] = "Asteroid", -- Big_Armory +["As the ammo is sparse, you might want to reuse ropes while mid-air."] = "弹药稀少,你可能要在空中重新使用绳索", -- A_Classic_Fairytale:dragon +["As the challenge was completed, Leaks A Lot set foot on the ground..."] = "Leaks A Lot挑战完成", -- A_Classic_Fairytale:first_blood +["As you are more experienced, I want you to lead them to battle."] = "你更有经验,我想要你带领他们去战斗", -- A_Space_Adventure:fruit01 +["As you can see I have survived our last encounter and I had time to plot my master plan!"] = "如你所见,我在我们上一次遭遇中活下来,我有时间实现我的伟大计划", -- A_Space_Adventure:death01 +["As you can see, there is no way to get on the other side!"] = "如你所见,这里没有路到另一边", -- A_Classic_Fairytale:dragon +["As you probably noticed, these rubber bands|are VERY elastic. Hedgehogs and many other|things will bounce off without taking any damage."] = "你可能注意到,这些橡皮筋很有弹性,|可以弹走刺猬和大部分东西", -- Basic_Training_-_Movement +["As you've seen, the dropped grenade roughly fell into your flying direction."] = "如你所见,手榴弹只会掉到你的飞碟下面", -- Basic_Training_-_Flying_Saucer +["Athlete"] = "运动员", -- Battalion +["Attack: Activate"] = "攻击: 激活", -- Racer +["Attack Captain Lime before he attacks back."] = "先发制人, 攻击Captain Lime", -- A_Space_Adventure:fruit02 +["Attack From Rope: %s"] = "从绳索攻击: %s", -- WxW +["Attack From Rope: You may only attack from a rope."] = "从绳索攻击: 你只能在绳索上攻击", -- WxW +["Attack rule: %s"] = "攻击规则: %s", -- WxW +["Attack: Select this continent"] = "攻击: 选择这个大陆", -- Continental_supplies +["Attack: [Space]"] = "攻击: [Space]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope +["Attack: Tap the [Bomb]"] = "攻击: [Bomb]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:shadow +["Attack the assassins before they attack back."] = "先发制人,攻击刺客", -- A_Space_Adventure:fruit02 +["Attack: Throw ball"] = "攻击: 扔球", -- Knockball +["At the end of the game your health was %d."] = "游戏的最后你还有%d血量", -- A_Space_Adventure:ice01 +["At the start of the game each enemy hog has only the weapon that he is named after."] = "在游戏的开始,每个敌人刺猬只有相同名字的武器", -- A_Space_Adventure:death02 +["Australia"] = "澳大利亚", -- Continental_supplies +["Available weapon specials:"] = "可用的武器特别模式:", -- Continental_supplies +["Average pilot"] = "平均水平的飞行员", -- User_Mission_-_RCPlane_Challenge +["Avoid bazookas, red and blue invaders."] = "避开火箭炮、红蓝入侵者", -- Space_Invasion +["Axes"] = "Axes", -- Bazooka_Battlefield +["Aye! Fellow! Let me exit this chamber of doom!"] = "喂,老兄!让我离开这个房间", -- A_Classic_Fairytale:epil +["Back Breaker"] = "Back Breaker", -- A_Classic_Fairytale:backstab +["Back in the village, after telling the villagers about the threat..."] = "回到村子,告诉村民关于威胁之后……", -- A_Classic_Fairytale:united +["Back in the village, the two tribes finally started to live in harmony."] = "回到村子,两个部落终于和睦相处", -- A_Classic_Fairytale:epil +["Back Jump: [Backspace] ×2"] = "后跳: [Backspace]×2", -- Basic_Training_-_Movement +["Back Jump: Double-tap the [Curvy Arrow]"] = "后跳: 双击[Curvy Arrow]", -- Basic_Training_-_Movement +["Back Jumping (1/2)"] = "后跳(1/2)", -- Basic_Training_-_Movement +["Back Jumping (2/2)"] = "后跳(2/2)", -- Basic_Training_-_Movement +["Backstab"] = "背刺", -- A_Classic_Fairytale:backstab +["Backwards jump: Press [Backspace] twice"] = "后跳: 按两次[Backspace]", -- A_Classic_Fairytale:first_blood +["Backwards jump: Tap the [Curvy Arrow] twice"] = "后跳: 按两次[Curvy Arrow]", -- A_Classic_Fairytale:first_blood +["Bacon"] = "Bacon", -- +["Bad Guy"] = "Bad Guy", -- User_Mission_-_The_Great_Escape +["Badmad"] = "Badmad", -- portal +["Bad Team"] = "Bad Team", -- User_Mission_-_The_Great_Escape +["Bad timing"] = "坏时机", -- A_Space_Adventure:fruit01 +["Baggy"] = "Baggy", -- +["Balrog"] = "Balrog", -- +["Bamboo Thicket"] = "竹林", +["Barrel Launcher"] = "油桶发射器", +["Barrel Placement Mode"] = "油桶放置模式", -- Construction_Mode +["BARREL PLACEMENT MODE"] = "油桶放置模式", -- HedgeEditor +["Barrier unlocked!"] = "障碍解锁了!", -- Basic_Training_-_Rope +["Baseballbat"] = "棒球棒", -- Continental_supplies +["Baseball bat specials cannot be used close to other hogs."] = "棒球棒不能在非常靠近其他刺猬的时候使用", -- Continental_supplies +["Baseball Bat with Ball"] = "棒球棒和球", -- Knockball +["Base damage has been modified to 12 per shot."] = "基础伤害被修改为每发12", -- Battalion +["Based on what you've learned, destroy the target on the girder and as always, land safely!"] = "用你学到的东西,破坏大梁上的目标,安全地着陆!", -- Basic_Training_-_Flying_Saucer +["Basically this is a combination of diving and launching."] = "本质上这是潜水和发射的结合", -- Basic_Training_-_Flying_Saucer +["Basic Bazooka Training"] = "基础火箭炮训练", -- Basic_Training_-_Bazooka +["Basic Grenade Training"] = "基础手榴弹训练", -- Basic_Training_-_Grenade +["Basic Movement Training"] = "基础移动训练", -- Basic_Training_-_Movement +["Basic Rope Training"] = "基础绳索训练", -- Basic_Training_-_Rope +["Basic Training"] = "基础训练", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope +["Basketball"] = "篮球", -- Basketball +["Bat balls at your enemies and|push them into the sea!"] = "挥动棒球把敌人打下海", +["Battalion"] = "Battalion", -- Battalion +["Battle Starts Now!"] = "战斗现在开始!", -- A_Space_Adventure:fruit01 +["Batty"] = "Batty", -- +["Bat your opponents through the|baskets and out of the map!"] = "把敌人打进篮筐", +["Bazooka Battlefield"] = "火箭炮战场", -- Bazooka_Battlefield +["Bazooka Master"] = "火箭炮大师", -- Basic_Training_-_Bazooka +["Bazookas are influenced by wind."] = "火箭炮受风力影响", -- Basic_Training_-_Bazooka +["Bazooka Training"] = "火箭炮训练", +["Bearded Beast"] = "Bearded Beast", -- +["Be careful, the future of Hogera is in your hands!"] = "注意,Hogera的未来在你手上!", -- A_Space_Adventure:cosmos +["Be careful, your fuel is limited from now on!"] = "注意,从现在开始你的燃料受到限制", -- Basic_Training_-_Flying_Saucer +["Be careful, your gadgets won't work in the bandit area. You should get an ice gun."] = "注意,你的小工具在强盗的领地不起作用,你应该找一把冰冻枪", -- A_Space_Adventure:ice01 +["Beep Loopers"] = "Beep Loopers", -- A_Classic_Fairytale:queen +["Beginner"] = "新手", -- User_Mission_-_RCPlane_Challenge +["Behind these trees on the east side there is Secret Base 17."] = "东边的树木后面就是秘密基地17", -- A_Space_Adventure:cosmos +["Below-average pilot"] = "平均水平之下的飞行员", -- User_Mission_-_RCPlane_Challenge +["Besides, why would I choose certain death?"] = "此外,为什么我要选择死亡?", -- A_Classic_Fairytale:queen +["Best laps per team: "] = "每队一圈最佳速度: ", +["Best team times: "] = "队伍最佳时间: ", -- Racer, TechRacer +["Better get yourself another health crate to heal your wounds."] = "最好得到另一个医疗箱治疗你的伤口", -- Basic_Training_-_Movement +["Better luck next time!"] = "祝你下次好运!", -- ClimbHome +["Better Safe Than Sorry"] = "安全比抱歉要好", -- A_Space_Adventure:desert02 +["Beware, any damage taken will stay until you complete the moon's main mission"] = "当心,任何受到的伤害会停留,直到你完成月球的主要任务", -- A_Space_Adventure:cosmos +["Beware of mines: They explode after 3 seconds."] = "当心地雷: 他们在3秒后爆炸", -- A_Classic_Fairytale:journey +["Beware of mines: They explode after 5 seconds."] = "当心地雷: 他们在5秒后爆炸", -- A_Classic_Fairytale:journey +["Beware, though! If you are slow, you die!"] = "当心,如果你太慢就会死!", -- A_Classic_Fairytale:dragon +["Beware, though! Many smugglers come often to explore these tunnels and scavenge whatever valuable items they can find."] = "当心,经常有很多走私者探索这些隧道,并搜寻值钱的东西", -- A_Space_Adventure:desert01 +["Beware, though, you will only be able to move slowly through the water."] = "当心,你只能慢慢地穿过这些水", -- Basic_Training_-_Flying_Saucer +["Big Armory"] = "大军械库", -- Big_Armory +["Billy Frost"] = "Billy Frost", -- A_Space_Adventure:ice01 +["Bingo"] = "Bingo", -- +["Bio-Filter: Aggressively removes enemies."] = "生物过滤器: 积极清除敌人", -- Construction_Mode +["Bio-Filter"] = "生物过滤器", -- Construction_Mode +["Biomechanic Team"] = "Biomechanic Team", -- A_Classic_Fairytale:family +["Bitter"] = "Bitter", -- +["Blanka"] = "Blanka", -- +["Blender"] = "Blender", -- A_Classic_Fairytale:family +["Bloodpie"] = "Bloodpie", -- A_Classic_Fairytale:backstab +["Bloodrocutor"] = "Bloodrocutor", -- A_Classic_Fairytale:shadow +["Bloodsucker"] = "Bloodsucker", -- A_Classic_Fairytale:shadow +["Blue"] = "蓝", -- +["Blue Team"] = "蓝队", -- User_Mission_-_Dangerous_Ducklings +["Bob"] = "Bob", -- A_Space_Adventure:cosmos +["Bobo"] = "Bobo", -- User_Mission_-_Nobody_Laugh +["Bone Jackson"] = "Bone Jackson", -- A_Classic_Fairytale:backstab +["Bonely"] = "Bonely", -- A_Classic_Fairytale:shadow +["Bones"] = "Bones", -- +["Boom!"] = "Boom!", +["BOOM! BOOM! BOOM! %s are the masters of destruction with %d destroyed invaders."] = "BOOM! BOOM! BOOM! %s 是破坏大师,消灭了%d个入侵者", -- Space_Invasion +["Boom! %s has destroyed %d invaders."] = "Boom! %s 消灭了%d个入侵者", -- Space_Invasion +["BOOM! %s really didn't like the invaders, so they decided to destroy as much as %d of them."] = "BOOM! %s 真的不喜欢入侵者,所以他们决定%d敌人来一个杀一个", -- Space_Invasion +["Boris"] = "Boris", -- A_Space_Adventure:moon01 +["Boss defeated! +30 points!"] = "打败首领!+30分", -- Space_Invasion +["Boss Slayer! +25 points!"] = "首领杀手!+25分", -- Space_Invasion +["Both Barrels"] = "两个油桶", -- +["Both your hedgehogs must survive."] = "你的两个刺猬都要活下来", -- User_Mission_-_Teamwork_2, User_Mission_-_Teamwork +["Bottom Feeder"] = "喂鱼人", -- Mutant +["Bounciness"] = "弹性", -- Basic_Training_-_Grenade +["Bouncing Bomb"] = "弹性炸弹", -- Basic_Training_-_Bazooka +["Bouncy Boomerang"] = "弹性回力镖", -- Continental_supplies +["Bouncy Girder: [4]"] = "弹性大梁: [4]", -- HedgeEditor +["Bouncy Land: [4]"] = "弹性地面: [4]", -- HedgeEditor +["Bouncy Land"] = "弹性地面", -- HedgeEditor +["Bounty: Get 6 weapons for each kill (even on own hogs)."] = "赏金: 每杀一个得到六个武器(即使是自己人)", -- Continental_supplies +["Bozo"] = "Bozo", -- +["Brain Blower"] = "Brain Blower", -- A_Classic_Fairytale:journey +["Brainiac"] = "Brainiac", -- A_Classic_Fairytale:epil, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:shadow +["Brainila"] = "Brainila", -- A_Classic_Fairytale:united +["Brain Stu"] = "Brain Stu", -- A_Classic_Fairytale:united +["Brain Teaser"] = "Brain Teaser", -- A_Classic_Fairytale:backstab +["Brigadier Briggs"] = "Brigadier Briggs", -- +["Bruce"] = "Bruce", -- A_Space_Adventure:moon01 +["Brutal Lily"] = "Brutal Lily", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil +["Brutus"] = "Brutus", -- A_Classic_Fairytale:backstab +["Build a fortress and destroy your enemy."] = "建造堡垒并消灭你的敌人", -- Construction_Mode +["Build an awesome race track by placing|waypoints which the hedgehogs have to|touch in any order to finish a round."] = "建造一个赛道,放置刺猬必须触碰的路标", -- Racer +["Build a track and race."] = "建造赛道并竞赛", +["Builder"] = "Builder", -- Battalion +["Build one of multiple different structures|to aid you in victory, at the cost of energy."] = "消耗能量建造多个不同结构", -- Construction_Mode +["Bullseye"] = "靶心", -- A_Classic_Fairytale:dragon +["Bunny"] = "Bunny", -- +["burp"] = "burp", -- +["Bushes"] = "Bushes", -- Bazooka_Battlefield +["Bushi"] = "Bushi", -- +["Buster"] = "Buster", -- +["But it proved to be no easy task!"] = "但事实证明这并非易事", -- A_Classic_Fairytale:dragon +["But I want my sandals!"] = "但我想要我的拖鞋!", -- A_Classic_Fairytale:queen +["But one thing's for sure:"] = "但有一件事是确定的:", -- A_Space_Adventure:final +["But that's impossible!"] = "但那不可能!", -- A_Classic_Fairytale:backstab +["But the ones alive are stronger in their heart!"] = "但活着的那一个内心比其他人强大", -- A_Classic_Fairytale:enemy +["But … they kidnapped you!"] = "但……他们绑架了你!", -- A_Classic_Fairytale:queen +["But...we died!"] = "但是……我们死了!", -- A_Classic_Fairytale:backstab +["But where can we go?"] = "但我们能去哪里?", -- A_Classic_Fairytale:united +["But why did you betray us?!"] = "但为什么你要背叛我们?!", -- A_Classic_Fairytale:queen +["But why would they help us?"] = "但为什么他们要帮助我们?", -- A_Classic_Fairytale:backstab +["But you're cannibals. It's what you do."] = "但你是食人族,这就是你做的事", -- A_Classic_Fairytale:enemy +["But you said you'd let her go!"] = "但你说了你会让她走!", -- A_Classic_Fairytale:journey +["But you saved me!"] = "但你救了我!", -- A_Classic_Fairytale:queen +["By the way, not only bazookas will bounce on water, but also greandes and many other things."] = "顺便说一下,不只是火箭炮会在水面弹跳,手榴弹和许多其他东西也行", -- Basic_Training_-_Bazooka +["By the way, not only bazookas will bounce on water, but also grenades and many other things."] = "顺便说一下,不只是火箭炮会在水面弹跳,手榴弹和许多其他东西也行", -- Basic_Training_-_Bazooka +["By the way, you can turn around without walking|by holding down Precise when you hit a walk control."] = "顺便说一下,你可以按着精确键+方向键不用走路就能转身", -- Basic_Training_-_Movement +["C-1"] = "C-1", -- portal +["C-2"] = "C-2", -- portal +["Callahan"] = "Callahan", -- +["Call me Beep! Well, 'cause I'm such a nice...person!"] = "叫我“Beep”,因为我是一个多好的……人!", -- A_Classic_Fairytale:family +["Cannibals"] = "食人族", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:first_blood +["Cannibal Sentry"] = "食人族哨兵", -- A_Classic_Fairytale:journey +["Cannibals?! You're the cannibals!"] = "食人族?!你才是食人族!", -- A_Classic_Fairytale:enemy +["Can you do it?"] = "你能做到吗?", -- A_Space_Adventure:ice02 +["Cappy"] = "Cappy", -- Basic_Training_-_Movement +["Captain Lime"] = "Captain Lime", -- A_Space_Adventure:fruit01, A_Space_Adventure:fruit02 +["Captain Lime offered his help if you assist him in battle."] = "如果你在战斗中帮助Captain Lime,他会给你提供帮助", -- A_Space_Adventure:fruit01 +["Capture The Flag"] = "夺取光环", -- Capture_the_Flag, CTF_Blizzard +["Careful, hedgehogs can't swim!"] = "小心,刺猬不会游泳!", -- Basic_Training_-_Movement +["Careless"] = "Careless", +["Carol"] = "Carol", -- A_Classic_Fairytale:family +["Challenge completed!"] = "挑战完成!", -- User_Mission_-_Rope_Knock_Challenge, User_Mission_-_That_Sinking_Feeling, SpeedShoppa +["CHALLENGE COMPLETE"] = "挑战完成", -- User_Mission_-_RCPlane_Challenge +["Challenge failed!"] = "挑战失败", -- SpeedShoppa +["Challenge objectives"] = "挑战目标", -- A_Space_Adventure:death02, A_Space_Adventure:desert03, A_Space_Adventure:final, A_Space_Adventure:fruit03, A_Space_Adventure:moon02 +["Challenge over!"] = "挑战结束", -- User_Mission_-_Rope_Knock_Challenge +["Challenge"] = "挑战", -- User_Mission_-_RCPlane_Challenge, User_Mission_-_Rope_Knock_Challenge, User_Mission_-_That_Sinking_Feeling, SpeedShoppa, ClimbHome +["Change bounciness: Tap [B]"] = "改变弹力: 按[B]", -- Basic_Training_-_Grenade +["Change Content: [Left], [Right]"] = "改变内容: [左]/[右]", -- HedgeEditor +["Change detonation timer: Tap the [Clock]"] = "改变引爆定时器: 按[Clock]", -- Basic_Training_-_Grenade, A_Classic_Fairytale:shadow +["Change direction: [Left]/[Right]"] = "改变方向: [左]/[右]", -- Basic_Training_-_Grenade +["Change Health Boost: [Left], [Right]"] = "改变加血: [左]/[右]", -- HedgeEditor +["Change Health: [Left], [Right]"] = "改变血量: [左]/[右]", -- HedgeEditor +["Change modification mode: [Left], [Right]"] = "改变修改模式: [左]/[右]", -- HedgeEditor +["Change Placement Mode: [Up], [Down]"] = "改变放置模式: [上]/[下]", -- HedgeEditor +["Change Rotation: [Left], [Right]"] = "改变旋转: [左]/[右]", -- HedgeEditor +["Change Sprite Frame: [Precise]+[Left], [Precise]+[Right]"] = "改变Sprite Frame: [精确]+[左],[精确]+[右]", -- HedgeEditor +["Change Sprite: [Left], [Right]"] = "改变Sprite: [左]/[右]", -- HedgeEditor +["Change Timer: [Left], [Right]"] = "改变定时器: [左]/[右]", -- HedgeEditor +["Change weapon: [Long jump] or [Slot 1]-[Slot 3]"] = "改变武器: [远跳]或[槽位1-3]", -- Tumbler +["Charmander"] = "Charmander", -- +["Chasing the blue hog"] = "追逐蓝刺猬", -- A_Space_Adventure:moon02 +["Cheater"] = "Cheater", -- User_Mission_-_RCPlane_Challenge +["Checkpoint reached!"] = "到达检查点!", -- A_Space_Adventure:cosmos, A_Space_Adventure:ice01, A_Space_Adventure:moon01 +["Chef"] = "厨师", -- Battalion, HedgeEditor +["Chester"] = "Chester", -- +["Chicken"] = "Chicken", -- +["Chief Sandologist"] = "Chief Sandologist", -- A_Space_Adventure:desert01 +["Chikorita"] = "Chikorita", -- +["Choose location: Left click"] = "选择位置: 左键", -- A_Classic_Fairytale:shadow +["Choose location: Tap the [Target] button, then tap on the spot you want to choose"] = "选择位置: 按[Target],然后按下你想选的位置", -- A_Classic_Fairytale:shadow +["Choose Selection/Placement/Deletion: [Left], [Right]"] = "选择/放置/删除: [左]/[右]", -- HedgeEditor +["Choose your continent wisely, as your decision will be permanent."] = "明智地选择你的大陆,你的决定会是永久的", -- Continental_supplies +["Choose your side! If you want to join the strange man, walk up to him.|Otherwise, walk away from him. If you decide to att...nevermind..."] = "选择你的阵营!如果你想加入奇怪的人,走近他。否则离远点。如果你决定攻……没什么……", -- A_Classic_Fairytale:shadow +["Chunli"] = "Chunli", -- +["Clark Kent"] = "Clark Kent", -- +["Cleaver"] = "菜刀", -- Construction_Mode +["Cleaver Placement Mode"] = "菜刀放置模式", -- Construction_Mode +["CLEAVER PLACEMENT MODE"] = "菜刀放置模式", -- HedgeEditor +["Climb Home"] = "爬回家", -- ClimbHome +["Closing in"] = "逼近", -- A_Classic_Fairytale:queen +["Clown"] = "小丑", -- HedgeEditor +["Clowns"] = "小丑", -- User_Mission_-_Nobody_Laugh +["Cluck-cluck time: [Fire an egg ~ Sabotages and cures poison ~ Cannot be fired close to another hog]"] = "Cluck-cluck 时间: [发射一个蛋,妨害和治疗中毒,不能靠得太近发射]", -- Continental_supplies +["Clumsy"] = "Clumsy", +["Cluster Bomb Training"] = "集束炸弹训练", -- Basic_Training_-_Cluster_Bomb +["Collateral Damage"] = "Collateral Damage", -- A_Classic_Fairytale:journey +["Collateral Damage II"] = "Collateral Damage II", -- A_Classic_Fairytale:journey +["- Collect all the blue crates"] = "- 收集所有蓝箱子", -- HedgeEditor +["Collect all the crates, but remember, our time in this life is limited!"] = "收集所有箱子,记住时间有限!", -- A_Classic_Fairytale:first_blood +["Collect or destroy all the health crates."] = "收集或破坏所有医疗箱", -- User_Mission_-_RCPlane_Challenge +["Collect or destroy the final crate to finish the training."] = "收集或破坏最后的箱子来完成训练", -- Basic_Training_-_Flying_Saucer +["- Collect the blue crate"] = "- 收集蓝箱子", -- HedgeEditor +["Collect the crate and attack!"] = "收集箱子并攻击!", -- WxW +["Collect the crate on the right."] = "收集右边的箱子", -- A_Classic_Fairytale:first_blood +["Collect the crates within the time limit!|If you fail, you'll have to try again."] = "在限制的时间内收集箱子,如果你失败了就要重来", -- A_Classic_Fairytale:first_blood +["Collect the first crate to begin!"] = "收集第一个箱子以开始", -- Basic_Training_-_Flying_Saucer +["Collect the freezer and get the device part from Thanta."] = "收集冰冻枪并从Thanta那里得到设备部件", -- A_Space_Adventure:ice01 +["Collect the green and purple invaders."] = "收集绿色和紫色入侵者", -- Space_Invasion +["Collect the remaining crates to complete the training."] = "收集剩余的箱子来完成训练", -- Basic_Training_-_Movement +["Collect the weapon crate and drop|a grenade from rope to destroy the barrels."] = "收集武器箱并从绳索丢下手榴弹来破坏油桶", -- Basic_Training_-_Rope +["Collect the weapon crate at the left coast!"] = "在左边海岸收集武器箱", -- A_Classic_Fairytale:journey +["Color Squad"] = "颜色小队", -- +["Come closer and die! … burp …"] = "靠近并去死!……打嗝……", -- A_Classic_Fairytale:queen +["Come closer, so that your training may continue!"] = "靠近点,这样你的训练就能继续", -- A_Classic_Fairytale:first_blood +["Comet"] = "Comet", -- Big_Armory +["Commander"] = "指挥官", -- HedgeEditor +["Compete to use as few planes as possible!"] = "比赛尽量少用飞机", -- User_Mission_-_RCPlane_Challenge +["Complete all main and side missions to complete the spacetrip mission."] = "完成所有主要和支线任务来完成太空旅行任务", -- A_Space_Adventure:cosmos +["Complete the obstacle course."] = "完成障碍课程", -- Basic_Training_-_Movement +["Complete the remaining side missions to complete this mission."] = "完成剩余的支线任务来完成这个任务", -- A_Space_Adventure:cosmos +["Complete the track as fast as you can!"] = "尽快完成赛道", +["Completion time: %.2fs"] = "完成时间: %.2f秒", -- User_Mission_-_Rope_Knock_Challenge +["Comrades! Sail me away!"] = "同志,我们走", -- A_Classic_Fairytale:queen +["Configuration accepted."] = "配置已接受", -- WxW +["Configuration phase"] = "配置阶段", -- WxW +["Congrats! You won!"] = "恭喜!你赢了!", -- A_Space_Adventure:moon01 +["Congratulations"] = "恭喜", -- Basic_Training_-_Rope +["Congratulations, you acquired the device part!"] = "恭喜,你得到了设备部件!", -- A_Space_Adventure:ice01 +["Congratulations, you are the best!"] = "恭喜,你是最棒的!", -- A_Space_Adventure:desert03 +["Congratulations, you are the fastest!"] = "恭喜,你是最快的!", -- A_Space_Adventure:moon02 +["Congratulations, you collected the device part!"] = "恭喜,你收集了设备部件!", -- A_Space_Adventure:ice01 +["Congratulations! You have completed the obstacle course!"] = "恭喜,你完成了障碍课程!", -- Basic_Training_-_Movement +["Congratulations! You have destroyed all targets within the time."] = "恭喜,你在时间内破坏了所有目标!", -- TargetPractice +["Congratulations, you have saved Hogera!"] = "恭喜,你救了Hogera!", -- A_Space_Adventure:final +["Congratulations! You have truly mastered this challenge! Don't forget to save the demo."] = "恭喜,你真正地掌握了这个挑战,别忘了保存demo", -- User_Mission_-_RCPlane_Challenge +["Congratulations! You've completed the Basic Rope Training!"] = "恭喜,你完成了基础绳索训练!", -- Basic_Training_-_Rope +["Congratulations! You've eliminated all targets|within the allowed time frame."] = "恭喜!你在规定时间内消灭了全部目标。", --Bazooka, Shotgun, SniperRifle +["Congratulations! You win."] = "恭喜,你赢了", -- Big_Armory +["Congratulations, you won!"] = "恭喜,你赢了!", -- A_Space_Adventure:death01, A_Space_Adventure:death02, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:fruit02, A_Space_Adventure:fruit03, A_Space_Adventure:ice02 +["Congratulations!"] = "恭喜", +["Conquering the galaxy"] = "征服星系", -- A_Space_Adventure:cosmos +["CONSTRUCTION MODE"] = "建造模式", -- Construction_Mode +["Construction Mode tool"] = "建造模式工具", -- Construction_Mode +["Construction Station: Allows placement of| girders, rubber, mines, sticky mines| and barrels."] = "建造站: 允许放置大梁、橡皮筋、地雷、黏性地雷和油桶", -- Construction_Mode +["Construction Station"] = "建造站", -- Construction_Mode +["Continental supplies"] = "大陆的物资", -- Continental_supplies +["Continent selection"] = "选择大陆", -- Continental_supplies +["Continents: Select a continent at the beginning."] = "大陆: 在开头选择一个大陆", -- Continental_supplies +["Control"] = "控制", -- Control +["Control pillars to score points."] = "控制柱子得分", +["Controls: Hold the Attack key (space by default) to|fire the rope, then, once attached use:|Left and Right to swing the rope;|Up and Down to contract and expand."] = "控制: 按着攻击键发射绳索,连上后|左和右摇晃,上和下伸缩", -- Basic_Training_-_Rope +["Copper"] = "Copper", -- User_Mission_-_Nobody_Laugh +["Corn"] = "Corn", -- A_Space_Adventure:fruit01 +["Corporal Calvin"] = "Corporal Calvin", -- +["Corporationals"] = "Corporationals", -- A_Classic_Fairytale:queen +["Corpsemonger"] = "Corpsemonger", -- A_Classic_Fairytale:shadow +["Corpse Thrower"] = "Corpse Thrower", -- A_Classic_Fairytale:epil +["Cost"] = "消耗", -- Construction_Mode +["Cost: %d"] = "消耗: %d", -- Construction_Mode +["Cotton Needer"] = "Cotton Needer", -- Mutant +["Count Hogula"] = "Count Hogula", -- +["Coward"] = "懦夫", -- A_Classic_Fairytale:queen +["Crate Before Attack: %s"] = "攻击之前的箱子: %s", -- WxW +["Crate Before Attack: You must collect a crate before you can attack."] = "攻击之前的箱子: 你必须在你能攻击之前收集一个箱子", -- WxW +["Crate Placer"] = "箱子放置器", -- Construction_Mode +["Crates: Crates drop more often with a higher chance of bonus ammo"] = "箱子: 箱子经常掉落有更高机会得到额外武器", -- Battalion +["Crates: Crates drop randomly and may be empty"] = "箱子: 箱子随机掉落而且可能是空的", -- Battalion +["Crates: Crates drop randomly with chance of being empty"] = "箱子: 箱子随机掉落,有可能是空的", -- Battalion +["Crates left: %d"] = "箱子还有: %d", -- User_Mission_-_RCPlane_Challenge +["Crates Left:"] = "箱子还有:", -- User_Mission_-_RCPlane_Challenge +["Crates per turn: %d"] = "每个回合的箱子: %d", -- WxW +["Crazy Gravity: Gravity randomly changes within a range from %i%% to %i%% with a period of %s"] = "疯狂的重力: 重力会随机改变,一段时间%s内从%i%%到%i%%", -- Gravity +["Crazy Runner"] = "Crazy Runner", -- A_Space_Adventure:moon02 +["Cricket Time"] = "板球时间", -- Continental_supplies +["Cricket time: [Fire away a 1 sec mine! ~ Cannot be fired close to another hog]"] = "板球时间: [发射一个一秒的地雷,不能靠得太近发射]", -- Continental_supplies +["CTF_Blizzard"] = "CTF_暴风雪", -- CTF_Blizzard +["Cursor: Build structure"] = "光标: 建造结构", -- Construction_Mode +["Cursor: Mode action"] = "光标: 模式动作", -- HedgeEditor +["|Cursor: Place crate"] = "|光标: 放置箱子", -- Construction_Mode +["Cursor: Place waypoint"] = "光标: 放置路径点", -- Racer +["Cutlass Cain"] = "Cutlass Cain", -- +["Cybernetic Empire"] = "Cybernetic Empire", +["Cyborg. It's what the aliens call themselves."] = "机器人,外星人这么称呼自己", -- A_Classic_Fairytale:enemy +["Dahmer"] = "Dahmer", -- A_Classic_Fairytale:backstab +["Daisy"] = "Daisy", -- +["DAMMIT, ROOKIE! GET OFF MY HEAD!"] = "新手,别站在我的头上", +["DAMMIT, ROOKIE!"] = "妈的,新手!", +["+%d ammo"] = "+%d弹药", -- Battalion +["+%d Ammo"] = "+%d弹药", -- Space_Invasion +["Dangerous Ducklings"] = "危险的小鸭子", +["Dark Strawberry"] = "Dark Strawberry", -- A_Space_Adventure:fruit02 +["+%d"] = "+%d", -- Battalion +["%d crate(s) remaining"] = "剩余%d箱子", -- SpeedShoppa +["%d damage was dealt in this game."] = "这场游戏造成了%d伤害", -- Mutant +["%d / %d"] = "%d / %d", -- Battalion +["%d | %d"] = "%d | %d", -- Mutant +["%d/%d"] = "%d/%d", -- SpeedShoppa +["Deadly Grape"] = "Deadly Grape", -- A_Space_Adventure:fruit02 +["Deadweight"] = "Deadweight", +["Deal 15 damage + 10% of your hog’s health to all hogs around you and get 2/3 back."] = "你附近的所有刺猬造成15伤害+你血量的10%,并返回2/3", -- Continental_supplies +["Deals 15 damage to all enemies in the circle."] = "对圈中所有敌人造成15伤害", -- Continental_supplies +["Deer"] = "Deer", -- +["Defeat all enemies!"] = "打败所有敌人!", -- portal +["Defeat!"] = "战胜!", -- HedgeEditor +["Defeat Professor Hogevil!"] = "打败Hogevil教授!", -- A_Space_Adventure:death01 +["Defeat the cannibals!"] = "打败食人族!", -- A_Classic_Fairytale:shadow +["Defeat the cannibals!|Grenade hint: set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"] = "打败食人族!|手榴弹提示: [1-5]设置定时器,[上]/[下]调整方向,[空格]蓄力投掷", -- A_Classic_Fairytale:shadow +["Defeat the cyborgs!"] = "打败机器人", -- A_Classic_Fairytale:enemy +["Defeat the enemy!"] = "打败敌人!", -- A_Classic_Fairytale:queen +["Delete Waypoint"] = "删除路径点", -- HedgeEditor +["Deletion Mode: [5]"] = "删除模式: [5]", -- HedgeEditor +["Deletion Mode"] = "删除模式", -- HedgeEditor +["Deletition Mode"] = "删除模式", -- HedgeEditor +["Demolition is fun!"] = "破坏很好玩!", +["Demo"] = "Demo", -- The_Specialists +["Dense Cloud"] = "Dense Cloud", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow, A_Classic_Fairytale:united +["Dense Cloud must have already told them everything..."] = "Dense Cloud一定已经告诉他们所有事情……", -- A_Classic_Fairytale:shadow +["Dense Cloud?! What are you doing?!"] = "Dense Cloud?!你在做什么?!", -- A_Classic_Fairytale:queen +["Depleted Kamikaze! +5 points!"] = "耗尽神风特攻队!+5分", -- Space_Invasion +["Derp"] = "Derp", -- User_Mission_-_Nobody_Laugh +["Desert Storm"] = "沙漠风暴", -- +["Destroy all targets with no more than 10 bazookas."] = "破坏所有目标,不超过10火箭炮", -- Basic_Training_-_Bazooka +["Destroy all targets with no more than 5 bazookas."] = "破坏所有目标,不超过5火箭炮", -- Basic_Training_-_Bazooka +["Destroy all the targets!"] = "破坏所有目标!", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["Destroyer of planes"] = "飞机破坏者", -- User_Mission_-_RCPlane_Challenge +["Destroy him, Leaks A Lot! He is responsible for the deaths of many of us!"] = "Leaks A Lot消灭他!他要为我们许多人的死亡负责", -- A_Classic_Fairytale:first_blood +["Destroy invaders and collect bonuses to score points."] = "消灭入侵者并收集奖励得分", -- Space_Invasion +["- Destroy the enemy"] = "- 消灭敌人", -- HedgeEditor +["- Destroy the red target"] = "- 破坏红色目标", -- HedgeEditor +["- Destroy the red targets"] = "- 破坏红色目标", -- HedgeEditor +["Destroy the targets!"] = "破坏目标", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["+%d flamer fuel!"] = "+%d喷火器燃料", -- Tumbler +["+%d health"] = "+%d血量", -- Mutant +["%d-Hit Combo! +%d points!"] = "%d连击,+%d分", -- Space_Invasion +["Did anyone follow you?"] = "有人跟着你吗?", -- A_Classic_Fairytale:united +["Did I miss something?"] = "我错过什么了吗?", -- Space_Invasion +["Did not finish"] = "没有完成", -- Racer, TechRacer +["Did you really think I've changed?"] = "你真的认为我改变了?", -- A_Classic_Fairytale:queen +["Did you really think that I've changed?"] = "你真的认为我改变了?", -- A_Classic_Fairytale:queen +["Did you really think that we needed the help of one of you?"] = "你真的认为我需要你们的帮助?", -- A_Classic_Fairytale:queen +["Did you see him coming?"] = "你有看到他来吗?", -- A_Classic_Fairytale:shadow +["Did you warn the village?"] = "你有警告村子吗?", -- A_Classic_Fairytale:shadow +["Die, die, die!"] = "死,死,死!", -- A_Classic_Fairytale:dragon +["Difficulty: "] = "难度: ", -- Continental_supplies +["Difficulty: Easy"] = "难度: 简单", -- A_Classic_Fairytale:first_blood +["Difficulty: Hard"] = "难度: 困难", -- A_Classic_Fairytale:first_blood +["Dimitry"] = "Dimitry", -- +["%d invaders have been destroyed in this game."] = "这场游戏有%d入侵者被消灭", -- Space_Invasion +["Disabled"] = "禁用", -- WxW +["Disguise as a Rockhopper Penguin"] = "假装成Rockhopper企鹅", -- Continental_supplies +["Disguise as a Rockhopper Penguin: [Swap place with a random enemy hog in the circle]"] = "假装成Rockhopper企鹅: [跟圈里的一个随机敌人交换位置]", -- Continental_supplies +["Displacer"] = "Displacer", -- +["Diver"] = "潜水员", -- User_Mission_-_Diver +["%d ms"] = "%d毫秒", -- HedgeEditor +["Doing stuff a monkey could do."] = "做一个猴子也能做的事情", -- A_Classic_Fairytale:queen +["Domination game"] = "支配游戏", -- Control +["Donald"] = "Donald", -- +["Do not destroy the crates!"] = "不要破坏箱子!", -- A_Space_Adventure:fruit02 +["Do not laugh, inexperienced one, for he speaks the truth!"] = "不要笑,经验不足的人,他讲的是事实!", -- A_Classic_Fairytale:backstab +["Do not let his words fool you, young one! He will stab you in the back as soon as you turn away!"] = "不要让他愚弄你,年轻人,他会在你转身后立刻在背后捅你一刀", -- A_Classic_Fairytale:first_blood +["Don't be foolish, son, there will be more."] = "别傻了,孩子,还会有更多", -- A_Space_Adventure:fruit01 +["Don't blow up the crate."] = "不要炸掉箱子", -- A_Classic_Fairytale:journey +["Don't destroy the device crate!"] = "不要破坏设备箱子!", -- A_Space_Adventure:desert01 +["Don't eliminate Captain Lime before collecting the last crate!"] = "在收集最后的箱子前不要消灭Captain Lime", -- A_Space_Adventure:fruit02 +["Don't hit me, you fools!"] = "别打我,你这个傻子!", -- A_Space_Adventure:moon01 +["Don't hit yourself!"] = "别打自己!", -- Basic_Training_-_Bazooka +["Don't touch the flames!"] = "不要碰火焰!", -- ClimbHome +["Don't you dare harming our tribe!"] = "你敢伤害我们的部落!", -- A_Classic_Fairytale:queen +["Double Kill!"] = "双杀!", +["Double kill!"] = "双杀!", -- Mutant +["Do you have any idea how bad an exploding arrow hurts?"] = "你知道爆炸箭的伤害有多糟糕吗?", -- A_Classic_Fairytale:queen +["Do you have any idea how valuable grass is?"] = "你知道这个草有多值钱吗?", -- A_Classic_Fairytale:enemy +["Do you have any idea what it's like in the village for a woman?"] = "你知道这对一个女人来说在村子里像什么?", -- A_Classic_Fairytale:queen +["Do you know where they are?"] = "你知道他们在哪吗?", -- A_Classic_Fairytale:queen +["Do you think you're some kind of god?"] = "你认为自己是某种神?", -- A_Classic_Fairytale:enemy +["Dragon's Lair"] = "龙的巢穴", -- A_Classic_Fairytale:dragon +["Dr. Banting"] = "Dr. Banting", -- +["Dr. Barnard"] = "Dr. Barnard", -- +["Dr. Blackwell"] = "Dr. Blackwell", -- +["Dr. Cornelius"] = "Dr. Cornelius", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01 +["Dr. Crushing"] = "Dr. Crushing", -- +["Dr. Drew"] = "Dr. Drew", -- +["Dr. Harvey"] = "Dr. Harvey", -- +["Dr. Hollows"] = "Dr. Hollows", -- +["Dr. Horace"] = "Dr. Horace", -- +["Drills"] = "Drills", -- A_Classic_Fairytale:backstab +["Drill Strike"] = "钻地空袭", -- Construction_Mode +["Dr. Jenner"] = "Dr. Jenner", -- +["Dr. Jung"] = "Dr. Jung", -- +["Drone Hunter! +10 points!"] = "Drone 猎人!+10分", -- Space_Invasion +["Drop a ball of dirt which turns into a|cluster on impact. Doesn’t end turn."] = "丢下一个泥球,撞击时变成一粒炸弹,不会结束回合", -- Continental_supplies +["Drop a bomb: [Drop some heroic wind that will turn into a bomb on impact]"] = "丢下炸弹: [丢下一些英雄的风,撞击时变成炸弹]", -- Continental_supplies +["- Dropped flags may be returned or recaptured"] = "- 掉落的光环可重新夺取或被送回", -- Capture_the_Flag +["Dropping a weapon while in water would just drown it, but launching one would work."] = "在水中丢下的武器会沉下去,发射的那个会起作用", -- Basic_Training_-_Flying_Saucer +["Drop weapon (while on rope): [Long Jump]"] = "丢下武器(在绳索时): [远跳]", -- Basic_Training_-_Rope +["Drowner"] = "Drowner", +["Dr. Parkinson"] = "Dr. Parkinson", -- +["Drunk greenhorn"] = "喝醉的新手", -- User_Mission_-_RCPlane_Challenge +["Drunk with power, perhaps!"] = "喝醉了有力量,大概吧", -- A_Classic_Fairytale:queen +["%d sec"] = "%d秒", -- Construction_Mode +["+%d seconds!"] = "+%d秒", -- Tumbler +["Dubloon Devil"] = "Dubloon Devil", -- +["Dude, all the plants are gone!"] = "老兄,所有植物都没了!", -- A_Classic_Fairytale:family +["Dude, can you see Ramon and Spiky?"] = "老兄,你能看到Ramon和Spiky吗?", -- A_Classic_Fairytale:journey +["Dude, it's unbearable!"] = "老兄,这是难以忍受的!", -- A_Classic_Fairytale:queen +["Dude, let me out!"] = "老兄,让我出去!", -- A_Classic_Fairytale:epil +["Dude, that outfit is so cool!"] = "老兄,这衣服真酷!", -- A_Classic_Fairytale:epil +["Dude, that's so cool!"] = "老兄,太酷了!", -- A_Classic_Fairytale:backstab +["Dude, this is boring!"] = "老兄,这很无聊!", -- A_Classic_Fairytale:queen +["Dude, we really need a new shaman..."] = "老兄,我们真的需要一个新的萨满……", -- A_Classic_Fairytale:shadow +["Dude, what's this place?!"] = "老兄,这是哪里?!", -- A_Classic_Fairytale:dragon +["Dude, where are we?"] = "老兄,我们在哪?", -- A_Classic_Fairytale:backstab +["Dude, wow! I just had the weirdest high!"] = "老兄,哇!我经历了最奇怪的事情!", -- A_Classic_Fairytale:backstab +["Dude, wow, you're so cute!"] = "老兄,哇,你真可爱!", -- A_Classic_Fairytale:queen +["Dud Mine Placement Mode"] = "哑弹地雷放置模式", -- HedgeEditor +["DUD MINE PLACEMENT MODE"] = "哑弹地雷放置模式", -- HedgeEditor +["Duration"] = "持续时间", -- Continental_supplies +["During the final testing of the device an accident happened."] = "在设备最后测试的时候,发生了意外", -- A_Space_Adventure:moon02 +["During the game you can get new RC planes by collecting the weapon crates."] = "在游戏中你可以收集武器箱子获得新的遥控飞机", -- A_Space_Adventure:desert03 +["Dust Storm"] = "沙尘暴", -- Continental_supplies +["Dust storm: [Deals 15 damage to all enemies in the circle]"] = "沙尘暴: [对圈中所有敌人造成15伤害]", -- Continental_supplies +["Each time you destroy all the targets on your current level you'll get teleported to the next level."] = "每次你破坏了当前关卡的所有目标后,你会传送到下一个关卡", -- A_Space_Adventure:desert03 +["Each time you play this missions enemy hogs will play in a random order."] = "每次你玩这个任务,敌人刺猬会随机顺序玩", -- A_Space_Adventure:death02 +["Each turn is only ONE SECOND!"] = "每个回合只有一秒!", -- Frenzy +["Each turn you get 1-3 random weapons"] = "每个回合你得到1-3随机武器", +["Each turn you get one random weapon"] = "每个回合你得到1随机武器", +["Each turn you'll have only one rope to use."] = "每个回合你只有一个绳索可用", -- A_Space_Adventure:moon02 +["Eagle Eye"] = "鹰眼", -- A_Classic_Fairytale:backstab +["Eagle Eye: [Blink to the impact ~ One shot]"] = "鹰眼: [闪现到子弹打中的位置,一发子弹]", -- Continental_supplies +["Ear Sniffer"] = "Ear Sniffer", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:epil +["EASY"] = "简单", -- Continental_supplies +["Eckles"] = "Eckles", -- User_Mission_-_Nobody_Laugh +["Eclipse"] = "Eclipse", -- Big_Armory +["Editing Commands: (Use while no weapon is selected)"] = "编辑命令: (未选择武器时使用)", -- HedgeEditor +["Ehm, okay ..."] = "嗯,好吧……", -- A_Space_Adventure:moon01 +["Elderbot"] = "Elderbot", -- A_Classic_Fairytale:family +["Elimate your captor."] = "消灭你的捕获者", -- User_Mission_-_The_Great_Escape +["Eliminate all targets before your time runs out.|You have unlimited ammo for this mission."] = "时间结束前消灭全部目标。无限弹药。", --Bazooka, Shotgun, SniperRifle +["Eliminate the enemy before the time runs out."] = "时间结束前消灭敌人", -- User_Mission_-_Diver, User_Mission_-_Spooky_Tree +["Eliminate the enemy hogs to win."] = "消灭敌人获胜", +["Eliminate the enemy."] = "消灭敌人", -- User_Mission_-_Bamboo_Thicket, User_Mission_-_Newton_and_the_Hammock, User_Mission_-_Nobody_Laugh +["Eliminate Unit 3378."] = "消灭单位3378", -- User_Mission_-_Teamwork +["Eliminate WatchBot 4000."] = "消灭监视机器人4000", -- User_Mission_-_Teamwork_2 +["Eliminate your captor."] = "消灭你的捕获者", -- User_Mission_-_The_Great_Escape +["Elite pilot"] = "精英飞行员", -- User_Mission_-_RCPlane_Challenge +["Elmo"] = "Elmo", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Enabled"] = "启用", -- WxW +["Enemy kills: Collect victim's weapons and +%d%% of its base health"] = "杀死敌人: 收集受害者的东西,+%d%%他的基础血量", -- Battalion +["Energetic Engineer"] = "充满活力的工程师", +["Engineer"] = "工程师", -- HedgeEditor, The_Specialists +["Enjoy the swim..."] = "游水愉快", +["Entered boredom phase! Discrepancies detected …"] = "进入厌倦阶段!检测到差异……", -- A_Classic_Fairytale:queen +["Epilogue"] = "后记", -- A_Classic_Fairytale:epil +["ERROR [getHogInfo]: Hog is nil!"] = "错误[getHogInfo]: 刺猬是nil!", -- Battalion +["Eugene"] = "Eugene", -- +["Europe"] = "欧洲", -- Continental_supplies +["Everyone knows this."] = "大家都知道这个", -- A_Classic_Fairytale:enemy +["Every single time!"] = "每一次!", -- A_Classic_Fairytale:dragon +["Everything looks OK..."] = "所有东西看起来没问题……", -- A_Classic_Fairytale:enemy +["Every time you kill an enemy hog your ammo will get reset next turn."] = "每次你杀死敌人,你的弹药会在下个回合重置", -- A_Space_Adventure:death02 +["Everywhere I look, I see hogs walking around …"] = "我看到的每个地方,都有刺猬在走动", -- A_Classic_Fairytale:epil +["Exactly, man! That was my dream."] = "太对了,那是我的梦", -- A_Classic_Fairytale:backstab +["Except me, of course! I just saved a whole planet!"] = "除了我,当然!我刚救了这个星球!", -- A_Space_Adventure:final +["Experienced beginner"] = "有经验的新手", -- User_Mission_-_RCPlane_Challenge +["Explore the tunnel with the other hedgehogs and search for the device."] = "和其他刺猬探索隧道找到设备", -- A_Space_Adventure:fruit02 +["Exploring the tunnel"] = "正在探索隧道", -- A_Space_Adventure:fruit02 +["Eye Chewer"] = "Eye Chewer", -- A_Classic_Fairytale:journey +["Fair Wind"] = "Fair Wind", -- +["Fall Damage"] = "坠落伤害", -- Basic_Training_-_Movement +["Fallen Angel"] = "Fallen Angel", -- Tentacle_Terror +["Family Reunion"] = "家庭团聚", -- A_Classic_Fairytale:family +["Fastest escape: %d turns"] = "最快逃离: %d回合", -- A_Space_Adventure:desert02 +["Fastest lap: %.3fs by %s"] = "最快一圈: %.3f秒 by %s", -- TrophyRace +["Fastest lap: "] = "最快一圈: ", +["Feeble Resistance"] = "Feeble Resistance", +["Fell From Grace"] = "Fell From Grace", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Fell From Heaven"] = "Fell From Heaven", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen +["Fell From Heaven is the best! Fell From Heaven is the greatest!"] = "Fell From Heaven是最好的!Fell From Heaven是最棒的!", -- A_Classic_Fairytale:family +["Femur Lover"] = "Femur Lover", -- A_Classic_Fairytale:shadow +["Fierce Competition! +8 points!"] = "凶猛的比赛!+8分", -- Space_Invasion +["Fiery Water"] = "Fiery Water", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:united +["Fiery Water?! Are you drunk again?"] = "Fiery Water,你又喝醉了?", -- A_Classic_Fairytale:queen +["Fighting instead of cultivating a beautiful friendship."] = "战斗而不是培养友谊", -- A_Classic_Fairytale:epil +["Fight: Press [Attack]"] = "战斗: 按[攻击]", -- A_Space_Adventure:fruit01 +["Filthy Blue"] = "Filthy Blue", -- User_Mission_-_Dangerous_Ducklings +["Final Challenge:"] = "最后的挑战:", -- Basic_Training_-_Rope +["Finally! We're out of this hellhole. Now go save the princess, %s!"] = "终于!我们从地狱出来了,现在去救公主,%s!", -- A_Classic_Fairytale:family +["Finally you are here!"] = "你终于来了", -- A_Space_Adventure:desert01, A_Space_Adventure:ice01 +["Final result"] = "最终结果", -- Mutant +["Final Targets"] = "最后的目标", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["Final team scores:"] = "最后的队伍分数:", -- Space_Invasion +["Find all the parts of the anti-gravity device."] = "找到反重力设备的所有部件", -- A_Space_Adventure:cosmos +["Find a way to detonate all the explosives and stay alive!"] = "想办法引爆所有爆炸物并活下来", -- A_Space_Adventure:final +["Find your tribe!|Cross the lake!"] = "找到你的部落!|穿过湖泊", -- A_Classic_Fairytale:dragon +["Finish this challenge as fast as possible to earn bonus points."] = "尽快完成这个挑战获得奖励分数", -- User_Mission_-_Rope_Knock_Challenge +["Finish waypoint placement"] = "完成路径点放置", -- Racer +["Finish your training."] = "完成你的训练", -- A_Classic_Fairytale:first_blood +["Finite Ropes"] = "有限的绳索", -- Basic_Training_-_Rope +["Fire a rocket with napalm."] = "发射带有凝固汽油弹的火箭", -- Continental_supplies +["Fire: [Precise]"] = "开火: [精确]", -- Space_Invasion, Tumbler +["Fire some exploding medicine that will heal 15 health to all hogs in its effect radius."] = "射击一些爆炸的药物会治疗它的影响半径内的所有刺猬", -- Continental_supplies +["Fire your hedgehog like a sticky mine."] = "发射你的刺猬,就像一个黏性地雷", -- Continental_supplies +["First aid kits?!"] = "急救包?!", -- A_Classic_Fairytale:united +["First Blood"] = "第一滴血", -- A_Classic_Fairytale:first_blood +["- First clan to capture the flag wins"] = "- 第一个夺得光环的战队赢", -- Capture_the_Flag +["- First clan to score %d captures wins"] = "- 第一个%d分的战队赢", -- Capture_the_Flag +["First killer will mutate"] = "第一个杀手会变异", -- Mutant +["First Steps"] = "第一步", -- A_Classic_Fairytale:first_blood +["- First team to capture the flag wins"] = "- 第一个夺得光环的队伍赢", -- Capture_the_Flag +["- First team to score %d captures wins"] = "- 第一个%d分的队伍赢", -- Capture_the_Flag +["Fishy"] = "Fishy", -- +["Flag captured!"] = "夺得光环!", +["Flag respawned!"] = "光环重生!", +["Flag returned!"] = "光环归还!", +["Flamer"] = "喷火器", +["Flaming Worm"] = "Flaming Worm", -- A_Classic_Fairytale:backstab +["Flare"] = "Flare", -- Continental_supplies +["Flawless victory!"] = "完美的胜利!", -- User_Mission_-_RCPlane_Challenge +["Flee: Press [Jump]"] = "逃走: 按[跳]", -- A_Space_Adventure:fruit01 +["Flesh for Brainz"] = "Flesh for Brainz", -- A_Classic_Fairytale:journey +["Flower Power"] = "Flower Power", -- Basic_Training_-_Rope +["Fly around and hurl explosives to your enemies."] = "到处飞并丢炸弹向敌人", -- Tumbler +["Flying Saucer Training"] = "飞碟训练", -- Basic_Training_-_Flying_Saucer +["Fly into space to fight off the invaders with barrels!"] = "飞到太空并用油桶击退入侵者", -- Space_Invasion +["Fly to the meteorite and detonate the explosives"] = "飞到陨石并引爆炸弹", -- A_Space_Adventure:cosmos +["Follow the path and destroy the next target."] = "沿着这条路,破坏下一个目标", -- Basic_Training_-_Rope +["For each kill you win %d seconds."] = "每次击杀获得%d秒", -- RopeKnocking +["Forgetfulness: You will lose all your weapons each turn."] = "健忘: 每个回合你会失去所有武器", -- Continental_supplies +["For the next crate, you have to do back jumps."] = "为拿到下一个箱子,你需要后跳", -- Basic_Training_-_Movement +["Four Eyes"] = "Four Eyes", -- +["Frankie"] = "Frankie", -- +["Frank"] = "Frank", -- User_Mission_-_Nobody_Laugh +["Free Dense Cloud and continue the mission!"] = "释放Dense Cloud并继续任务", -- A_Classic_Fairytale:journey +["FRENZY"] = "FRENZY", -- Frenzy +["Friendly Fire!"] = "友伤!", +["Friendly kills: Clear killer's pool and -%d%% of its base health"] = "杀死友军: 清除杀手的职业武器和工具,-%d%%他的基础血量", -- Battalion +["From the second turn and beyond the water rises."] = "从第二个回合开始水面上升", -- A_Space_Adventure:desert02 +["Frozen Bandits"] = "Frozen Bandits", -- A_Space_Adventure:ice01 +["Fruit"] = "水果", -- +["Fruit Assassins"] = "水果刺客", -- A_Space_Adventure:fruit02 +["Fruity"] = "Fruity", -- +["Fuel: %d"] = "燃料: %d", -- Tumbler +["Fuzzy Beard"] = "Fuzzy Beard", -- +["“g=150”, where 150 is 150% of normal gravity."] = "“g=150”就是正常重力的150%", -- Gravity +["“g=50, g2=150, period=4000” for gravity changing|from 50 to 150 and back with period of 4000 ms."] = "“g=50, g2=150, period=4000”,重力从50改成150,持续时间4000毫秒", -- Gravity +["Galaxy Guardians"] = "Galaxy Guardians", -- Big_Armory +["Game over!"] = "游戏结束", -- Space_Invasion +["Game Started!"] = "游戏开始!", +["Game? Was this a game to you?!"] = "游戏?这对你是一个游戏?!", -- A_Classic_Fairytale:enemy +["Gangsters"] = "Gangsters", -- +["GasBomb"] = "毒气炸弹", -- Continental_supplies +["Gas Gargler"] = "Gas Gargler", -- A_Classic_Fairytale:queen +["Gasp! A smuggler!"] = "喘气,一个走私者!", -- A_Space_Adventure:desert01 +["Gasp!"] = "喘气!", -- A_Space_Adventure:desert01 +["Gathering fruits all day long."] = "一天都在采集水果", -- A_Classic_Fairytale:queen +["Gear Placement Tool"] = "物体放置工具", -- HedgeEditor +["General information"] = "一般信息", -- Continental_supplies +["General information:"] = "一般信息", -- Continental_supplies +["General Lemon"] = "General Lemon", -- A_Space_Adventure:fruit01 +["Generator"] = "发电机", -- Construction_Mode +["Generator: Generates energy."] = "发电机: 产生能量", -- Construction_Mode +["Get Dense Cloud out of the pit!"] = "让Dense Cloud离开深坑", -- A_Classic_Fairytale:journey +["Get him, Spike!"] = "拿下他,Spike!", -- A_Space_Adventure:desert01 +["Get on over there and take him out!"] = "过去干掉他!", +["Get on the head of the mole."] = "到鼹鼠的头上去", -- A_Classic_Fairytale:first_blood +["Get past the flower."] = "穿过花朵", -- A_Classic_Fairytale:journey +["Get ready to fight!"] = "准备战斗!", -- A_Space_Adventure:moon01 +["Get that crate!"] = "拿到那个箱子!", -- A_Classic_Fairytale:first_blood +["Get the crate on the other side of the island!|"] = "拿到岛屿另一边的箱子|", -- A_Classic_Fairytale:journey +["Get the crate on the other side of the island."] = "拿到岛屿另一边的箱子", -- A_Classic_Fairytale:journey +["Get the final crate to the right to complete the training."] = "拿到最后的箱子到右边完成训练", -- Basic_Training_-_Movement +["Get the highest score to win."] = "拿到最高分获胜", -- Space_Invasion +["Get the next crate by jumping over the abyss."] = "跳过深渊拿到箱子", -- Basic_Training_-_Movement +["Getting ready"] = "准备好", -- A_Space_Adventure:cosmos, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:ice01, A_Space_Adventure:ice02, A_Space_Adventure:moon01 +["Getting Started"] = "开始了", -- Basic_Training_-_Rope +["Getting to the device"] = "取得设备", -- A_Space_Adventure:fruit02 +["Get to the crate using your flying saucer!"] = "用你的飞碟取得箱子", -- Basic_Training_-_Flying_Saucer +["Get to the target using your rope!"] = "用你的绳索到达目标", -- Basic_Training_-_Rope +["Get your teammates out of their natural prison and save the princess!"] = "让你的队友离开他们的自然监狱并且救公主", -- A_Classic_Fairytale:family +["Get your teammates out of their natural prison and save the princess!|Hint: Drilling holes should solve everything.|Hint: It might be a good idea to place a girder before starting to drill. Just saying.|Hint: All your hedgehogs need to be above the marked height!|Hint: Leaks A Lot needs to get really close to the princess!"] = "让你的队友离开他们的自然监狱并且救公主|提示: 钻洞能解决所有问题|提示: 在钻之前放一个大梁是好主意|提示: 你的所有刺猬都要在标记的高度之上|提示: Leaks A Lot需要非常靠近公主", -- A_Classic_Fairytale:family +["Giggles"] = "Giggles", -- +["Gimme Bones"] = "Gimme Bones", -- A_Classic_Fairytale:backstab +["Girder"] = "大梁", -- Construction_Mode +["Girder Placement Mode"] = "大梁放置模式", -- Construction_Mode +["GIRDER PLACEMENT MODE"] = "大梁放置模式", -- HedgeEditor +["Give a hog a preset identity and weapons"] = "给一个刺猬预设的身份和武器", -- HedgeEditor +["Give an entire team themed hats and names"] = "给一整个队伍主题帽子和名字", -- HedgeEditor +["Glark"] = "Glark", -- A_Classic_Fairytale:shadow +["Glasses"] = "Glasses", -- +["Glassy"] = "Glassy", -- +["Goal Definition Mode"] = "目标定义模式", -- HedgeEditor +["GOAL DEFINITION MODE"] = "目标定义模式", -- HedgeEditor +["Goal: Score %d points or more to win!"] = "目标: 得%d分或更多获胜", -- Mutant +["Go and collect the crate"] = "出发并收集箱子", -- A_Space_Adventure:cosmos +["Godai"] = "Godai", -- +["Go down and save these PAotH hogs!"] = "去下面救这些星球协会刺猬", -- A_Space_Adventure:moon01 +["Go, get him again!"] = "去,再抓到他", -- A_Space_Adventure:moon02 +["Goggles"] = "Goggles", -- +["Goggs"] = "Goggs", -- +["GO! GO! GO!"] = "GO! GO! GO!", +["Good birdy......"] = "乖鸟儿……", +["Good bye!"] = "再见", -- Basic_Training_-_Flying_Saucer +["Good idea, they'll never find us there!"] = "好主意,他们绝不会找到我们", -- A_Classic_Fairytale:united +["Good job!"] = "干得好", -- Basic_Training_-_Flying_Saucer, Basic_Training_-_Rope +["Good job! Defeat the rest of the aliens!"] = "干得好,打败剩下的外星人", -- A_Classic_Fairytale:queen +["Good job! Now destroy the final targets to finish the training."] = "干得好,现在破坏最后的目标完成训练", -- Basic_Training_-_Grenade +["Good luck!"] = "祝你好运", -- A_Space_Adventure:desert01, A_Space_Adventure:fruit02 +["Good luck...or else!"] = "祝你好远……或其他", -- A_Classic_Fairytale:journey +["Good luck out there!"] = "祝你好运", +["Good so far!"] = "到目前为止很好", +["Good to go!"] = "Good to go!", +["Good! You now control Cappy."] = "好,现在你控制Cappy了", -- Basic_Training_-_Movement +["Go on top of the flower."] = "到花的顶部", -- A_Classic_Fairytale:first_blood +["Go, quick!"] = "去,快点", -- A_Classic_Fairytale:backstab +["Gorkij"] = "Gorkij", -- A_Classic_Fairytale:journey +["Go surf!"] = "去冲浪", -- WxW +["Got 1 more saucer and 8 more seconds added to the clock"] = "获得1飞碟和8秒时间", -- A_Space_Adventure:ice02 +["Got 1 more saucer"] = "获得1飞碟", -- A_Space_Adventure:ice02 +["GOTCHA!"] = "GOTCHA", +["Go to Thanta and get the device part!"] = "接近Thanta拿到设备部件", -- A_Space_Adventure:ice01 +["Go to the surface!"] = "去到表面", -- A_Space_Adventure:fruit02 +["Go to the target."] = "去到目标", -- Basic_Training_-_Rope +["Go to the upper platform and get the weapons in the crates!"] = "到上面的平台拿箱子里的武器", -- A_Space_Adventure:moon01 +["Got the saucer!"] = "获得飞碟", -- A_Space_Adventure:cosmos +["Got to go back."] = "要回去了", -- A_Space_Adventure:cosmos +["Got you? You're acting weird."] = "懂你?你很奇怪", -- A_Classic_Fairytale:queen +["Grab mines/barrels: [High jump]"] = "拿走地雷/油桶: [高跳]", -- Tumbler +["Gravity: 100%"] = "重力: 100%", -- Gravity +["Great!"] = "很好", -- Basic_Training_-_Rope +["Great choice, Steve! Mind if I call you that?"] = "选得好,Steve,介意我这么叫你吗?", -- A_Classic_Fairytale:shadow +["Great! Let’s kill all these enemies, using portals."] = "很好,让我们用传送门杀死所有敌人", -- portal +["Great work! Now hit it with your Baseball Bat! |Tip: You can change weapon with 'Right Click'!"] = "干得好,现在用你的棒球棒打|提示: 你可以右键换武器", -- Basic_Training_-_Rope +["Great! You will be contacted soon for assistance."] = "很好,援助很快会联系你", -- A_Classic_Fairytale:shadow +["Green"] = "Green", -- +["Green Bananas"] = "Green Bananas", -- A_Space_Adventure:fruit01 +["Green double rings also give you a new flying saucer."] = "绿色双环会给你新的飞碟", -- A_Space_Adventure:ice02 +["Green Hog Grape"] = "Green Hog Grape", -- A_Space_Adventure:fruit01 +["Green hogs won't intentionally hurt you."] = "绿色刺猬不会故意伤害你", -- A_Space_Adventure:fruit01 +["Greenhorn"] = "新手", -- User_Mission_-_RCPlane_Challenge +["Green Lipstick Bullet"] = "绿色口红子弹", -- Continental_supplies +["Green lipstick bullet: [Poisonous, deals no damage]"] = "绿色口红子弹: [有毒,无伤害]", -- Continental_supplies +["Greetings, cloudy one!"] = "你好,Dense Cloud", -- A_Classic_Fairytale:shadow +["Greetings from the Navy, %s (%s), for being a distance of %d away from the mainland!"] = "海军的问候,%s(%s),远离大陆已有%d距离", -- ClimbHome +["Greetings, %s!"] = "你好,%s", -- A_Classic_Fairytale:dragon +["Grenades explode after 1 to 5 seconds (you decide)."] = "手榴弹在1-5秒后爆炸(由你决定)", -- Basic_Training_-_Grenade +["Grenades with high bounciness bounce a lot and behave chaotic."] = "高弹性的手榴弹跳得很高, 行为难测", -- Basic_Training_-_Grenade +["Grenade Training"] = "手榴弹训练", -- Basic_Training_-_Grenade +["Grenadiers"] = "手榴弹兵", -- Basic_Training_-_Grenade +["Grenadier"] = "手榴弹兵", -- Target_Practice_-_Grenade_easy, Target_Practice_-_Grenade_hard, HedgeEditor +["Grey"] = "Grey", -- +["Guards"] = "卫兵", -- A_Space_Adventure:cosmos +["Guile"] = "Guile", -- +["Guys, do you think there's more of them?"] = "伙计们,你们认为那里有更多食人族吗?", -- A_Classic_Fairytale:backstab +["HAHA!"] = "哈哈", -- A_Classic_Fairytale:enemy +["Haha!"] = "哈哈", -- A_Classic_Fairytale:united +["Haha! Come!"] = "哈哈,来", -- A_Classic_Fairytale:queen +["Hahahaha!"] = "哈哈哈", +["Haha, I love the look on your face!"] = "哈哈,我喜欢看你的表情", -- A_Classic_Fairytale:queen +["Haha, now THAT would be something!"] = "我也不会……", +["Haha, that was just a coincidence!"] = "哈哈,那只是一个巧合", -- A_Classic_Fairytale:queen +["Hammer"] = "锤子", -- Construction_Mode, Continental_supplies +["Hannibal"] = "Hannibal", -- A_Classic_Fairytale:epil +["Hapless Hogs"] = "Hapless Hogs", +[" Hapless Hogs left!"] = " Hapless Hogs left!", +["Happy with your race track?|Then stop building and start racing!"] = "对你的赛道满意吗?|停止建造开始比赛", -- Racer +["HARD"] = "困难", -- Continental_supplies +["Hard flying"] = "艰难飞行", -- A_Space_Adventure:ice02 +["Harris"] = "Harris", -- +["Harry Potter"] = "Harry Potter", -- +["Harry"] = "Harry", -- User_Mission_-_Nobody_Laugh +["H"] = "H", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01 +["Hatless Jerry"] = "Hatless Jerry", -- A_Classic_Fairytale:queen +["Have no illusions, your tribe is dead, indifferent of your choice."] = "不是幻觉,你的部落死了,无关你的选择", -- A_Classic_Fairytale:shadow +["Haven't found it yet ..."] = "还没找到……", -- A_Space_Adventure:desert01 +["Have we ever attacked you first?"] = "我们有先攻击你吗?", -- A_Classic_Fairytale:enemy +["H confirmed that there isn't such a PAotH activity logged."] = "H 确认这不是星球协会活动记录", -- A_Space_Adventure:desert01 +["Healing Station"] = "治疗站", -- Construction_Mode +["Healing Station: Heals nearby hogs."] = "治疗站: 治疗附近的刺猬", -- Construction_Mode +["Health and Mission Panel"] = "血量和任务面板", -- Basic_Training_-_Movement +["Health"] = "血量", -- Basic_Training_-_Movement +["Health Crate Placement Mode"] = "医疗箱放置模式", -- Construction_Mode +["HEALTH CRATE PLACEMENT MODE"] = "医疗箱放置模式", -- HedgeEditor +["Health: %d"] = "血量: %d", -- HedgeEditor +["Health: Hogs lose up to 7% base health per turn"] = "血量: 刺猬每个回合最高失去%7基础血量", -- Battalion +["Health Modification Mode"] = "血量修改模式", -- HedgeEditor +["HEALTH MODIFICATION MODE"] = "血量修改模式", -- HedgeEditor +["Heavenly Defense"] = "Heavenly Defense", -- Tentacle_Terror +["Heavy"] = "Heavy", +["Heavy Cannfantry"] = "Heavy Cannfantry", -- A_Classic_Fairytale:united +["Heckles"] = "Heckles", -- +["Heck, you even executed one of your own!"] = "你甚至执行了自己的一个", -- A_Classic_Fairytale:queen +["Hedge-cogs"] = "Hedge-cogs", -- A_Classic_Fairytale:enemy +["HEDGEEDITOR"] = "刺猬编辑器", -- HedgeEditor +["HedgeEditor tool"] = "刺猬编辑器工具", -- HedgeEditor +["Hedgehog"] = "刺猬", -- +["Hedgehog Projectile"] = "刺猬炮弹", -- Continental_supplies +["Hedgehog projectile: [Fire your hog like a Sticky Bomb]"] = "刺猬炮弹: [像黏性炸弹一样发射你的刺猬]", -- Continental_supplies +["Hedgehogs can not be deleted."] = "刺猬不能被删除", -- HedgeEditor +["Hedgehogs left: %d"] = "刺猬剩余: %d", -- User_Mission_-_That_Sinking_Feeling +["Hedgehogs will be revived after their death."] = "刺猬死后会被救活", -- Mutant +["Hedgehogs will start in the first waypoint."] = "刺猬会在第一个路径点开始", -- Racer +["Hedgibal Lecter"] = "Hedgibal Lecter", -- A_Classic_Fairytale:backstab +["He doesn't know it but this device is a part of the anti-gravity device."] = "他不知道这个,但这个设备是反重力设备的一部分", -- A_Space_Adventure:ice01 +["He has captured the rest of the PAotH team and awaits to capture you!"] = "他抓住了星球协会的剩余队伍,在等着抓你", -- A_Space_Adventure:moon01 +["Heh, it's not that bad."] = "水面在上升", +["Height over time"] = "高度图表", -- ClimbHome +["He is a very tough and very determined hedgehog. I would be extremely careful if I were you."] = "他是非常坚韧和坚定的刺猬,如果我是你,我会非常小心", -- A_Space_Adventure:moon02 +["Helena"] = "Helena", -- A_Space_Adventure:moon01 +["Hell Army"] = "Hell Army", -- portal +["Hello again, %s!"] = "再次问好,%s", -- A_Classic_Fairytale:family +["Help Disabled"] = "帮助已禁用", -- HedgeEditor +["Help Enabled"] = "帮助已启用", -- HedgeEditor +["Helpers: Each team starts with %d helper points"] = "工具: 每个队伍有%d工具分数", -- Battalion +["Helpers: Hogs will get 1 out of 2 helpers randomly each turn"] = "工具: 每个回合随机得到本职业两种之一工具", -- Battalion +["Help me, Leaks!"] = "救我,Leaks", -- A_Classic_Fairytale:journey +["Help me, please!!!"] = "救我,求你", -- A_Classic_Fairytale:journey +["Help me, please!"] = "救我,求求你", -- A_Classic_Fairytale:journey +["He moves like an eagle in the sky."] = "他移动像天空的鹰", -- A_Classic_Fairytale:first_blood +["He must be in the village already."] = "他一定已经在村子", -- A_Classic_Fairytale:journey +["HeneK"] = "HeneK", -- +["Here, let me help you!"] = "来,让我帮你", -- A_Classic_Fairytale:backstab +["Here, let me help you save her!"] = "来,让我帮你救她", -- A_Classic_Fairytale:family +["Here...pick your weapon!"] = "来……挑你的武器", -- A_Classic_Fairytale:first_blood +["Here! Take it!"] = "来,拿着它", -- A_Space_Adventure:ice01 +["Here we go!"] = "我们开始吧", -- A_Space_Adventure:moon01 +["Here you will find the current mission instructions."] = "在这里你能看到当前的任务指示", -- Basic_Training_-_Movement +["Here you will learn how to fly the flying saucer|and get so learn some cool tricks."] = "在这里你会学到怎么开飞碟|还有一些很酷的技巧", -- Basic_Training_-_Flying_Saucer +["Heroic Wind"] = "英雄的风", -- Continental_supplies +["He's so brave..."] = "他多勇敢……", -- A_Classic_Fairytale:first_blood +["He was the lab assistant of Dr. Goodhogan, the inventor of the anti-gravity device."] = "他是Dr.Goodhogan的实验室助手,反重力设备的发明者", -- A_Space_Adventure:moon02 +["He won't be selling us out anymore!"] = "他不会再出卖我们", -- A_Classic_Fairytale:backstab +["Hey, don't forget us! We still need to climb up!"] = "嘿,别忘了我们,我们还要爬上去", -- A_Classic_Fairytale:family +["Hey, guys!"] = "嘿,伙计们", -- A_Classic_Fairytale:backstab +["Hey guys!"] = "嘿,伙计们", -- A_Classic_Fairytale:united +["Hey! I was supposed to collect it!"] = "嘿,应该由我收集它", -- A_Space_Adventure:fruit02 +["Hey, %s! Finally you have come!"] = "嘿,%s,你终于来了", -- A_Space_Adventure:moon01 +["Hey, %s! Look, someone is stealing the saucer!"] = "嘿,%s,看,有人在偷飞碟", -- A_Space_Adventure:cosmos +["Hey! This is cheating!"] = "嘿,这是作弊", -- A_Classic_Fairytale:journey +["Hidden"] = "Hidden", -- portal +["High Gravity: Gravity is %i%%"] = "高重力: 重力是%i%%", -- Gravity +["High Jump: [Backspace]"] = "高跳: [Backspace]", -- Basic_Training_-_Movement +["High Jump: Tap the [Curvy Arrow] shortly"] = "高跳: 短按[Curvy Arrow]", -- Basic_Training_-_Movement +["--- Highland ---"] = "---高地---", -- Battalion +["Highlander: Eliminate hogs to take their weapons"] = "高地人: 消灭刺猬拿走他们的武器", -- Highlander +["Highland: Hogs get %d random weapons from their pool"] = "高地: 刺猬从他们的职业获得%d随机武器", -- Battalion +["--- Highland Mode ---"] = "---高地模式---", -- Battalion +["High Target"] = "高目标", -- Basic_Training_-_Bazooka +["Hightime"] = "高时间", -- A_Classic_Fairytale:first_blood +["Hightower"] = "高塔", -- +["Hill Guard"] = "Hill Guard", -- Bazooka_Battlefield +["Hi! Nice to meet you."] = "嗨,很高兴遇见你", -- A_Space_Adventure:ice01 +["--- Hint ---"] = "---提示---", -- Battalion +["Hint: Cinematics can be skipped with the [Precise] key."] = "提示: 对话可以按[精确]跳过", -- A_Classic_Fairytale:first_blood, A_Classic_Fairytale:shadow +["Hint: Drilling holes should solve everything."] = "提示: 钻洞可以解决所有问题", -- A_Classic_Fairytale:family +["Hint: Hold down [M] to review the mission texts."] = "提示: 按着[M]查看任务信息", -- A_Classic_Fairytale:first_blood +["Hint: If this mission panel disappears, you can|see it again by hitting the Pause or Quit key."] = "提示: 如果任务面板消失,按暂停或退出就能看到", -- Basic_Training_-_Movement +["Hint: It might be a good idea to place a girder before starting to drill. Just saying."] = "提示: 在钻地前放一个大梁是个好主意", -- A_Classic_Fairytale:family +["Hint: It might be easier if you vary the angle only slightly."] = "提示: 如果你稍微改变角度就会简单点", -- Basic_Training_-_Bazooka +["Hint: Just select the parachute, it opens automatically when you fall."] = "提示: 选择降落伞,它会在掉落时自动打开", -- A_Classic_Fairytale:first_blood +-- ["Hint: Kills won't transfer a hog's pool to the killer's pool"] = "", -- Battalion我没有翻译这一句,画蛇添足 +["Hint: Launch the bazooka horizontally at full power."] = "提示: 水平全力发射火箭炮", -- Basic_Training_-_Bazooka +["Hint: Pause the game to review the mission texts."] = "提示: 暂停游戏查看任务信息", -- A_Classic_Fairytale:first_blood +["Hint: Select the blow torch, aim and press [Fire]. Press [Fire] again to stop."] = "提示: 选择焊枪,瞄准并按[Fire], 再按[Fire]到顶部", -- A_Classic_Fairytale:journey +["Hint: Select the low gravity and press [Fire]."] = "提示: 选择低重力并按[Fire]", -- A_Classic_Fairytale:journey +["Hint: Select the rope, [Up] or [Down] to aim, [Attack] to fire, directional keys to move."] = "提示: 选择绳索,上下瞄准,攻击发射,方向移动", -- A_Classic_Fairytale:first_blood +["Hint: Select the Shoryuken and hit [Attack].|P.S.: You can use it mid-air."] = "提示: 选择升龙拳并按[攻击]|你可以在空中使用", -- A_Classic_Fairytale:first_blood +["Hint: %s needs to get really close to the princess!"] = "提示: %s需要非常靠近公主", -- A_Classic_Fairytale:family +["Hint: The rope only bends around objects.|When it doesn't hit anything, it's always straight."] = "提示: 绳索只在碰到物体时弯曲|没碰到东西,一直是直的", -- Basic_Training_-_Rope +["Hint: To jump higher, wait a bit before you hit “High Jump” a second time."] = "提示: 要跳得更高,等一下再跳第二次", -- Basic_Training_-_Movement +["Hint: To place a girder, select it,|then use [Left] and [Right] to select angle and length,|then choose a location for the girder."] = "提示: 要放置大梁,选择它|然后用[左]/[右]选择角度和长度|然后选择一个位置", -- A_Classic_Fairytale:shadow +["Hint: Use the quit key to see the team’s continent."] = "提示: 用退出键看队伍的大陆", -- Continental_supplies +["Hint: When you shorten the rope, you move faster!|And when you lengthen it, you move slower."] = "提示: 绳索短,移动快,绳索长,移动慢", -- Basic_Training_-_Rope +["Hint: you might want to stay out of sight and take all the crates...|"] = "提示: 你可能需要避免进入敌人的视线,拿到所有箱子……|", -- A_Classic_Fairytale:journey +["Hint: You might want to stay out of sight and take all the crates ..."] = "提示: 你可能需要避免进入敌人的视线,拿到所有箱子……", -- A_Classic_Fairytale:journey +["His arms are so strong!"] = "他的手臂多强壮!", -- A_Classic_Fairytale:first_blood +["hits"] = "hits", -- Basic_Training_-_Bazooka +["Hit the “Switch Hedgehog” key until you have|selected Cappy, the hedgehog with the cap!"] = "按“切换刺猬”键,直到你选中Cappy,有帽子的刺猬", -- Basic_Training_-_Movement +["Hmm … it's going slower than expected."] = "呃……它比想象中的要慢", -- A_Classic_Fairytale:queen +["Hmmm...actually...I didn't either."] = "呃……实际上……两个都不", -- A_Classic_Fairytale:enemy +["Hmmm, I’ll have to find some way of moving him off this anti-portal surface."] = "呃……我必须想办法把他从这个反传送门表层移走", -- portal +["Hmmm...it's a draw. How unfortunate!"] = "呃……这是平局,多不幸!", -- A_Classic_Fairytale:enemy +["Hmmm...perhaps a little more time will help."] = "呃……或许多一点时间会有帮助", -- A_Classic_Fairytale:first_blood +["Hmmm..."] = "呃...", +["Hm ... Now I ran out of fuel."] = "呃……现在没油了", -- A_Space_Adventure:cosmos +["Hog 100"] = "刺猬 100", -- A_Space_Adventure:fruit03 +["Hog 1"] = "刺猬 1", -- A_Space_Adventure:fruit03 +["Hog 3x5"] = "刺猬 3x5", -- A_Space_Adventure:fruit03 +["Hog 7+7"] = "刺猬 7+7", -- A_Space_Adventure:fruit03 +["Hog D"] = "刺猬 D", -- A_Space_Adventure:fruit03 +["Hog decar"] = "刺猬 decar", -- A_Space_Adventure:fruit03 +["Hog dertien"] = "刺猬 dertien", -- A_Space_Adventure:fruit03 +["Hog %d"] = "刺猬 %d", -- SimpleMission +["Hog EOF"] = "刺猬 EOF", -- A_Space_Adventure:fruit03 +["Hogera is definitely the last planet I saved!"] = "Hogera肯定是我救的最后一个星球", -- A_Space_Adventure:final +["Hogera is safe!"] = "Hogera安全了!", -- A_Space_Adventure:final +["Hog exi"] = "刺猬 exi", -- A_Space_Adventure:fruit03 +["Hog Hephaestus"] = "刺猬 Hephaestus", -- A_Space_Adventure:fruit03 +["Hog Identity Mode"] = "刺猬身份模式", -- HedgeEditor +["HOG IDENTITY MODE"] = "刺猬身份模式", -- HedgeEditor +["Hog III"] = "刺猬 III", -- A_Space_Adventure:fruit03 +["Hogminator"] = "Hogminator", -- A_Classic_Fairytale:family +["Hog nueve"] = "刺猬 nueve", -- A_Space_Adventure:fruit03 +["Hog octo"] = "刺猬 octo", -- A_Space_Adventure:fruit03 +["Hog onze"] = "刺猬 onze", -- A_Space_Adventure:fruit03 +["Hog Saturn"] = "刺猬 Saturn", -- A_Space_Adventure:fruit03 +["Hogs in sight!"] = "圈中有刺猬", -- Continental_supplies +["Hog Solo and GB"] = "刺猬 Solo 和绿色香蕉", -- A_Space_Adventure:fruit02 +["Hog Solo"] = "刺猬 Solo", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01, A_Space_Adventure:death02, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:desert03, A_Space_Adventure:final, A_Space_Adventure:fruit01, A_Space_Adventure:fruit02, A_Space_Adventure:fruit03, A_Space_Adventure:ice01, A_Space_Adventure:ice02, A_Space_Adventure:moon01, A_Space_Adventure:moon02 +["- Hogs will be revived"] = "- 刺猬会重生", -- Capture_the_Flag +["- Hogs will drop the flag when killed"] = "- 刺猬被杀会掉落光环", -- Capture_the_Flag +["Hog two"] = "刺猬 two", -- A_Space_Adventure:fruit03 +["Hold [Attack] to attach the rope."] = "长按[攻击]发射绳索", -- Basic_Training_-_Rope +["Hold the Attack key pressed for more power."] = "长按攻击键蓄力攻击", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["Holy shit!"] = "Holy shit!", -- Mutant +["Homing Bee"] = "蜜蜂枪", -- Construction_Mode +["Honda"] = "Honda", -- +["Honest Lee"] = "Honest Lee", -- A_Classic_Fairytale:enemy +["Hooks"] = "Hooks", -- +["Hooray! I actually did it! Hogera is safe!"] = "万岁!我真的做到了,Hogera安全了!", -- A_Space_Adventure:final +["Hooray! I've found it, now I have to get back to Captain Lime!"] = "万岁!我找到了,现在回去找Captain Lime", -- A_Space_Adventure:fruit02 +["Hooray! You are a champion!"] = "万岁!你是冠军!", -- A_Space_Adventure:ice02 +["Hopeless case"] = "绝望的情况", -- User_Mission_-_RCPlane_Challenge +["Hop on top of the next flower and advance to the left coast."] = "登上下一朵花的顶部,前进到左边海岸", -- A_Classic_Fairytale:journey +["Horns"] = "Horns", -- +["Hostage Situation"] = "人质情况", -- A_Classic_Fairytale:family +["How can I ever repay you for saving my life?"] = "你救了我,我要怎么报答?", -- A_Classic_Fairytale:journey +["How come in a village full of warriors, it's up to me to save it?"] = "怎么会在一个全是战士的村子,由我来拯救它?", -- A_Classic_Fairytale:dragon +["How could you betray us?"] = "你怎能背叛我们?", -- A_Classic_Fairytale:queen +["How difficult would you like it to be?"] = "你想玩什么难度?", -- A_Classic_Fairytale:first_blood +["HOW DO THEY KNOW WHERE WE ARE?"] = "他们怎么知道我们在哪?", -- A_Classic_Fairytale:united +["However, if you fail to do so, she dies a most violent death, just like your friend! Muahahaha!"] = "然而,如果你不这样做,她会死得很惨,就像你的朋友,哈哈哈", -- A_Classic_Fairytale:journey +["However, if you fail to do so, she dies a most violent death! Muahahaha!"] = "然而,如果你不这样做,她会死得很惨,哈哈哈", -- A_Classic_Fairytale:journey +["However, my mates don't agree with me on letting you go..."] = "然而,我的同伴不同意让你走……", -- A_Classic_Fairytale:dragon +["However, the army of %s is about to attack any moment now."] = "然而,%s的军队随时要进攻了", -- A_Space_Adventure:fruit01 +["How to Rope"] = "怎么使用绳索", -- Basic_Training_-_Rope +["How would you like being discriminated against?"] = "你想受到歧视吗?", -- A_Classic_Fairytale:queen +["Huh?"] = "哈?", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:queen +["Hunter"] = "猎人", --Bazooka, Shotgun, SniperRifle +["I ain't gonna sit around no more!"] = "我不要再呆在这里!", -- A_Classic_Fairytale:queen +["I already said I'm sorry!"] = "我已经说了我很抱歉!", -- A_Classic_Fairytale:epil +["I always suspected him!"] = "我一直怀疑他!", -- A_Classic_Fairytale:epil +["I am going to leave the kids play by themselves."] = "我打算让他们自己玩过家家", -- A_Classic_Fairytale:queen +["I am not ready for this planet yet. I should visit it when I have found all the other device parts."] = "我还没准备好去这个星球,我应该在找到所有设备部件后参观它", -- A_Space_Adventure:cosmos +["I am sorry but I was looking for a device that may be hidden somewhere around here."] = "抱歉,我在找可能藏在这里的一个设备", -- A_Space_Adventure:fruit02 +["I believe there's more of them."] = "我相信那里还有更多食人族", -- A_Classic_Fairytale:backstab +["I cannot let you go any further! … burp …"] = "我不能再让你走了……打嗝……", -- A_Classic_Fairytale:queen +["I can see you have been training diligently."] = "我能看到你一直在努力训练", -- A_Classic_Fairytale:first_blood +["I can't believe how blind we were."] = "我不能相信我们有多瞎", -- A_Classic_Fairytale:epil +["I can't believe it worked!"] = "我不敢相信它有用", -- A_Classic_Fairytale:shadow +["I can't believe this!"] = "我不相信这个!", -- A_Classic_Fairytale:enemy +["I can't believe what I'm hearing!"] = "我不敢相信听到了什么!", -- A_Classic_Fairytale:backstab +["I can't let you go further because …"] = "我不能再让你走了,因为……", -- A_Classic_Fairytale:queen +["I can't wait any more, I have to save myself!"] = "我不能再等了,我要救自己", -- A_Classic_Fairytale:shadow +["Ice Jake"] = "Ice Jake", -- A_Space_Adventure:ice01 +["I could just teleport myself there..."] = "我可以传送自己到那……", -- A_Classic_Fairytale:family +["Icy Girder: [3]"] = "冰大梁: [3]", -- HedgeEditor +["Icy Land: [3]"] = "冰地面: [3]", -- HedgeEditor +["Icy Land"] = "冰地面", -- HedgeEditor +["I'd better get going myself."] = "我最好自己去", -- A_Classic_Fairytale:journey +["Identity Thief"] = "Identity Thief", -- Mutant +["I didn't until about a month ago."] = "我一个月前才知道", -- A_Classic_Fairytale:enemy +["I don't care. It's worth a fortune! Good bye, you idiot!"] = "我不在乎,它值很多钱!再见,你这傻瓜!", -- A_Space_Adventure:fruit02 +["I don't know how you did that. But good work!|The next one should be easy as cake for you!"] = "我不知道你怎么做到的,但做得好|下一个对你应该很轻松", -- Basic_Training_-_Rope +["I don't know if I can forget what you've done!"] = "我希望我能忘记你做了什么", -- A_Classic_Fairytale:epil +["I don't know who I can trust anymore."] = "我不知道还能相信谁", -- A_Classic_Fairytale:epil +["I don't like your tone! You're hurting me!"] = "我不喜欢你的语气,你伤害了我", -- A_Classic_Fairytale:queen +["I feel something...a place! They will arrive near the circles!"] = "我感觉到了什么……一个地方!他们会达到圆圈附近", -- A_Classic_Fairytale:backstab +["If only I had a way..."] = "如果我有办法", -- A_Classic_Fairytale:backstab +["If only I were given a chance to explain my being here..."] = "如果我有机会解释我怎么在这里……", -- A_Classic_Fairytale:first_blood +["If only one enemy is left, you'll get bonus ammo."] = "如果还剩下一个敌人,你会得到额外弹药", -- A_Space_Adventure:death02 +["I forgot that she's the daughter of the chief, too..."] = "我也忘了她是首领的女儿", -- A_Classic_Fairytale:backstab +["I found it! Hooray!"] = "我找到它了,万岁", -- A_Space_Adventure:desert01 +["If some good old explosives were enough to save Hogera …"] = "如果爆炸物足以拯救Hogera", -- A_Space_Adventure:final +["If they try coming here, they can have a taste of my delicious knuckles!"] = "如果他们试图来这里,他们就能尝到我美味的拳头", -- A_Classic_Fairytale:united +["If you agree to provide the information we need, you will be spared!"] = "如果你提供我们需要的信息,我就放了你", -- A_Classic_Fairytale:shadow +["If you can get that crate fast enough, your beloved \"princess\" may go free."] = "如果你能尽快得到箱子,你最爱的“公主”就能自由", -- A_Classic_Fairytale:journey +["If you decide to help us, though, we will no longer need to find a new governor for the island."] = "如果你决定帮助我们,我就不需要给这个岛屿找一个新管理者", -- A_Classic_Fairytale:shadow +["If you don't want to slip away, you have to keep moving!"] = "如果你不想滑走,你必须保持移动", -- Basic_Training_-_Movement +["If you get stuck, use your Desert Eagle or restart the mission!"] = "如果你卡住了,使用沙漠之鹰或重开任务", -- A_Classic_Fairytale:journey +["If you get stuck, use your Desert Eagle or restart the mission!|"] = "如果你卡住了,使用沙漠之鹰或重开任务|", -- A_Classic_Fairytale:journey +["If you help us you can keep the device if you find it but we'll keep everything else."] = "如果你帮助我们,找到设备后可以带走,但我要其他东西", -- A_Space_Adventure:fruit02 +["If you hurt an enemy, you'll get one third of the damage dealt."] = "如果你伤害了一个敌人,你会得到造成伤害的三分之一", -- A_Space_Adventure:death02 +["If you injure a hedgehog you'll get 35% of the damage dealt."] = "如果你伤害了一个刺猬,你会得到造成伤害的35%", -- A_Space_Adventure:death02 +["If you just don’t care …"] = "如果你不在乎……", -- Continental_supplies +["If you kill a hedgehog with the respective weapon your health points will be set to 100."] = "如果你用各自的武器杀死一个刺猬,你的血量会设为100", -- A_Space_Adventure:death02 +["If you kill an enemy, your health will be set to 100."] = "如果你杀死一个敌人,你的血量会设为100", -- A_Space_Adventure:death02 +["If you know what I mean..."] = "如果你明白我的意思……", -- A_Classic_Fairytale:shadow +["If you miss a shot while trying to|re-attach, your rope is gone, too!"] = "如果你再次发射绳索失败,绳索就没了", -- Basic_Training_-_Rope +["If you say so..."] = "如果你这么说……", -- A_Classic_Fairytale:shadow +["If you skip a turn then the turn time left will be added to your next turn."] = "如果你跳过一个回合,剩余回合时间会加到下一回合", -- A_Space_Adventure:fruit03 +["If you wish to replay, there are other possible endings, too!"] = "如果你想再玩一次,还有其他可能的结局", -- A_Classic_Fairytale:epil +["Igmund"] = "Igmund", -- User_Mission_-_Nobody_Laugh +["I grew sick of the oppression! I broke free!"] = "我受够了压迫,我挣脱了!", -- A_Classic_Fairytale:queen +["I guess I can't go far without fuel!"] = "我猜没有燃料不能走多远", -- A_Space_Adventure:cosmos +["I guess we lost him!"] = "我猜我们跟丢他了", -- A_Space_Adventure:cosmos +["I guess you'll have to kill them."] = "我猜你必须杀了他们", -- A_Classic_Fairytale:dragon +["I have come to make you an offering..."] = "我来给你提供一个机会……", -- A_Classic_Fairytale:shadow +["I have heard that the local tribes say that many years ago some PAotH scientists were dumping their waste here."] = "我听本地部落说,很多年前一些星球协会科学家在这里倒垃圾", -- A_Space_Adventure:desert01 +["I have more important things to do!"] = "我还有很多重要的事要做", -- A_Classic_Fairytale:queen +["I have no idea where that mole disappeared...Can you see it?"] = "我不知道那鼹鼠消失到哪里……你能看到它吗?", -- A_Classic_Fairytale:shadow +["I have only 3 hogs available and they are all cadets."] = "我只有三个刺猬可用,他们都受过训练", -- A_Space_Adventure:fruit01 +["I have to follow that alien."] = "我必须跟着那个外星人", -- A_Classic_Fairytale:backstab +["I have to get back to the village!"] = "我必须回到村子", -- A_Classic_Fairytale:shadow +["I have to reach the surface as quickly as I can."] = "我必须尽快回到地面", -- A_Space_Adventure:desert02 +["I hope you are prepared for a small challenge, young one."] = "我希望你准备好应对一个小挑战,年轻人", -- A_Classic_Fairytale:first_blood +["I just don't want to sink to your level."] = "我只是不想陷入你的麻烦事", -- A_Classic_Fairytale:backstab +["I just forgot all checkpoints of incomplete missions."] = "我忘了没完成的任务的所有检查点", -- A_Space_Adventure:cosmos +["I just found out that they have captured your princess!"] = "我刚发现他们抓了你的公主", -- A_Classic_Fairytale:family +["I just want the strange device you found!"] = "我只想要你找到的奇怪设备", -- A_Space_Adventure:ice01 +["I just wonder where Ramon and Spiky disappeared..."] = "我只是好奇Ramon和Spiky消失去哪里……", -- A_Classic_Fairytale:journey +["I know and I'm terribly sorry!"] = "我知道,并且很抱歉", -- A_Classic_Fairytale:epil +["I know, my hero!"] = "我知道,我的英雄", -- A_Classic_Fairytale:epil +["I know that your resources are low due to the battle but I'll send two of my best hogs to assist you."] = "我知道因为战斗你的资源很少,但我会派出两个最好的刺猬帮助你", -- A_Space_Adventure:fruit02 +["I … like being with you, too."] = "我……也喜欢和你在一起", -- A_Classic_Fairytale:epil +["I'll get him!"] = "我会抓到他", -- A_Space_Adventure:cosmos +["I'll hold them off while you return to the village!"] = "我会在你回到村子时拖延他们", -- A_Classic_Fairytale:shadow +["I'll let you know whatever I know about him if you manage to catch me 3 times."] = "如果你能抓到我三次,我就告诉你关于他的事情", -- A_Space_Adventure:moon02 +["I'll make good use of it."] = "我会好好使用它", -- A_Space_Adventure:cosmos +["I'll protect you!"] = "我会保护你", -- A_Classic_Fairytale:epil +["I love Dense Cloud now!"] = "我现在喜欢Dense Cloud", -- A_Classic_Fairytale:epil +["I love you."] = "我爱你", -- A_Classic_Fairytale:epil +["I'm afraid I can't let you proceed!"] = "我恐怕不能让你继续下去", -- A_Classic_Fairytale:queen +["I'm afraid we cannot afford that."] = "我担心我们不能承担", -- A_Classic_Fairytale:queen +["Imagine those targets are the wolves that killed your parents! Take your anger out on them!"] = "想象那些目标是你的仇人,对他们发泄怒火", -- A_Classic_Fairytale:first_blood +["I'm...alive? How? Why?"] = "我……还活着?怎么?为什么?", -- A_Classic_Fairytale:backstab +["I'm a ninja."] = "我是一个忍者", -- A_Classic_Fairytale:dragon +["I marked the place of their arrival. You're welcome!"] = "我标记了他们到达的位置,不客气", -- A_Classic_Fairytale:backstab +["I may lost this battle, but I haven't lost the war yet!"] = "我输了这次战斗,但我还没输了这个战争", -- A_Space_Adventure:moon01 +["I'm certain that this is a misunderstanding, fellow hedgehogs!"] = "我确定这是误会,刺猬同志", -- A_Classic_Fairytale:first_blood +["I mean, none of you ceased to live."] = "我是说,你们都不会死", -- A_Classic_Fairytale:enemy +["I'm getting old for this!"] = "I'm getting old for this!", -- A_Classic_Fairytale:family +["I'm getting thirsty..."] = "我渴了……", -- A_Classic_Fairytale:family +["I'm glad this is over!"] = "我很高兴这一切结束了", -- A_Classic_Fairytale:epil +["I'm here to help you rescue her."] = "我来帮你救她", -- A_Classic_Fairytale:family +["I'm living a dream!"] = "我活在梦里", -- A_Classic_Fairytale:queen +["I'm not sure about that!"] = "我不确定", -- A_Classic_Fairytale:united +["IMPORTANT: To see the mission panel again, hold the mission panel key."] = "重要: 要再次看到任务面板,按任务面板键", -- Basic_Training_-_Movement +["IMPORTANT: To see the mission panel again, pause the game."] = "重要: 要再次看到任务面板,暂停游戏", -- Basic_Training_-_Movement +["Impressive...you are still dry as the corpse of a hawk after a week in the desert..."] = "令人印象深刻……", -- A_Classic_Fairytale:first_blood +["%i ms"] = "%i毫秒", -- Gravity +["I'm so glad this is finally over!"] = "我很高兴这一切终于结束了", -- A_Space_Adventure:final +["I'm so scared!"] = "我很害怕", -- A_Classic_Fairytale:united +["I'm still low on hogs. If you are not afraid I could use a set of extra hands."] = "我的刺猬还是很少,如果你不担心,我可以用一套额外的手", -- A_Space_Adventure:fruit02 +["I'm still with the aliens."] = "我还跟外星人在一起", -- A_Classic_Fairytale:queen +["I'm terribly sorry!"] = "我非常抱歉", -- A_Classic_Fairytale:queen +["I'm the spy! I've been giving you out!"] = "我是间谍,我一直在出卖你", -- A_Classic_Fairytale:queen +["In am also entrusting you with some rope."] = "我给你一些绳子", -- A_Space_Adventure:cosmos +["In case you haven't noticed, I'm a woman, too!"] = "如果你还没注意到,我也是一个女人", -- A_Classic_Fairytale:queen +["Increase the dust storm damage by sacrificing|your invulnerable ammo."] = "牺牲你的无敌的弹药来增加沙尘暴的伤害", -- Continental_supplies +["Incredible..."] = "不可思议的……", -- A_Classic_Fairytale:shadow +["Indestructible Girder: [2]"] = "不可破坏的大梁: [2]", -- HedgeEditor +["Indestructible Land: [2]"] = "不可破坏的地面: [2]", -- HedgeEditor +["Indestructible Land"] = "不可破坏的地面", -- HedgeEditor +["In each round, the worst hedgehog of the round is eliminated."] = "每个回合最差的刺猬会淘汰", -- TrophyRace +["I need to find the others!"] = "我需要找到其他人", -- A_Classic_Fairytale:backstab +["I need to get to the other side of this island, fast!"] = "我需要到达岛屿的一边,快", -- A_Classic_Fairytale:journey +["I need to move the tribe!"] = "我需要转移部落", -- A_Classic_Fairytale:united +["I need to prevent their arrival!"] = "我需要阻止他们到达", -- A_Classic_Fairytale:backstab +["I need to warn the others."] = "我需要警告其他人", -- A_Classic_Fairytale:backstab +["In fact, you are the only one that's been acting strangely."] = "事实上,你是唯一行为怪异的那个", -- A_Classic_Fairytale:backstab +["Initial health: %d"] = "初始血量: %d", -- Continental_supplies +["Initiate escape wish!"] = "发起逃跑愿望", -- A_Classic_Fairytale:queen +["In order to get to the other side, you need to get rid of the crates first."] = "为了到达另一边,你需要先得到箱子", -- A_Classic_Fairytale:dragon +["Insanity!"] = "神经病!", -- Mutant +["Inside %d"] = "%d里面", -- WxW +["Inside"] = "里面", -- WxW +["Instructions"] = "指令", -- Basic_Training_-_Flying_Saucer +["Instructor"] = "指导员", -- 01#Boot_Camp, User_Mission_-_Dangerous_Ducklings +["Insufficient Power"] = "能量不足", -- Construction_Mode +["Interesting idea, haha!"] = "有趣的主意,哈哈", -- A_Classic_Fairytale:enemy +["Interesting! Last time you said you killed a cannibal!"] = "有趣,上次你说你杀了一个食人族", -- A_Classic_Fairytale:backstab +["In the Ice Planet Flying Saucer Stadium ..."] = "在冰雪星球飞碟体育场", -- A_Space_Adventure:ice02 +["In the meantime, take these and return to your \"friend\"!"] = "同时,拿着这些回到你的“朋友”身边", -- A_Classic_Fairytale:shadow +["In the stadium, where the best pilots compete ..."] = "在体育场,最好的飞行员比赛的地方", -- A_Space_Adventure:ice02 +["In this accident, Professor Hogevil lost all his spines on his head!"] = "在这个事故,Hogevil教授失去了头上所有的刺", -- A_Space_Adventure:moon02 +["In this mission you get %d%% fuel."] = "在这个任务你得到了%d%%燃料", -- User_Mission_-_Diver +["In this mission you have infinite time."] = "在这个任务你有无限时间", -- portal +["Invalid Placement"] = "无效的放置", -- Construction_Mode +["Invasion"] = "入侵", -- A_Classic_Fairytale:united +["In your best (and only) flight you took out %d crates with one RC plane!"] = "在你最好的(并且唯一)的飞行中,一个遥控飞机得到了%d箱子", -- User_Mission_-_RCPlane_Challenge +["In your best flight you took out %d crates with one RC plane."] = "在你最好的飞行中,一个遥控飞机得到了%d箱子", -- User_Mission_-_RCPlane_Challenge +["I regret to end your little odyssey."] = "我后悔结束你的小奥德赛", -- A_Classic_Fairytale:queen +["I saw it with my own eyes!"] = "我亲眼看到了", -- A_Classic_Fairytale:shadow +["I see..."] = "我看看……", -- A_Classic_Fairytale:shadow +["I see you already took care of your enemies."] = "我看到你已经照顾了你的敌人", -- A_Classic_Fairytale:dragon +["I see you have already taken the leap of faith."] = "我看到你已经做了信仰之跃", -- A_Classic_Fairytale:first_blood +["I see you would like his punishment to be more...personal..."] = "我看你会希望他受到的处罚更……个人的……", -- A_Classic_Fairytale:first_blood +["I sense another wave of cannibals heading my way!"] = "我感觉到另一波食人族正在前来", -- A_Classic_Fairytale:backstab +["I sense another wave of cannibals heading our way!"] = "我感觉到另一波食人族正在前来", -- A_Classic_Fairytale:backstab +["%i s"] = "%i秒", -- Gravity +["I should get myself a portal device, maybe this crate has one."] = "我应该弄一个传送设备,这个箱子可能有一个", -- portal +["I should go now, goodbye!"] = "我现在该走了,再见", -- A_Space_Adventure:moon02 +["I shouldn't have drunk that last pint."] = "我不应该喝那么多", -- A_Classic_Fairytale:dragon +["Is this place in my head?"] = "这个地方在我的头里面?", -- A_Classic_Fairytale:dragon +["I still can't believe he sold us out like that."] = "我还是不能相信他就这样出卖我们", -- A_Classic_Fairytale:epil +["I still can't believe you forgave her!"] = "我还是不能相信你原谅她", -- A_Classic_Fairytale:epil +["I still have to get rid of the crates."] = "我还是必须拿到箱子", -- A_Classic_Fairytale:dragon +["Itami"] = "Itami", -- +["It doesn't matter. I won't let that alien hurt my daughter!"] = "没关系,我不会让那个外星人伤害我的女儿", -- A_Classic_Fairytale:dragon +["I think I love you!"] = "我想我喜欢你", -- A_Classic_Fairytale:epil +["I think we are safe here."] = "我想我们在这里是安全的", -- A_Classic_Fairytale:backstab +["I thought their shaman died when he tried our medicine!"] = "我想他们的萨满在吃我们的药时死了", -- A_Classic_Fairytale:shadow +["It is called 'Hogs of Steel'."] = "叫做“铁之刺猬”", -- A_Classic_Fairytale:enemy +["It is time to practice your fighting skills."] = "是时候练习你的战斗技能", -- A_Classic_Fairytale:first_blood +["It must be a childhood trauma..."] = "这一定是童年创伤", -- A_Classic_Fairytale:family +["It must be the aliens!"] = "这一定是外星人", -- A_Classic_Fairytale:backstab +["It must be the aliens' deed."] = "这一定是外星人的行为", -- A_Classic_Fairytale:backstab +["It must be the cyborgs again!"] = "这一定又是机器人", -- A_Classic_Fairytale:enemy +["It needs some practice, but you have infinite lives."] = "这需要一些训练,但你有无限的生命", -- Basic_Training_-_Rope +["I told you, I just found them."] = "我告诉过你,这是捡到的", -- A_Classic_Fairytale:backstab +["It only works in teleportation nodes of your own clan."] = "这只有在你的战队的传送节点生效", -- Construction_Mode +["It's a good thing SUDDEN DEATH is 99 turns away..."] = "我们很快就要淹没了", +["It's all about the right carrots, you know."] = "一切都是关于正确的胡萝卜,你知道", -- A_Classic_Fairytale:epil +["It's always up to women to clear up the mess men created!"] = "一直是女人在清理男人造成的脏乱", -- A_Classic_Fairytale:dragon +["It's amazing how quickly our lives can change."] = "我们的生活这么快就改变了", -- A_Classic_Fairytale:epil +["It's an ancient ritual of theirs."] = "这是他们的古代仪式", -- A_Classic_Fairytale:queen +["IT'S A SERIOUS MEDICAL CONDITION!"] = "这是一个严重的身体状况", -- A_Classic_Fairytale:queen +["It's a shame, I forgot how to do that!"] = "很丢脸,我忘了怎么做", -- A_Classic_Fairytale:family +["It's a shame, really!"] = "丢脸,真的", -- A_Classic_Fairytale:queen +["It seems that Professor Hogevil has prepared for your arrival!"] = "看来Hogevil教授已经准备好迎接你的到来", -- A_Space_Adventure:moon01 +["It's empty!"] = "这是空的", -- Battalion +["It's impossible to communicate with the spirits without a shaman."] = "有可能不需要萨满,和灵魂交流", -- A_Classic_Fairytale:shadow +["It's not that easy, so listen carefully:"] = "不是那么简单,所以认真听:", -- Basic_Training_-_Flying_Saucer +["It's over..."] = "结束了", -- A_Classic_Fairytale:shadow +["It's precious to me!"] = "这对我来说很珍贵", -- A_Classic_Fairytale:queen +["It's time you learned that your actions have consequences!"] = "是时候你该知道你的行动的后果", -- A_Classic_Fairytale:journey +["It's worth more than wood!"] = "这比木头值钱", -- A_Classic_Fairytale:enemy +["It's your fault you're there!"] = "你在那,是你的错", -- A_Classic_Fairytale:epil +["It wants our brains!"] = "它想要我们的脑子", -- A_Classic_Fairytale:shadow +["It was all a trick?!"] = "这是一个恶作剧?", -- A_Classic_Fairytale:queen +["It was all just bad luck!"] = "只是运气不好", -- ClimbHome +["It was completely useless!"] = "设备完全没用", -- A_Space_Adventure:final +["It was fun to watch."] = "看着很有趣", -- A_Classic_Fairytale:queen +["It was fun to watch, though."] = "看着很有趣", -- A_Classic_Fairytale:queen +["It was not a dream, unwise one!"] = "这不是梦,愚蠢的人", -- A_Classic_Fairytale:backstab +["It wasn't her fault!"] = "这不是她的错", -- A_Classic_Fairytale:epil +["It would be wiser to steal the space ship while the PAotH guards are taking a brake!"] = "最好是在星球协会卫兵休息时偷走太空船", -- A_Space_Adventure:cosmos +["Ivan"] = "Ivan", -- +["I've made it! Yeah!"] = "我做到了,耶!", -- A_Space_Adventure:moon01 +["I've seen this before. They just appear out of thin air."] = "我之前见过,他们就这样在空中出现", -- A_Classic_Fairytale:united +["I've thought that the best way to get the device is to let you collect most of the parts for me!"] = "我想得到设备的最好方法是你为我收集大部分部件", -- A_Space_Adventure:death01 +["I want to play a game..."] = "我想玩一个游戏", -- A_Classic_Fairytale:journey +["I want to see how it handles this!"] = "我想看它怎么处理这个", -- A_Classic_Fairytale:backstab +["I was heading home, you see!"] = "我在回家,你看!", -- A_Classic_Fairytale:queen +["I was so scared."] = "我很害怕", -- A_Classic_Fairytale:epil +["I was told that as the leader of the king's guard, no one knows this world better than you!"] = "有人告诉我,作为国王卫兵的首领,没人比你更懂这个世界", -- A_Space_Adventure:fruit01 +["I will never hand you the parts!"] = "我绝不会把部件交给你", -- A_Space_Adventure:death01 +["I wish to help you, %s!"] = "我希望帮助你,%s", -- A_Classic_Fairytale:dragon +["I wonder where Dense Cloud is..."] = "我想知道Dense Cloud在哪……", -- A_Classic_Fairytale:journey, A_Classic_Fairytale:shadow +["I wonder why I'm so angry all the time..."] = "我想知道为什么我总是那么生气", -- A_Classic_Fairytale:family +["I won't let you kill her!"] = "我不会让你杀了她", -- A_Classic_Fairytale:journey +["I won't let you kill the tribe!"] = "我不会让你杀了部落", -- A_Classic_Fairytale:queen +["I would gladly help you if we won this battle but under these circumstances I'll only help you if you fight for our side."] = "如果我们赢了这场战斗,我很高兴帮助你,但这种情况下,你站在我们这边才会得到帮助", -- A_Space_Adventure:fruit01 +["Jack"] = "Jack", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Jason"] = "Jason", -- +["Jeremiah"] = "Jeremiah", -- A_Classic_Fairytale:dragon +["Jetpack"] = "Jetpack", -- Big_Armory +["Jigglypuff"] = "Jigglypuff", -- +["Jim Morgan"] = "Jim Morgan", -- +["Jimmy"] = "Jimmy", -- +["Jingo"] = "Jingo", -- +["Joe"] = "Joe", -- A_Space_Adventure:moon01 +["John"] = "John", -- A_Classic_Fairytale:journey +["John Snow"] = "John Snow", -- A_Space_Adventure:ice01 +["Jolly Roger"] = "Jolly Roger", -- +["Jones"] = "Jones", -- +["Judas"] = "Judas", -- A_Classic_Fairytale:backstab +["Juicy"] = "Juicy", -- +["Jumping"] = "跳跃", -- Basic_Training_-_Movement +["Jumping is disabled"] = "跳跃已禁用", +["Just kidding, none of you have died!"] = "开玩笑的,你们不会死", -- A_Classic_Fairytale:enemy +["Just look at Leaks, may he rest in peace!"] = "看看Leaks,愿他安息", -- A_Classic_Fairytale:queen +["Just on a walk."] = "只是散步", -- A_Classic_Fairytale:united +["Just wait till I get my hands on that trauma! ARGH!"] = "只要等到我把手放在那外伤,啊", -- A_Classic_Fairytale:family +["Kaboom!"] = "Kaboom", -- Basic_Training_-_Flying_Saucer +["Kaboom! Hahahaha! Take this, stupid meteorite!"] = "Kaboom, 哈哈哈,笨陨石", -- A_Space_Adventure:final +["Kamikaze"] = "神风特攻队", -- Construction_Mode +["Kamikaze Expert! +15 points!"] = "神风特攻队专家,+15分", -- Space_Invasion +["Keep it up!"] = "保持下去", +["Ken"] = "Ken", -- +["Kenshi"] = "Kenshi", -- +["Kerguelen"] = "Kerguelen", -- Continental_supplies +["key."] = "key.", -- Continental_supplies +["Kill all enemy hedgehogs in a single turn."] = "在一个回合杀死所有敌人", -- Big_Armory +["Kill him or skip your turn."] = "杀死他,或者跳过", -- A_Classic_Fairytale:backstab +["Killing spree!"] = "杀戮狂欢", +["Killing the specialists"] = "杀死专家", -- A_Space_Adventure:death02 +["KILL IT!"] = "杀了它", -- A_Classic_Fairytale:first_blood +["Kills: %d"] = "杀死: %d", -- Space_Invasion +["Kill the aliens!"] = "杀死外星人", -- A_Classic_Fairytale:dragon +["Kill the cannibal!"] = "杀死食人族", -- A_Classic_Fairytale:first_blood +["Kill The Leader"] = "杀死领先者", -- WxW +["Kill The Leader: You must also hit the team with the most health."] = "杀死领先者: 你必须攻击最多血量的队伍", -- WxW +["Kill the traitor, %s, or spare his life!"] = "杀死叛徒,%s,或饶他一命", -- A_Classic_Fairytale:backstab +["--- King ---"] = "---国王---", -- Battalion +["King"] = "国王", -- Battalion +["--- King Mode ---"] = "---国王模式", -- Battalion +["Knight"] = "骑士", -- Battalion +["Knives"] = "Knives", -- +["Knockball"] = "Knockball", -- Knockball +["Knockball weapon"] = "Knockball武器", -- Knockball +["Knock off the enemies from the left-most place of the map!"] = "在地图的最左边击退敌人", -- A_Space_Adventure:fruit01 +["koda"] = "Koda", -- +["Kostya"] = "Kostya", -- +["Lady Mango"] = "Lady Mango", -- A_Space_Adventure:fruit01, A_Space_Adventure:fruit02 +["LandFlag Modification Mode"] = "LandFlag修改模式", -- HedgeEditor +["Land mines explode instantly."] = "地雷立即爆炸", -- User_Mission_-_Teamwork_2 +["Lassard"] = "Lassard", -- +["Last Resort: Having less than 25% base health gives kamikaze"] = "最后的手段: 少于25%基础血量给神风特攻队", -- Battalion +["Last Target!"] = "最后的目标", +["Launch a bouncy ball which explodes into a crate."] = "发射一个弹性球,爆炸后变成箱子", -- Continental_supplies +["Launch some bazookas to destroy the targets!"] = "发射一些火箭炮破坏目标", -- Basic_Training_-_Bazooka +["Leader"] = "Leader", -- A_Classic_Fairytale:enemy +["Leaderbot"] = "Leaderbot", -- A_Classic_Fairytale:queen +["Lead your allies to battle and eliminate all the enemies!"] = "带领你的盟友战斗,消灭所有敌人", -- A_Space_Adventure:fruit01 +["Leaks A Lot"] = "Leaks A Lot", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:journey, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow, A_Classic_Fairytale:united +["Leaks A Lot, depressed for killing his loved one, failed to save the village..."] = "Leaks A Lot很沮丧,他喜欢的人死了,没能拯救村子,", -- A_Classic_Fairytale:journey +["Leaks A Lot gave his life for his tribe! He should have survived!"] = "Leaks A Lot 为部落付出生命,他应该活下来的", -- A_Classic_Fairytale:first_blood +["Leaks A Lot must survive!"] = "Leaks A Lot 一定还活着", -- A_Classic_Fairytale:journey +["Leap of Faith"] = "信仰之跃", -- Basic_Training_-_Movement +["Led Heart"] = "Led Heart", -- A_Classic_Fairytale:queen +["Lee"] = "Lee", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Left and right"] = "左和右", -- WxW +["Left, right and roof"] = "左,右和顶部", -- WxW +["[Left], [Right]: Change between identities."] = "[左]/[右]: 改变身份", -- HedgeEditor +["[Left], [Right]: Change health value."] = "[左]/[右]: 改变血量", -- HedgeEditor +["Left/right: Choose crate contents"] = "[左]/[右]: 选择箱子内容", -- Construction_Mode +["Left/right: Choose structure type"] = "[左]/[右]: 选择结构类型", -- Construction_Mode +["Left/right: Choose structure type|Cursor: Build structure"] = "[左]/[右]: 选择结构类型|光标: 建造结构", -- Construction_Mode +["Legs"] = "Legs", -- +["Less tools, more fun"] = "更少工具,更多乐趣", -- Battalion +["Lestat"] = "Lestat", -- portal +["Let a continent provide your weapons!"] = "让一个大陆提供你的武器", -- Continental_supplies +["Let me test your skills a little, will you?"] = "让我测试你的技能,你愿意?", -- A_Classic_Fairytale:journey +["Let's get started!"] = "让我们开始", -- Basic_Training_-_Bazooka +["Let's go!"] = "Let's go!", -- A_Space_Adventure:moon02 +["Let's go home!"] = "让我们回家", -- A_Classic_Fairytale:journey +["Let's go, %s!"] = "Let's go,%s!", -- WxW +["Let's head back to the village!"] = "让我们回到村子", -- A_Classic_Fairytale:shadow +["Let's see what your comrade does now!"] = "让我们看看你的同志现在做什么", -- A_Classic_Fairytale:journey +["Let's show those cannibals what we're made of!"] = "让我们向那些食人族展示我们是什么做成的", -- A_Classic_Fairytale:backstab +["Let them have a taste of my fury!"] = "让他们尝尝我的狂怒", -- A_Classic_Fairytale:backstab +["Let us help, too!"] = "让我们帮忙", -- A_Classic_Fairytale:backstab +["Level 1 clear!"] = "关卡1完成", -- A_Space_Adventure:desert03 +["Level 2 clear!"] = "关卡2完成", -- A_Space_Adventure:desert03 +["Level Data Saved!"] = "关卡数据已保存", -- HedgeEditor +["Lightbender"] = "Lightbender", -- +["Light Cannfantry"] = "Light Cannfantry", -- A_Classic_Fairytale:united +["Limited Ammo"] = "有限弹药", -- Basic_Training_-_Bazooka +["Listen carefully! The bandit leader, Thanta, has recently found a very strange device."] = "认真听,强盗首领Thanta刚找到了一个非常奇怪的设备", -- A_Space_Adventure:ice01 +["Listen up, maggot!"] = "听好,小子!", -- User_Mission_-_Dangerous_Ducklings +["Listen up, maggot!!"] = "听好,小子!!", +["Little did they know that this hunt will mark them forever..."] = "他们不知道会永远记住这次打猎……", -- A_Classic_Fairytale:shadow +["Little Obstacle Course"] = "小障碍课程", -- Basic_Training_-_Rope +["Lively Lifeguard"] = "精力充沛的救生员", +["Lonely Cries"] = "孤独的哭泣", -- Continental_supplies +["Lonely Cries: [Rise the water if no hog is in the circle and deal 6 damage to all enemy hogs.]"] = "孤独的哭泣: [提升水面,如果圈中没有刺猬,并对所有敌人造成6伤害]", -- Continental_supplies +["Long Jump: [Enter]"] = "远跳: [Enter]", -- Basic_Training_-_Movement +["Long Jump: Tap the [Curvy Arrow] button for long"] = "远跳: [Curvy Arrow]", -- Basic_Training_-_Movement, A_Classic_Fairytale:first_blood +["Long Live The Queen"] = "女王万岁", -- A_Classic_Fairytale:queen +["Look around: [Mouse movement]"] = "到处看: [移动鼠标]", -- Basic_Training_-_Movement +["Look around: [Tap or swipe on the screen]"] = "到处看: [点或滑动屏幕]", -- Basic_Training_-_Movement +["Look, boss! There is the target!"] = "看,首领,那里有个目标", -- A_Space_Adventure:moon01 +["Look, I had no choice!"] = "看,我没得选", -- A_Classic_Fairytale:backstab +["Look out! There's more of them!"] = "注意,那里还有更多", -- A_Classic_Fairytale:backstab +["Look out! We're surrounded by cannibals!"] = "注意,我们被食人族包围了", -- A_Classic_Fairytale:enemy +["Looks like the whole world is falling apart!"] = "看起来整个世界崩溃了", -- A_Classic_Fairytale:enemy +["Look to the left and do a backwards jump towards the mushroom."] = "看向左边,用“后跳”跳上蘑菇", -- A_Classic_Fairytale:first_blood +["Loon"] = "Loon", -- The_Specialists +["Loopy"] = "Loopy", -- +["Losing Condition: Destroy"] = "失败条件: 破坏", -- HedgeEditor +["Low Gravity: Gravity is %i%%"] = "低重力: 重力是%i%%", -- Gravity +["Loyal Highlander: Eliminate enemy hogs to take their weapons"] = "忠诚的高地人: 消灭敌人并拿走他们的武器", -- Highlander +["Lt. Luke"] = "Lt. Luke", -- +["Lucifer"] = "Lucifer", -- portal +["Luck: %d%% (modifier for crates)"] = "幸运: %d%%(箱子的修改器)", -- Battalion +["Luckily, I've managed to snatch some of them."] = "很幸运,我们设法抢到一些", -- A_Classic_Fairytale:united +["Ludicrous kill!"] = "Ludicrous Kill", -- Mutant +["Lugia"] = "Lugia", -- +["Luigi"] = "Luigi", -- +["Made it!"] = "做到了", -- ClimbHome +["Mahoney"] = "Mahoney", -- +["Make fun of me when I fart …"] = "我放屁时取笑我", -- A_Classic_Fairytale:queen +["Manual: https://hedgewars.org/hedgeeditor"] = "手册: https://hedgewars.org/hedgeeditor", -- HedgeEditor +["Many long forgotten things can be found in the same tunnels that we are about to explore!"] = "在我们将要探索的同一条隧道中,可以找到许多久违的事物", -- A_Space_Adventure:fruit02 +["Many meters below the surface ..."] = "表面以下很多米……", -- A_Space_Adventure:desert02 +["Mario"] = "Mario", -- +["Mark gears for win/lose conditions"] = "标记物体-输赢条件", -- HedgeEditor +["Mark/unmark gear: [Left Click]"] = "标记/取消标记物体: [左键]", -- HedgeEditor +["- Massive weapon bonus on first turn"] = "- 第一回合大量武器奖励", -- Continental_supplies +["Max Citrus"] = "Max Citrus", -- A_Space_Adventure:fruit01 +["Maybe you should try an easier map next time."] = "你应该试一下简单的地图", -- Racer +["Maybe you should try an easier TechRacer map."] = "你应该试一下简单的TechRacer地图", -- TechRacer +["Maybe you should try easier waypoints next time."] = "你应该试一下简单的路径点", -- Racer +["May the spirits aid you in all your quests!"] = "愿祖先的灵魂在你所有的任务里帮助你", -- A_Classic_Fairytale:backstab +["Meals"] = "Meals", -- +["Medic"] = "Medic", -- Battalion +["Medicine"] = "药", -- Continental_supplies +["Medicine: [Fire some exploding medicine that will heal all hogs effected by the explosion]"] = "药: [引爆药物会治疗所有受到爆炸影响的刺猬]", -- Continental_supplies +["MEDIUM"] = "中等", -- Continental_supplies +["Mega kill!"] = "Mega kill!", -- Mutant +["Meiwes"] = "Meiwes", -- A_Classic_Fairytale:backstab +["mikade"] = "mikade", -- +["Mindy"] = "Mindy", -- A_Classic_Fairytale:united +["Mine Deployer"] = "地雷部署器", +["Mine Placement Mode"] = "地雷放置模式", -- Construction_Mode +["MINE PLACEMENT MODE"] = "地雷放置模式", -- HedgeEditor +["Mines explode after %d s."] = "地雷%d秒后爆炸", -- Mutant +["Mines time: 0s-5s"] = "地雷时间: 0-5秒", -- SimpleMission +["Mines time: 0 seconds"] = "地雷时间: 0秒", -- portal, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork, User_Mission_-_The_Great_Escape, A_Space_Adventure:desert01, A_Space_Adventure:final, A_Space_Adventure:fruit02, A_Space_Adventure:ice01 +["Mines time: 1.5 seconds"] = "地雷时间: 1.5秒", -- A_Space_Adventure:death01 +["Mines time: %.1fs"] = "地雷时间: %.1f秒", -- SimpleMission +["Mines time: 1 second"] = "地雷时间: 1秒", -- User_Mission_-_Diver, User_Mission_-_Newton_and_the_Hammock, A_Space_Adventure:desert02 +["Mines time: %.2fs"] = "地雷时间: %.2f秒", -- SimpleMission +["Mines time: 3 seconds"] = "地雷时间: 3秒", -- A_Classic_Fairytale:journey +["Mines time: 5 seconds"] = "地雷时间: 5秒", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:journey +["Mines time: %ds"] = "地雷时间: %d秒", -- SimpleMission +["Mine Strike"] = "地雷空袭", -- Construction_Mode +["Minion"] = "Minion", -- A_Space_Adventure:moon01 +["Minions"] = "Minions", -- A_Space_Adventure:moon01 +["Mission failed!"] = "任务失败", -- Big_Armory +["Mission failure in %d s"] = "任务失败在%d秒", -- Big_Armory +["Mission"] = "任务", -- HedgeEditor +["Mission lost!"] = "任务失败", -- Basic_Training_-_Grenade +["Mission Panel"] = "任务面板", -- Basic_Training_-_Movement +["Mission panel: [M]"] = "任务面板: [M]", -- Basic_Training_-_Movement +["Mission succeeded!"] = "任务成功", -- portal, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, User_Mission_-_Diver, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork_2, User_Mission_-_Teamwork, SimpleMission, HedgeEditor +["Mission won!"] = "任务胜利", -- Basic_Training_-_Grenade +["Mister Pear"] = "Mister Pear", -- A_Space_Adventure:fruit01, A_Space_Adventure:fruit02 +["Mixed %d"] = "混合%d", -- WxW +["Mixed"] = "混合", -- WxW +["Modes: Activate “highland”, “king” or “points” mode by putting mode=|into the script parameter"] = "模式: 把mode=***放入脚本参数|激活“highland”, “king” 或 “points”模式", -- Battalion +["Modifiers: Unlimited ammo, per-hog ammo"] = "修改器: 无限弹药,每个刺猬弹药", -- Battalion +["Modifiers: Unlimited ammo, shared clan ammo"] = "修改器: 无限弹药,战队共享弹药", -- Battalion +["Modifiers: Unlimited attacks, per-hog ammo"] = "修改器: 无限攻击,每个刺猬弹药", -- Battalion +["Modifiers: Unlimited attacks, shared clan ammo"] = "修改器: 无限攻击,战队共享弹药", -- Battalion +["Modify Sprite under Cursor: [Left Click]"] = "修改光标下的Sprite: [左键]", -- HedgeEditor +["Molly"] = "Molly", -- +["Molotov"] = "燃烧瓶", -- Continental_supplies +["Monster kill!"] = "Monster kill!", -- Mutant +["Monsters"] = "Monsters", -- +["Mooney"] = "Mooney", -- +["Morris"] = "Morris", -- +["Most mines are not active."] = "大多数地雷没有激活", -- A_Space_Adventure:desert02 +["Most of the destructible terrain in marked with blue color"] = "大多数可破坏的地形用蓝色标记", -- A_Space_Adventure:desert01 +["Most of the destructible terrain is marked with dashed lines."] = "大多数可破坏的地形用虚线标记", -- A_Space_Adventure:desert01 +["Most of the time you'll be able to use the freezer only."] = "大多数时间你只能用冰冻枪", -- A_Space_Adventure:ice01 +["Movement: [Up], [Down], [Left], [Right]"] = "移动: [上下左右]", +["Mr Mango"] = "Mr Mango", -- A_Space_Adventure:fruit01 +["Mudkip"] = "Mudkip", -- +["Multi-shot! +15 points!"] = "多次射击,+15分", -- Space_Invasion +["Multi-Use: You can take and use the same ammo type multiple times in a turn"] = "多次使用: 你可以在一个回合多次使用相同类型的武器", -- Highlander +["Muriel"] = "Muriel", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Muscle Dissolver"] = "Muscle Dissolver", -- A_Classic_Fairytale:shadow +["Mushroom Kingdom"] = "蘑菇王国", -- +["Mutant"] = "变种人", -- Mutant +["My First Bazooka"] = "我的第一个火箭炮", -- Basic_Training_-_Bazooka +["My flying saucer stopped working!"] = "我的飞碟停止工作了", -- A_Space_Adventure:ice01 +["Nade Boy"] = "Nade Boy", -- Basic_Training_-_Grenade +["Nah, probably everyone was just stupid."] = "呐,可能每个人都是笨蛋", -- A_Space_Adventure:final +["Name"] = "名字", -- A_Classic_Fairytale:queen +["Nancy Screw"] = "Nancy Screw", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:queen +["Napalm"] = "凝固汽油弹", -- Construction_Mode +["Napalm Rocket"] = "凝固汽油弹火箭", -- Continental_supplies +["Napalm rocket: [Fire a bomb with napalm!]"] = "凝固汽油弹火箭: [用凝固汽油弹点燃炸弹]", -- Continental_supplies +["Naranja Jed"] = "Naranja Jed", -- A_Space_Adventure:fruit01 +["Naughty Ninja"] = "Naughty Ninja", -- User_Mission_-_Dangerous_Ducklings +["Near a PAotH base on the moon ..."] = "在月球的星球协会基地附近……", -- A_Space_Adventure:moon01 +["Near Secret Base 17 of PAotH in the rural Hogland ..."] = "在rural Hogland的星球协会的秘密基地17附近……", -- A_Space_Adventure:cosmos +["nemo"] = "nemo", -- +["Neutralize your enemies and be careful!"] = "消灭你的敌人", -- A_Space_Adventure:moon01 +["New barrels per turn: %d"] = "每个回合新的油桶: %d", -- Tumbler +["New clan record: %.1fs"] = "新的战队记录: %.1f秒", -- Racer, TechRacer +["NEW fastest lap: "] = "新的最快一圈: ", +["New mines per turn: %d"] = "每个回合新的地雷: %d", -- Tumbler +["New race record: %.1fs"] = "新的竞赛记录: %.1f秒", -- Racer, TechRacer +["Newton and the Hammock"] = "牛顿和吊床", -- User_Mission_-_Newton_and_the_Hammock +["Next target is ready!"] = "下一个目标已准备好", -- Basic_Training_-_Flying_Saucer +["Next time you play \"Searching in the dust\" you'll have an RC plane available."] = "下一次你玩“尘中搜索”会得到一个可用的遥控飞机", -- A_Space_Adventure:desert03 +["Nice!"] = "Nice!", -- A_Space_Adventure:cosmos +["Nicely done, meatbags!"] = "做得好,肉包!", -- A_Classic_Fairytale:enemy +["Nice! Now hurry and get down! You have to rescue my friends!"] = "很好,现在赶快下来,你必须救出我的朋友", -- A_Space_Adventure:moon01 +["Nice, then I should get the part as soon as possible!"] = "很好,然后我应该尽快得到部件", -- A_Space_Adventure:ice01 +["Nice work!"] = "做得好", -- A_Classic_Fairytale:enemy +["Nice work, meatbags!"] = "做得好,肉包!", -- A_Classic_Fairytale:queen +["Nice work, %s!"] = "做得好,%s", -- A_Classic_Fairytale:dragon +["Nilarian"] = "Nilarian", -- A_Classic_Fairytale:queen +["Ninja"] = "忍者", -- Battalion, HedgeEditor, The_Specialists +["Ninpo"] = "Ninpo", -- +["Nobody Laugh"] = "没人笑", -- User_Mission_-_Nobody_Laugh +["Nobody managed to finish the race. What a shame!"] = "没人能够完成竞赛", -- Racer, TechRacer +["Nobody takes walks every day!"] = "没有人每天都散步", -- A_Classic_Fairytale:epil +["No continent selected"] = "没选择大陆", -- Continental_supplies +["No, I am afraid I had to travel light."] = "不,恐怕我不得不轻装旅行", -- A_Space_Adventure:moon01 +["No, I came back to help you out..."] = "不,我回来帮你出去……", -- A_Classic_Fairytale:shadow +["No...I wonder where they disappeared?!"] = "不……我想知道他们消失去哪了?!", -- A_Classic_Fairytale:journey +["Nom-Nom"] = "Nom-Nom", -- A_Classic_Fairytale:journey +["NomNom"] = "NomNom", -- A_Classic_Fairytale:united +["No Multi-Use: Once you used an ammo, you can’t take it again in this turn"] = "不能多次使用: 使用武器后本回合不能再用", -- Highlander +["Noo, Thanta has to stay alive!"] = "不,Thanta还活着", -- A_Space_Adventure:ice01 +["Nope. It was one fast mole, that's for sure."] = "不,这是一个很快的鼹鼠,可以肯定", -- A_Classic_Fairytale:shadow +["No! Please, help me!"] = "不,请帮助我", -- A_Classic_Fairytale:journey +["No problem, Captain!"] = "没问题,船长", -- A_Space_Adventure:fruit01 +["No problem, I would do anything for H!"] = "没问题,我会为H做任何事", -- A_Space_Adventure:desert01 +["No radar pings left!"] = "雷达探测没有了", -- Space_Invasion +["NORMAL"] = "普通", -- Continental_supplies +["Normal Girder: [1]"] = "普通大梁: [1]", -- HedgeEditor +["Normal Land: [1]"] = "普通地面: [1]", -- HedgeEditor +["Normal Land"] = "普通地面", -- HedgeEditor +["Normally, the mission panel disappears after a few seconds."] = "任务面板通常几秒后消失", -- Basic_Training_-_Movement +["Normal Rubber: [1]"] = "普通橡皮筋: [1]", -- HedgeEditor +["North America"] = "北美", -- Continental_supplies +["Not being able to fight or hunt."] = "不能够战斗或打猎", -- A_Classic_Fairytale:queen +["Note: Some weapons have a second option (See continent information). Find and use them with the \""] = "注意: 一些武器有第二个选项(看大陆信息),找到并使用它们with the \"", -- Continental_supplies +["Note: Some weapons have a second option (See continent information). Find and use them with the \"%s\" key."] = "注意: 一些武器有第二个选项(看大陆信息),找到并使用它们%s键", -- Continental_supplies +["Note: This basic training assumes default controls."] = "注意: 这个基础训练使用默认控制", -- Basic_Training_-_Movement +["Note: Walking is disabled in this mission."] = "注意: 在这个任务不能走动", -- Basic_Training_-_Grenade +["Note: We only give you grenades if you stay in your flying saucer."] = "注意: 我们只在你停留在飞碟的时候给你手榴弹", -- Basic_Training_-_Flying_Saucer +["Nothing of interest has happened."] = "没发生有趣的事情", -- Space_Invasion +["Not now, Fiery Water!"] = "不是现在,Fiery Water", -- A_Classic_Fairytale:backstab +["Not So Friendly Match"] = "不是那么友善的比赛", -- Basketball, Knockball +["Not you again! My head still hurts from last time!"] = "又是你,自从上次,我的头现在还痛", -- A_Classic_Fairytale:shadow +["No waypoint to be removed!"] = "没有能移除的路径点", -- Racer +["Now collect the 2 crates to the far left and right."] = "现在收集两个箱子,到达左边和右边的远处", -- Basic_Training_-_Flying_Saucer +["Now collect the next crate!"] = "现在收集下一个箱子", -- Basic_Training_-_Flying_Saucer +["Now dive just one more time and collect the next crate."] = "现在再潜水一次,收集下一个箱子", -- Basic_Training_-_Flying_Saucer +["No, we made sure of that!"] = "不,我们保证过", -- A_Classic_Fairytale:united +["Now find the next target! |Tip: Normally you lose health by falling down, so be careful!"] = "现在找到下一个目标|提示: 掉下去通常会掉血,小心点", -- Basic_Training_-_Rope +["Now for the supreme discipline of saucer flying, the underwater attack."] = "现在是飞碟的最高训练,水下攻击", -- Basic_Training_-_Flying_Saucer +["Now go and don't waste more of my time, you coward!"] = "现在走,不要浪费我的时间,你这个胆小鬼", -- A_Space_Adventure:fruit01 +["Now go and play the menu mission to complete the campaign."] = "现在回到菜单做任务完成战役", -- A_Space_Adventure:death01 +["Now go to the next crate."] = "现在去拿下一个箱子", -- Basic_Training_-_Movement +["No! What have I done?! What have YOU done?!"] = "不,我做了什么,你做了什么?!", -- A_Classic_Fairytale:journey +["No. Where did he come from?"] = "不,他们从哪来的?", -- A_Classic_Fairytale:shadow +["Now how do I get on the other side?!"] = "现在我怎么到达另一边?", -- A_Classic_Fairytale:dragon +["Now I have to climb these trees"] = "现在我要爬上这些树", -- A_Space_Adventure:cosmos +["No Wind Influcence"] = "没有风力影响", -- Basic_Training_-_Grenade +["No Wind Influence"] = "没有风力影响", -- Basic_Training_-_Grenade +["Now let's try to drop weapons while flying!"] = "现在让我们试着在飞行时丢下武器", -- Basic_Training_-_Flying_Saucer +["Now listen carefully! Below us there are tunnels that have been created naturally over the years"] = "现在认真听,我们下面是经过很多年自然创造的隧道", -- A_Space_Adventure:desert01 +["Now try to get out of this bounce house|and take the next crate."] = "现在试着离开这个弹跳的房子|并拿到下一个箱子", -- Basic_Training_-_Movement +["Now use it and go to the moon PAotH station to get more fuel!"] = "现在使用它,并到达月球星球协会站,得到更多燃料", -- A_Space_Adventure:cosmos +["Now you have the chance to try and claim the place that you deserve among the best."] = "现在你有机会去争取你应得的最好的位置", -- A_Space_Adventure:ice02 +["No. You and the rest of the tribe are safer there!"] = "不,你和部落的人在那里更安全", -- A_Classic_Fairytale:backstab +["Objective completed! Now land safely."] = "目标完成,现在安全着陆", -- Basic_Training_-_Flying_Saucer +["Objectives"] = "目标", -- A_Space_Adventure:ice01 +["Object Placer"] = "物体放置器", -- Construction_Mode +["Obliterate them!|Hint: You might want to take cover..."] = "消灭他们|提示: 你可能需要找掩护……", -- A_Classic_Fairytale:shadow +["Obstacle"] = "障碍", -- Basic_Training_-_Rope +["Obstacle course"] = "障碍课程", -- A_Classic_Fairytale:dragon +["Of course, but you're … special."] = "当然,但你是……特别的", -- A_Classic_Fairytale:epil +["Of course I am!"] = "我当然是", -- A_Classic_Fairytale:queen +["Of course I have to save her. What did I expect?!"] = "当然,我必须救她,我期望什么?!", -- A_Classic_Fairytale:family +["Of course! It's all obvious now!"] = "当然,现在都显而易见了", -- A_Classic_Fairytale:epil +["Of course, I will observe the battle and intervene if necessary."] = "当然,我会观察战斗,如果有必要就干扰", -- A_Space_Adventure:fruit01 +["OH, COME ON!"] = "OH, COME ON!", -- A_Classic_Fairytale:journey +["Oh man! Learn how to fly!"] = "Oh,学学怎么飞", -- A_Space_Adventure:ice02 +["Oh, my!"] = "Oh, my!", -- A_Classic_Fairytale:first_blood +["Oh, my! I forgot something!"] = "Oh, my! 我忘了某件事", -- A_Classic_Fairytale:queen +["Oh, my! This is even more entertaining than I've expected!"] = "Oh, my! 这比我想象的还要有趣", -- A_Classic_Fairytale:backstab +["Oh no, not %s!"] = "Oh no, 不要是%s", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united +["Oh no, the companions have betrayed %s and stole the anti-gravity device part!"] = "Oh no, 同伴已经背叛%s,偷走了反重力设备部件", -- A_Space_Adventure:fruit02 +["Oh no! You have died. Try again!"] = "Oh no, 你死了,再试一次", -- Basic_Training_-_Flying_Saucer +["Oh! Please spare me. You can take all my treasures!"] = "Oh, 请饶了我,你可以拿走我所有的财宝", -- A_Space_Adventure:ice01 +["Oh, silly me! I forgot that I'm the shaman."] = "Oh, 我傻了,我忘了我是萨满", -- A_Classic_Fairytale:backstab +["Oh, that. We were just having fun!"] = "Oh, 那个,我们只是在玩", -- A_Classic_Fairytale:queen +["Oh yeah! You sure know how to rope!"] = "Oh yeah! 你当然知道怎么用绳索", -- Basic_Training_-_Rope +["Oh yes! I got the device part! Now it belongs to me alone."] = "Oh yes! 我得到了设备部件,现在它只属于我", -- A_Space_Adventure:fruit02 +["Okay, I'll be extra careful!"] = "好的,我会额外小心", -- A_Space_Adventure:desert01 +["Okay, now destroy the target|using the baseball bat."] = "好的,现在破坏目标|使用棒球棒", -- Basic_Training_-_Rope +["Okay then!"] = "好吧", -- A_Space_Adventure:fruit02 +["Okay, then you have to go and take some of the weapons we have hidden in case of an emergency!"] = "好的,然后你必须走,并拿到我们防止意外藏起来的一些武器", -- A_Space_Adventure:moon01 +["Old One Eye"] = "Old One Eye", -- +["Oleg"] = "Oleg", -- +["Olive"] = "Olive", -- A_Classic_Fairytale:united +["Omnivore"] = "杂食性动物", -- A_Classic_Fairytale:first_blood +["Once upon a time, on an island with great natural resources, lived two tribes in heated conflict..."] = "从前,在一个岛屿上有很多自然资源,岛上两个部落激烈冲突……", -- A_Classic_Fairytale:first_blood +["Once you set off the proximity trigger, Mr. Mine is not your friend"] = "一旦触发近距离触发器,地雷先生就不是你的朋友了", -- ClimbHome +["One does not simply rope to the moon!"] = "一个人不应该简单地用绳索去月球", -- A_Space_Adventure:cosmos +["One flower: Incomplete side missions"] = "一朵花: 未完成支线任务", -- A_Space_Adventure:cosmos +["One shall not judge one by one's appearance!"] = "一个人不应该评判另一个人的外表", -- A_Classic_Fairytale:epil +["One tribe was peaceful, spending their time hunting and training, enjoying the small pleasures of life..."] = "一个部落很和平,花时间打猎和训练,享受平静的生活", -- A_Classic_Fairytale:first_blood +["Oneye"] = "Oneye", -- portal +["Only one hog per team allowed! Excess hogs will be removed"] = "只允许每个队伍一个刺猬,过量的刺猬会被移除", -- Mutant +["Only one hog per team allowed! Excess hogs will be removed."] = "只允许每个队伍一个刺猬,过量的刺猬会被移除", -- Mutant +["Only one team per clan allowed! Excess teams will be removed."] = "只允许每个战队一个队伍,过量的队伍会被移除", -- Mutant +["Only %s can be trusted with the crate."] = "%s只能信任箱子", -- A_Space_Adventure:fruit02 +["Only the best pilots can master the following stunts."] = "只有最好的飞行员能掌握下面的绝技", -- Basic_Training_-_Flying_Saucer +["Only two clans allowed! Excess hedgehogs will be removed."] = "只允许两个战队,过量的刺猬会被移除", -- CTF_Blizzard +["On the Ice Planet, where ice rules ..."] = "在冰雪统治的冰雪星球……", -- A_Space_Adventure:ice01 +["On the other side of the moon ..."] = "在月球的另一边", -- A_Space_Adventure:moon02 +["On the Planet of Sand, you have to double check your moves ..."] = "在沙的星球,你必须双倍检查你的行动……", -- A_Space_Adventure:desert01 +["On this map you get %d%% fuel."] = "在这个地图你得到%d%%燃料", -- TechRacer +["On this map you get infinite fuel."] = "在这个地图你得到无限燃料", -- TechRacer +["Oops...I dropped them."] = "噢……我放下它们", -- A_Classic_Fairytale:united +["Oops, I've been spotted and I have no weapons! I am doomed!"] = "噢,我被发现了,而且没有武器,我完蛋了", -- A_Space_Adventure:moon01 +["Oops! You have selected the wrong hedgehog! Just try again."] = "噢,你选择了错误的刺猬,再试一次", -- Basic_Training_-_Movement +["Open ammo menu: [Right click]"] = "打开弹药菜单: [右键]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope +["Open ammo menu: Tap the [Suitcase]"] = "打开弹药菜单: [Suitcase]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope +["Open that crate and we will continue!"] = "打开那个箱子并继续", -- A_Classic_Fairytale:first_blood +["Opposing Team: "] = "对方队伍: ", +["Orange"] = "Orange", -- +["Orlando Boom!"] = "Orlando Boom!", -- A_Classic_Fairytale:queen +["Or let the next player place waypoints|if less than 2 waypoints have been placed."] = "或者让下一个玩家放置路径点|如果已经放置的路径点少于两个", -- Racer +["Or maybe this was all part of an evil plan, so evil that even Prof. Hogevil can't think of it!"] = "或者这个是一个邪恶计划的全部,即使Hogevil教授也没想到", -- A_Space_Adventure:final +["Oscillating Gravity: Gravity periodically changes within a range from %i%% to %i%% with a period of %s"] = "摆动的重力: 重力周期地改变,一段时间%s,从%i%%到%i%%", -- Gravity +["Other kills don't give you points."] = "其他杀死不会给你分数", -- Mutant +["Ouch! That must have hurt. %s (%s) hit the ground with %d damage points."] = "那一定很疼,%s(%s)砸到地面造成%d伤害", -- ClimbHome +["Ouch! That must have hurt. You mutilated your poor hedgehog hog with %d damage."] = "那一定很疼,你让可怜的刺猬残疾,%d伤害", -- ClimbHome +["Ouch! You just took fall damage."] = "你掉下来受伤了", -- Basic_Training_-_Movement +["Our tribe, our beautiful island!"] = "我们的部落,我们漂亮的岛屿", -- A_Classic_Fairytale:enemy +["Out of ammo!"] = "弹药没了", -- A_Space_Adventure:desert03, Tumbler +["Out of ammo! Try again!"] = "弹药没了,再试一次", -- Basic_Training_-_Bazooka +["Over the Water"] = "穿过水面", -- Basic_Training_-_Rope +["PAotH"] = "PAotH", -- A_Space_Adventure:cosmos, A_Space_Adventure:death01, A_Space_Adventure:desert01, A_Space_Adventure:moon01 +["PAotH has sent explosives but unfortunately the trigger mechanism seems to be faulty!"] = "星球协会已经发送了爆炸物,但不幸的是触发装置故障了", -- A_Space_Adventure:cosmos +["Parachute"] = "降落伞", -- Continental_supplies +["Patches"] = "Patches", -- +["Pathetic Hog #1"] = "可怜刺猬一号", +["Pathetic Hog #2"] = "可怜刺猬二号", +["Paul McHoggy"] = "Paul McHoggy", -- A_Space_Adventure:ice01, A_Space_Adventure:ice02 +["Pause: [P]"] = "暂停: [P]", -- Basic_Training_-_Movement +["Pause: Tap the [Pause] button"] = "暂停: 按[暂停]键", -- Basic_Training_-_Movement +["Penalty: If you violate above rule, you have to skip in the next turn."] = "惩罚: 如果你违反了上面的规则,下一回合必须跳过", -- WxW +["Penguin Roar"] = "企鹅吼叫", -- Continental_supplies +["Penguin roar: [Deal 15 damage + 10% of your hog’s health to all hogs around you and get 2/3 back]"] = "企鹅吼叫: [对周围刺猬造成15伤害+10%你的血量,得到2/3回血]", -- Continental_supplies +["Penguin roar: [Deal 15 damage + 10% of your hogs health to all hogs around you and get 2/3 back]"] = "企鹅吼叫: [对周围刺猬造成15伤害+10%你的血量,得到2/3回血]", -- Continental_supplies +["Perfect! Now try to get the next crate without hurting yourself!"] = "完美,接下来拿到下一个箱子,不要伤到自己", -- A_Classic_Fairytale:first_blood +["Per-hog Ammo: Weapons are not shared between hogs"] = "每个刺猬弹药: 刺猬之间不共享武器", -- User_Mission_-_Nobody_Laugh +["Personal best: %.3f seconds"] = "个人最好成绩: %.3f秒", -- A_Space_Adventure:ice02 +["Per team weapons"] = "每个队伍武器", -- Continental_supplies +["Pfew! That was close!"] = "呼,好险", -- A_Classic_Fairytale:shadow +["Phosphat"] = "Phosphat", -- portal +["Physicist"] = "物理学家", -- HedgeEditor +["Piano Strike"] = "钢琴空袭", -- Construction_Mode +["Pikachu"] = "Pikachu", -- +["Pings left: %d"] = "雷达探测剩余: %d", -- Space_Invasion +["Pink"] = "Pink", -- +["Pirates"] = "Pirates", -- +["Place 2-%d waypoints using the waypoint placement tool."] = "使用路径点放置工具放置2-%d路径点", -- Racer +["Place 2 waypoints using the waypoint placement tool."] = "使用路径点放置工具放置2路径点", -- Racer +["Place air mines"] = "放置浮空雷", -- HedgeEditor +["Place barrels"] = "放置油桶", -- HedgeEditor +["Place cleavers"] = "放置菜刀", -- HedgeEditor +["Place/Delete Waypoint: [Left Click]"] = "放置/删除路径点: [左键]", -- HedgeEditor +["Place dud mines"] = "放置哑弹地雷", -- HedgeEditor +["Place Gears (and more): Gear Placement Tool"] = "放置物体(和更多): 物体放置工具", -- HedgeEditor +["Place Girder: Girder"] = "放置大梁: 大梁", -- HedgeEditor +["Place Girder: [Left Click]"] = "放置大梁: [左键]", -- HedgeEditor +["Place girders"] = "放置大梁", -- HedgeEditor +["Place health crates"] = "放置医疗箱", -- HedgeEditor +["Place hedgehogs: Place your hedgehogs at the start of the game."] = "放置刺猬: 在游戏开始时放置你的刺猬", -- WxW +["Placement Mode"] = "放置模式", -- HedgeEditor +["Place mines"] = "放置地雷", -- HedgeEditor +["Place, modify and delete gears (e.g. objects)|and waypoints, edit hedgehog settings, values,|victory conditions, and more."] = "放置,修改和删除物体和路径点|编辑刺猬设置,数值,胜利条件和更多", -- HedgeEditor +["Place Object: [Left Click]"] = "放置物体: [左键]", -- HedgeEditor +["Place or delete waypoints"] = "放置或删除路径点", -- HedgeEditor +["Place rubber"] = "放置橡皮筋", -- HedgeEditor +["Place Rubber: Rubber"] = "放置橡皮筋: 橡皮筋", -- HedgeEditor +["Place Sprite: [Left Click]"] = "放置Sprite: [左键]", -- HedgeEditor +["Place sprites to build land"] = "放置sprites以建造地面", -- HedgeEditor +["Place sticky mines"] = "放置黏性地雷", -- HedgeEditor +["Place targets"] = "放置目标", -- HedgeEditor +["Place utility crates"] = "放置工具箱", -- HedgeEditor +["Place Waypoint"] = "放置路径点", -- HedgeEditor +["Place waypoint"] = "放置路径点", -- Racer +["Place weapon crates"] = "放置武器箱", -- HedgeEditor +["- Place your clan flag at the end of your first turn"] = "- 第一个刺猬选好光环出现的位置|- 双方第一回合结束后出现光环", -- Capture_the_Flag +["- Place your team flag at the end of your first turn"] = "- 第一个刺猬选好光环出现的位置|- 双方第一回合结束后出现光环", -- Capture_the_Flag +["Planes used: %d"] = "已使用飞机: %d", -- User_Mission_-_RCPlane_Challenge +["Planes Used"] = "已使用飞机", -- User_Mission_-_RCPlane_Challenge +["Planes Used:"] = "已使用飞机:", -- User_Mission_-_RCPlane_Challenge +["Planets with all missions completed will be marked with two flowers."] = "完成所有任务的星球会标记两朵花", -- A_Space_Adventure:cosmos +["Planets with completed main missions will be marked with a flower."] = "完成主线任务的星球会标记一朵花", -- A_Space_Adventure:cosmos +["Play with me!"] = "和我玩", -- A_Classic_Fairytale:shadow +["Please click on a crate."] = "请点击一个箱子", -- HedgeEditor +["Please click on a gear."] = "请点击一个物体", -- HedgeEditor +["Please click on a hedgehog, barrel, health crate or dud mine."] = "请点击一个刺猬、油桶、医疗箱或哑弹地雷", -- HedgeEditor +["Please click on a hedgehog."] = "请点击一个刺猬", -- HedgeEditor +["Please place the waypoint further away from the waterline"] = "请远离水面放置路径点", -- Racer, TechRacer +["Please place the waypoint in the air and within the map boundaries"] = "请在空中放置路径点,不能超出边界", -- TechRacer +["Please place the waypoint in the air, within the map boundaries"] = "请在空中放置路径点,不能超出边界", -- Racer +["Please place your hedgehog first!"] = "请先放置你的刺猬", -- WxW +["Please, stop releasing your \"smoke signals\"!"] = "请停止释放你的“烟雾信号”", -- A_Classic_Fairytale:shadow +["Please wait …"] = "请等待……", -- WxW +["Point Blank Combo! +5 points!"] = "Point Blank Combo! +5分", -- Space_Invasion +["--- Points ---"] = "---分数---", -- Battalion +["--- Points Mode ---"] = "---分数模式---", -- Battalion +["Poison"] = "Poison", +["Poisonous Apple"] = "Poisonous Apple", -- A_Space_Adventure:fruit02 +["Poisonous, deals no damage."] = "有毒,无伤害", -- Continental_supplies +["Pokémon"] = "Pokémon", -- +["Poor %s (%s) died %d times."] = "可怜的%s(%s)死了%d次", -- Mutant +["Population"] = "人口", -- Continental_supplies +["Porkey"] = "Porkey", -- +["Portal hint: one goes to the destination, and one is the entrance.|"] = "传送门提示: 一个是终点,一个是入口|", -- A_Classic_Fairytale:dragon +["Portal hint: One goes to the destination, the other one is the entrance.|"] = "传送门提示: 一个是终点,其他是入口|", -- A_Classic_Fairytale:dragon +["Portal Mind Challenge"] = "传送门思维挑战", -- portal +["Precise Aim: [Left Shift]"] = "精确瞄准: [左Shift]", -- Basic_Training_-_Movement +["Precise Aim: [Left Shift] + [Up]/[Down]"] = "精确瞄准: [左Shift]+[上]/[下]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["Precise flying"] = "精确飞行", -- A_Space_Adventure:desert03 +["Precise: Remove previous waypoint"] = "精确: 移除上一个路径点", -- Racer +["Precise shooting"] = "精确射击", -- A_Space_Adventure:fruit03 +["Predator"] = "Predator", -- portal +["Prepare for battle!"] = "准备战斗", -- A_Space_Adventure:moon01 +["Prepare to fight"] = "准备战斗", -- A_Space_Adventure:moon01 +["Prepare to flee!"] = "准备逃跑", -- A_Space_Adventure:cosmos +["Prepare yourself, %s!"] = "准备好,%s", -- The_Specialists +["Press [Attack] (space bar by default) to start,|repeadedly tap the up, left and right movement keys to accelerate."] = "按[攻击](默认是空格)开始|重复按上左右加速", -- Basic_Training_-_Flying_Saucer +["Press [Attack] (space bar by default) to start,|repeatedly tap the up, left and right movement keys to accelerate."] = "按[攻击](默认是空格)开始|重复按上左右加速", -- Basic_Training_-_Flying_Saucer +["Press [Attack] to begin."] = "按[攻击]开始", -- A_Classic_Fairytale:first_blood +["Press [Attack] to confirm."] = "按[攻击]确认", -- Continental_supplies +["Press [Attack] to select this continent!"] = "按[攻击]选择这个大陆", -- Continental_supplies +["Press [Left] and [Right] to change the difficulty."] = "按[左]和[右]改变难度", -- A_Classic_Fairytale:first_blood +["Press [Left] or [Right] to move around, [Long Jump] to jump forwards."] = "按[左]或[右]移动,[远跳]向前跳", -- A_Classic_Fairytale:first_blood +["Press [Long jump] to accept this configuration and begin placing hedgehogs."] = "按[远跳]接受这个配置并开始放置刺猬", -- WxW +["Press [Long jump] to accept this configuration and start the game."] = "按[远跳]接受这个配置并开始游戏", -- WxW +["Press [M] to see the mission texts"] = "按[M]看任务信息", -- Basic_Training_-_Movement +["Press [Precise] to skip intro"] = "按[精确]跳过开头动画", +["Press [Up] and [Down] to move between menu items.|Press [Attack], [Left], or [Right] to toggle."] = "按[上]和[下]在菜单项目中移动|按[攻击][左][右]切换", -- WxW +["Prestigious Pilot"] = "有声望的飞行员", -- User_Mission_-_RCPlane_Challenge +["Princess"] = "公主", -- A_Classic_Fairytale:family, A_Classic_Fairytale:journey +["Princess Peach"] = "Princess Peach", -- +["Problems, dude? Chillax!"] = "老兄,有问题?Chillax!", -- A_Classic_Fairytale:epil +["Professional pilot"] = "专业飞行员", -- User_Mission_-_RCPlane_Challenge +["Professional stunt pilot"] = "专业绝技飞行员", -- User_Mission_-_RCPlane_Challenge +["Professor"] = "教授", -- A_Space_Adventure:death01, A_Space_Adventure:moon01 +["Professor Hogevil, then known as James Hogus, worked for PAotH back in my time."] = "Hogevil教授,当时称为James Hogus,为星球协会工作", -- A_Space_Adventure:moon02 +["Professor's Team"] = "教授的队伍", -- A_Space_Adventure:death01 +["Prof. Hogevil"] = "Hogevil教授", -- A_Space_Adventure:death01, A_Space_Adventure:moon01 +["Protect the King: When the king dies, so does the team"] = "保护国王: 国王死了,队伍也死", -- Battalion +["Protect yourselves!|Grenade hint: set the timer with [1-5], aim with [Up]/[Down] and hold [Space] to set power"] = "保护你自己!|手榴弹提示: [1-5]设置定时器,[上]/[下]瞄准,长按[空格]投掷", -- A_Classic_Fairytale:shadow +["Purple"] = "Purple", -- +["Pyro"] = "Pyro", -- HedgeEditor, The_Specialists +["Pyromancer"] = "Pyromancer", -- Battalion +["Quit: [Esc]"] = "退出: [Esc]", -- Basic_Training_-_Movement +["Race complexity limit reached"] = "到达竞赛复杂度限制", -- Racer, TechRacer +["Race failed!"] = "竞赛失败", -- A_Space_Adventure:moon02 +["Racer"] = "Racer", -- Racer +["Racer tool"] = "Racer 工具", -- Racer +["Race"] = "竞赛", -- TrophyRace +["Rachel"] = "Rachel", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Radar: Off"] = "雷达: 关", -- WxW +["Radar: On"] = "雷达: 开", -- WxW +["Radar Ping: [High jump]"] = "雷达探测: [高跳]", -- Space_Invasion +["Radar: Show after crate drop"] = "雷达: 箱子掉落后显示", -- WxW +["Raging Buffalo"] = "Raging Buffalo", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:united +["Ramesses"] = "Ramesses", -- +["Ramon"] = "Ramon", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow +["Random continent"] = "随机大陆", -- Continental_supplies +["Rank: %s"] = "等级: %s", -- User_Mission_-_RCPlane_Challenge +["Razac"] = "Razac", -- portal +["RC Plane Challenge"] = "遥控飞机挑战", -- User_Mission_-_RCPlane_Challenge +["RC Plane"] = "遥控飞机", -- Construction_Mode +["Reach and destroy the final target to win."] = "到达并破坏最后的目标获胜", -- Basic_Training_-_Rope +["Read the challenge objectives from within the mission for more details."] = "在任务里阅读挑战目标获得更多细节", -- A_Space_Adventure:death02, A_Space_Adventure:desert03, A_Space_Adventure:fruit03 +["Ready for Battle?"] = "准备战斗?", -- A_Space_Adventure:fruit01 +["Really?! You thought you could harm me with your little toys?"] = "真的吗?你认为能用你的小玩具伤害我?", -- A_Classic_Fairytale:shadow +["Red"] = "Red", -- +["Reflector Shield"] = "反射盾", -- Construction_Mode +["Reflector Shield: Reflects enemy projectiles."] = "反射盾: 反射敌人的炮弹", -- Construction_Mode +["Regurgitator"] = "Regurgitator", -- A_Classic_Fairytale:backstab +["Reinforcements! +2 of each weapon!"] = "援军,每个武器+2", -- A_Space_Adventure:death02 +["Reinforcements"] = "援军", -- A_Classic_Fairytale:backstab +["Release rope: [Attack]"] = "释放绳索: [攻击]", -- Basic_Training_-_Rope +["Remember: Hold down [Left Shift] to prevent slipping"] = "记住: 一直按[左Shift]防滑", -- Basic_Training_-_Movement +["Remember! Many will seek the anti-gravity device! Now go, hurry up!"] = "记住,很多人在找反重力设备,快点去", -- A_Space_Adventure:cosmos +["Remember: The rope only bend around objects, |if it doesn't hit anything it's always stright!"] = "记住: 绳索只在碰到物体时弯曲|没碰到东西,一直是直的", -- Basic_Training_-_Rope +["Remember this, pathetic animal: when the day comes, you will regret your blind loyalty!"] = "记住这个,可怜的动物: 当那天来到,你会为盲目的忠诚后悔", -- A_Classic_Fairytale:shadow +["Remember this, pathetic animal: When the day comes, you will regret your blind loyalty!"] = "记住这个,可怜的动物: 当那天来到,你会为盲目的忠诚后悔", -- A_Classic_Fairytale:shadow +["Replenishment: Weapons are restocked on turn start of a new hog"] = "补充: 在回合开始补充刺猬的武器", -- Highlander +["Repositioning Mode"] = "调整位置模式", -- HedgeEditor +["REPOSITIONING MODE"] = "调整位置模式", -- HedgeEditor +["Rescue the imprisoned PAotH team and get the fuel!"] = "解救被监禁的星球协会队伍并得到燃料", -- A_Space_Adventure:moon01 +["Respawner"] = "重生器", -- Construction_Mode +["Respawner: Resurrects dead hogs."] = "重生器: 复活死亡的刺猬", -- Construction_Mode +["Resurrector"] = "复活器", -- Construction_Mode +["Retract/Extend rope: [Up]/[Down]"] = "伸缩绳索:[上]/[下]", -- Basic_Training_-_Rope +["- Return the enemy flag to your base to score"] = "- 把敌人的光环带到你的基地得分", -- Capture_the_Flag +["Return to Leaks A Lot!"] = "回去找Leaks A Lot", -- A_Classic_Fairytale:shadow +["Return to the mission menu by pressing the \"Go back\" button."] = "按“返回”回到任务菜单", -- A_Space_Adventure:cosmos +["Return to the Surface"] = "回到表面", -- A_Space_Adventure:fruit02 +["Return to the training menu by pressing the “Go back” button."] = "按“返回”回到训练菜单", -- Basic_Training_-_Movement +["Rider"] = "Rider", -- portal +["Rifleman"] = "Rifleman", -- Battalion +["Righteous Beard"] = "Righteous Beard", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:first_blood, A_Classic_Fairytale:queen, A_Classic_Fairytale:united +["Ripe"] = "Ripe", -- +["Rise the water if nobody else is in the circle and deal 6 damage to all enemy hogs."] = "如果圈中没人,提升水面,并对所有敌人造成6伤害", -- Continental_supplies +["Robert Yellow Apple"] = "Robert Yellow Apple", -- A_Space_Adventure:fruit01 +["Rocket"] = "Rocket", -- Big_Armory +["Ronald"] = "Ronald", -- portal +["Roof"] = "顶部", -- WxW +["Rope-knocking Challenge"] = "绳索撞击挑战", -- User_Mission_-_Rope_Knock_Challenge +["Rope Master"] = "绳索大师", -- Basic_Training_-_Rope +["Ropes and Crates"] = "绳索和箱子", -- Challenge_-_Speed_Shoppa_-_Ropes +["Ropes can be fired again in the air!"] = "绳索可以在空中再次发射", -- A_Classic_Fairytale:first_blood +["Rope Team"] = "绳索队伍", -- Basic_Training_-_Rope +["Rope Training"] = "绳索训练", -- Basic_Training_-_Rope +["Rope Weapons"] = "绳索武器", -- Basic_Training_-_Rope +["Roshi"] = "Roshi", -- +["Rot Molester"] = "Rot Molester", -- A_Classic_Fairytale:shadow +["Rotten"] = "Rotten", -- +["Round draw"] = "平局", -- Racer, TechRacer +["Round %d (Sudden Death in round %d)"] = "%d局(突然死亡在%d局)", -- Battalion +["Round limit: %d"] = "回合限制: %d", -- Racer +["Round Limit: %d"] = "回合限制: %d", -- Space_Invasion +["Round limit:"] = "回合限制:", -- TechRacer +["Rounds complete: %d/%d"] = "回合完成: %d/%d", -- Racer, Space_Invasion, TechRacer +["Round's slowest lap: %.3fs by %s"] = "回合最慢的一圈: %s的%.3f秒", -- TrophyRace +["RS1"] = "红色草莓1", -- A_Space_Adventure:fruit03 +["RS2"] = "红色草莓2", -- A_Space_Adventure:fruit03 +["Rubber"] = "橡皮筋", -- Construction_Mode, HedgeEditor +["Rubber Placement Mode"] = "橡皮筋放置模式", -- Construction_Mode +["RUBBER PLACEMENT MODE"] = "橡皮筋放置模式", -- HedgeEditor +["Rules:"] = "规则:", -- Capture_the_Flag +["RULES:"] = "规则:", -- Frenzy +["Rules: "] = "规则: ", -- Mutant +["Run away, you coward!"] = "走开,你这个胆小鬼", -- A_Space_Adventure:desert01 +["Running displacement algorithm …"] = "正在运行取代算法", -- A_Classic_Fairytale:queen +["Running for survival"] = "快逃命", -- A_Space_Adventure:desert02 +["Rusted Diego"] = "Rusted Diego", -- +["Rusty Joe"] = "Rusty Joe", -- A_Classic_Fairytale:queen +["Ryu"] = "Ryu", -- +["%s (+1)"] = "%s (+1)", -- A_Space_Adventure:fruit03 +["%s: %.1fs"] = "%s: %.1f秒", -- Racer, TechRacer +["Sabotage all hogs in the circle and fire a cluster above you.|Sabotaged hogs lose health and have to deal with a very high gravity during their turn."] = "妨害圈中所有的刺猬,并在你头上发射一粒炸弹|受到妨害的刺猬掉血,并在他们的回合必须对付高重力", -- Continental_supplies +["Sabotage/Flare: [Sabotage all hogs in the circle and deal ~1 dmg OR Fire a cluster up into the air]"] = "妨害/Flare: [妨害圈中所有刺猬造成~1伤害,或发射一粒炸弹到空中]", -- Continental_supplies +["Saint"] = "Saint", -- HedgeEditor, The_Specialists +["Salivaslurper"] = "Salivaslurper", -- A_Classic_Fairytale:united +["Salty Dog"] = "Salty Dog", -- +["Salvation"] = "拯救", -- A_Classic_Fairytale:family +["Salvation was one step closer now..."] = "现在拯救更近一步……", -- A_Classic_Fairytale:dragon +["Sam"] = "Sam", -- A_Space_Adventure:cosmos +["Sandals?! I thought you left your ring!"] = "拖鞋?! 我以为你弄丢了你的戒指", -- A_Classic_Fairytale:queen +["%s and GB"] = "%s 和绿色香蕉", -- A_Space_Adventure:fruit02 +["%s and %s enter the battlefield"] = "%s和%s进入战场", -- A_Space_Adventure:fruit01 +["Sandstorm"] = "沙暴", -- A_Space_Adventure:desert01 +["Sandy"] = "Sandy", -- A_Space_Adventure:desert01 +["%s arrived at the Desert Planet!"] = "%s到达了沙漠星球", -- A_Space_Adventure:cosmos +["%s arrived at the Fruit Planet!"] = "%s到达了水果星球", -- A_Space_Adventure:cosmos +["%s arrived at the Ice Planet!"] = "%s到达了冰雪星球", -- A_Space_Adventure:cosmos +["%s arrived at the meteorite!"] = "%s到达了陨石", -- A_Space_Adventure:cosmos +["%s arrived at the moon!"] = "%s到达了月球", -- A_Space_Adventure:cosmos +["%s arrived at the Planet of Death!"] = "%s到达了死亡星球", -- A_Space_Adventure:cosmos +["Save as many hogs as possible!"] = "尽可能拯救刺猬", -- User_Mission_-_That_Sinking_Feeling +["Save Fell From Heaven!"] = "救Fell From Heaven", -- A_Classic_Fairytale:journey +["Save Leaks A Lot!|Hint: The switch hedgehog utility might be of help to you."] = "拯救Leaks A Lot!|提示: 切换刺猬工具可能对你很有帮助", -- A_Classic_Fairytale:shadow +["Save Level: [Precise]+[4]"] = "保存关卡: [精确]+[4]", -- HedgeEditor +["Save the princess! All your hogs must survive!|Hint: Kill the cyborgs first! Use the ammo very carefully!|Hint: You might want to spare a girder for cover!"] = "拯救公主,你的所有刺猬必须活着|提示: 先杀死机器人,小心使用弹药|提示: 你可能要节省大梁用来掩护", -- A_Classic_Fairytale:family +["Save the princess by collecting the crate in under 12 turns!"] = "在12回合内收集箱子救公主", -- A_Classic_Fairytale:journey +["Saving Hogera"] = "拯救Hogera", -- A_Space_Adventure:cosmos +["%s barely made it past the hogosphere."] = "%s勉强通过了hogosphere", -- ClimbHome +["%s bravely climbed up to a dizzy height of %d to reach home."] = "%s勇敢地爬上让人眩晕的%d高度,回到家", -- ClimbHome +["Scallywag"] = "Scallywag", -- +["Scalp Muncher"] = "Scalp Muncher", -- A_Classic_Fairytale:backstab +["Scenario"] = "场景", -- Big_Armory, portal, User_Mission_-_Bamboo_Thicket, User_Mission_-_Dangerous_Ducklings, User_Mission_-_Diver, User_Mission_-_Newton_and_the_Hammock, User_Mission_-_Nobody_Laugh, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork_2, User_Mission_-_Teamwork, User_Mission_-_The_Great_Escape +["Scientist"] = "科学家", -- Battalion +["%s climbed home in %d seconds!"] = "%s用了%d秒爬回家", -- ClimbHome +["%s (contd.)"] = "%s (contd.)", -- A_Classic_Fairytale:epil +["Score: %d"] = "分数: %d", -- Space_Invasion +["Score goal: %d"] = "分数目标: %d", -- Control +["Score graph"] = "分数图表", -- Mutant, Space_Invasion +["Score points by killing other hedgehogs."] = "杀死其他刺猬得分", -- Mutant +["Score points by killing other hedgehogs (see below)."] = "杀死其他刺猬得分(看下面)", -- Mutant +["Scores: "] = "分数: ", -- Capture_the_Flag +["Scores"] = "分数", -- Mutant +["Scores:"] = "分数:", -- Mutant +["Scoring: "] = "分数: ", -- Mutant +["%s couldn't escape, try again!"] = "%d没能逃离,再试一次", -- A_Space_Adventure:fruit01 +["Script parameter examples:"] = "脚本参数示例:", -- Gravity +["%s (+%d)"] = "%s (+%d)", -- Battalion +["%s: %d"] = "%s: %d", -- Capture_the_Flag, Control +["%s (%d)"] = "%s (%d)", -- Continental_supplies +["%s: %d (deaths: %d)"] = "%s: %d (死亡: %d)", -- Mutant +["%s (%d), %d sec"] = "%s (%d), %d秒", -- Continental_supplies +["%s: Did not finish"] = "%s: 没有完成", -- Racer, TechRacer +["%s did not finish the race."] = "%s没有完成竞赛", -- Racer, TechRacer +["%s didn't expect that."] = "%s没想到", -- User_Mission_-_Rope_Knock_Challenge +["%s died … and lives again!"] = "%s死了……又活了", -- Construction_Mode +["%s doesn’t really know how to handle a rope properly."] = "%s真不知道怎么正确地操作绳索", -- ClimbHome +["%s, %d sec"] = "%s,%d秒", -- Continental_supplies +["Search for the device with the help of the other hedgehogs."] = "在其他刺猬的帮助下搜索设备", -- A_Space_Adventure:fruit02 +["Searching in the dust"] = "在尘土中搜索", -- A_Space_Adventure:desert01 +["Searching the stars!"] = "搜索星星", -- A_Space_Adventure:cosmos +["Seduction"] = "诱惑", -- Continental_supplies +["Seems like every time you take a \"walk\", the enemy finds us!"] = "似乎每次你去“散步”,敌人都找到我们", -- A_Classic_Fairytale:backstab +["See that crate farther on the right?"] = "看看右边远处的箱子?", -- A_Classic_Fairytale:first_blood +["See ya!"] = "再见!", +["Segmentation Paul"] = "Segmentation Paul", -- A_Classic_Fairytale:dragon +["Select a placement mode and read the infos|in the mission panel to learn how to use it."] = "选择一个放置模式|阅读任务面板的信息,学会怎么使用它", -- HedgeEditor +["Select continent!"] = "选择大陆", -- Continental_supplies +["Select continent"] = "选择大陆", -- Continental_supplies +["Selection Mode"] = "选择模式", -- HedgeEditor +["Select, modify, or delete girders, rubbers and sprites"] = "选择,修改,删除大梁,橡皮筋和sprites", -- HedgeEditor +["Select/Place/Delete Gear: [Left Click]"] = "选择/放置/删除物体: [左键]", -- HedgeEditor +["Select, reposition and delete gears"] = "选择,调整位置和删除物体", -- HedgeEditor +["Select Rope"] = "选择绳索", -- Basic_Training_-_Rope +["Select “Switch Hedgehog” from the ammo menu and|hit the “Attack” key."] = "从弹药菜单选择“切换刺猬”|并按“攻击”键", -- Basic_Training_-_Movement +["Select “Switch Hedgehog” from the ammo menu and|hit the “Attack” key to proceed."] = "从弹药菜单选择“切换刺猬”|并按“攻击”键继续", -- Basic_Training_-_Movement +["Select the current continent."] = "选择当前的大陆", -- Continental_supplies +["Select the rope to begin!"] = "选择绳索开始", -- Basic_Training_-_Rope +["Select this item for a random continent."] = "选择这个项目,随机一个大陆", -- Continental_supplies +["Select Weapon"] = "选择武器", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["Select weapon: [Left click]"] = "选择武器: [左键]", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade +["Select win/lose condition: [Left], [Right]"] = "选择输/赢条件: [左]/[右]", -- HedgeEditor +["Select your continent/weaponset: with the \"Up\" or \"Down\" keys. You can also select one with the weapons menu."] = "选择你的大陆/武器集: [上]/[下],你也可以在武器菜单选择一个", -- Continental_supplies +["Select your continent/weaponset: With the \"Up\" or \"Down\" keys. You can also select one with the weapons menu."] = "选择你的大陆/武器集: [上]/[下],你也可以在武器菜单选择一个", -- Continental_supplies +["Select your continent with [Up]/[Down] or by selecting a representative weapon."] = "[上]/[下]或者选择代表性的武器选择你的大陆", -- Continental_supplies +["%s enters the battlefield"] = "%s进入战场", -- A_Space_Adventure:fruit01 +["Sergey"] = "Sergey", -- +["%s escaped successfully!"] = "%s成功逃离", -- A_Space_Adventure:fruit01 +["Set bounciness: [Left Shift] + [1]-[5]"] = "设置弹力: [左Shift]+[1-5]", -- Basic_Training_-_Grenade +["Set detonation timer: [1]-[5]"] = "设置引爆定时器: [1-5]", -- Basic_Training_-_Grenade +["Set Health: [Left Click]"] = "设置血量: [左键]", -- HedgeEditor +["Set Identity: [Left Click]"] = "设置身份: [左键]", -- HedgeEditor +["Set period to negative value for random gravity."] = "设置时间为负数以随机重力", -- Gravity +["Set the health of hogs, health crates, barrels and duds"] = "设置刺猬,医疗箱,油桶和哑弹地雷的血量", -- HedgeEditor +["Set to %d"] = "设为%d", -- HedgeEditor +["%s exploded."] = "%s爆炸了", -- User_Mission_-_Rope_Knock_Challenge +["%s fell from a high cliff."] = "%s从悬崖掉下", -- User_Mission_-_Rope_Knock_Challenge +["%s fell too fast."] = "%s掉得太快", -- User_Mission_-_Rope_Knock_Challenge +["%s fell victim to a weapon filter"] = "%s成为一个武器过滤器的受害者", -- Construction_Mode +["%s felt unstable."] = "%s感觉不稳定", -- User_Mission_-_Rope_Knock_Challenge +["%s felt victim to rope-knocking."] = "%s成为绳索撞击的受害者", -- User_Mission_-_Rope_Knock_Challenge +["%s flew like a rock."] = "%s像石头一样飞", -- User_Mission_-_Rope_Knock_Challenge +["%s gets an extra life"] = "%s得到额外的生命", -- Construction_Mode +["%s goes the way of the lemming."] = "%s走向旅鼠的道路", -- User_Mission_-_Rope_Knock_Challenge +["Sgt. Smith"] = "Sgt. Smith", -- +["%s had it coming."] = "%s即将来到", -- User_Mission_-_Rope_Knock_Challenge +["%s had no chance."] = "%s没有机会", -- User_Mission_-_Rope_Knock_Challenge +["... share your beauty with the world every morning, my princess!"] = "……每个早晨和世界分享你的美丽,我的公主", -- A_Classic_Fairytale:journey +["%s has been killed before taking enough damage first."] = "%s在先造成足够伤害前被杀了", -- SimpleMission +["%s has been knocked out."] = "%s被撞出去了", -- User_Mission_-_Rope_Knock_Challenge +["%s has been rescued from death"] = "%s被从死亡中解救", -- Construction_Mode +["%s has dropped the flag!"] = "%s掉下了光环", -- CTF_Blizzard +["%s has fallen victim to gravity."] = "%s成为重力的受害者", -- User_Mission_-_Rope_Knock_Challenge +["%s has mutated! +2 points"] = "%s变异了,+2分", -- Mutant +["%s has passed the best height of %s!"] = "%s超过了%s的最高记录", -- ClimbHome +["%s has scored!"] = "%s得分", -- Capture_the_Flag +["%s has to refuel the saucer."] = "%s必须给飞碟加油", -- A_Space_Adventure:moon01 +["%s hates Newton."] = "%s恨牛顿", -- User_Mission_-_Rope_Knock_Challenge +["She endangered the whole tribe!"] = "她快把整个部落灭绝", -- A_Classic_Fairytale:epil +["sheepluva"] = "sheepluva", -- +["Sheepy"] = "Sheepy", -- +["She's behind that tall thingy."] = "她在那个高的东西后面", -- A_Classic_Fairytale:family +["Shield boosted! +%d power"] = "护盾增加,+%d能量", -- Space_Invasion +["Shield depleted"] = "护盾耗尽", -- Space_Invasion +["Shield is fully recharged!"] = "护盾完全充能", +["Shield Master! +10 points!"] = "护盾大师,+10分", -- Space_Invasion +["Shield Miser! +%d points!"] = "护盾小气鬼,+%d分", -- Space_Invasion +["Shield OFF: %d power remaining"] = "护盾关: 还有%d能量", -- Space_Invasion +["Shield ON: %d power remaining"] = "护盾开: 还有%d能量", -- Space_Invasion +["Shield Seeker! +10 points!"] = "护盾寻找者,+10分", -- Space_Invasion +["Shinobi"] = "Shinobi", -- +["%s hit the ground."] = "%s砸到地面", -- User_Mission_-_Rope_Knock_Challenge +["Shoppa Love"] = "Shoppa Love", -- Challenge_-_Speed_Shoppa_-_Hedgelove +["Shotgun"] = "霰弹枪", -- Continental_supplies +["Sigh."] = "叹气", -- A_Classic_Fairytale:epil +["Silly"] = "Silly", +["Silver"] = "Silver", -- +["Sine Gun"] = "正弦枪", -- Construction_Mode +["Sinky"] = "Sinky", +["Sirius Lee"] = "Sirius Lee", -- A_Classic_Fairytale:enemy +["%s is dead, who was critical to this mission!"] = "%s死了,谁是这个任务的重要人物", -- SimpleMission +["%s is eliminated!"] = "%s淘汰了", -- User_Mission_-_Rope_Knock_Challenge +["%s is now as poor as a church mouse"] = "%s现在像一个可怜的教堂老鼠", -- Construction_Mode +["%s is now a zombie hedgehog"] = "%s现在是一个刺猬丧尸", -- Construction_Mode +["%s is suddenly low on ammo"] = "%s突然缺少弹药", -- Construction_Mode +["Skulls"] = "Skulls", -- Bazooka_Battlefield +["Slimer"] = "Slimer", -- +["Slippery"] = "Slippery", -- A_Classic_Fairytale:journey +["%s lost all the weapons"] = "%s失去所有武器", -- Construction_Mode +["%s lost, try again!"] = "%s输了,再试一次", -- A_Space_Adventure:death01, A_Space_Adventure:death02, A_Space_Adventure:desert01, A_Space_Adventure:desert02, A_Space_Adventure:desert03, A_Space_Adventure:final, A_Space_Adventure:fruit01, A_Space_Adventure:fruit02, A_Space_Adventure:fruit03, A_Space_Adventure:ice01, A_Space_Adventure:moon01 +["Slot %d: %s"] = "槽位%d: %s", -- Frenzy +["Slot keys save time! (F1-F10 by default)"] = "槽位按键(默认F1-F10)", -- Frenzy +["Slowpoke"] = "Slowpoke", -- +["%s made it past the hogosphere."] = "%s通过了hogosphere", -- ClimbHome +["%s managed to pass half of the distance towards home."] = "%s设法通过了回家的一半路程", -- ClimbHome +["%s may choose the rules."] = "%s可以选择规则", -- WxW +["Smith 0.97"] = "Smith 0.97", -- A_Classic_Fairytale:enemy +["Smith 0.98"] = "Smith 0.98", -- A_Classic_Fairytale:enemy +["Smith 0.99a"] = "Smith 0.99a", -- A_Classic_Fairytale:enemy +["Smith 0.99b"] = "Smith 0.99b", -- A_Classic_Fairytale:enemy +["Smith 0.99f"] = "Smith 0.99f", -- A_Classic_Fairytale:enemy +["Smith 1.0"] = "Smith 1.0", -- A_Classic_Fairytale:enemy +["Smugglers"] = "走私者", -- A_Space_Adventure:desert01 +["%s must collect the final crates."] = "%s必须收集最后的箱子", -- A_Space_Adventure:fruit02 +["%s must skip this turn for rule violation."] = "%s违反规则必须跳过这个回合", -- WxW +["Sneaks"] = "Sneaks", -- Bazooka_Battlefield +["%s never got the ninja diploma."] = "%s从没得到忍者文凭", -- ClimbHome +["%s never wanted to reach for the sky in the first place."] = "%s从没想一步登天", -- ClimbHome +["Sniper! +8 points!"] = "狙击手,+8分", -- Space_Invasion +["Sniper"] = "狙击手", -- HedgeEditor, The_Specialists +["Sniper Rifle"] = "狙击枪", -- Continental_supplies +["Sniper Training"] = "狙击训练", +["So, as promised I have brought you where I think that the device you are looking for is hidden."] = "所以,按照承诺,我带你去你寻找的设备所隐藏的地方", -- A_Space_Adventure:fruit02 +["So far, you had infinite ropes, but in the|real world, ropes are usually limited."] = "你有无限的绳索|但真实世界的绳索通常是有限的", -- Basic_Training_-_Rope +["So humiliating..."] = "好丢脸……", -- A_Classic_Fairytale:first_blood +["So, I believe that it's a good place to start."] = "所以,我相信这里是开始的好地方", -- A_Space_Adventure:desert01 +["So, I kindly ask for your help."] = "所以我请你帮忙", -- A_Space_Adventure:fruit01 +["So I shook my fist in the air!"] = "所以我在空气中打了一拳", -- A_Classic_Fairytale:epil +["Soldier"] = "战士", -- HedgeEditor, The_Specialists +["So, let me tell you what I know about Professor Hogevil."] = "所以,让我告诉你关于Hogevil教授的事情", -- A_Space_Adventure:moon02 +["Some parts of the land are indestructible."] = "地面的某些部分是不可破坏的", -- A_Space_Adventure:fruit03 +["Some sick game of yours?!"] = "你的某种恶心游戏?!", -- A_Classic_Fairytale:queen +["Some weapons can be dropped from the rope."] = "某些武器可以从绳索丢下", -- Basic_Training_-_Rope +["Somewhere else on the planet of fruits, Captain Lime helps %s"] = "水果星球的某个地方,Captain Lime帮助%s", -- A_Space_Adventure:fruit02 +["Somewhere else on the planet of fruits, %s gets closer to the device"] = "水果星球的某个地方,%s接近了设备", -- A_Space_Adventure:fruit02 +["Somewhere on the Planet of Fruits a terrible war is about to begin ..."] = "水果星球的某个地方,一个可怕的战争就要开始……", -- A_Space_Adventure:fruit01 +["Somewhere on the uninhabitable Death Planet ..."] = "不适合居住的死亡星球的某个地方……", -- A_Space_Adventure:death01 +["So, now I got the last part and I have your friends captured."] = "所以,现在我得到了最后的部件,并抓住了你的朋友", -- A_Space_Adventure:death01 +["So, %s, here we are ..."] = "所以,%s,我们到了……", -- A_Space_Adventure:cosmos +["So the princess was never heard of again ..."] = "所以再没有听说过公主……", -- A_Classic_Fairytale:family +["So, uhmm, how did you manage to teleport them so far?"] = "所以,呃,你怎么做到把他们传送得那么远?", -- A_Classic_Fairytale:epil +["Sour"] = "Sour", -- +["South America"] = "南美", -- Continental_supplies +["So? What will it be?"] = "所以,你的选择是什么?", -- A_Classic_Fairytale:shadow +["So you are able to launch projectiles into your aiming direction, always at full power."] = "所以你要瞄准目标发射炮弹(飞行中总是全力发射)", -- Basic_Training_-_Flying_Saucer +["So you are interested in Professor Hogevil, huh?"] = "所以你对Hogevil教授感兴趣?", -- A_Space_Adventure:moon02 +["So you basically did the dirty work for us."] = "所以你基本上为我们做脏活", -- A_Classic_Fairytale:dragon +["Space Invasion"] = "太空入侵", -- Space_Invasion +["SPACE INVASION"] = "太空入侵", -- Space_Invasion +["Spacetrip"] = "太空旅行", -- A_Space_Adventure:cosmos +["Spawn the crate and attack!"] = "创造箱子并攻击", -- WxW +["Specials: Kings and air generals drop helpers, not weapons"] = "特别: 国王和空军将军掉下工具,不是武器", -- Battalion +["Special weapons:"] = "特别模式武器:", -- Continental_supplies +["Special Weapons:"] = "特别模式武器:", -- Continental_supplies +["Speckles"] = "Speckles", -- +["Specs"] = "Specs", -- +["Specs Appeal"] = "Specs Appeal", -- +["Spectator"] = "Spectator", -- +["Speed Roping"] = "Speed Roping", -- Basic_Training_-_Rope +["Speed Shoppa"] = "Speed Shoppa", -- SpeedShoppa +["Spike"] = "Spike", -- A_Space_Adventure:desert01 +["Spikes"] = "Spikes", -- +["Spiky Cheese"] = "Spiky Cheese", -- A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:shadow +["%s, place the first hedgehog!"] = "%s,放置第一个刺猬", -- WxW +["Spleenlover"] = "Spleenlover", -- A_Classic_Fairytale:united +["Sponge"] = "Sponge", +["Spooky Tree"] = "怪树", +["Sprite Erasure Mode"] = "Sprite删除模式", -- HedgeEditor +["Sprite Modification Mode"] = "Sprite修改模式", -- HedgeEditor +["SPRITE MODIFICATION MODE"] = "Sprite修改模式", -- HedgeEditor +["Sprite Placement Mode"] = "Sprite放置模式", -- Construction_Mode +["SPRITE PLACEMENT MODE"] = "Sprite放置模式", -- HedgeEditor +["Sprite Testing Mode"] = "Sprite测试模式", -- Construction_Mode +["Squirtle"] = "Squirtle", -- +["Squishy"] = "Squishy", -- +["%s reached home in %.3f seconds. Congratulations!"] = "%s用时%.3f秒回到家,恭喜", -- ClimbHome +["%s: %s"] = "%s: %s", -- Continental_supplies +["%s (%s) destroyed %d invaders in one round."] = "%s (%s) 一个回合消灭了%d入侵者", -- Space_Invasion +["%s (%s) does not have to feel ashamed for their best height of %d."] = "%s (%s) 不必对他们最高记录%d感到惭愧", -- ClimbHome +["%s, select your continent!"] = "%s,选择你的大陆", -- Continental_supplies +["%s (%s) gave short shrift to the invaders: Longest combo of %d!"] = "%s (%s) 给了入侵者短暂的忏悔: 最长的连击%d", -- Space_Invasion +["%s (%s) has been invited to join the Planetary Association of the Hedgehogs, it destroyed a staggering %d invaders in just one round!"] = "%s (%s) 被邀请加入刺猬星球协会,它一回合消灭了%d入侵者", -- Space_Invasion +["%s (%s) has captured the flag %d times."] = "%s (%s) 夺走光环%d次", -- Capture_the_Flag +["%s (%s) hate life and suicided %d times."] = "%s (%s) 讨厌生活并自杀%d次", -- Mutant +["%s should try the rope training mission first."] = "%s应该先试试绳索训练", -- ClimbHome +["%s (%s) is addicted to killing: %d invaders destroyed in one round."] = "%s (%s) 沉迷杀戮: 一回合消灭了%d入侵者", -- Space_Invasion +["%s (%s) is a hardened hunter: No misses and %d hits in its best round!"] = "%s (%s) 是一个老练的猎人: 它最好的一回合没有失手打了%d次", -- Space_Invasion +["%s (%s) is a tumbleweed: %d points in one round."] = "%s (%s) 是一个风滚草: 一回合%d分", -- Space_Invasion +["%s (%s) is good at this: %d points in only one round!"] = "%s (%s) 擅长这个: 一回合%d分", -- Space_Invasion +["%s (%s) is Rambo in a hedgehog costume! He destroyed %d invaders in one round."] = "%s (%s) 是穿着刺猬衣服的Rambo,他一回合消灭了%d入侵者", -- Space_Invasion +["%s skipped ninja classes."] = "%d跳过了忍者课", -- ClimbHome +["%s spawned at a really bad position."] = "%s 出现在一个很糟糕的位置", -- User_Mission_-_Rope_Knock_Challenge +["%s splatted."] = "%s 扑通一声", -- User_Mission_-_Rope_Knock_Challenge +["%s (%s) reached a decent peak height of %d."] = "%s (%s) 到达一个相当好的高度%d", -- ClimbHome +["%s (%s) reached a peak height of %d."] = "%s (%s) 到达高度%d", -- ClimbHome +["%s (%s) reached for the sky and beyond with a height of %d!"] = "%s (%s) 为了到达天空和更远,高度%d", -- ClimbHome +["%s (%s) reached home in %.3f seconds."] = "%s (%s) 在%.3f秒回到家", -- ClimbHome +["%s (%s) shot %d invaders and never missed in the best round!"] = "%s (%s) 在最好的回合射击了%d入侵者并且没有失手", -- Space_Invasion +["%s (%s) struck like a meteor: %d points in only one round!"] = "%s (%s) 像陨石一样袭击: 一个回合%d分", -- Space_Invasion +["%s still had a long way to go."] = "%s还有很长的路要走", -- ClimbHome +["%s stumbled."] = "%s绊倒了", -- User_Mission_-_Rope_Knock_Challenge +["%s (%s) tumbles like no other: %d points in one round."] = "%s (%s) 像其他人一样翻滚: 一个回合%d分", -- Space_Invasion +["%s stumpled."] = "%s绊倒了", -- User_Mission_-_Rope_Knock_Challenge +["%s (%s) was certainly not afraid of heights: Peak height of %d!"] = "%s (%s) 真的不怕高: 高度%d", -- ClimbHome +["%s (%s) was lightning-fast! Longest combo of %d, absolutely insane!"] = "%s (%s) 快如闪电,最长的连击%d", -- Space_Invasion +["%s (%s) was on fire: Longest combo of %d."] = "%s (%s) 着火了: 最长的连击%d", -- Space_Invasion +["%s (%s) was panicly afraid of the water and decided to get in a safe distance of %d from it."] = "%s (%s) 怕水并爬到安全距离%d", -- ClimbHome +["%s (%s) was the best baby tumbler: %d points in one round."] = "%s (%s) 是最好的baby tumbler: 一个回合%d分", -- Space_Invasion +["%s (%s) was the greediest hedgehog and collected %d crates."] = "%s (%s) 是最贪婪的刺猬,收集了%d箱子", -- Mutant +["%s (%s) was undoubtedly the very best professional tumbler in this game: %d points in one round!"] = "%s (%s) 无疑是最棒的tumbler: 一个回合%d分", -- Space_Invasion +["Star"] = "Star", -- Big_Armory +["Status update"] = "状态更新", -- Racer, TechRacer +["Status Update"] = "状态更新", -- Space_Invasion +["Stay away from our weapons!"] = "远离我们的武器", -- A_Classic_Fairytale:queen +["Stay there, comrades!"] = "停在那里,同志们", -- A_Classic_Fairytale:queen +["Stay there to flee!"] = "停在那里以逃跑", -- A_Space_Adventure:fruit01 +["Steel Eye"] = "Steel Eye", -- A_Classic_Fairytale:queen +["Step 1: Activate your flying saucer but do NOT move yet!"] = "第一步: 激活你的飞碟但不要移动", -- Basic_Training_-_Flying_Saucer +["Step 1: Start roping"] = "第一步: 吊在空中", -- Basic_Training_-_Rope +["Step 2: Select grenade"] = "第二步: 选择手榴弹", -- Basic_Training_-_Rope +["Step 2: Select your grenade."] = "第二步: 选择你的手榴弹", -- Basic_Training_-_Flying_Saucer +["Step 3: Drop the grenade"] = "第三步: 丢下手榴弹", -- Basic_Training_-_Rope +["Step 3: Start flying and get yourself right above the target."] = "第三步: 飞到目标上面", -- Basic_Training_-_Flying_Saucer +["Step 4: Drop your grenade by pressing the [Long jump] key."] = "第四步: 按[远跳]丢手榴弹", -- Basic_Training_-_Flying_Saucer +["Step 5: Get away quickly and land safely anywhere."] = "第五步: 快速离开,安全地着陆", -- Basic_Training_-_Flying_Saucer +["Step By Step"] = "一步一步", -- A_Classic_Fairytale:first_blood +["Steve"] = "Steve", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["Sticky Mine"] = "黏性地雷", -- Continental_supplies +["Sticky Mine Placement Mode"] = "黏性地雷放置模式", -- Construction_Mode +["STICKY MINE PLACEMENT MODE"] = "黏性地雷放置模式", -- HedgeEditor +["Stop, comrades!"] = "停下,同志们", -- A_Classic_Fairytale:queen +["Stop right there, puny worms!"] = "就停在那,小虫子", -- A_Classic_Fairytale:queen +["Street Fighters"] = "Street Fighters", -- +["Strength: %d (multiplier for ammo)"] = "强度: %d(弹药的乘数)", -- Battalion +["Strong knockback, but no poison."] = "强力击退,无毒", -- Continental_supplies +["Stronglings"] = "Stronglings", -- A_Classic_Fairytale:shadow +["Structure Placement Mode"] = "结构放置模式", -- Construction_Mode +["Structure Placer"] = "结构放置器", -- Construction_Mode +["Stupid, stupid Hogerians!"] = "愚蠢的刺猬", -- A_Space_Adventure:final +["Subtract %d"] = "减去%d", -- HedgeEditor +["--- Sudden Death ---"] = "---突然死亡---", -- Battalion +["Summer Squash"] = "Summer Squash", -- A_Space_Adventure:fruit01 +["Sundaland"] = "Sundaland", -- Continental_supplies +["Sunflame"] = "Sunflame", -- Big_Armory +["Super weapons: A few crates contain very powerful weapons."] = "超级武器: 少量箱子包含非常强大的武器", -- WxW +["Super weapons: %s"] = "超级武器: %s", -- WxW +["Supplies: Each continent gives you unique weapons, specials and health."] = "补给: 每个大陆给你唯一的武器,特别模式和血量", -- Continental_supplies +["Support Station: Allows placement of crates."] = "支援站: 允许放置箱子", -- Construction_Mode +["Support Station"] = "支援站", -- Construction_Mode +["Sure!"] = "当然!", -- A_Classic_Fairytale:epil +["Surf Before Crate: %s"] = "箱子之前冲浪: %s", -- WxW +["Surf Before Crate: You must bounce off the water once before you can get crates."] = "箱子之前冲浪: 你必须在能够拿到箱子之前在水面弹跳一次", -- WxW +["Surfer! +15 points!"] = "冲浪者,+15分", -- Space_Invasion +["Surfer!"] = "冲浪者", -- WxW +["Surprise supplies: Get 1-3 random weapons each turn."] = "惊喜补给: 每个回合得到1-3随机武器", -- Continental_supplies +["Survive!"] = "幸存", -- A_Classic_Fairytale:shadow +["%s violated the “All But Last” rule and will be penalized."] = "%s 违反了“All But Last”规则,会受到惩罚", -- WxW +["%s violated the “Kill The Leader” rule and will be penalized."] = "%s 违反了“Kill The Leader”规则,会受到惩罚", -- WxW +["Swap place with a random enemy in the circle."] = "和圈中一个随机敌人交换位置", -- Continental_supplies +["%s was a good target."] = "%s是一个好目标", -- User_Mission_-_Rope_Knock_Challenge +["%s was close to home."] = "%s离家很近", -- ClimbHome +["%s was damn close to home."] = "%s离家非常近", -- ClimbHome +["%s was doomed from the beginning."] = "%s一开始就毁灭了", -- User_Mission_-_Rope_Knock_Challenge +["%s was extracted from the scheme"] = "%s是从方案中提取的", -- Continental_supplies +["%s was good, but not good enough."] = "%s很好,但不够好", -- ClimbHome +["%s was knocked away."] = "%s撞走了", -- User_Mission_-_Rope_Knock_Challenge +["%s was really unlucky."] = "%s真的很不幸", -- User_Mission_-_Rope_Knock_Challenge +["%s was shoved away."] = "%s推走了", -- User_Mission_-_Rope_Knock_Challenge +["%s was smashed."] = "%s粉碎了", -- User_Mission_-_Rope_Knock_Challenge +["Sweet"] = "Sweet", -- +["%s went over a quarter of the way towards home."] = "%s走过了四分之一回家的路", -- ClimbHome +["%s! Why?!"] = "%s!为什么?!", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united +["Swing, Leaks A Lot, on the wings of the wind!"] = "摇晃,Leaks A Lot,在风的翅膀", -- A_Classic_Fairytale:first_blood +["Swing: [Left]/[Right]"] = "摇晃: [左]/[右]", -- Basic_Training_-_Rope +["%s wins, congratulations!"] = "%s赢了,恭喜", -- A_Space_Adventure:moon01 +["%s wins!"] = "%s赢了", -- Racer, Space_Invasion, TechRacer, ClimbHome +["%s wins with a best time of %.1fs."] = "%s以最好时间%.1f秒赢了", -- Racer, TechRacer +["switch"] = "切换", -- Continental_supplies +["Switch: Drop ball of dirt from parachute (once)"] = "切换: 从降落伞丢下泥球(一次)", -- Continental_supplies +["Switched to "] = "切换到", +["Switch Hedgehog (1/3)"] = "切换刺猬(1/3)", -- Basic_Training_-_Movement +["Switch Hedgehog (2/3)"] = "切换刺猬(2/3)", -- Basic_Training_-_Movement +["Switch Hedgehog (3/3)"] = "切换刺猬(3/3)", -- Basic_Training_-_Movement +["Switch Hedgehog (Failed!)"] = "切换刺猬(失败)", -- Basic_Training_-_Movement +["Switch hedgehog: [Tabulator]"] = "切换刺猬: [Tabulator]", -- Basic_Training_-_Movement +["Switch Hog"] = "切换刺猬", -- Construction_Mode +["Switch: Select weapon special"] = "切换: 选择武器特别模式", -- Continental_supplies +["Switch: Toggle crate radar"] = "切换: 切换箱子雷达", -- WxW +["%s won!"] = "%s赢了", -- A_Space_Adventure:fruit01 +["Swords"] = "Swords", -- Bazooka_Battlefield +["Syntax Errol"] = "Syntax Errol", -- A_Classic_Fairytale:dragon +["%s, you may choose the rules."] = "%s,你可以选择规则", -- WxW +["szczur"] = "szczur", -- +["Tackleberry"] = "Tackleberry", -- +["Tails"] = "Tails", -- +["Talk about mixed signals..."] = "讨论关于混合信号……", -- A_Classic_Fairytale:dragon +["Tall Potato"] = "Tall Potato", -- A_Space_Adventure:fruit01 +["Tap [Pause] to see the mission texts"] = "按[Pause]看任务信息", -- Basic_Training_-_Movement +["Tap the “rotating arrow” button on the left|until you have selected Cappy, the hedgehog with the cap!"] = "按左边的“旋转箭头”按钮|直到你选中Cappy,有帽子的刺猬", -- Basic_Training_-_Movement +["Target"] = "目标", -- HedgeEditor +["Target Placement Mode"] = "目标放置模式", -- Construction_Mode +["TARGET PLACEMENT MODE"] = "目标放置模式", -- HedgeEditor +["Target Practice: Bazooka (easy)"] = "目标练习: 火箭炮(简单)", -- Target_Practice_-_Bazooka_easy +["Target Practice: Bazooka (hard)"] = "目标练习: 火箭炮(困难)", -- Target_Practice_-_Bazooka_hard +["Target Practice: Grenade (easy)"] = "目标练习: 手榴弹(简单)", -- Target_Practice_-_Grenade_easy +["Target Practice: Grenade (hard)"] = "目标练习: 手榴弹(困难)", -- Target_Practice_-_Grenade_hard +["Target Practice: Homing Bee"] = "目标练习: 蜜蜂枪", -- Target_Practice_-_Homing_Bee +["Target Practice: Shotgun"] = "目标练习: 霰弹枪", -- Target_Practice_-_Shotgun +["Target Puncher"] = "目标捶打者", -- Basic_Training_-_Rope +["Targets left: %d"] = "目标剩余: %d", -- TargetPractice +["Tatsujin"] = "Tatsujin", -- +["Tatters"] = "Tatters", -- +["Team %d"] = "队伍%d", -- SimpleMission +["Team %d: "] = "队伍%d: ", +["Team highscore: %d"] = "队伍高分: %d", -- Utils +["Team Identity Mode"] = "队伍身份模式", -- HedgeEditor +["TEAM IDENTITY MODE"] = "队伍身份模式", -- HedgeEditor +["Team lowscore: %d"] = "队伍低分: %d", -- Utils +["Teams are tied! Continue playing rounds until we have a winner!"] = "队伍平手,继续玩直到出现胜者", -- Space_Invasion +["Team's best time: %.3fs"] = "队伍最佳时间: %.3f秒", -- Utils +["Team Scores:"] = "队伍分数:", -- Control +["Team scores:"] = "队伍分数:", -- Space_Invasion +["Team's longest time: %.3fs"] = "队伍最长时间: %.3f秒", -- Utils +["Team's top accuracy: %d%"] = "队伍最高准确度: %d%", -- Utils +["Teamwork 2"] = "团队工作2", -- User_Mission_-_Teamwork_2 +["Teamwork"] = "团队工作", -- User_Mission_-_Teamwork +["TechRacer"] = "TechRacer", -- TechRacer +["Teleporation Node"] = "传送节点", -- Construction_Mode +["Teleportation Mode"] = "传送模式", -- Construction_Mode +["Teleportation Node: Allows teleportation| between other nodes."] = "传送节点: 允许在节点之间传送", -- Construction_Mode +["Teleportation Node"] = "传送节点", -- Construction_Mode +["Teleport"] = "传送", -- Construction_Mode, Frenzy +["Teleport hint: just use the mouse to select the destination!"] = "传送提示: 只要用鼠标选择目的地", -- A_Classic_Fairytale:dragon +["Teleport hint: Just use the mouse to select the destination!"] = "传送提示: 只要用鼠标选择目的地", -- A_Classic_Fairytale:dragon +["Teleport to the impact location."] = "传送到子弹打中的位置", -- Continental_supplies +["Teleport to the top of the map, expect fall damage!"] = "传送到地图顶部,预计掉落伤害", -- Continental_supplies +["Teleport unsuccessful. Please teleport within a clan teleporter's sphere of influence."] = "传送未成功,请传送到一个战队的传送器影响范围内", -- Construction_Mode +["Teleport Unsuccessful. Please teleport within a clan teleporter's sphere of influence."] = "传送未成功,请传送到一个战队的传送器影响范围内", -- Construction_Mode +["Tentacle Terror"] = "恐怖触手", -- Tentacle_Terror +["Textile industry: Will give you a parachute every second turn."] = "纺织工业: 每两个回合给你一个降落伞", -- Continental_supplies +["Thanks!"] = "谢谢", -- A_Classic_Fairytale:family +["Thanks, dude! It really means a lot to me."] = "谢谢老兄,这对我意义重大", -- A_Classic_Fairytale:epil +["Thanks, man! It really means a lot to me."] = "谢谢老兄,这对我意义重大", -- A_Classic_Fairytale:epil +["Thank you, Dr. Cornelius."] = "谢谢你,Dr. Cornelius", -- A_Space_Adventure:cosmos +["Thank you for meeting me on such a short notice!"] = "谢谢你在通知后立刻和我见面", -- A_Space_Adventure:desert01 +["Thank you, my hero!"] = "谢谢你,我的英雄", -- A_Classic_Fairytale:family +["Thank you, oh, thank you, Leaks A Lot!"] = "谢谢你,Leaks A Lot", -- A_Classic_Fairytale:journey +["Thank you, oh, thank you, my heroes!"] = "谢谢你,我的英雄", -- A_Classic_Fairytale:journey +["Thanta"] = "Thanta", -- A_Space_Adventure:ice01 +["That is, indeed, very weird..."] = "那是,确实,非常古怪", -- A_Classic_Fairytale:united +["That makes it almost invaluable!"] = "那让它几乎无敌", -- A_Classic_Fairytale:enemy +["That ought to show them!"] = "他们应该尝到苦头了", -- A_Classic_Fairytale:backstab +["That's all, folks!"] = "那就是全部,大伙", -- A_Classic_Fairytale:epil +["That's for my father!"] = "那是给我父亲的", -- A_Classic_Fairytale:backstab +["That shaman sure knows what he's doing!"] = "那个萨满肯定知道他在做什么", -- A_Classic_Fairytale:shadow +["That Sinking Feeling"] = "淹没的感觉", +["That's just the way it works, you know."] = "那就是它的工作方式", -- A_Classic_Fairytale:queen +["That's not our problem!"] = "那不是我们的问题", -- A_Classic_Fairytale:enemy +["That's typical of you!"] = "那是你的典型", -- A_Classic_Fairytale:family +["That's why he always wears a hat since then."] = "那就是为什么他从那时起一直戴帽子", -- A_Space_Adventure:moon02 +["That traitor won't be killing us anymore!"] = "那个叛徒不会再杀我们了", -- A_Classic_Fairytale:queen +["That was just mean!"] = "那真是卑鄙", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united +["That was pointless. The flag will respawn next round."] = "那是没意义的,光环会在下一个回合重生", -- CTF_Blizzard +["The adventure begins!"] = "冒险开始了", -- A_Space_Adventure:cosmos +["The air bombs are weaker than usual."] = "空袭炸弹比通常的要弱", -- Battalion +["The aliens respect me, even worship me!"] = "外星人尊重我,甚至崇拜我", -- A_Classic_Fairytale:queen +["The ally units share their ammo."] = "盟友单位共享他们的弹药", -- A_Space_Adventure:fruit01 +["The ammo of %s has been vaporized"] = "%s的弹药被蒸发", -- Construction_Mode +["The answer is ... entertainment. You'll see what I mean."] = "答案是……娱乐,你会明白我的意思", -- A_Classic_Fairytale:backstab +["The answer is...entertaintment. You'll see what I mean."] = "答案是……娱乐,你会明白我的意思", -- A_Classic_Fairytale:backstab +["The anti-portal surface is all over the floor, and I have nothing to kill him. Dropping something could hurt him enough to kill him."] = "地上全是反传送表层,我没有东西能杀了他。丢下某些能伤害他的东西来杀了他", -- portal +["The big bang"] = "大爆炸", -- A_Space_Adventure:final +["The Boss"] = "首领", -- +["The boss has fallen! Retreat!"] = "首领倒下了,撤退", -- A_Space_Adventure:moon01 +["The Bull's Eye"] = "靶心", -- A_Classic_Fairytale:first_blood +["The caves are well hidden, they won't find us there!"] = "洞穴隐藏得很好,他们不会发现我们在那里", -- A_Classic_Fairytale:united +["The clan of the Red Strawberry wants to take over the dominion and overthrow King Pineapple."] = "红色草莓的氏族想要推翻Pineapple国王接管统治", -- A_Space_Adventure:fruit01 +["The continent of cowards"] = "懦夫的大陆", -- Continental_supplies +["The continent of dust"] = "尘土的大陆", -- Continental_supplies +["The continent of firearms"] = "火枪的大陆", -- Continental_supplies +["The continent of greed"] = "贪婪的大陆", -- Continental_supplies +["The continent of guerilla tactics"] = "游击战术的大陆", -- Continental_supplies +["The continent of ice and science"] = "冰和科学的大陆", -- Continental_supplies +["The continent of medicine"] = "医学的大陆", -- Continental_supplies +["The continent of ninjas"] = "忍者的大陆", -- Continental_supplies +["The continent of sports"] = "运动的大陆", -- Continental_supplies +["The Crate Frenzy"] = "The Crate Frenzy", -- A_Classic_Fairytale:first_blood +["The Customer is King"] = "The Customer is King", -- Challenge_-_Speed_Shoppa_-_ShoppaKing +["The device part has been stolen!"] = "设备部件被偷了", -- A_Space_Adventure:fruit02 +["The device part is hidden in one of the crates! Go and get it!"] = "设备部件隐藏在其中一个箱子,去拿到它", -- A_Space_Adventure:desert01 +["The Devs"] = "The Devs", -- +["The Dilemma"] = "The Dilemma", -- A_Classic_Fairytale:shadow +["The editor weapons and tools have been added!"] = "武器和工具编辑器已添加", -- HedgeEditor +["The editor weapons and tools have been removed!"] = "武器和工具编辑器已移除", -- HedgeEditor +["The enemies aren't many anyway, it is going to be easy!"] = "敌人不是很多,应该很简单", -- A_Space_Adventure:fruit01 +["The enemy can't move but it might be a good idea to stay out of sight!"] = "敌人不能移动,但呆在视线外是个好主意", -- A_Classic_Fairytale:dragon +["The enemy has taken a crate which we really needed!"] = "敌人拿走了我们真正需要的箱子", -- SimpleMission +["The enemy hogs play in a random order."] = "敌人刺猬随机顺序游玩", -- A_Space_Adventure:death02 +["The enemy is hiding out on yonder ducky!"] = "敌人藏在那边的鸭子!", +["The Enemy Of My Enemy"] = "我的敌人的敌人", -- A_Classic_Fairytale:enemy +["The explosion is weaker than usual."] = "爆炸比通常要弱", -- Battalion +["The fastest hedgehog was %s from %s with a time of %.3fs."] = "最快的刺猬是%s从%s,时间%.3f秒", -- TrophyRace +["The fight begins!"] = "战斗开始", -- A_Space_Adventure:moon01 +["The final part"] = "最后的部件", -- A_Space_Adventure:death01 +["The final targets are quite tricky. You need to aim well."] = "最后的目标很难打中,你需要好好瞄准", -- Basic_Training_-_Bazooka +["The First Blood"] = "第一滴血", -- A_Classic_Fairytale:first_blood +["The First Encounter"] = "第一次遭遇", -- A_Classic_Fairytale:shadow +["The first hedgehog to kill someone becomes the Mutant."] = "第一个杀人的刺猬会成为变种人", -- Mutant +["The first hedgehog which scores %d or more wins the game."] = "第一个得分%d或更多的刺猬获胜", -- Mutant +["The first stop"] = "第一站", -- A_Space_Adventure:moon01 +["The first turn will last 25 sec and every other turn 15 sec."] = "第一回合25秒,其他回合15秒", -- A_Space_Adventure:fruit03 +["The flag will respawn next round."] = "光环会在下一回合重生", +["The flood has stopped! Challenge over."] = "洪水停止了,挑战结束", -- User_Mission_-_That_Sinking_Feeling +["The food bites back"] = "食物反咬一口", -- A_Classic_Fairytale:backstab +["The forgotten continent"] = "遗忘的大陆", -- Continental_supplies +["The giant umbrella from the last crate should help break the fall."] = "最后一个箱子的大伞应该能帮助你降落", -- A_Classic_Fairytale:first_blood +["The Great Escape"] = "大逃离", -- User_Mission_-_The_Great_Escape +["- The green target must survive"] = "- 绿色的目标必须幸存", -- HedgeEditor +["- The green targets must survive"] = "- 绿色的目标必须幸存", -- HedgeEditor +["The guardian"] = "守护者", -- A_Classic_Fairytale:shadow +["The hardships of the war turned %s (%s) into a killing machine: %d invaders destroyed in one round!"] = "战争的艰难把%s(%s)变成杀戮机器: 一个回合消灭了%d入侵者", -- Space_Invasion +["The health of your current hedgehog|is shown at the top right corner."] = "你当前刺猬的血量显示在右上角", -- Basic_Training_-_Movement +["The hedgehog with least points (or most deaths) becomes the Bottom Feeder."] = "分数最少(或者死最多次)的刺猬变成喂鱼人", -- Mutant +["The Hospital"] = "The Hospital", -- +["The Individualist"] = "The Individualist", -- A_Classic_Fairytale:shadow +["Their buildings were very primitive back then, even for an uncivilised island."] = "他们的建筑当时很原始, 即使是一个没有文明的岛屿", -- A_Classic_Fairytale:united +["The Iron Curtain"] = "The Iron Curtain", -- +["The Journey Back"] = "归途", -- A_Classic_Fairytale:journey +["The king of %s has died!"] = "%s的国王死了", -- Battalion +["The last encounter"] = "最后的遭遇", -- A_Space_Adventure:death01 +["The last surviving clan wins."] = "最后活着的战队赢", -- TrophyRace +["The leader escaped. Defeat the rest of the aliens!"] = "首领逃走了,打败剩下的外星人", -- A_Classic_Fairytale:queen +["The leader seems scared, he will probably flee."] = "首领似乎很害怕,他可能会逃走", -- A_Classic_Fairytale:queen +["The Leap of Faith"] = "信仰之跃", -- A_Classic_Fairytale:first_blood +["The meteorite has come too close and the anti-gravity device isn't powerful enough to stop it now."] = "陨石靠得太近,反重力设备不够强大以停止它", -- A_Space_Adventure:cosmos +["The Moonwalk"] = "The Moonwalk", -- A_Classic_Fairytale:journey +["The Mutant has super weapons and a lot of health."] = "变种人有超级武器和很多血量", -- Mutant +["The Mutant has super-weapons and a lot of health."] = "变种人有超级武器和很多血量", -- Mutant +["The Mutant loses health quickly, but gains health by killing."] = "变种人掉血很快,但杀人会加血", -- Mutant +["The Mutant loses health quickly if he doesn't keep scoring kills."] = "变种人掉血很快,如果他不保持杀人得分", -- Mutant +["The Navy greets %s for managing to get in a distance of %d away from the mainland!"] = "海军问候%s,因为他设法远离大陆%d距离", -- ClimbHome +["The next 4 times you play the \"The last encounter\" mission you'll get 20 more hit points and a laser sight."] = "接下来四次你玩“最后的遭遇”任务,会得到额外20血量和一个激光瞄准", -- A_Space_Adventure:death02 +["The next crate is an utility crate."] = "下一个箱子是工具箱", -- Basic_Training_-_Movement +["The next one is pretty hard! |Tip: You have to do multiple swings!"] = "下一个很难|提示: 你必须多次摇晃", -- Basic_Training_-_Rope +["The next target can only be reached by something called “bouncing bomb”."] = "下一个目标只能由“弹跳炸弹”碰到", -- Basic_Training_-_Bazooka +["The next target is high in the sky."] = "下一个目标在天上", -- Basic_Training_-_Bazooka +["Then how do they keep appearing?"] = "那他们怎么继续出现?", -- A_Classic_Fairytale:shadow +["The Ninja-Samurai Alliance"] = "忍者武士联盟", -- +["Then prepare for battle!"] = "那就准备战斗", -- A_Space_Adventure:death01 +["Then what am I?"] = "那我是什么?", -- A_Classic_Fairytale:epil +["The only woman, huh?"] = "唯一的女人,唔?", -- A_Classic_Fairytale:epil +["The oppression of the elders, of course!"] = "长辈的压迫,当然", -- A_Classic_Fairytale:queen +["The opression of the elders, of course!"] = "长辈的压迫,当然", -- A_Classic_Fairytale:queen +["The other hog has died, he should have survived!"] = "其他刺猬都死了,他应该还活着", -- A_Space_Adventure:moon02 +["The other one were all cannibals, spending their time eating the organs of fellow hedgehogs..."] = "其他全是食人族,花时间吃刺猬的器官……", -- A_Classic_Fairytale:first_blood +["The Police"] = "The Police", -- +["The power of love! No, wait, the power of the aliens!"] = "爱的力量,不,等一下,外星人的力量", -- A_Classic_Fairytale:queen +["The RC plane only carries 2 weak bombs."] = "遥控飞机只携带两个弱炸弹", -- Battalion +["There are a variety of structures available to aid you."] = "有不同的结构可以帮助你", -- Construction_Mode +["There are no snarky comments this time."] = "这次没有刻薄的评论", -- Space_Invasion +["There is one below us!"] = "我们下面有一个", -- A_Space_Adventure:ice01 +["There must be a spy among us!"] = "那一定是我们中间的间谍", -- A_Classic_Fairytale:backstab +["There's more of them? When did they become so hungry?"] = "那里有更多食人族?他们什么时候变得这么饥饿?", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united +["There's nothing more satisfying for me than seeing you share your beauty with the world every morning, my princess!"] = "没有什么比看到你每个早晨和世界分享你的美丽更让我满足,我的公主", -- A_Classic_Fairytale:journey +["There's nothing more satisfying to us than seeing you share your beauty..."] = "没有什么比看到你分享你的美丽更让我们满足……", -- A_Classic_Fairytale:journey +["There's nothing more satisfying to us than seeing you share your beauty with the world every morning, my princess!"] = "没有什么比看到你每个早晨和世界分享你的美丽更让我们满足,我的公主", -- A_Classic_Fairytale:journey +["The respawner respawns %s"] = "重生器重生了%s", -- Construction_Mode +["The Rising"] = "The Rising", -- A_Classic_Fairytale:first_blood +["The rope won't get reset."] = "绳索不会被重置", -- A_Space_Adventure:death02 +["The Savior"] = "The Savior", -- A_Classic_Fairytale:journey +["The score and deaths are shown next to the team bar."] = "分数和死亡在队伍栏旁边显示", -- Mutant +["These girders are slippery, like ice."] = "这些大梁像冰一样滑", -- Basic_Training_-_Movement +["These primitive people are so funny!"] = "这些原始人真搞笑", -- A_Classic_Fairytale:backstab +["These weapon specials cannot be used close to other hogs."] = "这些武器特别不能靠近其他刺猬使用", -- Continental_supplies +["The Shadow Falls"] = "暗影降临", -- A_Classic_Fairytale:shadow +["The Showdown"] = "The Showdown", -- A_Classic_Fairytale:shadow +["The Slaughter"] = "The Slaughter", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:first_blood +["The Society of Perfectionists greets %s (%s): No misses and %d hits in its best round."] = "完美主义者协会问候%s(%s): 在他最好的回合攻击%d次没有失手", -- Space_Invasion +["The Specialists: Each hedgehog starts with its own weapon set"] = "专家: 每个刺猬有自己的武器集", -- The_Specialists +["The spinning arrows above your hedgehog show|which hedgehog is selected right now."] = "你的刺猬的上面显示的旋转箭头表示正在选择他", -- Basic_Training_-_Movement +["The spirits of the ancerstors are surely pleased, Leaks A Lot."] = "祖先的灵魂一定很高兴,Leaks A Lot", -- A_Classic_Fairytale:first_blood +["The spirits of the ancestors are surely pleased, Leaks A Lot."] = "祖先的灵魂一定很高兴,Leaks A Lot", -- A_Classic_Fairytale:first_blood +["The targets will guide you through the training."] = "目标会指引你通过训练", -- Basic_Training_-_Rope +["The team continued their quest of finding the rest of the tribe."] = "队伍继续他们寻找剩余部落的任务", -- A_Classic_Fairytale:queen +["The teams are tied for the fastest time."] = "队伍的最快时间平手", -- Racer, TechRacer +["The teams were tied, so an additional round has been played to determine the winner."] = "队伍平手,所以要再玩一个回合决定胜者", -- Space_Invasion +["The teams were tied, so %d additional rounds have been played to determine the winner."] = "队伍平手,所以要再玩%d回合决定胜者", -- Space_Invasion +["The time that you have left when you reach the blue hedgehog will be added to the next turn."] = "你碰到蓝色刺猬的剩余时间会添加到下一回合", -- A_Space_Adventure:moon02 +["The Torment"] = "The Torment", -- A_Classic_Fairytale:first_blood +["The truth about Professor Hogevil"] = "关于Hogevil教授的真相", -- A_Space_Adventure:moon02 +["The tunnel entrance is over there."] = "隧道的入口在那里", -- A_Space_Adventure:desert01 +["The tunnel is about to get flooded!"] = "隧道就要淹没了", -- A_Space_Adventure:desert02 +["The Tunnel Maker"] = "The Tunnel Maker", -- A_Classic_Fairytale:journey +["The Ultimate Weapon"] = "终极武器", -- A_Classic_Fairytale:first_blood +["The Union"] = "联盟", -- A_Classic_Fairytale:enemy +["The Union: You can select a hedgehog at the start of your turns."] = "联盟: 你可以在回合开始选择一个刺猬", -- Continental_supplies +["The village, unprepared, was destroyed by the cyborgs..."] = "村子,没有准备好,被机器人摧毁了", -- A_Classic_Fairytale:journey +["The walk of Fame"] = "The walk of Fame", -- A_Classic_Fairytale:shadow +["The wasted youth"] = "The wasted youth", -- A_Classic_Fairytale:first_blood +["The way you handled your little internal conflicts …"] = "你处理你的小内部冲突的方式……", -- A_Classic_Fairytale:queen +["The weapon in that last crate was bestowed upon us by the ancients!"] = "最后的箱子里的武器是祖先赋予我们的", -- A_Classic_Fairytale:first_blood +["The what?!"] = "The what?!", -- A_Classic_Fairytale:dragon +["The wind whispers that you are ready to become familiar with tools, now..."] = "风声低语你准备好变得熟悉工具,现在……", -- A_Classic_Fairytale:first_blood +["The wrong hedgehog has taken the crate."] = "错误的刺猬拿到了箱子", -- SimpleMission +["They are all waiting back in the village, haha."] = "他们都在村子里等着,哈哈", -- A_Classic_Fairytale:enemy +["They are up there! Take this rope and hurry!"] = "他们在那里,拿着这个绳索并赶快", -- A_Space_Adventure:moon01 +["They Call Me Bullseye! +16 points!"] = "他们叫我靶心,+16分", -- Space_Invasion +["They have weapons we've never seen before!"] = "他们有我们从没见过的武器", -- A_Classic_Fairytale:united +["They keep appearing like this. It's weird!"] = "他们一直这样出现,有点奇怪", -- A_Classic_Fairytale:united +["They killed %s! You bastards!"] = "他们杀了%s,你这个混蛋", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:united +["They must be trying to weaken us!"] = "他们一定是在试图削弱我们", -- A_Classic_Fairytale:enemy +["They never learn"] = "They never learn", -- A_Classic_Fairytale:journey +["They stumbled upon a pile of weapons, they seemed to be getting closer."] = "他们绊倒在一堆武器,他们似乎更接近了", -- A_Classic_Fairytale:queen +["They told us to wear these clothes. They said that this is the newest trend."] = "他们叫我们穿上这些衣服,他们说这是新潮流", -- A_Classic_Fairytale:enemy +["They've been manipulating us all this time!"] = "他们一直在操控我们", -- A_Classic_Fairytale:enemy +["They won't hesitate to attack you in order to rob you!"] = "他们不会犹豫攻击并抢劫你", -- A_Space_Adventure:desert01 +["The Zoo"] = "The Zoo", -- +["Thighlicker"] = "Thighlicker", -- A_Classic_Fairytale:united +["Things are going to get messy around here."] = "这里的事情越来越乱", -- A_Space_Adventure:fruit01 +["This allows to select any hedgehog in your team!"] = "这个允许你选择队伍中任意刺猬", -- Basic_Training_-_Movement +["This allows you to create a crate anywhere|within your clan's area of influence,|at the cost of energy."] = "这个允许你在战队的影响范围内|任何地方创造一个箱子", -- Construction_Mode +["This allows you to create and place mines,|sticky mines and barrels anywhere within your|clan's area of influence at the cost of energy."] = "这个允许你在战队的影响范围内|任何地方创造和放置一个地雷、黏性地雷和油桶", -- Construction_Mode +["This allows you to create and place mines,|sticky mines and barrels anywhere within your|clan's area of influence at the cost of energy.|Up/down: Choose object type|Left/right: Choose timer (for mines)|Cursor: Place object"] = "这个允许你在战队的影响范围内|任何地方创造和放置一个地雷、黏性地雷和油桶|上/下: 选择物体|左/右: 选择定时器(地雷)|光标: 放置物体", -- Construction_Mode +["This almost concludes our tutorial."] = "我们的教程就要结束了", -- Basic_Training_-_Flying_Saucer +["This also increases the effectiveness of Medicine."] = "这个能增加药效", -- Continental_supplies +["This game wasn’t really exciting."] = "这个游戏不是真的刺激", -- Space_Invasion +["This is a new personal best, congratulations!"] = "这是新的个人最佳记录,恭喜", -- A_Space_Adventure:death02, A_Space_Adventure:desert02, A_Space_Adventure:fruit03 +["This is a new personal best time, congratulations!"] = "这是新的个人最佳时间,恭喜", -- A_Space_Adventure:ice02, A_Space_Adventure:moon02 +["This is Cappy."] = "这个是Cappy", -- Basic_Training_-_Movement +["This is it! It's time to make Fell From Heaven fall for me..."] = "就是这个,是时候让Fell From Heaven爱上我……", -- A_Classic_Fairytale:first_blood +["This island is the only place left on Earth with grass on it!"] = "这个岛屿是地球上唯一有草的地方", -- A_Classic_Fairytale:enemy +["This is seems like a wealthy hedgehog, nice ..."] = "好像是个有钱的刺猬,很好……", -- A_Space_Adventure:desert01 +["This is the mission panel."] = "这是任务面板", -- Basic_Training_-_Movement +["This is the Olympic stadium of saucer flying."] = "这是飞碟的奥林匹克体育场", -- A_Space_Adventure:ice02 +["This is the Olympic Stadium of Saucer Flying."] = "这是飞碟的奥林匹克体育场", -- A_Space_Adventure:ice02 +["This is typical!"] = "这是典型的", -- A_Classic_Fairytale:dragon +["This must be some kind of sorcery!"] = "这一定是某种巫术", -- A_Classic_Fairytale:shadow +["This must be the caves!"] = "这一定是洞穴", -- A_Classic_Fairytale:backstab +["This one's tricky."] = "这个有点难,打个洞", +["This planet seems dangerous!"] = "这个星球似乎很危险", -- A_Space_Adventure:cosmos +["This rain is really something..."] = "下大雨了", +["This round’s award for ultimate disappointment goes to: Everyone!"] = "这个回合的终极失望奖给: 每个人", -- ClimbHome +["This seems like a wealthy hedgehog, nice ..."] = "好像是个有钱的刺猬,很好……", -- A_Space_Adventure:desert01 +["This %s is so naive! I'm going to shoot this fool so I can keep that device for myself!"] = "这个%s真幼稚,我打算射击这个傻子,自己留着设备", -- A_Space_Adventure:fruit02 +["This was an awesome performance! But this challenge can be finished with even just one RC plane. Can you figure out how?"] = "真是精彩的表演,但这个挑战可以只用一个遥控飞机完成,你能做到吗", -- User_Mission_-_RCPlane_Challenge +["This will be fun!"] = "这会很有趣", -- A_Classic_Fairytale:enemy +["This will be useful when I need a new platform or if I want to rise."] = "这会很有用,当我需要一个新的平台,或者我想要上升", -- portal +["This will certainly come in handy."] = "这肯定会派上用场", -- User_Mission_-_Teamwork_2 +["This will certianly come in handy."] = "这肯定会派上用场", -- User_Mission_-_Teamwork_2 +["Thompson"] = "Thompson", -- +["Those aliens are destroying the island!"] = "这些外星人正在破坏岛屿", -- A_Classic_Fairytale:family +["Those were scheduled for disposal anyway."] = "不管怎样,这些都是预定要处理的", -- A_Classic_Fairytale:dragon +["Throw a 1 second mine!"] = "投掷一个1秒的地雷", -- Continental_supplies +["Throw a baseball at your foes|and send them flying!"] = "向你的敌人投掷一个棒球|让他们起飞", -- Knockball +["Throw a grenade to destroy the target!"] = "投掷一个手榴破坏毁目标", -- Basic_Training_-_Grenade +["Throw some grenades to destroy the targets!"] = "投掷一些手榴破坏毁目标", -- Basic_Training_-_Grenade +["Thug #%d"] = "暴徒#%d", -- A_Space_Adventure:death01 +["Tie-breaking round %d"] = "打破平局的回合%d", -- Space_Invasion +["Timbers"] = "Timbers", -- +["Time: %.1fs"] = "时间: %.1f秒", -- Racer, TechRacer +["Time: %.3fs by %s"] = "时间: %s的%.3f秒", -- TrophyRace +["Time: %.3fs"] = "时间: %.3f秒", -- TrophyRace +["Time Box"] = "时光箱", -- Construction_Mode +["Timed Kamikaze! +10 points!"] = "Timed神风特攻队,+10分", -- Space_Invasion +["Time extended! +%dsec"] = "时间延长,+%d秒", -- Space_Invasion +["Time extension: %ds"] = "时间延长: %d秒", -- Tumbler +["Time for a more interesting stunt, but first just collect the next crate!"] = "更有趣的绝技时间,但先收集下一个箱子", -- Basic_Training_-_Flying_Saucer +["Timer"] = "定时器", -- Basic_Training_-_Grenade +["Time's up!"] = "时间到", -- Basic_Training_-_Sniper_Rifle, SpeedShoppa, Space_Invasion +["Time’s up!"] = "时间到", -- TargetPractice +["Time to run!"] = "逃跑时间", -- A_Space_Adventure:fruit01 +["Tip: Changing your aim while flying is very difficult, so adjust it before you take off."] = "提示: 在飞行时改变瞄准是很难的,所以在起飞前调整好", -- Basic_Training_-_Flying_Saucer +["Tip: Don't remain for too long in the water, or you won't make it."] = "提示: 不要在水里停留太长时间", -- Basic_Training_-_Flying_Saucer +["Tip: If you get stuck in this training, use \"Skip turn\" to restart the current objective."] = "提示: 如果你在这个训练卡住,使用“跳过回合”来重新开始当前目标", -- Basic_Training_-_Flying_Saucer +["Tip: See the \"esc\" key (this menu) if you want to see the currently playing teams continent, or that continents specials."] = "提示: 按“Esc”如果你想看当前游玩的队伍的大陆,或者大陆的特别模式", -- Continental_supplies +["Tip: See the \"Esc\" key (this menu) if you want to see the currently playing teams continent, or that continents specials."] = "提示: 按“Esc”如果你想看当前游玩的队伍的大陆,或者大陆的特别模式", -- Continental_supplies +["Tip: The rope physics are different than in the real world, |use it to your advantage!"] = "提示: 绳索的物理效果不同于真实世界|利用它为你带来好处", -- Basic_Training_-_Rope +["Tip: You can change your flying saucer|in mid-flight by hitting the [Attack] key twice."] = "提示: 你可以在空中按两次[攻击]改变你的飞碟", -- Basic_Training_-_Flying_Saucer +["Tiyuri"] = "Tiyuri", -- +["Toad"] = "Toad", -- +["To begin, walk to the crate to the right."] = "走到右边的箱子", -- Basic_Training_-_Movement +["To begin with the training, hit the attack key!"] = "按攻击键开始训练", -- Basic_Training_-_Movement +["To begin with the training, select the bazooka from the ammo menu!"] = "从弹药菜单选择火箭炮开始训练", -- Basic_Training_-_Bazooka +["To begin with the training, select the grenade from the ammo menu!"] = "从弹药菜单选择手榴弹开始训练", -- Basic_Training_-_Grenade +["To begin with the training, tap the attack button!"] = "按攻击键开始训练", -- Basic_Training_-_Movement +["To finish hedgehog selection, just do anything|with him, like walking."] = "要完成刺猬选择,随便做点什么,比如走动", -- Basic_Training_-_Movement +["To get over the next obstacles, keep some distance from the wall before you back jump."] = "克服下一个障碍,后跳前和墙壁保持一点距离", -- Basic_Training_-_Movement +["To get over the water, you have to do multiple|rope shots and swings."] = "要越过水面,你必须多次发射绳索和摇晃", -- Basic_Training_-_Rope +["Toggle Editing Weapons and Tools: [Precise]+[2]"] = "切换编辑武器和工具: [精确]+[2]", -- HedgeEditor +["Toggle Help: [Precise]+[1]"] = "切换帮助: [精确]+[1]", -- HedgeEditor +["Toggle Placement/Deletion: [Left], [Right]"] = "切换放置/删除: [左]/[右]", -- HedgeEditor +["Toggle Shield: [Long jump]"] = "切换护盾: [远跳]", -- Space_Invasion +["To help you, of course!"] = "当然是来帮你", -- A_Classic_Fairytale:journey +["To launch a projectile in mid-flight, hold [Precise] and press [Long jump]."] = "要在飞行中发射炮弹,长按[精确]和按[远跳]", -- Basic_Training_-_Flying_Saucer +["Tony"] = "Tony", -- +["Too bad! Then you should really leave!"] = "太糟糕了,然后你真的该离开", -- A_Space_Adventure:fruit01 +["Too slow! Try again ..."] = "太慢了,再试一次", -- A_Space_Adventure:moon02 +["Top-class elite pilot"] = "顶级精英飞行员", -- User_Mission_-_RCPlane_Challenge +["To reach higher ground, walk to a ledge, look to the left, then do a back jump."] = "要到达更高的地面,走到墙边,看向左边,后跳", -- Basic_Training_-_Movement +["Torn Muscle"] = "Torn Muscle", -- A_Classic_Fairytale:journey +["To the caves..."] = "去洞穴……", -- A_Classic_Fairytale:united +["Touch all waypoints as fast as you can!"] = "尽快触碰所有路径点", -- Racer +["- Touch the sparkles near your base to teleport"] = "- 触碰你的基地附近的火花来传送", -- CTF_Blizzard +["To win the game, %s has to get the bottom crates and come back to the surface."] = "要在游戏中获胜,%s必须得到底部的箱子并回到表面", -- A_Space_Adventure:fruit02 +["To win the game you had to collect the 2 crates with no specific order."] = "要在游戏中获胜,你必须收集两个箱子(无指定顺序)", -- A_Space_Adventure:desert01 +["To win the game you have to eliminate Professor Hogevil."] = "要在游戏中获胜,你必须干掉Hogevil教授", -- A_Space_Adventure:death01 +["To win the game you have to find the right crate."] = "要在游戏中获胜,你必须找到正确的箱子", -- A_Space_Adventure:desert01 +["To win the game you have to go next to Thanta."] = "要在游戏中获胜,你必须靠近Thanta", -- A_Space_Adventure:ice01 +["To win the game you have to go to the surface."] = "要在游戏中获胜,你必须去到表面", -- A_Space_Adventure:desert02 +["To win the game you have to pass into the rings in time."] = "要在游戏中获胜,你必须在时间内进入环", -- A_Space_Adventure:ice02 +["To win the game you have to stand next to Thanta."] = "要在游戏中获胜,你必须站在Thanta旁边", -- A_Space_Adventure:ice01 +["Toxic Team"] = "Toxic Team", -- User_Mission_-_Diver, User_Mission_-_Spooky_Tree, User_Mission_-_Teamwork +["Track completed!"] = "赛道完成", -- Racer, TechRacer +["Training"] = "训练", -- Basic_Training_-_Flying_Saucer, Basic_Training_-_Rope +["Training complete!"] = "训练完成", -- Basic_Training_-_Flying_Saucer +["Traitors"] = "叛徒", -- A_Classic_Fairytale:epil +["Traitors don't get to shout around here!"] = "叛徒别在这里大叫", -- A_Classic_Fairytale:epil +["Trapper"] = "Trapper", -- HedgeEditor +["Travel carefully as your fuel is limited"] = "旅行小心因为你的燃料是有限的", -- A_Space_Adventure:cosmos +["Travel to all the neighbor planets and collect all the pieces"] = "旅行到所有邻近行星收集所有部件", -- A_Space_Adventure:cosmos +["Treasure: Massive weapon bonus in first turn."] = "宝藏: 第一回合大量武器奖励", -- Continental_supplies +["Tribe"] = "部落", -- A_Classic_Fairytale:backstab +["TrophyRace"] = "TrophyRace", +["Trunks"] = "Trunks", -- +["Try again!"] = "再试一次", -- Basic_Training_-_Flying_Saucer +["Try it now and dive here to collect the crate on the right girder."] = "现在在这里试着潜水来收集右边大梁的箱子", -- Basic_Training_-_Flying_Saucer +["Try not to get spotted by the guards!"] = "试着不被卫兵发现", -- A_Space_Adventure:cosmos +["Try out different bounciness levels to reach difficult targets."] = "试着用不同的弹力炸到困难的目标", -- Basic_Training_-_Grenade +["Try to be smart and eliminate them quickly. This way you might scare off the rest!"] = "试着尽快消灭他们,这样你可能吓跑剩下的", -- A_Space_Adventure:fruit01 +["Try to keep as many allies alive as possible."] = "试着尽可能让盟友活下来", -- A_Space_Adventure:fruit01 +["Try to land softly, as you can still take fall damage!"] = "缓慢着陆,不然你会受到坠落伤害", -- Basic_Training_-_Flying_Saucer +["Try to protect the chief! You won't lose if he dies, but it is advised that he survives."] = "试着保护首领,如果他死了你不会输,但是建议让他活着", -- A_Classic_Fairytale:united +["Try to reach and destroy the next target quickly."] = "试着快速接近并破坏下一个目标", -- Basic_Training_-_Rope +["Tumbler"] = "Tumbler", -- Tumbler +["Turn around: [Left Shift] + [Left]/[Right]"] = "转身: [左Shift]+[左]/[右]", -- Basic_Training_-_Movement +["Turning Around"] = "转身", -- Basic_Training_-_Movement +["Turns: Hogs get %d random weapon(s) from their pool"] = "回合: 刺猬从他们职业得到%d随机武器", -- Battalion +["Turns: King's health is set to %d%% of the team health"] = "回合: 国王的血量设为队伍血量的%d%%", -- Battalion +["Turns left: %d"] = "剩余回合: %d", -- A_Classic_Fairytale:journey +["Turns: Refill %d weapon and %d helper points|and randomize weapons and helpers based on team points"] = "回合: 基于队伍分数重新装满%d武器和%d工具分数|和随机武器和工具", -- Battalion +["Turns until arrival: %d"] = "回合直到到达: %d", -- A_Classic_Fairytale:backstab +["Turn Time: %dsec"] = "回合时间: %d秒", -- Space_Invasion +["Twenty-Twenty"] = "Twenty-Twenty", -- +["Two flowers: All missions complete"] = "两朵花: 所有任务完成", -- A_Space_Adventure:cosmos +["Two little hogs cooperating, getting past obstacles..."] = "两个小刺猬合作通过障碍", -- A_Classic_Fairytale:journey +["Ugly Mug"] = "Ugly Mug", -- +["Uhm...I met one of them and took his weapons."] = "呃……我遇到他们其中一个并拿走他的武器", -- A_Classic_Fairytale:shadow +["Uhmm, it's … uhm … my ring!"] = "呃……这是……我的戒指", -- A_Classic_Fairytale:queen +["Uhmm...ok no."] = "呃……好吧,不是", -- A_Classic_Fairytale:enemy +["Ukemi"] = "Ukemi", -- +["Ultra kill!"] = "Ultra kill", -- Mutant +["unC0Rr"] = "unC0Rr", -- +["Under Construction"] = "建造", -- A_Classic_Fairytale:shadow +["Under normal circumstances we could easily defeat them but we have kindly sent most of our men to the Kingdom of Sand to help with the annual dusting of the king's palace."] = "在正常情况我们可以轻松打败他们,但我们好心地派出大部分人去沙之王国,帮助国王宫殿的每年打扫", -- A_Space_Adventure:fruit01 +["Under the meteorite’s shadow ..."] = "陨石的阴影之下", -- A_Space_Adventure:cosmos +["Under the meteorites shadow ..."] = "陨石的阴影之下", -- A_Space_Adventure:cosmos +["Unexpected Igor"] = "Unexpected Igor", -- A_Classic_Fairytale:dragon +["Unique new weapons"] = "新的唯一武器", -- Continental_supplies +["Unit"] = "单位", +["Unit 0x0007"] = "单位 0x0007", -- A_Classic_Fairytale:family +["Unit 189"] = "单位 189", -- +["Unit 234"] = "单位 234", -- +["Unit 333"] = "单位 333", -- +["Unit 334a$7%;.*"] = "单位 334a$7%;.*", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:family, A_Classic_Fairytale:queen, A_Classic_Fairytale:united +["Unit 3378"] = "单位 3378", +["Unit 485"] = "单位 485", -- +["Unit 527"] = "单位 527", -- +["Unit 638"] = "单位 638", -- +["Unit 709"] = "单位 709", -- +["Unit 835"] = "单位 835", +["Unit 881"] = "单位 881", -- User_Mission_-_Newton_and_the_Hammock +["Unit 883"] = "单位 883", -- +["United We Stand"] = "团结则存", -- A_Classic_Fairytale:united +["Unlike bazookas, grenades are not influenced by wind."] = "不像火箭炮,手榴弹不受风力影响", -- Basic_Training_-_Grenade +["Unlimited Attacks: Attacks don't end your turn"] = "无限攻击: 攻击不会结束你的回合", -- User_Mission_-_Diver, User_Mission_-_Nobody_Laugh, User_Mission_-_Spooky_Tree +["Unlucky Sods"] = "Unlucky Sods", -- User_Mission_-_Rope_Knock_Challenge +["Unstoppable!"] = "无人能挡!", +["Unsuspecting Louts"] = "Unsuspecting Louts", -- User_Mission_-_Rope_Knock_Challenge +["Up/Down: Adjust dust storm damage"] = "上/下: 调整沙尘暴伤害", -- Continental_supplies +["Up/Down: Browse through continents"] = "上/下: 浏览大陆", -- Continental_supplies +["Up/Down: Change placement mode"] = "上/下: 改变放置模式", -- HedgeEditor +["Up/down: Choose crate type"] = "上/下: 选择箱子类型", -- Construction_Mode +["Up/down: Choose object type|1-5/Switch/Left/Right: Choose mine timer|Cursor: Place object"] = "上/下: 选择物体类型|1-5/切换/上/下: 选择地雷定时器", -- Construction_Mode +["Upper-class elite pilot"] = "上等精英飞行员", -- User_Mission_-_RCPlane_Challenge +["Upside-Down World"] = "颠倒的世界", -- Continental_supplies +["Use it wisely!"] = "明智地使用它", -- A_Classic_Fairytale:dragon +["Use it with precaution!"] = "小心使用", -- A_Classic_Fairytale:first_blood +["User Challenge"] = "玩家挑战", +["!"] = "!", -- User_Mission_-_Dangerous_Ducklings +["User Mission"] = "玩家任务", -- HedgeEditor +["Use the attack key twice to change the flying saucer while being in air."] = "升空时按两次攻击键改变飞碟", -- A_Space_Adventure:ice02 +["Use the attack key twice to change the flying saucer while floating in mid-air."] = "飞行时按两次攻击键改变飞碟", -- A_Space_Adventure:ice02 +["Use the bazooka and the flying saucer to get the freezer."] = "使用火箭炮和飞碟得到冰冻枪", -- A_Space_Adventure:ice01 +["Use the flying saucer from the crate to fly to the moon."] = "使用从箱子里得到的飞碟飞到月球", -- A_Space_Adventure:cosmos +["Use the flying saucer to fly the other planets."] = "使用飞碟飞到其他星球", -- A_Space_Adventure:cosmos +["Use the flying saucer to fly to the other planets."] = "使用飞碟飞到其他星球", -- A_Space_Adventure:cosmos +["Use the parachute to get the next crate."] = "使用降落伞得到下一个箱子", -- A_Classic_Fairytale:first_blood +["Use the portal gun to get to the next crate, then use the new gun to get to the final destination!|"] = "使用传送门枪得到下一个箱子,然后使用新的武器到达终点|", -- A_Classic_Fairytale:dragon +["Use the RC plane and destroy the all the targets."] = "使用遥控飞机破坏所有目标", -- A_Space_Adventure:desert03 +["Use the rope in order to catch the blue hedgehog"] = "使用绳索追上蓝色刺猬", -- A_Space_Adventure:moon02 +["Use the rope to complete the obstacle course!"] = "使用绳索完成障碍课程", -- Basic_Training_-_Rope +["Use the rope to get on the head of the mole, young one!"] = "使用绳索登上鼹鼠的头,年轻人", -- A_Classic_Fairytale:first_blood +["Use the rope to get to the crate"] = "使用绳索接近箱子", -- A_Space_Adventure:cosmos +["Use the rope to get to the target!"] = "使用绳索接近目标", -- Basic_Training_-_Rope +["Use the rope to knock your enemies to their doom."] = "使用绳索把你的敌人撞下去", -- User_Mission_-_Rope_Knock_Challenge +["Use the rope to quickly get to the surface!"] = "使用绳索快速到达表面", -- A_Space_Adventure:desert02 +["Use the saucer and fly away"] = "使用飞碟飞走", -- A_Space_Adventure:cosmos +["Use the saucer and fly to the moon"] = "使用飞碟飞到月球", -- A_Space_Adventure:cosmos +["Use the shield to protect yourself from bazookas."] = "使用护盾保护自己不受火箭炮伤害", -- Space_Invasion +["Use the structure placer to place structures."] = "使用结构放置器放置结构", -- Construction_Mode +["Use your ammo wisely."] = "明智地使用你的弹药", -- A_Space_Adventure:desert01 +["Use your available weapons in order to eliminate the enemies."] = "使用可用的武器消灭敌人", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 +["Use your ready time to think."] = "使用你的准备时间来思考", -- Frenzy +["Use your rope to collect all crates as fast as possible."] = "使用绳索尽快收集所有箱子", -- SpeedShoppa +["Use your rope to get from start to finish as fast as you can!"] = "使用绳索尽快从起点到达终点", +["Use your rope to get to the next target, then destroy it!"] = "使用绳索接近下一个目标,然后破坏它", -- Basic_Training_-_Rope +["Utility Crate Placement Mode"] = "工具箱放置模式", -- Construction_Mode +["UTILITY CRATE PLACEMENT MODE"] = "工具箱放置模式", -- HedgeEditor +["Utility crates extend your time."] = "工具箱延长你的时间", -- Tumbler +["Variants: Hogs will be randomized from 12 different variants"] = "职业: 刺猬会随机变成12个不同的职业之一", -- Battalion +["Variants: Kings and air generals are disabled"] = "职业: 国王和空军将军被禁用", -- Battalion +["Variants: The last hog of each team will be a king"] = "职业: 每个队伍最后一个的刺猬变成国王", -- Battalion +["Vedgies"] = "Vedgies", -- A_Classic_Fairytale:journey +["Vega"] = "Vega", -- +["Vegan Jack"] = "Vegan Jack", -- A_Classic_Fairytale:enemy +["Very valuable, haha!"] = "非常值钱,哈哈", -- A_Classic_Fairytale:queen +["Victory!"] = "胜利", -- Basic_Training_-_Rope +["Victory Condition: Collect"] = "胜利条件: 收集", -- HedgeEditor +["Victory Condition: Destroy"] = "胜利条件: 破坏", -- HedgeEditor +["Victory for %s!"] = "%s胜利", -- Capture_the_Flag +["Violence is not the answer to your problems!"] = "暴力不是你的问题的答案", -- A_Classic_Fairytale:first_blood +["Visit the planets of Ice, Desert and Fruit before you proceed to the Death Planet"] = "在你继续死亡星球之前,参观冰冻、沙漠和水果星球", -- A_Space_Adventure:cosmos +["Vladimir"] = "Vladimir", -- +["Void"] = "Void", -- Big_Armory +["Voldemort"] = "Voldemort", -- portal +["Voltorb"] = "Voltorb", -- +["Wait a moment …"] = "等一下……", -- A_Space_Adventure:final +["Walking on Ice"] = "冰上行走", -- Basic_Training_-_Movement +["Walk: [Left] and [Right]"] = "行走: [左]/[右]", -- Basic_Training_-_Movement +["Walk: [Left]/[Right]"] = "行走: [左]/[右]", -- Basic_Training_-_Bazooka +["Wall Before Crate: You must touch the marked wall before you can get crates."] = "箱子之前的墙壁: 在你能得到箱子之前,你必须触碰标记的墙壁", -- WxW +["Walls Before Crate: You must touch the %d marked walls before you can get crates."] = "箱子之前的墙壁: 在你能得到箱子之前,你必须触碰%d标记的墙壁", -- WxW +["Wall set: No walls"] = "墙壁设置: 无", -- WxW +["Wall set: %s (%d walls)"] = "墙壁设置: %s(%d墙壁)", -- WxW +["Wall set: %s"] = "墙壁设置: %s", -- WxW +["Walls left: %d"] = "墙壁剩余: %d", -- WxW +["Wall to wall"] = "墙到墙", -- WxW +["Waluigi"] = "Waluigi", -- +["Wario"] = "Wario", -- +["Warming Up"] = "热身", -- Basic_Training_-_Grenade +["Warning: Fire cake detected"] = "警告: 检测到火蛋糕", -- ClimbHome +["Warning: Never ever leave the flying saucer while in water!"] = "警告: 在水里千万不要离开飞碟", -- Basic_Training_-_Flying_Saucer +["WARNING: Sabotage detected!"] = "警告: 检测到妨害", -- Continental_supplies +["Warrior"] = "战士", -- Battalion +[" was extracted from the scheme|- This continent will be able to use the specials from the other continents!"] = "- 是从方案中提取|- 这个大陆可以从其他大陆使用特别模式", -- Continental_supplies +["WatchBot 4000"] = "WatchBot 4000", -- User_Mission_-_Teamwork_2 +["Watch your steps, young one!"] = "小心脚下,年轻人", -- A_Classic_Fairytale:first_blood +["Watermelon Heart"] = "Watermelon Heart", -- A_Space_Adventure:fruit02 +["Water: Rises by 37 per turn"] = "水面: 每个回合上升37", -- Battalion +["Waypoint Editing Mode"] = "路径点编辑模式", -- HedgeEditor +["WAYPOINT EDITING MODE"] = "路径点编辑模式", -- HedgeEditor +["Waypoint placed. Available points remaining: %d"] = "路径点已放置,可用路径点还有: %d", -- Racer +["Waypoint placement phase"] = "路径点放置阶段", -- Racer +["Waypoint removed. Available points: %d"] = "路径点已移除,可用的路径点: %d", -- Racer +["Waypoints remaining: %d"] = "路径点还有: %d", -- Racer, TechRacer +["Weaklings"] = "Weaklings", -- A_Classic_Fairytale:shadow +["We all know what happens when you get frightened..."] = "我们都知道当你害怕时发生了什么", -- A_Classic_Fairytale:first_blood +["Weapon Crate Placement Mode"] = "武器箱放置模式", -- Construction_Mode +["WEAPON CRATE PLACEMENT MODE"] = "武器箱放置模式", -- HedgeEditor +["Weapon Filter"] = "武器过滤器", -- Construction_Mode +["Weapon Filter: Dematerializes all ammo| carried by enemies entering it."] = "武器过滤器: 进入的敌人的所有弹药消失", -- Construction_Mode +["weaponschemes"] = "武器方案", -- Continental_supplies +["Weapons: Each team starts with %d weapon points"] = "武器: 每个队伍有%d武器分数", -- Battalion +["Weapons: Hogs will get 1 out of 3 weapons randomly each turn"] = "武器: 每个回合刺猬会随机得到本职业三种之一的武器", -- Battalion +["Weapons: Nearly every hog variant gets 1 kamikaze"] = "武器: 几乎每个刺猬职业得到一个神风特攻队", -- Battalion +["Weapon specials: Some weapons have special modes (see weapon description)."] = "武器特别模式: 某些武器有特别模式(看武器描述)", -- Continental_supplies +["Weapons reset: The weapons are reset after each turn."] = "武器重置: 每个回合之后武器重置", -- WxW +["We are indeed."] = "我们确实是", -- A_Classic_Fairytale:backstab +["We can't defeat them!"] = "我们没办法打败他们", -- A_Classic_Fairytale:shadow +["We can't hold them up much longer!"] = "我不能拖住他们更久", -- A_Classic_Fairytale:united +["We can't let them take over our little island!"] = "我们不能让他们接管我们的小岛", -- A_Classic_Fairytale:enemy +["We come in peace! Just let our friends go!"] = "我们为和平而来,让我们的朋友走", -- A_Classic_Fairytale:queen +["We could just have blown up the meteorite from the the beginning!"] = "我们可以在开头直接炸了陨石!", -- A_Space_Adventure:final +["We don't have time for that now!"] = "我们现在没有时间找戒指", -- A_Classic_Fairytale:queen +["We have lost an object which was critical to this mission."] = "我们失去了这个任务中的一个重要物品", -- SimpleMission +["We have no time to waste..."] = "我们没有时间可以浪费……", -- A_Classic_Fairytale:journey +["We have nowhere else to live!"] = "我们没有其它地方可以生活", -- A_Classic_Fairytale:enemy +["We have spotted the enemy! We'll attack when the enemies start gathering!"] = "我们发现了敌人,我们会在敌人开始聚集的时候攻击", -- A_Space_Adventure:fruit02 +["We have to find our folk!"] = "我们必须找到我们的人", -- A_Classic_Fairytale:queen +["We have to hurry! Are you armed?"] = "我们必须赶快,你武装了吗?", -- A_Space_Adventure:moon01 +["We have to protect the village!"] = "我们必须保护村子", -- A_Classic_Fairytale:united +["We have to unite and defeat those cylergs!"] = "我们必须联合打败那些cylergs", -- A_Classic_Fairytale:enemy +["Welcome home! Please take a seat"] = "欢迎回家,请坐", -- ClimbHome +["Welcome, Leaks A Lot!"] = "欢迎,Leaks A Lot", -- A_Classic_Fairytale:journey +["Welcome, %s, surprised to see me?"] = "欢迎,%s,看到我很惊讶?", -- A_Space_Adventure:death01 +["Welcome to the Death Planet!"] = "欢迎来到死亡星球", -- A_Space_Adventure:cosmos +["Welcome to the Desert Planet!"] = "欢迎来到沙漠星球", -- A_Space_Adventure:cosmos +["Welcome to the Fruit Planet!"] = "欢迎来到水果星球", -- A_Space_Adventure:cosmos +["Welcome to the meteorite!"] = "欢迎来到陨石", -- A_Space_Adventure:cosmos +["Welcome to the moon!"] = "欢迎来到月球", -- A_Space_Adventure:cosmos +["Welcome to the Planet of Ice!"] = "欢迎来到冰雪星球", -- A_Space_Adventure:cosmos +["Well done."] = "做得好", +["Well done! Let's destroy the next target!"] = "做得好,让我们破坏下一个目标", -- Basic_Training_-_Rope +["Well done! The next target awaits."] = "做得好,下一个目标在等着", -- Basic_Training_-_Rope +["We'll give you a problem then!"] = "我们给你一个问题", -- A_Classic_Fairytale:enemy +["We'll play a game first."] = "我们先来玩一个游戏", -- A_Space_Adventure:moon02 +["We'll spare your life for now!"] = "我们现在饶你一命", -- A_Classic_Fairytale:backstab +["Well, that escalated quickly!"] = "好吧,越来越快了", -- ClimbHome +["Well that was an unnecessary act of violence."] = "好吧,那是一个没有必要的暴力行为", -- A_Classic_Fairytale:epil +["Well, that was an unnecessary act of violence."] = "好吧,那是一个没有必要的暴力行为", -- A_Classic_Fairytale:epil +["Well, that was a waste of time."] = "浪费时间", -- A_Classic_Fairytale:dragon +["We'll use our communicators to contact you."] = "我们会用我们的通讯器联系你", -- A_Space_Adventure:cosmos +["Well, well! Isn't that the cutest thing you've ever seen?"] = "这是不是你见过的最可爱的事情?", -- A_Classic_Fairytale:journey +["Well, yes. This was a cyborg television show."] = "是的,这是一个机器人电视节目", -- A_Classic_Fairytale:enemy +["Well, you're about to wake up!"] = "好,你就要醒了", -- A_Classic_Fairytale:queen +["We made sure noone followed us!"] = "我们确保没人跟着我们", -- A_Classic_Fairytale:backstab +["We need it to get split into at least two parts."] = "我们需要它分成至少两个部分", -- A_Space_Adventure:cosmos +["We need to go back!"] = "我们需要回去", -- A_Classic_Fairytale:queen +["We need to hurry!"] = "我们需要赶快", -- A_Classic_Fairytale:queen +["We need to move!"] = "我们需要转移", -- A_Classic_Fairytale:united +["We need to prevent their arrival!"] = "我们需要阻止他们到达", -- A_Classic_Fairytale:backstab +["We need to warn the village."] = "我们需要警告村子", -- A_Classic_Fairytale:shadow +["We need you to go there and detonate them yourself! Good luck!"] = "我们需要你去那里引爆它们,祝你好运", -- A_Space_Adventure:cosmos +["We oppressed her, the only woman in the tribe!"] = "我们压迫了她,部落里唯一的女人", -- A_Classic_Fairytale:epil +["We're terribly sorry!"] = "我们很抱歉", -- A_Classic_Fairytale:epil +["We risk our lives going through challenges."] = "我们冒着生命危险经历挑战", -- A_Classic_Fairytale:queen +["We should better report this and continue our watch!"] = "我们最好报告这个并继续监视", -- A_Space_Adventure:cosmos +["We should head back to the village now."] = "我们应该现在回去村子", -- A_Classic_Fairytale:shadow +["We, the youth, have to constantly prove our value."] = "我们,年轻人,必须不断地证明我们的价值", -- A_Classic_Fairytale:queen +["We trusted you, you fool!"] = "我们信任你,你个傻子", -- A_Classic_Fairytale:queen +["We were trying to save her and we got lost."] = "我们试着救她,失败了", -- A_Classic_Fairytale:family +["We were your home! Your family!"] = "我们是你的家庭,你的家人", -- A_Classic_Fairytale:queen +["We won't accept you destroying our village!"] = "我们不会接受你破坏我们的村子", -- A_Classic_Fairytale:queen +["We won't let you hurt any more of us!"] = "我们不会再让你伤害我们", -- A_Classic_Fairytale:queen +["We won't let you hurt her!"] = "我们不会让你伤害她", -- A_Classic_Fairytale:journey +["We work and work until we sweat blood."] = "我们工作和工作,直到我们流血", -- A_Classic_Fairytale:queen +["What?! A cannibal? Here? There is no time to waste! Come, you are prepared."] = "什么?!一个食人族?这里?没有时间可浪费,来,你准备好了", -- A_Classic_Fairytale:first_blood +["What?!"] = "什么?!", -- A_Classic_Fairytale:queen +["What a douche!"] = "真是个傻瓜", -- A_Classic_Fairytale:enemy +["What am I gonna...eat, yo?"] = "那我吃什么……", -- A_Classic_Fairytale:family +["What are you doing at a distance so great, young one?"] = "你在这么远的地方做什么,年轻人?", -- A_Classic_Fairytale:first_blood +["What are you doing? Let her go!"] = "你在做什么?让她走", -- A_Classic_Fairytale:journey +["What a ride!"] = "What a ride", -- A_Classic_Fairytale:shadow +["What a strange cave!"] = "多么奇怪的洞穴", -- A_Classic_Fairytale:dragon +["What a strange feeling!"] = "多么奇怪的感觉", -- A_Classic_Fairytale:backstab +["What could you possibly forget in that cage?"] = "你可能会忘记那个笼子里的什么?", -- A_Classic_Fairytale:queen +["What does it look like?"] = "它看起来像什么?", -- A_Classic_Fairytale:queen +["What do my faulty eyes observe? A spy!"] = "我的眼睛看到了什么?一个间谍!", -- A_Classic_Fairytale:first_blood +["What do you say? Are you in?"] = "如何?你在听吗?", -- A_Space_Adventure:fruit02 +["What do you say? Will you fight for us?"] = "如何?你会和我们战斗?", -- A_Space_Adventure:fruit01 +["What do you want to do?"] = "你想做什么?", -- A_Space_Adventure:fruit01 +["Whatever floats your boat..."] = "你爱做什么做什么……", -- A_Classic_Fairytale:shadow +["What?! For all this struggle I just win some ... time? Oh dear!"] = "什么?!我所有的奋斗只赢得一点……时间?", -- portal +["What has %s ever done to you?"] = "%s对你做了什么?", -- A_Classic_Fairytale:backstab +["What? Here? How did they find us?!"] = "什么?这里?他们怎么找到我们的?!", -- A_Classic_Fairytale:backstab +["What? Is it over already?"] = "什么?已经结束了?", -- ClimbHome +["What is it that you forgot?"] = "你忘记的是什么?", -- A_Classic_Fairytale:queen +["What is this place?"] = "这个地方是哪里?", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy +["What oppression? You were the most unoppressed member of the tribe!"] = "什么压迫?你是部落里最不受到压迫的人", -- A_Classic_Fairytale:queen +["What shall we do with the traitor?"] = "我们要对叛徒做什么?", -- A_Classic_Fairytale:backstab +["What's in the box, you ask? Let's find out!"] = "你问箱子里有什么?让我们看看", -- Basic_Training_-_Movement +["What the?"] = "What the?", -- A_Classic_Fairytale:queen +["WHAT?! You're the ones attacking us!"] = "什么?!你就是攻击我们的那个!", -- A_Classic_Fairytale:enemy +["When?"] = "什么时候?", -- A_Classic_Fairytale:enemy +["When I find it..."] = "当我找到它……", -- A_Classic_Fairytale:dragon +["When you're in mid-air, you can continue to aim|and fire another rope if you're not attached."] = "你在空中的时候,可以继续瞄准和发射绳索", -- Basic_Training_-_Rope +["Where are all these crates coming from?!"] = "这些箱子都是从哪来的?!", -- A_Classic_Fairytale:shadow +["Where are they?!"] = "他们在哪里?!", -- A_Classic_Fairytale:backstab +["Where did that alien run?"] = "外星人跑去哪里?", -- A_Classic_Fairytale:dragon +["Where did you get the exploding apples?"] = "你在哪里得到爆炸苹果?", -- A_Classic_Fairytale:shadow +["Where did you get the exploding apples and the magic bow that shoots many arrows?"] = "你在哪里得到爆炸苹果和发射许多箭的魔法弓?", -- A_Classic_Fairytale:shadow +["Where did you get the magic bow that shoots many arrows?"] = "你在哪里得到发射许多箭的魔法弓?", -- A_Classic_Fairytale:shadow +["Where did you get the weapons in the forest, Dense Cloud?"] = "你在森林的哪里得到武器,Dense Cloud?", -- A_Classic_Fairytale:backstab +["Where do you get that?!"] = "你在哪里得到那个?!", -- A_Classic_Fairytale:enemy +["Where have you been?!"] = "你去了哪里?!", -- A_Classic_Fairytale:backstab +["Where have you been?"] = "你去了哪里?", -- A_Classic_Fairytale:united +["While in modification mode, you can change|the LandFlag by clicking on an object."] = "在修改模式,你可以点击一个对象改变LandFlag", -- HedgeEditor +["White Tee"] = "White Tee", -- A_Space_Adventure:ice01 +["Who's there?! I'll get you!"] = "谁在那里?!我会抓到你", -- A_Space_Adventure:desert01 +["Why?"] = "为什么?", -- A_Classic_Fairytale:queen +["Why are you doing this?"] = "为什么你这么做?", -- A_Classic_Fairytale:journey +["Why are you helping us, uhm...?"] = "为什么你帮助我们,呃……", -- A_Classic_Fairytale:family +["Why can't he just let her go?!"] = "为什么他就不能让她走?!", -- A_Classic_Fairytale:family +["… why did I risk my life to collect all the parts of the anti-gravity device?"] = "……为什么我要冒着生命危险收集反重力设备的所有部件?", -- A_Space_Adventure:final +["Why did you do this?"] = "你为什么要这么做?", -- A_Classic_Fairytale:queen +["Why did you kill your father?"] = "为什么你杀了你的父亲?", -- A_Classic_Fairytale:queen +["Why do men keep hurting me?"] = "为什么男人一直伤害我?", -- A_Classic_Fairytale:first_blood +["Why do you always have to call me names?"] = "为什么你一直必须叫我名字?", -- A_Classic_Fairytale:queen +["Why do you keep betraying us?"] = "为什么你一直背叛我们?", -- A_Classic_Fairytale:queen +["Why do you not like me?"] = "为什么你不喜欢我?", -- A_Classic_Fairytale:shadow +["Why do you want to take over our island?"] = "为什么你想要接管我们的岛屿?", -- A_Classic_Fairytale:enemy +["Why me?!"] = "为什么是我?", -- A_Classic_Fairytale:backstab +["Why %s? Why?"] = "为什么,%s,为什么?", -- A_Classic_Fairytale:backstab +["Why, why, why, why!"] = "为什么,为什么!", -- A_Classic_Fairytale:queen +["Why would they do this?"] = "他们为什么这样做?", -- A_Classic_Fairytale:backstab +["- Will get 1-3 random weapons"] = "- 会得到1-3随机武器", -- Continental_supplies +["- Will Get 1-3 random weapons"] = "- 会得到1-3随机武器", -- Continental_supplies +["- Will give you a parachute every second turn."] = "- 每两个回合会给你一个降落伞", -- Continental_supplies +["Will this ever end?"] = "这一切会结束吗?", +["Will you give me the other parts?"] = "你会给我其它部件吗?", -- A_Space_Adventure:death01 +["Win"] = "赢", -- A_Space_Adventure:ice01 +["Wind"] = "风", -- Basic_Training_-_Bazooka +["Winner: %s"] = "胜者: %s", -- Mutant +["Winning time: %s"] = "获胜时间: %s", -- Racer, TechRacer +["Wise Oak"] = "Wise Oak", -- A_Classic_Fairytale:backstab, A_Classic_Fairytale:dragon, A_Classic_Fairytale:enemy, A_Classic_Fairytale:epil, A_Classic_Fairytale:family, A_Classic_Fairytale:queen +["With Dense Cloud on the land of shadows, I'm the village's only hope..."] = "Dense Cloud在阴影之地,我是村子的唯一希望……", -- A_Classic_Fairytale:journey +["With low bounciness, it barely bounces at all, but it is much more predictable."] = "低弹力几乎不跳,但更容易预测", -- Basic_Training_-_Grenade +["With the rest of the tribe gone, it was up to %s to save the village."] = "部落剩下的人走了,到%s上场拯救村子", -- A_Classic_Fairytale:dragon +["Worry not, for it is a peaceful animal! There is no reason to be afraid..."] = "别担心,这是一个和平的动物,没有理由害怕……", -- A_Classic_Fairytale:first_blood +["Wow, what a dream!"] = "哇,一个梦!", -- A_Classic_Fairytale:backstab +["Xeli"] = "Xeli", -- +["Xerxes"] = "Xerxes", -- +["Y3K1337"] = "Y3K1337", -- A_Classic_Fairytale:journey, A_Classic_Fairytale:shadow +["Yay, we won!"] = "耶,我们赢了", -- A_Classic_Fairytale:enemy +["Y Chwiliad"] = "Y Chwiliad", -- A_Classic_Fairytale:dragon +["Yeah...I think it's a 'he', lol."] = "耶……我想这是一个“他”,笑", -- A_Classic_Fairytale:shadow +["Yeah, sure! I died. Hilarious!"] = "耶,当然,我死了,非常有趣", -- A_Classic_Fairytale:backstab +["Yeah, sure! I died. Hillarious!"] = "耶,当然,我死了,非常有趣", -- A_Classic_Fairytale:backstab +["Yeah, take that!"] = "耶,拿着那个", -- A_Classic_Fairytale:dragon +["Yeah? Watcha gonna do? Cry?"] = "你要做什么?哭吗?", -- A_Classic_Fairytale:journey +["Yeah, well, for some dude to be happy, some other dude has to suffer."] = "很好,一些人开心,一些人受苦", -- A_Classic_Fairytale:queen +["Yellow"] = "Yellow", -- +["Yellow Pepper"] = "Yellow Pepper", -- A_Space_Adventure:fruit01 +["Yellow Watermelons"] = "Yellow Watermelons", -- A_Space_Adventure:fruit01 +["Yes!"] = "是的!", -- A_Classic_Fairytale:enemy +["Yes, but you're … different!"] = "是的,但你是……不同的!", -- A_Classic_Fairytale:queen +["Yes, yeees! You are now ready to enter the real world!"] = "Yes,你现在准备好进入真实世界了", -- A_Classic_Fairytale:first_blood +["Yeti"] = "Yeti", -- +["Yikes!"] = "Yikes", -- A_Space_Adventure:desert01 +["Yo, dude! Get away from our weapons!"] = "老兄,离开我们的武器", -- A_Classic_Fairytale:queen +["Yo, dude, we're here, too!"] = "老兄,我们也在这里", -- A_Classic_Fairytale:family +["Yo, escort my buttocks!"] = "Yo,护送我的臀部", -- A_Classic_Fairytale:queen +["Yoshi"] = "Yoshi", -- +["Yo, the aliens gave me plants. Medicinal plants. Lots of it."] = "外星人给了我植物,药草,很多", -- A_Classic_Fairytale:queen +["You are far from home, and the water is rising, climb up as high as you can!|Your score will be based on your height."] = "你离家很远,水面在上升,尽可能爬到高处|你的分数基于你的高度", -- ClimbHome +["You are given the chance to turn your life around..."] = "你有机会改变自己的生活……", -- A_Classic_Fairytale:shadow +["You are in control of all the active ally units."] = "你能控制所有激活的盟友单位", -- A_Space_Adventure:fruit01 +["You are indeed the best PAotH pilot."] = "你是最好的星球协会飞行员", -- A_Space_Adventure:desert03 +["You are out of danger, time to go to the moon!"] = "你安全了,是时候去月球", -- A_Space_Adventure:cosmos +["You are playing with our lives here!"] = "你在玩弄我们的生命", -- A_Classic_Fairytale:enemy +["You are sabotaged, RUN!"] = "你被妨害了,快跑", -- Continental_supplies +["You are the one who fled! So, you are alive."] = "你是逃跑的那个,所以,你活着", -- A_Space_Adventure:fruit02 +["You bear impressive skills, %s!"] = "你拥有令人印象深刻的技能,%s", -- A_Classic_Fairytale:dragon +["You can also hold down the key for “Precise Aim” to prevent slipping."] = "你可以一直按着“精确瞄准”来阻止滑动", -- Basic_Training_-_Movement +["You can always trust me!"] = "你可以一直信任我", -- A_Classic_Fairytale:epil +["You can always trust me! I love you!"] = "你可以一直信任我,我爱你", -- A_Classic_Fairytale:epil +["You can avoid some battles."] = "你可以避开一些战斗", -- A_Space_Adventure:desert01 +["You can change the detonation timer of grenades."] = "你可以改变手榴弹的引爆定时器", -- Basic_Training_-_Grenade +["You can choose another planet by replaying this mission."] = "你可以重新游玩这个任务,选择另一个星球", -- A_Space_Adventure:cosmos +["You can dive with your flying saucer!"] = "你可以用飞碟潜水", -- Basic_Training_-_Flying_Saucer +["You can even change your aiming direction in mid-flight if you first hold [Precice] and then press [Up] or [Down]."] = "你甚至可以在飞行中改变瞄准方向,长按[精确]再按[上]或[下]", -- Basic_Training_-_Flying_Saucer +["You can even change your aiming direction in mid-flight if you first hold [Precise] and then press [Up] or [Down]."] = "你甚至可以在飞行中改变瞄准方向,长按[精确]再按[上]或[下]", -- Basic_Training_-_Flying_Saucer +["You can further customize the race by changing the scheme script paramater."] = "你可以改变方案脚本参数,进一步自定义竞赛", -- TechRacer +["You can further customize the race by changing the scheme script parameter."] = "你可以改变方案脚本参数,进一步自定义竞赛", -- TechRacer +["You can only use the sniper rifle or the watermelon bomb."] = "你只能用狙击枪或西瓜炸弹", -- A_Space_Adventure:fruit03 +["You can practice moving around and using utilities in this mission.|However, it will never end!"] = "你可以练习到处移动,使用这个任务的工具|无论如何,它永远不会结束", -- A_Classic_Fairytale:epil +["You can set the bounciness of grenades (and grenade-like weapons)."] = "你可以设置手榴弹的弹性(和类似手榴弹的武器)", -- Basic_Training_-_Grenade +["- You can switch between hogs at the start of your turns. (Not first one)"] = " 你可以在你的回合开始切换刺猬(不是第一个)", -- Continental_supplies +["You can’t open a portal on the blue surface."] = "你不能在蓝色表层打开传送门", -- portal +["You can use the other 2 hogs to assist you."] = "你可以使用其他两个刺猬帮助你", -- A_Space_Adventure:fruit02 +["You can use the rope to reach new places."] = "你可以使用绳索到达新位置", -- Basic_Training_-_Rope +["You choose well, %s!"] = "你选得好,%s", -- A_Space_Adventure:fruit01 +["You completed the mission in %.3f seconds."] = "你用%.3f秒完成任务", -- A_Space_Adventure:ice02 +["You completed the mission in %d rounds."] = "你用%d回合完成任务", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 +["You couldn't have come to a worse time, %s!"] = "你不可能来过更糟糕的时刻,%s", -- A_Space_Adventure:fruit01 +["You couldn't possibly believe that after refusing my offer I'd just let you go!"] = "你不可能相信拒绝我的提议之后我会让你走", -- A_Classic_Fairytale:journey +["You'd almost swear the water was rising!"] = "我不会游泳,你呢……", +["You'd better watch your steps..."] = "你最好小心脚下……", -- A_Classic_Fairytale:journey +["You defended yourself against Captain Lime."] = "你保卫自己对抗Captain Lime", -- A_Space_Adventure:fruit02 +["You defended yourself against %s."] = "你保卫自己对抗%s", -- A_Space_Adventure:fruit02 +["You did great, %s! However, we aren't out of danger yet!"] = "你做得很好,%s,然而,我们还没有脱离危险", -- A_Space_Adventure:cosmos +["You did not make it in time, try again!"] = "你没有在时间内完成,再试一次", -- Basic_Training_-_Rope +["You don't deserve my sacrifice!"] = "你不值得我牺牲", -- A_Classic_Fairytale:queen +["You drove Professor Hogevil away."] = "你赶走了Hogevil教授", -- A_Space_Adventure:moon01 +["You drove the minions away."] = "你赶走了手下", -- A_Space_Adventure:moon01 +["You earned the \"Rope Master\" achievement for finishing in under 50 seconds."] = "在50秒内完成,你获得“绳索大师”成就", -- Basic_Training_-_Rope +["You endangered your whole tribe, you bastard!"] = "你让整个部落快要灭绝,你这个混蛋", -- A_Classic_Fairytale:queen +["You failed!"] = "你失败了", -- Basic_Training_-_Rope +["You failed to kill all enemies in a single turn."] = "你没能在一个回合杀死所有敌人", -- Big_Armory +["You failed to kill all enemies in this turn."] = "你没能在这个回合杀死所有敌人", -- Big_Armory +["You fought bravely and you helped us win this battle!"] = "你勇敢地帮助我们赢得战斗", -- A_Space_Adventure:fruit02 +["You give me no choice!"] = "你让我没的选择", -- A_Classic_Fairytale:queen +["You got a killer mask there, amigo!"] = "你在那里得到一个杀手面具,amigo!", -- A_Classic_Fairytale:epil +["You got me!"] = "你抓到我了", -- A_Space_Adventure:moon02 +["You had %.1fs remaining on the clock (+%d points)."] = "你剩下%.1f秒(+%d分)", -- TargetPractice +["You had %.2fs remaining on the clock (+%d points)."] = "你剩下%.2f秒(+%d分)", -- Basic_Training_-_Sniper_Rifle +["You have 7 turns until the next wave arrives.|Make sure the arriving cannibals are greeted appropriately!|If the hog dies, the cause is lost.|Hint: you might want to use some mines..."] = "你有7回合直到下一波到达|确保到达的食人族受到适当的迎接|提示: 你可能要用一些地雷……", -- A_Classic_Fairytale:backstab +["You have 7 turns until the next wave arrives.|Make sure the arriving cannibals are greeted appropriately!|If the hog dies, the cause is lost.|Hint: You might want to use some mines ..."] = "你有7回合直到下一波到达|确保到达的食人族受到适当的迎接|提示: 你可能要用一些地雷……", -- A_Classic_Fairytale:backstab +["You have acquired the last device part."] = "你得到了最后的设备部件", -- A_Space_Adventure:death01 +["You have activated Switch Hedgehog!"] = "你激活了切换刺猬", -- Basic_Training_-_Movement +["You have beaten the challenge!"] = "你打败了挑战", -- ClimbHome +["You have beaten the team record, congratulations!"] = "你打破了队伍记录,恭喜", -- Utils +["You have been giving us out to the enemy, haven't you!"] = "你把我们送给敌人,你没有吗?", -- A_Classic_Fairytale:backstab +["You have chosen the perfect moment to leave."] = "你选择了完美时刻离开", -- A_Classic_Fairytale:united +["You have chosen to fight!"] = "你选择战斗", -- A_Space_Adventure:fruit01 +["You have chosen to flee."] = "你选择逃跑", -- A_Space_Adventure:fruit01 +["You have collected %d out of %d crate(s)."] = "你收集了%d箱子(总数%d)", -- SpeedShoppa +["You have collected the “Switch Hedgehog” utility!"] = "你选择了“切换刺猬”工具", -- Basic_Training_-_Movement +["You have completed the Basic Bazooka Training!"] = "你完成了基础火箭炮训练", -- Basic_Training_-_Bazooka +["You have completed the Basic Grenade Training!"] = "你完成了基础手榴弹训练", -- Basic_Training_-_Grenade +["You have completed the Basic Movement Training!"] = "你完成了基础移动训练", -- Basic_Training_-_Movement +["You have completed this challenge in %.2f s (+%d points)."] = "你用%.2f秒完成了这个挑战(+%d分)", -- User_Mission_-_Rope_Knock_Challenge +["You have destroyed all targets!"] = "你破坏了所有目标", -- TargetPractice +["You have destroyed all the targets."] = "你破坏了所有目标", -- A_Space_Adventure:desert03 +["You have destroyed %d of %d targets."] = "你破坏了%d目标(总数%d)", -- Basic_Training_-_Bazooka +["You have destroyed %d of %d targets (+%d points)."] = "你破坏了%d目标(总数%d)(+%d分)", -- Basic_Training_-_Sniper_Rifle, TargetPractice +["You have dropped %d missiles."] = "你丢下了%d导弹", -- User_Mission_-_RCPlane_Challenge +["You have eliminated all visible enemy hedgehogs!"] = "你消灭了所有看得见的敌人", -- A_Space_Adventure:fruit01 +["You have eliminated Professor Hogevil."] = "你消灭了Hogevil教授", -- A_Space_Adventure:moon01 +["You have eliminated the evil minions."] = "你消灭了邪恶的手下", -- A_Space_Adventure:moon01 +["You have escaped successfully."] = "你成功地逃离", -- A_Space_Adventure:desert02 +["You have failed to complete your task, young one!"] = "你没能完成你的任务,年轻人", -- A_Classic_Fairytale:journey +["You have failed to save the tribe!"] = "你没能拯救部落", -- A_Classic_Fairytale:backstab +["You have finally figured it out!"] = "你终于弄清楚了", -- A_Classic_Fairytale:enemy +["You have finished the Basic Rope Training!"] = "你完成了基础绳索训练", -- Basic_Training_-_Rope +["You have finished the bazooka training!"] = "你完成了火箭炮训练", -- Basic_Training_-_Bazooka +["You have finished the challenge in %.3f s."] = "你用%.3f秒完成挑战", -- SpeedShoppa +["You have finished the challenge!"] = "你完成挑战", -- User_Mission_-_RCPlane_Challenge +["You have finished the Flying Saucer Training!"] = "你完成了飞碟训练", -- Basic_Training_-_Flying_Saucer +["You have finished the target practice!"] = "你完成了目标训练", -- TargetPractice +["You have kidnapped our whole tribe!"] = "你绑架了我们的整个部落", -- A_Classic_Fairytale:enemy +["You have killed all enemies."] = "你杀了所有敌人", -- Big_Armory +["You have killed an innocent hedgehog!"] = "你杀死了一个无辜的刺猬", -- A_Classic_Fairytale:backstab +["You have killed %d of 16 hedgehogs (+%d points)."] = "你杀死了%d刺猬(总数16)(+%d分)", -- User_Mission_-_Rope_Knock_Challenge +["You have killed %d of %d hedgehogs (+%d points)."] = "你杀死了%d刺猬(总数%d)(+%d分)", -- RopeKnocking +["You have launched %d bazookas."] = "你发射了%d火箭炮", -- Target_Practice_-_Bazooka_easy, Target_Practice_-_Bazooka_hard, Basic_Training_-_Bazooka +["You have launched %d homing bees."] = "你发射了%d蜜蜂", -- Target_Practice_-_Homing_Bee +["You have made %d shots."] = "你射击了%d次", -- Basic_Training_-_Sniper_Rifle +["You have managed to catch the blue hedgehog in %.3f seconds."] = "你用%.3f秒抓住了蓝色刺猬", -- A_Space_Adventure:moon02 +["You have never worked a bit in your life!"] = "在你的生命里从来没有做过一点工作", -- A_Classic_Fairytale:queen +["You have nothing to be afraid of now."] = "你现在不用担心了", -- A_Classic_Fairytale:epil +["You haven't rescued anyone."] = "你还没有解救任何人", -- User_Mission_-_That_Sinking_Feeling +["You have perfectly beaten the challenge!"] = "你完美地打败了挑战", -- User_Mission_-_RCPlane_Challenge +["You have proven yourself worthy to see our most ancient secret!"] = "你证明了自己值得看我们最古老的秘密", -- A_Classic_Fairytale:first_blood +["You have proven yourselves worthy!"] = "你证明了自己有价值", -- A_Classic_Fairytale:enemy +["You have reached the take-off area successfully!"] = "你成功地到达起飞区域", -- A_Space_Adventure:fruit01 +["You have rescued H and Dr. Cornelius."] = "你解救了H和Dr. Cornelius", -- A_Space_Adventure:death01 +["You have SCORED!!"] = "你得分了", +["You have shot %d times."] = "你射击了%d次", -- TargetPractice +["You have successfully eliminated Professor Hogevil."] = "你成功地消灭了Hogevil教授", -- A_Space_Adventure:death01 +["You have successfully finished the campaign!"] = "你成功地完成了战役", -- A_Classic_Fairytale:epil +["You have successfully finished the sniper rifle training!"] = "你成功地完成了狙击枪训练", -- Basic_Training_-_Sniper_Rifle +["You have thrown %d cluster bombs."] = "你投掷了%d集束炸弹", -- Target_Practice_-_Cluster_Bomb +["You have thrown %d grenades."] = "你投掷了%d手榴弹", -- Target_Practice_-_Grenade_easy, Target_Practice_-_Grenade_hard +["You have to be careful and must not die!"] = "你必须小心而且不能死", -- A_Space_Adventure:cosmos +["You have to catch the other hog 3 times."] = "你必须抓到其他刺猬三次", -- A_Space_Adventure:moon02 +["You have to complete the main mission on moon in order to travel to other planets."] = "你必须完成月球上的主要任务才能旅行到其他星球", -- A_Space_Adventure:cosmos +["You have to continue alone from now on."] = "你现在必须一个人继续", -- A_Space_Adventure:cosmos +["You have to destroy all the explosives without dying!"] = "你必须破坏所有爆炸物,不能死掉", -- A_Space_Adventure:final +["You have to destroy all the targets."] = "你必须破坏所有目标", -- A_Space_Adventure:desert03 +["You have to destroy the target above by dropping a grenade on it from your flying saucer."] = "你必须从你的飞碟丢手榴弹破坏上面的目标", -- Basic_Training_-_Flying_Saucer +["You have to destroy two targets, but the previous technique would be very difficult or dangerous to use."] = "你必须破坏两个目标,但手榴弹炸不到上面", -- Basic_Training_-_Flying_Saucer +["You have to drop the grenade from rope!"] = "你必须从绳索丢手榴弹", -- Basic_Training_-_Rope +["You have to eliminate all the enemies."] = "你必须消灭所有敌人", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 +["You have to eliminate all the visible enemies."] = "你必须消灭所有看得见的敌人", -- A_Space_Adventure:fruit01 +["You have to get the weapons and rescue the PAotH researchers."] = "你必须得到武器并解救星球协会研究员", -- A_Space_Adventure:moon01 +["You have to get to the left-most land and remove any enemy hog from there."] = "你必须到达最左边的地面,干掉那里的敌人", -- A_Space_Adventure:fruit01 +["You have to go back to the moon!"] = "你必须回到月球", -- A_Space_Adventure:cosmos +["You have to move upwards, not downwards, %s!"] = "你必须上去,不是下去,%s", -- ClimbHome +["You have to reach the left-most place on the map."] = "你必须到达地图上最左边的位置", -- A_Space_Adventure:fruit01 +["You have to stand very close to him"] = "你必须站在他旁边", -- A_Space_Adventure:moon02 +["You have to travel again"] = "你必须再次旅行", -- A_Space_Adventure:cosmos +["You have to try again!"] = "你必须再试一次", -- A_Space_Adventure:cosmos +["You have triggered the secret Do-Not-Rope-to-the-Moon Defense System."] = "你触发了秘密的“不要用绳索去月球”防御系统", -- A_Space_Adventure:cosmos +["You have unlocked the target radar!"] = "你解锁了目标雷达", -- TargetPractice +["You have used %d flying saucers."] = "你使用了%d飞碟", -- A_Space_Adventure:ice02 +["You have used %d RC planes."] = "你使用了%d遥控飞机", -- User_Mission_-_RCPlane_Challenge +["You have used only 1 RC plane. Outstanding!"] = "你只用了一个遥控飞机,优秀!", -- User_Mission_-_RCPlane_Challenge +["You have violated PAotH regulations!"] = "你违反了星球协会条例", -- A_Space_Adventure:cosmos +["You have won the game by proving true cooperative skills!"] = "你证明真正的合作技巧,赢了游戏", -- A_Classic_Fairytale:enemy +["You just appeared out of thin air!"] = "你在空中出现", -- A_Classic_Fairytale:backstab +["You just can't let it go, can you!"] = "你就不能让它走吗", -- A_Classic_Fairytale:queen +["You just committed suicide..."] = "你自杀了", -- A_Classic_Fairytale:shadow +["You just got yourself some extra health.|The more health your hedgehogs have, the better!"] = "你得到了一点额外血量|你的刺猬的血量越多越好", -- Basic_Training_-_Movement +["You killed my father, you monster!"] = "你杀了我的父亲,你这个怪物", -- A_Classic_Fairytale:backstab +["You know...taking a stroll."] = "你知道……散一下步", -- A_Classic_Fairytale:backstab +["You know what? I don't even regret anything!"] = "你知道什么?我什么都不后悔", -- A_Classic_Fairytale:backstab +["You'll get an extra sniper rifle every time you kill an enemy hog with a limit of max 4 rifles."] = "你以最多4狙击枪的限制杀死敌人会得到一个额外的狙击枪", -- A_Space_Adventure:fruit03 +["You'll get an extra teleport every time you kill an enemy hog with a limit of max 2 teleports."] = "你以最多2传送的限制杀死敌人会得到一个额外的传送", -- A_Space_Adventure:fruit03 +["You'll get extra time in case you need it when you pass a ring."] = "当你通过一个环的时候会得到额外的时间", -- A_Space_Adventure:ice02 +["You'll have only 2 watermelon bombs during the game."] = "你在游戏中只有两个西瓜炸弹", -- A_Space_Adventure:fruit03 +["You'll have only one RC plane at the start of the mission."] = "你在任务开始只有一个遥控飞机", -- A_Space_Adventure:desert03 +["You'll have to eliminate Captain Lime at the end."] = "你必须在最后消灭Captain Lime", -- A_Space_Adventure:fruit02 +["You'll have to eliminate %s at the end."] = "你必须在最后消灭%s", -- A_Space_Adventure:fruit02 +["You'll lose if you die or if your time is up."] = "你死了或时间到就会输", -- A_Space_Adventure:moon02 +["You'll see what I mean!"] = "你会明白我的意思", -- A_Classic_Fairytale:enemy +["You lost your target, try again!"] = "你失去了你的目标,再试一次", -- TargetPractice +["You may find it handy."] = "你会发现它很好用", -- A_Space_Adventure:cosmos +["You may only attack from a rope!"] = "你只能从绳索攻击", -- WxW +["You may only place 1 Extra Time crate per turn."] = "你每个回合只能放置一个额外时间箱子", -- Construction_Mode +["You may only place %d crates per round."] = "你每个回合只能放置%d箱子", -- Construction_Mode +["- You may only score when your flag is in your base"] = "- 只有当你的光环在你的基地时得分", -- Capture_the_Flag +["You meatbags are pretty slow, you know!"] = "你这个肉包太慢了,你知道吗!", -- A_Classic_Fairytale:enemy +["You might want to find a way to instantly kill arriving cannibals!"] = "你可能需要找一个办法立即杀死到达的食人族", -- A_Classic_Fairytale:backstab +["You must attack from a rope, after you collected a crate!"] = "你必须在收集一个箱子之后从绳索攻击", -- WxW +["You must first collect a crate before you attack!"] = "你必须在你攻击前收集一个箱子", -- WxW +["You must survive the flood in order to score."] = "你必须在洪水中活下来才能得分", -- User_Mission_-_That_Sinking_Feeling +["You never give me plants!"] = "你从来没有给我植物", -- A_Classic_Fairytale:queen +["Young one, you are telling us that they can instantly change location without a shaman?"] = "年轻人,你告诉我们,他们不需要萨满就能立即改变位置?", -- A_Classic_Fairytale:united +["You now have infinite fuel, grenades and bazookas for fun."] = "你现在有无限的燃料、手榴弹、火箭炮来玩", -- Basic_Training_-_Flying_Saucer +["You only get 1 rope this time, don't waste it!"] = "你这次只有一个绳索,不要浪费", -- Basic_Training_-_Rope +["You only have 2 flying saucers this time."] = "你这次只有两个飞碟", -- Basic_Training_-_Flying_Saucer +["You only have one flying saucer this time."] = "你这次只有一个飞碟", -- Basic_Training_-_Flying_Saucer +["You probably know what to do next..."] = "你可能知道接下来要做什么……", -- A_Classic_Fairytale:first_blood +["Your accuracy was %.1f%%."] = "你的准确度是%.1f%%", -- Basic_Training_-_Bazooka, TargetPractice +["Your accuracy was %.1f%% (+%d points)."] = "你的准确度是%.1f%%(+%d分)", -- TargetPractice +["Your ammo is limited this time."] = "这次你的弹药有限", -- Basic_Training_-_Bazooka +["Your deaths will be avenged, %s!"] = "你的死会被复仇,%s", -- A_Classic_Fairytale:enemy +["Your death will not be in vain, Dense Cloud!"] = "你的死不会是徒劳的,Dense Cloud", -- A_Classic_Fairytale:shadow +["You're a coward!"] = "你是个懦夫", -- A_Classic_Fairytale:queen +["You're...alive!? But we saw you die!"] = "你……还活着?!可是我看到你死了", -- A_Classic_Fairytale:backstab +["You're a pathetic liar!"] = "你是个可怜的骗子", -- A_Classic_Fairytale:backstab +["You're funny!"] = "你好有趣", -- A_Classic_Fairytale:journey +["You're getting pretty good! |Tip: When you shorten you rope, you move faster!|And when you lengthen it, you move slower."] = "你做得很好|提示: 绳索短,移动快,绳索长,移动慢", -- Basic_Training_-_Rope +["You're on your way to freeing your tribe!"] = "你在用你的方式解放你的部落", -- A_Classic_Fairytale:queen +["You're pathetic! You are not worthy of my attention..."] = "你真可怜,你不值得我注意", -- A_Classic_Fairytale:shadow +["You're probably wondering why I bought you back..."] = "你可能想知道为什么我带你回来……", -- A_Classic_Fairytale:backstab +["You're probably wondering why I brought you back ..."] = "你可能想知道为什么我带你回来……", -- A_Classic_Fairytale:backstab +["Your escape took you %d turns."] = "你的逃离用了你%d回合", -- A_Space_Adventure:desert02 +["You're so brave! I feel safe with you."] = "你真勇敢,我感觉和你一起很安全", -- A_Classic_Fairytale:epil +["You're some piece of hypocrite junkie!"] = "你是某些伪君子垃圾的碎片", -- A_Classic_Fairytale:queen +["You're terrorizing the forest...We won't catch anything like this!"] = "你在威胁森林……我们不会抓到像这样的东西", -- A_Classic_Fairytale:shadow +["You retrieved the lost part."] = "你找回了遗失的部件", -- A_Space_Adventure:fruit02 +["Your fastest escape so far: %d turns"] = "你至今最快的逃离: %d回合", -- A_Space_Adventure:desert02 +["Your fastest victory so far: %d rounds"] = "你至今最快的胜利: %d回合", -- A_Space_Adventure:death02, A_Space_Adventure:fruit03 +["Your first destination is the moon in order to get more fuel."] = "你在月球的第一个目标是得到更多燃料", -- A_Space_Adventure:cosmos +["Your hedgehog died!"] = "你的刺猬死了", -- User_Mission_-_That_Sinking_Feeling +["Your hedgehog has been revived!"] = "你的刺猬被复活了", -- Basic_Training_-_Bazooka, Basic_Training_-_Grenade, Basic_Training_-_Movement, Basic_Training_-_Rope +["Your hedgehog was panicly afraid of the water and decided to go in a safe distance of %d from it."] = "你的刺猬怕水并爬到安全距离%d", -- ClimbHome +["Your height over time"] = "高度图表", -- ClimbHome +["Your hogs must survive!"] = "你的刺猬必须活下来", -- A_Classic_Fairytale:journey +["Your movement skills will be evaluated now."] = "现在要评价你的移动技能", -- A_Classic_Fairytale:first_blood +["Your next task is to collect some crates by using the rope!"] = "你的下一个任务是用绳索收集一些箱子", -- A_Classic_Fairytale:first_blood +["Your personal best time so far: %.3f seconds"] = "你至今最佳时间: %.3f秒", -- A_Space_Adventure:ice02, A_Space_Adventure:moon02 +["Your rank: %s"] = "你的等级: %s", -- User_Mission_-_RCPlane_Challenge +["Your rope is gone! Try again!"] = "你的绳索没了,再试一次", -- Basic_Training_-_Rope +["You saved %d of 8 hegehogs."] = "你救了%d/8刺猬", -- User_Mission_-_That_Sinking_Feeling +["You see, hedgehog spikes are very, very valuable."] = "你看,刺猬spikes非常有价值", -- A_Classic_Fairytale:queen +["You see the wind strength at the bottom right corner."] = "你可以在右下角看到风力", -- Basic_Training_-_Bazooka +["You see the wind strength at the top."] = "你可以在顶部看到风力", -- Basic_Training_-_Bazooka +["You should have known that we don't rely on meatbags!"] = "你应该知道我们不会依赖肉包", -- A_Classic_Fairytale:queen +["You should know this more than anyone, Leaks!"] = "你应该比任何人都了解,Leaks", -- A_Classic_Fairytale:queen +["You speak great truth, Hannibal. Here, take a sip!"] = "你说了大实话,Hannibal,来,尝一口", -- A_Classic_Fairytale:epil +["You've been assaulting us, we have been just defending ourselves!"] = "你袭击我们,我们只是保护自己", -- A_Classic_Fairytale:enemy +["You've reached the goal!| |Time: "] = "你已经达到目标| |时间: ", +["You will be avenged!"] = "你会被复仇的", -- A_Classic_Fairytale:shadow +["You will fail if you run out of ammo and there are still targets available."] = "如果这里还有目标,而你没有弹药,你会失败", -- A_Space_Adventure:desert03 +["You will gain some extra ammo from the crates the next time you play the \"Getting to the device\" mission."] = "下一次你玩“取得设备”任务会从箱子得到一些额外弹药", -- A_Space_Adventure:fruit03 +["You will play every 3 turns."] = "你会游玩每3回合", -- A_Space_Adventure:fruit01 +["- You will recieve 2-4 weapons on each kill! (Even on own hogs)"] = "- 你每次杀死会收到2-4武器(即使是自己的刺猬)", -- Continental_supplies +["You won't believe what happened to me!"] = "你不会相信在我身上发生了什么事", -- A_Classic_Fairytale:backstab +["Yuck! I bet they'll keep worshipping her even after I save the village!"] = "啊,我打赌即使我拯救了村子,他们会一直崇拜她", -- A_Classic_Fairytale:family +["Yumme Gunpowder"] = "Yumme Gunpowder", -- +["Zealandia"] = "Zealandia", -- Continental_supplies +["Zombie"] = "Zombie", -- +["Zombi"] = "Zombi", -- portal +["'Zooka Team"] = "'Zooka Team", +["Zoom: [Pinch] with 2 fingers"] = "缩放: 两个手指捏", -- Basic_Training_-_Movement +["Zoom: [Rotate mouse wheel]"] = "缩放: 鼠标滚轮", -- Basic_Training_-_Movement +["Zork"] = "Zork", -- A_Classic_Fairytale:dragon, A_Classic_Fairytale:family, A_Classic_Fairytale:queen } diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/zh_CN.txt --- a/share/hedgewars/Data/Locale/zh_CN.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/zh_CN.txt Sun Mar 24 14:33:57 2024 -0400 @@ -1,71 +1,464 @@ -; Simplified Chinese locale +; Chinese locale +; Weapon names 00:00=手榴弹 -00:01=子母炸弹 +00:01=集束炸弹 00:02=火箭炮 00:03=蜜蜂枪 -00:04=散弹枪 -00:05=气锤 -00:06=掠过 +00:04=霰弹枪 +00:05=电锤 +00:06=跳过 00:07=绳索 00:08=地雷 00:09=沙漠之鹰 00:10=炸药 -00:11=球棒 +00:11=棒球棒 00:12=升龙拳 +; Short for “second” (unit of time) 00:13=秒 00:14=降落伞 00:15=空袭 -00:16=地雷袭击 -00:17=喷灯 -00:18=钢板 +00:16=地雷空袭 +00:17=焊枪 +00:18=建造 00:19=传送 00:20=切换 00:21=迫击炮 00:22=鞭子 -00:23=神风特工队 +00:23=神风特攻队 00:24=蛋糕 -00:25=吸引 +00:25=诱惑 00:26=西瓜炸弹 -00:27=地狱礼花 +00:27=地狱手雷 00:28=钻地火箭 -00:29=弹珠枪 -00:30=燃烧弹 +00:29=滚珠枪 +00:30=凝固汽油弹 00:31=遥控飞机 00:32=低重力 -00:33=附加伤害 -00:34=刀枪不入 -00:35=加时 +00:33=额外伤害 +00:34=无敌 +00:35=额外时间 00:36=激光瞄准 00:37=吸血 00:38=狙击枪 00:39=飞碟 00:40=燃烧瓶 -00:41=鸟人 -00:42=传送门枪 +00:41=鸟儿 +00:42=便携传送设备 00:43=钢琴空袭 -00:44=老芝士 +00:44=旧林堡干酪 00:45=正弦枪 00:46=火焰喷射器 00:47=黏性地雷 00:48=锤子 -00:49=复活 -00:50=钻地火箭空袭 +00:49=复活器 +00:50=钻地空袭 00:51=泥球 -00:52=没有选择武器 +00:52=未选择武器 00:53=时光箱 -00:54=土地喷雾 -00:55=急冻枪 +00:54=地面喷雾 +00:55=冷冻枪 00:56=菜刀 00:57=橡皮筋 00:58=浮空雷 00:59=爬行者 00:60=机枪 +00:61=哨兵机器人 -01:00=载入中 … -01:01=平手 -01:02= %1 胜! +; Game messages and HUD texts +01:00=加载中 … +01:01=平局 +01:02=%1 胜利! 01:03=音量 %1% -01:04=暂停 -01:05=退出 (%1/%2)? -01:06=出现紧急情况! +01:04=已暂停 +01:05=退出吗? (%1/%2) +01:06=突然死亡! +01:07=%1 剩余 +01:08=燃料: %1% +01:09=同步中 ... +01:10=这不会结束你的回合! +01:11=这个武器或工具还不可用! +01:12=最后回合,即将进入突然死亡! +01:13=还有 %1 回合进入突然死亡! +01:14=准备好, %1! +; Bounciness adjectives +01:15=轻微 +01:16=较低 +01:17=正常 +01:18=较高 +01:19=极端 +01:20=%1 弹力 +01:21=已静音 +01:22=已启用自动跳过 +01:23=自动镜头 关 +01:24=自动镜头 开 +01:25=按下目标按钮以标记目标 +01:26=工具在突然死亡中不可用 +; E.g. “+25” when gaining health from crate or vampirism +01:27=+%1 +01:28=空的! +01:29=未知按键 +01:30=%1 和 %2 胜利! +01:31=%1, %2 和 %3 胜利! +01:32=%1, %2, %3 和 %4 胜利! +01:33=%1, %2, %3, %4 和 %5 胜利! +01:34=%1, %2, %3, %4, %5 和 %6 胜利! +01:35=%1, %2, %3, %4, %5, %6 和 %7 胜利! +01:36=所有人胜利! +01:37=%1 走了. +01:38=%1 回来了. +01:39=%1 自动跳过回合. +01:40=%1 fps +01:41=Lua 解析: 关 +01:42=Lua 解析: 开 +01:43=在线游戏不允许 Lua 解析! +; Ammo count in ammo menu +01:44=%1× +; Chat. %1 = player, %2 = message +01:45=%1: %2 +; Clan chat. %1 = player, %2 = message +01:46=[战队] %1: %2 +; Hedgehog chat. %1 = hog name, %2 = message +01:47=[%1]: %2 +; Symbol for unknown mine timer +01:48=? +01:49=使用/lua 命令后不能录制视频. + +; 根据翻译页面的描述,02开头的消息不需要全部翻译 +; Event messages +; Normal hog (%1) died (0 health) +02:00=%1 离开了这个世界 +02:00=%1 去玩其它游戏了 +02:00=可怜的 %1 +02:00=%1 死了 +02:00=%1 牺牲了 +02:00=%1 失败了 +02:00=%1 在装死 +02:00=%1 在等待复活 +02:00=再见, %1! +02:00=%1 不再感觉到痛 + +; Normal hog (%1) drowned +02:01=%1 模仿泰坦尼克号 +02:01=%1 溺水了 +02:01=%1 咕噜咕噜 +02:01=%1 溅起水花 +02:01=%1 忘了带救生衣 +02:01=%1 忘了带泳圈 +02:01=%1 看起来很渴 +02:01=%1 湿透了 +02:01=%1 不会游泳 +02:01=%1 让水面上升了一点点 + +; Round starts +02:02=游戏开始 +02:02=我们开始吧 +02:02=欢迎来到刺猬战争 +02:02=赢或死 +02:02=祝你好运并玩得开心 + +; Round ends and a team (%1) wins +02:03=%1 赢了! +02:03=%1 是冠军! +02:03=%1 是胜者! +02:03=%1 获胜 +02:03=恭喜 %1! + +; Round ends in a draw +02:04=平局 +02:04=平局! 我们要再来一次… +02:04=平局. 大家勇敢地战斗到最后 +02:04=喂! 谁是胜利者?! +02:04=大家都死了. 很好! + +; New health crate +02:05=救援物资来了! +02:05=这会让你感觉好点 +02:05=一个治疗药水! 哦,搞错游戏了 +02:05=紧急快递 +02:05=治疗疾病! +02:05=这会很有帮助 +02:05=用这个箱子治疗自己! +02:05=治疗你的伤口 +02:05=活得更久一点 +02:05=拿着这个药 + +; New ammo crate +02:06=更多武器! +02:06=里面有什么? +02:06=一个礼物! +02:06=特别快递! +02:06=别让敌人拿到它 +02:06=新的玩具 +02:06=谁会第一个拿到它? +02:06=进攻是最好的防御 +02:06=谁捡到就是谁的! +02:06=收集或炸了它? + +; New utility crate +02:07=工具时间! +02:07=实用工具! +02:07=这个看起来很有用 +02:07=工程师的礼物 +02:07=有人订购了工具箱? + +; Hog (%1) skips their turn +02:08=%1 很无聊... +02:08=%1 休息一下 +02:08=%1 在发呆 +02:08=%1 在思考人生 +02:08=%1 享受安静 + +; Hog (%1) hurts themselves only +02:09=%1 应该练习瞄准! +02:09=%1 离自杀还差一步 +02:09=%1 完全是故意的 +02:09=%1 的武器显然故障了 +02:09=%1 是他最大的敌人 + +; Home run: Hog (%1) uses baseball bat to throw other hog far out of the left/right map bounds +02:10=本垒打! +02:10=谁说刺猬不能飞? +02:10=噢! 那一定很疼 +02:10=漂亮的一击! +02:10=那可能打破记录! + +; Hog (%1) has to leave (team is gone) +02:11=%1 要去睡觉了! +02:11=%1 太忙不能玩 +02:11=%1 要做作业 +02:11=%1 接到了重要的电话 +02:11=%1 去拿外卖了 + +; Hog (%1) was poisoned +02:12=%1 感觉不舒服 +02:12=%1 应该买一个防毒面具 +02:12=%1 需要去医院 +02:12=%1 要呆在家不能上学 +02:12=%1 应该捡一个医疗箱 + +; Hog (%1) was resurrected by the Resurrector utility +02:13=%1 复活了 +02:13=%1 又活过来了 +02:13=%1 和亡灵法师做了交易 +02:13=%1 准备好再死一次 +02:13=%1 投了一个币 + +; Hog (%1) explodes after an kamikaze attack +02:14=永远记住 %1 +02:14=%1 执行战术自我毁灭 +02:14=%1 使用了一次性武器 +02:14=%1 无所畏惧 +02:14=%1 没有什么可失去的 + +; Hog (%1) returned from time-travel with the time box +; These texts are intentionally kept simple and clear to not confuse the player +02:15=%1 从时间旅行返回 +02:15=时间旅行者 %1 回来了 +02:15=%1 走出了时光箱子 +02:15=%1 回到了我们的时间线 +02:15=欢迎回到我们的时间, %1! + +; Hog (%1) runs out of turn time (not shown in infinite attack mode) +02:16=%1 太慢了 +02:16=%1 忘了看时间 +02:16=%1 在浪费时间 +02:16=%1 需要更多时间 +02:16=%1 不知道有时间限制 + +; King (%1) has died +02:17=%1 的王冠被夺走 +02:17=%1 像真正的国王一样死去 +02:17=%1 死了,王国也死了 +02:17=%1 有太多敌人 +02:17=%1 被将军了 + +; 重新分类 +; Weapon categories/subcaptions +03:00=投掷武器 +03:01=投掷武器 +03:02=弹道武器 +03:03=制导武器 +03:04=枪 +03:05=挖掘工具 +03:06=行动 +03:07=运输工具 +03:08=近距离炸弹 +03:09=枪 +03:10=近距离炸弹 +03:11=近战 +03:12=武术 +03:13=未使用 +03:14=运输工具 +03:15=空袭 +03:16=空袭 +03:17=挖掘工具 +03:18=工具 +03:19=运输工具 +03:20=行动 +03:21=弹道武器 +03:22=近战 +03:23=(真正的) 武术 +03:24=远程控制炸弹 +03:25=行动 +03:26=投掷武器 +03:27=投掷武器 +03:28=弹道武器 +03:29=弹道武器 +03:30=空袭 +03:31=远程控制炸弹 +03:32=短暂的效果 +03:33=短暂的效果 +03:34=短暂的效果 +03:35=短暂的效果 +03:36=短暂的效果 +03:37=短暂的效果 +03:38=枪 +03:39=运输工具 +03:40=投掷武器 +03:41=运输工具 +03:42=运输工具 +; the misspelled "Beethoven" is intentional (-> to beat) +03:43=空袭 +03:44=投掷武器 +03:45=枪 +03:46=枪 +03:47=投掷武器 +03:48=近战 +03:49=工具 +03:50=空袭 +03:51=投掷武器 +03:52=UNUSED +03:53=工具 +03:54=工具 +03:55=枪 +03:56=投掷武器 +03:57=工具 +03:58=飘浮近距离炸弹 +03:59=未完成的武器 +03:60=枪 +03:61=未完成的武器 + +; 简短描述 +; Weapon descriptions (use | as line breaks) +04:00=定时器归零就会爆炸|1-5: 设置定时器|精确 + 1-5: 设置弹力|攻击: 长按蓄力攻击 +04:01=爆裂成更小的炸弹|1-5: 设置定时器|精确 + 1-5: 设置弹力|攻击: 长按蓄力攻击 +04:02=受风力影响|攻击: 长按蓄力攻击 +04:03=蜜蜂会飞向目标|不全力射击可提高精度|光标: 选择目标|攻击: 长按蓄力攻击 +04:04=两发子弹|攻击: 射击 +04:05=钻入地下|攻击: 开始或停止|左/右: 钻地时移动 +04:06=跳过这个回合|攻击: 跳过回合 +04:07=使用绳索快速行动|丢下炸弹或撞向刺猬|可在空中多次使用绳索|攻击: 发射或放开绳索|上/下/左/右: 伸长缩短和摇晃|远跳: 丢下炸弹 +04:08=放下并撤退|攻击: 放下地雷|精确 + 1-5: 设置弹力 +04:09=四发子弹|攻击: 射击 +04:10=放下并撤退|攻击: 放下炸药 +04:11=把刺猬打下水|或打飞地雷|攻击: 挥动球棒 +04:12=靠近刺猬给他一拳|跳起来打到高处|攻击: 动手 +04:13=未使用 +04:14=安全着陆|自动打开一次|刮风时请注意风向|攻击: 下降时收起降落伞|上/下/左/右: 调整方向|远跳: 丢下炸弹 +04:15=呼叫飞机轰炸敌人|左/右: 调整方向|光标: 选择目标位置 +04:16=呼叫飞机丢下地雷|左/右: 调整方向|光标: 选择目标位置 +04:17=用焊枪挖洞|或攻击刺猬|攻击: 点火|上/下: 调整方向 +04:18=用大梁造一条路或掩体|左/右: 调整方向|光标: 放置在有效位置 +04:19=传送到有效的指定位置|左/右: 调整方向|光标: 选择目标位置 +04:20=允许你在当前回合换人|攻击: 确认切换|切换: 选择下一个|精确 + 切换: 选择上一个 +04:21=撞击时会向后方投掷集束炸弹|攻击: 全力发射 +04:22=鞭打刺猬或物体|攻击: 挥动鞭子 +04:23=牺牲自己,冲向指定方向|攻击: 冲锋 +04:24=生日快乐! |蛋糕会走向刺猬给他们惊喜|攻击: 放下蛋糕,控制爆炸 +04:25=吸引刺猬跳向自己|可以解冻刺猬|攻击: 不分敌友的诱惑 +04:26=爆裂成更多炸弹|1-5: 设置定时器|攻击: 长按蓄力攻击 +04:27=爆炸并燃烧|攻击: 长按蓄力攻击 +04:28=钻到表面或一段时间后爆炸|攻击: 长按蓄力攻击 +04:29=发射大量会爆炸的滚珠|攻击: 全力发射|上/下: 调整方向 +04:30=呼叫飞机焚烧敌人|左/右: 调整方向|光标: 选择目标位置 +04:31=收集箱子,丢下炸弹,冲向刺猬|攻击: 启动飞机,丢炸弹|远跳: 音乐|左/右: 调整方向 +04:32=跳得更远,或让刺猬飞得更远|攻击: 激活 +04:33=造成更多伤害|攻击: 激活 +04:34=不受伤害|攻击: 激活 +04:35=加30秒|攻击: 激活 +04:36=打得更准|攻击: 激活 +04:37=造成伤害的80%|攻击: 激活 +04:38=两发子弹|距离越远,伤害越高|激活后停在原地|攻击: 激活,射击|左/右: 调整方向(发射后) +04:39=起飞前带好武器|攻击: 激活/取消激活|上/左/右: 飞一下|远跳: 丢下炸弹|精确 + 远跳: 向准星方向开火|精确 + 上/下: 调整瞄准 +04:40=瓶子装着会燃烧的液体|攻击: 长按蓄力攻击 +04:41=鸟儿会带你飞|丢下有毒的蛋|攻击: 激活,丢蛋|上/左/右: 飞一下 +04:42=传送自己、刺猬、武器|对橡皮筋不起作用|攻击: 发射一个传送门|切换: 改变传送门颜色 +04:43=使用者会牺牲|光标: 选择目标位置|F1-F9: 弹钢琴 +04:44=这个奶酪很臭,会让刺猬中毒|1-5: 设置定时器|精确 + 1-5: 设置弹力|攻击: 长按蓄力攻击 +04:45=后座力很强|攻击: 射击 +04:46=喷射火焰|攻击: 激活|上/下: 调整方向|左/右: 调整力度 +04:47=两个黏性地雷|攻击: 长按蓄力攻击 +04:48=把刺猬锤进地下|攻击: 挥动锤子 +04:49=用你的血量救活朋友|注意不要复活敌人|攻击: 长按攻击缓慢复活|上: 加速复活 +04:50=把地下的刺猬炸出来|左/右: 调整方向|1-5: 设置定时器|光标: 选择目标位置 +04:51=没有伤害,但会击退刺猬和物品|攻击: 长按蓄力攻击 +04:52=未使用 +04:53=时空旅行,随时会回来|突然死亡、仅剩一人、国王不可用|攻击: 激活 +04:54=掩埋刺猬,堵住隧道|攻击: 激活/取消激活|上/下: 调整方向|左/右: 调整力度 +04:55=冰冻刺猬|让地面变滑或水面结冰|攻击: 激活/取消激活|上/下: 调整方向 +04:56=速度越快,伤害越高|攻击: 长按蓄力攻击 +04:57=弹走大部分东西|取消坠落伤害|左/右: 调整方向|光标: 放置在有效位置 +04:58=跟随靠近的刺猬并爆炸|比地雷弱|攻击: 长按蓄力攻击 +04:59=这个武器还没完成|攻击: 部署 +04:60=发射大量子弹|攻击: 全力发射|上/下: 调整方向 +04:61=这个武器还没完成|攻击: 部署机器人 + +; Game goal strings +05:00=游戏模式 +05:01=以下规则适用: +05:02=放置国王: 为你的国王选一个受保护的开始位置 +05:03=低重力: 小心脚下! +05:04=无敌: 刺猬 (几乎) 无敌 +05:05=吸血: 刺猬获得造成伤害的 80% 治疗效果 +05:06=报应: 刺猬会受到造成的伤害 +05:07=保护国王: 别让你的国王死! +05:08=放置刺猬: 游戏开始前放置你的刺猬 +05:09=炮兵: 刺猬不能走路 +05:10=坚不可摧的地形: 多数武器不能破坏地形 +05:11=共享武器: 相同颜色的队伍共享武器 +05:12=地雷定时器: 地雷 %1 秒引爆 +05:13=地雷定时器: 地雷立即引爆 +05:14=地雷定时器: 地雷 0 - 5 秒引爆 +05:15=伤害修改器: (几乎) 所有武器造成 %1% 伤害 +05:16=救护人员: 刺猬在回合结束时恢复初始血量 +05:17=AI 重生: AI 刺猬死后重生 +05:18=无限攻击: 攻击不会结束回合 +05:19=固定的武器: 武器在回合结束时重置 +05:20=每个刺猬的武器: 刺猬间不共享武器 +05:21=标签队伍: 战队中的队伍连续行动|共享时间: 战队中的队伍共享回合时间 +05:22=大风: 风力影响几乎所有东西 + +; Chat command help +06:00=客户端聊天命令列表 +06:01=/togglechat: 切换聊天显示 +06:02=/clan : 发送消息给战队成员 +06:03=/me : 聊天动作,如 “/me eats pizza” 变成 “* Player eats pizza” +06:04=/pause: 切换暂停 +06:05=/pause: 切换自动跳过 +06:06=/fullscreen: 切换全屏 +06:07=/quit: 退出游戏 +06:08=/help: 客户端聊天命令列表 +06:09=/help taunts: 嘲讽聊天命令列表 +06:10=/history: 切换更长的聊天记录显示 +06:11=/lua: 切换 Lua 解析 (for developers) +06:12=嘲讽聊天命令列表 +06:13="text": 文本放在说话气泡 +06:14='text': 文本放在显示文本 +06:15=-text-: 文本放在显示文本 +06:16=上面的命令,可以加数字在开头以选择刺猬,如-2我在这里- +06:17=/hsa : 下次攻击时文本放在说话气泡 +06:18=/hta : 下次攻击时文本放在思考气泡 +06:19=/hya : 下次攻击时文本放在叫喊气泡 +06:20=/hurrah: 使刺猬笑 +06:21=/ilovelotsoflemonade: 使刺猬尿尿 +06:22=/juggle: 抛球杂技 +06:23=/rollup: 使刺猬卷起 +06:24=/shrug: 使刺猬耸肩 +06:25=/wave: 使刺猬挥手 +06:26=未知命令或无效参数,在聊天中说“/help”获得命令列表 +06:27=/help room: 房间聊天命令列表 +06:28=你不在线! +06:29=/bubble: 使刺猬屏住呼吸 +06:30=/happy: 使刺猬看起来开心 +06:31=/sad: 使刺猬看起来伤心 diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Locale/zh_TW.txt --- a/share/hedgewars/Data/Locale/zh_TW.txt Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Locale/zh_TW.txt Sun Mar 24 14:33:57 2024 -0400 @@ -341,7 +341,7 @@ 02:07=好重...好重... 02:07=會有用的 -; Hog (%1) skips his turn +; Hog (%1) skips their turn 02:08=%1 太無聊了... 02:08=%1 不想被打擾! 02:08=%1 太懶了 @@ -379,7 +379,7 @@ 02:08=%1 怕刺激 02:08=%1 睡著了 -; Hog (%1) hurts himself only +; Hog (%1) hurts themselves only 02:09=%1 該練練瞄準了! 02:09=%1 似乎看自己很不爽。 02:09=%1 在表演烏龍! @@ -544,7 +544,7 @@ 04:50=有人躲在地下嗎?|用鑽地火箭空襲將它挖出來!|定時器控制挖掘的深度.|左/右方向鍵: 決定攻擊方向|1-5: 設定定時器|游標: 選定目標 04:51=使用泥球攻擊不會結束回合.|沒有攻擊力,但可以擊退刺蝟與物體.|攻擊鍵: 按住蓄力 04:52=UNUSED -04:53=離開隊友,進行一趟時空之旅.|準備隨時回歸,或強制回歸由於進入意外死亡模式,或只剩下一隻刺蝟.|注意,不能在以下狀況使用: 意外死亡模式,剩下一隻刺蝟時,與國王身上.|攻擊鍵: 激活 +04:53=離開隊友,進行一趟時空之旅.|準備隨時回歸,或強制回歸由於進入意外死亡模式,或只剩下一隻刺蝟.|注意,不能在以下狀況使用\: 意外死亡模式,剩下一隻刺蝟時,與國王身上.|攻擊鍵: 激活 04:54=噴灑一串神奇薄片來產生地形.|可用來: 搭建橋樑,埋葬敵人,封鎖隧道.|攻擊鍵: 激活|上/下方向鍵: 發射中移動準心|左/右方向鍵: 更改發射力道 04:55=回到冰河時期!|可冷凍刺蝟,使地面變滑|或凍結水面避免淹死.|攻擊鍵: 激活/停止冷凍射線|上/下方向鍵: 發射中移動準心 04:56=你可以丟兩把菜刀用來攻擊/擋住敵人|也可以用來擋住隧道或利用它來攀爬!|請小心!玩刀是很危險的.|攻擊鍵: 按住蓄力(兩次) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Maps/ClimbHome/map.lua --- a/share/hedgewars/Data/Maps/ClimbHome/map.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Maps/ClimbHome/map.lua Sun Mar 24 14:33:57 2024 -0400 @@ -11,7 +11,6 @@ local RecordHeight = 33000 local RecordHeightHogName = nil local Fire = {} ---local BoomFire = nil local HH = {} local totalHedgehogs = 0 local deadHedgehogs = 0 @@ -91,10 +90,9 @@ MineDudPercent = 0 EnableGameFlags(gfOneClanMode) DisableGameFlags(gfBottomBorder+gfBorder) - --This reduced startup time by only about 15% and looked ugly - --EnableGameFlags(gfDisableLandObjects) - -- force seed instead. Some themes will still be easier, but at least you won't luck out on the same theme - Seed = ClimbHome + -- gfDisableLandObjects is not used. This reduced startup time by only about 15% and looked ugly + -- Force seed so the land objects are the same. Some themes will still be easier, but at least you won't luck out on the same theme + Seed = "" -- Disable Sudden Death WaterRise = 0 HealthDecrease = 0 @@ -125,7 +123,6 @@ end function onGameStart() - --SetClanColor(ClansCount-1, 0x0000ffff) appears to be broken SendHealthStatsOff() local recordInfo = "" if isSinglePlayer then @@ -139,7 +136,6 @@ local x = 1818 for h,i in pairs(HH) do if h ~= nil then - -- SetGearPosition(h,x,32549) SetGearPosition(h,x,108) SetHealth(h,1) if x < 1978 then x = x+32 else x = 1818 end @@ -151,7 +147,7 @@ SetState(h,bor(GetState(h),gstInvisible)) end end --- 1925,263 - Mr. Mine position + -- 1925,263 - Mr. Mine position MrMine = AddGear(1925,263,gtMine,0,0,0,0) for i=0, TeamsCount-1 do SetTeamLabel(GetTeamName(i), "0") @@ -225,17 +221,10 @@ CakeTries = 0 end ---function onGearDelete(gear) --- if gear == WaterRise and MaxHeight > 500 and CurrentHedgehog ~= nil and band(GetState(CurrentHedgehog),gstHHDriven) ~= 0 then --- WaterRise = AddGear(0,0,gtWaterUp, 0, 0, 0, 0) --- end ---end - function FireBoom(x,y,d) -- going to add for rockets too PlaySound(sndExplosion) AddVisualGear(x,y,vgtExplosion,0,false) -- should approximate circle by removing corners - --if BoomFire == nil then BoomFire = {} end for i = 0,50 do fx = GetRandom(d)-div(d,2) fy = GetRandom(d)-div(d,2) @@ -253,7 +242,6 @@ SetTag(flame, 999999+i) SetFlightTime(flame, 0) Fire[flame]=1 --- BoomFire[flame]=1 end end @@ -267,13 +255,6 @@ dummySkip = 0 end - --if BoomFire ~= nil then - -- for f,i in pairs(BoomFire) do - -- if band(GetState(f),gstCollision~=0) then DeleteGear(f) end - -- end - -- BoomFire = nil - --end - for s,i in pairs(Stars) do local _, Y = GetVisualGearValues(s) if Y ~= nil and Y > WaterLine + 500 then @@ -333,7 +314,7 @@ AddCaption(loc("Don't touch the flames!")) CakeFireWarning = true end - FireBoom(cx,cy,200) -- todo animate + FireBoom(cx,cy,200) -- TODO: animate DeleteGear(Cake) end end @@ -372,7 +353,6 @@ 999999999, -- frameticks sprStar, -- star 0, c) - --, 0xFFCC00FF) -- could be fun to make colour shift as you rise... Stars[s] = 1 end end @@ -693,7 +673,6 @@ if teamBests[teamName] < actualHeight then teamBests[teamName] = actualHeight end if teamScoreStats[teamName] == nil then teamScoreStats[teamName] = {} end table.insert(teamScoreStats[teamName], actualHeight) - --SendStat(siClanHealth, tostring(teamBests[teamName]), teamName) end function makeMultiPlayerWinnerStat(gear) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/first_blood.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/first_blood.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Campaign/A_Classic_Fairytale/first_blood.lua Sun Mar 24 14:33:57 2024 -0400 @@ -487,7 +487,7 @@ end local x = GetX(youngh) local y = GetY(youngh) - return x < 3005 and y > 1500 and StoppedGear(youngh) + return x > 2575 and x < 3016 and y > 1538 and StoppedGear(youngh) end function CheckOnOrPastMoleHead() diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.hwp Binary file share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.hwp has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/cosmos.lua Sun Mar 24 14:33:57 2024 -0400 @@ -117,11 +117,7 @@ HealthDecrease = 0 -- completed main missions status = getCompletedStatus() - if status.death01 then - Map = "cosmos2_map" - else - Map = "cosmos_map" -- custom map included in file - end + Map = "cosmos_map" -- custom map included in file Theme = "Nature" -- Hero teamC.name = AddMissionTeam(teamC.color) @@ -187,6 +183,15 @@ end function onGameStart() + -- Place meteorite on map + if status.final then + -- Campaign complete: Blown-up meteorite sprite + PlaceSprite(3171, 909, sprCustom2, 0, nil, false, false, false) + elseif status.death01 then + -- death01 mission complete: Normal meteorite sprite + PlaceSprite(3171, 909, sprCustom1, 0, nil, false, false, false) + end + -- wait for the first turn to start AnimWait(hero.gear, 3000) @@ -626,8 +631,8 @@ end end if status.final then - vgear = AddVisualGear(3070, 810, vgtBeeTrace, 0, false) - vgear = AddVisualGear(3070, 790, vgtBeeTrace, 0, false) + vgear = AddVisualGear(3080, 810, vgtBeeTrace, 0, false) + vgear = AddVisualGear(3080, 790, vgtBeeTrace, 0, false) end end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death02.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death02.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/death02.lua Sun Mar 24 14:33:57 2024 -0400 @@ -123,12 +123,14 @@ function onGearDelete(gear) if isHog(gear) then - -- Set health to 100 (with heal effect, if health was smaller) - local healthDiff = 100 - GetHealth(hero.gear) - if healthDiff > 1 then - HealHog(hero.gear, healthDiff, true, 0x00FF00FF) - else - SetHealth(hero.gear, 100) + if CurrentHedgehog == hero.gear then + -- Set health to 100 (with heal effect, if health was smaller) + local healthDiff = 100 - GetHealth(hero.gear) + if healthDiff > 1 then + HealHog(hero.gear, healthDiff, true, 0x00FF00FF) + else + SetHealth(hero.gear, 100) + end end local deadHog = getHog(gear) if deadHog.weapon == amMortar then @@ -155,7 +157,7 @@ end function onGearDamage(gear, damage) - if isHog(gear) and GetHealth(hero.gear) then + if isHog(gear) and GetHealth(hero.gear) and CurrentHedgehog == hero.gear then local bonusHealth = div(damage, 3) HealHog(hero.gear, bonusHealth, true, 0xFF0000FF) end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon01.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon01.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon01.lua Sun Mar 24 14:33:57 2024 -0400 @@ -20,6 +20,7 @@ local checkPointReached = 1 -- 1 is start of the game local afterDialog02 = false local gameOver = false +local minionsDead = false -- dialogs local dialog01 = {} local dialog02 = {} @@ -262,6 +263,9 @@ EndTurn(true) end end + if minionsDead and (not (professor.dead or GetHealth(professor.gear) == nil or GetHealth(professor.gear) == 0)) then + FollowGear(professor.gear) + end end function onPrecise() @@ -444,9 +448,11 @@ end function minionsDeath(gear) + minionsDead = true if professor.dead or GetHealth(professor.gear) == nil or GetHealth(professor.gear) == 0 then return end if gameOver then return end if (not IsHogAlive(hero.gear)) or (not StoppedGear(hero.gear)) then return end + SetTeamPassive(teamC.name, false) AddAnim(dialog05) end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon02.lua --- a/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon02.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Campaign/A_Space_Adventure/moon02.lua Sun Mar 24 14:33:57 2024 -0400 @@ -130,6 +130,9 @@ runnerTime = runnerTime + runner.places[currentPosition].turnTime SetTeamLabel(teamB.name, string.format(loc("%.1fs"), runnerTime/1000)) else + if currentPosition > 2 then + AddCaption(loc("Go, get him again!"), capcolDefault, capgrpGameState) + end SetWeapon(amRope) SetTurnTimeLeft(runner.places[currentPosition].turnTime + previousTimeLeft) previousTimeLeft = 0 @@ -268,9 +271,6 @@ function moveRunner() if currentPosition == 4 then currentPosition = currentPosition + 1 - if GetX(hero.gear) > GetX(runner.gear) then - HogTurnLeft(runner.gear, false) - end AddAnim(dialog02) -- Update time record @@ -295,7 +295,6 @@ AddAmmo(hero.gear, amRope, 1) if currentPosition ~= 1 then if currentPosition > 1 and currentPosition < 4 then - AnimCaption(hero.gear, loc("Go, get him again!"), 3000) AnimSay(runner.gear, loc("You got me!"), SAY_SAY, 3000) end runnerCaught = true @@ -306,6 +305,9 @@ SetGearPosition(runner.gear, runner.places[currentPosition].x, runner.places[currentPosition].y) EndTurn(true) end + if runner.gear and hero.gear then + HogTurnLeft(runner.gear, GetX(hero.gear) < GetX(runner.gear)) + end end function lose() @@ -341,6 +343,7 @@ end function win() + AnimSetInputMask(0) SendStat(siGameResult, loc("Congratulations, you are the fastest!")) -- siCustomAchievements were added earlier SendStat(siPointType, "!TIME") diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Challenge/ClimbHome.lua --- a/share/hedgewars/Data/Missions/Challenge/ClimbHome.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Challenge/ClimbHome.lua Sun Mar 24 14:33:57 2024 -0400 @@ -4,7 +4,6 @@ -- trying to allow random theme, but fixed theme objects... -- Also skip some ugly themes, or ones where the sky is "meh" ---local themes = { "Art","Cake","City","EarthRise","Halloween","Olympics","Underwater","Bamboo","Castle","Compost","Eyes","Hell","Planes","Bath","Cave","CrazyMission","Freeway","Island","Sheep","Blox","Cheese","Deepspace","Fruit","Jungle","Snow","Brick","Christmas","Desert","Golf","Nature","Stage" } local themes = {"Christmas","Hell","Bamboo","City","Island","Bath","Compost","Jungle","Desert","Nature","Olympics","Brick","EarthRise","Sheep","Cake","Freeway","Snow","Castle","Fruit","Stage","Cave","Golf","Cheese","Halloween"} local totalHedgehogs = 0 local HH = {} @@ -13,9 +12,9 @@ function onGameInit() + Theme = themes[GetRandom(#themes)+1] -- Ensure people get same map for same theme - Theme = themes[GetRandom(#themes)+1] - Seed = ClimbHome + Seed = "" TurnTime = MAX_TURN_TIME EnableGameFlags(gfOneClanMode) DisableGameFlags(gfBottomBorder+gfBorder) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Challenge/User_Mission_-_Rope_Knock_Challenge.lua --- a/share/hedgewars/Data/Missions/Challenge/User_Mission_-_Rope_Knock_Challenge.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Challenge/User_Mission_-_Rope_Knock_Challenge.lua Sun Mar 24 14:33:57 2024 -0400 @@ -1,348 +1,133 @@ -HedgewarsScriptLoad("/Scripts/Utils.lua") HedgewarsScriptLoad("/Scripts/Locale.lua") +HedgewarsScriptLoad("/Scripts/RopeKnocking.lua") -local hhs = {} -local missionWon = nil -local missionEndHandled = false -local endTimer = 1000 -local hogsKilled = 0 -local finishTime -local ouchies = false -local valkyriesTimer = -1 +-- In this mission, the names of the enemy hogs are chosen randomly from this list. +-- As a nod to the community, this list contains names of actual users/players; +-- Mostly developers, contributors, high-ranking players in a shoppa tournament, +-- highly active forum users. -local HogData = { - {"amn", "NinjaFull",false}, - {"alfadur", "NoHat",false}, - {"Anachron", "war_americanww2helmet",false}, - {"Bufon", "ShaggyYeti",false}, - {"burp", "lambda",false}, - {"Blue", "cap_blue",false}, - {"bender", "NoHat",false}, - {"Castell", "NoHat",false}, - {"cekoto", "NoHat",false}, - {"CheezeMonkey", "NoHat",false}, - {"claymore", "NoHat",false}, - {"CIA-144", "cyborg1",false}, - {"cri.the.grinch", "sf_blanka",false}, - {"eldiablo", "Evil",false}, - {"Displacer", "fr_lemon",false}, - {"doomy", "NoHat",false}, - {"Falkenauge", "NoHat",false}, - {"FadeOne", "NoHat",false}, - {"hayaa", "NoHat",false}, - {"Hermes", "laurel",false}, - {"Henek", "WizardHat",false}, - {"HedgeKing", "NoHat",false}, - {"Izack1535", "NoHat",false}, - {"Kiofspa", "NoHat",false}, - {"KoBeWi", "NoHat",false}, - {"Komplex", "NoHat",false}, - {"koda", "poke_mudkip",false}, - {"Lalo", "NoHat",false}, - {"Logan", "NoHat",false}, - {"lollkiller", "NoHat",false}, - {"Luelle", "NoHat",false}, - {"mikade", "Skull",false}, - {"Mushi", "sm_daisy",false}, - {"Naboo", "NoHat",false}, - {"nemo", "bb_bub",false}, - {"practice", "NoHat",false}, - {"Prof. Panic", "NoHat",false}, - {"Randy", "zoo_Sheep",false}, - {"rhino", "NinjaTriangle",false}, - {"Radissthor", "NoHat",false}, - {"Sami", "sm_peach",false}, - {"soreau", "NoHat",false}, - {"Solar", "pinksunhat",false}, - {"sparkle", "NoHat",false}, - {"szczur", "mp3",false}, - {"sdw195", "NoHat",false}, - {"sphrix", "TeamTopHat",false}, - {"sheepluva", "zoo_Sheep",false}, - {"Smaxx", "NoHat",false}, - {"shadowzero", "NoHat",false}, - {"Star and Moon", "SparkleSuperFun",false}, - {"The 24", "NoHat",false}, - {"TLD", "NoHat",false}, - {"Tiyuri", "sf_ryu",false}, - {"unC0Rr", "cyborg1",false}, - {"Waldsau", "cyborg1",false}, - {"wolfmarc", "knight",false}, - {"Wuzzy", "fr_orange",false}, - {"Xeli", "android",false} +-- NOTE: These names are intentionally not translated. +local hogData = { + {"amn", "NinjaFull"}, + {"alfadur", "NoHat"}, + {"Anachron", "war_americanww2helmet"}, + {"Bufon", "ShaggyYeti"}, + {"burp", "lambda"}, + {"Blue", "cap_blue"}, + {"bender", "NoHat"}, + {"Castell", "NoHat"}, + {"cekoto", "NoHat"}, + {"CheezeMonkey", "NoHat"}, + {"claymore", "NoHat"}, + {"CIA-144", "cyborg1"}, + {"cri.the.grinch", "sf_blanka"}, + {"eldiablo", "Evil"}, + {"Displacer", "fr_lemon"}, + {"doomy", "NoHat"}, + {"Falkenauge", "NoHat"}, + {"FadeOne", "NoHat"}, + {"hayaa", "NoHat"}, + {"Hermes", "laurel"}, + {"Henek", "WizardHat"}, + {"HedgeKing", "NoHat"}, + {"Izack1535", "NoHat"}, + {"Kiofspa", "NoHat"}, + {"KoBeWi", "NoHat"}, + {"Komplex", "NoHat"}, + {"koda", "poke_mudkip"}, + {"Lalo", "NoHat"}, + {"Logan", "NoHat"}, + {"lollkiller", "NoHat"}, + {"Luelle", "NoHat"}, + {"mikade", "Skull"}, + {"Mushi", "sm_daisy"}, + {"Naboo", "NoHat"}, + {"nemo", "bb_bub"}, + {"practice", "NoHat"}, + {"Prof. Panic", "NoHat"}, + {"Randy", "zoo_Sheep"}, + {"rhino", "NinjaTriangle"}, + {"Radissthor", "NoHat"}, + {"Sami", "sm_peach"}, + {"soreau", "NoHat"}, + {"Solar", "pinksunhat"}, + {"sparkle", "NoHat"}, + {"szczur", "mp3"}, + {"sdw195", "NoHat"}, + {"sphrix", "TeamTopHat"}, + {"sheepluva", "zoo_Sheep"}, + {"Smaxx", "NoHat"}, + {"shadowzero", "NoHat"}, + {"Star and Moon", "SparkleSuperFun"}, + {"The 24", "NoHat"}, + {"TLD", "NoHat"}, + {"Tiyuri", "sf_ryu"}, + {"unC0Rr", "cyborg1"}, + {"Waldsau", "cyborg1"}, + {"wolfmarc", "knight"}, + {"Wuzzy", "fr_orange"}, + {"Xeli", "android"} +} - } - -local playerTeamName - -function GetKillScore() - return math.ceil((hogsKilled / 16)*6000) -end - -function ProtectEnemies() - for i=1, 16 do - if hhs[i] and GetHealth(hhs[i]) then - SetEffect(hhs[i], heInvulnerable, 1) - end +local function assignNamesAndHats(team) + for t=1, #team do + local d = 1 + GetRandom(#hogData) + team[t].name = hogData[d][1] + team[t].hat = hogData[d][2] + table.remove(hogData, d) end end -function GameOverMan() - StopMusicSound(sndRideOfTheValkyries) - valkyriesTimer = -1 - missionWon = false - ProtectEnemies() - SendStat(siGameResult, loc("Challenge over!")) - local score = GetKillScore() - SendStat(siCustomAchievement, string.format(loc("You have killed %d of 16 hedgehogs (+%d points)."), hogsKilled, score)) - SendStat(siPointType, "!POINTS") - SendStat(siPlayerKills, tostring(score), playerTeamName) - - -- Update highscore - updateChallengeRecord("Highscore", score) - - EndGame() -end - -function GG() - missionWon = true - local completeTime = (TurnTime - finishTime) / 1000 - ShowMission(loc("Rope-knocking Challenge"), loc("Challenge completed!"), loc("Congratulations!") .. "|" .. string.format(loc("Completion time: %.2fs"), completeTime), 0, 0) - PlaySound(sndHomerun) - SendStat(siGameResult, loc("Challenge completed!")) - local hogScore = GetKillScore() - local timeScore = math.ceil((finishTime/TurnTime)*6000) - local score = hogScore + timeScore - - SendStat(siCustomAchievement, string.format(loc("You have killed %d of 16 hedgehogs (+%d points)."), hogsKilled, hogScore)) - SendStat(siCustomAchievement, string.format(loc("You have completed this challenge in %.2f s (+%d points)."), completeTime, timeScore)) - SendStat(siPointType, "!POINTS") - SendStat(siPlayerKills, tostring(score), playerTeamName) - SetTeamLabel(playerTeamName, tostring(score)) - - -- Update highscore - updateChallengeRecord("Highscore", score) - - if hhs[0] and GetHealth(hhs[0]) then - SetEffect(hhs[0], heInvulnerable, 1) - end - SetTurnTimeLeft(MAX_TURN_TIME) -end - -function AssignCharacter(p) - - done = false - sanityCheck = 0 - - while(done == false) do - i = 1+ GetRandom(#HogData) - if HogData[i][3] == false then - HogData[i][3] = true - done = true - SetHogName(hhs[p], HogData[i][1]) - SetHogHat(hhs[p], HogData[i][2]) - elseif HogData[i][3] == true then - sanityCheck = sanityCheck +1 - if sanityCheck == 100 then - done = true - SetHogName(hhs[p], "Newbie") - SetHogHat(hhs[p], "NoHat") - end - end - - end - -end - -function onGameInit() - - --Seed = 1 - GameFlags = gfBorder + gfSolidLand - - TurnTime = 180 * 1000 - Map = "Ropes" - Theme = "Eyes" - - -- Disable Sudden Death - WaterRise = 0 - HealthDecrease = 0 - - CaseFreq = 0 - MinesNum = 0 - Explosives = 0 +local enemyTeam1 = { + { x = 3350, y = 570 }, + { x = 3039, y = 1300 }, + { x = 2909, y = 430 }, + { x = 2150, y = 879 }, + { x = 1735, y = 1136 }, + { x = 1563, y = 553 }, + { x = 679, y = 859 }, + { x = 1034, y = 251 }, +} +local enemyTeam2 = { + { x = 255, y = 91 }, + { x = 2671, y = 7 }, + { x = 2929, y = 244 }, + { x = 1946, y = 221 }, + { x = 3849, y = 1067 }, + { x = 3360, y = 659 }, + { x = 3885, y = 285 }, + { x = 935, y = 1160 }, +} - playerTeamName = AddMissionTeam(-1) - hhs[0] = AddMissionHog(1) - - AddTeam(loc("Unsuspecting Louts"), -2, "Simple", "Island", "Default", "cm_face") - for i = 1, 8 do - -- The name "generic" is a placeholder and will be replaced in AssignCharacter - hhs[i] = AddHog("generic", 0, 1, "NoHat") - end - - AddTeam(loc("Unlucky Sods"), -2, "Simple", "Island", "Default", "cm_balrog") - for i = 9, 16 do - hhs[i] = AddHog("generic", 0, 1, "NoHat") - end - -end - - - -function onGameStart() - SendHealthStatsOff() - - local recordInfo = getReadableChallengeRecord("Highscore") - if recordInfo == nil then - recordInfo = "" - else - recordInfo = "|" .. recordInfo - end - ShowMission ( - loc("Rope-knocking Challenge"), - loc("Challenge"), - loc("Use the rope to knock your enemies to their doom.") .. "|" .. - loc("Finish this challenge as fast as possible to earn bonus points.").. recordInfo, - -amRope, 4000) - SetTeamLabel(playerTeamName, "0") - - PlaceGirder(46,1783, 0) +assignNamesAndHats(enemyTeam1) +assignNamesAndHats(enemyTeam2) - SetGearPosition(hhs[0], 2419, 1769) - SetGearPosition(hhs[1], 3350, 570) - SetGearPosition(hhs[2], 3039, 1300) - SetGearPosition(hhs[3], 2909, 430) - SetGearPosition(hhs[4], 2150, 879) - SetGearPosition(hhs[5], 1735, 1136) - SetGearPosition(hhs[6], 1563, 553) - SetGearPosition(hhs[7], 679, 859) - SetGearPosition(hhs[8], 1034, 251) - SetGearPosition(hhs[9], 255, 67) - SetGearPosition(hhs[10], 2671, 7) - SetGearPosition(hhs[11], 2929, 244) - SetGearPosition(hhs[12], 1946, 221) - SetGearPosition(hhs[13], 3849, 1067) - SetGearPosition(hhs[14], 3360, 659) - SetGearPosition(hhs[15], 3885, 285) - SetGearPosition(hhs[16], 935, 1160) - HogTurnLeft(hhs[0], true) - - for i = 1, 16 do - AssignCharacter(i) - end - -end - -function onGameTick() - - if (TurnTimeLeft == 1) and (missionWon == nil) then - GameOverMan() - end - - if missionWon ~= nil then - - endTimer = endTimer - 1 - if endTimer == 1 then - EndGame() - end - - if not missionEndHandled then - if missionWon == true then - SaveMissionVar("Won", "true") - AddCaption(loc("Victory!"), capcolDefault, capgrpGameState) - end - missionEndHandled = true - end - - end - -end - -function onGameTick20() - if (valkyriesTimer > 0) then - valkyriesTimer = valkyriesTimer - 20 - if valkyriesTimer <= 0 then - StopMusicSound(sndRideOfTheValkyries) - end - end -end - -function onGearDamage(gear, damage) - - if gear == hhs[0] then - ouchies = true - StopMusicSound(sndRideOfTheValkyries) - valkyriesTimer = -1 - ProtectEnemies() - end +RopeKnocking({ + missionName = loc("Rope-knocking Challenge"), + map = "Ropes", + theme = "Eyes", + turnTime = 180000, + valkyries = true, + playerTeam = { + x = 2419, + y = 1769, + faceLeft = true, + }, + enemyTeams = { + { + name = loc("Unsuspecting Louts"), + flag = "cm_face", + hogs = enemyTeam1, + }, + { + name = loc("Unlucky Sods"), + flag = "cm_balrog", + hogs = enemyTeam2, + }, + }, + onGameStart = function() + PlaceGirder(46,1783, 0) + end, +}) - if gear ~= hhs[0] and GetGearType(gear) == gtHedgehog and missionWon == nil and ouchies == false then - - AddVisualGear(GetX(gear), GetY(gear), vgtBigExplosion, 0, false) - DeleteGear(gear) - PlaySound(sndExplosion) - AddCaption(string.format(knockTaunt(), GetHogName(gear)), capcolDefault, capgrpMessage) - - hogsKilled = hogsKilled +1 - SetTeamLabel(playerTeamName, tostring(GetKillScore())) - - if hogsKilled == 15 then - PlayMusicSound(sndRideOfTheValkyries) - -- Time in ms after which to return to normal music - valkyriesTimer = 20000 - elseif hogsKilled == 16 then - finishTime = TurnTimeLeft - GG() - end - - end - -end - -function knockTaunt() - local r = math.random(0,23) - local taunt - if r == 0 then taunt = loc("%s has been knocked out.") - elseif r == 1 then taunt = loc("%s hit the ground.") - elseif r == 2 then taunt = loc("%s splatted.") - elseif r == 3 then taunt = loc("%s was smashed.") - elseif r == 4 then taunt = loc("%s felt unstable.") - elseif r == 5 then taunt = loc("%s exploded.") - elseif r == 6 then taunt = loc("%s fell from a high cliff.") - elseif r == 7 then taunt = loc("%s goes the way of the lemming.") - elseif r == 8 then taunt = loc("%s was knocked away.") - elseif r == 9 then taunt = loc("%s was really unlucky.") - elseif r == 10 then taunt = loc("%s felt victim to rope-knocking.") - elseif r == 11 then taunt = loc("%s had no chance.") - elseif r == 12 then taunt = loc("%s was a good target.") - elseif r == 13 then taunt = loc("%s spawned at a really bad position.") - elseif r == 14 then taunt = loc("%s was doomed from the beginning.") - elseif r == 15 then taunt = loc("%s has fallen victim to gravity.") - elseif r == 16 then taunt = loc("%s hates Newton.") -- Isaac Newton - elseif r == 17 then taunt = loc("%s had it coming.") - elseif r == 18 then taunt = loc("%s is eliminated!") - elseif r == 19 then taunt = loc("%s fell too fast.") - elseif r == 20 then taunt = loc("%s flew like a rock.") - elseif r == 21 then taunt = loc("%s stumbled.") - elseif r == 22 then taunt = loc("%s was shoved away.") - elseif r == 23 then taunt = loc("%s didn't expect that.") - end - return taunt -end - -function onGearDelete(gear) - - if (gear == hhs[0]) and (missionWon == nil) then - GameOverMan() - end - -end - -function onAmmoStoreInit() - SetAmmo(amRope, 9, 0, 0, 0) -end - -function onNewTurn() - SetWeapon(amRope) -end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Scenario/Big_Armory.lua --- a/share/hedgewars/Data/Missions/Scenario/Big_Armory.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Scenario/Big_Armory.lua Sun Mar 24 14:33:57 2024 -0400 @@ -2,7 +2,7 @@ HedgewarsScriptLoad("/Scripts/Locale.lua") local heroAmmo = {} -for a=0, amCreeper do +for a=0, amMinigun do if a == amExtraTime then heroAmmo[a] = 2 elseif a ~= amNothing and a ~= amCreeper then diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Missions/Training/Basic_Training_-_Rope.lua --- a/share/hedgewars/Data/Missions/Training/Basic_Training_-_Rope.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Missions/Training/Basic_Training_-_Rope.lua Sun Mar 24 14:33:57 2024 -0400 @@ -14,6 +14,7 @@ HedgewarsScriptLoad("/Scripts/Locale.lua") HedgewarsScriptLoad("/Scripts/Utils.lua") +HedgewarsScriptLoad("/Scripts/Achievements.lua") -- Map definition automatically converted from HWMAP file by hwmap2lua.sh local map = @@ -42,6 +43,8 @@ local gameOver = false -- game over (only victory possible) local currentTarget = 0 -- current target ID. First target = 1 local flawless = true -- flawless if no damage taken and no mistake made +local flowerPower = false -- random flower visual gears appear all ower the place +local bonusFlowerPlaced = false -- a hidden flower sprite was placed local cpX, cpY = 208, 1384 -- hog checkpoint, initialized with start coords @@ -118,6 +121,7 @@ SetHealth(hog, initHogHealthFinal) AddAmmo(hog, amRope, 1) SetGearVelocity(hog, 0, 0) + flowerPower = false if setPos then PlaySound(sndWarp) @@ -207,6 +211,13 @@ end function onGameTick() + + if flowerPower then + for i=1,2 do + AddVisualGear(math.random(-1024, LAND_WIDTH+1024), math.random(TopY-1024, LAND_HEIGHT), vgtBeeTrace, 0, false) + end + end + if gameOver or (not CurrentHedgehog) then return end @@ -255,7 +266,13 @@ if isInFinalChallenge then local dX, dY = GetGearVelocity(CurrentHedgehog) local x, y = GetGearPosition(CurrentHedgehog) - if band(GetState(CurrentHedgehog), gstHHDriven) ~= 0 and GetAmmoCount(CurrentHedgehog, amRope) == 0 and + local driven = band(GetState(CurrentHedgehog), gstHHDriven) ~= 0 + if driven and y > 1310 and x < 338 and not flowerPower then + -- Player reached the bonus flower. Enable Flower Power mode! + PlaySound(sndKiss) + flowerPower = true + end + if driven and GetAmmoCount(CurrentHedgehog, amRope) == 0 and GetFlightTime(CurrentHedgehog) == 0 and (not ropeGear) and math.abs(dX) < 5 and math.abs(dY) < 5 and (x < 3417 or y > 471) then @@ -268,6 +285,12 @@ end function onGameTick20() + if flowerPower then + if math.random(1,2) == 1 then + local vg = AddVisualGear(GetX(CurrentHedgehog), GetY(CurrentHedgehog), vgtStraightShot, sprTargetBee, false, 1) + SetVisualGearValues(vg, nil, nil, nil, nil, math.random(0, 360), nil, nil, nil, nil, 0xFFFFFFC0) + end + end if not gameOver and not target1Reached and CurrentHedgehog and gearIsInCircle(CurrentHedgehog, targetData[1][1], targetData[1][2], 48, false) then ShowMission(loc("Basic Rope Training"), loc("Target Puncher"), loc("Okay, now destroy the target|using the baseball bat.").."|".. @@ -362,6 +385,12 @@ 2, 25000) eraseGirder(4) eraseGirder(5) + -- Sneakingly place a flower sprite near spawn when player reached the last section + -- When the player reaches it, Flower Power mode is enabled + if not bonusFlowerPlaced then + PlaceSprite(240, 1360, sprTargetBee, 0) + bonusFlowerPlaced = true + end AddAmmo(hog, amRope, 1) SetHealth(hog, initHogHealthFinal) isInFinalChallenge = true @@ -379,6 +408,9 @@ AddAmmo(hog, amRope, 0) SendStat(siCustomAchievement, loc("Oh yeah! You sure know how to rope!")) SendStat(siGameResult, loc("You have finished the Basic Rope Training!")) + if flowerPower then + awardAchievement(loc("Flower Power")) + end EndGame() SetState(hog, gstWinner) gameOver = true diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Music/Jungle.ogg diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Battalion.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Battalion.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Battalion.lua Sun Mar 24 14:33:57 2024 -0400 @@ -804,7 +804,7 @@ PlaySound(sndShotgunReload) if GetRandom(100) < emptyCrateChance then - AddCaption(loc("It's empty!"), msgColor, capgrpMessage) + AddCaption(GetEngineString("TMsgStrId", sidEmptyCrate), msgColor, capgrpMessage) return elseif GetRandom(100) < bonusCrateChance then factor = 3 @@ -834,7 +834,7 @@ if GetRandom(100) < emptyCrateChance then if IsHogLocal(CurHog) then - AddCaption(loc("It's empty!"), msgColor, capgrpMessage) + AddCaption(GetEngineString("TMsgStrId", sidEmptyCrate), msgColor, capgrpMessage) end return elseif GetRandom(100) < bonusCrateChance then @@ -880,7 +880,7 @@ if GetRandom(100) < emptyCrateChance then if IsHogLocal(CurHog) then - AddCaption(loc("It's empty!"), msgColor, capgrpMessage) + AddCaption(GetEngineString("TMsgStrId", sidEmptyCrate), msgColor, capgrpMessage) end return elseif GetRandom(100) < bonusCrateChance then @@ -1532,7 +1532,7 @@ useVariantHats = params['mutate'] end - if params['strength'] ~= nil and tonumber(params['strength']) > 0 then + if params['strength'] ~= nil and tonumber(params['strength']) ~= nil and tonumber(params['strength']) > 0 then strength = tonumber(params['strength']) -- Highland if mode == 'highland' then @@ -1561,7 +1561,7 @@ end end - if params['luck'] ~= nil and tonumber(params['luck']) > 0 then + if params['luck'] ~= nil and tonumber(params['luck']) and tonumber(params['luck']) > 0 then luck = tonumber(params['luck']) healthCrateChance = div(healthCrateChance * luck, 100) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Capture_the_Flag.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Capture_the_Flag.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Capture_the_Flag.lua Sun Mar 24 14:33:57 2024 -0400 @@ -49,9 +49,9 @@ -- 0.3 --------- -- [fufufufu kamikaze fix] --- added nill checks to make sure the player doesn't generate errors by producing a nil value in hhs[] when he uses kamikaze +-- added nil checks to make sure the player doesn't generate errors by producing a nil value in hhs[] when using kamikaze -- added a check to make sure the player doesn't kamikaze straight down and make the flag's starting point underwater --- added a check to make sure the player drops the flag if he has it and he uses kamikaze +-- added a check to make sure the player drops the flag if they have it and they use kamikaze -------- -- 0.4 @@ -62,7 +62,7 @@ -- fix piano strike exploit -- changed delay to allow for better portals -- changed starting feedback a little --- increased the radius around the circle indicating the flag thief so that it doesn't obscure his health +-- increased the radius around the circle indicating the flag thief so that it doesn't obscure their health -------- -- 0.5 @@ -597,7 +597,7 @@ function onGearResurrect(gear) if GetGearType(gear) == gtHedgehog then - -- mark the flag thief as dead if he needed a respawn + -- mark the flag thief as dead if they needed a respawn for i = 0, ClansCount-1 do if gear == fThief[i] then FlagThiefDead(gear) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Continental_supplies.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Continental_supplies.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Continental_supplies.lua Sun Mar 24 14:33:57 2024 -0400 @@ -236,8 +236,27 @@ select_wep = "" quit_hint = "" end + local gameFlagPrepend = "" + local continentInfoPlace = loc("Continents: Select a continent after placing your hogs.") + local continentInfoNormal = loc("Continents: Select a continent at the beginning.") + local continentInfo = continentInfoNormal + if GetGameFlag(gfKing) then + gameFlagPrepend = gameFlagPrepend .. GetEngineString("TGoalStrId", gidKing).."|" + if GetGameFlag(gfPlaceHog) then + gameFlagPrepend = gameFlagPrepend .. GetEngineString("TGoalStrId", gidPlaceHog).."|" + else + gameFlagPrepend = gameFlagPrepend .. GetEngineString("TGoalStrId", gidPlaceKing).."|" + end + continentInfo = continentInfoPlace + else + if GetGameFlag(gfPlaceHog) then + gameFlagPrepend = gameFlagPrepend .. GetEngineString("TGoalStrId", gidPlaceHog).."|" + continentInfo = continentInfoPlace + end + end local general_information = - loc("Continents: Select a continent at the beginning.").."|".. + gameFlagPrepend.. + continentInfo.."|".. loc("Supplies: Each continent gives you unique weapons, specials and health.").."|".. loc("Weapon specials: Some weapons have special modes (see weapon description).").. select_wep.. @@ -1144,9 +1163,15 @@ function onGameInit() SuddenDeathTurns= SuddenDeathTurns+1 + -- Disable GameFlags that are incompatible with this game + DisableGameFlags(gfPerHogAmmo, gfSharedAmmo, gfResetWeps) end function onEndTurn() + if(TotalRounds == -1) then + -- Do nothing if placing hogs + return + end if(CS.TEAM_CONTINENT[GetHogTeamName(CurrentHedgehog)]==0) then CS.TEAM_CONTINENT[GetHogTeamName(CurrentHedgehog)]=GetRandom(#CS.CONTINENT_INFORMATION)+1 @@ -1188,7 +1213,7 @@ SetAttackState(true) --when all hogs are "placed" - if(GetCurAmmoType()~=amTeleport) + if(TotalRounds ~= -1) then --will run once when the game really starts (after placing hogs and so on if(CS.INIT_TEAMS[GetHogTeamName(CurrentHedgehog)] == nil) @@ -2142,7 +2167,11 @@ CS.PARACHUTE_IS_ON=1 elseif(GetGearType(gearUid)==gtSwitcher) then - CS.SWITCH_HOG_IS_ON=true + if not CS.GAME_STARTED then + DeleteGear(gearUid) + else + CS.SWITCH_HOG_IS_ON=true + end end end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/HedgeEditor.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/HedgeEditor.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/HedgeEditor.lua Sun Mar 24 14:33:57 2024 -0400 @@ -397,91 +397,91 @@ loc_noop("Clowns"), {"WhySoSerious","clown-copper","clown-crossed","clown","Joker"}, {loc_noop("Baggy"),loc_noop("Bingo"),loc_noop("Bobo"),loc_noop("Bozo"),loc_noop("Buster"),loc_noop("Chester"),loc_noop("Copper"),loc_noop("Heckles"),loc_noop("Giggles"),loc_noop("Jingo"),loc_noop("Molly"),loc_noop("Loopy"),loc_noop("Patches"),loc_noop("Tatters")}, - "R","cm_balls","Mobster","Rubberduck","Castle" + "R","cm_balls","Mobster_qau","Rubberduck","Castle" }, { loc_noop("Street Fighters"), {"sf_balrog","sf_blanka","sf_chunli","sf_guile","sf_honda","sf_ken","sf_ryu","sf_vega"}, {loc_noop("Balrog"),loc_noop("Blanka"),loc_noop("Chunli"),loc_noop("Guile"),loc_noop("Honda"),loc_noop("Ken"),loc_noop("Ryu"),loc_noop("Vega")}, - "F","cm_balrog","Surfer","dragonball","Castle" + "F","cm_balrog","Surfer_qau","dragonball","Castle" }, { loc_noop("Cybernetic Empire"), {"cyborg1","cyborg2"}, {loc_noop("Unit 189"),loc_noop("Unit 234"),loc_noop("Unit 333"),loc_noop("Unit 485"),loc_noop("Unit 527"),loc_noop("Unit 638"),loc_noop("Unit 709"),loc_noop("Unit 883")}, - "R","cm_binary","Robot","Grave","Castle" + "R","cm_binary","Robot_qau","Grave","Castle" }, { loc_noop("Color Squad"), {"hair_blue","hair_green","hair_red","hair_yellow","hair_purple","hair_grey","hair_orange","hair_pink"}, {loc_noop("Blue"),loc_noop("Green"),loc_noop("Red"),loc_noop("Yellow"),loc_noop("Purple"),loc_noop("Grey"),loc_noop("Orange"),loc_noop("Pink")}, - "F","mauritius","Singer","Grave","Castle" + "F","mauritius","Singer_qau","Grave","Castle" }, { loc_noop("Fruit"), {"fr_apple","fr_banana","fr_lemon","fr_orange","fr_pumpkin","fr_tomato"}, {loc_noop("Juicy"),loc_noop("Squishy"),loc_noop("Sweet"),loc_noop("Sour"),loc_noop("Bitter"),loc_noop("Ripe"),loc_noop("Rotten"),loc_noop("Fruity")}, - "R","cm_mog","Default","Cherry","Castle" + "R","cm_mog","Default_qau","Cherry","Castle" }, { loc_noop("The Police"), {"bobby","bobby2v","policecap","policegirl","royalguard"}, {loc_noop("Hightower"),loc_noop("Lassard"),loc_noop("Callahan"),loc_noop("Jones"),loc_noop("Harris"),loc_noop("Thompson"),loc_noop("Mahoney"),loc_noop("Hooks"),loc_noop("Tackleberry")}, - "R","cm_star","British","Statue","Castle" + "R","cm_star","British_qau","Statue","Castle" }, { loc_noop("The Ninja-Samurai Alliance"), {"NinjaFull","NinjaStraight","NinjaTriangle","Samurai","StrawHat","StrawHatEyes","StrawHatFacial","naruto"}, {loc_noop("Bushi"),loc_noop("Tatsujin"),loc_noop("Itami"),loc_noop("Arashi"),loc_noop("Shinobi"),loc_noop("Ukemi"),loc_noop("Godai"),loc_noop("Kenshi"),loc_noop("Ninpo")}, - "R","japan","Default","octopus","Castle" + "R","japan","Default_qau","octopus","Castle" }, { loc_noop("Pokémon"), {"poke_ash","poke_charmander","poke_chikorita","poke_jigglypuff","poke_lugia","poke_mudkip","poke_pikachu","poke_slowpoke","poke_squirtle","poke_voltorb"}, {loc_noop("Ash"),loc_noop("Charmander"),loc_noop("Chikorita"),loc_noop("Jigglypuff"),loc_noop("Lugia"),loc_noop("Mudkip"),loc_noop("Pikachu"),loc_noop("Slowpoke"),loc_noop("Squirtle"),loc_noop("Voltorb")}, - "FR","cm_pokemon","Default","pokeball","Castle" + "FR","cm_pokemon","Default_qau","pokeball","Castle" }, { loc_noop("The Zoo"), {"zoo_Bat","zoo_Beaver","zoo_Bunny","zoo_Deer","zoo_Hedgehog","zoo_Moose","zoo_Pig","zoo_Porkey","zoo_Sheep","zoo_chicken","zoo_elephant","zoo_fish","zoo_frog","zoo_snail","zoo_turtle"}, {loc_noop("Batty"),loc_noop("Tails"),loc_noop("Bunny"),loc_noop("Deer"),loc_noop("Spikes"),loc_noop("Horns"),loc_noop("Bacon"),loc_noop("Porkey"),loc_noop("Sheepy"),loc_noop("Chicken"),loc_noop("Trunks"),loc_noop("Fishy"),loc_noop("Legs"),loc_noop("Slimer"),loc_noop("Roshi")}, - "FR","cm_birdy","Default","Bone","Castle" + "FR","cm_birdy","Default_qau","Bone","Castle" }, { loc_noop("The Devs"), {"ushanka","zoo_Sheep","bb_bob","Skull","poke_mudkip","lambda","WizardHat","sf_ryu","android","fr_lemon","mp3"}, {loc_noop("unC0Rr"), loc_noop("sheepluva"), loc_noop("nemo"), loc_noop("mikade"), loc_noop("koda"), loc_noop("burp"),loc_noop("HeneK"),loc_noop("Tiyuri"),loc_noop("Xeli"),loc_noop("Displacer"),loc_noop("szczur")}, - "FR","cm_hw","Classic","Statue","Castle" + "FR","cm_hw","Classic_qau","Statue","Castle" }, { loc_noop("Mushroom Kingdom"), {"sm_daisy","sm_luigi","sm_mario","sm_peach","sm_toad","sm_wario","NoHat","NoHat"}, {loc_noop("Daisy"),loc_noop("Luigi"),loc_noop("Mario"),loc_noop("Princess Peach"),loc_noop("Toad"),loc_noop("Wario"),loc_noop("Yoshi"),loc_noop("Waluigi")}, - "FR","comoros","Default","Badger","Castle" + "FR","comoros","Default_qau","Badger","Castle" }, { loc_noop("Pirates"), {"pirate_jack","pirate_jack_bandana"}, {loc_noop("Rusted Diego"),loc_noop("Fuzzy Beard"),loc_noop("Al.Kaholic"),loc_noop("Morris"),loc_noop("Yumme Gunpowder"),loc_noop("Cutlass Cain"),loc_noop("Jim Morgan"),loc_noop("Silver"),loc_noop("Dubloon Devil"),loc_noop("Ugly Mug"),loc_noop("Fair Wind"),loc_noop("Scallywag"),loc_noop("Salty Dog"),loc_noop("Bearded Beast"),loc_noop("Timbers"),loc_noop("Both Barrels"),loc_noop("Jolly Roger")}, - "R","cm_pirate","Pirate","chest","Castle" + "R","cm_pirate","Pirate_qau","chest","Castle" }, { loc_noop("Gangsters"), {"Moustache","Cowboy","anzac","Bandit","thug","Jason","NinjaFull","chef"}, {loc_noop("The Boss"),loc_noop("Jimmy"),loc_noop("Frankie"),loc_noop("Morris"),loc_noop("Mooney"),loc_noop("Knives"),loc_noop("Tony"),loc_noop("Meals")}, - "F","cm_anarchy","Mobster","deadhog","Castle" + "F","cm_anarchy","Mobster_qau","deadhog","Castle" }, @@ -489,7 +489,7 @@ loc_noop("Twenty-Twenty"), {"Glasses","lambda","SunGlasses","Sniper","Terminator_Glasses","Moustache_glasses","doctor","punkman","rasta"}, {loc_noop("Specs"),loc_noop("Speckles"),loc_noop("Spectator"),loc_noop("Glasses"),loc_noop("Glassy"),loc_noop("Harry Potter"),loc_noop("Goggles"),loc_noop("Clark Kent"),loc_noop("Goggs"),loc_noop("Lightbender"),loc_noop("Specs Appeal"),loc_noop("Four Eyes")}, - "R","cm_face","Default","eyecross","Castle" + "R","cm_face","Default_qau","eyecross","Castle" }, @@ -497,28 +497,28 @@ loc_noop("Monsters"), {"Skull","Jason","ShaggyYeti","Zombi","cyclops","Mummy","hogpharoah","vampirichog"}, {loc_noop("Bones"),loc_noop("Jason"),loc_noop("Yeti"),loc_noop("Zombie"),loc_noop("Old One Eye"),loc_noop("Ramesses"),loc_noop("Xerxes"),loc_noop("Count Hogula")}, - "FR","cm_vampire","Default","octopus","Castle" + "FR","cm_vampire","Default_qau","octopus","Castle" }, { loc_noop("The Iron Curtain"), {"ushanka","war_sovietcomrade1","war_sovietcomrade1","ushanka"}, {loc_noop("Alex"),loc_noop("Sergey"),loc_noop("Vladimir"),loc_noop("Andrey"),loc_noop("Dimitry"),loc_noop("Ivan"),loc_noop("Oleg"),loc_noop("Kostya"),loc_noop("Anton"),loc_noop("Eugene")}, - "R","cm_soviet","Russian","skull","Castle" + "R","cm_soviet","Russian_qau","skull","Castle" }, { loc_noop("Desert Storm"), {"war_desertofficer","war_desertgrenadier1","war_desertmedic","war_desertsapper1","war_desertgrenadier2","war_desertgrenadier4","war_desertsapper2","war_desertgrenadier5"}, {loc_noop("Brigadier Briggs"),loc_noop("Lt. Luke"),loc_noop("Sgt. Smith"),loc_noop("Corporal Calvin"),loc_noop("Frank"),loc_noop("Joe"),loc_noop("Sam"),loc_noop("Donald")}, - "F","bhutan","Default","Grave","Castle" + "F","bhutan","Default_qau","Grave","Castle" }, { loc_noop("The Hospital"), {"doctor","nurse","war_britmedic","war_desertmedic","war_germanww2medic"}, {loc_noop("Dr. Blackwell"),loc_noop("Dr. Drew"),loc_noop("Dr. Harvey"),loc_noop("Dr. Crushing"),loc_noop("Dr. Jenner"),loc_noop("Dr. Barnard"),loc_noop("Dr. Parkinson"),loc_noop("Dr. Banting"),loc_noop("Dr. Horace"),loc_noop("Dr. Hollows"),loc_noop("Dr. Jung")}, - "R","cm_firstaid","Default","heart","Castle" + "R","cm_firstaid","Default_qau","heart","Castle" } } @@ -1503,7 +1503,7 @@ if not tFort then tFort = "Castle" end if not tGrave then tGrave = "Statue" end if not tFlag then tFlag= "hedgewars" end - if not tVoice then tVoice = "Default" end + if not tVoice then tVoice = "Default_qau" end lastRecordedTeam = GetHogTeamName(gear) @@ -3117,9 +3117,9 @@ elseif (preciseOn == true) and (s == 1) then helpDisabled = not(helpDisabled) if helpDisabled then - AddCaption(loc("Help Disabled"), colorInfoMessage, capgrpVolume) + AddCaption(loc("Help Disabled"), capcolSetting, capgrpVolume) else - AddCaption(loc("Help Enabled"), colorInfoMessage, capgrpVolume) + AddCaption(loc("Help Enabled"), capcolSetting, capgrpVolume) end updateHelp() elseif (cat[cIndex] == loc("Sprite Placement Mode")) or (cat[cIndex] == loc("Girder Placement Mode")) or (cat[cIndex] == loc("Rubber Placement Mode")) or (cat[cIndex] == loc("Sprite Modification Mode")) then @@ -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, } diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Highlander.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Highlander.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Highlander.lua Sun Mar 24 14:33:57 2024 -0400 @@ -323,13 +323,13 @@ if currHog ~= lastHog then - -- re-assign ammo to this guy, so that his entire ammo set will + -- re-assign ammo to this fellow, so that their entire ammo set will -- be visible during another player's turn if lastHog ~= nil and GetHealth(lastHog) then ConvertValues(lastHog) end - -- give the new hog what he is supposed to have, too + -- give the new hog what they are supposed to have, too ConvertValues(CurrentHedgehog) end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua Sun Mar 24 14:33:57 2024 -0400 @@ -21,6 +21,7 @@ HedgewarsScriptLoad("/Scripts/Locale.lua") HedgewarsScriptLoad("/Scripts/Tracker.lua") HedgewarsScriptLoad("/Scripts/Params.lua") +HedgewarsScriptLoad("/Scripts/Utils.lua") --[[ MUTANT SCRIPT @@ -59,6 +60,7 @@ local teamsDead = {} local teamsDeleted = {} local hogLimitHit = false +local teamLimitHit = false local cnthhs local circles = {} @@ -157,7 +159,7 @@ end function limitHogsClan(gear) - hogLimitHit = true + teamLimitHit = true SetEffect(gear, heResurrectable, 0) setGearValue(gear, "excess", true) DeleteGear(gear) @@ -197,8 +199,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() @@ -789,8 +793,9 @@ if not gameOver then local winner = createEndGameStats() if winner then - SendStat(siGameResult, string.format(loc("%s wins!"), winner)) - AddCaption(string.format(loc("%s wins!"), winner), capcolDefault, capgrpGameState) + local winText = formatEngineString(GetEngineString("TMsgStrId", sidWinner), winner) + SendStat(siGameResult, winText) + AddCaption(winText, capcolDefault, capgrpGameState) end gameOver = true end @@ -813,6 +818,11 @@ if GetGearType(gear) == gtHedgehog then numhhs = numhhs - 1 + if (not gameOver) and (gear == mutant) then + mutant = nil + mt_hurt = false + end + local found for i=0, #hhs do if hhs[i] == gear then diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Racer.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Racer.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Racer.lua Sun Mar 24 14:33:57 2024 -0400 @@ -32,6 +32,7 @@ HedgewarsScriptLoad("/Scripts/Locale.lua") HedgewarsScriptLoad("/Scripts/OfficialChallenges.lua") HedgewarsScriptLoad("/Scripts/Params.lua") +HedgewarsScriptLoad("/Scripts/Utils.lua") ------------------ -- Got Variables? @@ -55,6 +56,9 @@ local specialPointsY = {} local specialPointsCount = 0 +local landObjectPoints = {} +local landObjects = {} + local TeamRope = false local waypointCursor = false @@ -101,6 +105,7 @@ local wpCol = {} local wpActive = {} local wpRad = 450 +local WAYPOINT_RADIUS_MIN = 40 local wpCount = 0 local wpLimit = 8 @@ -187,22 +192,25 @@ TeamRope = true end if params["rounds"] ~= nil then - roundLimit = math.max(1, math.floor(tonumber(params["rounds"]))) + roundLimit = tonumber(params["rounds"]) if type(roundLimit) ~= "number" then roundLimit = 3 end + roundLimit = math.max(1, math.floor(roundLimit)) end if params["waypointradius"] ~= nil then - wpRad = math.max(40, math.floor(tonumber(params["waypointradius"]))) + wpRad = tonumber(params["waypointradius"]) if type(wpRad) ~= "number" then wpRad = 450 end + wpRad = math.max(WAYPOINT_RADIUS_MIN, math.floor(wpRad)) end if params["maxwaypoints"] ~= nil then - wpLimit = math.max(2, math.floor(tonumber(params["maxwaypoints"]))) + wpLimit = tonumber(params["maxwaypoints"]) if type(wpLimit) ~= "number" then wpLimit = 8 end + wpLimit = math.max(2, math.floor(wpLimit)) end end @@ -501,17 +509,17 @@ local roundDraw = false if #clanScores >= 2 and clanScores[1].score == clanScores[2].score and clanScores[1].score ~= MAX_TURN_TIME then roundDraw = true - SendStat(siGameResult, loc("Round draw")) + SendStat(siGameResult, GetEngineString("TMsgStrId", sidDraw)) SendStat(siCustomAchievement, loc("The teams are tied for the fastest time.")) elseif #sortedTeams >= 1 then - SendStat(siGameResult, string.format(loc("%s wins!"), sortedTeams[1].name)) + SendStat(siGameResult, formatEngineString(GetEngineString("TMsgStrId", sidWinner), sortedTeams[1].name)) SendStat(siCustomAchievement, string.format(loc("%s wins with a best time of %.1fs."), sortedTeams[1].name, (sortedTeams[1].score/1000))) for i=1,#unfinishedArray do SendStat(siCustomAchievement, unfinishedArray[i]) end else roundDraw = true - SendStat(siGameResult, loc("Round draw")) + SendStat(siGameResult, GetEngineString("TMsgStrId", sidDraw)) SendStat(siCustomAchievement, loc("Nobody managed to finish the race. What a shame!")) if specialPointsCount > 0 then SendStat(siCustomAchievement, loc("Maybe you should try an easier map next time.")) @@ -609,11 +617,10 @@ ---------------------------------- function onGameInit() - EnableGameFlags(gfInfAttack) + EnableGameFlags(gfInfAttack, gfSolidLand) -- Force-disable various game flags that would break the script DisableGameFlags(gfKing, gfSwitchHog, gfAISurvival, gfPlaceHog, gfTagTeam) CaseFreq = 0 - TurnTime = 90000 WaterRise = 0 HealthDecrease = 0 end @@ -635,6 +642,20 @@ end function onGameStart() + + -- Adjust pre-defined waypoints in scaled drawn maps + if MapGen == mgDrawn and MapFeatureSize ~= 12 and specialPointsCount > 0 then + local landW = RightX - LeftX + 1 + local landH = LAND_HEIGHT - TopY + -- Reposition pre-defined waypoints + for i = 0, (specialPointsCount-1) do + specialPointsX[i] = LeftX + div(specialPointsX[i] * landW, 4096) + specialPointsY[i] = TopY + div(specialPointsY[i] * landH, 2048) + end + -- Scale waypoint size + wpRad = math.max(WAYPOINT_RADIUS_MIN, div(wpRad * landW, 4096)) + end + if ClansCount >= 2 then SendGameResultOff() SendRankingStatsOff() @@ -642,6 +663,11 @@ SendAchievementsStatsOff() end + -- Keep track of land objects that got placed by the scheme (mines, air mines, barrels) + for id, _ in pairs(landObjects) do + table.insert(landObjectPoints, { type = GetGearType(id), x = GetX(id), y = GetY(id) }) + end + SetSoundMask(sndIncoming, true) SetSoundMask(sndMissed, true) @@ -833,8 +859,17 @@ end end - -- Set the waypoints to unactive on new round if gameBegun and not gameOver then + + -- Reset land objects so each player starts with same racing conditions + for id,_ in pairs(landObjects) do + DeleteGear(id) + end + for i=1, #landObjectPoints do + AddGear(landObjectPoints[i].x, landObjectPoints[i].y, landObjectPoints[i].type, 0, 0, 0, 0) + end + + -- Set the waypoints to unactive for i = 0,(wpCount-1) do wpActive[i] = false wpCol[i] = waypointColour @@ -946,7 +981,7 @@ waypointCursor = false end - -- has the player started his tumbling spree? + -- has the player started? if (CurrentHedgehog ~= nil) then --airstrike conversion used to be here @@ -979,7 +1014,7 @@ end - -- if the player has expended his tunbling time, stop him tumbling + -- if the player has expended their time, stop if TurnTimeLeft <= 20 and not turnSkipped then DisableTumbler() end @@ -1022,18 +1057,20 @@ end function onGearAdd(gear) - - if GetGearType(gear) == gtHedgehog then + local gt = GetGearType(gear) + if gt == gtHedgehog then hhs[numhhs] = gear numhhs = numhhs + 1 SetEffect(gear, heResurrectable, 1) - elseif GetGearType(gear) == gtAirAttack then + elseif gt == gtAirAttack then cGear = gear local x,y = GetGearPosition(cGear) SetGearPosition(cGear, 10000, y) - elseif (not gameBegun) and GetGearType(gear) == gtAirBomb then + elseif (gt == gtMine or gt == gtAirMine or gt == gtExplosives) then + landObjects[gear] = true + elseif (not gameBegun) and gt == gtAirBomb then DeleteGear(gear) - elseif GetGearType(gear) == gtRope and TeamRope then + elseif gt == gtRope and TeamRope then SetTag(gear,1) SetGearValues(gear,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,GetClanColor(GetHogClan(CurrentHedgehog))) end @@ -1043,6 +1080,8 @@ if GetGearType(gear) == gtAirAttack then cGear = nil + elseif landObjects[gear] == true then + landObjects[gear] = nil elseif gear == cameraGear then cameraGear = nil end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua Sun Mar 24 14:33:57 2024 -0400 @@ -2,6 +2,7 @@ HedgewarsScriptLoad("/Scripts/Locale.lua") HedgewarsScriptLoad("/Scripts/Tracker.lua") HedgewarsScriptLoad("/Scripts/Params.lua") +HedgewarsScriptLoad("/Scripts/Utils.lua") --[[ Space Invasion @@ -203,7 +204,7 @@ -- hog awards SI.awardRoundScore = nil -- hog with most score in 1 round (min. 50) SI.awardRoundKills = nil -- most kills in 1 round (min. 5) -SI.awardAccuracy = nil -- awarded to hog who didn’t miss once in his round, with most kills (min. 5) +SI.awardAccuracy = nil -- awarded to hog who didn’t miss once in their round, with most kills (min. 5) SI.awardCombo = nil -- hog with longest combo (min. 5) @@ -584,8 +585,10 @@ if lGameOver then local winnerTeam = teamStats[1].name - AddCaption(string.format(loc("%s wins!"), winnerTeam), capcolDefault, capgrpGameState) - SendStat(siGameResult, string.format(loc("%s wins!"), winnerTeam)) + local winText = formatEngineString(GetEngineString("TMsgStrId", sidWinner), winnerTeam) + + AddCaption(winText, capcolDefault, capgrpGameState) + SendStat(siGameResult, winText) for i = 1, TeamsCount do SendStat(siPointType, "!POINTS") @@ -1040,26 +1043,26 @@ function onParameters() parseParams() - if params["rounds"] ~= nil then + if params["rounds"] ~= nil and tonumber(params["rounds"]) then SI.roundLimit = math.floor(tonumber(params["rounds"])) end - if params["barrels"] ~= nil then + if params["barrels"] ~= nil and tonumber(params["barrels"]) then SI.startBarrels = math.floor(tonumber(params["barrels"])) end - if params["pings"] ~= nil then + if params["pings"] ~= nil and tonumber(params["pings"]) then SI.startRadShots = math.floor(tonumber(params["pings"])) end - if params["shield"] ~= nil then + if params["shield"] ~= nil and tonumber(params["shield"]) then SI.startShield = math.min(250-80, math.floor(tonumber(params["shield"]))) end - if params["barrelbonus"] ~= nil then + if params["barrelbonus"] ~= nil and tonumber(params["barrelbonus"]) then SI.barrelBonus = math.floor(tonumber(params["barrelbonus"])) end - if params["shieldbonus"] ~= nil then + if params["shieldbonus"] ~= nil and tonumber(params["shieldbonus"]) then SI.shieldBonus = math.floor(tonumber(params["shieldbonus"])) end - if params["timebonus"] ~= nil then + if params["timebonus"] ~= nil and tonumber(params["timebonus"]) then SI.timeBonus = math.floor(tonumber(params["timebonus"])) end if params["forcetheme"] == "false" then diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/TechRacer.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/TechRacer.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/TechRacer.lua Sun Mar 24 14:33:57 2024 -0400 @@ -470,17 +470,18 @@ local roundDraw = false if #clanScores >= 2 and clanScores[1].score == clanScores[2].score and clanScores[1].score ~= MAX_TURN_TIME then roundDraw = true - SendStat(siGameResult, loc("Round draw")) + SendStat(siGameResult, GetEngineString("TMsgStrId", sidDraw)) SendStat(siCustomAchievement, loc("The teams are tied for the fastest time.")) elseif #sortedTeams >= 1 then - SendStat(siGameResult, string.format(loc("%s wins!"), sortedTeams[1].name)) + + SendStat(siGameResult, formatEngineString(GetEngineString("TMsgStrId", sidWinner), sortedTeams[1].name)) SendStat(siCustomAchievement, string.format(loc("%s wins with a best time of %.1fs."), sortedTeams[1].name, (sortedTeams[1].score/1000))) for i=1,#unfinishedArray do SendStat(siCustomAchievement, unfinishedArray[i]) end else roundDraw = true - SendStat(siGameResult, loc("Round draw")) + SendStat(siGameResult, GetEngineString("TMsgStrId", sidDraw)) SendStat(siCustomAchievement, loc("Nobody managed to finish the race. What a shame!")) SendStat(siCustomAchievement, loc("Maybe you should try an easier TechRacer map.")) end @@ -682,9 +683,10 @@ roundLimit = tonumber(params["rounds"]) - if (roundLimit == 0) or (roundLimit == nil) then + if roundLimit == nil then roundLimit = 3 end + roundLimit = math.max(1, math.floor(roundLimit)) if mapID == nil then mapID = 2 + GetRandom(7) @@ -1047,7 +1049,7 @@ end - -- start the player tumbling with a boom once their turn has actually begun + -- start the player with a boom once their turn has actually begun if racerActive == false then if (TurnTimeLeft > 0) and (TurnTimeLeft ~= TurnTime) then @@ -1076,7 +1078,7 @@ activationStage = 202 end - -- has the player started his tumbling spree? + -- has the player started? if (CurrentHedgehog ~= nil) then -- if the RACE has started, show tracktimes and keep tabs on waypoints diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Multiplayer/WxW.lua --- a/share/hedgewars/Data/Scripts/Multiplayer/WxW.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Multiplayer/WxW.lua Sun Mar 24 14:33:57 2024 -0400 @@ -118,7 +118,7 @@ ABL: All But Last: Players must not only attack the team with the lowest total health KTL: Kill The Leader: If players hit some enemy hedgehog, at least one of them must be a hog from the team with the highest total health. - The ABL and KTL rules exclude each other. If a player breaks the rule (if enabled), he must + The ABL and KTL rules exclude each other. If a player breaks the rule (if enabled), they must skip in the next round. SW false Super Weapons: A few crates may contain very powerful weapons (melon, hellish grenade, RC plane, ballgun) maxcrates 12 Number of crates which can be at maximum in the game (limited to up to 100 to avoid lag) diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/RopeKnocking.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Scripts/RopeKnocking.lua Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,441 @@ +HedgewarsScriptLoad("/Scripts/Locale.lua") +HedgewarsScriptLoad("/Scripts/Utils.lua") + +local hhs = {} +local deadHogs = {} +local missionWon = nil +local endTimer = 1000 +local hogsKilled = 0 +local totalEnemies = 0 +local finishTime +local playerFail = false +local ropeGear = nil +local endGameCalled = false +local missionEndHandled = false +local valkyriesTimer = -1 + +local valkyriesDuration = 20000 +local timeBonus = 6000 +local killBonus = 6000 +local playValkyries = false + +local extraTime + +local playerTeamName +local missionName = loc("Rope-knocking Challenge") +-- Mission type: +-- 0 = none (no special handling) +-- 1 = challenge (saves mission vars) +local missionType = 1 + +local function getKillScore() + return div(hogsKilled * killBonus, totalEnemies) +end + +local function protectEnemies() + -- Protect enemy hogs + for i=1, totalEnemies do + if hhs[i] and GetHealth(hhs[i]) then + SetEffect(hhs[i], heInvulnerable, 1) + SetEffect(hhs[i], heResurrectable, 1) + end + end +end + +local function killStr(killed, total, score) + if total == 16 then + return string.format(loc("You have killed %d of 16 hedgehogs (+%d points)."), killed, score) + else + return string.format(loc("You have killed %d of %d hedgehogs (+%d points)."), killed, total, score) + end +end + +local function gameOver() + StopMusicSound(sndRideOfTheValkyries) + valkyriesTimer = -1 + missionWon = false + SendStat(siGameResult, loc("Challenge over!")) + local score = getKillScore() + SendStat(siCustomAchievement, killStr(hogsKilled, totalEnemies, score)) + SendStat(siPointType, "!POINTS") + SendStat(siPlayerKills, tostring(score), playerTeamName) + protectEnemies() + if not endGameCalled then + EndGame() + endGameCalled = true + end + if missionType == 1 then + -- Update highscore + updateChallengeRecord("Highscore", score) + end +end + +local function victory(onVictory) + missionWon = true + local e = 0 + if extraTime then + e = extraTime + end + local totalTime = TurnTime + e * totalEnemies + local completeTime = (totalTime - finishTime) / 1000 + ShowMission(missionName, loc("Challenge completed!"), loc("Congratulations!") .. "|" .. string.format(loc("Completion time: %.2fs"), completeTime), 0, 0) + PlaySound(sndHomerun) + -- Protect player hog + if hhs[0] and GetHealth(hhs[0]) then + SetEffect(hhs[0], heInvulnerable, 1) + SetEffect(hhs[0], heResurrectable, 1) + end + SendStat(siGameResult, loc("Challenge completed!")) + local hogScore = getKillScore() + local timeScore = div(finishTime * timeBonus, totalTime) + local score = hogScore + timeScore + SendStat(siCustomAchievement, killStr(hogsKilled, totalEnemies, hogScore)) + SendStat(siCustomAchievement, string.format(loc("You have completed this challenge in %.2f s (+%d points)."), completeTime, timeScore)) + SendStat(siPointType, "!POINTS") + SendStat(siPlayerKills, tostring(score), playerTeamName) + SetTeamLabel(playerTeamName, tostring(score)) + SetTurnTimeLeft(MAX_TURN_TIME) + + if missionType == 1 then + -- Update highscore + updateChallengeRecord("Highscore", score) + end + if onVictory then + onVictory() + end +end + +local function knockTaunt() + local r = math.random(0,23) + local taunt + if r == 0 then taunt = loc("%s has been knocked out.") + elseif r == 1 then taunt = loc("%s hit the ground.") + elseif r == 2 then taunt = loc("%s splatted.") + elseif r == 3 then taunt = loc("%s was smashed.") + elseif r == 4 then taunt = loc("%s felt unstable.") + elseif r == 5 then taunt = loc("%s exploded.") + elseif r == 6 then taunt = loc("%s fell from a high cliff.") + elseif r == 7 then taunt = loc("%s goes the way of the lemming.") + elseif r == 8 then taunt = loc("%s was knocked away.") + elseif r == 9 then taunt = loc("%s was really unlucky.") + elseif r == 10 then taunt = loc("%s felt victim to rope-knocking.") + elseif r == 11 then taunt = loc("%s had no chance.") + elseif r == 12 then taunt = loc("%s was a good target.") + elseif r == 13 then taunt = loc("%s spawned at a really bad position.") + elseif r == 14 then taunt = loc("%s was doomed from the beginning.") + elseif r == 15 then taunt = loc("%s has fallen victim to gravity.") + elseif r == 16 then taunt = loc("%s hates Newton.") -- Isaac Newton + elseif r == 17 then taunt = loc("%s had it coming.") + elseif r == 18 then taunt = loc("%s is eliminated!") + elseif r == 19 then taunt = loc("%s fell too fast.") + elseif r == 20 then taunt = loc("%s flew like a rock.") + elseif r == 21 then taunt = loc("%s stumbled.") + elseif r == 22 then taunt = loc("%s was shoved away.") + elseif r == 23 then taunt = loc("%s didn't expect that.") + end + return taunt +end + +local function declareEnemyKilled(gear, onVictory) + if deadHogs[gear] or playerFail then + return + end + deadHogs[gear] = true + hogsKilled = hogsKilled + 1 + + -- Award extra time, if available + if extraTime and extraTime ~= 0 then + SetTurnTimeLeft(TurnTimeLeft + extraTime) + AddCaption(string.format(loc("+%d seconds!"), div(extraTime, 1000)), 0xFFFFFFFF, capgrpMessage2) + end + + SetTeamLabel(playerTeamName, tostring(getKillScore())) + + if hogsKilled == totalEnemies - 1 then + if playValkyries then + PlayMusicSound(sndRideOfTheValkyries) + valkyriesTimer = valkyriesDuration + end + elseif hogsKilled == totalEnemies then + finishTime = TurnTimeLeft + victory(onVictory) + end +end + +--[[ +RopeKnocking function! + +This creates a rope-knocking challenge. +The player spawns with one hog and a rope and must kill all other hogs +by rope-knocking before the time runs out. +The player wins points for each kill and gets a time bonus for killing +all enemies. + +params is a table with all the required parameters. +Fields of the params table: + + MANDATORY: + - map: Map name + - theme: Theme name + - turnTime: Turn time + - playerTeam: Player team info: + { + x, y: Start position + faceLeft: If true, hog faces left + } + - enemyTeams: Table of enemy team tables. each enemy team table has this format: + { + name: Team name + flag: Flag + hogs: Hogs table: + { + x, y: Position + faceLeft: If true, hog faces left + hat: Hat name + name: Hog name + } + } + + OPTIONAL: + - missionName: Mission name + - missionType: + 0: None/other: No special handling + 1: Challenge: Will save mission variables at end (default) + - killBonus: Score for killing all hogs (one hog scores ca. (killBonus/ 0) then + valkyriesTimer = valkyriesTimer - 20 + if valkyriesTimer <= 0 then + StopMusicSound(sndRideOfTheValkyries) + end + end + local drown = (hhs[0]) and (band(GetState(hhs[0]), gstDrowning) ~= 0) + if drown and missionWon == nil then + -- Player hog drowns + playerFail = true + return + end + for i=1, totalEnemies do + local hog = hhs[i] + drown = (hog) and (not deadHogs[hog]) and (band(GetState(hhs[i]), gstDrowning) ~= 0) + if drown then + declareEnemyKilled(hog, params.onVictory) + end + end + + if ropeGear and not missionWon and band(GetState(ropeGear), gstCollision) ~= 0 then + -- Hide mission on first rope attach + HideMission() + end + end + + _G.onGearDamage = function(gear, damage) + + if gear == hhs[0] then + -- Player hog hurts itself + playerFail = true + StopMusicSound(sndRideOfTheValkyries) + valkyriesTimer = -1 + protectEnemies() + end + + if gear ~= hhs[0] and GetGearType(gear) == gtHedgehog and not deadHogs[gear] and missionWon == nil and playerFail == false then + -- Enemy hog took damage + AddVisualGear(GetX(gear), GetY(gear), vgtBigExplosion, 0, false) + DeleteGear(gear) + PlaySound(sndExplosion) + AddCaption(string.format(knockTaunt(), GetHogName(gear)), 0xFFFFFFFF, capgrpMessage) + + declareEnemyKilled(gear, params.onVictory) + end + + end + + _G.onGearAdd = function(gear) + if GetGearType(gear) == gtRope then + ropeGear = gear + end + end + + _G.onGearDelete = function(gear) + + if (gear == hhs[0]) and (missionWon == nil) then + playerFail = true + gameOver() + end + + if GetGearType(gear) == gtHedgehog and gear ~= hhs[0] and not deadHogs[gear] then + declareEnemyKilled(gear, params.onVictory) + end + + if GetGearType(gear) == gtRope then + ropeGear = nil + end + + end + + if params.onAmmoStoreInit then + _G.onAmmoStoreInit = params.onAmmoStoreInit + else + _G.onAmmoStoreInit = function() + SetAmmo(amRope, 9, 0, 0, 0) + end + + _G.onNewTurn = function() + SetWeapon(amRope) + end + end + +end diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Scripts/Utils.lua --- a/share/hedgewars/Data/Scripts/Utils.lua Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Scripts/Utils.lua Sun Mar 24 14:33:57 2024 -0400 @@ -127,6 +127,22 @@ end end +-- Insert parameters %1 to %9 into an engine string and returns the result. +-- * text: engine string with parameters (from GetEngineString) +-- * ...: Arguments to insert into the string. The number of arguments MUST match +-- the number of available arguments of the engine string +-- +-- Example: formatEngineString(GetEngineString("TMsgStrId", sidWinner), "My Team") +-- to create a string showing the winning team. +function formatEngineString(text, ...) + local input = text + for i=1, 9 do + text = string.gsub(text, "%%"..i, "%%s") + end + text = string.format(text, ...) + return text +end + --[[ GLOBAL VARIABLES ]] -- Shared common land color values for land sprites. diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Sounds/dynamitefuse.ogg Binary file share/hedgewars/Data/Sounds/dynamitefuse.ogg has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Sounds/dynamiteimpact.ogg Binary file share/hedgewars/Data/Sounds/dynamiteimpact.ogg has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Themes/Hoggywood/SkyL.png diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/Themes/Underwater/theme.cfg --- a/share/hedgewars/Data/Themes/Underwater/theme.cfg Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/Themes/Underwater/theme.cfg Sun Mar 24 14:33:57 2024 -0400 @@ -15,8 +15,6 @@ object = coral, 3, 10, 193, 38, 32, 2, 128, 66, 66, 94, 39, 0, 88, 167 object = coral2, 3, 119, 146, 23, 22, 1, 5, 0, 123, 130 flakes = 20, 20, 150, 0, 5 -sd-flakes = 40, 20, 150, 0, 5 -; TODO: Use this when rising flakes don't look so strange: -; sd-flakes = 40, 20, 60, 0, -100 +sd-flakes = 40, 20, 60, 0, -100 rq-sky = 0, 70, 210 sd-tint = $a9, $52, $52, $ff diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/misc/hedgewars.png Binary file share/hedgewars/Data/misc/hedgewars.png has changed diff -r 64740eec84ad -r 4c523ed1d35c share/hedgewars/Data/misc/hedgewars.xpm --- a/share/hedgewars/Data/misc/hedgewars.xpm Sun Mar 24 14:05:06 2024 -0400 +++ b/share/hedgewars/Data/misc/hedgewars.xpm Sun Mar 24 14:33:57 2024 -0400 @@ -1,239 +1,516 @@ /* XPM */ -static char *Icon___x__x__[] = { +static char *hedgehog[] = { /* columns rows colors chars-per-pixel */ -"32 32 201 2", -" c #010101", -". c #0A0709", -"X c #1A1A1A", -"o c #454545", -"O c #94005E", -"+ c #AE005D", -"@ c #970063", -"# c #9B0064", -"$ c #94066D", -"% c #9D036A", -"& c #9E086E", -"* c #880873", -"= c #960E74", -"- c #980D73", -"; c #8C1578", -": c #8C1A7D", -"> c #9D1075", -", c #9D177C", -"< c #91187D", -"1 c #9B1C7D", -"2 c #A20065", -"3 c #AA0067", -"4 c #A3046B", -"5 c #A0096F", -"6 c #A20C73", -"7 c #AD0B72", -"8 c #A41276", -"9 c #AD1074", -"0 c #A61479", -"q c #A81C7B", -"w c #7B2083", -"e c #772E8F", -"r c #7D2C8E", -"t c #732F91", -"y c #6B3596", -"u c #653B9B", -"i c #6D3A9B", -"p c #743294", -"a c #783092", -"s c #713899", -"d c #5641A1", -"f c #504BA9", -"g c #5A4EAD", -"h c #4D5AB6", -"j c #5652B0", -"k c #5058B5", -"l c #6444A3", -"z c #7C4CA8", -"x c #7654B2", -"c c #4363BF", -"v c #3D67C3", -"b c #3A69C5", -"n c #3C73CE", -"m c #2C77D1", -"M c #237CD6", -"N c #2C78D2", -"B c #2F7ED8", -"V c #3E7BD5", -"C c #5F71CC", -"Z c #6F6ECA", -"A c #627FD6", -"S c #6C7ED6", -"D c #617ED9", -"F c #6F7ED9", -"G c #881F82", -"H c #A11F83", -"J c #AC1D82", -"K c #872386", -"L c #8A2387", -"P c #8D2689", -"I c #8A2C8F", -"U c #952388", -"Y c #812F91", -"T c #873595", -"R c #8D3192", -"E c #8A3799", -"W c #84399A", -"Q c #AE2285", -"! c #A32488", -"~ c #A92D8D", -"^ c #B1258A", -"/ c #B32C8C", -"( c #BD298E", -") c #B5308E", -"_ c #B72E91", -"` c #BE2C91", -"' c #BA3495", -"] c #BA3895", -"[ c #BC3C9A", -"{ c #BF3BA0", -"} c #C32E93", -"| c #C23391", -" . c #CF3D9D", -".. c #C13DA2", -"X. c #CA3BA0", -"o. c #BE419C", -"O. c #8148A4", -"+. c #8152B0", -"@. c #C0439E", -"#. c #C345A3", -"$. c #C54EA5", -"%. c #CE4AA3", -"&. c #C645AA", -"*. c #C74EA9", -"=. c #C94AAE", -"-. c #D045AA", -";. c #D04BAF", -":. c #C750A8", -">. c #CC59AB", -",. c #CB4DB1", -"<. c #CC51B4", -"1. c #CD5AB0", -"2. c #D45CBF", -"3. c #D363BA", -"4. c #D65FC3", -"5. c #D769C0", -"6. c #DB6CC3", -"7. c #DE66C9", -"8. c #DE6BCE", -"9. c #DB74C5", -"0. c #DE76CB", -"q. c #DF6DD0", -"w. c #E068CC", -"e. c #EA7FCF", -"r. c #E06FD1", -"t. c #E274D5", -"y. c #E47BD6", -"u. c #E376D8", -"i. c #E479D9", -"p. c #E87FDE", -"a. c #1E83DC", -"s. c #2482DB", -"d. c #2B80DA", -"f. c #3682DC", -"g. c #1B8BE3", -"h. c #3187E0", -"j. c #3D8BE4", -"k. c #2790E8", -"l. c #3594EC", -"z. c #3D94EC", -"x. c #4C90EA", -"c. c #5494EB", -"v. c #469CF4", -"b. c #4C98F1", -"n. c #5A97F1", -"m. c #529AF2", -"M. c #5A9FF8", -"N. c #6B87E1", -"B. c #49A1F9", -"V. c #59A3F6", -"C. c #52A6FE", -"Z. c #5DA7FE", -"A. c #56AFFF", -"S. c #5CABFF", -"D. c #56B4FF", -"F. c #58B5FF", -"G. c #54BBFF", -"H. c #898989", -"J. c #8A918D", -"K. c #949394", -"L. c #9C9B9C", -"P. c #B5B4B5", -"I. c #E483D2", -"U. c #E789D6", -"Y. c #E781DA", -"T. c #E985DC", -"R. c #EA89DC", -"E. c #EC91DE", -"W. c #EE9BDA", -"Q. c #EC87E2", -"!. c #EC8CE2", -"~. c #EE93E1", -"^. c #F195E6", -"/. c #F29BE7", -"(. c #F49EE9", -"). c #F4A6E5", -"_. c #F5A2EB", -"`. c #F9A6EF", -"'. c #F6AAED", -"]. c #F5B2EC", -"[. c #F6A2F0", -"{. c #FAA6F1", -"}. c #FAAAF3", -"|. c #FBB7F5", -" X c #FEB1F8", -".X c #C8C7C8", -"XX c #CBCBCC", -"oX c #CDD1CF", -"OX c #F3D2EB", -"+X c #F7C6F0", -"@X c #F8C1F1", -"#X c #F8CAF2", -"$X c #FFD1FC", -"%X c #FDDBFB", -"&X c #F9E6F7", -"*X c #FDE5FA", -"=X c #FDEDFB", -"-X c #F3FCF5", -";X c #FCF3FA", -":X c #FEFEFE", -">X c None", +"254 256 254 2 ", +" c #002100210021", +". c #0B970B970B97", +"X c #122212221222", +"o c #18FF18FF18FF", +"O c #228422842284", +"+ c #2A582A582A58", +"@ c #33AD33AD33AD", +"# c #3DF33DF33DF3", +"$ c #433943394339", +"% c #4B824B824B82", +"& c #54FB54FB54FB", +"* c #5BDF5BDF5BDF", +"= c #644064406440", +"- c #6A5E6A5E6A5E", +"; c #755B755B755B", +": c #7D447D447D44", +"> c #9F9F080C6E72", +", c #98040F827521", +"< c #9C450C147214", +"1 c #8FBC17977C4B", +"2 c #8DBA197A7E00", +"3 c #964D111E768C", +"4 c #9B13111D76AA", +"5 c #927F14CA79DB", +"6 c #993215387A6B", +"7 c #9718198D7E60", +"8 c #A05909856F8C", +"9 c #A1BF0C3C71C5", +"0 c #A45A124875AF", +"q c #A5D717F877BB", +"w c #A6C114D079B7", +"e c #A83C174C7BF3", +"r c #A72E1B7B79D8", +"t c #A9891B1D7DA0", +"y c #A9DC225F7DE4", +"u c #7FAF27178A56", +"i c #7C112AB08D5F", +"p c #775E2F299164", +"a c #78282E7390B1", +"s c #6FDB366897F0", +"d c #6F543714987A", +"f c #671B3F0A9F87", +"g c #6B4C3B1B9C07", +"h c #72FB33769543", +"j c #5F3546BAA66A", +"k c #5DA64848A7D1", +"l c #56F94EB7AD90", +"z c #5A5D4B93AABE", +"x c #54EC50D0AF6F", +"c c #4ECC56BCB4BB", +"v c #4C5A592AB6E2", +"b c #468F5EB6BBD4", +"n c #49EE5C2FB998", +"m c #52155390B1DF", +"M c #509F5E00BBA6", +"N c #62F44321A32F", +"B c #750A4345A3C7", +"V c #7EB34BA4ABE0", +"C c #7A4454C4B42F", +"Z c #769558E9B7E9", +"A c #76485CEFBB90", +"S c #439761A5BE71", +"D c #746360BCBF22", +"F c #3E5B66B1C2F2", +"G c #3AFC6A0BC5F3", +"H c #368A6E63C9D7", +"J c #38026CF8C88D", +"K c #2F62755BD00F", +"L c #320272C8CDBD", +"P c #2DB176F9D17D", +"I c #2C357880D2D9", +"U c #30D67D58D777", +"Y c #405D64BDC137", +"T c #56A0753ED102", +"R c #6DC26DC5CAE5", +"E c #7237652BC32A", +"W c #707668F7C69E", +"Q c #6C0871E6CEB6", +"! c #6A487553D1CE", +"~ c #67957B00D6F3", +"^ c #684D79A6D5B1", +"/ c #661B7E31D9EE", +"( c #87F01F4F831C", +") c #89AD1D6D8189", +"_ c #952C1D7B8204", +"` c #AB5D1D1A81A2", +"' c #84CE222185C0", +"] c #80DD2606892D", +"[ c #8EDA2A638DAC", +"{ c #9339218D85A7", +"} c #915325608924", +"| c #8CDA2E889166", +" . c #8A83335D95D5", +".. c #8867378599CA", +"X. c #86583BEF9DA2", +"o. c #ADFB227D8466", +"O. c #AD892BD68377", +"+. c #AF6324668957", +"@. c #B09B26548736", +"#. c #B0AD26AE8AB3", +"$. c #B2D52A7D8CEE", +"%. c #AF9D31338698", +"&. c #B069333E87C9", +"*. c #B27F34798AA8", +"=. c #B3C93BE28CDF", +"-. c #B45D2D5A91D5", +";. c #B6FC31FC942E", +":. c #B918357393AE", +">. c #BB7339A19743", +",. c #B7F133FA982E", +"<. c #B93D360C9A28", +"1. c #BC213B289CB9", +"2. c #84B23F49A0CC", +"3. c #BD843DC8A171", +"4. c #B59C40A58FA0", +"5. c #B70E445A91D5", +"6. c #B9C84B5895F4", +"7. c #B9E54BA6961E", +"8. c #BF8640E79CEF", +"9. c #BCD7532D9A88", +"0. c #BFA95A729ED1", +"q. c #830542C9A3D2", +"w. c #809A47B2A845", +"e. c #803E486EA91A", +"r. c #BF3940DCA46F", +"t. c #C11743C49F34", +"y. c #C0615C629FEA", +"u. c #C1A044E6A3FC", +"i. c #C5754B88A59B", +"p. c #C26546A7A9E1", +"a. c #C4F04B2AAD15", +"s. c #C12D5E56A11E", +"d. c #C8265063A992", +"f. c #C9FB53A0AC71", +"g. c #CC6357FBAFB6", +"h. c #C6C54E93B167", +"j. c #C7EC509EB34C", +"k. c #C9D25400B66A", +"l. c #CE8D5BB4B2D6", +"z. c #CB4A5698B909", +"x. c #CD825AA8BCD7", +"c. c #D05D5F0DB58C", +"v. c #C3856467A4AA", +"b. c #C5556936A770", +"n. c #C7216DBAAA21", +"m. c #C9CB7487AE1F", +"M. c #D1346084B6BA", +"N. c #D3A864D2BA52", +"B. c #D68B6A04BE86", +"V. c #CD177D15B327", +"C. c #CFDB5ED9C0D5", +"Z. c #D2B56400C5B8", +"A. c #D7E16C67C08B", +"S. c #D9216E83C23A", +"D. c #D48B674BC8E2", +"F. c #D6D46B68CCC7", +"G. c #D8C96EDBCEB1", +"H. c #DBA77316C5F6", +"J. c #DB51730ACD4B", +"K. c #DF6B79BBCB64", +"L. c #DC2574BFD1ED", +"P. c #DF547A53D4F6", +"I. c #E1C27DEFCED5", +"U. c #E1C27E72D681", +"Y. c #351781DBDBBE", +"T. c #38B1858ADF4D", +"R. c #3A9E87A5E146", +"E. c #3D6B8A80E403", +"W. c #637E8369DEA6", +"Q. c #40DE8E2DE77B", +"!. c #42218F90E8D3", +"~. c #5F2A8C65E6CA", +"^. c #5DF68ED2E908", +"/. c #456392EFEC01", +"(. c #48B6967BEF58", +"). c #532494C7EE2D", +"_. c #5BDB9325ECEB", +"`. c #49E497AEF09D", +"'. c #4D009AEEF39F", +"]. c #59AF978FF0FB", +"[. c #53FC9E6AF712", +"{. c #58729A3BF351", +"}. c #5468A007F88F", +"|. c #61E086A2E196", +" X c #605E89F3E48C", +".X c #5450A297FAF0", +"XX c #841A841A841A", +"oX c #8C3C8C3C8C3C", +"OX c #941294129412", +"+X c #9A049A049A04", +"@X c #A3A8A3A8A3A8", +"#X c #ACA1ACA1ACA1", +"$X c #B62DB62DB62D", +"%X c #BD03BD03BD03", +"&X c #CF28824CB63A", +"*X c #D06D8591B80A", +"=X c #D0E086CBB8D2", +"-X c #D3148C68BC24", +";X c #D5079183BF22", +":X c #D6D29611C1D3", +">X c #D8029900C383", +",X c #D9CA9DB6C64B", +"X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>X= & >X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>Xp v : >X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>Xr k.f.u 4 >X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>Xt z.Z.l.c : 3 >X>X>X>X>X>X>X>X>X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>Xy l.Z.Z.B.h.l > >X>X>X>X>X>X>X>X>X>X>X>X>X", -">X>X>X>X4 >X>X>X>X>X>Xt j.Z.A.S.n.C f * % >X>X>X>X>X>X>X>X>X>X>X", -">X>X>X4 l g l i p r K : T P L R ~ ] [ ] ^ 8 O >X>X>X>X>X>X>X>X>X", -">X>X>X>Xs g.s.s.s.M M s.d 8 %.I.(._.{.`._.E.3.Q # >X>X>X>X>X>X>X", -">X>X>X>X4 D C.m.m.b.v.p ' E.}._.(././.(./._.`._.1.& >X>X>X>X>X>X", -">X>X>X>X>XT A.Z.D.D.W .}._././.(._.(.(././.^./.}.6.6 >X>X>X>X>X", -">X>X>X>X>X2 A D.N.W ( _._./.(.(.(.(.(./.'.+X@X_.].$X9.# >X>X>X>X", -">X>X>X>X>X>X1 E ; 7 i._.(.(.(.(.(._./.].:X:X:X;X:X:X:X$.>X>X>X>X", -">X>X>X>X4 < i b y -.!.(.(.(._.(.(.(./.*X:X:X:X:X:X:X-XOX6 >X>X>X", -">X>X6 r k m a.m ! 7.!._.(.(.(.(.(./._.:X:X:XL.P.:XH. XX>.>X>X>X", -">X* n a.M B z.C ( t.R._.(.(.(.(.(./.'.;X:X.X X :Xo J.W.# >X>X", -">X# a V v.V.G.z X.t.R._./._.(./.(.(.(.=X:X.X X :XK.. oX).% >X>X", -">X>X>X0 F G.S 2 ,.t.Y._./.(.(._.(._.^.#X:X:XK.P.:X:X-X=X~.8 >X>X", -">X>X>X>X4 x I w ,.u.y.(.(._.(.(._.(.(./.&X:X:X:X=X+X%X'.U.) ] >X", -">X>X>X>X>X+ h f .t.7.~.(./.(.(./.(.(.(._.#X&X*X|.{.3.U.U.] ^.q ", -">X>X>X>X>Xp g.h ` i.t.Q.}._./.(.(.(.(.(./.(.~.6.3.@.$.}.3.#. X) ", -">X>X>X>X: m d.c.! 7.,...3.E.`./.(._.(.(./._./.| @.r._.}.' 3.U.q ", -">X>X>X4 c M x.F.O.7 [ :.o.Q 3.{./.(.(._.(./._.{. X_._.R.^ r.J >X", -">X>X4 j g.z.F.V.U 6.}.}.}.E.Q 9.`./.(./.(.(.(././.).[.] 8 6 >X>X", -">X>X- l g Z N.x } [.(././.}.9._ (.(.(._._.(.(.(.^.^.,.% >X>X>X>X", -">X>X>X>X>X4 4 2 &.Q.(.(./._.T.Q t.y.R.~.~.~.Q.Q.Y.=.- >X>X>X>X>X", -">X>X>X>X>X>X>X>X^ u.R./.(.{.*.[ i.q.8.8.q.r.i.q.' & >X>X>X>X>X>X", -">X>X>X>X>X>X>X>X% _ 4.r.8.#.& ,.q.t.t.t.q.4.| J | 0 >X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X6 0 6 - &.' Q ^ ^ ^ Q / *.U. X[ >X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>X>X>X6 q.!.~.I.U.*.] }.}._.}.@.>X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>X>X>X>X' u.Q.^.~.Q &.Q.~.(.r.8 >X>X>X>X>X>X", -">X>X>X>X>X>X>X>X>X>X>X>X>X>X% ^ ,.&.J % 0 ..,...6 >X>X>X>X>X>X>X" +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXzXs.y > q 5.;XFXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXm.< > > > > > > r >XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXm.> > > > > > > > > > 5.cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXwX8 > > > > > > > > > > > q ;XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX9.> > > > > > > > > > > > > > =.xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXFXr > > > > > > > > > > > > > > > 0 &XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcX> > > > > > > > > > > > > > > > > > =.zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXwX> > > > > > , s < > > > > > > > > > > q V.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqX> > > > > > 5 L b ( > > > > > > > > > > > &.kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX8X> > > > > > 2 K P L f < > > > > > > > > > > 0 m.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX8X> > > > > > 2 K I I I S ' > > > > > > > > > > > %.qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX7X> > > > > > 2 P I I I P K N , > > > > > > > > > > 9 n.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX,X> > > > > > ( I I I I I I I Y ' > > > > > > > > > > > O.9XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX>X> > > > > > ( I I I I I I I I K k < > > > > > > > > > > 9 v.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX:X> > > > > > ' P I I I I I I I I I F u > > > > > > > > > > > y 8XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX;X> > > > > > ' I I I I I I I I I I I K z 3 > > > > > > > > > > 9 0.AXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX*X> > > > > > u I I I I I I I I I I I I I F i 8 > > > > > > > > > > r 8XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&X> > > > > > i I I I I I I I I I I I I I I P l , > > > > > > > > > > > 9.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXV.> > > > > > i I I I I I I I I I I I I I I I I J a > > > > > > > > > > > q 6XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXm.> > > > > > p I I I I I I I I I I I I I I I I I I x 5 > > > > > > > > > > > 6.bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXn.> > > > > > p I I I I I I I Y.Q.U I I I I I I I I I H a < > > > > > > > > > > q >XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXn.> > > > > > s I I I I I I I Q..X.XE.I I I I I I I I I I m 1 > > > > > > > > > > > 6.cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXv.> > > > > > d I I I I I I I /..X.X.X(.Y.I I I I I I I I I H h > > > > > > > > > > > q ;XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXs.> > > > > > s I I I I I I I /..X.X.X.X.XE.I I I I I I I I I P c 1 > > > > > > > > > > > 4.cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX0.> > > > > > g I I I I I I I /..X.X.X.X.X.X'.Y.I I I I I I I I I L s < > > > > > > > > > > 0 -XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX9.> > > > > > g I I I I I I I /..X.X.X.X.X.X.X.XQ.I I I I I I I I I K n 2 > > > > > > > > > > > =.lXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX6.> > > > > > f I I I I I I I /..X.X.X.X.X.X.X.X.X'.Y.I I I I I I I I I L g < > > > > > > > > > > 0 &XKXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX6.> > > > > > j I I I I I I I (..X.X.X.X.X.X.X.X.X.X.XQ.I I I I I I I I I I b 2 > > > > > > > > > > > *.kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX5.> > > > > > j I I I I I I I (..X.X.X.X.X.X.X.X.X.X.X.X'.T.I I I I I I I I I L g < > > > > > > > > > > 0 m.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX5.> > > > > > k I I I I I I I (..X.X.X.X.X.X.X.X.X.X.X.X.X.X!.I I I I I I I I I I S ' > > > > > > > > > > > %.kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX=.> > > > > > z I I I I I I I (..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.T.I I I I I I I I I L f , > > > > > > > > > > 9 n.FXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX%.> > > > > > l I I I I I I I '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.!.U I I I I I I I I I S ' > > > > > > > > > > > O.qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX%.> > > > > > l I I I I I I I '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X'.T.I I I I I I I I I K j , > > > > > > > > > > > b.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXO.> > > > > > m I I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X/.U I I I I I I I I I S ] > > > > > > > > > > > y 0XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXO.> > > > > > c I I I I I I I '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.R.I I I I I I I I I I k , > > > > > > > > > > 9 v.AXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXy > > > > > > v I I I I I I U '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X(.I I I I I I I I I I F u > > > > > > > > > > > y 8XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXr > > > > > > n P I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.R.I I I I I I I I I K z , > > > > > > > > > > > 0.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKXr > > > > > > b I I I I I I I '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X(.U I I I I I I I I P H i > > > > > > > > > > > r 7XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX0 > > > > > > b I I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XE.I I I I I I I I I P l 3 > > > > > > > > > > > 9.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX0 > > > > > > F I I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X(.Y.I I I I I I I I I H p > > > > > > > > > > > q ,XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX> > > > > > > F I I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.E.I I I I I I I I I I m 5 > > > > > > > > > > > 6.bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXFX> > > > > > > F I I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X(.Y.I I I I I I I I I H h > > > > > > > > > > > q ;XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDX> > > > > > > G I I I I I I I .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XE.I I I I I I I I I P m 1 > > > > > > > > > > > 6.cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXAX> > > > > > < G I I I I I I U .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X(.Y.P I I I I I I I I H s < > > > > > > > > > > 0 -XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbX> > > > > > < H I I I I I I U .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XQ.I I I I I I I I I I c 1 > > > > > > > > > > > =.zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX7X0.*.%.%.=.9.v.m.&X:X7XqXzXcXbXAXKXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcX> > > > > > , J I I I I I I U .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X`.T.I I I I I I I I I H g < > > > > > > > > > > q *XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqXO.> > > > > > > > > > > > > > > 0 q y O.*.6.0.n.V.;X,X9XkXcXcXZXDXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcX> > > > > > , H P I I I I I Y..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XE.I I I I I I I I I I n 2 > > > > > > > > > > > *.zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX7X9 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 9 q r y %.=.6.v.n.=X:X8XqXzXcXbXAXKXPXPXPXPXPXPXPXPXPXxX> > > > > > 3 H I I I I I I U .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[._.~./ b m x z z z z z l x l ' > > > > > > > > > > > q V.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbXq > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > < r r O.=.6.0.n.&X-X,X0X:X> > > > > > 5 H I I I I I P Y..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.~.! Z V .{ 6 4 < > > > > > > > > > > > > > > > > > > > > > > > > > > &.kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXn.> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > i h g f k l c Y |.^.].]..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[. XR w.| 7 > > > > > > > > 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > 0 m.FXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXy 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 4 4 6 _ [ ..q.V V A W Q ~ W.~._.]..X.X.X.X.X.X~.W X._ > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > O.8XFXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX8 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 > > > > > > > > > > > > 9 4 4 w } | X.q.} < 8 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 q 6.:XbXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX> > > > > > > s l j f s a u ' 2 5 < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 9 =.-XZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXy > > > > > > ' K I I I I I P P P K L G S v x z j g h a ' 2 5 , > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 8 > 9 5.8XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX0.> > > > > > > m P I I I I I I I I I I I I I I I I I I I P P P K J F n m l j f s a u ' 2 3 < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 8 > 8 0 r o.o.@.@.@.@.o.` w 9 8 > 8 8 > > > 8 > > > > > > > > > > > > > > > > > > > r m.bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXwX> > > > > > > 2 K I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I P H G b v m j f g h i ' 2 5 < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > o.:.i.c.S.I.4XyXyXyXyXyXyXyXyXyXyXyXyXuXrX3XI.B.g.t.-.e > > > > > > > > > > > > > > > > > > 8 8 8 8 9.kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX5.> > > > > > > z P I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I L G S n m l j g s a ] 2 2 , < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > 9 @.t.N..r 9 8 8 8 8 > > > > > > > > > > > > > 8 4.0XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXkX> > > > > > > 2 L I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I K H F S n x k s 3 > > > > > > > > > > > > > > > > > 9 @.i.K.rXyXiXuXtXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4XB.1.r 8 > > > > > > > > > > > > > > 8 8 4.0XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX9.> > > > > > > j I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I K j 5 > > > > > > > > > > > > > > > 0 :.N.4XyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.f.@.9 8 > 8 > > > > > > > > > > 8 8 5.kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcX> > > > > > > 3 J I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I K n 2 > > > > > > > > > > > > > > 0 :.N.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > > > > > > > > 8 8 8 y.bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXs.> > > > > > > g I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I F a > > > > > > > > > > > > > > t g.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.t.q 8 > > > > > > > > > > > > 0 =XKXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbX4 > > > > > > 3 G I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I P I I I I I I I I I I I I I I I I I I I I I I I I I I I I K j , > > > > > > > > > > > > 9 :.H.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4Xc.o.8 > > > > > > > > > > > > %.wXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXm.> > > > > > > s I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I S ' > > > > > > > > > 8 > > 0 t. > > > > > > > > > 9 n.FXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZXr > > > > > > < Y I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I K f < > > > > > > > > > > < 0 u. > > > > > > > > > > O.qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&X> > > > > > > p I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I F ' > > > > > > > > > > > 9 8.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXH.$.8 > > > > > > > > > > 8 V.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDXy > > > > > > > T R.T.T.Y.Y.U U I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I P m 3 > > > > > > > > > > 8 :.I.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXB.t > > > > > > > > > > 8 =.bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX;X> > > > > > > ..}..X.X.X.X.X[.'.'.'.`.(.!.!.E.E.T.T.T.U Y.I I U I I I I I U I I I I I I I I I I I I I I I I I I I I I I I L g > > > > > > > > > > > ` S.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXf.9 8 8 > > > > > > > > r 9XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX%.> > > > > > > ^ .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X'.[.'.`.(./.!.E.E.T.T.Y.Y.U U I I I I I I I I I I I I I I I I H ] > > > > > > > > > > 9 d.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX1X:.8 > > > > > > > > > 9 &XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX6X> > > > > > > [ .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X'..X.X[.'.'.`././.!.E.R.R.T.Y.U I I S 2 > > > > > > > > > > @.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXM.0 8 8 > > > > > > > > 0.FXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX*.> > > > > > > Q .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XQ 3 > > > > > > > > > 9 h.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXyX1X;.8 8 > > > > > > > > =.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX8X> > > > > > > { .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XD < > > > > > > > > > +.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXl.9 > > > > > > > > > O.xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX5.> > > > > > > W .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XZ > > > > > > > > > > >.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.t 8 > > > > > > > > r lXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXwX> > > > > > > 7 ]..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.Xe.> > > > > > > > > < g.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5X:.> > > > > > > > > q qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX9.> > > > > > > A .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X].V > > > > > > > > > q H.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXd.8 > > > > > > > > 0 0XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXzX9 > > > > > > 6 ]..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XV > > > > > > > > > o. > > > > > > 0 0XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXs.> > > > > > > C .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.e.< > > > > > > > > $.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXK.q > > > > > > > > 0 qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcXq > > > > > > 4 _..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XZ 8 > > > > > > > > :.rXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > > > > > 0 lXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXn.> > > > > > > V .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XD > 8 8 > > > > > > -.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX1Xo.> > > > > > > > r cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZXq > > > > > 8 < ~..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XQ > > > > > > > > > -.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4Xo.> > > > > > > > y ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXV.> > > > > > 8 q..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XW.3 > > > > > > > < ,.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4Xo.> > > > > 8 8 8 =.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDXr > > > > > > < W..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[..X^._ > > > > > > > 8 -.D.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX1Xo.8 > > > > > > > y.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX-X> > > > > > > ...X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.[.[ > > > > > > > > -.D.G.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtX1X` > > > > > > > > =XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKXO.> > > > > > < / .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X2.< > > > > > > > +.D.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.e 8 > > > > > > 8 qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX>X> > > > > 8 8 | .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XW > > > > > > > > ` Z.F.G.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXH.9 > > > > > > > y bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX%.> > > > > > > Q .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.Q 4 > > > > > > > 0 x.F.F.L.yXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXM.8 8 > > > > > > 5.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX8X> > > > > > > } {..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X~.A { 9 > > > > > > > 8 k.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXuXyXyXyXuXuXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXyXyXyXyXyXyXyXtXf.8 > > > > > > 8 &XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX=.> > > > > > > W .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.! X.4 > > > > > > > > > > p.F.F.F.G.tXiXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXsXMXGXHXLXLXLXLXGXNXnXpXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXtXyXiXpXpXpXpXiXyXyXyXyXyXyXyXtX:.> > > > > > 8 9 kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXwX> > > > > > > { ]..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X^.Z _ 8 > > > > > > > > > > > -.F.F.F.F.U.yXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXsXBXLXPXPXPXPXPXPXPXPXPXPXPXPXHXMXpXyXuXtXyXyXyXyXyXyXyXyXyXyXiXnXGXLXLXPXPXPXLXGXnXpXyXyXyXyXtX5Xt > > > > > 8 8 O.FXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX5.> > > > > > > A .X.X.X.X.X.X.X.X.X.X.X.X.X[.Q ..9 8 > > > > > > > > > > > > ` Z.F.F.F.G.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXuXuXyXaXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXGXaXuXyXyXyXyXyXyXtXuXuXMXLXPXPXPXPXPXPXPXPXPXPXLXGXpXyXyXyXyXI.8 8 > > > > > 8 m.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX> > > > > > > 7 ]..X.X.X.X.X.X.X.X.X.X^.C _ > > > 8 > > > > > > > > > > 8 8 C.F.F.F.F.L.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXuXiXSXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXBXiXyXyXyXyXuXuXaXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXNXiXyXyXyXf.> > > > > > 8 8 kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXy.> > > > > > > Z .X.X.X.X.X.X.X[.Q .> > > > > > > > > > > > > > > 8 8 > 3.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXsXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXmXyXyXyXyXdXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXaXyXyXyX-.8 > > > > > > =.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXvX8 > > > > > > 6 _..X.X.X.X XC 5 > > > > > > > > > > > > > > > > > > 8 +.D.F.F.F.F.G.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXnXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXBXiXyXsXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXdXyXyX1X9 > > > > > 8 8 ;XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXb.> > > > > > < V .X].R | 8 8 > > > > > > > > > > > > > > > > > > > 9 C.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXmXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXHXsXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXnXyXyXc.> > > > > > > q ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZX0 > > > > > > 4 e.6 > > 8 8 > > > > > > > > > > > > > > > > > > > 3.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXdXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXnXyXyX$.> > > > > > > v.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXm.> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > ` D.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXaXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXsXuX > > > 0 zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDXr > > > > > > > > > > > > > > > > > > > , p l 4 > > > > > > 8 z.F.F.F.F.F.F.F.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXaXtXi.8 8 > > > > > 6.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXFX6Xy 8 > > > > > > > > > > > > > > > > 2 k J P s > > > > > > > -.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXHXiX5Xr > > > > > > 9 kXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXzXn.r > > > > > > > > > > > > > > > > , h b P I I G 3 > > > > > > > C.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXsXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXNXyXM.> > > > > > > 6.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDX,X4.8 > > > > > > > > > > > > > > > > ( k H I I I I I s > > > > > > > <.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXuXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXaXrX@.> > > > > > 9 wXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXlXb.r > > > > > > > > > > > > > > > > 3 s b P I I I I I I G , > > > > > > w Z.F.F.F.F.F.F.F.G.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXsXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXHXiXB.> > > > > > > 9.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXAX:X*.8 > > > > > > > > > > > > > > > < ' z H I I I I I I I I P f > > > > > > > 3.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXmXyX@.8 8 8 > > > > zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXkXv.r > > > > > > > > > > > > > > > > 3 s b P I I I I I I I I I I L 3 > > > > > > w Z.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXaXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXyXS.8 8 8 > > > > v.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZX;X&.8 > > > > > > > > > > > > > > > < ' l H I I I I I I I I I I I I I z > > > > > > < 1.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXNXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXmXtX@.> 8 > > > > 0 ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXwX0.q > > > > > > > > > > > > > > > > 5 d S I I I I I I I I I I I I I I I P ( > > > > > > 9 C.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXHXuXN.> 8 > > > > > &XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZX-X%.> > > > > > > > > > > > > > > > > ] l L I I I I I I I I I I I I I I I I I b > > > > > > > -.F.F.F.F.F.F.F.F.F.G.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXaXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXaXrXt 8 > > > > > O.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXqX9.0 > > > > > > > > > > > > > > > > 5 g F I I I I I I I I I I I I I I I I I I I I h > > > > > > 8 C.F.F.F.F.F.F.F.F.F.G.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXMXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXSXyXf.> > > > > > > qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZX&XO.> > > > > > > > > > > > > > > > > u x L I I I I I I I I I I I I I I I I I I I I I L , > > > > > > +.F.F.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXuXyXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXJX#X& + + $ XXVXPXPXPXPXPXPXPXPXLXiX4X0 > > > > > > 0.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXLX9X9.< > > > > > > > > > > > > > > > > 5 N F K I I I I I I I I I I I I I I I I I I I I I I I l > > > > > > > a.F.F.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXuXiXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXfX+ X oXPXPXPXPXPXPXPXPXnXyX>.> > > > > > 0 DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXZX&Xo.> > > > > > > > > > > > > > > > > i m K I I I I I I I I I I I I I I I I I I I I I I I I I P ] > > > > > > w D.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXpXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX#X. = PXPXPXPXPXPXPXBXyXH.> > > > > > > ,XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXLX8X6.> > > > > > > > > > > > > > > > > 1 f G I I I I I I I I I I I I I I I I I I I I I I I I I I I I H < > > > > > > ,.F.F.F.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXsXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXgXOX; : @XCXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXgX. ; PXPXPXPXPXPXLXiXrXy > > > > > > 6.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXbXV.y > > > > > > > > > > > > > > > > < i c I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I l > > > > > > > k.F.F.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXMXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXfX@ . = VXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXJX@ %XPXPXPXPXPXPXaXyXi.> > > > > > 0 DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXwX6.> > > > > > > > > > > > > > > > > 1 f G I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I i > > > > > > ` D.F.F.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXNXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXXX o %XPXPXPXPXPXPXPXPXPXPXPXPXPXPX+X @ PXPXPXPXPXPXnXyXI.8 > > > > > > 7XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXDX0.> > > > > > > > > > > > > > > > < i c K I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I P < > > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX: X %XPXPXPXPXPXPXPXPXPXPXPXPXLX@ $XPXPXPXPXPXBXyXrX` > > > > > > 0.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXAX%.> > > > > > > > > > > > > > > 1 j J I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I P v 8 > > > > > > k.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXSXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX#X o VXPXPXPXPXPXPXPXPXPXPXPXhX * PXPXPXPXPXGXyXyXi.> > > > > > r LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PX6.> > > > > > > > > > > > > , a v K I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I T.B > > > > > > w Z.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXGXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXJXO & LXPXPXPXPXPXPXPXPXPXPXOX o PXPXPXPXPXLXiXyXK.8 > > > > > > zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"qX> > > > > > > > > > > > 2 k J I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I U R.'..X} > > > > > > -.D.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXGXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXoX fXPXPXPXPXPXPXPXPXPXPX= jXPXPXPXPXLXiXyX5Xq > > > > > > &XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"v.> > > > > > > > > 5 p n K I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I U T./.[..X.X].> > > > > > > p.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXGXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX+ * PXPXPXPXPXPXPXPXPXPX% %XPXPXPXPXPXpXyXyX:.> > > > > > 6.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"&.> > > > > > > 2 k J I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I Y.E.'..X.X.X.X.XQ > > > > > > 9 C.F.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXgX o CXPXPXPXPXPXPXPXPXPX$ #XPXPXPXPXPXaXyXyXl.> > > > > > q LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"O.> > > > > > 2 F P I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I U T./..X.X.X.X.X.X.X.XC > > > > > > ` Z.F.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXNXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXOX $XPXPXPXPXPXPXPXPXPX% #XPXPXPXPXPXaXtXuXI.8 > > > > > > xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"7.> > > > > > > 2 n I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I Y.E.'..X.X.X.X.X.X.X.X.X.X| > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXMXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX= XXPXPXPXPXPXPXPXPXPX= %XPXPXPXPXPXsXyXuX5Xw > > > > > > >XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +";X> > > > > > > > < N L I I I I I I I I I I I I I I I I I I I I I I I I I I I I I U T.(..X.X.X.X.X.X.X.X.X.X.X.X.X6 > > > > > > 3.D.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXnXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX% = PXPXPXPXPXPXPXPXPXoX jXPXPXPXPXPXsXyXyXyX;.8 8 > > > > b.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"FXr > > > > > > > > > u G I I I I I I I I I I I I I I I I I I I I I I I I I I Y.Q.'..X.X.X.X.X.X.X.X.X.X.X.X.X.X~.> > > > > > 8 C.D.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXaXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX@ % PXPXPXPXPXPXPXPXPXfX o LXPXPXPXPXPXaXyXyXyXi.8 8 > > > > *.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PX8X< 8 > > > > > > > > 5 m I I I I I I I I I I I I I I I I I I I I I I I R.`..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XQ > > > > > > 9 Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX# % PXPXPXPXPXPXPXPXPXJXO * PXPXPXPXPXPXaXyXyXyXB.> > > > > > 0 LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPX;X9 > > > > > > > > > < g L I I I I I I I I I I I I I I I I I I Y.E.[..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XZ > > > > > > t F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXuXuXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX& & PXPXPXPXPXPXPXPXPXPX: %XPXPXPXPXPXLXpXyXyXyX > > > > > bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPX9Xr 8 > > > > > > > > > ' F I I I I I I I I I I I I I I U R.`..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X..8 > > > > > $.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX; ; PXPXPXPXPXPXPXPXPXPXVXo @ PXPXPXPXPXPXLXiXyXyXyXrX0 > > > > > > 0XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXZX5.> > > > > > > > > > 3 x I I I I I I I I I I I Y.!.[..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XR > 8 > > > > > 1.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXnXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX@X +XPXPXPXPXPXPXPXPXPXPXPX+X . fXPXPXPXPXPXPXLXyXyXyXyXyXo.8 > > > > > ;XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXLX&X8 > > > > > > > > > > h I I I I I I I U R.`..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X^.5 > > > > > > > p.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXpXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXVX. jXPXPXPXPXPXPXPXPXPXPXPXPX= oXPXPXPXPXPXPXPXBXyXyXyXyXyX:.> > > > > > n.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXwXy > > > > > > > > > > 2 Y I I I U !.[..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.| > > > > > > > > k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX$ @ PXPXPXPXPXPXPXPXPXPXPXPXPXLX- oXPXPXPXPXPXPXPXPXMXyXyXyXyXtXi.> > > > > > 9.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXDX9.> > > > > > > > > > < z E.'..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.}.C > > > > > > > > 9 C.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXNXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX$X +XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX#X+ @ fXPXPXPXPXPXPXPXPXPXaXyXyXyXyXyXl.> > > > > > *.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXLX-X0 > > > > > > > > > > X.]..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.^ < > > > > > > > > 9 D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXsXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX$ + JXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXgXOXXX+XhXPXPXPXPXPXPXPXPXPXPXLXiXyXyXyXyXyXA.> > > > > > r PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXlXO.> > > > > > > > > > { W..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.]._ < > > > > > > > > 9 F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXhX. $XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXBXyXyXyXyXyXyXK.8 > > > > 8 9 KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXDXy.> > > > > > > > > > < A }..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.X.> 8 > > > > > > > > ` F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXMXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX#X. XXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXsXyXyXyXyXyXyX > > > 8 > 9.zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPX:Xq > > > > > > > > > > ..^..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.E 8 > > > > > > > > > > ` F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX#X. XXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXuXyXyXyXyXyXyX4X9 8 8 > > > > 8 8 v.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX*.> > > > > > > 8 8 > _ ^ .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X|.9 8 > > > > > > > > > > #.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXNXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXhX% . @ $XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXmXtXyXyXyXyXyXyX4X0 > > > > > > > > > r 6XLXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKXv.< > > > > > > > > > < Z ]..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X].[ < 8 > > > > > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXpXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXhXOX- - XXfXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXiXyXyXyXyXyXyXyXrX0 > > > > > > > > > > 8 v.FXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX7Xr > > > > > > > > > > ..~..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.e.> > > > > > > > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXsXuXyXyXyXyXyXyXyXyX0 > > > > > > > > > > 8 > *.vXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcX=.> > > > > > > > > > 7 ! .X.X.X.X.X.X.X.X.X.X.X.X.X}.Q < 8 > > > > > > > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXiXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXBXuXyXyXyXyXyXyXyXyXyXw > > > > > > > > > > > > 8 y zXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXm.> > > > > > > > > > w w.[.[..X.X.X.X.X.X.X.X.X.X~.6 > 8 > > > > > > > > > > > > ,.F.F.F.F.G.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXuXmXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXiXyXyXyXyXyXyXyXyXyXyXw > > > > > > > > > > > > > > r kXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX9Xy > > > > > > > > > > | ~..X.X.X.X.X.X.X.X.X}. .8 > > > > > > , ( > > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXpXyXyXyXyXyXyXyXyXyXyXyXw > > > > > > > > > > > > > > 8 r cXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbX6.> > > > > > > > > > 6 R .X.X.X.X.X.X.X.XZ 8 8 > > > > > > j h > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXpXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXpXtXyXyXyXyXyXyXyXyXyXyXyXw > > > > > > :.8 > > > > > > > 8 y AXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXV.9 > > > > > > > > > < q.]..X.X.X.X.X^ , > > > > > > > a K s > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXnXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXNXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXGXiXuXyXyXyXyXyXyXyXyXyXyXyXrXe > > > > > 8 H.H.0 8 > > > > > > > 5.KXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqXy > > > > > > > > 8 > } X.X}..X]._ > > > > > > > 1 J I g > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXMXtXpXNXLXPXPXPXPXPXPXPXPXPXPXHXdXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrX0 > > > > > 8 K.yX > > > > > > > &XPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXAX9.> > > > > > > > > 8 4 E }..X2.8 > > > > > > < v I I f > > > > > > #.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXyXyXyXyXyXyXyXyXyXyXyXuXGXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXHXuXyXyXyXiXdXBXLXLXPXPXLXHXNXaXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5X0 > > > > > 8 I.yXyX > > > > > > 0 zXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX-X9 > > > > > > > > > < q.A > > > > > > > > f I I P j > > > > > > ` F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXuXyXyXyXyXyXyXuXyXyXyXyXyXyXiXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXpXyXyXyXyXyXuXtXuXyXiXiXiXuXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX1X9 > > > > > 8 > > > > > > =.LXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXlXO.> > > > > > > > > > > > > > > > > > ' K I I I z > > > > > > t F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXnXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > 8 8 5XyXyXyXyXB.8 > > > > > > > 7XPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDX0.8 > > > > > > > > > > > > > > > 5 F P I I I v > > > > > > 0 D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXuXyXyXyXyXyXyXyXyXyXyXyXyXyXuXiXGXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXNXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXK.> > > > 8 8 8 tXyXyXyXyXyXu.> > > > > > > O.LXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX>X9 8 > > > > > > > > > > > > 8 l I I I I I b > > > > > > 9 Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXiXBXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXBXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXB.> > > > > 8 e yXyXyXyXyXyX5Xt > > > > > > > 7XPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXcX%.> > > > > > > > > > > > h I I I I I I F < > > > > 8 > x.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXuXMXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXNXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXg.> > > > > 8 @.yXyXyXyXyXyXuXN.> > > > > > > =.PXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKXv.8 > > > > > > > > > 2 L I I I I I I P < > > > > > > k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXaXHXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXmXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXt.e > > > > > :.yXyXyXyXyXyXyXyXo.> > > > > > 8 cXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX6X> > > > > > > > < S I I I I I I I I 5 > > > > > > p.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXiXMXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXHXpXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXi.> 8 f.yXyXyXyXyXyXyXyXyXyXyXyX:.> > > > > > i.yXyXyXyXyXyXyXyXM.8 > > > > > > &XPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX;X> > > > > > > > j I I I I I I I I I ' > > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXuXyXiXNXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXnXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXi.8 8 8 N.yXyXyXyXyXyXyXyXyXyXyXyXy > > > > > > g.yXyXyXyXyXyXyXyX5Xe > > > > > > *.PX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX0 > > > > > > > i P I I I I I I I I I s > > > > > > +.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXnXGXLXPXPXPXPXPXPXPXPXPXPXPXLXGXdXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXt.8 > 8 q rXyXyXyXyXyXyXyXyXyXyXyX5X0 > > > > > 8 H.yXyXyXyXyXyXyXyXyX8.> > > > > > 8 DX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXFX%.> > > > > > > 5 J I I I I I I I I I I N > > > > > < w F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXiXsXMXGXHXLXLXLXHXGXNXsXiXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5X:.> > > > l.yXyXyXyXyXyXyXyXyXyXyXyXI.8 > > > > > 8 1XyXyXyXyXyXyXyXyXyXB.> > > > > > > wX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXb.> > > > > > > < m I I I I I I I I I I I v > > > > > 8 9 D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXuXyXtXuXuXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.y > > > > @.tXyXyXyXyXyXyXyXyXyXyXyXyXN.8 8 > > > > 9 yXyXyXyXyXyXyXyXyXyX1X9 > > > > > > -X", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX7X> > > > > > > > g I I I I I I I I I I I I G > > > > > > > k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXc.9 > > > > 0 I.yXyXyXyXyXyXyXyXyXyXyXyXyXi.> > > > > > $.yXyXyXyXyXyXyXyXyXyXrXt 8 > > > > > b.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXvXr > > > > > > > ' K I I I I I I I I I I I I P 5 > > > > > > r.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > 9 B.yXyXyXyXyXyXyXyXyXyXyXyXyXyX$.> > > > > > t.yXyXyXyXyXyXyXyXyXyXyX%.> > > > > > 6.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX5.> > > > > > > 5 F I I I I I I I I I I I I I I ] 8 > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xg.0 > > > > > > l.yXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xw > > > > > > M.yXyXyXyXyXyXyXyXyXyXyX:.> > > > > > *.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXV.> > > > > > > > l I I I I I I I I I I I I I I I N > > > > > > w D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xc.t > > > > > > 9 l.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXK.8 > > > > > > > > > > > O.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqX< > > > > > > > h K I I I I I I I I I I I I I I I M > > > > > > 8 x.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4XS.1.0 > > > > > > > 0 B.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXg.8 > > > > > 0 yXyXyXyXyXyXyXyXyXyXyXyXt.> > > > > > O.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXAXO.> > > > > > > 1 H I I I I I I I I I I I I I I I U ).> > > > > > 8 r.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5X1XI.H.l.t.o.9 8 8 8 > > > > > > o.I.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX-.> > > > > > :.yXyXyXyXyXyXyXyXyXyXyXyX8.> > > > > > &.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX0.> > > > > > > > S I I I I I P I I I I I I I I I I (..X[ > > > > > 8 #.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXU.c.t.:.-.@.t e w 0 9 8 > > > > > > > > > > 8 8 8 t.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xw > > > > > > l.yXyXyXyXyXyXyXyXyXyXyXyX-.> > > > > > 5.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX:X8 > > > > > > > j I I I I I I I I I I I I I I I I T..X.XC > > > > > 8 w Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXN.9 > > > > > > > > > > > > > > > > > > > > > 8 @.H.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXS.8 > > > > > 8 I.yXyXyXyXyXyXyXyXyXyXyXyX` > > > > > > y.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxXq > > > > > > > u P I I I I I I I I I I I I I I I U [..X.X/ 8 > > > > 8 8 h.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXo.> > > > > > > > > > > > > > > > > > > > > e M.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXt.> > > > > > e tXyXyXyXyXyXyXyXyXyXyXyX5Xw > > > > > > V.", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX*.> > > > > > > 5 G I I I I I I I I I I I I I I I I '..X.X.X].4 > > > > 8 > ,.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXt > > > > > > > > > > > > > > > > > > 8 t c.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXt > > > > > > 8.yXyXyXyXyXyXyXyXyXyXyXyXH.> > > > > > > 8X", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXn.> > > > > > > > c I I I I I I I I I I I I I I I I !..X.X.X.X.Xq.> > > > > > ` D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXd.> > > > > > > > > > > > > > > 8 8 :.B.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXK.8 > > > > > > A.yXyXyXyXyXyXyXyXyXyXyXyXd.> > > > > > > bX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX8X8 > > > > > > > d P I I I I I I I I I I I I I I I T..X.X.X.X.X.XQ > > > > > > > j.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrX-.> > > > > > > > > > > 8 9 -.c.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXt.> > > > > 8 9 eXyXyXyXyXyXyXyXyXyXyXyXrX` > > > > > > y LX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbXr > > > > > > > ( L I I I I I I I I I I I I I I I U [..X.X.X.X.X.X].4 > > > > > 8 -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXN.+.0 9 > 9 9 9 t :.f.H.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xe > > > > > > :.yXyXyXyXyXyXyXyXyXyXrXU.z.8 > > > > 8 8 v.PX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX6.> > > > > > > , Y I I I I I I I I I I I I I I I I `..X.X.X.X.X.X.X[.X.> > > > > 8 9 Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.Z.C.x.k.z.z.x.C.Z.F.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrX1X1X1X5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXB.> > > > > > 8 B.yXyXyXyXyXyXyXtX3XP.F.F.$.> > > > > 8 8 0XPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&X> > > > > > > > z I I I I I I I I I I I I I I I I E..X.X.X.X.X.X.X.X.XQ > > > > > > > p.F.F.F.F.F.F.F.F.F.F.D.x.u.;.` 0 8 > > > > > > > 9 ` ` >.B.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX:.> > > > > > w 5XyXyXtXeX3XU.L.G.F.F.F.z.9 > > > > > > y KXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXkX0 > > > > > > > h I I I I I I I I I I I I I I I I T..X.X.X.X.X.X.X.X.X.X].7 > > > > > > ` F.F.F.F.F.F.F.F.p.-.w > > > > 8 > > > > > > > > > > > > > > t t.K.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > > > -.J.G.G.G.F.F.F.F.F.F.F.F.$.> > > > > > 8 V.PXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDXO.> > > > > > > 2 H I I I I I I I I I I I I I I I U '..X.X.X.X.X.X.X.X.X.X.XV > > > > > > > k.F.F.F.F.x.,.w > > > > > > > > > > > > > > > > > > > > > > > > > t f.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXd.> > > > > > > j.F.F.F.F.F.F.F.F.F.F.F.p.8 > > > > > > 0 ZXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX0.> > > > > > > < n I I I I I I I I I I I I I I I I !..X.X.X.X.X.X.X.X.X.X.X.X|.> > > > > > > -.F.F.z.#.8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > w t.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xe 8 > > > > > t D.F.F.F.F.F.F.F.F.F.F.x.0 > > > > > > > m.PXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX,X> > > > > > > > f I I I I I I I I I I I I I I I I R..X.X.X.X.X.X.X.X.X.X.X.X.X.X| > > > > > > 9 j.-.9 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 0 i.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXc.> > > > > > > 8.F.F.F.F.F.F.F.F.F.F.D.` > > > > > > > r AXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcXq > > > > > > > ] K I I I I I I I I I I I I I I I Y..X.X.X.X.X.X.X.X.X.X.X.X.X.X.XR > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > t S.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXuXyXyXyXyXrX` > > > > > > 0 Z.F.F.F.F.F.F.F.F.F.D.$.> > > > > > > > :XPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX*.> > > > > > > 5 F I I I I I I I I I I I I I I I I `..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[._ > > > > > > > > > > > > > > > > > > > 9 t $.8.u.d.f.d.u.>.#.` > > > > > > > > > > > > > > 9 t.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXN.8 > > > > > 8 ;.F.F.F.F.F.F.F.F.F.D.-.> > > > > > > 8 5.LXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXm.> > > > > > > < x I I I I I I I I I I I I I I I I !..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XA > > > > > > > > > > > > > > > > t t.B.1XrXyXyXyXyXyXyXyXyXyXeXU.N.>.e > > > > > > > > > > > > @.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXo.8 > > > > > 8 x.F.F.F.F.F.F.F.F.Z.#.> > > > > > > > r bXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX0X> > > > > > > > s I I I I I I I I I I I I I I I I T..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.].w > > > > > > > > > > > > w 8.K.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5XS.:.8 > > > > > > > > > > e I.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXN.> > > > > > > -.F.F.F.F.F.F.F.F.z.t > > > > > > > > 0 8XPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZXy > > > > > > > 2 L I I I I I I I I I I I I I I I U [..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.X.> > > > > > > > > > 0 t.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.>.9 > > > > > > > > 8 0 K.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrX` > > > > > > > z.F.F.F.F.F.F.D.1.9 > > > > > > > > 9 ;XPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX6.> > > > > > > , S I I I I I I I I I I I I I I I I (..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X}.R > 8 > > > > > > > > :.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXH.@.> > > > > > > 8 > 0 I.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXg.8 > > > > > > $.F.F.F.F.F.F.a.` > > > > > > > > > > V.LXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX-X> > > > > > > > k I I I I I I I I I I I I I I I I E..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X^ < > > > > > > > > 0 M.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xd.9 > > > > > > > 8 e 1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX3X0 8 > > > > > 9 z.F.F.F.D.p.` > > > > > > > > > > 8 V.PXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXlX0 > > > > > > > i I I I I I I I I I I I I I I I I Y..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X_._ > > > > > > > > t I.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXH.0 > > > > > > > > @.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXtX<.> > > > > > > -.D.Z.a.-.9 8 > > > > > > > > > > < :XPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXFXO.> > > > > > > 5 J I I I I I I I I I I I I I I I I '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.| > > > > > > > > @.4XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXI.e > > > > > > > 8 u.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXx.8 > > > > > > > ` ` < > > > > > > > > > > > > > o.qXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXv.> > > > > > > < n I I I I I I I I I I I I I I I I /..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XA > > > > > > > > ` 4XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > > > > 8 K.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeXD.e > > > > > > > > > > > > > > > > > > > > > > > 6.ZXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPX6X8 > > > > > > > f I I I I I I I I I I I I I I I I R..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X_.4 > > > > > > > w A.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.0 > > > > > > > t tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX2XF.<.> > > > > > > > > > > > > > > > > > > > > 8 r >XPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXcXr > > > > > > > u K I I I I I I I I I I I I I I I U [..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.Xq.8 8 > > > > > 9 k.P.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXN.9 > > > > > > > l.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXuXtXL.F.z.8 > > > > > > > > > > > > > > > > > > > > 0 m.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXKX4.> > > > > > > < F I I I I I I I I I I I I I I I I '..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X^.< > 8 > > > > > 3.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX>.> > > > > > > e rXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXtXeXF.F.D.` > > > > > > > > > > > 8 8 > > > > > > r m.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPX-X> > > > > > > > l I I I I I I I I I I I I I I I I !..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.Xe.> > > > > > > ` D.D.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4X0 > > > > > > 8 M.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXP.F.F.F.,.8 > > > > > > > > > > > > 8 8 8 > 9 5.6XFXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXDXr > > > > > > > h I I I I I I I I I I I I I I I P T.[..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.[.9 > > > > > > 8 h.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXl.> > > > > > > #.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4XG.F.F.F.a.8 > > > > > > > > > 8 > > > 9 O.s.7XAXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPX7X> > > > > > > 2 L I I I I I I I I I I I I I I I U [..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.R 8 > > > > > > +.D.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xw > > > > > > 8 I.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXP.F.F.F.F.C.0 > > > > > > > O.v.v.m.-X6XlXZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXm.> > > > > > < S I I I I I I I I I I I I I I I I (..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.XX.8 8 > > > > > p.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXi.> > > > > > > h.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX > > > > > > q cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPX0.> > > > > > 5 g N z x v S F G H L I I I I I I Q..X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X[.6 > > > > > > 9 Z.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.8 8 > > > > > o.P.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX1XG.F.F.F.F.F.F.-.> > > > > > > 8 :XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXv.> > > > > > > > > > > > > > < 3 5 2 2 u p s f Z E R ^ W.~._.].}..X.X.X.X.X.X.X.X.X.X.X.X.X|.< > > > > > > $.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXt > > > > > > 0 D.G.U.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX3XL.F.F.F.F.F.F.F.1.> > > > > > > > 0.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPX>X> > > > > > > > > > > > > > > > > > > > > > > > > > > > > 4 5 _ { [ .X.e.Z E R ! W. X^._.E > > > > > > > 3.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX8.> > > > > > > x.F.F.G.U.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX3XG.G.F.F.F.F.F.F.F.p.8 > > > > > > > O.AXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXAXr > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 > > 8 > > 8 8 > 8 > > < 6 4 > > > > > > 8 j.F.F.F.F.G.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXl.> > > > > > > a.F.F.F.F.G.P.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX2XG.F.F.F.F.F.F.F.F.F.j.9 > > > > > > > q lXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPX;X> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 > > > > 8 > 8 C.F.F.F.F.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXH.> > > > > > > r.F.F.F.F.F.F.F.L.3XtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeXU.G.F.F.F.F.F.F.F.F.F.F.z.0 > > > > > > > 8 6XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXLXm.> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 8 9 Z.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.8 > > > > > 8 >.F.F.F.F.F.F.F.F.F.G.P.eXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX2XG.G.F.F.F.F.F.F.F.F.F.F.F.z.w > > > > > > > > m.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPX8XO.> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 9 D.F.F.F.F.F.G.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.L.2XeXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXU.P.G.F.G.F.F.F.F.F.F.F.F.F.F.F.x.w > > > > > > > > 9.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXLXkX-Xn.0.6.4.%.y q > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 9 D.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.9 > > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.P.3XtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeXU.G.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.z.w > > > > > > > > *.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXLXLXDXbXxXqX7X:X=Xm.v.9.5.&.y q > > > > > > > > > > > > > > > > > > > > > > > 8 8 9 C.F.F.F.F.F.F.F.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXK.> > > > > > > 3.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.2X3XrXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXeXU.P.G.F.F.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.k.w > > > > > > > > O.AXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXPXLXKXAXcXkX9X7X;X&Xn.s.6.*.O.r 8 > > > > > > > > 8 8 8 C.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXB.> > > > > > > r.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.U.3XeXrXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXrX3XU.P.L.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.j.0 > > > > > > > > y bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXLXDXbXzXn.8 > > > > > > a.F.F.F.F.F.F.F.G.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXd.> > > > > > 8 j.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.P.P.U.U.3X2X3X3X3X3X3X3X2X2XU.U.P.L.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.p.9 > > > > > > > > r xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqX> > > > > > > <.F.F.F.F.F.F.F.D.G.rXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX;.> > > > > > 8 C.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.1.8 8 > > > > > > > r xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXAX8 8 > > > > > ` D.F.F.F.F.F.F.G.F.L.rXyXyXtXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeX0 > > > > > > w F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.-.> > > > > > > > > r xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&.> > > > > > 8 C.F.F.F.F.F.F.F.F.F.G.yXyXuXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXB.> > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.x.` > > > > > > > > > o.cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXV.> > > > > > > <.F.F.F.F.F.F.F.F.F.F.L.rXtXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX;.> > > > > > > p.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.h.0 > > > > > > > > > O.bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX8 > > > > > > w Z.D.F.F.F.F.F.F.F.F.F.F.eXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXP.9 > > > > > > 0 C.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.;.> > > > > > > > > > :.ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX*.> > > > > > > 1.D.F.F.F.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeX-.> > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.k.` > > > > > > > > 8 8 9.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX,X> > > > > 8 8 9 C.F.F.F.F.F.F.F.F.F.F.F.F.L.3XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXP.j.8 > > > > > > > z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.,.8 > > > > > > > > > 9 m.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXO.> > > > > > > o.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.eXyXyXyXtXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXrXU.G.C.` > > > > > > > +.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.h.` > > > > > > > > > > q 6XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX7X> > > > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.2XrXyXyXyXyXtXyXyXyXuXyXyXyXyXyXyXyXyXtX3XP.G.F.D.-.> > > > > > > > k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.C.-.8 > > > > > > > > > > y zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX=.> > > > > > > 8 p.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.L.2XeXrXyXyXyXuXtXyXyXyXyXtXeX2XP.G.F.F.F.F.<.> > > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.1.w > > > > > > > > > > > > 0 qXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX0 > > > > > > > 9 a.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.D.G.G.L.L.L.U.P.P.L.L.G.F.F.F.F.F.F.F.3.> > > > > > > > w Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.r.w > > > > > > > > > > > > > > 8 y bXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX;X> > > > > > > > 9 u.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.,.> > > > > > > > > j.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.u.e > > > > > > > > > > > > > > > > > > 5.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX0.> > > > > > > > 8 ,.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.C.-.> > > > > > > > > 3.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.r.e > > > > > > > > > > > > 8 8 > > > > > > 8 >XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX=.> > > > > > > > > ` x.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.h.` > > > > > > > > > -.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.x.,.0 > > > > > > > > > > > > > ;.I.o.> > > > > > > y AXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXAX*.> > > > > > > > > 8 <.Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.C.-.> > > > > > > > > > > -.k.D.F.F.F.F.F.F.F.F.F.F.F.F.F.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.h.#.8 > > > > > > > > > > > > > ` B.rXyX3X0 > > > > > > > m.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZX*.> > > > > > > > > > 9 1.Z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.C.-.9 > > > > > > > > > > > > > 8 ` <.x.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.j.-.w > > > > > > > > > > > > > > 0 d.5XyXyXyXyXN.8 8 > > > > > q ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDX6.> > > > > > > > > > > 9 $.a.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.3.+.8 > > > > > > > > > > > > > > > > > > 8 ` <.k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.h.-.w > > > > > > > > > > > > > > > 8 1.1XyXyXyXyXyXyXtX;.> > > > > > 8 =XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXm.< 8 > > > > > > > > > > 8 8 +.<.h.z.Z.F.F.F.F.F.Z.z.p.,.` 9 > > > > > > > > > > > > > > > > > > > > > > > > 8 0 #.r.C.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.k.3.` 9 8 > > > > > > > > > > > > > > > > >. > > > 8 %.KXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX0Xy > > > > > > > > > > > > > > > 8 8 8 0 9 9 > > > 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 0 +.3.h.C.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.x.p.,.` 8 8 > > 8 > > > > > > > > > > > > > > 9 >. > > > 8 8 zXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXDXv.9 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > ` > > > > > > > > > > > > > > > > > > > > > > 9 ` -.<.3.h.z.C.D.F.F.D.F.D.F.Z.F.F.F.Z.x.k.a.3.,.+.e 8 > > > > > > > > > > > > > > > > > > > > > 0 t.1XyXyXyXyXyXyXyXyXyXyXyXyXyXyXH.8 > > > > > > V.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXlX6.9 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > w D.C.,.w > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 8 8 ` w w w w 0 9 > > > > > > > > > > > > > > > > > > > > > > > > > > > > 9 @.M.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXr > > > > > > =.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXwX9.9 8 > > > > > > > > > > > > > > > > > > > > > > > > < > > > > > > ` F.F.F.Z.h.+.9 > 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 0 t.I.yXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyX1.> > > > > > q DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcXm.#.8 > > > > > > > > > > > > > > > > > > > > > 9 w > > > > > > +.D.F.F.D.F.D.C.j.-.9 8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 w >.D.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXN.> > > > > > 8 xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXxX&X6.r 9 > > > > > > > > > > > > > 9 y 9.;XcXV.> > > > > > -.F.F.F.F.F.F.F.2XyX1XM.:.e > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 > > > > > 8 8 > 8 t t.S.5XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.> > > > > > > 9XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKXxX7X&Xv.9.5.*.*.=.5.9.b.-X8XcXLXPXPXPXV.> > > > > > -.F.F.F.F.F.F.F.G.yXyXyXyXrXH.d.$.0 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 8 > > 8 8 t >.l. > > > > > :XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&X> > > > > > $.F.F.F.F.F.F.F.F.3XyXyXyXyXyXyXyX5XL.l.1.+.0 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 9 -.u.M. > > > > > =XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX-X> > > > > > +.F.F.F.F.F.F.F.F.L.yXyXyXyXyXyXyXyXyXtXyXyXeXU.B.g.t.;.o.t 0 0 9 > > > > > > > > > > 9 0 e 9 > > > > > > i.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXrXq > > > > > > &XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX,X> > > > > > ` D.F.F.F.F.F.F.F.F.2XyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX5X3X > > > > 8 j.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX5Xq > > > > > > &XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqX> > > > > > 0 Z.F.F.F.F.F.F.F.F.G.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXe > > > > > 8 a.tXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX9 8 > > > > > ;XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbX8 > > > > > 8 C.F.F.F.F.F.F.F.F.F.G.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX4X9 > > > > > 8 z.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuX > > > > > > 7XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX` > > > > > > p.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXB.> > > > > > 9 Z.F.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXB.> > > > > > > lXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX6.> > > > > > #.F.F.F.F.F.F.F.F.F.F.F.U.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXt.> > > > > > ` D.F.L.rXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXi.> > > > > > q ZXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&X> > > > > 8 9 C.F.F.F.F.F.F.F.F.F.F.F.P.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXrXq > > > > > > <.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX@.> > > > > > %.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX8 > > > > 8 > u.F.F.F.F.F.F.F.F.F.F.F.F.P.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXB.8 > > > > > > j.F.F.F.F.L.yXyXyXyXyXyXyXyXyXyXyXyXyXyXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyX > > > > > v.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX%.> > > > 8 8 ` D.F.F.F.F.F.F.F.F.F.F.F.F.L.eXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtX$.8 > > > > > w D.F.F.F.F.F.L.rXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXi.8 > > > > > > 9XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX&X8 > > > > > 8 p.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeXk.8 > > > > > > ;.F.F.F.F.F.F.F.G.eXtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXI.9 8 > > > > > y FXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbX9 > > > > > 8 e D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.G.2XtXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXeXP.D.` > > > > > > 8 z.F.F.F.F.F.F.F.F.G.U.tXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXU.<.8 8 > > > > > v.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXs.> > > > > > > <.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.P.4XtXyXyXyXyXyXyXyXyXyXyXyXyXrXU.P.F.F.3.> > > > > > > o.F.F.F.F.F.F.F.F.F.F.F.L.3XtXyXuXuXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXyXtXU.G.k.8 8 8 > > > > 9 xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxX9 > > > > > > 8 j.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.U.3XeXeXrXrXeXeX2XP.G.F.F.F.F.z.8 > > > > > > > >.F.F.F.F.F.F.F.F.F.F.F.F.F.G.2XrXtXyXuXyXyXyXyXyXyXyXyXyXyXyXyXyX5XU.G.F.D.` > > 8 > > > > 9.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXs.> > > > > > > 0 C.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.C.` > > > > > > > > 8 k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.2XeXyXtXyXyXyXtXuXtXyXtXeXU.L.F.F.F.F.-.> > > > > > > 0 xXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbXr > > > > > > > t C.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.` > > > > > > > > > > w x.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.L.L.P.P.P.P.L.F.F.F.F.F.F.F.F.1.8 > > > > > > 8 n.PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX;X> > > > > > > > t C.F.G.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.` > > > > > > > > > > > > w x.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.1.8 8 > > > > > 8 O.DXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX9.> > > > > > > > w z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.x.` > > > > > > > > > > > > > > 9 h.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.;.> > 8 > > > > > 8 qXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXZXO.> > > > > > > > 0 r.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.p.0 > > > > > > 8 8 8 8 > > > > > > 8 3.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.x.+.8 8 > > > > > > 9 &XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXzXq > > > > > > > > > +.z.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.x.$.> > > > > > > > > 0 r 8 > > > > > > > > ` k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.Z.3.9 8 > > > > > > > > v.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX0Xq > > > > > > > > > 9 ,.z.D.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.x.;.9 > > > > > > > > > 9 8XxXt > > > > > > > > > > @.k.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.D.Z.u.q > > > > > > > > > > 9.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX8Xq > > > > > > > > > > 9 +.p.C.D.F.F.F.F.F.F.F.F.F.F.C.p.+.9 > 8 > > > > > > > > 0 7XPXLXzXt > > > > > > > > > > > ` 3.C.D.F.F.F.F.F.F.F.F.F.F.F.Z.k.,.0 > > > > > > > > > > > s.HXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqXr > > > > > > > > > > > 8 < t -.3.p.h.j.h.p.3.-.t 9 8 > > > > > > > > > > > q 9XPXPXPXPXvX%.> > > > > > > > > > > > 4 e -.3.a.k.z.C.z.k.p.<.+.0 > > > > > > > > > > > > > m.LXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXcX*.> > > > > > > > > > 8 > > > > > > > > > > > > > 8 > > > > > > > > > > %.zXPXPXPXPXPXPXFXs.> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > r ,XPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXFXn.8 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 8 v.DXPXPXPXPXPXPXPXPXPX,Xr > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 5.cXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXqX&.> > > > > > > > > > > > > > > > > > > > > > > > > > > > > O.9XPXPXPXPXPXPXPXPXPXPXPXPXAXv.9 > > > > > > > > > > > > > > > > > > > > > > > > > > > > > y 6XLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLX;Xr > > > > > > > > > > > > > > > > > > > > > > > > 8 r =XKXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXxXy.9 > > > > > > > > > > > > > > > > > > > > > > > > > r -XKXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKX:X&.> > > > > > > > > > > > > > > > > > > > 8 O.-XFXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXbXm.y 8 > > > > > > > > > > > > > > > > > > > > 4.,XKXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXLXkXm.O.> > > > > > > > > > > > 8 > 8 y n.wXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXKXqXm.&.8 > > > > > > > > > > 9 > > q 6.;XvXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX", +"PXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXbX7XV.0.*.r 0 9 0 t &.9.m.7XbXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXFXqX:Xm.s.6.>.=.5.9.v.V.7XxXLXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPXPX" }; diff -r 64740eec84ad -r 4c523ed1d35c tools/build_vcpkg.bat --- a/tools/build_vcpkg.bat Sun Mar 24 14:05:06 2024 -0400 +++ b/tools/build_vcpkg.bat Sun Mar 24 14:33:57 2024 -0400 @@ -55,7 +55,7 @@ echo Running cmake... set ERRORLEVEL= -cmake . -DCMAKE_TOOLCHAIN_FILE="%VCPKG_PATH%\scripts\buildsystems\vcpkg.cmake" -G"NMake Makefiles" %CROSS_COMPILE_FLAG% %BUILD_SERVER_FLAG% "%PREFIX_FLAG%" -DCMAKE_BUILD_TYPE="%BUILD_TYPE%" -DSDL2_BUILDING_LIBRARY=1 +cmake . -DCMAKE_TOOLCHAIN_FILE="%VCPKG_PATH%\scripts\buildsystems\vcpkg.cmake" -G"NMake Makefiles" %CROSS_COMPILE_FLAG% %BUILD_SERVER_FLAG% "%PREFIX_FLAG%" -DCMAKE_BUILD_TYPE="%BUILD_TYPE%" -DSDL2_BUILDING_LIBRARY=1 -DNOVIDEOREC=1 if %ERRORLEVEL% NEQ 0 goto exitpoint diff -r 64740eec84ad -r 4c523ed1d35c tools/check_lua_locale_files.sh --- a/tools/check_lua_locale_files.sh Sun Mar 24 14:05:06 2024 -0400 +++ b/tools/check_lua_locale_files.sh Sun Mar 24 14:33:57 2024 -0400 @@ -1,5 +1,5 @@ #!/bin/sh - echo "*** Luacheck of Lua locale files:" -luacheck ../share/hedgewars/Data/Locale/*.lua --globals locale -q +luacheck ../share/hedgewars/Data/Locale/*.lua --globals locale --no-max-line-length -q echo "Missing translations in Lua locale files:" grep -c -- "^--" ../share/hedgewars/Data/Locale/*.lua diff -r 64740eec84ad -r 4c523ed1d35c tools/hw2irc/123.45.67.89.auth --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/123.45.67.89.auth Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1 @@ +sample_user=5177a58dc65c8a14dc90c69db3bf3dd2 diff -r 64740eec84ad -r 4c523ed1d35c tools/hw2irc/net/ercatec/hw/INetClient.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw/INetClient.java Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,71 @@ +/* + * Java net client for Hedgewars, a free turn based strategy game + * Copyright (c) 2011 Richard Karolyi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +package net.ercatec.hw; + +import java.util.List; + +public interface INetClient +{ + public static enum UserFlagType { UNKNOWN, ADMIN, INROOM, REGISTERED }; + public static enum BanType { BYNICK, BYIP }; + + public void onConnectionLoss(); + public void onDisconnect(String reason); + + public void onMalformedMessage(String contents); + + public String onPasswordHashNeededForAuth(); + + public void onChat(String user, String message); + public void onWelcomeMessage(String message); + + public void onNotice(int number); + public String onNickCollision(String nick); + public void onNickSet(String nick); + + public void onLobbyJoin(String[] users); + public void onLobbyLeave(String user, String reason); + + // TODO flags => enum array? + public void onRoomInfo(String name, String flags, String newName, + int nUsers, int nTeams, String owner, String map, + String style, String scheme, String weapons); + public void onRoomDel(String name); + + public void onRoomJoin(String[] users); + public void onRoomLeave(String[] users); + + public void onPing(); + public void onPong(); + + public void onUserFlagChange(String user, UserFlagType flag, boolean newValue); + + public void onUserInfo(String user, String ip, String version, String room); + + public void onBanListEntry(BanType type, String target, String duration, String reason); + public void onBanListEnd(); + + public void logDebug(String message); + public void logError(String message); + + public void sanitizeInputs(final String[] inputs); +/* + public void onEngineMessage(String message); +*/ +} diff -r 64740eec84ad -r 4c523ed1d35c tools/hw2irc/net/ercatec/hw/ProtocolConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw/ProtocolConnection.java Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,649 @@ +/* + * Java net client for Hedgewars, a free turn based strategy game + * Copyright (c) 2011 Richard Karolyi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +package net.ercatec.hw; + +import java.lang.*; +import java.lang.IllegalArgumentException; +import java.lang.Runnable; +import java.lang.Thread; +import java.io.*; +import java.net.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.Vector; +// for auth +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.NoSuchAlgorithmException; + +public final class ProtocolConnection implements Runnable +{ + private static final String DEFAULT_HOST = "netserver.hedgewars.org"; + private static final int DEFAULT_PORT = 46631; + private static final String PROTOCOL_VERSION = "53"; + + private final Socket socket; + private BufferedReader fromSvr; + private PrintWriter toSvr; + private final INetClient netClient; + private boolean quit; + private boolean debug; + + private final String host; + private final int port; + + private String nick; + + public ProtocolConnection(INetClient netClient) throws Exception { + this(netClient, DEFAULT_HOST); + } + + public ProtocolConnection(INetClient netClient, String host) throws Exception { + this(netClient, host, DEFAULT_PORT); + } + + public ProtocolConnection(INetClient netClient, String host, int port) throws Exception { + this.netClient = netClient; + this.host = host; + this.port = port; + this.nick = nick = ""; + this.quit = false; + + fromSvr = null; + toSvr = null; + + try { + socket = new Socket(host, port); + fromSvr = new BufferedReader(new InputStreamReader(socket.getInputStream())); + toSvr = new PrintWriter(socket.getOutputStream(), true); + } + catch(Exception ex) { + throw ex; + } + + ProtocolMessage firstMsg = processNextMessage(); + if (firstMsg.getType() != ProtocolMessage.Type.CONNECTED) { + closeConnection(); + throw new Exception("First Message wasn't CONNECTED."); + } + + } + + public void run() { + + try { + while (!quit) { + processNextMessage(); + } + } + catch(Exception ex) { + netClient.logError("FATAL: Run loop died unexpectedly!"); + ex.printStackTrace(); + handleConnectionLoss(); + } + + // only gets here when connection was closed + } + + public void processMessages() { + processMessages(false); + } + + public Thread processMessages(boolean inNewThread) + { + if (inNewThread) + return new Thread(this); + + run(); + return null; + } + + public void processNextClientFlagsMessages() + { + while (!quit) { + if (!processNextMessage(true).isValid) + break; + } + } + + private ProtocolMessage processNextMessage() { + return processNextMessage(false); + } + + private void handleConnectionLoss() { + closeConnection(); + netClient.onConnectionLoss(); + } + + public void close() { + this.closeConnection(); + } + + private synchronized void closeConnection() { + if (quit) + return; + + quit = true; + try { + if (fromSvr != null) + fromSvr.close(); + } catch(Exception ex) {}; + try { + if (toSvr != null) + toSvr.close(); + } catch(Exception ex) {}; + try { + socket.close(); + } catch(Exception ex) {}; + } + + private String resumeLine = ""; + + private ProtocolMessage processNextMessage(boolean onlyIfClientFlags) + { + String line; + final List parts = new ArrayList(32); + + while (!quit) { + + if (!resumeLine.isEmpty()) { + line = resumeLine; + resumeLine = ""; + } + else { + try { + line = fromSvr.readLine(); + + if (onlyIfClientFlags && (parts.size() == 0) + && !line.equals("CLIENT_FLAGS")) { + resumeLine = line; + // return invalid message + return new ProtocolMessage(); + } + } + catch(Exception whoops) { + handleConnectionLoss(); + break; + } + } + + if (line == null) { + handleConnectionLoss(); + // return invalid message + return new ProtocolMessage(); + } + + if (!quit && line.isEmpty()) { + + if (parts.size() > 0) { + + ProtocolMessage msg = new ProtocolMessage(parts); + + netClient.logDebug("Server: " + msg.toString()); + + if (!msg.isValid()) { + netClient.onMalformedMessage(msg.toString()); + if (msg.getType() != ProtocolMessage.Type.BYE) + continue; + } + + final String[] args = msg.getArguments(); + netClient.sanitizeInputs(args); + + + final int argc = args.length; + + try { + switch (msg.getType()) { + + case PING: + netClient.onPing(); + break; + + case LOBBY__JOINED: + try { + assertAuthNotIncomplete(); + } + catch (Exception ex) { + disconnect(); + netClient.onDisconnect(ex.getMessage()); + } + netClient.onLobbyJoin(args); + break; + + case LOBBY__LEFT: + netClient.onLobbyLeave(args[0], args[1]); + break; + + case CLIENT_FLAGS: + String user; + final String flags = args[0]; + if (flags.length() < 2) { + //netClient.onMalformedMessage(msg.toString()); + break; + } + final char mode = flags.charAt(0); + if ((mode != '-') && (mode != '+')) { + //netClient.onMalformedMessage(msg.toString()); + break; + } + + final int l = flags.length(); + + for (int i = 1; i < l; i++) { + // set flag type + final INetClient.UserFlagType flag; + // TODO support more flags + switch (flags.charAt(i)) { + case 'a': + flag = INetClient.UserFlagType.ADMIN; + break; + case 'i': + flag = INetClient.UserFlagType.INROOM; + break; + case 'u': + flag = INetClient.UserFlagType.REGISTERED; + break; + default: + flag = INetClient.UserFlagType.UNKNOWN; + break; + } + + for (int j = 1; j < args.length; j++) { + netClient.onUserFlagChange(args[j], flag, mode=='+'); + } + } + break; + + case CHAT: + netClient.onChat(args[0], args[1]); + break; + + case INFO: + netClient.onUserInfo(args[0], args[1], args[2], args[3]); + break; + + case PONG: + netClient.onPong(); + break; + + case NICK: + final String newNick = args[0]; + if (!newNick.equals(this.nick)) { + this.nick = newNick; + } + netClient.onNickSet(this.nick); + sendMessage(new String[] { "PROTO", PROTOCOL_VERSION }); + break; + + case NOTICE: + // nickname collision + if (args[0].equals("0")) + setNick(netClient.onNickCollision(this.nick)); + break; + + case ASKPASSWORD: + try { + final String pwHash = netClient.onPasswordHashNeededForAuth(); + doAuthPart1(pwHash, args[0]); + } + catch (Exception ex) { + disconnect(); + netClient.onDisconnect(ex.getMessage()); + } + break; + + case ROOMS: + final int nf = ProtocolMessage.ROOM_FIELD_COUNT; + for (int a = 0; a < argc; a += nf) { + handleRoomInfo(args[a+1], Arrays.copyOfRange(args, a, a + nf)); + } + + case ROOM_ADD: + handleRoomInfo(args[1], args); + break; + + case ROOM_DEL: + netClient.onRoomDel(args[0]); + break; + + case ROOM_UPD: + handleRoomInfo(args[0], Arrays.copyOfRange(args, 1, args.length)); + break; + + case BYE: + closeConnection(); + if (argc > 0) + netClient.onDisconnect(args[0]); + else + netClient.onDisconnect(""); + break; + + case SERVER_AUTH: + try { + doAuthPart2(args[0]); + } + catch (Exception ex) { + disconnect(); + netClient.onDisconnect(ex.getMessage()); + } + break; + } + // end of message + return msg; + } + catch(IllegalArgumentException ex) { + + netClient.logError("Illegal arguments! " + + ProtocolMessage.partsToString(parts) + + "caused: " + ex.getMessage()); + + return new ProtocolMessage(); + } + } + } + else + { + parts.add(line); + } + } + + netClient.logError("WARNING: Message wasn't parsed correctly: " + + ProtocolMessage.partsToString(parts)); + // return invalid message + return new ProtocolMessage(); // never to be reached + } + + private void handleRoomInfo(final String name, final String[] info) throws IllegalArgumentException + { + // TODO room flags enum array + + final int nUsers; + final int nTeams; + + try { + nUsers = Integer.parseInt(info[2]); + } + catch(IllegalArgumentException ex) { + throw new IllegalArgumentException( + "Player count is not an valid integer!", + ex); + } + + try { + nTeams = Integer.parseInt(info[3]); + } + catch(IllegalArgumentException ex) { + throw new IllegalArgumentException( + "Team count is not an valid integer!", + ex); + } + + netClient.onRoomInfo(name, info[0], info[1], nUsers, nTeams, + info[4], info[5], info[6], info[7], info[8]); + } + + private static final String AUTH_SALT = PROTOCOL_VERSION + "!hedgewars"; + private static final int PASSWORD_HASH_LENGTH = 32; + public static final int SERVER_SALT_MIN_LENGTH = 16; + private static final String AUTH_ALG = "SHA-1"; + private String serverAuthHash = ""; + + private void assertAuthNotIncomplete() throws Exception { + if (!serverAuthHash.isEmpty()) { + netClient.logError("AUTH-ERROR: assertAuthNotIncomplete() found that authentication was not completed!"); + throw new Exception("Authentication was not finished properly!"); + } + serverAuthHash = ""; + } + + private void doAuthPart2(final String serverAuthHash) throws Exception { + if (!this.serverAuthHash.equals(serverAuthHash)) { + netClient.logError("AUTH-ERROR: Server's authentication hash is incorrect!"); + throw new Exception("Server failed mutual authentication! (wrong hash provided by server)"); + } + netClient.logDebug("Auth: Mutual authentication successful."); + this.serverAuthHash = ""; + } + + private void doAuthPart1(final String pwHash, final String serverSalt) throws Exception { + if ((pwHash == null) || pwHash.isEmpty()) { + netClient.logDebug("AUTH: Password required, but no password hash was provided."); + throw new Exception("Auth: Password needed, but none specified."); + } + if (pwHash.length() != PASSWORD_HASH_LENGTH) { + netClient.logError("AUTH-ERROR: Your password hash has an unexpected length! Should be " + + PASSWORD_HASH_LENGTH + " but is " + pwHash.length() + ); + throw new Exception("Auth: Your password hash length seems wrong."); + } + if (serverSalt.length() < SERVER_SALT_MIN_LENGTH) { + netClient.logError("AUTH-ERROR: Salt provided by server is too short! Should be at least " + + SERVER_SALT_MIN_LENGTH + " but is " + serverSalt.length() + ); + throw new Exception("Auth: Server violated authentication protocol! (auth salt too short)"); + } + + final MessageDigest sha1Digest; + + try { + sha1Digest = MessageDigest.getInstance(AUTH_ALG); + } + catch(NoSuchAlgorithmException ex) { + netClient.logError("AUTH-ERROR: Algorithm required for authentication (" + + AUTH_ALG + ") not available!" + ); + return; + } + + + // generate 130 bit base32 encoded value + // base32 = 5bits/char => 26 chars, which is more than min req + final String clientSalt = + new BigInteger(130, new SecureRandom()).toString(32); + + final String saltedPwHash = + clientSalt + serverSalt + pwHash + AUTH_SALT; + + final String saltedPwHash2 = + serverSalt + clientSalt + pwHash + AUTH_SALT; + + final String clientAuthHash = + new BigInteger(1, sha1Digest.digest(saltedPwHash.getBytes("UTF-8"))).toString(16); + + serverAuthHash = + new BigInteger(1, sha1Digest.digest(saltedPwHash2.getBytes("UTF-8"))).toString(16); + + sendMessage(new String[] { "PASSWORD", clientAuthHash, clientSalt }); + +/* When we got password hash, and server asked us for a password, perform mutual authentication: + * at this point we have salt chosen by server + * client sends client salt and hash of secret (password hash) salted with client salt, server salt, + * and static salt (predefined string + protocol number) + * server should respond with hash of the same set in different order. + + if(m_passwordHash.isEmpty() || m_serverSalt.isEmpty()) + return; + + QString hash = QCryptographicHash::hash( + m_clientSalt.toAscii() + .append(m_serverSalt.toAscii()) + .append(m_passwordHash) + .append(cProtoVer->toAscii()) + .append("!hedgewars") + , QCryptographicHash::Sha1).toHex(); + + m_serverHash = QCryptographicHash::hash( + m_serverSalt.toAscii() + .append(m_clientSalt.toAscii()) + .append(m_passwordHash) + .append(cProtoVer->toAscii()) + .append("!hedgewars") + , QCryptographicHash::Sha1).toHex(); + + RawSendNet(QString("PASSWORD%1%2%1%3").arg(delimiter).arg(hash).arg(m_clientSalt)); + +Server: ("ASKPASSWORD", "5S4q9Dd0Qrn1PNsxymtRhupN") +Client: ("PASSWORD", "297a2b2f8ef83bcead4056b4df9313c27bb948af", "{cc82f4ca-f73c-469d-9ab7-9661bffeabd1}") +Server: ("SERVER_AUTH", "06ecc1cc23b2c9ebd177a110b149b945523752ae") + + */ + } + + public void sendCommand(final String command) + { + String cmd = command; + + // don't execute empty commands + if (cmd.length() < 1) + return; + + // replace all newlines since they violate protocol + cmd = cmd.replace('\n', ' '); + + // parameters are separated by one or more spaces. + final String[] parts = cmd.split(" +"); + + // command is always CAPS + parts[0] = parts[0].toUpperCase(); + + sendMessage(parts); + } + + public void sendPing() + { + sendMessage("PING"); + } + + public void sendPong() + { + sendMessage("PONG"); + } + + private void sendMessage(final String msg) + { + sendMessage(new String[] { msg }); + } + + private void sendMessage(final String[] parts) + { + if (quit) + return; + + netClient.logDebug("Client: " + messagePartsToString(parts)); + + boolean malformed = false; + String msg = ""; + + for (final String part : parts) + { + msg += part + '\n'; + if (part.isEmpty() || (part.indexOf('\n') >= 0)) { + malformed = true; + break; + } + } + + if (malformed) { + netClient.onMalformedMessage(messagePartsToString(parts)); + return; + } + + try { + toSvr.print(msg + '\n'); // don't use println, since we always want '\n' + toSvr.flush(); + } + catch(Exception ex) { + netClient.logError("FATAL: Couldn't send message! " + ex.getMessage()); + ex.printStackTrace(); + handleConnectionLoss(); + } + } + + private String messagePartsToString(String[] parts) { + + if (parts.length == 0) + return "([empty message])"; + + String result = "(\"" + parts[0] + '"'; + for (int i=1; i < parts.length; i++) + { + result += ", \"" + parts[i] + '"'; + } + result += ')'; + + return result; + } + + public void disconnect() { + sendMessage(new String[] { "QUIT", "Client quit" }); + closeConnection(); + } + + public void disconnect(final String reason) { + sendMessage(new String[] { "QUIT", reason.isEmpty()?"-":reason }); + closeConnection(); + } + + public void sendChat(String message) { + + String[] lines = message.split("\n"); + + for (String line : lines) + { + if (!message.trim().isEmpty()) + sendMessage(new String[] { "CHAT", line }); + } + } + + public void joinRoom(final String roomName) { + + sendMessage(new String[] { "JOIN_ROOM", roomName }); + } + + public void leaveRoom(final String roomName) { + + sendMessage("PART"); + } + + public void requestInfo(final String user) { + + sendMessage(new String[] { "INFO", user }); + } + + public void setNick(final String nick) { + + this.nick = nick; + sendMessage(new String[] { "NICK", nick }); + } + + public void kick(final String nick) { + + sendMessage(new String[] { "KICK", nick }); + } + + public void requestRoomsList() { + + sendMessage("LIST"); + } +} + diff -r 64740eec84ad -r 4c523ed1d35c tools/hw2irc/net/ercatec/hw/ProtocolMessage.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw/ProtocolMessage.java Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,280 @@ +/* + * Java net client for Hedgewars, a free turn based strategy game + * Copyright (c) 2011 Richard Karolyi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +package net.ercatec.hw; + +import java.util.Arrays; +import java.util.List; +import java.util.Iterator; + +public final class ProtocolMessage +{ + public static final int ROOM_FIELD_COUNT = 9; + + private static int minServerVersion = 49; + + public static enum Type { + // for unknown message types + _UNKNOWN_MESSAGETYPE_, + // server messages + ERROR, + PING, + PONG, + NICK, + PROTO, + ASKPASSWORD, + SERVER_AUTH, + CONNECTED, + SERVER_MESSAGE, + BYE, + INFO, + NOTICE, + CHAT, + LOBBY__JOINED, + LOBBY__LEFT, + ROOMS, + ROOM, + ROOM_ADD, + ROOM_DEL, + ROOM_UPD, + ROOM__JOINED, + ROOM__LEFT, + CFG, + TOGGLE_RESTRICT_TEAMS, + CLIENT_FLAGS, + CF, // this just an alias and will be mapped to CLIENT_FLAGS + EM // engine messages + } + + public final boolean isValid; + private Type type; + private String[] args; + +/* + public ProtocolMessage(String messageType) + { + args = new String[0]; + + try + { + type = Type.valueOf(messageType); + isValid = messageSyntaxIsValid(); + } + catch (IllegalArgumentException whoops) + { + type = Type._UNKNOWN_MESSAGETYPE_; + args = new String[] { messageType }; + isValid = false; + } + } +*/ + + private final String[] emptyArgs = new String[0]; + + private String[] withoutFirst(final String[] array, final int amount) { + return Arrays.copyOfRange(array, amount, array.length); + } + + private final List parts; + + // invalid Message + public ProtocolMessage() { + this.parts = Arrays.asList(emptyArgs); + this.args = emptyArgs; + this.isValid = false; + } + + public ProtocolMessage(final List parts) + { + this.parts = parts; + this.args = emptyArgs; + + final int partc = parts.size(); + + if (partc < 1) { + isValid = false; + return; + } + + try { + type = Type.valueOf(parts.get(0).replaceAll(":", "__")); + } + catch (IllegalArgumentException whoops) { + type = Type._UNKNOWN_MESSAGETYPE_; + } + + if (type == Type._UNKNOWN_MESSAGETYPE_) { + args = parts.toArray(args); + isValid = false; + } + else { + // all parts after command become arguments + if (partc > 1) + args = withoutFirst(parts.toArray(args), 1); + isValid = checkMessage(); + } + } + + private boolean checkMessage() + { + int argc = args.length; + + switch (type) + { + // no arguments allowed + case PING: + case PONG: + case TOGGLE_RESTRICT_TEAMS: + if (argc != 0) + return false; + break; + + // one argument or more + case EM: // engine messages + case LOBBY__JOINED: // list of joined players + case ROOM__JOINED: // list of joined players + if (argc < 1) + return false; + break; + + // one argument + case SERVER_MESSAGE: + case BYE: // disconnect reason + case ERROR: // error message + case NICK: // nickname + case PROTO: // protocol version + case SERVER_AUTH: // last stage of mutual of authentication + case ASKPASSWORD: // request for auth with salt + if (argc != 1) + return false; + break; + + case NOTICE: // argument should be a number + if (argc != 1) + return false; + try { + Integer.parseInt(args[0]); + } + catch (NumberFormatException e) { + return false; + } + break; + + // two arguments + case CONNECTED: // server description and version + case CHAT: // player nick and chat message + case LOBBY__LEFT: // player nick and leave reason + case ROOM__LEFT: // player nick and leave reason + if (argc != 2) + return false; + break; + + case ROOM: // "ADD" (or "UPD" + room name ) + room attrs or "DEL" and room name + if(argc < 2) + return false; + + final String subC = args[0]; + + if (subC.equals("ADD")) { + if(argc != ROOM_FIELD_COUNT + 1) + return false; + this.type = Type.ROOM_ADD; + this.args = withoutFirst(args, 1); + } + else if (subC.equals("UPD")) { + if(argc != ROOM_FIELD_COUNT + 2) + return false; + this.type = Type.ROOM_UPD; + this.args = withoutFirst(args, 1); + } + else if (subC.equals("DEL") && (argc == 2)) { + this.type = Type.ROOM_DEL; + this.args = withoutFirst(args, 1); + } + else + return false; + break; + + // two arguments or more + case CFG: // setting name and list of setting parameters + if (argc < 2) + return false; + break; + case CLIENT_FLAGS: // string of changed flags and player name(s) + case CF: // alias of CLIENT_FLAGS + if (argc < 2) + return false; + if (this.type == Type.CF) + this.type = Type.CLIENT_FLAGS; + break; + + // four arguments + case INFO: // info about a player, name, ip/id, version, room + if (argc != 4) + return false; + break; + + // multiple of ROOM_FIELD_COUNT arguments (incl. 0) + case ROOMS: + if (argc % ROOM_FIELD_COUNT != 0) + return false; + break; + } + + return true; + } + + private void maybeSendPassword() { + + } + + public Type getType() + { + return type; + } + + public String[] getArguments() + { + return args; + } + + public boolean isValid() + { + return isValid; + } + + public static String partsToString(final List parts) + { + final Iterator iter = parts.iterator(); + + if (!iter.hasNext()) + return "( -EMPTY- )"; + + String result = "(\"" + iter.next(); + + while (iter.hasNext()) { + result += "\", \"" + iter.next(); + } + + return result + "\")"; + } + + public String toString() { + return partsToString(this.parts); + } +} diff -r 64740eec84ad -r 4c523ed1d35c tools/hw2irc/net/ercatec/hw2ircsvr/Connection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw2ircsvr/Connection.java Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,1814 @@ +package net.ercatec.hw2ircsvr; + +import net.ercatec.hw.INetClient; +import net.ercatec.hw.ProtocolConnection; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.Collections; +import java.util.Vector; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import java.lang.IllegalArgumentException; + +// for auth files +import java.util.Properties; +import java.io.FileInputStream; +import java.io.IOException; + +/* TODO + * disconnect clients that are not irc clients + * disconnect excess flooders + * recognizes stuff after : as single arg + * collect pre-irc-join messages and show on join + * allow negating regexps + * ban + * banlist + * commandquery // wth did I mean by that? + * more room info + * filter rooms + * warnings + * global notice + */ + +/** + * @author sheepluva + * + * based on jircs by Alexander Boyd + */ +public class Connection implements INetClient, Runnable +{ + private static final String DESCRIPTION_SHORT + = "connect to hedgewars via irc!"; + + private static final String VERSION = "0.6.7-Alpha_2015-11-07"; + + + private static final String MAGIC_BYTES = "[\1\2\3]"; + private static final char MAGIC_BYTE_ACTION = ((char)1); // ^A + private static final char MAGIC_BYTE_BOLD = ((char)2); // ^B + private static final char MAGIC_BYTE_COLOR = ((char)3); // ^C + + private static final String[] DEFAULT_MOTD = { + " ", + " "+MAGIC_BYTE_COLOR+"06"+MAGIC_BYTE_BOLD+" SUCH FLUFFY!", + " ", + " "+MAGIC_BYTE_COLOR+"04 MUCH BAH "+MAGIC_BYTE_COLOR+"00__ _ ", + " "+MAGIC_BYTE_COLOR+"00 .-.' `; `-."+MAGIC_BYTE_COLOR+"00_ __ _ ", + " "+MAGIC_BYTE_COLOR+"00 (_, .-:' `; `"+MAGIC_BYTE_COLOR+"00-._ ", + " "+MAGIC_BYTE_COLOR+"14 ,'"+MAGIC_BYTE_COLOR+"02o "+MAGIC_BYTE_COLOR+"00( (_, ) ", + " "+MAGIC_BYTE_COLOR+"14 (__"+MAGIC_BYTE_COLOR+"00,-' "+MAGIC_BYTE_COLOR+"15,'"+MAGIC_BYTE_COLOR+"12o "+MAGIC_BYTE_COLOR+"00( )> ", + " "+MAGIC_BYTE_COLOR+"00 ( "+MAGIC_BYTE_COLOR+"15(__"+MAGIC_BYTE_COLOR+"00,-' ) ", + " "+MAGIC_BYTE_COLOR+"00 `-'._.--._( ) ", + " "+MAGIC_BYTE_COLOR+"14 ||| |||"+MAGIC_BYTE_COLOR+"00`-'._.--._.-' ", + " "+MAGIC_BYTE_COLOR+"15 ||| ||| ", + " "+MAGIC_BYTE_COLOR+"07"+MAGIC_BYTE_BOLD+" WOW! ", + " "+MAGIC_BYTE_COLOR+"09 VERY SHEEP ", + " ", + " ", + " ", + " "+MAGIC_BYTE_COLOR+"4 Latest hw2irc crimes/changes:", + " ping: ping of hwserver will only get reply if irc client pingable", + " ping: pings of irc clients will only get reply if hwserver pingable", + " rooms: id-rotation, make channel names at least 2 digits wide", + " auth: support passhash being loaded local auth file and irc pass (sent as cleartext - DO NOT USE!)", + " ", + " ", + }; + + + private static final String DEFAULT_QUIT_REASON = "User quit"; + // NOT final + private static char CHAT_COMMAND_CHAR = '\\'; + + private final class Room { + public final int id; + public final String chan; + public String name; + private String owner = ""; + public int nPlayers = 0; + public int nTeams = 0; + + public Room(final int id, final String name, final String owner) { + this.id = id; + this.chan = (id<10?"#0":"#") + id; + this.name = name; + this.setOwner(owner); + } + + public String getOwner() { return this.owner; } + + public void setOwner(final String owner) { + // don't to this for first owner + if (!this.owner.isEmpty()) { + + // owner didn't change + if (this.owner.equals(owner)) + return; + + // update old room owner + final Player oldOwner = allPlayers.get(this.owner); + + if (oldOwner != null) + oldOwner.isRoomAdm = false; + + } + + // update new room owner + final Player newOwner = allPlayers.get(owner); + + if (newOwner != null) + newOwner.isRoomAdm = true; + + this.owner = owner; + + } + } + + private final class Player { + public final String nick; + public final String ircNick; + private boolean isAdm; + private boolean isCont; + private boolean isReg; + public boolean inRoom; + public boolean isRoomAdm; + private String ircId; + private String ircHostname; + private boolean announced; + + // server info + private String version = ""; + private String ip = ""; + private String room = ""; + + public Player(final String nick) { + this.nick = nick; + this.ircNick = hwToIrcNick(nick); + this.announced = false; + updateIrcHostname(); + } + + public String getIrcHostname() { return ircHostname; } + public String getIrcId() { return ircId; } + + public String getRoom() { + if (room.isEmpty()) + return room; + + return "[" + ((isAdm?"@":"") + (isRoomAdm?"+":"") + this.room); + } + + public boolean needsAnnounce() { + return !announced; + } + + public void setAnnounced() { + announced = true; + } + + public void setInfo(final String ip, final String version, final String room) { + if (this.version.isEmpty()) { + this.version = version; + this.ip = ip.replaceAll("^\\[|]$", ""); + updateIrcHostname(); + } + + if (room.isEmpty()) + this.room = room; + else + this.room = room.replaceAll("^\\[[@+]*", ""); + } + + public boolean isServerAdmin() { return isAdm; } + //public boolean isContributor() { return isCont; } + public boolean isRegistered() { return isReg; } + + public void setServerAdmin(boolean isAdm) { + this.isAdm = isAdm; updateIrcHostname(); } + public void setContributor(boolean isCont) { + this.isCont = isCont; updateIrcHostname(); } + public void setRegistered(boolean isReg) { + this.isReg = isReg; updateIrcHostname(); } + + private void updateIrcHostname() { + ircHostname = ip.isEmpty()?"":(ip + '/'); + ircHostname += "hw/"; + if (!version.isEmpty()) + ircHostname += version; + if (isAdm) + ircHostname += "/admin"; + else if (isCont) + ircHostname += "/contributor"; + else if (isReg) + ircHostname += "/member"; + else + ircHostname += "/player"; + + updateIrcId(); + } + + private void updateIrcId() { + ircId = ircNick + "!~" + ircNick + "@" + ircHostname; + } + } + + public String hw404NickToIrcId(String nick) { + nick = hwToIrcNick(nick); + return nick + "!~" + nick + "@hw/404"; + } + + // hash tables are thread-safe + private final Map allPlayers = new Hashtable(); + private final Map roomPlayers = new Hashtable(); + private final Map roomsById = new Hashtable(); + private final Map roomsByName = new Hashtable(); + private final List roomsSorted = new Vector(); + + private final List ircPingQueue = new Vector(); + + private static final String DEFAULT_SERVER_HOST = "netserver.hedgewars.org"; + private static String SERVER_HOST = DEFAULT_SERVER_HOST; + private static int IRC_PORT = 46667; + + private String hostname; + + private static final String LOBBY_CHANNEL_NAME = "#lobby"; + private static final String ROOM_CHANNEL_NAME = "#room"; + + // hack + // TODO: , + private static final char MAGIC_SPACE = ' '; + private static final char MAGIC_ATSIGN = '៙'; + private static final char MAGIC_PERCENT = '%'; + private static final char MAGIC_PLUS = '+'; + private static final char MAGIC_EXCLAM = '❢'; + private static final char MAGIC_COMMA = ','; + private static final char MAGIC_COLON = ':'; + + private static String hwToIrcNick(final String nick) { + return nick + .replace(' ', MAGIC_SPACE) + .replace('@', MAGIC_ATSIGN) + .replace('%', MAGIC_PERCENT) + .replace('+', MAGIC_PLUS) + .replace('!', MAGIC_EXCLAM) + .replace(',', MAGIC_COMMA) + .replace(':', MAGIC_COLON) + ; + } + private static String ircToHwNick(final String nick) { + return nick + .replace(MAGIC_COLON, ':') + .replace(MAGIC_COMMA, ',') + .replace(MAGIC_EXCLAM, '!') + .replace(MAGIC_PLUS, '+') + .replace(MAGIC_PERCENT, '%') + .replace(MAGIC_ATSIGN, '@') + .replace(MAGIC_SPACE, ' ') + ; + } + + private ProtocolConnection hwcon; + private boolean joined = false; + private boolean ircJoined = false; + + private void collectFurtherInfo() { + hwcon.sendPing(); + hwcon.processNextClientFlagsMessages(); + } + + public void onPing() { + send("PING :" + globalServerName); + } + + public void onPong() { + if (!ircPingQueue.isEmpty()) + send(":" + globalServerName + " PONG " + globalServerName + + " :" + ircPingQueue.remove(0)); + + } + + public void onConnectionLoss() { + quit("Connection Loss"); + } + + public void onDisconnect(final String reason) { + quit(reason); + } + + public String onPasswordHashNeededForAuth() { + return passwordHash; + } + + public void onMalformedMessage(String contents) + { + this.logError("MALFORMED MESSAGE: " + contents); + } + + public void onChat(final String user, final String message) { + String ircId; + Player player = allPlayers.get(user); + if (player == null) { + // fake user - so probably a notice + sendChannelNotice(message, hwToIrcNick(user)); + //logWarning("onChat(): Couldn't find player with specified nick! nick: " + user); + //send(":" + hw404NickToIrcId(user) + " PRIVMSG " + //+ LOBBY_CHANNEL_NAME + " :" + hwActionToIrc(message)); + } + else + send(":" + player.getIrcId() + " PRIVMSG " + + LOBBY_CHANNEL_NAME + " :" + hwActionToIrc(message)); + } + + public void onWelcomeMessage(final String message) { + } + + public void onNotice(int number) { + } + + public void onBanListEntry(BanType type, String target, String duration, String reason) { + // TODO + } + public void onBanListEnd() { + // TODO + } + + public String onNickCollision(final String nick) { + return nick + "_"; + } + + public void onNickSet(final String nick) { + final String newNick = hwToIrcNick(nick); + // tell irc client + send(":" + ownIrcNick + "!~" + username + "@" + + hostname + " NICK :" + nick); + ownIrcNick = newNick; + updateLogPrefix(); + logInfo("Nickname set to " + nick); + } + + private void flagAsInLobby(final Player player) { + if (!ircJoined) + return; + final String ircNick = player.ircNick; + if (player.isServerAdmin()) + send(":room-part!~@~ MODE " + LOBBY_CHANNEL_NAME + " -h+o " + ircNick + " " + ircNick); + //else + //send(":room-part!~@~ MODE " + LOBBY_CHANNEL_NAME + " +v " + ircNick); + } + + private void flagAsInRoom(final Player player) { + if (!ircJoined) + return; + final String ircNick = player.ircNick; + if (player.isServerAdmin()) + send(":room-join!~@~ MODE " + LOBBY_CHANNEL_NAME + " -o+h " + ircNick + " " + ircNick); + //else + //send(":room-join!~@~ MODE " + LOBBY_CHANNEL_NAME + " -v " + ircNick); + } + +// TODO somewhere: escape char for magic chars! + +// TODO /join with playername => FOLLOW :D + + public void sendPlayerMode(final Player player) { + char c; + if (player.isServerAdmin()) + c = player.inRoom?'h':'o'; + else if (player.isRegistered()) + c = 'v'; + else + // no mode + return; + + send(":server-join!~@~ MODE " + LOBBY_CHANNEL_NAME + " +" + c + " " + player.ircNick); + } + + private Player ownPlayer = null; + + public void onLobbyJoin(final String[] users) { + + final List newPlayers = new ArrayList(users.length); + + // process joins + for (final String user : users) { + final Player player = new Player(user); + if (ownPlayer == null) + ownPlayer = player; + newPlayers.add(player); + allPlayers.put(user, player); + } + + // make sure we get the client flags before we announce anything + collectFurtherInfo(); + + // get player info + // NOTE: if player is in room, then info was already retrieved + for (final Player player : newPlayers) { + if (!player.inRoom) + hwcon.requestInfo(player.nick); + } + + /* DISABLED - we'll announce later - when receiving info + // announce joins + if (ircJoined) { + for (final Player player : newPlayers) { + final String ircId = player.getIrcId(); + send(":" + ircId + + " JOIN "+ lobbyChannel.name); + sendPlayerMode(player); + } + } + */ + if (!ircJoined) { + // don't announced players that were there before join already + for (final Player player : newPlayers) { + player.setAnnounced(); + } + } + + if (!joined) { + joined = true; + // forget password hash, we don't need it anymore. + passwordHash = ""; + logInfo("Hedgewars server/lobby joined."); + sendSelfNotice("Hedgewars server was joined successfully"); + // do this after join so that rooms can be assigned to their owners + hwcon.requestRoomsList(); + } + } + + private void makeIrcJoinLobby() { + sendGlobal("INVITE " + ownIrcNick + " " + LOBBY_CHANNEL_NAME); + try{Thread.sleep(3000);}catch(Exception e){} + join(lobbyChannel.name); + sendSelfNotice("Joining lobby-channel: " + lobbyChannel.name); + } + + private void announcePlayerJoinLobby(final Player player) { + player.setAnnounced(); + send(":" + player.getIrcId() + + " JOIN "+ lobbyChannel.name); + sendPlayerMode(player); + } + + public void onLobbyLeave(final String user, final String reason) { + final Player player = allPlayers.get(user); + if (player == null) { + logWarning("onLobbyLeave(): Couldn't find player with specified nick! nick: " + user); + sendIfJoined(":" + hw404NickToIrcId(user) + + " PART " + lobbyChannel.name + " " + reason); + } + else { + if (ircJoined && player.needsAnnounce()) + announcePlayerJoinLobby(player); + sendIfJoined(":" + player.getIrcId() + + " PART " + lobbyChannel.name + " " + reason); + allPlayers.remove(user); + } + } + + private int lastRoomId = 0; + + public void onRoomInfo(final String name, final String flags, + final String newName, final int nUsers, + final int nTeams, final String owner, + final String map, final String style, + final String scheme, final String weapons) { + + Room room = roomsByName.get(name); + + if (room == null) { + // try to reuse old ids + if (lastRoomId >= 90) + lastRoomId = 9; + + // search for first free + while(roomsById.containsKey(++lastRoomId)) { } + + room = new Room(lastRoomId, newName, owner); + roomsById.put(lastRoomId, room); + roomsByName.put(newName, room); + roomsSorted.add(room); + } + else if (!room.name.equals(newName)) { + room.name = newName; + roomsByName.put(newName, roomsByName.remove(name)); + } + + // update data + room.setOwner(owner); + room.nPlayers = nUsers; + room.nTeams = nTeams; + } + + public void onRoomDel(final String name) { + final Room room = roomsByName.remove(name); + + if (room != null) { + roomsById.remove(room.id); + roomsSorted.remove(room); + } + } + + public void onRoomJoin(final String[] users) { + } + + public void onRoomLeave(final String[] users) { + } + + // TODO vector that remembers who's info was requested for manually + List requestedInfos = new Vector(); + + public void onUserInfo(final String user, final String ip, final String version, final String room) { + Player player = allPlayers.get(user); + if (player != null) { + player.setInfo(ip, version, room); + if (ircJoined) { + if (player.needsAnnounce()) + announcePlayerJoinLobby(player); + } + else { + if (player == ownPlayer) { + + makeIrcJoinLobby(); + } + } + } + + // if MANUAL send notice + if (requestedInfos.remove(user)) { + final String nick = hwToIrcNick(user); + sendServerNotice(nick + " - " + buildInfoString(ip, version, room)); + } + } + + public void onUserFlagChange(final String user, final UserFlagType flag, final boolean newValue) { + final Player player = allPlayers.get(user); + if (player == null) { + logError("onUserFlagChange(): Couldn't find player with specified nick! nick: " + user); + return; + } + switch (flag) { + case ADMIN: + player.setServerAdmin(newValue); + if (newValue) { + logDebug(user + " is server admin"); + //sendIfJoined(":server!~@~ MODE " + LOBBY_CHANNEL_NAME + " -v+o " + player.ircNick + " " + player.ircNick); + } + break; + case INROOM: + player.inRoom = newValue; + if (newValue) { + flagAsInRoom(player); + logDebug(user + " entered a room"); + // get new room info + hwcon.requestInfo(player.nick); + } + else { + flagAsInLobby(player); + logDebug(user + " returned to lobby"); + player.inRoom = false; + } + break; + case REGISTERED: + player.setRegistered(newValue); + break; + default: break; + } + } + + public class Channel + { + private String topic; + private final String name; + private final Map players; + + public Channel(final String name, final String topic, final Map players) { + this.name = name; + this.topic = topic; + this.players = players; + } + } + + public void logInfo(final String message) { + System.out.println(this.logPrefix + ": " + message); + } + + public void logDebug(final String message) { + System.out.println(this.logPrefix + "| " + message); + } + + public void logWarning(final String message) { + System.err.println(this.logPrefix + "? " + message); + } + + public void logError(final String message) { + System.err.println(this.logPrefix + "! " + message); + } + + + //private static final Object mutex = new Object(); + private boolean joinSent = false; + private Socket socket; + private String username; + private String ownIrcNick; + private String description; + private static Map connectionMap = new HashMap(); + // TODO those MUST NOT be static! + //private Map channelMap = new HashMap(); + private final Channel lobbyChannel; + private static String globalServerName; + private String logPrefix; + private final String clientId; + private String passwordHash = ""; + + private final Connection thisConnection; + + public Connection(Socket socket, final String clientId) throws Exception + { + this.ownIrcNick = "NONAME"; + this.socket = socket; + this.hostname = ((InetSocketAddress)socket.getRemoteSocketAddress()) + .getAddress().getHostAddress(); + this.clientId = clientId; + updateLogPrefix(); + thisConnection = this; + logInfo("New Connection"); + + this.hwcon = null; + + try { + this.hwcon = new ProtocolConnection(this, SERVER_HOST); + logInfo("Connection to " + SERVER_HOST + " established."); + } + catch(Exception ex) { + final String errmsg = "Could not connect to " + SERVER_HOST + ": " + + ex.getMessage(); + logError(errmsg); + sendQuit(errmsg); + } + + final String lobbyTopic = " # " + SERVER_HOST + " - HEDGEWARS SERVER LOBBY # "; + this.lobbyChannel = new Channel(LOBBY_CHANNEL_NAME, lobbyTopic, allPlayers); + + // start in new thread + if (hwcon != null) { + (this.hwcon.processMessages(true)).start(); + } + } + + private void updateLogPrefix() { + if (ownIrcNick == null) + this.logPrefix = clientId + " "; + else + this.logPrefix = clientId + " [" + ownIrcNick + "] "; + } + + private void setNick(final String nick) { + if (passwordHash.isEmpty()) { + try { + final Properties authProps = new Properties(); + final String authFile = this.hostname + ".auth"; + logInfo("Attempting to load auth info from " + authFile); + authProps.load(new FileInputStream(authFile)); + passwordHash = authProps.getProperty(nick, ""); + if (passwordHash.isEmpty()) + logInfo("Auth info file didn't contain any password hash for: " + nick); + } catch (IOException e) { + logInfo("Auth info file couldn't be loaded."); + } + } + + // append _ just in case + if (!passwordHash.isEmpty() || nick.endsWith("_")) { + ownIrcNick = nick; + hwcon.setNick(ircToHwNick(nick)); + } + else { + final String nick_ = nick + "_"; + ownIrcNick = nick_; + hwcon.setNick(ircToHwNick(nick_)); + } + } + + public String getRepresentation() + { + return ownIrcNick + "!~" + username + "@" + hostname; + } + + private static int lastClientId = 0; + + /** + * @param args + */ + public static void main(String[] args) throws Throwable + { + if (args.length > 0) + { + SERVER_HOST = args[0]; + } + if (args.length > 1) + { + IRC_PORT = Integer.parseInt(args[1]); + } + + globalServerName = "hw2irc"; + + if (!SERVER_HOST.equals(DEFAULT_SERVER_HOST)) + globalServerName += "~" + SERVER_HOST; + + final int port = IRC_PORT; + ServerSocket ss = new ServerSocket(port); + System.out.println("Listening on port " + port); + while (true) + { + Socket s = ss.accept(); + final String clientId = "client" + (++lastClientId) + '-' + + ((InetSocketAddress)s.getRemoteSocketAddress()) + .getAddress().getHostAddress(); + try { + Connection clientCon = new Connection(s, clientId); + //clientCon.run(); + Thread clientThread = new Thread(clientCon, clientId); + clientThread.start(); + } + catch (Exception ex) { + System.err.println("FATAL: Server connection thread " + clientId + " crashed on startup! " + ex.getMessage()); + ex.printStackTrace(); + } + + System.out.println("Note: Not accepting new clients for the next " + SLEEP_BETWEEN_LOGIN_DURATION + "s, trying to avoid reconnecting too quickly."); + Thread.sleep(SLEEP_BETWEEN_LOGIN_DURATION * 1000); + System.out.println("Note: Accepting clients again!"); + } + } + + private static final int SLEEP_BETWEEN_LOGIN_DURATION = 122; + + private boolean hasQuit = false; + + public synchronized void quit(final String reason) { + if (hasQuit) + return; + + hasQuit = true; + // disconnect from hedgewars server + if (hwcon != null) + hwcon.disconnect(reason); + // disconnect irc client + sendQuit("Quit: " + reason); + // wait some time so that last data can be pushed + try { + Thread.sleep(200); + } + catch (Exception e) { } + // terminate + terminateConnection = true; + } + + + private static String hwActionToIrc(final String chatMsg) { + if (!chatMsg.startsWith("/me ") || (chatMsg.length() <= 4)) + return chatMsg; + + return MAGIC_BYTE_ACTION + "ACTION " + chatMsg.substring(4) + MAGIC_BYTE_ACTION; + } + + private static String ircActionToHw(final String chatMsg) { + if (!chatMsg.startsWith(MAGIC_BYTE_ACTION + "ACTION ") || (chatMsg.length() <= 9)) + return chatMsg; + + return "/me " + chatMsg.substring(8, chatMsg.length() - 1); + } + +// TODO: why is still still being called when joining bogus channel name? + public void join(String channelName) + { + if (ownPlayer == null) { + sendSelfNotice("Trying to join while ownPlayer == null. Aborting!"); + quit("Something went horribly wrong."); + return; + } + + + final Channel channel = getChannel(channelName); + + // TODO reserve special char for creating a new ROOM + // it will be named after the player name by default + // can be changed with /topic after + + // not a valid channel + if (channel == null) { + sendSelfNotice("You cannot manually create channels here."); + sendGlobal(ERR_NOSUCHCHANNEL + ownIrcNick + " " + channel.name + + " :No such channel"); + return; + } + + // TODO if inRoom "Can't join rooms while still in room" + + // TODO set this based on room host/admin mode maybe + +/* :testuser2131!~r@asdasdasdasd.at JOIN #asdkjasda +:weber.freenode.net MODE #asdkjasda +ns +:weber.freenode.net 353 testuser2131 @ #asdkjasda :@testuser2131 +:weber.freenode.net 366 testuser2131 #asdkjasda :End of /NAMES list. +:weber.freenode.net NOTICE #asdkjasda :[freenode-info] why register and identify? your IRC nick is how people know you. http://freenode.net/faq.shtml#nicksetup + +*/ + send(":" + ownPlayer.getIrcId() + " JOIN " + + channelName); + + //send(":sheeppidgin!~r@localhost JOIN " + channelName); + + ircJoined = true; + + sendGlobal(":hw2irc MODE #lobby +nt"); + + sendTopic(channel); + + sendNames(channel); + + } + + private void sendTopic(final Channel channel) { + if (channel.topic != null) + sendGlobal(RPL_TOPIC + ownIrcNick + " " + channel.name + + " :" + channel.topic); + else + sendGlobal(RPL_NOTOPIC + ownIrcNick + " " + channel.name + + " :No topic is set"); + } + + private void sendNames(final Channel channel) { + // There is no error reply for bad channel names. + + if (channel != null) { + // send player list + for (final Player player : channel.players.values()) { + + final String prefix; + + if (player.isServerAdmin()) + prefix = (player.isServerAdmin())?"@":"%"; + else + prefix = (player.isRegistered())?"+":""; + + sendGlobal(RPL_NAMREPLY + ownIrcNick + " = " + channel.name + + " :" + prefix + player.ircNick); + } + } + + sendGlobal(RPL_ENDOFNAMES + ownIrcNick + " " + channel.name + + " :End of /NAMES list"); + } + + private void sendList(final String filter) { + // id column size + //int idl = 1 + String.valueOf(lastRoomId).length(); + + //if (idl < 3) + //idl = 3; + + // send rooms list + sendGlobal(RPL_LISTSTART + ownIrcNick + //+ String.format(" %1$" + idl + "s #P #T Name", "ID")); + + String.format(" %1$s #P #T Name", "ID")); + + if (filter.isEmpty() || filter.equals(".")) { + // lobby + if (filter.isEmpty()) + sendGlobal(RPL_LIST + ownIrcNick + " " + LOBBY_CHANNEL_NAME + + " " + allPlayers.size() + " :" + lobbyChannel.topic); + + // room list could be changed by server while we reply client + synchronized (roomsSorted) { + for (final Room room : roomsSorted) { + sendGlobal(RPL_LIST + ownIrcNick + //+ String.format(" %1$" + idl + "s %2$2d :%3$2d %4$s", + + String.format(" %1$s %2$d :%3$d %4$s", + room.chan, room.nPlayers, room.nTeams, room.name)); + } + } + } + // TODO filter + + sendGlobal(RPL_LISTEND + ownIrcNick + " " + " :End of /LIST"); + } + + private List findPlayers(final String expr) { + List matches = new ArrayList(allPlayers.size()); + + try { + final int flags = Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE; + final Pattern regx = Pattern.compile(expr, flags); + + for (final Player p : allPlayers.values()) { + if ((regx.matcher(p.nick).find()) + || (regx.matcher(p.ircId).find()) + //|| (regx.matcher(p.version).find()) + //|| ((p.ip.length() > 2) && regx.matcher(p.ip).find()) + || (!p.getRoom().isEmpty() && regx.matcher(p.getRoom()).find()) + ) matches.add(p); + } + } + catch(PatternSyntaxException ex) { + sendSelfNotice("Pattern not understood: " + ex.getMessage()); + } + + return matches; + } + + private String buildInfoString(final String ip, final String version, final String room) { + return (ip.equals("[]")?"":ip + " ") + version + (room.isEmpty()?"":" " + room); + } + + private void sendWhoForPlayer(final Player player) { + sendWhoForPlayer(LOBBY_CHANNEL_NAME, player.ircNick, (player.inRoom?player.getRoom():""), player.getIrcHostname()); + } + + private void sendWhoForPlayer(final Player player, final String info) { + sendWhoForPlayer(LOBBY_CHANNEL_NAME, player.ircNick, info, player.getIrcHostname()); + } + + private void sendWhoForPlayer(final String nick, final String info) { + sendWhoForPlayer(LOBBY_CHANNEL_NAME, nick, info); + } + + private void sendWhoForPlayer(final String channel, final String nick, final String info) { + final Player player = allPlayers.get(nick); + + if (player == null) + sendWhoForPlayer("OFFLINE", hwToIrcNick(nick), info, "hw/offline"); + else + sendWhoForPlayer(channel, player.ircNick, info, player.getIrcHostname()); + } + + private void sendWhoForPlayer(final String channel, final String ircNick, final String info, final String hostname) { + sendGlobal(RPL_WHOREPLY + channel + " " + channel + + " ~" + ircNick + " " + hostname + + " " + globalServerName + " " + ircNick + + " H :0 " + info); + } + + private void sendWhoEnd(final String ofWho) { + send(RPL_ENDOFWHO + ownIrcNick + " " + ofWho + + " :End of /WHO list."); + } + + private void sendMotd() { + sendGlobal(RPL_MOTDSTART + ownIrcNick + " :- Message of the Day -"); + final String mline = RPL_MOTD + ownIrcNick + " :"; + for(final String line : DEFAULT_MOTD) { + sendGlobal(mline + line); + } + sendGlobal(RPL_ENDOFMOTD + ownIrcNick + " :End of /MOTD command."); + } + + private Channel getChannel(final String name) { + if (name.equals(LOBBY_CHANNEL_NAME)) { + return lobbyChannel; + } + + return null; + } + + private enum Command + { + PASS(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.passwordHash = args[0]; + } + }, + NICK(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.setNick(args[0]); + } + }, + USER(1, 4) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + if (con.username != null) + { + con.send("NOTICE AUTH :You can't change your user " + + "information after you've logged in right now."); + return; + } + con.username = args[0]; + String forDescription = args.length > 3 ? args[3] + : "(no description)"; + con.description = forDescription; + /* + * Now we'll send the user their initial information. + */ + con.sendGlobal(RPL_WELCOME + con.ownIrcNick + " :Welcome to " + + globalServerName + " - " + DESCRIPTION_SHORT); + con.sendGlobal("004 " + con.ownIrcNick + " " + globalServerName + + " " + VERSION); + + con.sendMotd(); + + } + }, + MOTD(0, 0) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.sendMotd(); + } + }, + PING(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.ircPingQueue.add(args[0]); + con.hwcon.sendPing(); + } + }, + PONG(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.hwcon.sendPong(); + } + }, + NAMES(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final Channel channel = con.getChannel(args[0]); + con.sendNames(channel); + } + }, + LIST(0, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + // TODO filter by args[1] (comma sep list of chans), make # optional + // ignore args[1] (server), TODO: maybe check and send RPL_NOSUCHSERVER if wrong + con.sendList(args.length > 0?args[0]:""); + } + }, + JOIN(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + if (args.length < 1) { + con.sendSelfNotice("You didn't specify what you want to join!"); + return; + } + + if (con.ownPlayer == null) { + con.sendSelfNotice("Lobby is not ready to be joined yet - hold on a second!"); + return; + } + + if (args[0].equals(LOBBY_CHANNEL_NAME)) { + //con.sendSelfNotice("Lobby can't be joined manually!"); + con.join(LOBBY_CHANNEL_NAME); + return; + } + con.sendSelfNotice("Joining rooms is not supported yet, sorry!"); + } + }, + WHO(0, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + if (args.length < 1) + return; + + final String target = args[0]; + + Map players = null; + + if (target.equals(LOBBY_CHANNEL_NAME)) { + players = con.allPlayers; + } + // on channel join WHO is called on channel + else if (target.equals(ROOM_CHANNEL_NAME)) { + players = con.roomPlayers; + } + + if (players != null) { + for (final Player player : players.values()) { + con.sendWhoForPlayer(player); + } + } + // not a known channel. assume arg is player name + // TODO support search expressions! + else { + final String nick = ircToHwNick(target); + final Player player = con.allPlayers.get(nick); + if (player != null) + con.sendWhoForPlayer(player); + else { + con.sendSelfNotice("WHO: No player named " + nick + ", interpreting term as pattern."); + List matches = con.findPlayers(target); + if (matches.isEmpty()) + con.sendSelfNotice("No Match."); + else { + for (final Player match : matches) { + con.sendWhoForPlayer(match); + } + } + } + } + + con.sendWhoEnd(target); + } + }, + WHOIS(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + // there's an optional param in the beginning that we don't care about + final String targets = args[args.length-1]; + for (final String target : targets.split(",")) { + if (target.isEmpty()) + continue; + final String nick = ircToHwNick(target); + // flag this nick as manually requested, so that response is shown + if (con.ircJoined) { + con.requestedInfos.add(nick); + con.hwcon.requestInfo(nick); + } + + final Player player = con.allPlayers.get(nick); + if (player != null) { + con.send(RPL_WHOISUSER + con.ownIrcNick + " " + target + " ~" + + target + " " + player.getIrcHostname() + " * : " + + player.ircNick); + // TODO send e.g. channels: @#lobby or @#123 + con.send(RPL_ENDOFWHOIS + con.ownIrcNick + " " + target + + " :End of /WHOIS list."); + } + } + } + }, + USERHOST(1, 5) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + /* + // TODO set server host + con.hostname = "hw/" + SERVER_HOST; + + ArrayList replies = new ArrayList(); + for (String s : arguments) + { + Connection user = connectionMap.get(s); + if (user != null) + replies.add(user.nick + "=+" + con.ownIrc + "@" + + con.hostname); + } + con.sendGlobal(RPL_USERHOST + con.ownIrcNick + " :" + + delimited(replies.toArray(new String[0]), " ")); + */ + } + }, + MODE(0, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final boolean forChan = args[0].startsWith("#"); + + if (args.length == 1) + { + if (forChan) { + //con.sendGlobal(ERR_NOCHANMODES + args[0] + // + " :Channel doesn't support modes"); + con.sendGlobal(RPL_CHANNELMODEIS + con.ownIrcNick + " " + args[0] + + " +nt"); + } + else + { + // TODO + con.sendSelfNotice("User mode querying not supported yet."); + } + } + else if (args.length == 2) { + + if (forChan) { + + final int l = args[1].length(); + + for (int i = 0; i < l; i++) { + + final char c = args[1].charAt(i); + + switch (c) { + case '+': + // skip + break; + case '-': + // skip + break; + case 'b': + con.sendGlobal(RPL_ENDOFBANLIST + + con.ownIrcNick + " " + args[0] + + " :End of channel ban list"); + break; + case 'e': + con.sendGlobal(RPL_ENDOFEXCEPTLIST + + con.ownIrcNick + " " + args[0] + + " :End of channel exception list"); + break; + default: + con.sendGlobal(ERR_UNKNOWNMODE + c + + " :Unknown MODE flag " + c); + break; + + } + } + } + // user mode + else { + con.sendGlobal(ERR_UMODEUNKNOWNFLAG + args[0] + + " :Unknown MODE flag"); + } + } + else + { + con.sendSelfNotice("Specific modes not supported yet."); + } + } + }, + PART(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + String[] channels = args[0].split(","); + boolean doQuit = false; + + for (String channelName : channels) + { + if (channelName.equals(LOBBY_CHANNEL_NAME)) { + doQuit = true; + } + // TODO: part from room + /* + synchronized (mutex) + { + Channel channel = channelMap.get(channelName); + if (channelName == null) + con + .sendSelfNotice("You're not a member of the channel " + + channelName + + ", so you can't part it."); + else + { + channel.send(":" + con.getRepresentation() + + " PART " + channelName); + channel.channelMembers.remove(con); + if (channel.channelMembers.size() == 0) + channelMap.remove(channelName); + } + } + */ + } + + final String reason; + + if (args.length > 1) + reason = args[1]; + else + reason = DEFAULT_QUIT_REASON; + + // quit after parting + if (doQuit) + con.quit(reason); + } + }, + QUIT(0, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final String reason; + + if (args.length == 0) + reason = DEFAULT_QUIT_REASON; + else + reason = args[0]; + + con.quit(reason); + } + }, + PRIVMSG(2, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + String message = ircActionToHw(args[1]); + if (message.charAt(0) == CHAT_COMMAND_CHAR) { + if (message.length() < 1 ) + return; + message = message.substring(1); + // TODO maybe \rebind CUSTOMCMDCHAR command + con.hwcon.sendCommand(con.ircToHwNick(message)); + } + else + con.hwcon.sendChat(con.ircToHwNick(message)); + } + }, + TOPIC(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final String chan = args[0]; + + final Channel channel = con.getChannel(chan); + + if (channel == null) { + con.sendSelfNotice("No such channel for topic viewing: " + + chan); + return; + } + + // The user wants to see the channel topic. + if (args.length == 1) + con.sendTopic(channel); + // The user wants to set the channel topic. + else + channel.topic = args[1]; + } + }, + KICK(3, 3) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final String victim = args[1]; + con.logInfo("Issuing kick for " + victim); + // "KICK #channel nick :kick reason (not relevant)" + con.hwcon.kick(ircToHwNick(victim)); + } + } + ; + public final int minArgumentCount; + public final int maxArgumentCount; + + private Command(int min, int max) + { + minArgumentCount = min; + maxArgumentCount = max; + } + + public abstract void run(Connection con, String prefix, + String[] arguments) throws Exception; + } + + public static String delimited(String[] items, String delimiter) + { + StringBuffer response = new StringBuffer(); + boolean first = true; + for (String s : items) + { + if (first) + first = false; + else + response.append(delimiter); + response.append(s); + } + return response.toString(); + } + + protected void sendQuit(String quitMessage) + { + send(":" + getRepresentation() + " QUIT :" + quitMessage); + } + + @Override + public void run() + { + try + { + doServer(); + } + catch (Exception e) { + e.printStackTrace(); + } + finally + { + // TODO sense? + if (ownIrcNick != null && connectionMap.get(ownIrcNick) == this) { + quit("Client disconnected."); + } + + try { + socket.close(); + } + catch (Exception e) { } + + quit("Connection terminated."); + } + } + + protected void sendGlobal(String string) + { + send(":" + globalServerName + " " + string); + } + + private LinkedBlockingQueue outQueue = new LinkedBlockingQueue( + 1000); + + private Thread outThread = new Thread() + { + public void run() + { + try + { + OutputStream out = socket.getOutputStream(); + while (!terminateConnection) + { + String s = outQueue.take(); + s = s.replace("\n", "").replace("\r", ""); + s = s + "\r\n"; + out.write(s.getBytes()); + out.flush(); + } + } + catch (Exception ex) + { + thisConnection.logError("Outqueue died"); + //ex.printStackTrace(); + } + finally { + outQueue.clear(); + outQueue = null; + try + { + socket.close(); + } + catch (Exception e2) + { + e2.printStackTrace(); + } + } + } + }; + + private boolean terminateConnection = false; + + private void doServer() throws Exception + { + outThread.start(); + InputStream socketIn = socket.getInputStream(); + BufferedReader clientReader = new BufferedReader(new InputStreamReader( + socketIn)); + String line; + while (!terminateConnection && ((line = clientReader.readLine()) != null)) + { + processLine(line); + } + } + + public void sanitizeInputs(final String[] inputs) { + + // no for-each loop, because we need write access to the elements + + final int l = inputs.length; + + for (int i = 0; i < l; i++) { + inputs[i] = inputs[i].replaceAll(MAGIC_BYTES, " "); + } + } + + private void processLine(final String line) throws Exception + { + String l = line; + + // log things + if (l.startsWith("PASS") || l.startsWith("pass")) + this.logInfo("IRC-Client provided PASS"); + else + this.logDebug("IRC-Client: " + l); + + String prefix = ""; + if (l.startsWith(":")) + { + String[] tokens = l.split(" ", 2); + prefix = tokens[0]; + l = (tokens.length > 1 ? tokens[1] : ""); + } + String[] tokens1 = l.split(" ", 2); + String command = tokens1[0]; + l = tokens1.length > 1 ? tokens1[1] : ""; + String[] tokens2 = l.split("(^| )\\:", 2); + String trailing = null; + l = tokens2[0]; + if (tokens2.length > 1) + trailing = tokens2[1]; + ArrayList argumentList = new ArrayList(); + if (!l.equals("")) + argumentList.addAll(Arrays.asList(l.split(" "))); + if (trailing != null) + argumentList.add(trailing); + final String[] args = argumentList.toArray(new String[0]); + + // process command + + // numeric commands + if (command.matches("[0-9][0-9][0-9]")) + command = "N" + command; + + final Command commandObject; + + try { + commandObject = Command.valueOf(command.toUpperCase()); + } + catch (Exception ex) { + // forward raw unknown command to hw server + hwcon.sendCommand(ircToHwNick(line)); + return; + } + + if (args.length < commandObject.minArgumentCount + || args.length > commandObject.maxArgumentCount) + { + sendSelfNotice("Invalid number of arguments for this" + + " command, expected not more than " + + commandObject.maxArgumentCount + " and not less than " + + commandObject.minArgumentCount + " but got " + args.length + + " arguments"); + return; + } + commandObject.run(this, prefix, args); + } + + /** + * Sends a notice from the server to the user represented by this + * connection. + * + * @param string + * The text to send as a notice + */ + + private void sendSelfNotice(final String string) + { + send(":" + globalServerName + " NOTICE " + ownIrcNick + " :" + string); + } + + private void sendChannelNotice(final String string) { + sendChannelNotice(string, globalServerName); + } + + private void sendChannelNotice(final String string, final String from) { + // TODO send to room if user is in room + send(":" + from + " NOTICE " + LOBBY_CHANNEL_NAME + " :" + string); + } + + private void sendServerNotice(final String string) + { + if (ircJoined) + sendChannelNotice(string, "[INFO]"); + + sendSelfNotice(string); + } + + private String[] padSplit(final String line, final String regex, int max) + { + String[] split = line.split(regex); + String[] output = new String[max]; + for (int i = 0; i < output.length; i++) + { + output[i] = ""; + } + for (int i = 0; i < split.length; i++) + { + output[i] = split[i]; + } + return output; + } + + public void sendIfJoined(final String s) { + if (joined) + send(s); + } + + public void send(final String s) + { + final Queue testQueue = outQueue; + if (testQueue != null) + { + this.logDebug("IRC-Server: " + s); + testQueue.add(s); + } + } + +final static String RPL_WELCOME = "001 "; +final static String RPL_YOURHOST = "002 "; +final static String RPL_CREATED = "003 "; +final static String RPL_MYINFO = "004 "; +final static String RPL_BOUNCE = "005 "; +final static String RPL_TRACELINK = "200 "; +final static String RPL_TRACECONNECTING = "201 "; +final static String RPL_TRACEHANDSHAKE = "202 "; +final static String RPL_TRACEUNKNOWN = "203 "; +final static String RPL_TRACEOPERATOR = "204 "; +final static String RPL_TRACEUSER = "205 "; +final static String RPL_TRACESERVER = "206 "; +final static String RPL_TRACESERVICE = "207 "; +final static String RPL_TRACENEWTYPE = "208 "; +final static String RPL_TRACECLASS = "209 "; +final static String RPL_TRACERECONNECT = "210 "; +final static String RPL_STATSLINKINFO = "211 "; +final static String RPL_STATSCOMMANDS = "212 "; +final static String RPL_STATSCLINE = "213 "; +final static String RPL_STATSNLINE = "214 "; +final static String RPL_STATSILINE = "215 "; +final static String RPL_STATSKLINE = "216 "; +final static String RPL_STATSQLINE = "217 "; +final static String RPL_STATSYLINE = "218 "; +final static String RPL_ENDOFSTATS = "219 "; +final static String RPL_UMODEIS = "221 "; +final static String RPL_SERVICEINFO = "231 "; +final static String RPL_ENDOFSERVICES = "232 "; +final static String RPL_SERVICE = "233 "; +final static String RPL_SERVLIST = "234 "; +final static String RPL_SERVLISTEND = "235 "; +final static String RPL_STATSVLINE = "240 "; +final static String RPL_STATSLLINE = "241 "; +final static String RPL_STATSUPTIME = "242 "; +final static String RPL_STATSOLINE = "243 "; +final static String RPL_STATSHLINE = "244 "; +final static String RPL_STATSPING = "246 "; +final static String RPL_STATSBLINE = "247 "; +final static String RPL_STATSDLINE = "250 "; +final static String RPL_LUSERCLIENT = "251 "; +final static String RPL_LUSEROP = "252 "; +final static String RPL_LUSERUNKNOWN = "253 "; +final static String RPL_LUSERCHANNELS = "254 "; +final static String RPL_LUSERME = "255 "; +final static String RPL_ADMINME = "256 "; +final static String RPL_ADMINEMAIL = "259 "; +final static String RPL_TRACELOG = "261 "; +final static String RPL_TRACEEND = "262 "; +final static String RPL_TRYAGAIN = "263 "; +final static String RPL_NONE = "300 "; +final static String RPL_AWAY = "301 "; +final static String RPL_USERHOST = "302 "; +final static String RPL_ISON = "303 "; +final static String RPL_UNAWAY = "305 "; +final static String RPL_NOWAWAY = "306 "; +final static String RPL_WHOISUSER = "311 "; +final static String RPL_WHOISSERVER = "312 "; +final static String RPL_WHOISOPERATOR = "313 "; +final static String RPL_WHOWASUSER = "314 "; +final static String RPL_ENDOFWHO = "315 "; +final static String RPL_WHOISCHANOP = "316 "; +final static String RPL_WHOISIDLE = "317 "; +final static String RPL_ENDOFWHOIS = "318 "; +final static String RPL_WHOISCHANNELS = "319 "; +final static String RPL_LISTSTART = "321 "; +final static String RPL_LIST = "322 "; +final static String RPL_LISTEND = "323 "; +final static String RPL_CHANNELMODEIS = "324 "; +final static String RPL_UNIQOPIS = "325 "; +final static String RPL_NOTOPIC = "331 "; +final static String RPL_TOPIC = "332 "; +final static String RPL_INVITING = "341 "; +final static String RPL_SUMMONING = "342 "; +final static String RPL_INVITELIST = "346 "; +final static String RPL_ENDOFINVITELIST = "347 "; +final static String RPL_EXCEPTLIST = "348 "; +final static String RPL_ENDOFEXCEPTLIST = "349 "; +final static String RPL_VERSION = "351 "; +final static String RPL_WHOREPLY = "352 "; +final static String RPL_NAMREPLY = "353 "; +final static String RPL_KILLDONE = "361 "; +final static String RPL_CLOSING = "362 "; +final static String RPL_CLOSEEND = "363 "; +final static String RPL_LINKS = "364 "; +final static String RPL_ENDOFLINKS = "365 "; +final static String RPL_ENDOFNAMES = "366 "; +final static String RPL_BANLIST = "367 "; +final static String RPL_ENDOFBANLIST = "368 "; +final static String RPL_ENDOFWHOWAS = "369 "; +final static String RPL_INFO = "371 "; +final static String RPL_MOTD = "372 "; +final static String RPL_INFOSTART = "373 "; +final static String RPL_ENDOFINFO = "374 "; +final static String RPL_MOTDSTART = "375 "; +final static String RPL_ENDOFMOTD = "376 "; +final static String RPL_YOUREOPER = "381 "; +final static String RPL_REHASHING = "382 "; +final static String RPL_YOURESERVICE = "383 "; +final static String RPL_MYPORTIS = "384 "; +final static String RPL_TIME = "391 "; +final static String RPL_USERSSTART = "392 "; +final static String RPL_USERS = "393 "; +final static String RPL_ENDOFUSERS = "394 "; +final static String RPL_NOUSERS = "395 "; +final static String ERR_NOSUCHNICK = "401 "; +final static String ERR_NOSUCHSERVER = "402 "; +final static String ERR_NOSUCHCHANNEL = "403 "; +final static String ERR_CANNOTSENDTOCHAN = "404 "; +final static String ERR_TOOMANYCHANNELS = "405 "; +final static String ERR_WASNOSUCHNICK = "406 "; +final static String ERR_TOOMANYTARGETS = "407 "; +final static String ERR_NOSUCHSERVICE = "408 "; +final static String ERR_NOORIGIN = "409 "; +final static String ERR_NORECIPIENT = "411 "; +final static String ERR_NOTEXTTOSEND = "412 "; +final static String ERR_NOTOPLEVEL = "413 "; +final static String ERR_WILDTOPLEVEL = "414 "; +final static String ERR_BADMASK = "415 "; +final static String ERR_UNKNOWNCOMMAND = "421 "; +final static String ERR_NOMOTD = "422 "; +final static String ERR_NOADMININFO = "423 "; +final static String ERR_FILEERROR = "424 "; +final static String ERR_NONICKNAMEGIVEN = "431 "; +final static String ERR_ERRONEUSNICKNAME = "432 "; +final static String ERR_NICKNAMEINUSE = "433 "; +final static String ERR_NICKCOLLISION = "436 "; +final static String ERR_UNAVAILRESOURCE = "437 "; +final static String ERR_USERNOTINCHANNEL = "441 "; +final static String ERR_NOTONCHANNEL = "442 "; +final static String ERR_USERONCHANNEL = "443 "; +final static String ERR_NOLOGIN = "444 "; +final static String ERR_SUMMONDISABLED = "445 "; +final static String ERR_USERSDISABLED = "446 "; +final static String ERR_NOTREGISTERED = "451 "; +final static String ERR_NEEDMOREPARAMS = "461 "; +final static String ERR_ALREADYREGISTRED = "462 "; +final static String ERR_NOPERMFORHOST = "463 "; +final static String ERR_PASSWDMISMATCH = "464 "; +final static String ERR_YOUREBANNEDCREEP = "465 "; +final static String ERR_YOUWILLBEBANNED = "466 "; +final static String ERR_KEYSET = "467 "; +final static String ERR_CHANNELISFULL = "471 "; +final static String ERR_UNKNOWNMODE = "472 "; +final static String ERR_INVITEONLYCHAN = "473 "; +final static String ERR_BANNEDFROMCHAN = "474 "; +final static String ERR_BADCHANNELKEY = "475 "; +final static String ERR_BADCHANMASK = "476 "; +final static String ERR_NOCHANMODES = "477 "; +final static String ERR_BANLISTFULL = "478 "; +final static String ERR_NOPRIVILEGES = "481 "; +final static String ERR_CHANOPRIVSNEEDED = "482 "; +final static String ERR_CANTKILLSERVER = "483 "; +final static String ERR_RESTRICTED = "484 "; +final static String ERR_UNIQOPPRIVSNEEDED = "485 "; +final static String ERR_NOOPERHOST = "491 "; +final static String ERR_NOSERVICEHOST = "492 "; +final static String ERR_UMODEUNKNOWNFLAG = "501 "; +final static String ERR_USERSDONTMATCH = "502 "; + +} diff -r 64740eec84ad -r 4c523ed1d35c tools/hw2irc/restart --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/restart Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,8 @@ +#!/bin/bash +sudo -u nobody killall java; sleep 3; sudo -u nobody killall -9 java; +sleep 30; +#sudo -u nobody java net.ercatec.hw2ircsvr.Connection netserver.hedgewars.org 1337 >>out.sheepy 2>>err.sheepy & disown +sudo -u nobody bash -c 'java net.ercatec.hw2ircsvr.Connection netserver.hedgewars.org 1337 >>out.sheepy 2>&1 & disown' +sleep 122 +#sudo -u nobody java net.ercatec.hw2ircsvr.Connection >>out 2>>err & disown +sudo -u nobody bash -c 'java net.ercatec.hw2ircsvr.Connection >>out 2>&1 & disown' diff -r 64740eec84ad -r 4c523ed1d35c tools/pas2c/Pas2C.hs --- a/tools/pas2c/Pas2C.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/tools/pas2c/Pas2C.hs Sun Mar 24 14:33:57 2024 -0400 @@ -715,6 +715,9 @@ liftM (parens . (<> text " + 1")) $ initExpr2C' e initExpr2C' (BuiltInFunction "pred" [e]) = liftM (parens . (<> text " - 1")) $ initExpr2C' e +initExpr2C' (BuiltInFunction "round" [e]) = do + e <- initExpr2C' e + return $ text "(int)" <> parens e initExpr2C' b@(BuiltInFunction _ _) = error $ show b initExpr2C' (InitTypeCast t' i) = do e <- initExpr2C i @@ -964,12 +967,17 @@ wrapPhrase p@(Phrases _) = p wrapPhrase p = Phrases [p] +parensExpr2C :: Expression -> State RenderState Doc +parensExpr2C bop@(BinOp _ _ _) = liftM parens $ expr2C bop +parensExpr2C set@(SetExpression _ ) = liftM parens $ expr2C set +parensExpr2C e = expr2C e + expr2C :: Expression -> State RenderState Doc expr2C (Expression s) = return $ text s expr2C bop@(BinOp op expr1 expr2) = do - e1 <- expr2C expr1 + e1 <- parensExpr2C expr1 t1 <- gets lastType - e2 <- expr2C expr2 + e2 <- parensExpr2C expr2 t2 <- gets lastType case (op2C op, t1, t2) of ("+", BTAString, BTAString) -> expr2C $ BuiltInFunCall [expr1, expr2] (SimpleReference $ Identifier "_strconcatA" (fff t1 t2 BTString)) @@ -993,8 +1001,8 @@ ("==", BTString, BTString) -> expr2C $ BuiltInFunCall [expr1, expr2] (SimpleReference $ Identifier "_strcompare" (fff t1 t2 BTBool)) ("!=", BTString, _) -> expr2C $ BuiltInFunCall [expr1, expr2] (SimpleReference $ Identifier "_strncompare" (fff t1 t2 BTBool)) - ("&", BTBool, _) -> return $ parens e1 <+> text "&&" <+> parens e2 - ("|", BTBool, _) -> return $ parens e1 <+> text "||" <+> parens e2 + ("&", BTBool, _) -> return $ e1 <+> text "&&" <+> e2 + ("|", BTBool, _) -> return $ e1 <+> text "||" <+> e2 (_, BTRecord t1 _, BTRecord t2 _) -> do i <- op2CTyped op [SimpleType (Identifier t1 undefined), SimpleType (Identifier t2 undefined)] ref2C $ FunCall [expr1, expr2] (SimpleReference i) @@ -1011,17 +1019,17 @@ _ -> error "'in' against not set expression" (o, _, _) | o `elem` boolOps -> do modify(\s -> s{lastType = BTBool}) - return $ parens e1 <+> text o <+> parens e2 + return $ e1 <+> text o <+> e2 | otherwise -> do o' <- return $ case o of "/(float)" -> text "/(float)" -- pascal returns real value _ -> text o e1' <- return $ case (o, t1, t2) of ("-", BTInt False, BTInt False) -> parens $ text "(int64_t)" <+> parens e1 - _ -> parens e1 + _ -> e1 e2' <- return $ case (o, t1, t2) of ("-", BTInt False, BTInt False) -> parens $ text "(int64_t)" <+> parens e2 - _ -> parens e2 + _ -> e2 return $ e1' <+> o' <+> e2' where fff t1 t2 = BTFunction False False [(False, t1), (False, t2)] @@ -1052,7 +1060,7 @@ modify(\s -> s{isFunctionType = False}) -- reset if isfunc then ref2CF ref False else ref2CF ref True expr2C (PrefixOp op expr) = do - e <- expr2C expr + e <- parensExpr2C expr lt <- gets lastType case lt of BTRecord t _ -> do @@ -1062,8 +1070,8 @@ o <- return $ case op of "not" -> text "!" _ -> text (op2C op) - return $ o <> parens e - _ -> return $ text (op2C op) <> parens e + return $ o <> e + _ -> return $ text (op2C op) <> e expr2C Null = return $ text "NULL" expr2C (CharCode a) = do modify(\s -> s{lastType = BTChar}) diff -r 64740eec84ad -r 4c523ed1d35c tools/pas2c/PascalBasics.hs --- a/tools/pas2c/PascalBasics.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/tools/pas2c/PascalBasics.hs Sun Mar 24 14:33:57 2024 -0400 @@ -2,7 +2,7 @@ module PascalBasics where import Text.Parsec.Combinator -import Text.Parsec.Char +import Text.Parsec.Char hiding (string') import Text.Parsec.Prim import Text.Parsec.Token import Text.Parsec.Language @@ -17,7 +17,7 @@ string' = void . string builtin :: [String] -builtin = ["succ", "pred", "low", "high", "ord", "inc", "dec", "exit", "break", "continue", "length", "copy"] +builtin = ["succ", "pred", "low", "high", "ord", "inc", "dec", "exit", "break", "continue", "length", "copy", "round"] pascalLanguageDef :: GenLanguageDef String u Identity pascalLanguageDef diff -r 64740eec84ad -r 4c523ed1d35c tools/pas2c/PascalParser.hs --- a/tools/pas2c/PascalParser.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/tools/pas2c/PascalParser.hs Sun Mar 24 14:33:57 2024 -0400 @@ -4,7 +4,7 @@ ) where -import Text.Parsec +import Text.Parsec hiding (string') import Text.Parsec.Token import Text.Parsec.Expr import Control.Monad diff -r 64740eec84ad -r 4c523ed1d35c tools/pas2c/PascalPreprocessor.hs --- a/tools/pas2c/PascalPreprocessor.hs Sun Mar 24 14:05:06 2024 -0400 +++ b/tools/pas2c/PascalPreprocessor.hs Sun Mar 24 14:33:57 2024 -0400 @@ -1,7 +1,7 @@ {-# LANGUAGE ScopedTypeVariables #-} module PascalPreprocessor where -import Text.Parsec +import Text.Parsec hiding (string') import Control.Monad.IO.Class import Control.Monad import System.IO diff -r 64740eec84ad -r 4c523ed1d35c tools/rc/convert.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/rc/convert.sh Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,20 @@ +#!/usr/bin/env sh + +ls ../rc || exit + +rm -rdfv build engine +mkdir -p build engine +cd build +cmake -DNOSERVER=on -DBUILD_ENGINE_C=on -DLUA_SYSTEM=on -DNOVIDEOREC=off ../../../ +cmake --build . --target engine_c + +# this one you can get from pip: pip install scan-build +intercept-build cmake --build . --target hwengine +c2rust transpile --emit-build-files --emit-modules --reduce-type-annotations --binary hwengine compile_commands.json --output-dir=../engine + +cd ../engine +sed -i 's/f128.*//g' Cargo.toml +sed -i 's/extern crate f128.*//g' lib.rs +sed -i 's/mod src {/mod src{\npub mod to_f64;/g' lib.rs +find -type f -name '*.rs' -exec sed -i 's/f128/f64/g' {} \; -exec sed -i 's/f64::f64/f64/g' {} \; -exec sed -i 's/use ::f64;/use crate::src::to_f64::to_f64;/g' {} \; -exec sed -i 's/f64::new/to_f64/g' {} \; +cp ../to_f64.rs src/ diff -r 64740eec84ad -r 4c523ed1d35c tools/rc/to_f64.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/rc/to_f64.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,4 @@ +pub fn to_f64>(v: T) -> f64 { + v.into() +} + diff -r 64740eec84ad -r 4c523ed1d35c tools/replay2hwd.hs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/replay2hwd.hs Sun Mar 24 14:33:57 2024 -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 ", 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)) diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/hs-echo/app/Main.hs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/hs-echo/app/Main.hs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,48 @@ +{-# LANGUAGE OverloadedStrings #-} +module Main where + +import Text.Megaparsec (Parsec, parseMaybe) +import Text.URI +import System.Environment (getEnv) +import Data.Text (Text, pack, unpack) +import Data.Maybe +import Control.Monad (when) +import Network.AMQP +import qualified Data.ByteString.Lazy.Char8 as BL + +assert :: String -> Bool -> a -> a +assert message False x = error message +assert _ _ x = x + +unRpack = unpack . unRText + +main :: IO () +main = do + amqpUri <- getEnv "AMQP_URL" + let uri = fromJust $ parseMaybe (parser :: Parsec Int Text URI) $ pack amqpUri + when (uriScheme uri /= mkScheme "amqp") $ error "AMQP_URL environment variable scheme should be amqp" + let Right (Authority (Just (UserInfo username (Just password))) rHost maybePort) = uriAuthority uri + + conn <- openConnection' (unRpack rHost) (fromInteger . toInteger $ fromMaybe 5672 maybePort) "/" (unRText username) (unRText password) + chan <- openChannel conn + + (queueName, messageCount, consumerCount) <- declareQueue chan newQueue + bindQueue chan queueName "irc" "cmd.echo.hedgewars" + + -- subscribe to the queue + consumeMsgs chan queueName Ack (myCallback chan) + + getLine -- wait for keypress + closeConnection conn + putStrLn "connection closed" + + +myCallback :: Channel -> (Message,Envelope) -> IO () +myCallback chan (msg, env) = do + let message = BL.tail . BL.dropWhile (/= '\n') $ msgBody msg + putStrLn $ "received message: " ++ (BL.unpack $ message) + + publishMsg chan "irc" "say.hedgewars" + newMsg {msgBody = message} + + ackEnv env \ No newline at end of file diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/hs-echo/hs-ping.cabal --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/hs-echo/hs-ping.cabal Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,39 @@ +cabal-version: 2.4 +name: hs-ubot-ping-plugin +version: 0.1.0.0 + +-- A short (one-line) description of the package. +-- synopsis: + +-- A longer description of the package. +-- description: + +-- A URL where users can report bugs. +-- bug-reports: + +-- The license under which the package is released. +-- license: +author: Andrey Korotaev +maintainer: a.korotaev@hedgewars.org + +-- A copyright notice. +-- copyright: +-- category: +extra-source-files: CHANGELOG.md + +executable hs-ubot-ping-plugin + main-is: Main.hs + + -- Modules included in this executable, other than Main. + -- other-modules: + + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + build-depends: base ^>=4.14.1.0, + megaparsec >= 9.0, + modern-uri >= 0.3, + text >= 1.2, + bytestring, + amqp >= 0.22 + hs-source-dirs: app + default-language: Haskell2010 diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/ubot-mingpt-plugin/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/ubot-mingpt-plugin/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,15 @@ +[package] +name = "ubot-mingpt-plugin" +version = "0.1.0" +authors = ["Andrey Korotaev "] +edition = "2018" + +[dependencies] +tch = "0.4" +anyhow = "1.0" +tokio-amqp = "1.0" +lapin = "1.7" +tokio = {version="1.6", features = ["full"]} +rand = "0.8" +futures = "0.3" + diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/ubot-mingpt-plugin/src/main.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/ubot-mingpt-plugin/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,337 @@ +/* This example uses the tinyshakespeare dataset which can be downloaded at: + https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt + + This is mostly a rust port of https://github.com/karpathy/minGPT +*/ + +extern crate tch; +use anyhow::{bail, Result as AHResult}; +use std::{io, io::Write}; +use tch::data::TextData; +use tch::nn::{ModuleT, OptimizerConfig}; +use tch::{nn, Device, IndexOp, Kind, Tensor}; + +use futures::prelude::*; +use lapin::{options::*, types::FieldTable, BasicProperties, Connection, ConnectionProperties}; + +use tokio_amqp::*; + +const LEARNING_RATE: f64 = 0.0003; +const BLOCK_SIZE: i64 = 128; +const BATCH_SIZE: i64 = 64; +const EPOCHS: i64 = 100; +const SAMPLING_LEN: i64 = 512; + +#[derive(Debug, Copy, Clone)] +struct Config { + vocab_size: i64, + n_embd: i64, + n_head: i64, + n_layer: i64, + block_size: i64, + attn_pdrop: f64, + resid_pdrop: f64, + embd_pdrop: f64, +} + +// Weight decay only applies to the weight matrixes in the linear layers +const NO_WEIGHT_DECAY_GROUP: usize = 0; +const WEIGHT_DECAY_GROUP: usize = 1; + +// Custom linear layer so that different groups can be used for weight +// and biases. +#[derive(Debug)] +struct Linear { + pub ws: Tensor, + pub bs: Tensor, +} + +impl nn::Module for Linear { + fn forward(&self, xs: &Tensor) -> Tensor { + xs.matmul(&self.ws.tr()) + &self.bs + } +} + +fn linear(vs: nn::Path, in_dim: i64, out_dim: i64) -> Linear { + let wd = vs.set_group(WEIGHT_DECAY_GROUP); + let no_wd = vs.set_group(NO_WEIGHT_DECAY_GROUP); + Linear { + ws: wd.randn("weight", &[out_dim, in_dim], 0.0, 0.02), + bs: no_wd.zeros("bias", &[out_dim]), + } +} + +fn linear_no_bias(vs: nn::Path, in_dim: i64, out_dim: i64) -> Linear { + let wd = vs.set_group(WEIGHT_DECAY_GROUP); + let no_wd = vs.set_group(NO_WEIGHT_DECAY_GROUP); + Linear { + ws: wd.randn("weight", &[out_dim, in_dim], 0.0, 0.02), + bs: no_wd.zeros_no_train("bias", &[out_dim]), + } +} + +fn causal_self_attention(p: &nn::Path, cfg: Config) -> impl ModuleT { + let key = linear(p / "key", cfg.n_embd, cfg.n_embd); + let query = linear(p / "query", cfg.n_embd, cfg.n_embd); + let value = linear(p / "value", cfg.n_embd, cfg.n_embd); + let proj = linear(p / "proj", cfg.n_embd, cfg.n_embd); + let mask_init = + Tensor::ones(&[cfg.block_size, cfg.block_size], (Kind::Float, p.device())).tril(0); + let mask_init = mask_init.view([1, 1, cfg.block_size, cfg.block_size]); + // let mask = p.var_copy("mask", &mask_init); + let mask = mask_init; + nn::func_t(move |xs, train| { + let (sz_b, sz_t, sz_c) = xs.size3().unwrap(); + let sizes = [sz_b, sz_t, cfg.n_head, sz_c / cfg.n_head]; + let k = xs.apply(&key).view(sizes).transpose(1, 2); + let q = xs.apply(&query).view(sizes).transpose(1, 2); + let v = xs.apply(&value).view(sizes).transpose(1, 2); + let att = q.matmul(&k.transpose(-2, -1)) * (1.0 / f64::sqrt(sizes[3] as f64)); + let att = att.masked_fill( + &mask.i((.., .., ..sz_t, ..sz_t)).eq(0.), + std::f64::NEG_INFINITY, + ); + let att = att.softmax(-1, Kind::Float).dropout(cfg.attn_pdrop, train); + let ys = att + .matmul(&v) + .transpose(1, 2) + .contiguous() + .view([sz_b, sz_t, sz_c]); + ys.apply(&proj).dropout(cfg.resid_pdrop, train) + }) +} + +fn block(p: &nn::Path, cfg: Config) -> impl ModuleT { + let ln1 = nn::layer_norm(p / "ln1", vec![cfg.n_embd], Default::default()); + let ln2 = nn::layer_norm(p / "ln2", vec![cfg.n_embd], Default::default()); + let attn = causal_self_attention(p, cfg); + let lin1 = linear(p / "lin1", cfg.n_embd, 4 * cfg.n_embd); + let lin2 = linear(p / "lin2", 4 * cfg.n_embd, cfg.n_embd); + nn::func_t(move |xs, train| { + let xs = xs + xs.apply(&ln1).apply_t(&attn, train); + let ys = xs + .apply(&ln2) + .apply(&lin1) + .gelu() + .apply(&lin2) + .dropout(cfg.resid_pdrop, train); + xs + ys + }) +} + +fn gpt(p: &nn::Path, cfg: Config) -> impl ModuleT { + let p = &p.set_group(NO_WEIGHT_DECAY_GROUP); + let tok_emb = nn::embedding( + p / "tok_emb", + cfg.vocab_size, + cfg.n_embd, + Default::default(), + ); + let pos_emb = p.zeros("pos_emb", &[1, cfg.block_size, cfg.n_embd]); + let ln_f = nn::layer_norm(p / "ln_f", vec![cfg.n_embd], Default::default()); + let head = linear_no_bias(p / "head", cfg.n_embd, cfg.vocab_size); + let mut blocks = nn::seq_t(); + for block_idx in 0..cfg.n_layer { + blocks = blocks.add(block(&(p / block_idx), cfg)); + } + nn::func_t(move |xs, train| { + let (_sz_b, sz_t) = xs.size2().unwrap(); + let tok_emb = xs.apply(&tok_emb); + let pos_emb = pos_emb.i((.., ..sz_t, ..)); + (tok_emb + pos_emb) + .dropout(cfg.embd_pdrop, train) + .apply_t(&blocks, train) + .apply(&ln_f) + .apply(&head) + }) +} + +/// Generates some sample string using the GPT model. +fn sample(data: &TextData, gpt: &impl ModuleT, input: Tensor) -> String { + let mut input = input; + let mut result = String::new(); + for _index in 0..SAMPLING_LEN { + let logits = input.apply_t(gpt, false).i((0, -1, ..)); + let sampled_y = logits.softmax(-1, Kind::Float).multinomial(1, true); + let last_label = i64::from(&sampled_y); + result.push(data.label_to_char(last_label)); + input = Tensor::cat(&[input, sampled_y.view([1, 1])], 1).narrow(1, 1, BLOCK_SIZE); + } + result +} + +#[tokio::main] +async fn main() -> AHResult<()> { + let device = Device::cuda_if_available(); + let mut vs = nn::VarStore::new(device); + let data = TextData::new("10.log")?; + let labels = data.labels(); + println!("Dataset loaded, {} labels.", labels); + let cfg = Config { + vocab_size: labels, + n_embd: 384, // was 512 + n_head: 8, + n_layer: 8, + block_size: BLOCK_SIZE, + attn_pdrop: 0.1, + resid_pdrop: 0.1, + embd_pdrop: 0.1, + }; + let gpt = gpt(&(&vs.root() / "gpt"), cfg); + let args: Vec<_> = std::env::args().collect(); + if args.len() < 2 { + bail!("usage: main (train|predict weights.ot seqstart)") + } + match args[1].as_str() { + "train" => { + let mut opt = nn::AdamW::default().build(&vs, LEARNING_RATE)?; + opt.set_weight_decay_group(NO_WEIGHT_DECAY_GROUP, 0.0); + opt.set_weight_decay_group(WEIGHT_DECAY_GROUP, 0.1); + let mut idx = 0; + vs.load("384.ot")?; + for epoch in 1..(1 + EPOCHS) { + let mut sum_loss = 0.; + let mut cnt_loss = 0.; + for batch in data.iter_shuffle(BLOCK_SIZE + 1, BATCH_SIZE) { + let xs = batch + .narrow(1, 0, BLOCK_SIZE) + .to_kind(Kind::Int64) + .to_device(device); + let ys = batch + .narrow(1, 1, BLOCK_SIZE) + .to_kind(Kind::Int64) + .to_device(device); + let logits = xs.apply_t(&gpt, true); + let loss = logits + .view([BATCH_SIZE * BLOCK_SIZE, labels]) + .cross_entropy_for_logits(&ys.view([BATCH_SIZE * BLOCK_SIZE])); + opt.backward_step_clip(&loss, 0.5); + sum_loss += f64::from(loss); + cnt_loss += 1.0; + idx += 1; + if idx % 10 == 0 { + print!("{}", '.'); + io::stdout().flush()?; + } + if idx % 1000 == 0 { + println!("Epoch: {} loss: {:5.3}", epoch, sum_loss / cnt_loss); + let input = Tensor::zeros(&[1, BLOCK_SIZE], (Kind::Int64, device)); + println!("Sample: {}", sample(&data, &gpt, input)); + if let Err(err) = vs.save(format!("gpt{:08}.ot", idx)) { + println!("error while saving {}", err); + } + sum_loss = 0.; + cnt_loss = 0.; + } + } + } + } + "predict" => { + let amqp_url = std::env::var("AMQP_URL").expect("expected AMQP_URL env variabe"); + let conn = Connection::connect(&amqp_url, ConnectionProperties::default().with_tokio()) + .await?; + + let pub_channel = conn.create_channel().await?; + let sub_channel = conn.create_channel().await?; + + let queue = sub_channel + .queue_declare( + &"", + QueueDeclareOptions { + exclusive: true, + auto_delete: true, + ..QueueDeclareOptions::default() + }, + FieldTable::default(), + ) + .await?; + + sub_channel + .queue_bind( + queue.name().as_str(), + "irc", + "cmd.say.hedgewars", + QueueBindOptions::default(), + FieldTable::default(), + ) + .await?; + + sub_channel + .queue_bind( + queue.name().as_str(), + "irc", + "msg.hedgewars", + QueueBindOptions::default(), + FieldTable::default(), + ) + .await?; + + let mut subscriber = sub_channel + .basic_consume( + queue.name().as_str(), + &"", + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await?; + + vs.load(args[2].as_str())?; + + let mut buffer = Vec::new(); + + while let Some(amqp_message) = subscriber.next().await { + let (_, delivery) = amqp_message.expect("error in consumer"); + delivery.ack(BasicAckOptions::default()).await?; + + if delivery.routing_key.as_str() == "msg.hedgewars" { + let chat_message = String::from_utf8_lossy(&delivery.data); + if let Some((_who, message)) = chat_message.split_once('\n') { + buffer.push('\n'); + buffer.extend(message.chars()); + if buffer.len() >= BLOCK_SIZE as usize { + let _ = buffer.drain(0..=buffer.len() - BLOCK_SIZE as usize); + } + } + } else { + let chat_message = String::from_utf8_lossy(&delivery.data); + let seed = chat_message.split_once('\n').map(|(_, s)| s).unwrap_or(""); + buffer.push('\n'); + buffer.extend(seed.chars()); + + if buffer.len() >= BLOCK_SIZE as usize { + let _ = buffer.drain(0..=buffer.len() - BLOCK_SIZE as usize); + } + + let input = Tensor::zeros(&[1, BLOCK_SIZE], (Kind::Int64, device)); + for (idx, c) in buffer.iter().rev().enumerate() { + let _filled = input + .i((0, BLOCK_SIZE - 1 - idx as i64)) + .fill_(data.char_to_label(*c).unwrap_or(0) as i64); + } + + let proceeded_message = &sample(&data, &gpt, input); + let final_message = proceeded_message + .split_once('\n') + .map(|(m, _)| m) + .unwrap_or(proceeded_message); + let final_message = &format!("{}{}", seed, final_message); + + println!("{} --> {}", seed, proceeded_message); + + pub_channel + .basic_publish( + "irc", + "say.hedgewars", + BasicPublishOptions::default(), + final_message.as_bytes().to_vec(), + BasicProperties::default(), + ) + .await?; + } + } + } + _ => bail!("usage: main (train|predict weights.ot)"), + }; + + Ok(()) +} diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/ubot-plugin-janitor/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/ubot-plugin-janitor/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,12 @@ +[package] +name = "ubot-plugin-janitor" +version = "0.1.0" +edition = "2018" + +[dependencies] +anyhow = "1.0" +tokio-amqp = "1.0" +lapin = "1.7" +tokio = {version="1.6", features = ["full"]} +rand = "0.8" +futures = "0.3" diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/ubot-plugin-janitor/src/main.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/ubot-plugin-janitor/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,84 @@ +use anyhow::Result as AHResult; + +use futures::prelude::*; +use lapin::{options::*, types::FieldTable, BasicProperties, Connection, ConnectionProperties}; + +use tokio_amqp::*; + +#[tokio::main] +async fn main() -> AHResult<()> { + let amqp_url = std::env::var("AMQP_URL").expect("expected AMQP_URL env variabe"); + let conn = Connection::connect(&amqp_url, ConnectionProperties::default().with_tokio()).await?; + + let pub_channel = conn.create_channel().await?; + let sub_channel = conn.create_channel().await?; + + let queue = sub_channel + .queue_declare( + &"", + QueueDeclareOptions { + exclusive: true, + auto_delete: true, + ..QueueDeclareOptions::default() + }, + FieldTable::default(), + ) + .await?; + + sub_channel + .queue_bind( + queue.name().as_str(), + "irc", + "*.hedgewars", + QueueBindOptions::default(), + FieldTable::default(), + ) + .await?; + + let mut subscriber = sub_channel + .basic_consume( + queue.name().as_str(), + &"", + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await?; + + let mut last_joined = None; + let mut talking_to = None; + + while let Some(amqp_message) = subscriber.next().await { + let (_, delivery) = amqp_message.expect("error in consumer"); + delivery.ack(BasicAckOptions::default()).await?; + + match delivery.routing_key.as_str() { + "msg.hedgewars" => { + let chat_message = String::from_utf8_lossy(&delivery.data); + if let Some((who, _)) = chat_message.split_once('\n') { + let who = Some(who.to_owned()); + if talking_to == who || last_joined == who { + talking_to = who; + pub_channel + .basic_publish( + "irc", + "cmd.say.hedgewars", + BasicPublishOptions::default(), + vec![], + BasicProperties::default(), + ) + .await?; + } else { + last_joined = None; + talking_to = None; + } + } + } + "join.hedgewars" => { + last_joined = Some(String::from_utf8_lossy(&delivery.data).to_string()); + } + _ => (), + } + } + + Ok(()) +} diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/url-bot-rs/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/url-bot-rs/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,88 @@ +[package] +name = "url-bot-rs" +version = "0.3.1" +description = "Minimal IRC URL bot in Rust" +keywords = ["irc", "bot", "title"] +repository = "https://github.com/nuxeh/url-bot-rs" +authors = ["Edward Cragg "] +license = "ISC" +readme = "README.md" +build = "build.rs" +edition = "2018" +include = [ + "src/**/*", + "build.rs", + "Cargo.*", + "README.md", + "COPYING", + "example.config.toml" +] + +[build-dependencies] +built = { version = "0.4.4", features = ["git2"] } +man = "0.3.0" + +[dev-dependencies] +tiny_http = "0.8.0" +diff = "0.1.12" +tempfile = "3.2.0" + +[dependencies] +irc = "0.13.6" +tokio-core = "0.1.18" +rusqlite = "0.14.0" +chrono = "0.4.19" +docopt = "1.1.0" +serde = "1.0.123" +serde_derive = "1.0.104" +itertools = "0.10.0" +regex = "1.4.3" +lazy_static = "1.4.0" +failure = "0.1.8" +reqwest = { version = "0.11.0", features = ["blocking", "cookies", "json"] } +serde_rusqlite = "0.14.0" +mime = "0.3.16" +humansize = "1.1.0" +unicode-segmentation = "1.7.1" +toml = "0.5.8" +directories = "3.0.1" +log = "0.4.13" +stderrlog = "0.5.1" +atty = "0.2.14" +scraper = { version = "0.12.0", default-features = false, features = [] } +phf = "0.7.24" + +anyhow = "1.0" +tokio-amqp = "1.0" +lapin = "1.7" +tokio = {version="1.6", features = ["full"]} +futures = "0.3" +url = "2.2" +rand = "0.8" + +[dependencies.image] +version = "0.22.5" +default-features = false +features = ["gif_codec", "jpeg", "png_codec", "pnm", "tiff", "bmp"] + +[features] +default = [] +sqlite_bundled = ["rusqlite/bundled"] + +[package.metadata.deb] +extended-description = """\ +Standalone IRC bot; for resolving URLs posted, retrieving, and posting page +titles to a configurable IRC server and channels""" +maintainer-scripts = "debian" +assets = [ + ["example.config.toml", "usr/share/doc/url-bot-rs/", "644"], + ["target/assets/url-bot-rs.1", "usr/local/share/man/man1/", "644"], + ["systemd/url-bot-rs.service", "lib/systemd/system/", "644"], + ["target/release/url-bot-rs", "usr/bin/", "755"], + ["target/release/url-bot-get", "usr/bin/", "755"] +] + +[badges] +coveralls = { repository = "nuxeh/url-bot-rs", branch = "master", service = "github" } +codecov = { repository = "nuxeh/url-bot-rs", branch = "master" } +travis-ci = { repository = "nuxeh/url-bot-rs", branch = "master" } diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot-plugins/url-bot-rs/src/bin/ubot-url-plugin.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot-plugins/url-bot-rs/src/bin/ubot-url-plugin.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,331 @@ +use url_bot_rs::config::Rtd; +use url_bot_rs::VERSION; +use url_bot_rs::{feat, http::resolve_url, param, plugins::TITLE_PLUGINS, tld::TLD}; + +use anyhow::Result as AHResult; +use atty::{is, Stream}; +use directories::ProjectDirs; +use docopt::Docopt; +use failure::Error; +use lazy_static::lazy_static; +use log::{error, info}; +use regex::Regex; +use reqwest::Url; +use serde_derive::Deserialize; +use std::collections::HashSet; +use std::path::PathBuf; +use stderrlog::{ColorChoice, Timestamp}; + +use lapin::{options::*, types::FieldTable, BasicProperties, Connection, ConnectionProperties}; +use tokio_amqp::*; + +use futures::prelude::*; + +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +use std::sync::mpsc; +use std::thread; + +// docopt usage string +const USAGE: &str = " +URL munching IRC bot. + +Usage: + ubot-url-plugin [options] [-v...] [--conf=PATH...] [--conf-dir=DIR...] + +Options: + -h --help Show this help message. + --version Print version. + -v --verbose Show extra information. + -t --timestamp Force timestamps. +"; + +#[derive(Debug, Deserialize, Default)] +pub struct Args { + flag_verbose: usize, + flag_conf: Vec, + flag_conf_dir: Vec, + flag_timestamp: bool, +} + +const MIN_VERBOSITY: usize = 2; + +#[derive(Debug, PartialEq)] +enum TitleResp { + Title(String), + Error(String), +} + +/// Run available plugins on a single URL, return the first successful title. +fn process_plugins(rtd: &Rtd, url: &Url) -> Option { + let result: String = TITLE_PLUGINS + .iter() + .filter(|p| p.check(&rtd.conf.plugins, url)) + .filter_map(|p| p.evaluate(&rtd, url).ok()) + .take(1) + .collect(); + + if result.is_empty() { + None + } else { + Some(result) + } +} + +/// find titles in a message and generate responses +fn process_titles(rtd: &Rtd, msg: &str) -> impl Iterator { + let mut responses: Vec = vec![]; + + let mut num_processed = 0; + let mut dedup_urls = HashSet::new(); + + // look at each space-separated message token + for token in msg.split_whitespace() { + // the token must not contain unsafe characters + if contains_unsafe_chars(token) { + continue; + } + + // get a full URL for tokens without a scheme + let maybe_token = if feat!(rtd, partial_urls) { + add_scheme_for_tld(token) + } else { + None + }; + + let token = maybe_token.as_ref().map_or(token, String::as_str); + + // the token must be a valid url + let url = match token.parse::() { + Ok(url) => url, + _ => continue, + }; + + // the scheme must be http or https + if !["http", "https"].contains(&url.scheme()) { + continue; + } + + // skip duplicate urls within the message + if dedup_urls.contains(&url) { + continue; + } + + info!("[{}] RESOLVE <{}>", rtd.conf.network.name, token); + + // try to get the title from the url + let title = if let Some(title) = process_plugins(rtd, &url) { + title + } else { + match resolve_url(token, rtd) { + Ok(title) => title, + Err(err) => { + error!("{:?}", err); + responses.push(TitleResp::Error(err.to_string())); + continue; + } + } + }; + + // limit response length, see RFC1459 + + let msg = utf8_truncate(&format!("⤷ {}", title), 510); + + info!("[{}] {}", rtd.conf.network.name, msg); + + responses.push(TitleResp::Title(msg.to_string())); + + dedup_urls.insert(url); + + // limit the number of processed URLs + num_processed += 1; + if num_processed == param!(rtd, url_limit) { + break; + } + } + + responses.into_iter() +} + +// regex for unsafe characters, as defined in RFC 1738 +const RE_UNSAFE_CHARS: &str = r#"[{}|\\^~\[\]`<>"]"#; + +/// does the token contain characters not permitted by RFC 1738 +fn contains_unsafe_chars(token: &str) -> bool { + lazy_static! { + static ref UNSAFE: Regex = Regex::new(RE_UNSAFE_CHARS).unwrap(); + } + UNSAFE.is_match(token) +} + +/// truncate to a maximum number of bytes, taking UTF-8 into account +fn utf8_truncate(s: &str, n: usize) -> String { + s.char_indices() + .take_while(|(len, c)| len + c.len_utf8() <= n) + .map(|(_, c)| c) + .collect() +} + +lazy_static! { + static ref REPEATED_DOTS: Regex = Regex::new(r"\.\.+").unwrap(); +} + +/// if a token has a recognised TLD, but no scheme, add one +pub fn add_scheme_for_tld(token: &str) -> Option { + if token.parse::().is_err() { + if token.starts_with(|s: char| !s.is_alphabetic()) { + return None; + } + + if REPEATED_DOTS.is_match(&token) { + return None; + } + + let new_token = format!("http://{}", token); + + if let Ok(url) = new_token.parse::() { + if !url.domain()?.contains('.') { + return None; + } + + // reject email addresses + if url.username() != "" { + return None; + } + + let tld = url.domain()?.split('.').last()?; + + if TLD.contains(tld) { + return Some(new_token); + } + } + } + + None +} + +fn init_rtd() -> AHResult { + // parse command line arguments with docopt + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.version(Some(VERSION.to_string())).deserialize()) + .unwrap_or_else(|e| e.exit()); + + // avoid timestamping when piped, e.g. systemd + let timestamp = if is(Stream::Stderr) || args.flag_timestamp { + Timestamp::Second + } else { + Timestamp::Off + }; + + stderrlog::new() + .module(module_path!()) + .modules(vec![ + "url_bot_rs::message", + "url_bot_rs::config", + "url_bot_rs::http", + ]) + .verbosity(args.flag_verbose + MIN_VERBOSITY) + .timestamp(timestamp) + .color(ColorChoice::Never) + .init() + .unwrap(); + + let dirs = ProjectDirs::from("org", "", "url-bot-rs").unwrap(); + let default_conf_dir = dirs.config_dir(); + + let default_conf = default_conf_dir.join("config.toml"); + + let rtd: Rtd = Rtd::new().conf(&default_conf).load()?.init_http_client()?; + + Ok(rtd) +} + +fn random_string(size: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(size) + .map(char::from) + .collect() +} + +#[tokio::main] +async fn main() -> AHResult<()> { + let (tx1, rx1) = mpsc::channel::(); + let (tx2, rx2) = mpsc::channel(); + + thread::spawn(move || { + let rtd = init_rtd().expect("RTD not initialized"); + + loop { + let message = &rx1.recv().expect("rx1 recv error"); + let titles: Vec<_> = process_titles(&rtd, message).collect(); + tx2.send(titles).expect("tx2 send error"); + } + }); + let amqp_url = std::env::var("AMQP_URL").expect("expected AMQP_URL env variabe"); + let conn = Connection::connect(&amqp_url, ConnectionProperties::default().with_tokio()).await?; + + let pub_channel = conn.create_channel().await?; + let sub_channel = conn.create_channel().await?; + + let queue = sub_channel + .queue_declare( + &random_string(32), + QueueDeclareOptions { + exclusive: true, + auto_delete: true, + ..QueueDeclareOptions::default() + }, + FieldTable::default(), + ) + .await?; + + sub_channel + .queue_bind( + queue.name().as_str(), + "irc", + "msg.hedgewars", + QueueBindOptions::default(), + FieldTable::default(), + ) + .await?; + + let mut subscriber = sub_channel + .basic_consume( + queue.name().as_str(), + &random_string(32), + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await?; + + while let Some(amqp_message) = subscriber.next().await { + let (_, delivery) = amqp_message.expect("error in consumer"); + delivery.ack(BasicAckOptions::default()).await?; + + let chat_message = String::from_utf8(delivery.data)?; + if let Some((_who, message)) = chat_message.split_once('\n') { + tx1.send(message.to_owned())?; + let titles = rx2.recv()?; + + for title in titles { + let title_message = match title { + TitleResp::Title(t) => t, + TitleResp::Error(e) => e, + }; + pub_channel + .basic_publish( + "irc", + "say.hedgewars", + BasicPublishOptions::default(), + title_message.as_bytes().to_vec(), + BasicProperties::default(), + ) + .await?; + } + } + } + + Ok(()) +} diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot/Cargo.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot/Cargo.toml Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,15 @@ +[package] +name = "ubot" +version = "0.1.0" +authors = ["Andrey Korotaev "] +edition = "2018" + +[dependencies] +tokio-amqp = "1.0" +lapin = "1.7" +tokio = {version="1.6", features = ["full"]} +irc = "0.15" +anyhow = "1.0" +futures = "0.3" +url = "2.2" +rand = "0.8" diff -r 64740eec84ad -r 4c523ed1d35c tools/ubot/src/main.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/ubot/src/main.rs Sun Mar 24 14:33:57 2024 -0400 @@ -0,0 +1,195 @@ +use lapin::{ + message::Delivery, options::*, types::FieldTable, BasicProperties, Connection, + ConnectionProperties, +}; +use tokio_amqp::*; + +use futures::prelude::*; +use irc::client::prelude::*; + +use anyhow::{bail, Result as AHResult}; + +use url::Url; + +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +fn url2irc_config(url: &str) -> AHResult { + let url = Url::parse(url)?; + + if url.scheme() != "irc" { + bail!("Expected 'irc' scheme") + } + + Ok(Config { + nickname: Some(url.username().to_owned()), + nick_password: url.password().map(|s| s.to_owned()), + server: url.host_str().map(|s| s.to_owned()), + port: url.port(), + channels: vec![format!("#{}", &url.path()[1..])], + //use_mock_connection: true, + ..Config::default() + }) +} + +fn random_string(size: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(size) + .map(char::from) + .collect() +} + +async fn handle_irc(pub_channel: &lapin::Channel, irc_message: &Message) -> AHResult<()> { + match &irc_message.command { + Command::PRIVMSG(msgtarget, message) => { + let target = irc_message + .response_target() + .expect("Really expected PRIVMSG would have a source"); + let target = if target.starts_with('#') { + &target[1..] + } else { + &target + }; + + let who = irc_message.source_nickname().unwrap_or(msgtarget); + + if message.starts_with("!") { + if let Some((cmd, param)) = message.split_once(' ') { + pub_channel + .basic_publish( + "irc", + &format!("cmd.{}.{}", &cmd[1..], target), + BasicPublishOptions::default(), + format!("{}\n{}", who, param).as_bytes().to_vec(), + BasicProperties::default(), + ) + .await?; + } else { + pub_channel + .basic_publish( + "irc", + &format!("cmd.{}.{}", &message[1..], target), + BasicPublishOptions::default(), + who.as_bytes().to_vec(), + BasicProperties::default(), + ) + .await?; + } + } else { + pub_channel + .basic_publish( + "irc", + &format!("msg.{}", target), + BasicPublishOptions::default(), + format!("{}\n{}", who, message).as_bytes().to_vec(), + BasicProperties::default(), + ) + .await?; + } + } + Command::JOIN(channel, _, _) => { + pub_channel + .basic_publish( + "irc", + &format!("join.{}", &channel[1..]), + BasicPublishOptions::default(), + irc_message + .source_nickname() + .expect("Hey, who joined?") + .as_bytes() + .to_vec(), + BasicProperties::default(), + ) + .await?; + } + Command::PART(channel, _) => { + pub_channel + .basic_publish( + "irc", + &format!("part.{}", &channel[1..]), + BasicPublishOptions::default(), + irc_message + .source_nickname() + .expect("Hey, who left?") + .as_bytes() + .to_vec(), + BasicProperties::default(), + ) + .await?; + } + _ => (), + } + + Ok(()) +} + +async fn handle_amqp( + irc_client: &mut Client, + irc_channel: &str, + delivery: Delivery, +) -> AHResult<()> { + let message = String::from_utf8(delivery.data)?; + Ok(irc_client.send_privmsg(irc_channel, message)?) +} + +#[tokio::main] +async fn main() -> AHResult<()> { + let amqp_url = std::env::var("AMQP_URL").expect("expected AMQP_URL env variabe"); + let irc_url = std::env::var("IRC_URL").expect("expected IRC_URL env variabe"); + let conn = Connection::connect(&amqp_url, ConnectionProperties::default().with_tokio()).await?; + + let pub_channel = conn.create_channel().await?; + let sub_channel = conn.create_channel().await?; + + let irc_config = url2irc_config(&irc_url)?; + let irc_channel = irc_config.channels[0].to_owned(); + let mut irc_client = Client::from_config(irc_config).await?; + let mut irc_stream = irc_client.stream()?; + irc_client.identify()?; + + let queue = sub_channel + .queue_declare( + &random_string(32), + QueueDeclareOptions { + exclusive: true, + auto_delete: true, + ..QueueDeclareOptions::default() + }, + FieldTable::default(), + ) + .await?; + + sub_channel + .queue_bind( + queue.name().as_str(), + "irc", + &format!("say.{}", &irc_channel[1..]), + QueueBindOptions::default(), + FieldTable::default(), + ) + .await?; + + let mut subscriber = sub_channel + .basic_consume( + queue.name().as_str(), + &random_string(32), + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await?; + + loop { + tokio::select! { + Some(irc_message) = irc_stream.next() => handle_irc(&pub_channel, &irc_message?).await?, + Some(amqp_message) = subscriber.next() => { + let (_, delivery) = amqp_message.expect("error in consumer"); + delivery + .ack(BasicAckOptions::default()) + .await?; + + handle_amqp(&mut irc_client, &irc_channel, delivery).await? + } + } + } +}