pull in neglected fix
authornemo
Sun, 06 Aug 2023 18:24:39 -0400
changeset 15973 2d9d07ccb8ef
parent 15971 cee831693af1 (diff)
parent 15972 303cf91e5233 (current diff)
child 15974 8bb07b0f50ca
pull in neglected fix
--- a/CMakeLists.txt	Thu Jan 12 22:15:24 2023 +0100
+++ b/CMakeLists.txt	Sun Aug 06 18:24:39 2023 -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)
@@ -177,6 +177,7 @@
                               "-fno-warn-unused-do-bind"
                               "-O0"
                               )
+    set(USE_DEBUG_LIBRARIES TRUE)
 else()
     list(APPEND haskell_flags "-w" # no warnings
                               "-O2"
@@ -249,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()
--- a/QTfrontend/CMakeLists.txt	Thu Jan 12 22:15:24 2023 +0100
+++ b/QTfrontend/CMakeLists.txt	Sun Aug 06 18:24:39 2023 -0400
@@ -14,7 +14,7 @@
 include(CheckLibraryExists)
 
 find_package(SDL2 REQUIRED CONFIG)
-find_package(SDL2_mixer 2 REQUIRED) #audio in SDLInteraction
+find_package(SDL2_mixer REQUIRED CONFIG) #audio in SDLInteraction
 include_directories(${SDL2_INCLUDE_DIRS})
 include_directories(${SDL2_MIXER_INCLUDE_DIRS})
 
@@ -229,16 +229,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::SDL2)
-    endif()
-
     list(APPEND HW_LINK_LIBS
         ole32
         oleaut32
--- a/QTfrontend/hedgewars.qrc	Thu Jan 12 22:15:24 2023 +0100
+++ b/QTfrontend/hedgewars.qrc	Sun Aug 06 18:24:39 2023 -0400
@@ -1,6 +1,7 @@
 <RCC>
     <qresource prefix="/">
         <file alias="Ammos.png">../share/hedgewars/Data/Graphics/AmmoMenu/Ammos_base.png</file>
+        <file alias="Ammos_ExtraDamage_comma.png">../share/hedgewars/Data/Graphics/AmmoMenu/Ammos_ExtraDamage_comma.png</file>
         <file alias="keys.csv">../share/hedgewars/Data/misc/keys.csv</file>
         <file>res/css/qt.css</file>
         <file>res/css/chat.css</file>
--- a/QTfrontend/hwconsts.h	Thu Jan 12 22:15:24 2023 +0100
+++ b/QTfrontend/hwconsts.h	Sun Aug 06 18:24:39 2023 -0400
@@ -136,3 +136,5 @@
   7, 21, 32, 33, 35, 60\
 }
 
+/* Ammo ID for extra damage */
+#define HW_AMMOTYPE_EXTRADAMAGE 32
--- a/QTfrontend/res/credits.csv	Thu Jan 12 22:15:24 2023 +0100
+++ b/QTfrontend/res/credits.csv	Sun Aug 06 18:24:39 2023 -0400
@@ -172,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",
--- a/QTfrontend/ui/widget/selectWeapon.cpp	Thu Jan 12 22:15:24 2023 +0100
+++ b/QTfrontend/ui/widget/selectWeapon.cpp	Sun Aug 06 18:24:39 2023 -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) :
--- a/README.md	Thu Jan 12 22:15:24 2023 +0100
+++ b/README.md	Sun Aug 06 18:24:39 2023 -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
 
--- a/hedgewars/CMakeLists.txt	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/CMakeLists.txt	Sun Aug 06 18:24:39 2023 -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)
 
--- a/hedgewars/hwengine.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/hwengine.pas	Sun Aug 06 18:24:39 2023 -0400
@@ -19,8 +19,10 @@
 {$INCLUDE "options.inc"}
 
 {$IFDEF WINDOWS}
+{$IFNDEF SKIP_RESOURCES}
 {$R res/hwengine.rc}
 {$ENDIF}
+{$ENDIF}
 
 {$IFDEF HWLIBRARY}
 unit hwengine;
--- a/hedgewars/uAIMisc.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uAIMisc.pas	Sun Aug 06 18:24:39 2023 -0400
@@ -148,7 +148,7 @@
             (Gear^.Health = 0) and
              (Gear^.Damage < 35))
              )  and
-        (Targets.Count < 256) then
+        (Targets.Count < 255) then
         begin
         with Targets.ar[Targets.Count] do
             begin
--- a/hedgewars/uChat.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uChat.pas	Sun Aug 06 18:24:39 2023 -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);
@@ -54,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;
@@ -782,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
@@ -1062,7 +1104,9 @@
                 SetLine(InputStr, '', true);
                 ResetCursor();
                 end
-            else CleanupInput
+            else
+                CleanupInput;
+            oldInput:= '';
             end;
         SDL_SCANCODE_RETURN, SDL_SCANCODE_KP_ENTER:
             begin
@@ -1337,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;
--- a/hedgewars/uCommandHandlers.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uCommandHandlers.pas	Sun Aug 06 18:24:39 2023 -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;
 
--- a/hedgewars/uGearsHandlersMess.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uGearsHandlersMess.pas	Sun Aug 06 18:24:39 2023 -0400
@@ -264,6 +264,7 @@
 
 procedure HideHog(HH: PHedgehog);
 begin
+    if HH^.Gear = nil then exit;
     ScriptCall('onHogHide', HH^.Gear^.Uid);
     DeleteCI(HH^.Gear);
     if FollowGear = HH^.Gear then
@@ -6409,6 +6410,7 @@
 procedure doStepTardisWarp(Gear: PGear);
 var HH: PHedgehog;
     i,j,cnt: LongWord;
+    restoreBottomY: LongInt;
     s: ansistring;
 begin
 HH:= Gear^.Hedgehog;
@@ -6505,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, false);
+            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
--- a/hedgewars/uGearsUtils.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uGearsUtils.pas	Sun Aug 06 18:24:39 2023 -0400
@@ -42,6 +42,7 @@
 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;
@@ -934,7 +935,12 @@
     FindPlace(Gear, withFall, Left, Right, skipProximity, true);
 end;
 
-procedure FindPlace(var Gear: PGear; withFall: boolean; Left, Right: LongInt; skipProximity, deleteOnFail: boolean);
+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;
@@ -963,11 +969,11 @@
         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
+                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));
 
@@ -975,13 +981,13 @@
 
                 repeat
                     inc(y);
-                until (y >= cWaterLine) or
+                until (y >= Bottom) or
                         (ignoreOverlap and 
                                 (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, lfAll)
--- a/hedgewars/uLocale.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uLocale.pas	Sun Aug 06 18:24:39 2023 -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;
 
--- a/hedgewars/uRenderUtils.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uRenderUtils.pas	Sun Aug 06 18:24:39 2023 -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
@@ -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);
@@ -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.
--- a/hedgewars/uScript.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uScript.pas	Sun Aug 06 18:24:39 2023 -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);
@@ -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,
@@ -3225,7 +3292,7 @@
     if CheckLuaParamCount(L, 1, 'HideHog', 'gearUid') then
         begin
         gear:= GearByUID(Trunc(lua_tonumber(L, 1)));
-        if (gear <> nil) and (gear^.hedgehog <> nil) then
+        if (gear <> nil) and (gear^.hedgehog <> nil) and (gear^.hedgehog^.gear <> nil) then
             begin
             HideHog(gear^.hedgehog);
             lua_pushboolean(L, true);
@@ -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);
--- a/hedgewars/uStore.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uStore.pas	Sun Aug 06 18:24:39 2023 -0400
@@ -474,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
--- a/hedgewars/uUtils.pas	Thu Jan 12 22:15:24 2023 +0100
+++ b/hedgewars/uUtils.pas	Sun Aug 06 18:24:39 2023 -0400
@@ -34,6 +34,9 @@
 procedure EscapeCharA(var a: ansistring; e: char);
 procedure UnEscapeCharA(var a: ansistring; e: char);
 
+procedure ReplaceChars(var a: shortstring; c1, c2: char);
+procedure ReplaceCharsA(var a: ansistring; c1, c2: char);
+
 function ExtractFileDir(s: shortstring) : shortstring;
 function ExtractFileName(s: shortstring) : shortstring;
 
@@ -47,6 +50,8 @@
 function  EnumToStr(const en : TSprite) : shortstring; overload;
 function  EnumToStr(const en : TMapGen) : shortstring; overload;
 function  EnumToStr(const en : TWorldEdge) : shortstring; overload;
+function  EnumToStr(const en : TMsgStrId) : shortstring; overload;
+function  EnumToStr(const en : TGoalStrId) : shortstring; overload;
 
 function  Min(a, b: LongInt): LongInt; inline;
 function  MinD(a, b: double) : double; inline;
@@ -304,6 +309,28 @@
 until (i <= 0);
 end; { UnEscapeCharA }
 
+// Replace all characters c1 with c2 in shortstring a
+procedure ReplaceChars(var a: shortstring; c1, c2: char);
+var i: LongInt;
+begin
+repeat
+    i:= Pos(c1, a);
+    if (i > 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))
@@ -354,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
--- a/misc/libphyslayer/physfscompat.h	Thu Jan 12 22:15:24 2023 +0100
+++ b/misc/libphyslayer/physfscompat.h	Sun Aug 06 18:24:39 2023 -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
 
--- a/project_files/HedgewarsMobile/Classes/SupportViewController.m	Thu Jan 12 22:15:24 2023 +0100
+++ b/project_files/HedgewarsMobile/Classes/SupportViewController.m	Sun Aug 06 18:24:39 2023 -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");
--- a/project_files/hwc/CMakeLists.txt	Thu Jan 12 22:15:24 2023 +0100
+++ b/project_files/hwc/CMakeLists.txt	Sun Aug 06 18:24:39 2023 -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}
--- a/rust/hedgewars-server/Cargo.toml	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/Cargo.toml	Sun Aug 06 18:24:39 2023 -0400
@@ -5,7 +5,8 @@
 authors = [ "Andrey Korotaev <a.korotaev@hedgewars.org>" ]
 
 [features]
-official-server = ["mysql_async", "sha1"]
+tls-connections = ["tokio-native-tls"]
+official-server = ["mysql_async", "sha1", "tls-connections"]
 default = []
 
 [dependencies]
@@ -25,6 +26,7 @@
 sha1 = { version = "0.10.0", optional = true }
 slab = "0.4"
 tokio = { version = "1.16", features = ["full"]}
+tokio-native-tls = { version = "0.3", optional = true }
 
 hedgewars-network-protocol = { path = "../hedgewars-network-protocol" }
 
--- a/rust/hedgewars-server/src/core.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/core.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -1,6 +1,5 @@
 pub mod anteroom;
 pub mod client;
-pub mod events;
 pub mod indexslab;
 pub mod room;
 pub mod server;
--- a/rust/hedgewars-server/src/core/anteroom.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/core/anteroom.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -32,6 +32,7 @@
 
 impl BanCollection {
     fn new() -> Self {
+        todo!("add nick bans");
         Self {
             ban_ips: vec![],
             ban_timeouts: vec![],
--- a/rust/hedgewars-server/src/core/client.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/core/client.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -10,6 +10,7 @@
         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;
@@ -29,6 +30,7 @@
 
 impl HwClient {
     pub fn new(id: ClientId, protocol_number: u16, nick: String) -> HwClient {
+        todo!("add quiet flag");
         HwClient {
             id,
             nick,
--- a/rust/hedgewars-server/src/core/events.rs	Thu Jan 12 22:15:24 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-use slab::Slab;
-use std::{
-    convert::TryInto,
-    iter,
-    num::NonZeroU32,
-    time::{Duration, Instant},
-};
-
-struct Event<Data> {
-    event_id: u32,
-    data: Data,
-}
-
-#[derive(Clone)]
-pub struct Timeout {
-    tick_index: u32,
-    event_index: u32,
-    event_id: u32,
-}
-
-pub struct TimedEvents<Data, const MAX_TIMEOUT: usize> {
-    events: [Slab<Event<Data>>; MAX_TIMEOUT],
-    current_time: Instant,
-    current_tick_index: u32,
-    next_event_id: u32,
-    events_count: u32,
-}
-
-impl<Data, const MAX_TIMEOUT: usize> TimedEvents<Data, MAX_TIMEOUT> {
-    pub fn new() -> Self {
-        Self {
-            events: [0; MAX_TIMEOUT].map(|_| Slab::new()),
-            current_time: Instant::now(),
-            current_tick_index: 0,
-            next_event_id: 0,
-            events_count: 0,
-        }
-    }
-
-    pub fn set_timeout(&mut self, seconds_delay: NonZeroU32, data: Data) -> Timeout {
-        let tick_index = (self.current_tick_index
-            + std::cmp::min(seconds_delay.get(), MAX_TIMEOUT as u32))
-            % MAX_TIMEOUT as u32;
-        let event_id = self.next_event_id;
-        self.next_event_id += 1;
-        let event = Event { event_id, data };
-
-        let entry = self.events[tick_index as usize].vacant_entry();
-        let event_index = entry.key() as u32;
-        entry.insert(event);
-
-        self.events_count += 1;
-
-        Timeout {
-            tick_index,
-            event_index,
-            event_id,
-        }
-    }
-
-    pub fn cancel_timeout(&mut self, timeout: Timeout) -> Option<Data> {
-        let events = &mut self.events[timeout.tick_index as usize];
-        if matches!(events.get(timeout.event_index as usize), Some(Event { event_id: id, ..}) if *id == timeout.event_id)
-        {
-            self.events_count -= 1;
-            Some(events.remove(timeout.event_index as usize).data)
-        } else {
-            None
-        }
-    }
-
-    pub fn poll(&mut self, time: Instant) -> Vec<Data> {
-        let mut result = vec![];
-        let second = Duration::from_secs(1);
-        while time - self.current_time > second {
-            self.current_time += second;
-            self.current_tick_index = (self.current_tick_index + 1) % MAX_TIMEOUT as u32;
-            result.extend(
-                self.events[self.current_tick_index as usize]
-                    .drain()
-                    .map(|e| e.data),
-            );
-        }
-        self.events_count -= result.len() as u32;
-        result
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.events_count == 0
-    }
-}
-
-mod test {
-    use super::TimedEvents;
-    use std::{
-        num::NonZeroU32,
-        time::{Duration, Instant},
-    };
-
-    #[test]
-    fn events_test() {
-        let mut events = TimedEvents::<u32, 30>::new();
-        let now = Instant::now();
-
-        let timeouts = (1..=3)
-            .map(|n| events.set_timeout(NonZeroU32::new(n).unwrap(), n))
-            .collect::<Vec<_>>();
-
-        let second = Duration::from_secs(1);
-        assert_eq!(events.cancel_timeout(timeouts[1].clone()), Some(2));
-        assert_eq!(events.poll(now + second), vec![1]);
-        assert!(events.poll(now + second).is_empty());
-        assert!(events.poll(now + 2 * second).is_empty());
-        assert_eq!(events.poll(now + 3 * second), vec![3]);
-    }
-}
--- a/rust/hedgewars-server/src/core/server.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/core/server.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -189,6 +189,7 @@
 
 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();
--- a/rust/hedgewars-server/src/handlers/common.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/handlers/common.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -514,6 +514,7 @@
     result: Result<VoteResult, VoteError>,
     response: &mut super::Response,
 ) {
+    todo!("voting result needs to be processed with raised privileges");
     let room_id = room_control.room().id;
     super::common::get_vote_data(room_control.room().id, &result, response);
 
--- a/rust/hedgewars-server/src/handlers/inanteroom.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/handlers/inanteroom.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -13,8 +13,6 @@
     HwProtocolMessage, HwProtocolMessage::LoadRoom, HwServerMessage::*,
 };
 use log::*;
-#[cfg(feature = "official-server")]
-use openssl::sha::sha1;
 use std::{
     fmt::{Formatter, LowerHex},
     num::NonZeroU16,
--- a/rust/hedgewars-server/src/handlers/inlobby.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/handlers/inlobby.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -26,6 +26,8 @@
 ) {
     use hedgewars_network_protocol::messages::HwProtocolMessage::*;
 
+    todo!("add kick/ban handlers");
+
     match message {
         CreateRoom(name, password) => match server.create_room(client_id, name, password) {
             Err(CreateRoomError::InvalidName) => response.warn(ILLEGAL_ROOM_NAME),
@@ -47,6 +49,7 @@
             }
         },
         Chat(msg) => {
+            todo!("add client quiet flag");
             response.add(
                 ChatMsg {
                     nick: server.client(client_id).nick.clone(),
--- a/rust/hedgewars-server/src/main.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/main.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -27,6 +27,7 @@
     let args: Vec<String> = 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..]) {
--- a/rust/hedgewars-server/src/server/database.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/server/database.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -1,5 +1,6 @@
 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};
 
@@ -25,14 +26,96 @@
 
 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<AccountInfo>),
+    CheckerAccount { is_registered: bool },
+}
+
 pub struct Database {
     pool: Pool,
+    query_rx: Receiver<DatabaseQuery>,
+    response_tx: Sender<DatabaseResponse>,
 }
 
 impl Database {
     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 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;
+            }
         }
     }
 
@@ -40,9 +123,9 @@
         let mut connection = self.pool.get_conn().await?;
         let result = CHECK_ACCOUNT_EXISTS_QUERY
             .with(params! { "username" => nick })
-            .first(&mut connection)
+            .first::<u32, _>(&mut connection)
             .await?;
-        Ok(!result.is_empty())
+        Ok(!result.is_some())
     }
 
     pub async fn get_account(
--- a/rust/hedgewars-server/src/server/demo.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/server/demo.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -135,6 +135,7 @@
         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)? {
--- a/rust/hedgewars-server/src/server/io.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/server/io.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -2,6 +2,7 @@
     fs::{File, OpenOptions},
     io::{Error, ErrorKind, Read, Result, Write},
     sync::{mpsc, Arc},
+    task::Waker,
     thread,
 };
 
@@ -23,8 +24,9 @@
         let (core_tx, io_rx) = mpsc::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() {
@@ -138,7 +140,7 @@
                 io_tx.send((request_id, response));
                 waker.wake();
             }
-        });
+        });*/
 
         Self { core_rx, core_tx }
     }
--- a/rust/hedgewars-server/src/server/network.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hedgewars-server/src/server/network.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -1,22 +1,24 @@
 use bytes::{Buf, Bytes};
 use log::*;
 use slab::Slab;
+use std::io::Error;
+use std::pin::Pin;
+use std::task::{Context, Poll};
 use std::{
     iter::Iterator,
     net::{IpAddr, SocketAddr},
     time::Duration,
 };
 use tokio::{
-    io::AsyncReadExt,
+    io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf},
     net::{TcpListener, TcpStream},
     sync::mpsc::{channel, Receiver, Sender},
 };
+#[cfg(feature = "tls-connections")]
+use tokio_native_tls::{TlsAcceptor, TlsStream};
 
 use crate::{
-    core::{
-        events::{TimedEvents, Timeout},
-        types::ClientId,
-    },
+    core::types::ClientId,
     handlers,
     handlers::{IoResult, IoTask, ServerState},
     protocol::{self, ProtocolDecoder, ProtocolError},
@@ -25,7 +27,6 @@
 use hedgewars_network_protocol::{
     messages::HwServerMessage::Redirect, messages::*, parser::server_message,
 };
-use tokio::io::AsyncWriteExt;
 
 const PING_TIMEOUT: Duration = Duration::from_secs(15);
 
@@ -56,9 +57,65 @@
     }
 }
 
+enum ClientStream {
+    Tcp(TcpStream),
+    #[cfg(feature = "tls-connections")]
+    Tls(TlsStream<TcpStream>),
+}
+
+impl Unpin for ClientStream {}
+
+impl AsyncRead for ClientStream {
+    fn poll_read(
+        self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &mut ReadBuf<'_>,
+    ) -> Poll<std::io::Result<()>> {
+        use ClientStream::*;
+        match Pin::into_inner(self) {
+            Tcp(stream) => Pin::new(stream).poll_read(cx, buf),
+            #[cfg(feature = "tls-connections")]
+            Tls(stream) => Pin::new(stream).poll_read(cx, buf),
+        }
+    }
+}
+
+impl AsyncWrite for ClientStream {
+    fn poll_write(
+        self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<Result<usize, Error>> {
+        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),
+        }
+    }
+
+    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
+        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),
+        }
+    }
+
+    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
+        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),
+        }
+    }
+}
+
 struct NetworkClient {
     id: ClientId,
-    socket: TcpStream,
+    stream: ClientStream,
     receiver: Receiver<Bytes>,
     peer_addr: SocketAddr,
     decoder: ProtocolDecoder,
@@ -67,27 +124,27 @@
 impl NetworkClient {
     fn new(
         id: ClientId,
-        socket: TcpStream,
+        stream: ClientStream,
         peer_addr: SocketAddr,
         receiver: Receiver<Bytes>,
     ) -> Self {
         Self {
             id,
-            socket,
+            stream,
             peer_addr,
             receiver,
             decoder: ProtocolDecoder::new(PING_TIMEOUT),
         }
     }
 
-    async fn read(
-        socket: &mut TcpStream,
+    async fn read<T: AsyncRead + AsyncWrite + Unpin>(
+        stream: &mut T,
         decoder: &mut ProtocolDecoder,
     ) -> protocol::Result<HwProtocolMessage> {
-        let result = decoder.read_from(socket).await;
+        let result = decoder.read_from(stream).await;
         if matches!(result, Err(ProtocolError::Timeout)) {
-            if Self::write(socket, Bytes::from(HwServerMessage::Ping.to_raw_protocol())).await {
-                decoder.read_from(socket).await
+            if Self::write(stream, Bytes::from(HwServerMessage::Ping.to_raw_protocol())).await {
+                decoder.read_from(stream).await
             } else {
                 Err(ProtocolError::Eof)
             }
@@ -96,8 +153,8 @@
         }
     }
 
-    async fn write(socket: &mut TcpStream, mut data: Bytes) -> bool {
-        !data.has_remaining() || matches!(socket.write_buf(&mut data).await, Ok(n) if n > 0)
+    async fn write<T: AsyncWrite + Unpin>(stream: &mut T, mut data: Bytes) -> bool {
+        !data.has_remaining() || matches!(stream.write_buf(&mut data).await, Ok(n) if n > 0)
     }
 
     async fn run(mut self, sender: Sender<ClientUpdate>) {
@@ -111,7 +168,7 @@
             tokio::select! {
                 server_message = self.receiver.recv() => {
                     match server_message {
-                        Some(message) => if !Self::write(&mut self.socket, message).await {
+                        Some(message) => if !Self::write(&mut self.stream, message).await {
                             sender.send(Error("Connection reset by peer".to_string())).await;
                             break;
                         }
@@ -120,7 +177,7 @@
                         }
                     }
                 }
-                client_message = Self::read(&mut self.socket, &mut self.decoder) => {
+                client_message = Self::read(&mut self.stream, &mut self.decoder) => {
                      match client_message {
                         Ok(message) => {
                             if !sender.send(Message(message)).await {
@@ -128,9 +185,10 @@
                             }
                         }
                         Err(e) => {
+                            todo!("send cmdline errors");
                             sender.send(Error(format!("{}", e))).await;
                             if matches!(e, ProtocolError::Timeout) {
-                                Self::write(&mut self.socket, Bytes::from(HwServerMessage::Bye("Ping timeout".to_string()).to_raw_protocol())).await;
+                                Self::write(&mut self.stream, Bytes::from(HwServerMessage::Bye("Ping timeout".to_string()).to_raw_protocol())).await;
                             }
                             break;
                         }
@@ -141,8 +199,16 @@
     }
 }
 
+#[cfg(feature = "tls-connections")]
+struct TlsListener {
+    listener: TcpListener,
+    acceptor: TlsAcceptor,
+}
+
 pub struct NetworkLayer {
     listener: TcpListener,
+    #[cfg(feature = "tls-connections")]
+    tls: TlsListener,
     server_state: ServerState,
     clients: Slab<Sender<Bytes>>,
 }
@@ -151,36 +217,84 @@
     pub async fn run(&mut self) {
         let (update_tx, mut update_rx) = channel(128);
 
-        loop {
-            tokio::select! {
-                Ok((stream, addr)) = self.listener.accept() => {
-                    if let Some(client) = self.create_client(stream, addr).await {
-                        tokio::spawn(client.run(update_tx.clone()));
+        async fn accept_plain_branch(
+            layer: &mut NetworkLayer,
+            value: (TcpStream, SocketAddr),
+            update_tx: Sender<ClientUpdate>,
+        ) {
+            let (stream, addr) = value;
+            if let Some(client) = layer.create_client(ClientStream::Tcp(stream), addr).await {
+                tokio::spawn(client.run(update_tx));
+            }
+        }
+
+        #[cfg(feature = "tls-connections")]
+        async fn accept_tls_branch(
+            layer: &mut NetworkLayer,
+            value: (TcpStream, SocketAddr),
+            update_tx: Sender<ClientUpdate>,
+        ) {
+            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));
                     }
                 }
-                client_message = update_rx.recv(), if !self.clients.is_empty() => {
-                    use ClientUpdateData::*;
-                    match client_message {
-                        Some(ClientUpdate{ client_id, data: Message(message) } ) => {
-                            self.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 self.server_state, client_id, &mut response);
-                            self.handle_response(response).await;
-                        }
-                        None => unreachable!()
-                    }
+                Err(e) => {
+                    warn!("Unable to establish TLS connection: {}", e);
+                }
+            }
+        }
+
+        async fn client_message_branch(
+            layer: &mut NetworkLayer,
+            client_message: Option<ClientUpdate>,
+        ) {
+            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!(),
+            }
+        }
+
+        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, update_tx.clone()).await,
+                client_message = 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, update_tx.clone()).await,
+                Ok(value) = self.tls.listener.accept() => accept_tls_branch(self, value, update_tx.clone()).await,
+                client_message = update_rx.recv(), if !self.clients.is_empty() => client_message_branch(self, client_message).await
             }
         }
     }
 
     async fn create_client(
         &mut self,
-        stream: TcpStream,
+        stream: ClientStream,
         addr: SocketAddr,
     ) -> Option<NetworkClient> {
         let entry = self.clients.vacant_entry();
@@ -263,6 +377,10 @@
 
 pub struct NetworkLayerBuilder {
     listener: Option<TcpListener>,
+    #[cfg(feature = "tls-connections")]
+    tls_listener: Option<TcpListener>,
+    #[cfg(feature = "tls-connections")]
+    tls_acceptor: Option<TlsAcceptor>,
     clients_capacity: usize,
     rooms_capacity: usize,
 }
@@ -273,6 +391,10 @@
             clients_capacity: 1024,
             rooms_capacity: 512,
             listener: None,
+            #[cfg(feature = "tls-connections")]
+            tls_listener: None,
+            #[cfg(feature = "tls-connections")]
+            tls_acceptor: None,
         }
     }
 }
@@ -285,6 +407,22 @@
         }
     }
 
+    #[cfg(feature = "tls-connections")]
+    pub fn with_tls_acceptor(self, listener: TlsAcceptor) -> Self {
+        Self {
+            tls_acceptor: Option::from(listener),
+            ..self
+        }
+    }
+
+    #[cfg(feature = "tls-connections")]
+    pub fn with_tls_listener(self, listener: TlsAcceptor) -> Self {
+        Self {
+            tls_acceptor: Option::from(listener),
+            ..self
+        }
+    }
+
     pub fn build(self) -> NetworkLayer {
         let server_state = ServerState::new(self.clients_capacity, self.rooms_capacity);
 
@@ -292,6 +430,11 @@
 
         NetworkLayer {
             listener: self.listener.expect("No listener provided"),
+            #[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,
         }
--- a/rust/hwphysics/Cargo.toml	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hwphysics/Cargo.toml	Sun Aug 06 18:24:39 2023 -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hwphysics/benches/ecs_bench.rs	Sun Aug 06 18:24:39 2023 -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::<P>();
+    manager.register::<V>();
+
+    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::<P>();
+        manager.register::<V>();
+
+        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::<P>();
+    manager.register::<V>();
+
+    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::<V>(*id);
+            }
+            std::mem::swap(&mut gears1, &mut gears2);
+        })
+    });
+}
+
+criterion_group!(
+    benches,
+    array_run,
+    component_run,
+    component_multirun,
+    component_add_remove
+);
+criterion_main!(benches);
--- a/rust/hwphysics/src/data.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hwphysics/src/data.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -9,17 +9,28 @@
     slice,
 };
 
+const MAX_TYPES: usize = 8;
+
 pub trait TypeTuple: Sized {
-    fn get_types(types: &mut Vec<TypeId>);
+    fn get_types(_types: &mut [TypeId; MAX_TYPES]) -> usize;
 }
 
 impl TypeTuple for () {
-    fn get_types(_types: &mut Vec<TypeId>) {}
+    fn get_types(_types: &mut [TypeId; MAX_TYPES]) -> usize {
+        0
+    }
 }
 
 impl<T: 'static> TypeTuple for &T {
-    fn get_types(types: &mut Vec<TypeId>) {
-        types.push(TypeId::of::<T>());
+    fn get_types(types: &mut [TypeId; MAX_TYPES]) -> usize {
+        if MAX_TYPES > 0 {
+            unsafe {
+                *types.get_unchecked_mut(0) = TypeId::of::<T>();
+            }
+            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<TypeId>) {
-                $(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<F: FnMut(GearId, Self)>(slices: &[*mut u8], count: usize, mut f: F) {
+            unsafe fn iter<FI: FnMut(GearId, Self)>(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<TypeId>) {
-                $(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<F: FnMut(GearId, Self)>(slices: &[*mut u8], count: usize, mut f: F) {
+            unsafe fn iter<FI: FnMut(GearId, Self)>(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::<GearId>() * 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)
     }
@@ -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<T: 'static>(&mut self, gear_id: GearId) {
         if let Some(type_index) = self.get_type_index::<T>() {
+            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 {
@@ -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,7 +586,11 @@
                 }
 
                 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),
+                    );
                 }
             }
         }
@@ -547,12 +610,12 @@
     }
 
     pub fn iter<T: TypeIter + 'static>(&mut self) -> DataIterator<T> {
-        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) => {
@@ -569,7 +632,7 @@
 pub struct DataIterator<'a, T> {
     data: &'a mut GearDataManager,
     types: u64,
-    type_indices: Vec<i8>,
+    type_indices: [i8; MAX_TYPES],
     tags: u64,
     phantom_types: PhantomData<T>,
 }
@@ -578,7 +641,7 @@
     fn new(
         data: &'a mut GearDataManager,
         types: u64,
-        type_indices: Vec<i8>,
+        type_indices: [i8; MAX_TYPES],
     ) -> DataIterator<'a, T> {
         Self {
             data,
@@ -590,12 +653,12 @@
     }
 
     pub fn with_tags<U: TypeTuple + 'static>(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;
             }
         }
@@ -609,8 +672,13 @@
 
     #[inline]
     pub fn run_id<F: FnMut(GearId, T)>(&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);
     }
 }
 
@@ -619,7 +687,12 @@
     use super::{super::common::GearId, GearDataManager};
 
     #[derive(Clone)]
-    struct Datum {
+    struct DatumA {
+        value: u32,
+    }
+
+    #[derive(Clone)]
+    struct DatumB {
         value: u32,
     }
 
@@ -629,15 +702,15 @@
     #[test]
     fn direct_access() {
         let mut manager = GearDataManager::new();
-        manager.register::<Datum>();
+        manager.register::<DatumA>();
         for i in 1..=5 {
-            manager.add(GearId::new(i as u16).unwrap(), &Datum { value: i * i });
+            manager.add(GearId::new(i as u16).unwrap(), &DatumA { value: i * i });
         }
 
         for i in 1..=5 {
             assert_eq!(
                 manager
-                    .get::<Datum>(GearId::new(i as u16).unwrap())
+                    .get::<DatumA>(GearId::new(i as u16).unwrap())
                     .unwrap()
                     .value,
                 i * i
@@ -648,46 +721,72 @@
     #[test]
     fn single_component_iteration() {
         let mut manager = GearDataManager::new();
-        manager.register::<Datum>();
+        manager.register::<DatumA>();
+
         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::<Datum>();
+        manager.register::<DatumA>();
         manager.register::<Tag>();
-        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::<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::<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::<DatumA>();
+        manager.register::<DatumB>();
+
+        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::<DatumA>(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);
+    }
 }
--- a/rust/hwphysics/src/lib.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/hwphysics/src/lib.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -1,6 +1,6 @@
 pub mod collision;
 pub mod common;
-mod data;
+pub mod data;
 mod grid;
 pub mod physics;
 
--- a/rust/mapgen/src/theme.rs	Thu Jan 12 22:15:24 2023 +0100
+++ b/rust/mapgen/src/theme.rs	Sun Aug 06 18:24:39 2023 -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<Rect>,
+    outland_rects: Vec<Rect>,
+    anchors: Vec<Rect>,
+    overlays: Vec<LandObjectOverlay>,
+}
+
+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<Flakes>,
+    music: String,
+    sky: Color,
+    tint: Color,
+}
+
+#[derive(Default)]
 pub struct Theme {
+    border_color: Color,
+    clouds_count: u16,
+    flatten_flakes: bool,
     land_texture: Option<ThemeSprite>,
     border_texture: Option<ThemeSprite>,
+    land_objects: Vec<LandObject>,
+    spays: Vec<LandSpray>,
+    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<Theme, ThemeLoadError> {
Binary file share/hedgewars/Data/Graphics/AmmoMenu/Ammos_ExtraDamage_comma.png has changed
Binary file share/hedgewars/Data/Graphics/AmmoMenu/Ammos_bw_ExtraDamage_comma.png has changed
--- a/share/hedgewars/Data/Locale/es.txt	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/es.txt	Sun Aug 06 18:24:39 2023 -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.
--- a/share/hedgewars/Data/Locale/hedgewars_es.ts	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/hedgewars_es.ts	Sun Aug 06 18:24:39 2023 -0400
@@ -14,7 +14,7 @@
     </message>
     <message>
         <source>Revision %1 (%2)</source>
-        <translation type="unfinished"></translation>
+        <translation>Revision %1 (%2)</translation>
     </message>
     <message>
         <source>Visit our homepage: %1</source>
@@ -22,7 +22,7 @@
     </message>
     <message>
         <source>This program is distributed under the %1.</source>
-        <translation type="unfinished"></translation>
+        <translation>Este programa es distribuido bajo el 1%.</translation>
     </message>
     <message>
         <source>GNU GPL v2</source>
@@ -80,11 +80,11 @@
     </message>
     <message>
         <source>Credits</source>
-        <translation type="unfinished"></translation>
+        <translation>Créditos</translation>
     </message>
     <message>
         <source>Other people</source>
-        <translation type="unfinished"></translation>
+        <translation>Otras personas</translation>
     </message>
     <message>
         <source>%1 (alias %2)</source>
@@ -107,11 +107,11 @@
     </message>
     <message>
         <source>Extended Credits</source>
-        <translation type="unfinished"></translation>
+        <translation>Créditos extendidos</translation>
     </message>
     <message>
         <source>An extended credits list can be found in the CREDITS text file.</source>
-        <translation type="unfinished"></translation>
+        <translation>Una lista de créditos extendidos puede encontrarse en el archivo de texto CREDITS.</translation>
     </message>
     <message>
         <source>&lt;a href=&quot;https://visualstudio.microsoft.com&quot;&gt;VC++&lt;/a&gt;: %1</source>
@@ -119,14 +119,14 @@
     </message>
     <message>
         <source>Unknown Compiler: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Compilador Desconocido: %1</translation>
     </message>
 </context>
 <context>
     <name>AbstractPage</name>
     <message>
         <source>Go back</source>
-        <translation type="unfinished"></translation>
+        <translation>Atrás</translation>
     </message>
 </context>
 <context>
@@ -137,23 +137,23 @@
     </message>
     <message>
         <source>Nick</source>
-        <translation type="unfinished"></translation>
+        <translation>Apodo</translation>
     </message>
     <message>
         <source>IP/Nick</source>
-        <translation type="unfinished"></translation>
+        <translation>IP/Apodo</translation>
     </message>
     <message>
         <source>Reason</source>
-        <translation type="unfinished"></translation>
+        <translation>Razón</translation>
     </message>
     <message>
         <source>Duration</source>
-        <translation type="unfinished"></translation>
+        <translation>Duración</translation>
     </message>
     <message>
         <source>Ok</source>
-        <translation type="unfinished"></translation>
+        <translation>Ok</translation>
     </message>
     <message>
         <source>Cancel</source>
@@ -165,15 +165,15 @@
     </message>
     <message>
         <source>Warning</source>
-        <translation type="unfinished"></translation>
+        <translation>Alerta</translation>
     </message>
     <message>
         <source>permanent</source>
-        <translation type="unfinished"></translation>
+        <translation>permanente</translation>
     </message>
     <message>
         <source>Ban player</source>
-        <translation type="unfinished"></translation>
+        <translation>Banear Jugador</translation>
     </message>
     <message>
         <source>Please specify an IP address.</source>
@@ -188,14 +188,14 @@
     <name>DataManager</name>
     <message>
         <source>Use Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Usar Default</translation>
     </message>
 </context>
 <context>
     <name>FeedbackDialog</name>
     <message>
         <source>View</source>
-        <translation type="unfinished"></translation>
+        <translation>Ver</translation>
     </message>
     <message>
         <source>Cancel</source>
@@ -203,15 +203,15 @@
     </message>
     <message>
         <source>Send Feedback</source>
-        <translation type="unfinished"></translation>
+        <translation>Enviar Feedback</translation>
     </message>
     <message>
         <source>We are always happy about suggestions, ideas, or bug reports.</source>
-        <translation type="unfinished"></translation>
+        <translation>Sugerencias, ideas y reportes de fallos son bienvenidos.</translation>
     </message>
     <message>
         <source>Send us feedback!</source>
-        <translation type="unfinished"></translation>
+        <translation>Envíe sus comentarios!</translation>
     </message>
     <message>
         <source>If you found a bug, you can see if it&apos;s already been reported here: </source>
--- a/share/hedgewars/Data/Locale/pl.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/pl.lua	Sun Aug 06 18:24:39 2023 -0400
@@ -2989,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
 }
--- a/share/hedgewars/Data/Locale/tips_de.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_de.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>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!</tip>
     <tip>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!</tip>
     <tip>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!</tip>
-    <tip>Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit nur so zum Spaß erstellen. Triff die Entwickler auf <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>Hedgewars ist freie Open-Source-Software, die wir in unserer Freizeit nur so zum Spaß erstellen. Triff die Entwickler auf <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>Von Zeit zu Zeit wird es offizielle Turniere geben. Bevorstehende Ereignisse werden auf <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a> ein paar Tage im Voraus angekündigt.</tip>
     <tip>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!</tip>
     <tip>Hedgewars läuft auf vielen verschiedenen Betriebssystemen, unter anderem Microsoft Windows, macOS und GNU/Linux.</tip>
--- a/share/hedgewars/Data/Locale/tips_en.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_en.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>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!</tip>
     <tip>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!</tip>
     <tip>Hedgewars is free software (Open Source) we create in our spare time. Share it with your family and friends as you like!</tip>
-    <tip>Hedgewars is free software (Open Source) we create in our spare time, just for fun! Meet the developers in <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>Hedgewars is free software (Open Source) we create in our spare time, just for fun! Meet the developers in <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>From time to time there will be official tournaments. Upcoming events will be announced at <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a> some days in advance.</tip>
     <tip>Hedgewars is available in many languages. If the translation in your language seems to be missing or outdated, feel free to contact us!</tip>
     <tip>Hedgewars can be run on lots of different operating systems including Microsoft Windows, macOS and GNU/Linux.</tip>
--- a/share/hedgewars/Data/Locale/tips_gd.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_gd.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>’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!</tip>
     <tip>’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?</tip>
     <tip>’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!</tip>
-    <tip>’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 <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>’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 <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>Bi fèill-chluiche oifigeil againn o àm gu àm. Sgaoilidh sinn brathan-naidheachd mu na tachartasan air <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a> beagan làithean ro làimh.</tip>
     <tip>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?</tip>
     <tip>Gabhaidh Hedgewars a ruith air iomadh siostam-obrachaidh, a’ gabhail a-steach Microsoft Windows, MacOS agus GNU/Linux.</tip>
--- a/share/hedgewars/Data/Locale/tips_hu.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_hu.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>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!</tip>
     <tip>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!</tip>
     <tip>A Hedgewars egy szabad szoftver (nyílt forráskódú), amit a szabadidőnkben fejlesztünk. Nyugodtan oszd meg családoddal, barátaiddal is!</tip>
-    <tip>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 <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a> csatornán találkozhatsz.</tip>
+    <tip>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 <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a> csatornán találkozhatsz.</tip>
     <tip>Időről időre hivatalos bajnokságok indulnak. A közelgő eseményeket pár nappal előre bejelentjük a <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a> webhelyen.</tip>
     <tip>A Hedgewars sok nyelven elérhető. Ha a te nyelved fordítása hiányzik vagy elavult, nyugodtan keress minket!</tip>
     <tip>A Hedgewars számos operációs rendszerre elérhető, többek között Microsoft Windowsra, macOS-re és GNU/Linuxra is.</tip>
--- a/share/hedgewars/Data/Locale/tips_it.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_it.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>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!</tip>
     <tip>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!</tip>
     <tip>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!</tip>
-    <tip>Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Incontra gli sviluppatori sul canale <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>Hedgewars è un programma Open Source e gratuito che creiamo nel nostro tempo libero. Incontra gli sviluppatori sul canale <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>Di tanto in tanto ci saranno tornei ufficiali. Gli eventi saranno annunciati su <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a> con qualche giorno di anticipo.</tip>
     <tip>Hedgewars è disponibile in molte lingue. Se la traduzione nella tua lingua sembra mancante o non aggiornata, sentiti libero di contattaci!</tip>
     <tip>Hedgewars può essere usato su molti sistemi operativi differenti come Microsoft Windows, Mac OS X e GNU/Linux.</tip>
--- a/share/hedgewars/Data/Locale/tips_pl.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_pl.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -11,7 +11,7 @@
     <tip>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!</tip>
     <tip>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!</tip>
     <tip>Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie. Jeśli tylko chcesz, rozdaj ją swojej rodzinie i kolegom!</tip>
-    <tip>Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie, tylko dla zabawy! Poznaj twórców na <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>Hedgewars jest darmową grą o otwartym kodzie, którą tworzymy w naszym wolnym czasie, tylko dla zabawy! Poznaj twórców na <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>Od czasu do czasu będą organizowane mistrzostwa. Będą one ogłaszane z wyprzedzeniem na <a href="http://www.hedgewars.org/">http://www.hedgewars.org/</a>.</tip>
     <tip>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ć!</tip>
     <tip>Hedgewars może być uruchomione na różnych systemach operacyjnych, takich jak Microsoft Windows, Mac OS X, oraz GNU/Linux.</tip>
--- a/share/hedgewars/Data/Locale/tips_ru.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_ru.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время. Если у вас возникают вопросы, задайте их на нашем форуме или посетите наш IRC канал!</tip>
     <tip>Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время. Если вам понравилась игра, помогите нам денежным вознаграждением или вкладом в виде вашей работы!</tip>
     <tip>Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время. Распространяйте его среди друзей и членов семьи!</tip>
-    <tip>Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время в своё удовольствие! Встретиться с разработчиками можно тут <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>Hedgewars - это открытое и свободное программное обеспечение, которое мы создаём в наше свободное время в своё удовольствие! Встретиться с разработчиками можно тут <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>Время от времени проводятся официальные турниры. Предстоящие события анонсируются на <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a> за несколько дней.</tip>
     <tip>Hedgewars доступен на многих языках. Если русский перевод устарел или содержит ошибки, сообщите нам или последнему переводчику в списке!</tip>
     <tip>Hedgewars запускается на множестве различных операционных систем, включая Microsoft Windows, Mac OS и Linux.</tip>
--- a/share/hedgewars/Data/Locale/tips_zh_CN.xml	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Locale/tips_zh_CN.xml	Sun Aug 06 18:24:39 2023 -0400
@@ -13,7 +13,7 @@
     <tip>刺猬战争是我们在空余时间做出的开源免费软件。如果你有问题,在我们的论坛询问,或访问IRC房间!</tip>
     <tip>刺猬战争是我们在空余时间做出的开源免费软件。如果你喜欢它,可以给我们捐赠,或贡献自己的工作!</tip>
     <tip>刺猬战争是我们在空余时间做出的开源免费软件。如果你喜欢就和家人朋友分享它!</tip>
-    <tip>刺猬战争是我们在空余时间做出的开源免费软件,只为好玩!在这里与开发人员会面 <a href="irc://irc.freenode.net/hedgewars">#hedgewars</a>!</tip>
+    <tip>刺猬战争是我们在空余时间做出的开源免费软件,只为好玩!在这里与开发人员会面 <a href="irc://irc.libera.chat/hedgewars">#hedgewars</a>!</tip>
     <tip>游戏不时会举办赛事,官网会提前公布即将举办的活动 <a href="https://www.hedgewars.org/">https://www.hedgewars.org/</a>.</tip>
     <tip>刺猬战争提供多种语言,如果你使用的语言翻译缺失或过时,请联系我们!</tip>
     <tip>刺猬战争能在多个操作系统上运行,包括 Microsoft Windows, macOS and GNU/Linux.</tip>
--- a/share/hedgewars/Data/Scripts/Multiplayer/Battalion.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Multiplayer/Battalion.lua	Sun Aug 06 18:24:39 2023 -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)
--- a/share/hedgewars/Data/Scripts/Multiplayer/Continental_supplies.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Multiplayer/Continental_supplies.lua	Sun Aug 06 18:24:39 2023 -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
 
--- a/share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Multiplayer/Mutant.lua	Sun Aug 06 18:24:39 2023 -0400
@@ -21,6 +21,7 @@
 HedgewarsScriptLoad("/Scripts/Locale.lua")
 HedgewarsScriptLoad("/Scripts/Tracker.lua")
 HedgewarsScriptLoad("/Scripts/Params.lua")
+HedgewarsScriptLoad("/Scripts/Utils.lua")
 
 --[[
     MUTANT SCRIPT
@@ -792,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
--- a/share/hedgewars/Data/Scripts/Multiplayer/Racer.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Multiplayer/Racer.lua	Sun Aug 06 18:24:39 2023 -0400
@@ -32,6 +32,7 @@
 HedgewarsScriptLoad("/Scripts/Locale.lua")
 HedgewarsScriptLoad("/Scripts/OfficialChallenges.lua")
 HedgewarsScriptLoad("/Scripts/Params.lua")
+HedgewarsScriptLoad("/Scripts/Utils.lua")
 
 ------------------
 -- Got Variables?
@@ -191,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(WAYPOINT_RADIUS_MIN, 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
 
@@ -505,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."))
--- a/share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Multiplayer/Space_Invasion.lua	Sun Aug 06 18:24:39 2023 -0400
@@ -2,6 +2,7 @@
 HedgewarsScriptLoad("/Scripts/Locale.lua")
 HedgewarsScriptLoad("/Scripts/Tracker.lua")
 HedgewarsScriptLoad("/Scripts/Params.lua")
+HedgewarsScriptLoad("/Scripts/Utils.lua")
 
 --[[
 Space Invasion
@@ -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
--- a/share/hedgewars/Data/Scripts/Multiplayer/TechRacer.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Multiplayer/TechRacer.lua	Sun Aug 06 18:24:39 2023 -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)
--- a/share/hedgewars/Data/Scripts/Utils.lua	Thu Jan 12 22:15:24 2023 +0100
+++ b/share/hedgewars/Data/Scripts/Utils.lua	Sun Aug 06 18:24:39 2023 -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.
--- a/tools/build_vcpkg.bat	Thu Jan 12 22:15:24 2023 +0100
+++ b/tools/build_vcpkg.bat	Sun Aug 06 18:24:39 2023 -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
 
--- a/tools/pas2c/PascalBasics.hs	Thu Jan 12 22:15:24 2023 +0100
+++ b/tools/pas2c/PascalBasics.hs	Sun Aug 06 18:24:39 2023 -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
--- a/tools/pas2c/PascalParser.hs	Thu Jan 12 22:15:24 2023 +0100
+++ b/tools/pas2c/PascalParser.hs	Sun Aug 06 18:24:39 2023 -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
--- a/tools/pas2c/PascalPreprocessor.hs	Thu Jan 12 22:15:24 2023 +0100
+++ b/tools/pas2c/PascalPreprocessor.hs	Sun Aug 06 18:24:39 2023 -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/rc/convert.sh	Sun Aug 06 18:24:39 2023 -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/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/rc/to_f64.rs	Sun Aug 06 18:24:39 2023 -0400
@@ -0,0 +1,4 @@
+pub fn to_f64<T: Into<f64>>(v: T) -> f64 {
+    v.into()
+}
+