Merge default transitional_engine
authorunC0Rr
Wed, 28 Aug 2024 15:34:49 +0200
branchtransitional_engine
changeset 16022 2003b466b279
parent 16021 6a3dc15b78b9 (current diff)
parent 16020 9be943326d9c (diff)
child 16023 0fd23fc57947
Merge default
QTfrontend/CMakeLists.txt
hedgewars/uAIAmmoTests.pas
hedgewars/uTypes.pas
hedgewars/uVariables.pas
rust/integral-geometry/src/lib.rs
rust/lib-hedgewars-engine/src/instance.rs
rust/lib-hedgewars-engine/src/lib.rs
rust/lib-hedgewars-engine/src/world.rs
--- a/INSTALL.md	Wed Aug 28 15:31:51 2024 +0200
+++ b/INSTALL.md	Wed Aug 28 15:34:49 2024 +0200
@@ -81,6 +81,11 @@
     - `regex-tdfa`
     - `binary` >= 0.8.5.1
 
+If you use the `Cabal` based build process:
+    - `zlib` is not needed.
+    - `network` >= 3.0
+    - `network-bsd` >= 2.8.1
+
 Building
 --------
 
@@ -140,6 +145,16 @@
 
 That's all! Enjoy!
 
+### Building the Hedgewars Server only
+
+The Hedgewars Server can also be built separately using `Cabal`. All necessary
+files, including the `hedgewars-server.cabal`, are in the `gameServer`
+subdirectory.
+For most users, the server isn't needed, and this possibility is targeted
+primarily at packagers. If you don't know how to build Haskell projects with
+`Cabal`, this option is likely not for you. Instead use the `cmake` based
+instructions above.
+
 Troubleshooting
 ---------------
 
--- a/QTfrontend/CMakeLists.txt	Wed Aug 28 15:31:51 2024 +0200
+++ b/QTfrontend/CMakeLists.txt	Wed Aug 28 15:34:49 2024 +0200
@@ -14,7 +14,12 @@
 include(CheckLibraryExists)
 
 find_package(SDL2 REQUIRED CONFIG)
-find_package(SDL2_mixer REQUIRED) #audio in SDLInteraction
+if(WIN32 AND VCPKG_TOOLCHAIN)
+	find_package(SDL2_mixer REQUIRED CONFIG) #audio in SDLInteraction
+else()
+	find_package(SDL2_mixer 2 REQUIRED) #audio in SDLInteraction
+endif()
+
 include_directories(${SDL2_INCLUDE_DIRS})
 include_directories(${SDL2_MIXER_INCLUDE_DIRS})
 
--- a/QTfrontend/net/newnetclient.cpp	Wed Aug 28 15:31:51 2024 +0200
+++ b/QTfrontend/net/newnetclient.cpp	Wed Aug 28 15:34:49 2024 +0200
@@ -948,8 +948,9 @@
 
             for(int i = 1; i < lst.size(); ++i)
             {
-                emit chatStringFromNet(tr("%1 *** %2 has joined the room").arg('\x03').arg(lst[i]));
                 m_playersModel->playerJoinedRoom(lst[i], isChief && (lst[i] != mynick));
+                if(!m_playersModel->isFlagSet(lst[i], PlayersListModel::Ignore))
+                        emit chatStringFromNet(tr("%1 *** %2 has joined the room").arg('\x03').arg(lst[i]));
             }
             return;
         }
@@ -962,12 +963,14 @@
                 return;
             }
 
-            if (lst.size() < 3)
-                emit chatStringFromNet(tr("%1 *** %2 has left").arg('\x03').arg(lst[1]));
-            else
-            {
-                QString leaveMsg = QString(lst[2]);
-                emit chatStringFromNet(tr("%1 *** %2 has left (%3)").arg('\x03').arg(lst[1]).arg(HWApplication::translate("server", leaveMsg.toLatin1().constData())));
+            if(!m_playersModel->isFlagSet(lst[1], PlayersListModel::Ignore)) {
+				if (lst.size() < 3)
+					emit chatStringFromNet(tr("%1 *** %2 has left").arg('\x03').arg(lst[1]));
+				else
+				{
+					QString leaveMsg = QString(lst[2]);
+					emit chatStringFromNet(tr("%1 *** %2 has left (%3)").arg('\x03').arg(lst[1]).arg(HWApplication::translate("server", leaveMsg.toLatin1().constData())));
+				}
             }
             m_playersModel->playerLeftRoom(lst[1]);
             return;
--- a/gameServer/ClientIO.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/ClientIO.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -20,6 +20,7 @@
 module ClientIO where
 
 import qualified Control.Exception as Exception
+import Control.Monad
 import Control.Monad.State
 import Control.Concurrent.Chan
 import Control.Concurrent
--- a/gameServer/HWProtoChecker.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/HWProtoChecker.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -20,6 +20,7 @@
 module HWProtoChecker where
 
 import Data.Maybe
+import Control.Monad
 import Control.Monad.Reader
 --------------------------------------
 import CoreTypes
--- a/gameServer/HWProtoCore.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/HWProtoCore.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -19,6 +19,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 module HWProtoCore where
 
+import Control.Monad
 import Control.Monad.Reader
 import Data.Maybe
 import qualified Data.ByteString.Char8 as B
--- a/gameServer/HWProtoLobbyState.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/HWProtoLobbyState.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -21,6 +21,7 @@
 
 import Data.Maybe
 import Data.List
+import Control.Monad
 import Control.Monad.Reader
 import qualified Data.ByteString.Char8 as B
 --------------------------------------
--- a/gameServer/HandlerUtils.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/HandlerUtils.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -18,6 +18,7 @@
 
 module HandlerUtils where
 
+import Control.Monad
 import Control.Monad.Reader
 import qualified Data.ByteString.Char8 as B
 import Data.List
--- a/gameServer/ServerState.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/ServerState.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -30,6 +30,7 @@
     io
     ) where
 
+import Control.Monad
 import Control.Monad.State.Strict
 import Data.Set as Set(Set)
 import Data.Word
--- a/gameServer/Votes.hs	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/Votes.hs	Wed Aug 28 15:34:49 2024 +0200
@@ -19,6 +19,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 module Votes where
 
+import Control.Monad
 import Control.Monad.Reader
 import Control.Monad.State.Strict
 import ServerState
--- a/gameServer/hedgewars-server.cabal	Wed Aug 28 15:31:51 2024 +0200
+++ b/gameServer/hedgewars-server.cabal	Wed Aug 28 15:34:49 2024 +0200
@@ -1,5 +1,5 @@
 Name:                hedgewars-server
-Version:             0.1
+Version:             1.1.0
 Synopsis:            hedgewars server
 Description:         hedgewars server
 Homepage:            https://www.hedgewars.org/
@@ -11,37 +11,71 @@
 Cabal-version:       >=1.10
 
 
+flag officialServer
+  description: Build for official server
+  default:     False
+  manual:      True
+
 Executable hedgewars-server
   main-is: hedgewars-server.hs
+  other-modules:
+    Actions
+    ClientIO
+    CommandHelp
+    ConfigFile
+    Consts
+    CoreTypes
+    Data.TConfig
+    EngineInteraction
+    FloodDetection
+    HWProtoChecker
+    HWProtoCore
+    HWProtoInRoomState
+    HWProtoLobbyState
+    HWProtoNEState
+    HandlerUtils
+    JoinsMonitor
+    NetRoutines
+    OfficialServer.DBInteraction
+    Opts
+    RoomsAndClients
+    ServerCore
+    ServerState
+    Store
+    Utils
+    Votes
 
   default-language:    Haskell2010
 
 -- Don't forget to update INSTALL.md and .travis.yml when you change these dependencies!
   Build-depends:
     base >= 4.8,
-    containers,
-    vector,
+    binary >= 0.8.5.1,
     bytestring,
-    network >= 2.3 && < 3.2,
+    containers,
+    deepseq,
+    entropy,
+    hslogger,
+    mtl >= 2,
+    network >= 3.0 && < 3.2,
     network-bsd >= 2.8.1 && < 2.9,
+    process,
     random,
-    time,
-    mtl >= 2,
+    regex-tdfa,
     sandi,
-    hslogger,
-    process,
-    deepseq,
+    SHA,
+    time,
     utf8-string,
-    SHA,
-    entropy,
-    zlib >= 0.5.3 && < 0.7,
-    regex-tdfa,
-    binary >= 0.8.5.1,
+    vector
 
 -- These dependencies are for OFFICIAL_SERVER only and do not need to be mentioned in docs
-    yaml >= 0.8.30,
-    aeson,
-    text >= 1.2
+  if flag(officialServer)
+    build-depends:
+      aeson,
+      text >= 1.2,
+      yaml >= 0.8.30,
+      zlib >= 0.5.3 && < 0.7
+    cpp-options:   -DOFFICIAL_SERVER
 
   if !os(windows)
     build-depends: unix
--- a/hedgewars/uAI.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uAI.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -228,12 +228,14 @@
                         if dAngle > 0 then
                             begin
                             AddAction(BestActions, aia_Up, aim_push, 300 + random(250), 0, 0);
-                            AddAction(BestActions, aia_Up, aim_release, dAngle, 0, 0)
+                            AddAction(BestActions, aia_waitAngle, ap.Angle, 1, 0, 0);
+                            AddAction(BestActions, aia_Up, aim_release, 1, 0, 0)
                             end
                         else if dAngle < 0 then
                             begin
                             AddAction(BestActions, aia_Down, aim_push, 300 + random(250), 0, 0);
-                            AddAction(BestActions, aia_Down, aim_release, -dAngle, 0, 0)
+                            AddAction(BestActions, aia_waitAngle, ap.Angle, 1, 0, 0);
+                            AddAction(BestActions, aia_Down, aim_release, 1, 0, 0)
                             end
                         end;
 
--- a/hedgewars/uAIAmmoTests.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uAIAmmoTests.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -2478,7 +2478,7 @@
 Vy:= (Targ.Point.Y - y) * 1 / 1024;
 ap.Angle:= DxDy2AttackAnglef(Vx, -Vy);
 // Minigun angle is limited
-if (ap.Angle < Ammoz[amMinigun].minAngle) or (ap.Angle > Ammoz[amMinigun].maxAngle) then
+if (abs(ap.Angle) < Ammoz[amMinigun].minAngle) or (abs(ap.Angle) > Ammoz[amMinigun].maxAngle) then
     exit(BadTurn);
 
 // Apply inaccuracy
--- a/hedgewars/uGears.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uGears.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -223,6 +223,14 @@
     end;
 end;
 
+procedure processFlakes;
+var i: Longword;
+begin
+    if GameTicks and $7 = 0 then
+        for i:= 1 to FlakesCount do
+            doStepSnowflake(@Flakes[i - 1])
+end;
+
 procedure ProcessGears;
 var t, tmpGear: PGear;
     i, j, AliveCount: LongInt;
@@ -256,6 +264,8 @@
 if StepSoundTimer > 0 then
     dec(StepSoundTimer, 1);
 
+processFlakes;
+
 t:= GearsList;
 while t <> nil do
     begin
@@ -700,6 +710,7 @@
 procedure DrawGears;
 var Gear: PGear;
     x, y: LongInt;
+    i: Longword;
 begin
 Gear:= GearsList;
 while Gear <> nil do
@@ -713,6 +724,17 @@
     Gear:= Gear^.NextGear
     end;
 
+for i:= 1 to FlakesCount do
+    begin
+    Gear:= @Flakes[i - 1];
+    if (Gear^.State and gstInvisible = 0) and (Gear^.Message and gmRemoveFromList = 0) then
+        begin
+        x:= hwRound(Gear^.X) + WorldDx;
+        y:= hwRound(Gear^.Y) + WorldDy;
+        RenderGear(Gear, x, y);
+        end;
+    end;
+
 if SpeechHogNumber > 0 then
     DrawHHOrder();
 end;
@@ -997,13 +1019,29 @@
 snowRight:= max(LAND_WIDTH,4096)+512;
 snowLeft:= -(snowRight-LAND_WIDTH);
 
+FlakesCount:= 0;
+{
 if (not hasBorder) and cSnow then
+    begin
     for i:= vobCount * Longword(max(LAND_WIDTH,4096)) div 2048 downto 1 do
         begin
         rx:=GetRandom(snowRight - snowLeft);
         ry:=GetRandom(750);
         AddGear(rx + snowLeft, LongInt(LAND_HEIGHT) + ry - 1300, gtFlake, 0, _0, _0, 0)
         end
+    end
+}
+if (not hasBorder) and cSnow then
+    begin
+     FlakesCount:= vobCount * Longword(max(LAND_WIDTH,4096)) div 2048;
+     SetLength(Flakes, FlakesCount);
+     for i:= 0 to FlakesCount - 1 do
+        begin
+        rx:=GetRandom(snowRight - snowLeft);
+        ry:=GetRandom(750);
+        initializeGear(@Flakes[i], rx + snowLeft, LongInt(LAND_HEIGHT) + ry - 1300, gtFlake, 0, _0, _0, 0, 0)
+        end
+     end
 end;
 
 // sort clans horizontally (bubble-sort, because why not)
--- a/hedgewars/uGearsList.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uGearsList.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -22,6 +22,7 @@
 interface
 uses uFloat, uTypes, SDLh;
 
+procedure initializeGear(gear: PGear; X, Y: LongInt; Kind: TGearType; State: Longword; dX, dY: hwFloat; Timer, newUid: LongWord);
 function  AddGear(X, Y: LongInt; Kind: TGearType; State: Longword; dX, dY: hwFloat; Timer: LongWord): PGear;
 function  AddGear(X, Y: LongInt; Kind: TGearType; State: Longword; dX, dY: hwFloat; Timer, newUid: LongWord): PGear;
 procedure DeleteGear(Gear: PGear);
@@ -167,6 +168,680 @@
 Gear^.PrevGear:= nil
 end;
 
+procedure initializeGear(gear: PGear; X, Y: LongInt; Kind: TGearType; State: Longword; dX, dY: hwFloat; Timer, newUid: LongWord);
+var cakeData: PCakeData;
+begin
+    FillChar(gear^, sizeof(TGear), 0);
+    gear^.X:= int2hwFloat(X);
+    gear^.Y:= int2hwFloat(Y);
+    gear^.Target.X:= NoPointX;
+    gear^.Kind := Kind;
+    gear^.State:= State;
+    gear^.Active:= true;
+    gear^.dX:= dX;
+    gear^.dY:= dY;
+    gear^.doStep:= doStepHandlers[Kind];
+    gear^.CollisionIndex:= -1;
+    gear^.Timer:= Timer;
+    if newUid = 0 then
+         gear^.uid:= GCounter
+    else gear^.uid:= newUid;
+    gear^.SoundChannel:= -1;
+    gear^.ImpactSound:= sndNone;
+    gear^.Density:= _1;
+    // Define ammo association, if any.
+    gear^.AmmoType:= GearKindAmmoTypeMap[Kind];
+    gear^.CollisionMask:= lfAll;
+    gear^.Tint:= $FFFFFFFF;
+    gear^.Data:= nil;
+    gear^.Sticky:= false;
+
+    if CurrentHedgehog <> nil then
+        begin
+        gear^.Hedgehog:= CurrentHedgehog;
+        if (CurrentHedgehog^.Gear <> nil) and (hwRound(CurrentHedgehog^.Gear^.X) = X) and (hwRound(CurrentHedgehog^.Gear^.Y) = Y) then
+            gear^.CollisionMask:= lfNotCurHogCrate
+        end;
+
+    if (Ammoz[Gear^.AmmoType].Ammo.Propz and ammoprop_NeedTarget <> 0) then
+        gear^.Z:= cHHZ+1
+    else gear^.Z:= cUsualZ;
+
+    // set gstInBounceEdge if gear spawned inside the bounce world edge
+    if WorldEdge = weBounce then
+        if (hwRound(gear^.X) - Gear^.Radius < leftX) or (hwRound(gear^.X) + Gear^.Radius > rightX) then
+            case gear^.Kind of
+                // list all gears here that could collide with the bounce world edge
+                gtHedgehog,
+                gtFlame,
+                gtMine,
+                gtAirBomb,
+                gtDrill,
+                gtNapalmBomb,
+                gtCase,
+                gtAirMine,
+                gtExplosives,
+                gtGrenade,
+                gtShell,
+                gtBee,
+                gtDynamite,
+                gtClusterBomb,
+                gtMelonPiece,
+                gtCluster,
+                gtMortar,
+                gtKamikaze,
+                gtCake,
+                gtWatermelon,
+                gtGasBomb,
+                gtHellishBomb,
+                gtBall,
+                gtRCPlane,
+                gtSniperRifleShot,
+                gtShotgunShot,
+                gtDEagleShot,
+                gtSineGunShot,
+                gtMinigunBullet,
+                gtEgg,
+                gtPiano,
+                gtSMine,
+                gtSnowball,
+                gtKnife,
+                gtCreeper,
+                gtSentry,
+                gtMolotov,
+                gtFlake,
+                gtGrave,
+                gtPortal,
+                gtTarget:
+                gear^.State := gear^.State or gstInBounceEdge;
+            end;
+
+    case Kind of
+              gtFlame: Gear^.Boom := 2;  // some additional expl in there are x3, x4 this
+           gtHedgehog: Gear^.Boom := 30;
+               gtMine: Gear^.Boom := 50;
+               gtCase: Gear^.Boom := 25;
+            gtAirMine: Gear^.Boom := 30;
+         gtExplosives: Gear^.Boom := 75;
+            gtGrenade: Gear^.Boom := 50;
+              gtShell: Gear^.Boom := 50;
+                gtBee: Gear^.Boom := 50;
+        gtShotgunShot: Gear^.Boom := 25;
+         gtPickHammer: Gear^.Boom := 6;
+    //           gtRope: Gear^.Boom := 2; could be funny to have rope attaching to hog deal small amount of dmg?
+         gtDEagleShot: Gear^.Boom := 7;
+           gtDynamite: Gear^.Boom := 75;
+        gtClusterBomb: Gear^.Boom := 20;
+         gtMelonPiece,
+            gtCluster: Gear^.Boom := Timer;
+             gtShover: Gear^.Boom := 30;
+          gtFirePunch: Gear^.Boom := 30;
+            gtAirBomb: Gear^.Boom := 30;
+          gtBlowTorch: Gear^.Boom := 2;
+             gtMortar: Gear^.Boom := 20;
+               gtWhip: Gear^.Boom := 30;
+           gtKamikaze: Gear^.Boom := 30; // both shove and explosion
+               gtCake: Gear^.Boom := cakeDmg; // why is cake damage a global constant
+         gtWatermelon: Gear^.Boom := 75;
+        gtHellishBomb: Gear^.Boom := 90;
+              gtDrill: if Gear^.State and gsttmpFlag = 0 then
+                            Gear^.Boom := 50
+                       else Gear^.Boom := 30;
+               gtBall: Gear^.Boom := 40;
+            gtRCPlane: Gear^.Boom := 25;
+    // sniper rifle is distance linked, this Boom is just an arbitrary scaling factor applied to timer-based-damage
+    // because, eh, why not..
+    gtSniperRifleShot: Gear^.Boom := 100000;
+                gtEgg: Gear^.Boom := 10;
+              gtPiano: Gear^.Boom := 80;
+            gtGasBomb: Gear^.Boom := 20;
+        gtSineGunShot: Gear^.Boom := 35;
+              gtSMine: Gear^.Boom := 30;
+        gtSnowball: Gear^.Boom := 200000; // arbitrary scaling for the shove
+             gtHammer: if cDamageModifier > _1 then // scale it based on cDamageModifier?
+                             Gear^.Boom := 2
+                        else Gear^.Boom := 3;
+        gtPoisonCloud: Gear^.Boom := 20;
+              gtKnife: Gear^.Boom := 40000; // arbitrary scaling factor since impact-based
+            gtCreeper: Gear^.Boom := 100;
+      gtMinigunBullet: Gear^.Boom := 2;
+             gtSentry: Gear^.Boom := 40;
+        end;
+
+    case Kind of
+         gtGrenade,
+         gtClusterBomb,
+         gtGasBomb: begin
+                    gear^.ImpactSound:= sndGrenadeImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 5;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    gear^.Density:= _1_5;
+                    gear^.RenderTimer:= true;
+                    if gear^.Timer = 0 then
+                        gear^.Timer:= 3000
+                    end;
+      gtWatermelon: begin
+                    gear^.ImpactSound:= sndMelonImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 6;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _2;
+                    gear^.RenderTimer:= true;
+                    if gear^.Timer = 0 then
+                        gear^.Timer:= 3000
+                    end;
+      gtMelonPiece: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Density:= _2;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_995;
+                    gear^.Radius:= 4
+                    end;
+        gtHedgehog: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= cHHRadius;
+                    gear^.Elasticity:= _0_35;
+                    gear^.Friction:= _0_999;
+                    gear^.Angle:= cMaxAngle div 2;
+                    gear^.Density:= _3;
+                    gear^.Z:= cHHZ;
+                    if (GameFlags and gfAISurvival) <> 0 then
+                        if gear^.Hedgehog^.BotLevel > 0 then
+                            gear^.Hedgehog^.Effects[heResurrectable] := 1;
+                    if (GameFlags and gfArtillery) <> 0 then
+                        gear^.Hedgehog^.Effects[heArtillery] := 1;
+                    // this would presumably be set in the frontend
+                    // if we weren't going to do that yet, would need to reinit GetRandom
+                    // oh, and, randomising slightly R and B might be nice too.
+                    //gear^.Tint:= $fa00efff or ((random(80)+128) shl 16)
+                    //gear^.Tint:= $faa4efff
+                    //gear^.Tint:= (($e0+random(32)) shl 24) or
+                    //             ((random(80)+128) shl 16) or
+                    //             (($d5+random(32)) shl 8) or $ff
+                    {c:= GetRandom(32);
+                    gear^.Tint:= (($e0+c) shl 24) or
+                                 ((GetRandom(90)+128) shl 16) or
+                                 (($d5+c) shl 8) or $ff}
+                    end;
+       gtParachute: begin
+                    gear^.Tag:= 1; // hog face dir. 1 = right, -1 = left
+                    gear^.Z:= cCurrHHZ;
+                    end;
+           gtShell: begin
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    gear^.Radius:= 4;
+                    gear^.Density:= _1;
+                    gear^.AdvBounce:= 1;
+                    end;
+           gtSnowball: begin
+                    gear^.ImpactSound:= sndMudballImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 4;
+                    gear^.Density:= _0_5;
+                    gear^.AdvBounce:= 1;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    end;
+
+         gtFlake: begin
+                    with Gear^ do
+                        begin
+                        Pos:= 0;
+                        Radius:= 1;
+                        DirAngle:= random(360);
+                        Sticky:= true;
+                        if State and gstTmpFlag = 0 then
+                            begin
+                            dx.isNegative:= GetRandom(2) = 0;
+                            dx.QWordValue:= QWord($40DA) * GetRandom(10000) * 8;
+                            dy.isNegative:= false;
+                            dy.QWordValue:= QWord($3AD3) * GetRandom(7000) * 8;
+                            if GetRandom(2) = 0 then
+                                dx := -dx;
+                            Tint:= $FFFFFFFF
+                            end
+                        else
+                            Tint:= (ExplosionBorderColor shr RShift and $FF shl 24) or
+                                   (ExplosionBorderColor shr GShift and $FF shl 16) or
+                                   (ExplosionBorderColor shr BShift and $FF shl 8) or $FF;
+                        State:= State or gstInvisible;
+                        // use health field to store current frameticks
+                        if vobFrameTicks > 0 then
+                            Health:= random(vobFrameTicks)
+                        else
+                            Health:= 0;
+                        // use timer to store currently displayed frame index
+                        if gear^.Timer = 0 then Timer:= random(vobFramesCount);
+                        Damage:= (random(2) * 2 - 1) * (vobVelocity + random(vobVelocity)) * 8
+                        end
+                    end;
+           gtGrave: begin
+                    gear^.ImpactSound:= sndGraveImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 10;
+                    gear^.Elasticity:= _0_6;
+                    gear^.Z:= 1;
+                    end;
+             gtBee: begin
+                    gear^.Radius:= 5;
+                    if gear^.Timer = 0 then gear^.Timer:= 500;
+                    gear^.RenderTimer:= true;
+                    gear^.Elasticity:= _0_9;
+                    gear^.Tag:= 0;
+                    gear^.State:= Gear^.State or gstSubmersible
+                    end;
+       gtSeduction: begin
+                    gear^.Radius:= cSeductionDist;
+                    end;
+     gtShotgunShot: begin
+                    if gear^.Timer = 0 then gear^.Timer:= 900;
+                    gear^.Radius:= 2
+                    end;
+      gtPickHammer: begin
+                    gear^.Radius:= 10;
+                    if gear^.Timer = 0 then gear^.Timer:= 4000
+                    end;
+       gtHammerHit: begin
+                    gear^.Radius:= 8;
+                    if gear^.Timer = 0 then gear^.Timer:= 125
+                    end;
+            gtRope: begin
+                    gear^.Radius:= 3;
+                    gear^.Friction:= _450 * _0_01 * cRopePercent;
+                    RopePoints.Count:= 0;
+                    gear^.Tint:= $D8D8D8FF;
+                    gear^.Tag:= 0; // normal rope render
+                    gear^.CollisionMask:= lfNotCurHogCrate //lfNotObjMask or lfNotHHObjMask;
+                    end;
+            gtMine: begin
+                    gear^.ImpactSound:= sndMineImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Health:= 10;
+                    gear^.State:= gear^.State or gstMoving;
+                    gear^.Radius:= 2;
+                    gear^.Elasticity:= _0_55;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _1;
+                    if gear^.Timer = 0 then
+                        begin
+                        if cMinesTime < 0 then
+                            begin
+                            gear^.Timer:= getrandom(51)*100;
+                            gear^.Karma:= 1;
+                            end
+                        else
+                            gear^.Timer:= cMinesTime;
+                        end;
+                    gear^.RenderTimer:= true;
+                    end;
+         gtAirMine: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.ImpactSound:= sndAirMineImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Health:= 30;
+                    gear^.State:= gear^.State or gstMoving or gstNoGravity or gstSubmersible;
+                    gear^.Radius:= 8;
+                    gear^.Elasticity:= _0_55;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _1;
+                    gear^.Angle:= 175; // Radius at which air bombs will start "seeking". $FFFFFFFF = unlimited. check is skipped.
+                    gear^.Power:= cMaxWindSpeed.QWordValue div 2; // hwFloat converted. 1/2 g default. defines the "seek" speed when a gear is in range.
+                    gear^.Pos:= cMaxWindSpeed.QWordValue * 3 div 2; // air friction. slows it down when not hitting stuff
+                    gear^.Tag:= 0;
+                    if gear^.Timer = 0 then
+                        begin
+                        if cMinesTime < 0 then
+                            begin
+                            gear^.Timer:= getrandom(13)*100;
+                            gear^.Karma:= 1;
+                            end
+                        else
+                            gear^.Timer:= cMinesTime div 4;
+                        end;
+                    gear^.RenderTimer:= true;
+                    gear^.WDTimer:= gear^.Timer
+                    end;
+           gtSMine: begin
+                    gear^.Health:= 10;
+                    gear^.State:= gear^.State or gstMoving;
+                    gear^.Radius:= 2;
+                    gear^.Elasticity:= _0_55;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _1_6;
+                    gear^.AdvBounce:= 1;
+                    gear^.Sticky:= true;
+                    if gear^.Timer = 0 then gear^.Timer:= 500;
+                    gear^.RenderTimer:= true;
+                    end;
+           gtKnife: begin
+                    gear^.ImpactSound:= sndKnifeImpact;
+                    gear^.AdvBounce:= 1;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    gear^.Density:= _4;
+                    gear^.Radius:= 7;
+                    gear^.Sticky:= true;
+                    end;
+            gtCase: begin
+                    gear^.ImpactSound:= sndCaseImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 16;
+                    gear^.Elasticity:= _0_3;
+                    if gear^.Timer = 0 then gear^.Timer:= 500
+                    end;
+      gtExplosives: begin
+                    gear^.AdvBounce:= 1;
+                    if GameType in [gmtDemo, gmtRecord] then
+                        gear^.RenderHealth:= true;
+                    gear^.ImpactSound:= sndGrenadeImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 16;
+                    gear^.Elasticity:= _0_4;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _6;
+                    gear^.Health:= cBarrelHealth;
+                    gear^.Z:= cHHZ-1
+                    end;
+      gtDEagleShot: begin
+                    gear^.Radius:= 1;
+                    gear^.Health:= 50;
+                    gear^.Data:= nil;
+                    end;
+      gtSniperRifleShot: begin
+                    gear^.Radius:= 1;
+                    gear^.Health:= 50
+                    end;
+        gtDynamite: begin
+                    gear^.ImpactSound:= sndDynamiteImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 3;
+                    gear^.Elasticity:= _0_55;
+                    gear^.Friction:= _0_03;
+                    gear^.Density:= _2;
+                    if gear^.Timer = 0 then gear^.Timer:= 5000;
+                    end;
+         gtCluster: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    gear^.Radius:= 2;
+                    gear^.Density:= _1_5;
+                    gear^.RenderTimer:= true
+                    end;
+          gtShover: begin
+                    gear^.Radius:= 20;
+                    gear^.Tag:= 0;
+                    gear^.Timer:= 50;
+                    end;
+           gtFlame: begin
+                    gear^.Tag:= GetRandom(32);
+                    gear^.Radius:= 1;
+                    gear^.Health:= 5;
+                    gear^.Density:= _1;
+                    gear^.FlightTime:= 9999999; // determines whether in-air flames do damage. disabled by default
+                    if (gear^.dY.QWordValue = 0) and (gear^.dX.QWordValue = 0) then
+                        begin
+                        gear^.dY:= (getrandomf - _0_8) * _0_03;
+                        gear^.dX:= (getrandomf - _0_5) * _0_4
+                        end
+                    end;
+       gtFirePunch: begin
+                    if gear^.Timer = 0 then gear^.Timer:= 3000;
+                    gear^.Radius:= 15;
+                    gear^.Tag:= Y
+                    end;
+       gtAirAttack: begin
+                    gear^.Health:= 6;
+                    gear^.Damage:= 30;
+                    gear^.Z:= cHHZ+2;
+                    gear^.Karma:= 0; // for sound effect: 0 = normal, 1 = underwater
+                    gear^.Radius:= 150;
+                    gear^.FlightTime:= 0; // for timeout in weWrap
+                    gear^.Power:= 0; // count number of wraps in weWrap
+                    gear^.WDTimer:= 0; // number of required wraps
+                    gear^.Density:= _19;
+                    gear^.Tint:= gear^.Hedgehog^.Team^.Clan^.Color shl 8 or $FF
+                    end;
+         gtAirBomb: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 5;
+                    gear^.Density:= _2;
+                    gear^.Elasticity:= _0_55;
+                    gear^.Friction:= _0_995
+                    end;
+       gtBlowTorch: begin
+                    gear^.Radius:= cHHRadius + cBlowTorchC - 1;
+                    if gear^.Timer = 0 then gear^.Timer:= 7500
+                    end;
+        gtSwitcher: begin
+                    gear^.Z:= cCurrHHZ
+                    end;
+          gtTarget: begin
+                    gear^.ImpactSound:= sndGrenadeImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 10;
+                    gear^.Elasticity:= _0_3;
+                    end;
+          gtTardis: begin
+                    gear^.Pos:= 1; // tardis phase
+                    gear^.Tag:= 0; // 1 = hedgehog died, disappeared, took damage or moved
+                    gear^.Z:= cCurrHHZ+1;
+                    end;
+          gtMortar: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 4;
+                    gear^.Elasticity:= _0_2;
+                    gear^.Friction:= _0_08;
+                    gear^.Density:= _1;
+                    end;
+            gtWhip: gear^.Radius:= 20;
+          gtHammer: gear^.Radius:= 20;
+        gtKamikaze: begin
+                    gear^.Health:= 2048;
+                    gear^.Radius:= 20
+                    end;
+            gtCake: begin
+                    gear^.Health:= 2048;
+                    gear^.Radius:= 7;
+                    gear^.Z:= cOnHHZ;
+                    gear^.RenderTimer:= false;
+                    gear^.DirAngle:= -90 * hwSign(Gear^.dX);
+                    gear^.FlightTime:= 100; // (roughly) ticks spent dropping, used to skip getting up anim when stuck.
+                                            // Initially set to a high value so cake has at least one getting up anim.
+                    if not dX.isNegative then
+                        gear^.Angle:= 1
+                    else
+                        gear^.Angle:= 3;
+                    New(cakeData);
+                    gear^.Data:= Pointer(cakeData);
+                    end;
+     gtHellishBomb: begin
+                    gear^.ImpactSound:= sndHellishImpact1;
+                    gear^.nImpactSounds:= 4;
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 4;
+                    gear^.Elasticity:= _0_5;
+                    gear^.Friction:= _0_96;
+                    gear^.Density:= _1_5;
+                    gear^.RenderTimer:= true;
+                    if gear^.Timer = 0 then gear^.Timer:= 5000
+                    end;
+           gtDrill: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    if gear^.Timer = 0 then
+                        gear^.Timer:= 5000;
+                    // Tag for drill strike. if 1 then first impact occured already
+                    gear^.Tag := 0;
+                    // Pos for state. If 1, drill is drilling
+                    gear^.Pos := 0;
+                    gear^.Radius:= 4;
+                    gear^.Density:= _1;
+                    end;
+            gtBall: begin
+                    gear^.ImpactSound:= sndGrenadeImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 5;
+                    gear^.Tag:= random(8);
+                    if gear^.Timer = 0 then gear^.Timer:= 5000;
+                    gear^.Elasticity:= _0_7;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _1_5;
+                    end;
+         gtBallgun: begin
+                    if gear^.Timer = 0 then gear^.Timer:= 5001;
+                    end;
+         gtRCPlane: begin
+                    if gear^.Timer = 0 then gear^.Timer:= 15000;
+                    gear^.Health:= 3;
+                    gear^.Radius:= 8;
+                    gear^.Tint:= gear^.Hedgehog^.Team^.Clan^.Color shl 8 or $FF
+                    end;
+         gtJetpack: begin
+                    gear^.Health:= 2000;
+                    gear^.Damage:= 100;
+                    gear^.State:= Gear^.State or gstSubmersible
+                    end;
+         gtMolotov: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 6;
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    gear^.Density:= _2
+                    end;
+           gtBirdy: begin
+                    gear^.Radius:= 16; // todo: check
+                    gear^.Health := 2000;
+                    gear^.FlightTime := 2;
+                    gear^.Z:= cCurrHHZ;
+                    end;
+             gtEgg: begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 4;
+                    gear^.Elasticity:= _0_6;
+                    gear^.Friction:= _0_96;
+                    gear^.Density:= _1;
+                    if gear^.Timer = 0 then
+                        gear^.Timer:= 3000
+                    end;
+          gtPortal: begin
+                    gear^.ImpactSound:= sndMelonImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Radius:= 17;
+                    // set color
+                    gear^.Tag:= 2 * gear^.Timer;
+                    gear^.Timer:= 15000;
+                    gear^.RenderTimer:= false;
+                    gear^.Health:= 100;
+                    gear^.Sticky:= true;
+                    end;
+           gtPiano: begin
+                    gear^.Radius:= 32;
+                    gear^.Density:= _50;
+                    end;
+     gtSineGunShot: begin
+                    gear^.Radius:= 5;
+                    gear^.Health:= 6000;
+                    end;
+    gtFlamethrower: begin
+                    gear^.Tag:= 10;
+                    if gear^.Timer = 0 then gear^.Timer:= 10;
+                    gear^.Health:= 500;
+                    gear^.Damage:= 100;
+                    end;
+         gtLandGun: begin
+                    gear^.Tag:= 10;
+                    if gear^.Timer = 0 then gear^.Timer:= 10;
+                    gear^.Health:= 1000;
+                    gear^.Damage:= 100;
+                    end;
+     gtPoisonCloud: begin
+                    if gear^.Timer = 0 then gear^.Timer:= 5000;
+                    gear^.WDTimer:= gear^.Timer; // initial timer
+                    gear^.dY:= int2hwfloat(-4 + longint(getRandom(8))) / 1000;
+                    gear^.Tint:= $C0C000C0
+                    end;
+     gtResurrector: begin
+                    gear^.Radius := cResurrectorDist;
+                    gear^.Tag := 0;
+                    gear^.Tint:= $F5DB35FF
+                    end;
+         gtWaterUp: begin
+                    gear^.Tag := 47;
+                    end;
+      gtNapalmBomb: begin
+                    gear^.Elasticity:= _0_8;
+                    gear^.Friction:= _0_8;
+                    if gear^.Timer = 0 then gear^.Timer:= 1000;
+                    gear^.Radius:= 5;
+                    gear^.Density:= _1_5;
+                    end;
+          gtIceGun: begin
+                    gear^.Health:= 1000;
+                    gear^.Radius:= 8;
+                    gear^.Density:= _0;
+                    gear^.Tag:= 0; // sound state: 0 = no sound, 1 = ice beam sound, 2 = idle sound
+                    end;
+         gtCreeper: begin
+                    // TODO: Finish creeper initialization implementation
+                    gear^.Radius:= cHHRadius;
+                    gear^.Elasticity:= _0_35;
+                    gear^.Friction:= _0_93;
+                    gear^.Density:= _5;
+
+                    gear^.AdvBounce:= 1;
+                    gear^.ImpactSound:= sndAirMineImpact;
+                    gear^.nImpactSounds:= 1;
+                    gear^.Health:= 30;
+                    gear^.Radius:= 8;
+                    gear^.Angle:= 175; // Radius at which it will start "seeking". $FFFFFFFF = unlimited. check is skipped.
+                    gear^.Power:= cMaxWindSpeed.QWordValue div 2; // hwFloat converted. 1/2 g default. defines the "seek" speed when a gear is in range.
+                    gear^.Pos:= cMaxWindSpeed.QWordValue * 3 div 2; // air friction. slows it down when not hitting stuff
+                    if gear^.Timer = 0 then
+                        gear^.Timer:= 5000;
+                    gear^.WDTimer:= gear^.Timer
+                    end;
+         gtMinigun: begin
+                    // Timer. First, it's the timer before shooting. Then it will become the shooting timer and is set to Karma
+                    if gear^.Timer = 0 then
+                        gear^.Timer:= 601;
+                    // minigun shooting time. 1 bullet is fired every 50ms
+                    gear^.Karma:= 3451;
+                    end;
+     gtMinigunBullet: begin
+                    gear^.Radius:= 1;
+                    gear^.Health:= 2;
+                    gear^.Karma:= 5; //impact radius
+                    gear^.Pos:= 0; //uses non-global hit order
+                    gear^.Data:= nil;
+                    end;
+            gtSentry: begin
+                    gear^.Radius:= cHHRadius;
+                    gear^.Health:= cSentryHealth;
+                    gear^.Friction:= _0_999;
+                    gear^.Elasticity:= _0_35;
+                    gear^.Density:= _3;
+                    gear^.Tag:= 0;
+                    gear^.Timer:= 1000;
+                    gear^.WDTimer:= 0;
+                    end;
+    gtGenericFaller:begin
+                    gear^.AdvBounce:= 1;
+                    gear^.Radius:= 1;
+                    gear^.Elasticity:= _0_9;
+                    gear^.Friction:= _0_995;
+                    gear^.Density:= _1;
+                    end;
+        end;
+end;
 
 function AddGear(X, Y: LongInt; Kind: TGearType; State: Longword; dX, dY: hwFloat; Timer: LongWord): PGear;
 begin
@@ -175,685 +850,15 @@
 function AddGear(X, Y: LongInt; Kind: TGearType; State: Longword; dX, dY: hwFloat; Timer, newUid: LongWord): PGear;
 var gear: PGear;
     //c: byte;
-    cakeData: PCakeData;
 begin
 if newUid = 0 then
     inc(GCounter);
 
 AddFileLog('AddGear: #' + inttostr(GCounter) + ' (' + inttostr(x) + ',' + inttostr(y) + '), d(' + floattostr(dX) + ',' + floattostr(dY) + ') type = ' + EnumToStr(Kind));
 
-
 New(gear);
-FillChar(gear^, sizeof(TGear), 0);
-gear^.X:= int2hwFloat(X);
-gear^.Y:= int2hwFloat(Y);
-gear^.Target.X:= NoPointX;
-gear^.Kind := Kind;
-gear^.State:= State;
-gear^.Active:= true;
-gear^.dX:= dX;
-gear^.dY:= dY;
-gear^.doStep:= doStepHandlers[Kind];
-gear^.CollisionIndex:= -1;
-gear^.Timer:= Timer;
-if newUid = 0 then
-     gear^.uid:= GCounter
-else gear^.uid:= newUid;
-gear^.SoundChannel:= -1;
-gear^.ImpactSound:= sndNone;
-gear^.Density:= _1;
-// Define ammo association, if any.
-gear^.AmmoType:= GearKindAmmoTypeMap[Kind];
-gear^.CollisionMask:= lfAll;
-gear^.Tint:= $FFFFFFFF;
-gear^.Data:= nil;
-gear^.Sticky:= false;
-
-if CurrentHedgehog <> nil then
-    begin
-    gear^.Hedgehog:= CurrentHedgehog;
-    if (CurrentHedgehog^.Gear <> nil) and (hwRound(CurrentHedgehog^.Gear^.X) = X) and (hwRound(CurrentHedgehog^.Gear^.Y) = Y) then
-        gear^.CollisionMask:= lfNotCurHogCrate
-    end;
-
-if (Ammoz[Gear^.AmmoType].Ammo.Propz and ammoprop_NeedTarget <> 0) then
-    gear^.Z:= cHHZ+1
-else gear^.Z:= cUsualZ;
-
-// set gstInBounceEdge if gear spawned inside the bounce world edge
-if WorldEdge = weBounce then
-    if (hwRound(gear^.X) - Gear^.Radius < leftX) or (hwRound(gear^.X) + Gear^.Radius > rightX) then
-        case gear^.Kind of
-            // list all gears here that could collide with the bounce world edge
-            gtHedgehog,
-            gtFlame,
-            gtMine,
-            gtAirBomb,
-            gtDrill,
-            gtNapalmBomb,
-            gtCase,
-            gtAirMine,
-            gtExplosives,
-            gtGrenade,
-            gtShell,
-            gtBee,
-            gtDynamite,
-            gtClusterBomb,
-            gtMelonPiece,
-            gtCluster,
-            gtMortar,
-            gtKamikaze,
-            gtCake,
-            gtWatermelon,
-            gtGasBomb,
-            gtHellishBomb,
-            gtBall,
-            gtRCPlane,
-            gtSniperRifleShot,
-            gtShotgunShot,
-            gtDEagleShot,
-            gtSineGunShot,
-            gtMinigunBullet,
-            gtEgg,
-            gtPiano,
-            gtSMine,
-            gtSnowball,
-            gtKnife,
-            gtCreeper,
-            gtSentry,
-            gtMolotov,
-            gtFlake,
-            gtGrave,
-            gtPortal,
-            gtTarget:
-            gear^.State := gear^.State or gstInBounceEdge;
-        end;
-
-case Kind of
-          gtFlame: Gear^.Boom := 2;  // some additional expl in there are x3, x4 this
-       gtHedgehog: Gear^.Boom := 30;
-           gtMine: Gear^.Boom := 50;
-           gtCase: Gear^.Boom := 25;
-        gtAirMine: Gear^.Boom := 30;
-     gtExplosives: Gear^.Boom := 75;
-        gtGrenade: Gear^.Boom := 50;
-          gtShell: Gear^.Boom := 50;
-            gtBee: Gear^.Boom := 50;
-    gtShotgunShot: Gear^.Boom := 25;
-     gtPickHammer: Gear^.Boom := 6;
-//           gtRope: Gear^.Boom := 2; could be funny to have rope attaching to hog deal small amount of dmg?
-     gtDEagleShot: Gear^.Boom := 7;
-       gtDynamite: Gear^.Boom := 75;
-    gtClusterBomb: Gear^.Boom := 20;
-     gtMelonPiece,
-        gtCluster: Gear^.Boom := Timer;
-         gtShover: Gear^.Boom := 30;
-      gtFirePunch: Gear^.Boom := 30;
-        gtAirBomb: Gear^.Boom := 30;
-      gtBlowTorch: Gear^.Boom := 2;
-         gtMortar: Gear^.Boom := 20;
-           gtWhip: Gear^.Boom := 30;
-       gtKamikaze: Gear^.Boom := 30; // both shove and explosion
-           gtCake: Gear^.Boom := cakeDmg; // why is cake damage a global constant
-     gtWatermelon: Gear^.Boom := 75;
-    gtHellishBomb: Gear^.Boom := 90;
-          gtDrill: if Gear^.State and gsttmpFlag = 0 then
-                        Gear^.Boom := 50
-                   else Gear^.Boom := 30;
-           gtBall: Gear^.Boom := 40;
-        gtRCPlane: Gear^.Boom := 25;
-// sniper rifle is distance linked, this Boom is just an arbitrary scaling factor applied to timer-based-damage
-// because, eh, why not..
-gtSniperRifleShot: Gear^.Boom := 100000;
-            gtEgg: Gear^.Boom := 10;
-          gtPiano: Gear^.Boom := 80;
-        gtGasBomb: Gear^.Boom := 20;
-    gtSineGunShot: Gear^.Boom := 35;
-          gtSMine: Gear^.Boom := 30;
-    gtSnowball: Gear^.Boom := 200000; // arbitrary scaling for the shove
-         gtHammer: if cDamageModifier > _1 then // scale it based on cDamageModifier?
-                         Gear^.Boom := 2
-                    else Gear^.Boom := 3;
-    gtPoisonCloud: Gear^.Boom := 20;
-          gtKnife: Gear^.Boom := 40000; // arbitrary scaling factor since impact-based
-        gtCreeper: Gear^.Boom := 100;
-  gtMinigunBullet: Gear^.Boom := 2;
-         gtSentry: Gear^.Boom := 40;
-    end;
-
-case Kind of
-     gtGrenade,
-     gtClusterBomb,
-     gtGasBomb: begin
-                gear^.ImpactSound:= sndGrenadeImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 5;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                gear^.Density:= _1_5;
-                gear^.RenderTimer:= true;
-                if gear^.Timer = 0 then
-                    gear^.Timer:= 3000
-                end;
-  gtWatermelon: begin
-                gear^.ImpactSound:= sndMelonImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 6;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _2;
-                gear^.RenderTimer:= true;
-                if gear^.Timer = 0 then
-                    gear^.Timer:= 3000
-                end;
-  gtMelonPiece: begin
-                gear^.AdvBounce:= 1;
-                gear^.Density:= _2;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_995;
-                gear^.Radius:= 4
-                end;
-    gtHedgehog: begin
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= cHHRadius;
-                gear^.Elasticity:= _0_35;
-                gear^.Friction:= _0_999;
-                gear^.Angle:= cMaxAngle div 2;
-                gear^.Density:= _3;
-                gear^.Z:= cHHZ;
-                if (GameFlags and gfAISurvival) <> 0 then
-                    if gear^.Hedgehog^.BotLevel > 0 then
-                        gear^.Hedgehog^.Effects[heResurrectable] := 1;
-                if (GameFlags and gfArtillery) <> 0 then
-                    gear^.Hedgehog^.Effects[heArtillery] := 1;
-                // this would presumably be set in the frontend
-                // if we weren't going to do that yet, would need to reinit GetRandom
-                // oh, and, randomising slightly R and B might be nice too.
-                //gear^.Tint:= $fa00efff or ((random(80)+128) shl 16)
-                //gear^.Tint:= $faa4efff
-                //gear^.Tint:= (($e0+random(32)) shl 24) or
-                //             ((random(80)+128) shl 16) or
-                //             (($d5+random(32)) shl 8) or $ff
-                {c:= GetRandom(32);
-                gear^.Tint:= (($e0+c) shl 24) or
-                             ((GetRandom(90)+128) shl 16) or
-                             (($d5+c) shl 8) or $ff}
-                end;
-   gtParachute: begin
-                gear^.Tag:= 1; // hog face dir. 1 = right, -1 = left
-                gear^.Z:= cCurrHHZ;
-                end;
-       gtShell: begin
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                gear^.Radius:= 4;
-                gear^.Density:= _1;
-                gear^.AdvBounce:= 1;
-                end;
-       gtSnowball: begin
-                gear^.ImpactSound:= sndMudballImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 4;
-                gear^.Density:= _0_5;
-                gear^.AdvBounce:= 1;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                end;
 
-     gtFlake: begin
-                with Gear^ do
-                    begin
-                    Pos:= 0;
-                    Radius:= 1;
-                    DirAngle:= random(360);
-                    Sticky:= true;
-                    if State and gstTmpFlag = 0 then
-                        begin
-                        dx.isNegative:= GetRandom(2) = 0;
-                        dx.QWordValue:= QWord($40DA) * GetRandom(10000) * 8;
-                        dy.isNegative:= false;
-                        dy.QWordValue:= QWord($3AD3) * GetRandom(7000) * 8;
-                        if GetRandom(2) = 0 then
-                            dx := -dx;
-                        Tint:= $FFFFFFFF
-                        end
-                    else
-                        Tint:= (ExplosionBorderColor shr RShift and $FF shl 24) or
-                               (ExplosionBorderColor shr GShift and $FF shl 16) or
-                               (ExplosionBorderColor shr BShift and $FF shl 8) or $FF;
-                    State:= State or gstInvisible;
-                    // use health field to store current frameticks
-                    if vobFrameTicks > 0 then
-                        Health:= random(vobFrameTicks)
-                    else
-                        Health:= 0;
-                    // use timer to store currently displayed frame index
-                    if gear^.Timer = 0 then Timer:= random(vobFramesCount);
-                    Damage:= (random(2) * 2 - 1) * (vobVelocity + random(vobVelocity)) * 8
-                    end
-                end;
-       gtGrave: begin
-                gear^.ImpactSound:= sndGraveImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 10;
-                gear^.Elasticity:= _0_6;
-                gear^.Z:= 1;
-                end;
-         gtBee: begin
-                gear^.Radius:= 5;
-                if gear^.Timer = 0 then gear^.Timer:= 500;
-                gear^.RenderTimer:= true;
-                gear^.Elasticity:= _0_9;
-                gear^.Tag:= 0;
-                gear^.State:= Gear^.State or gstSubmersible
-                end;
-   gtSeduction: begin
-                gear^.Radius:= cSeductionDist;
-                end;
- gtShotgunShot: begin
-                if gear^.Timer = 0 then gear^.Timer:= 900;
-                gear^.Radius:= 2
-                end;
-  gtPickHammer: begin
-                gear^.Radius:= 10;
-                if gear^.Timer = 0 then gear^.Timer:= 4000
-                end;
-   gtHammerHit: begin
-                gear^.Radius:= 8;
-                if gear^.Timer = 0 then gear^.Timer:= 125
-                end;
-        gtRope: begin
-                gear^.Radius:= 3;
-                gear^.Friction:= _450 * _0_01 * cRopePercent;
-                RopePoints.Count:= 0;
-                gear^.Tint:= $D8D8D8FF;
-                gear^.Tag:= 0; // normal rope render
-                gear^.CollisionMask:= lfNotCurHogCrate //lfNotObjMask or lfNotHHObjMask;
-                end;
-        gtMine: begin
-                gear^.ImpactSound:= sndMineImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Health:= 10;
-                gear^.State:= gear^.State or gstMoving;
-                gear^.Radius:= 2;
-                gear^.Elasticity:= _0_55;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _1;
-                if gear^.Timer = 0 then
-                    begin
-                    if cMinesTime < 0 then
-                        begin
-                        gear^.Timer:= getrandom(51)*100;
-                        gear^.Karma:= 1;
-                        end
-                    else
-                        gear^.Timer:= cMinesTime;
-                    end;
-                gear^.RenderTimer:= true;
-                end;
-     gtAirMine: begin
-                gear^.AdvBounce:= 1;
-                gear^.ImpactSound:= sndAirMineImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Health:= 30;
-                gear^.State:= gear^.State or gstMoving or gstNoGravity or gstSubmersible;
-                gear^.Radius:= 8;
-                gear^.Elasticity:= _0_55;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _1;
-                gear^.Angle:= 175; // Radius at which air bombs will start "seeking". $FFFFFFFF = unlimited. check is skipped.
-                gear^.Power:= cMaxWindSpeed.QWordValue div 2; // hwFloat converted. 1/2 g default. defines the "seek" speed when a gear is in range.
-                gear^.Pos:= cMaxWindSpeed.QWordValue * 3 div 2; // air friction. slows it down when not hitting stuff
-                gear^.Tag:= 0;
-                if gear^.Timer = 0 then
-                    begin
-                    if cMinesTime < 0 then
-                        begin
-                        gear^.Timer:= getrandom(13)*100;
-                        gear^.Karma:= 1;
-                        end
-                    else
-                        gear^.Timer:= cMinesTime div 4;
-                    end;
-                gear^.RenderTimer:= true;
-                gear^.WDTimer:= gear^.Timer
-                end;
-       gtSMine: begin
-                gear^.Health:= 10;
-                gear^.State:= gear^.State or gstMoving;
-                gear^.Radius:= 2;
-                gear^.Elasticity:= _0_55;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _1_6;
-                gear^.AdvBounce:= 1;
-                gear^.Sticky:= true;
-                if gear^.Timer = 0 then gear^.Timer:= 500;
-                gear^.RenderTimer:= true;
-                end;
-       gtKnife: begin
-                gear^.ImpactSound:= sndKnifeImpact;
-                gear^.AdvBounce:= 1;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                gear^.Density:= _4;
-                gear^.Radius:= 7;
-                gear^.Sticky:= true;
-                end;
-        gtCase: begin
-                gear^.ImpactSound:= sndCaseImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 16;
-                gear^.Elasticity:= _0_3;
-                if gear^.Timer = 0 then gear^.Timer:= 500
-                end;
-  gtExplosives: begin
-                gear^.AdvBounce:= 1;
-                if GameType in [gmtDemo, gmtRecord] then
-                    gear^.RenderHealth:= true;
-                gear^.ImpactSound:= sndGrenadeImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 16;
-                gear^.Elasticity:= _0_4;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _6;
-                gear^.Health:= cBarrelHealth;
-                gear^.Z:= cHHZ-1
-                end;
-  gtDEagleShot: begin
-                gear^.Radius:= 1;
-                gear^.Health:= 50;
-                gear^.Data:= nil;
-                end;
-  gtSniperRifleShot: begin
-                gear^.Radius:= 1;
-                gear^.Health:= 50
-                end;
-    gtDynamite: begin
-                gear^.ImpactSound:= sndDynamiteImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 3;
-                gear^.Elasticity:= _0_55;
-                gear^.Friction:= _0_03;
-                gear^.Density:= _2;
-                if gear^.Timer = 0 then gear^.Timer:= 5000;
-                end;
-     gtCluster: begin
-                gear^.AdvBounce:= 1;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                gear^.Radius:= 2;
-                gear^.Density:= _1_5;
-                gear^.RenderTimer:= true
-                end;
-      gtShover: begin
-                gear^.Radius:= 20;
-                gear^.Tag:= 0;
-                gear^.Timer:= 50;
-                end;
-       gtFlame: begin
-                gear^.Tag:= GetRandom(32);
-                gear^.Radius:= 1;
-                gear^.Health:= 5;
-                gear^.Density:= _1;
-                gear^.FlightTime:= 9999999; // determines whether in-air flames do damage. disabled by default
-                if (gear^.dY.QWordValue = 0) and (gear^.dX.QWordValue = 0) then
-                    begin
-                    gear^.dY:= (getrandomf - _0_8) * _0_03;
-                    gear^.dX:= (getrandomf - _0_5) * _0_4
-                    end
-                end;
-   gtFirePunch: begin
-                if gear^.Timer = 0 then gear^.Timer:= 3000;
-                gear^.Radius:= 15;
-                gear^.Tag:= Y
-                end;
-   gtAirAttack: begin
-                gear^.Health:= 6;
-                gear^.Damage:= 30;
-                gear^.Z:= cHHZ+2;
-                gear^.Karma:= 0; // for sound effect: 0 = normal, 1 = underwater
-                gear^.Radius:= 150;
-                gear^.FlightTime:= 0; // for timeout in weWrap
-                gear^.Power:= 0; // count number of wraps in weWrap
-                gear^.WDTimer:= 0; // number of required wraps
-                gear^.Density:= _19;
-                gear^.Tint:= gear^.Hedgehog^.Team^.Clan^.Color shl 8 or $FF
-                end;
-     gtAirBomb: begin
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 5;
-                gear^.Density:= _2;
-                gear^.Elasticity:= _0_55;
-                gear^.Friction:= _0_995
-                end;
-   gtBlowTorch: begin
-                gear^.Radius:= cHHRadius + cBlowTorchC - 1;
-                if gear^.Timer = 0 then gear^.Timer:= 7500
-                end;
-    gtSwitcher: begin
-                gear^.Z:= cCurrHHZ
-                end;
-      gtTarget: begin
-                gear^.ImpactSound:= sndGrenadeImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 10;
-                gear^.Elasticity:= _0_3;
-                end;
-      gtTardis: begin
-                gear^.Pos:= 1; // tardis phase
-                gear^.Tag:= 0; // 1 = hedgehog died, disappeared, took damage or moved
-                gear^.Z:= cCurrHHZ+1;
-                end;
-      gtMortar: begin
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 4;
-                gear^.Elasticity:= _0_2;
-                gear^.Friction:= _0_08;
-                gear^.Density:= _1;
-                end;
-        gtWhip: gear^.Radius:= 20;
-      gtHammer: gear^.Radius:= 20;
-    gtKamikaze: begin
-                gear^.Health:= 2048;
-                gear^.Radius:= 20
-                end;
-        gtCake: begin
-                gear^.Health:= 2048;
-                gear^.Radius:= 7;
-                gear^.Z:= cOnHHZ;
-                gear^.RenderTimer:= false;
-                gear^.DirAngle:= -90 * hwSign(Gear^.dX);
-                gear^.FlightTime:= 100; // (roughly) ticks spent dropping, used to skip getting up anim when stuck.
-                                        // Initially set to a high value so cake has at least one getting up anim.
-                if not dX.isNegative then
-                    gear^.Angle:= 1
-                else
-                    gear^.Angle:= 3;
-                New(cakeData);
-                gear^.Data:= Pointer(cakeData);
-                end;
- gtHellishBomb: begin
-                gear^.ImpactSound:= sndHellishImpact1;
-                gear^.nImpactSounds:= 4;
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 4;
-                gear^.Elasticity:= _0_5;
-                gear^.Friction:= _0_96;
-                gear^.Density:= _1_5;
-                gear^.RenderTimer:= true;
-                if gear^.Timer = 0 then gear^.Timer:= 5000
-                end;
-       gtDrill: begin
-                gear^.AdvBounce:= 1;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                if gear^.Timer = 0 then
-                    gear^.Timer:= 5000;
-                // Tag for drill strike. if 1 then first impact occured already
-                gear^.Tag := 0;
-                // Pos for state. If 1, drill is drilling
-                gear^.Pos := 0;
-                gear^.Radius:= 4;
-                gear^.Density:= _1;
-                end;
-        gtBall: begin
-                gear^.ImpactSound:= sndGrenadeImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 5;
-                gear^.Tag:= random(8);
-                if gear^.Timer = 0 then gear^.Timer:= 5000;
-                gear^.Elasticity:= _0_7;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _1_5;
-                end;
-     gtBallgun: begin
-                if gear^.Timer = 0 then gear^.Timer:= 5001;
-                end;
-     gtRCPlane: begin
-                if gear^.Timer = 0 then gear^.Timer:= 15000;
-                gear^.Health:= 3;
-                gear^.Radius:= 8;
-                gear^.Tint:= gear^.Hedgehog^.Team^.Clan^.Color shl 8 or $FF
-                end;
-     gtJetpack: begin
-                gear^.Health:= 2000;
-                gear^.Damage:= 100;
-                gear^.State:= Gear^.State or gstSubmersible
-                end;
-     gtMolotov: begin
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 6;
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                gear^.Density:= _2
-                end;
-       gtBirdy: begin
-                gear^.Radius:= 16; // todo: check
-                gear^.Health := 2000;
-                gear^.FlightTime := 2;
-                gear^.Z:= cCurrHHZ;
-                end;
-         gtEgg: begin
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 4;
-                gear^.Elasticity:= _0_6;
-                gear^.Friction:= _0_96;
-                gear^.Density:= _1;
-                if gear^.Timer = 0 then
-                    gear^.Timer:= 3000
-                end;
-      gtPortal: begin
-                gear^.ImpactSound:= sndMelonImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Radius:= 17;
-                // set color
-                gear^.Tag:= 2 * gear^.Timer;
-                gear^.Timer:= 15000;
-                gear^.RenderTimer:= false;
-                gear^.Health:= 100;
-                gear^.Sticky:= true;
-                end;
-       gtPiano: begin
-                gear^.Radius:= 32;
-                gear^.Density:= _50;
-                end;
- gtSineGunShot: begin
-                gear^.Radius:= 5;
-                gear^.Health:= 6000;
-                end;
-gtFlamethrower: begin
-                gear^.Tag:= 10;
-                if gear^.Timer = 0 then gear^.Timer:= 10;
-                gear^.Health:= 500;
-                gear^.Damage:= 100;
-                end;
-     gtLandGun: begin
-                gear^.Tag:= 10;
-                if gear^.Timer = 0 then gear^.Timer:= 10;
-                gear^.Health:= 1000;
-                gear^.Damage:= 100;
-                end;
- gtPoisonCloud: begin
-                if gear^.Timer = 0 then gear^.Timer:= 5000;
-                gear^.WDTimer:= gear^.Timer; // initial timer
-                gear^.dY:= int2hwfloat(-4 + longint(getRandom(8))) / 1000;
-                gear^.Tint:= $C0C000C0
-                end;
- gtResurrector: begin
-                gear^.Radius := cResurrectorDist;
-                gear^.Tag := 0;
-                gear^.Tint:= $F5DB35FF
-                end;
-     gtWaterUp: begin
-                gear^.Tag := 47;
-                end;
-  gtNapalmBomb: begin
-                gear^.Elasticity:= _0_8;
-                gear^.Friction:= _0_8;
-                if gear^.Timer = 0 then gear^.Timer:= 1000;
-                gear^.Radius:= 5;
-                gear^.Density:= _1_5;
-                end;
-      gtIceGun: begin
-                gear^.Health:= 1000;
-                gear^.Radius:= 8;
-                gear^.Density:= _0;
-                gear^.Tag:= 0; // sound state: 0 = no sound, 1 = ice beam sound, 2 = idle sound
-                end;
-     gtCreeper: begin
-                // TODO: Finish creeper initialization implementation
-                gear^.Radius:= cHHRadius;
-                gear^.Elasticity:= _0_35;
-                gear^.Friction:= _0_93;
-                gear^.Density:= _5;
-
-                gear^.AdvBounce:= 1;
-                gear^.ImpactSound:= sndAirMineImpact;
-                gear^.nImpactSounds:= 1;
-                gear^.Health:= 30;
-                gear^.Radius:= 8;
-                gear^.Angle:= 175; // Radius at which it will start "seeking". $FFFFFFFF = unlimited. check is skipped.
-                gear^.Power:= cMaxWindSpeed.QWordValue div 2; // hwFloat converted. 1/2 g default. defines the "seek" speed when a gear is in range.
-                gear^.Pos:= cMaxWindSpeed.QWordValue * 3 div 2; // air friction. slows it down when not hitting stuff
-                if gear^.Timer = 0 then
-                    gear^.Timer:= 5000;
-                gear^.WDTimer:= gear^.Timer
-                end;
-     gtMinigun: begin
-                // Timer. First, it's the timer before shooting. Then it will become the shooting timer and is set to Karma
-                if gear^.Timer = 0 then
-                    gear^.Timer:= 601;
-                // minigun shooting time. 1 bullet is fired every 50ms
-                gear^.Karma:= 3451;
-                end;
- gtMinigunBullet: begin
-                gear^.Radius:= 1;
-                gear^.Health:= 2;
-                gear^.Karma:= 5; //impact radius
-                gear^.Pos:= 0; //uses non-global hit order
-                gear^.Data:= nil;
-                end;
-        gtSentry: begin
-                gear^.Radius:= cHHRadius;
-                gear^.Health:= cSentryHealth;
-                gear^.Friction:= _0_999;
-                gear^.Elasticity:= _0_35;
-                gear^.Density:= _3;
-                gear^.Tag:= 0;
-                gear^.Timer:= 1000;
-                gear^.WDTimer:= 0;
-                end;
-gtGenericFaller:begin
-                gear^.AdvBounce:= 1;
-                gear^.Radius:= 1;
-                gear^.Elasticity:= _0_9;
-                gear^.Friction:= _0_995;
-                gear^.Density:= _1;
-                end;
-    end;
+initializeGear(gear, X, Y, Kind, State, dX, dY, Timer, newUid);
 
 InsertGearToList(gear);
 AddGear:= gear;
--- a/hedgewars/uIO.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uIO.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -470,8 +470,8 @@
     RemoveCmd
     end;
 
-if (headcmd <> nil) and tmpflag and (not CurrentTeam^.hasGone) then
-    checkFails(GameTicks < LongWord(hiTicks shl 16) + headcmd^.loTime,
+if (headcmd <> nil) and tmpflag and (not CurrentTeam^.hasGone) and (GameTicks < LongWord(hiTicks shl 16) + headcmd^.loTime) then
+    checkFails(true,
             'oops, queue error. in buffer: ' + headcmd^.cmd +
             ' (' + IntToStr(GameTicks) + ' > ' +
             IntToStr(hiTicks shl 16 + headcmd^.loTime) + ')',
--- a/hedgewars/uSound.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uSound.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -508,9 +508,16 @@
             GetFallbackV := sndNooo
         else
             GetFallbackV := sndUhOh
-    else if (snd in [sndDrat, sndBugger]) then
-        GetFallbackV := sndStupid
-    else if (snd in [sndGonnaGetYou, sndIllGetYou, sndJustYouWait, sndCutItOut, sndLeaveMeAlone]) then
+    else if (snd = sndCover) then
+        if random(2) = 0 then
+            GetFallbackV := sndWatchThis
+        else
+            GetFallbackV := sndFire
+    else if (snd in [sndBugger]) then
+        GetFallbackV := sndDrat
+    else if (snd in [sndDrat]) then
+        GetFallbackV := sndBugger
+    else if (snd in [sndGonnaGetYou, sndIllGetYou, sndRevenge, sndCutItOut, sndLeaveMeAlone]) then
         GetFallbackV := sndRegret
     else if (snd in [sndOhDear, sndSoLong]) then
         GetFallbackV := sndByeBye
--- a/hedgewars/uTypes.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uTypes.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -547,6 +547,8 @@
 
     TDirtyTag = packed array of array of byte;
 
+    TGearPackArray = packed array of TGear;
+
     TPreview  = packed array[0..127, 0..31] of byte;
     TPreviewAlpha  = packed array[0..127, 0..255] of byte;
 
--- a/hedgewars/uVariables.pas	Wed Aug 28 15:31:51 2024 +0200
+++ b/hedgewars/uVariables.pas	Wed Aug 28 15:34:49 2024 +0200
@@ -2590,6 +2590,8 @@
 
 var
     LandDirty: TDirtyTag;
+    Flakes: TGearPackArray;
+    FlakesCount: Longword;
     hasBorder: boolean;
     hasGirders: boolean;
     playHeight, playWidth, leftX, rightX, topY: LongInt;  // idea is that a template can specify height/width.  Or, a map, a height/width by the dimensions of the image.  If the map has pixels near top of image, it triggers border.
@@ -3120,6 +3122,8 @@
     SDLWindow:= nil;
     SDLGLContext:= nil;
 
+    FlakesCount:= 0;
+
     for i:= Low(ClansArray) to High(ClansArray) do
         begin
         ClansArray[i]:= nil;
--- a/project_files/hwc/rtl/system.c	Wed Aug 28 15:31:51 2024 +0200
+++ b/project_files/hwc/rtl/system.c	Wed Aug 28 15:34:49 2024 +0200
@@ -215,8 +215,8 @@
 
 string255 fpcrtl_floatToStr(double n) {
     string255 t;
-    sprintf(t.str, "%f", n);
-    t.len = strlen(t.str);
+    
+    t.len = sprintf(t.str, "%f", n);
 
     return t;
 }
@@ -352,9 +352,8 @@
 LongInt str_to_int(char *src)
 {
     int i;
-    int len = strlen(src);
     char *end;
-    for(i = 0; i < len; i++)
+    for(i = 0; src[i]; i++)
     {
         if(src[i] == '$'){
             // hex
@@ -387,51 +386,41 @@
 LongInt fpcrtl_random(LongInt l) {
     // random(0) is undefined in docs but effectively returns 0 in free pascal
     if (l == 0) {
-        printf("WARNING: random(0) called!\n");
+        //printf("WARNING: random(0) called!\n");
         return 0;
     }
     return (LongInt) (rand() / (double) RAND_MAX * l);
 }
 
 void __attribute__((overloadable)) fpcrtl_str__vars(float x, string255 *s) {
-    sprintf(s->str, "%f", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%f", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(double x, string255 *s) {
-    sprintf(s->str, "%f", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%f", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(uint8_t x, string255 *s) {
-    sprintf(s->str, "%u", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%u", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(int8_t x, string255 *s) {
-    sprintf(s->str, "%d", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%d", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(uint16_t x, string255 *s) {
-    sprintf(s->str, "%u", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%u", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(int16_t x, string255 *s) {
-    sprintf(s->str, "%d", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%d", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(uint32_t x, string255 *s) {
-    sprintf(s->str, "%u", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%u", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(int32_t x, string255 *s) {
-    sprintf(s->str, "%d", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%d", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(uint64_t x, string255 *s) {
-    sprintf(s->str, "%llu", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%llu", x);
 }
 void __attribute__((overloadable)) fpcrtl_str__vars(int64_t x, string255 *s) {
-    sprintf(s->str, "%lld", x);
-    s->len = strlen(s->str);
+    s->len = sprintf(s->str, "%lld", x);
 }
 
 /*
--- a/qmlfrontend/CMakeLists.txt	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/CMakeLists.txt	Wed Aug 28 15:34:49 2024 +0200
@@ -9,7 +9,7 @@
 set(CMAKE_AUTOMOC ON)
 set(CMAKE_AUTORCC ON)
 
-find_package(Qt5 COMPONENTS Core Quick REQUIRED)
+find_package(Qt6 COMPONENTS Core Quick REQUIRED)
 
 add_executable(${PROJECT_NAME} "main.cpp" "qml.qrc"
     "hwengine.cpp" "hwengine.h"
@@ -25,4 +25,4 @@
     "rooms_model.cpp" "rooms_model.h"
     )
 
-target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Quick)
+target_link_libraries(${PROJECT_NAME} Qt6::Core Qt6::Network Qt6::Quick)
--- a/qmlfrontend/Page1.qml	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/Page1.qml	Wed Aug 28 15:34:49 2024 +0200
@@ -1,68 +1,132 @@
-import QtQuick 2.7
+import QtQuick
 import Hedgewars.Engine 1.0
 
 Page1Form {
-    focus: true
+  property HWEngine hwEngine
+  property var keyBindings: ({
+      "long": {
+        [Qt.Key_Space]: Engine.Attack,
+        [Qt.Key_Up]: Engine.ArrowUp,
+        [Qt.Key_Right]: Engine.ArrowRight,
+        [Qt.Key_Down]: Engine.ArrowDown,
+        [Qt.Key_Left]: Engine.ArrowLeft,
+        [Qt.Key_Shift]: Engine.Precision
+      },
+      "simple": {
+        [Qt.Key_Tab]: Engine.SwitchHedgehog,
+        [Qt.Key_Enter]: Engine.LongJump,
+        [Qt.Key_Backspace]: Engine.HighJump,
+        [Qt.Key_Y]: Engine.Accept,
+        [Qt.Key_N]: Engine.Deny
+      }
+    })
+  property NetSession netSession
 
-    property HWEngine hwEngine
-    property NetSession netSession
-
-    Component {
-        id: hwEngineComponent
+  focus: true
 
-        HWEngine {
-            engineLibrary: "../rust/lib-hedgewars-engine/target/debug/libhedgewars_engine.so"
-            dataPath: "../share/hedgewars/Data"
-            previewAcceptor: PreviewAcceptor
-            onPreviewImageChanged: previewImage.source = "image://preview/image"
-            onPreviewIsRendering: previewImage.source = "qrc:/res/iconTime.png"
-        }
+  Component.onCompleted: {
+    hwEngine = hwEngineComponent.createObject();
+  }
+  Keys.onPressed: event => {
+    if (event.isAutoRepeat) {
+      return;
+    }
+    let action = keyBindings["simple"][event.key];
+    if (action !== undefined) {
+      gameView.engineInstance.simpleEvent(action);
+      event.accepted = true;
+      return;
     }
+    action = keyBindings["long"][event.key];
+    if (action !== undefined) {
+      gameView.engineInstance.longEvent(action, Engine.Set);
+      event.accepted = true;
+    }
+  }
+  Keys.onReleased: event => {
+    if (event.isAutoRepeat) {
+      return;
+    }
+    const action = keyBindings["long"][event.key];
+    if (action !== undefined) {
+      gameView.engineInstance.longEvent(action, Engine.Unset);
+      event.accepted = true;
+    }
+  }
+  netButton.onClicked: {
+    netSession = netSessionComponent.createObject();
+    netSession.open();
+  }
 
-    Component {
-        id: netSessionComponent
+  Component {
+    id: hwEngineComponent
 
-        NetSession {
-            nickname: "test0272"
-            url: "hwnet://gameserver.hedgewars.org:46632"
-        }
-    }
-
-    Component.onCompleted: {
-        hwEngine = hwEngineComponent.createObject()
-    }
+    HWEngine {
+      dataPath: "../share/hedgewars/Data"
+      engineLibrary: "../rust/lib-hedgewars-engine/target/debug/libhedgewars_engine.so"
+      previewAcceptor: PreviewAcceptor
 
-    tickButton {
-        onClicked: {
-            tickButton.visible = false
-            gameView.tick(100)
-        }
+      onPreviewImageChanged: previewImage.source = "image://preview/image"
+      onPreviewIsRendering: previewImage.source = "qrc:/res/iconTime.png"
+    }
+  }
+
+  Component {
+    id: netSessionComponent
+
+    NetSession {
+      nickname: "test0272"
+      url: "hwnet://gameserver.hedgewars.org:46632"
     }
-    gameButton {
-        visible: !gameView.engineInstance
-        onClicked: {
-            const engineInstance = hwEngine.runQuickGame()
-            gameView.engineInstance = engineInstance
-        }
+  }
+
+  tickButton {
+    onClicked: {
+      tickButton.visible = false;
+    }
+  }
+
+  Timer {
+    id: advancingTimer
+
+    interval: 100
+    repeat: true
+    running: !tickButton.visible
+
+    onTriggered: {
+      gameView.tick(100);
+      gameView.update();
     }
-    button1 {
-        visible: !gameView.engineInstance
-        onClicked: {
-            hwEngine.getPreview()
-        }
+  }
+
+  gameButton {
+    visible: !gameView.engineInstance
+
+    onClicked: {
+      const engineInstance = hwEngine.runQuickGame();
+      gameView.engineInstance = engineInstance;
     }
-    netButton.onClicked: {
-        netSession = netSessionComponent.createObject()
-        netSession.open()
-    }
+  }
+
+  button1 {
+    visible: !gameView.engineInstance
 
-    Keys.onPressed: {
-        if (event.key === Qt.Key_Enter)
-            gameView.engineInstance.longEvent(Engine.Attack, Engine.Set)
+    onClicked: {
+      hwEngine.getPreview();
     }
+  }
+
+  preview {
+    visible: !gameView.engineInstance
+  }
 
-    Keys.onReleased: {
-        if (event.key === Qt.Key_Enter)
-            gameView.engineInstance.longEvent(Engine.Attack, Engine.Unset)
+  gameMouseArea {
+    onPositionChanged: event => {
+      gameView.engineInstance.moveCamera(Qt.point(event.x - gameMouseArea.lastPoint.x, event.y - gameMouseArea.lastPoint.y));
+      gameMouseArea.lastPoint = Qt.point(event.x, event.y);
     }
+    onPressed: event => {
+      gameMouseArea.lastPoint = Qt.point(event.x, event.y);
+    }
+  }
 }
--- a/qmlfrontend/Page1Form.ui.qml	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/Page1Form.ui.qml	Wed Aug 28 15:34:49 2024 +0200
@@ -1,6 +1,6 @@
-import QtQuick 2.7
-import QtQuick.Controls 2.0
-import QtQuick.Layouts 1.3
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
 
 import Hedgewars.Engine 1.0
 
@@ -8,18 +8,19 @@
   id: element
   property alias button1: button1
   property alias previewImage: previewImage
+  property alias preview: preview
   property alias gameButton: gameButton
-  width: 1024
-  height: 800
   property alias netButton: netButton
   property alias tickButton: tickButton
   property alias gameView: gameView
+  property alias gameMouseArea: gameMouseArea
 
   ColumnLayout {
     anchors.fill: parent
 
     RowLayout {
       Layout.alignment: Qt.AlignHCenter
+      Layout.fillHeight: false
 
       Button {
         id: button1
@@ -38,6 +39,7 @@
     }
 
     Rectangle {
+      id: preview
       border.color: "orange"
       border.width: 5
       radius: 5
@@ -80,6 +82,13 @@
 
       Layout.fillWidth: true
       Layout.fillHeight: true
+
+      MouseArea {
+        id: gameMouseArea
+        anchors.fill: parent
+
+        property point lastPoint
+      }
     }
   }
 
--- a/qmlfrontend/engine_instance.cpp	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/engine_instance.cpp	Wed Aug 28 15:34:49 2024 +0200
@@ -88,17 +88,21 @@
 }
 
 void EngineInstance::simpleEvent(Engine::SimpleEventType event_type) {
-  simple_event(m_instance.get(), event_type);
+  simple_event(m_instance.get(),
+               static_cast<hwengine::SimpleEventType>(event_type));
 }
 
 void EngineInstance::longEvent(Engine::LongEventType event_type,
                                Engine::LongEventState state) {
-  long_event(m_instance.get(), event_type, state);
+  long_event(m_instance.get(), static_cast<hwengine::LongEventType>(event_type),
+             static_cast<hwengine::LongEventState>(state));
 }
 
 void EngineInstance::positionedEvent(Engine::PositionedEventType event_type,
                                      qint32 x, qint32 y) {
-  positioned_event(m_instance.get(), event_type, x, y);
+  positioned_event(m_instance.get(),
+                   static_cast<hwengine::PositionedEventType>(event_type), x,
+                   y);
 }
 
 void EngineInstance::renderFrame() { render_frame(m_instance.get()); }
--- a/qmlfrontend/engine_instance.h	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/engine_instance.h	Wed Aug 28 15:34:49 2024 +0200
@@ -15,7 +15,7 @@
  public:
   explicit EngineInstance(const QString& libraryPath,const QString& dataPath,
                           QObject* parent = nullptr);
-  ~EngineInstance();
+  ~EngineInstance() override;
 
   Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged)
 
--- a/qmlfrontend/engine_interface.h	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/engine_interface.h	Wed Aug 28 15:34:49 2024 +0200
@@ -42,25 +42,51 @@
 using long_event_t = decltype(hwengine::long_event);
 using positioned_event_t = decltype(hwengine::positioned_event);
 
+}  // extern "C"
+
+Q_NAMESPACE
+
+/*
 using SimpleEventType = hwengine::SimpleEventType;
 using LongEventType = hwengine::LongEventType;
 using LongEventState = hwengine::LongEventState;
 using PositionedEventType = hwengine::PositionedEventType;
+*/
 
-}  // extern "C"
+// NOTE: have to copy these to be able to register then in Qt meta object system
+enum class LongEventState {
+  Set,
+  Unset,
+};
 
-Q_NAMESPACE
+enum class LongEventType {
+  ArrowUp,
+  ArrowDown,
+  ArrowLeft,
+  ArrowRight,
+  Precision,
+  Attack,
+};
+
+enum class PositionedEventType {
+  CursorMove,
+  CursorClick,
+};
+
+enum class SimpleEventType {
+  SwitchHedgehog,
+  Timer,
+  LongJump,
+  HighJump,
+  Accept,
+  Deny,
+};
 
 Q_ENUM_NS(SimpleEventType)
 Q_ENUM_NS(LongEventType)
 Q_ENUM_NS(LongEventState)
 Q_ENUM_NS(PositionedEventType)
 
-};  // namespace
-
-Q_DECLARE_METATYPE(Engine::SimpleEventType)
-Q_DECLARE_METATYPE(Engine::LongEventType)
-Q_DECLARE_METATYPE(Engine::LongEventState)
-Q_DECLARE_METATYPE(Engine::PositionedEventType)
+};  // namespace Engine
 
 #endif  // ENGINE_H
--- a/qmlfrontend/game_view.cpp	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/game_view.cpp	Wed Aug 28 15:34:49 2024 +0200
@@ -1,114 +1,109 @@
 #include "game_view.h"
 
 #include <QtQuick/qquickwindow.h>
+
 #include <QCursor>
+#include <QOpenGLFramebufferObjectFormat>
+#include <QQuickOpenGLUtils>
 #include <QTimer>
 #include <QtGui/QOpenGLContext>
-#include <QtGui/QOpenGLShaderProgram>
+
+class GameViewRenderer : public QQuickFramebufferObject::Renderer {
+ public:
+  explicit GameViewRenderer() = default;
+
+  GameViewRenderer(const GameViewRenderer&) = delete;
+  GameViewRenderer(GameViewRenderer&&) = delete;
+  GameViewRenderer& operator=(const GameViewRenderer&) = delete;
+  GameViewRenderer& operator=(GameViewRenderer&&) = delete;
+
+  void render() override;
+  QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override;
+  void synchronize(QQuickFramebufferObject* fbo) override;
+
+  QPointer<GameView> m_gameView;
+  QPointer<QQuickWindow> m_window;
+  bool m_dirty{true};
+  QSizeF m_gameViewSize;
+};
+
+void GameViewRenderer::render() {
+  const auto engine = m_gameView->engineInstance();
+
+  if (!engine) {
+    return;
+  }
+
+  if (m_dirty) {
+    m_dirty = false;
+    engine->setOpenGLContext(QOpenGLContext::currentContext());
+  }
 
-GameView::GameView(QQuickItem* parent)
-    : QQuickItem(parent), m_delta(0), m_windowChanged(true) {
-  connect(this, &QQuickItem::windowChanged, this,
-          &GameView::handleWindowChanged);
+  engine->renderFrame();
+
+  QQuickOpenGLUtils::resetOpenGLState();
+}
+
+QOpenGLFramebufferObject* GameViewRenderer::createFramebufferObject(
+    const QSize& size) {
+  QOpenGLFramebufferObjectFormat format;
+  format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
+  format.setSamples(8);
+  auto fbo = new QOpenGLFramebufferObject(size, format);
+  return fbo;
+}
+
+void GameViewRenderer::synchronize(QQuickFramebufferObject* fbo) {
+  if (!m_gameView) {
+    m_gameView = qobject_cast<GameView*>(fbo);
+    m_window = fbo->window();
+  }
+
+  if (const auto currentSize = m_gameView->size();
+      currentSize != m_gameViewSize) {
+    m_gameViewSize = currentSize;
+    m_dirty = true;
+  }
+
+  m_gameView->executeActions();
+}
+
+GameView::GameView(QQuickItem* parent) : QQuickFramebufferObject(parent) {
+  setMirrorVertically(true);
 }
 
 void GameView::tick(quint32 delta) {
-  m_delta = delta;
-
-  if (window()) {
-    QTimer* timer = new QTimer(this);
-    connect(timer, &QTimer::timeout, window(), &QQuickWindow::update);
-    timer->start(100);
-
-    // window()->update();
-  }
+  addAction([delta](auto engine) { engine->advance(delta); });
 }
 
 EngineInstance* GameView::engineInstance() const { return m_engineInstance; }
 
-void GameView::handleWindowChanged(QQuickWindow* win) {
-  if (win) {
-    connect(win, &QQuickWindow::beforeSynchronizing, this, &GameView::sync,
-            Qt::DirectConnection);
-    connect(win, &QQuickWindow::sceneGraphInvalidated, this, &GameView::cleanup,
-            Qt::DirectConnection);
-
-    win->setClearBeforeRendering(false);
-
-    m_windowChanged = true;
-  }
+QQuickFramebufferObject::Renderer* GameView::createRenderer() const {
+  return new GameViewRenderer{};
 }
 
-void GameView::cleanup() { m_renderer.reset(); }
+void GameView::executeActions() {
+  if (!m_engineInstance) {
+    return;
+  }
+
+  for (const auto& action : m_actions) {
+    action(m_engineInstance);
+  }
+
+  m_actions.clear();
+}
 
 void GameView::setEngineInstance(EngineInstance* engineInstance) {
   if (m_engineInstance == engineInstance) {
     return;
   }
 
-  cleanup();
   m_engineInstance = engineInstance;
 
-  emit engineInstanceChanged(m_engineInstance);
+  Q_EMIT engineInstanceChanged(m_engineInstance);
 }
 
-void GameView::sync() {
-  if (!m_renderer && m_engineInstance) {
-    m_engineInstance->setOpenGLContext(window()->openglContext());
-    m_renderer.reset(new GameViewRenderer());
-    m_renderer->setEngineInstance(m_engineInstance);
-    connect(window(), &QQuickWindow::beforeRendering, m_renderer.data(),
-            &GameViewRenderer::paint, Qt::DirectConnection);
-  }
-
-  if (m_windowChanged || (m_viewportSize != size())) {
-    m_windowChanged = false;
-
-    if (m_engineInstance)
-      m_engineInstance->setOpenGLContext(window()->openglContext());
-
-    m_viewportSize = size().toSize();
-    m_centerPoint = QPoint(m_viewportSize.width(), m_viewportSize.height()) / 2;
-  }
-
-  if (m_engineInstance) {
-    const auto delta = mapFromGlobal(QCursor::pos()).toPoint() - m_centerPoint;
-
-    m_engineInstance->moveCamera(delta);
-
-    QCursor::setPos(window()->screen(), mapToGlobal(m_centerPoint).toPoint());
-  }
-
-  if (m_renderer) {
-    m_renderer->tick(m_delta);
-  }
+void GameView::addAction(std::function<void(EngineInstance*)>&& action) {
+  m_actions.append(std::move(action));
 }
-
-GameViewRenderer::GameViewRenderer()
-    : QObject(), m_delta(0), m_engineInstance(nullptr) {}
-
-GameViewRenderer::~GameViewRenderer() {}
-
-void GameViewRenderer::tick(quint32 delta) { m_delta = delta; }
-
-void GameViewRenderer::setEngineInstance(EngineInstance* engineInstance) {
-  m_engineInstance = engineInstance;
-}
-
-void GameViewRenderer::paint() {
-  if (m_delta == 0) {
-    return;
-  }
-
-  if (m_engineInstance) {
-    m_engineInstance->advance(m_delta);
-    m_engineInstance->renderFrame();
-  }
-
-  // m_window->resetOpenGLState();
-}
-
-void GameViewRenderer::onViewportSizeChanged(QQuickWindow* window) {
-  if (m_engineInstance)
-    m_engineInstance->setOpenGLContext(window->openglContext());
-}
--- a/qmlfrontend/game_view.h	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/game_view.h	Wed Aug 28 15:34:49 2024 +0200
@@ -1,34 +1,13 @@
 #ifndef GAMEVIEW_H
 #define GAMEVIEW_H
 
-#include <QQuickItem>
-
 #include <QPointer>
+#include <QQuickFramebufferObject>
 #include <QScopedPointer>
-#include <QtGui/QOpenGLFunctions>
-#include <QtGui/QOpenGLShaderProgram>
 
 #include "engine_instance.h"
 
-class GameViewRenderer : public QObject, protected QOpenGLFunctions {
-  Q_OBJECT
- public:
-  explicit GameViewRenderer();
-  ~GameViewRenderer() override;
-
-  void tick(quint32 delta);
-  void setEngineInstance(EngineInstance* engineInstance);
-
- public slots:
-  void paint();
-  void onViewportSizeChanged(QQuickWindow* window);
-
- private:
-  quint32 m_delta;
-  QPointer<EngineInstance> m_engineInstance;
-};
-
-class GameView : public QQuickItem {
+class GameView : public QQuickFramebufferObject {
   Q_OBJECT
 
   Q_PROPERTY(EngineInstance* engineInstance READ engineInstance WRITE
@@ -40,25 +19,22 @@
   Q_INVOKABLE void tick(quint32 delta);
 
   EngineInstance* engineInstance() const;
+  Renderer* createRenderer() const override;
+  void executeActions();
 
- signals:
+ Q_SIGNALS:
   void engineInstanceChanged(EngineInstance* engineInstance);
 
- public slots:
-  void sync();
-  void cleanup();
+ public Q_SLOTS:
   void setEngineInstance(EngineInstance* engineInstance);
 
- private slots:
-  void handleWindowChanged(QQuickWindow* win);
-
  private:
-  quint32 m_delta;
-  QScopedPointer<GameViewRenderer> m_renderer;
-  bool m_windowChanged;
   QPointer<EngineInstance> m_engineInstance;
   QSize m_viewportSize;
   QPoint m_centerPoint;
+  QList<std::function<void(EngineInstance*)>> m_actions;
+
+  void addAction(std::function<void(EngineInstance*)>&& action);
 };
 
 #endif  // GAMEVIEW_H
--- a/qmlfrontend/hwengine.h	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/hwengine.h	Wed Aug 28 15:34:49 2024 +0200
@@ -4,12 +4,11 @@
 #include <QList>
 #include <QObject>
 
-#include "engine_interface.h"
 #include "game_config.h"
+#include "preview_acceptor.h"
 
 class QQmlEngine;
 class EngineInstance;
-class PreviewAcceptor;
 
 class HWEngine : public QObject {
   Q_OBJECT
@@ -24,7 +23,7 @@
 
  public:
   explicit HWEngine(QObject* parent = nullptr);
-  ~HWEngine();
+  ~HWEngine() override;
 
   Q_INVOKABLE void getPreview();
   Q_INVOKABLE EngineInstance* runQuickGame();
--- a/qmlfrontend/main.cpp	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/main.cpp	Wed Aug 28 15:34:49 2024 +0200
@@ -18,7 +18,6 @@
 }
 
 int main(int argc, char* argv[]) {
-  QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
   QGuiApplication app(argc, argv);
 
   QQmlApplicationEngine engine;
@@ -34,14 +33,15 @@
   qmlRegisterType<HWEngine>("Hedgewars.Engine", 1, 0, "HWEngine");
   qmlRegisterType<GameView>("Hedgewars.Engine", 1, 0, "GameView");
   qmlRegisterType<NetSession>("Hedgewars.Engine", 1, 0, "NetSession");
-  qmlRegisterUncreatableType<EngineInstance>("Hedgewars.Engine", 1, 0,
-                                             "EngineInstance",
-                                             "Create by HWEngine run methods");
+  qmlRegisterUncreatableType<EngineInstance>(
+      "Hedgewars.Engine", 1, 0, "EngineInstance",
+      QStringLiteral("Create by HWEngine run methods"));
 
   qmlRegisterUncreatableMetaObject(Engine::staticMetaObject, "Hedgewars.Engine",
-                                   1, 0, "Engine", "Namespace: only enums");
+                                   1, 0, "Engine",
+                                   QStringLiteral("Namespace: only enums"));
 
-  engine.load(QUrl(QLatin1String("qrc:/main.qml")));
+  engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
   if (engine.rootObjects().isEmpty()) return -1;
 
   return app.exec();
--- a/qmlfrontend/main.qml	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/main.qml	Wed Aug 28 15:34:49 2024 +0200
@@ -1,6 +1,6 @@
-import QtQuick 2.7
-import QtQuick.Controls 2.0
-import QtQuick.Layouts 1.3
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
 
 ApplicationWindow {
   visible: true
--- a/qmlfrontend/players_model.cpp	Wed Aug 28 15:31:51 2024 +0200
+++ b/qmlfrontend/players_model.cpp	Wed Aug 28 15:34:49 2024 +0200
@@ -23,7 +23,7 @@
 QVariant PlayersListModel::data(const QModelIndex &index, int role) const {
   if (!index.isValid() || index.row() < 0 || index.row() >= rowCount() ||
       index.column() != 0)
-    return QVariant(QVariant::Invalid);
+    return QVariant{};
 
   return m_data.at(index.row()).value(role);
 }
@@ -270,8 +270,8 @@
 }
 
 void PlayersListModel::updateSortData(const QModelIndex &index) {
-  QString result =
-      QString("%1%2%3%4%5%6")
+  const auto result =
+      QStringLiteral("%1%2%3%4%5%6")
           // room admins go first, then server admins, then friends
           .arg(1 - index.data(RoomAdmin).toInt())
           .arg(1 - index.data(ServerAdmin).toInt())
@@ -320,11 +320,12 @@
   if (!txt.open(QIODevice::ReadOnly)) return;
 
   QTextStream stream(&txt);
-  stream.setCodec("UTF-8");
 
   while (!stream.atEnd()) {
     QString str = stream.readLine();
-    if (str.startsWith(";") || str.isEmpty()) continue;
+    if (str.startsWith(';') || str.isEmpty()) {
+      continue;
+    }
 
     set.insert(str.trimmed());
   }
@@ -351,7 +352,6 @@
   if (!txt.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
 
   QTextStream stream(&txt);
-  stream.setCodec("UTF-8");
 
   stream << "; this list is used by Hedgewars - do not edit it unless you know "
             "what you're doing!"
--- a/rust/hedgewars-checker/Cargo.toml	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-checker/Cargo.toml	Wed Aug 28 15:34:49 2024 +0200
@@ -5,14 +5,14 @@
 edition = "2018"
 
 [dependencies]
-rust-ini = "0.18"
-dirs = "4"
+rust-ini = "0.19"
+dirs = "5.0"
 argparse = "0.2"
 log = "0.4"
 stderrlog = "0.5"
 netbuf = "0.4"
 tempfile = "3.0"
-base64 = "0.13"
+base64 = "0.21"
 hedgewars-network-protocol = { path = "../hedgewars-network-protocol" }
 anyhow = "1.0"
 tokio = {version="1", features = ["full"]}
--- a/rust/hedgewars-checker/src/main.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-checker/src/main.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -1,5 +1,6 @@
-use anyhow::{bail, Result};
+use anyhow::{anyhow, bail, Result};
 use argparse::{ArgumentParser, Store};
+use base64::{engine::general_purpose, Engine};
 use hedgewars_network_protocol::{
     messages::HwProtocolMessage as ClientMessage, messages::HwServerMessage::*, parser,
 };
@@ -7,18 +8,19 @@
 use log::{debug, info, warn};
 use netbuf::Buf;
 use std::{io::Write, str::FromStr};
+use tokio::time::MissedTickBehavior;
 use tokio::{io, io::AsyncWriteExt, net::TcpStream, process::Command, sync::mpsc};
 
 async fn check(executable: &str, data_prefix: &str, buffer: &[String]) -> Result<Vec<String>> {
     let mut replay = tempfile::NamedTempFile::new()?;
 
-    for line in buffer.into_iter() {
-        replay.write(&base64::decode(line)?)?;
+    for line in buffer.iter() {
+        replay.write_all(&general_purpose::STANDARD.decode(line)?)?;
     }
 
     let temp_file_path = replay.path();
 
-    let mut home_dir = dirs::home_dir().unwrap();
+    let mut home_dir = dirs::home_dir().ok_or(anyhow!("Home path not detected"))?;
     home_dir.push(".hedgewars");
 
     debug!("Checking replay in {}", temp_file_path.to_string_lossy());
@@ -43,7 +45,7 @@
 
     let mut engine_lines = output
         .stderr
-        .split(|b| *b == '\n' as u8)
+        .split(|b| *b == b'\n')
         .skip_while(|l| *l != b"WINNERS" && *l != b"DRAW");
 
     // debug!("Engine lines: {:?}", &engine_lines);
@@ -83,7 +85,7 @@
 
     // println!("Engine lines: {:?}", &result);
 
-    if result.len() > 0 {
+    if !result.is_empty() {
         Ok(result)
     } else {
         bail!("no data from engine")
@@ -118,8 +120,16 @@
 
     let mut buf = Buf::new();
 
+    let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
+    interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
+
     loop {
         let r = tokio::select! {
+            _ = interval.tick() => {
+                // Send Ping
+                stream.write_all(ClientMessage::Ping.to_raw_protocol().as_bytes()).await?;
+                None
+            },
             _ = stream.readable() => None,
             r = results_receiver.recv() => r
         };
@@ -133,27 +143,27 @@
                     debug!("Check result: [{:?}]", result);
 
                     stream
-                        .write(
+                        .write_all(
                             ClientMessage::CheckedOk(result)
                                 .to_raw_protocol()
                                 .as_bytes(),
                         )
                         .await?;
                     stream
-                        .write(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
+                        .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
                         .await?;
                 }
                 Err(e) => {
                     info!("Check failed: {:?}", e);
                     stream
-                        .write(
+                        .write_all(
                             ClientMessage::CheckedFail("error".to_owned())
                                 .to_raw_protocol()
                                 .as_bytes(),
                         )
                         .await?;
                     stream
-                        .write(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
+                        .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
                         .await?;
                 }
             }
@@ -183,7 +193,7 @@
                 Connected(_, _) => {
                     info!("Connected");
                     stream
-                        .write(
+                        .write_all(
                             ClientMessage::Checker(
                                 protocol_number,
                                 username.to_owned(),
@@ -196,12 +206,15 @@
                 }
                 Ping => {
                     stream
-                        .write(ClientMessage::Pong.to_raw_protocol().as_bytes())
+                        .write_all(ClientMessage::Pong.to_raw_protocol().as_bytes())
                         .await?;
                 }
+                Pong => {
+                    // do nothing
+                }
                 LogonPassed => {
                     stream
-                        .write(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
+                        .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
                         .await?;
                 }
                 Replay(lines) => {
@@ -216,12 +229,12 @@
                     info!("Chat [{}]: {}", nick, msg);
                 }
                 RoomAdd(fields) => {
-                    let l = fields.into_iter();
-                    info!("Room added: {}", l.skip(1).next().unwrap());
+                    let mut l = fields.into_iter();
+                    info!("Room added: {}", l.nth(1).unwrap());
                 }
                 RoomUpdated(name, fields) => {
-                    let l = fields.into_iter();
-                    let new_name = l.skip(1).next().unwrap();
+                    let mut l = fields.into_iter();
+                    let new_name = l.nth(1).unwrap();
 
                     if name != new_name {
                         info!("Room renamed: {}", new_name);
@@ -245,7 +258,7 @@
 async fn get_protocol_number(executable: &str) -> Result<u16> {
     let output = Command::new(executable).arg("--protocol").output().await?;
 
-    Ok(u16::from_str(&String::from_utf8(output.stdout).unwrap().trim()).unwrap_or(55))
+    Ok(u16::from_str(String::from_utf8(output.stdout)?.trim()).unwrap_or(55))
 }
 
 #[tokio::main]
@@ -254,15 +267,18 @@
         .verbosity(3)
         .timestamp(stderrlog::Timestamp::Second)
         .module(module_path!())
-        .init()
-        .unwrap();
+        .init()?;
 
-    let mut frontend_settings = dirs::home_dir().unwrap();
+    let mut frontend_settings = dirs::home_dir().ok_or(anyhow!("Home path not detected"))?;
     frontend_settings.push(".hedgewars/settings.ini");
 
     let i = Ini::load_from_file(frontend_settings.to_str().unwrap()).unwrap();
-    let username = i.get_from(Some("net"), "nick").unwrap();
-    let password = i.get_from(Some("net"), "passwordhash").unwrap();
+    let username = i
+        .get_from(Some("net"), "nick")
+        .ok_or(anyhow!("Nickname not found in frontend config"))?;
+    let password = i
+        .get_from(Some("net"), "passwordhash")
+        .ok_or(anyhow!("Password not found in frontend config"))?;
 
     let mut exe = "/usr/local/bin/hwengine".to_string();
     let mut prefix = "/usr/local/share/hedgewars/Data".to_string();
@@ -279,7 +295,7 @@
     info!("Executable: {}", exe);
     info!("Data dir: {}", prefix);
 
-    let protocol_number = get_protocol_number(&exe.as_str()).await.unwrap_or_default();
+    let protocol_number = get_protocol_number(exe.as_str()).await?;
 
     info!("Using protocol number {}", protocol_number);
 
@@ -288,8 +304,8 @@
 
     let (network_result, checker_result) = tokio::join!(
         connect_and_run(
-            &username,
-            &password,
+            username,
+            password,
             protocol_number,
             replay_sender,
             results_receiver
--- a/rust/hedgewars-engine-messages/Cargo.toml	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-engine-messages/Cargo.toml	Wed Aug 28 15:34:49 2024 +0200
@@ -5,6 +5,6 @@
 edition = "2018"
 
 [dependencies]
-nom = "4.1"
+nom = "7.1"
 byteorder = "1.2"
 queues = "1.1"
--- a/rust/hedgewars-engine-messages/src/messages.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-engine-messages/src/messages.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -128,7 +128,7 @@
 
 #[derive(Debug, PartialEq, Clone)]
 pub enum EngineMessage {
-    Unknown,
+    Unknown(Vec<u8>),
     Empty,
     Synced(SyncedEngineMessage, u32),
     Unsynced(UnsyncedEngineMessage),
@@ -172,7 +172,7 @@
             NextTurn => em![b'N'],
             Switch => em![b'S'],
             Timer(t) => vec![b'0' + t],
-            Slot(s) => vec![b'~' , *s],
+            Slot(s) => vec![b'~', *s],
             SetWeapon(s) => vec![b'~', *s],
             Put(x, y) => {
                 let mut v = vec![b'p'];
@@ -180,14 +180,14 @@
                 v.write_i24::<BigEndian>(*y).unwrap();
 
                 v
-            },
+            }
             CursorMove(x, y) => {
                 let mut v = vec![b'P'];
                 v.write_i24::<BigEndian>(*x).unwrap();
                 v.write_i24::<BigEndian>(*y).unwrap();
 
                 v
-            },
+            }
             HighJump => em![b'J'],
             LongJump => em![b'j'],
             Skip => em![b','],
@@ -242,7 +242,7 @@
     fn to_unwrapped(&self) -> Vec<u8> {
         use self::EngineMessage::*;
         match self {
-            Unknown => unreachable!("you're not supposed to construct such messages"),
+            Unknown(_) => unreachable!("you're not supposed to construct such messages"),
             Empty => unreachable!("you're not supposed to construct such messages"),
             Synced(SyncedEngineMessage::TimeWrap, _) => vec![b'#', 0xff, 0xff],
             Synced(msg, timestamp) => {
--- a/rust/hedgewars-engine-messages/src/parser.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-engine-messages/src/parser.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -1,126 +1,169 @@
+use std::str;
+
+use nom::branch::alt;
+use nom::bytes::streaming::*;
+use nom::combinator::*;
+use nom::error::{ErrorKind, ParseError};
+use nom::multi::*;
+use nom::number::streaming::*;
+use nom::sequence::{pair, preceded, terminated, tuple};
+use nom::{Err, IResult, Parser};
+
 use crate::messages::{
     ConfigEngineMessage::*, EngineMessage::*, KeystrokeAction::*, SyncedEngineMessage::*,
     UnorderedEngineMessage::*, *,
 };
-use nom::{Err::Error, *};
-use std::str;
 
-macro_rules! eof_slice (
-  ($i:expr,) => (
-    {
-      if ($i).input_len() == 0 {
-        Ok(($i, $i))
-      } else {
-        Err(Error(error_position!($i, ErrorKind::Eof::<u32>)))
-      }
+fn eof_slice<I>(i: I) -> IResult<I, I>
+where
+    I: nom::InputLength + Clone,
+{
+    if i.input_len() == 0 {
+        Ok((i.clone(), i))
+    } else {
+        Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)))
     }
-  );
-);
+}
+fn unrecognized_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
+    map(rest, |i: &[u8]| Unknown(i.to_owned()))(input)
+}
 
-named!(unrecognized_message<&[u8], EngineMessage>,
-    do_parse!(rest >> (Unknown))
-);
+fn string_tail(input: &[u8]) -> IResult<&[u8], String> {
+    map_res(rest, str::from_utf8)(input).map(|(i, s)| (i, s.to_owned()))
+}
 
-named!(string_tail<&[u8], String>, map!(map_res!(rest, str::from_utf8), String::from));
-
-named!(length_without_timestamp<&[u8], usize>,
-    map_opt!(rest_len, |l| if l > 2 { Some(l - 2) } else { None } )
-);
+fn length_without_timestamp(input: &[u8]) -> IResult<&[u8], usize> {
+    map_opt(rest_len, |l| if l > 2 { Some(l - 2) } else { None })(input)
+}
 
-named!(synced_message<&[u8], SyncedEngineMessage>, alt!(
-        do_parse!(tag!("L") >> (Left(Press)))
-      | do_parse!(tag!("l") >> ( Left(Release) ))
-      | do_parse!(tag!("R") >> ( Right(Press) ))
-      | do_parse!(tag!("r") >> ( Right(Release) ))
-      | do_parse!(tag!("U") >> ( Up(Press) ))
-      | do_parse!(tag!("u") >> ( Up(Release) ))
-      | do_parse!(tag!("D") >> ( Down(Press) ))
-      | do_parse!(tag!("d") >> ( Down(Release) ))
-      | do_parse!(tag!("Z") >> ( Precise(Press) ))
-      | do_parse!(tag!("z") >> ( Precise(Release) ))
-      | do_parse!(tag!("A") >> ( Attack(Press) ))
-      | do_parse!(tag!("a") >> ( Attack(Release) ))
-      | do_parse!(tag!("N") >> ( NextTurn ))
-      | do_parse!(tag!("j") >> ( LongJump ))
-      | do_parse!(tag!("J") >> ( HighJump ))
-      | do_parse!(tag!("S") >> ( Switch ))
-      | do_parse!(tag!(",") >> ( Skip ))
-      | do_parse!(tag!("1") >> ( Timer(1) ))
-      | do_parse!(tag!("2") >> ( Timer(2) ))
-      | do_parse!(tag!("3") >> ( Timer(3) ))
-      | do_parse!(tag!("4") >> ( Timer(4) ))
-      | do_parse!(tag!("5") >> ( Timer(5) ))
-      | do_parse!(tag!("p") >> x: be_i24 >> y: be_i24 >> ( Put(x, y) ))
-      | do_parse!(tag!("P") >> x: be_i24 >> y: be_i24 >> ( CursorMove(x, y) ))
-      | do_parse!(tag!("f") >> s: string_tail >> ( SyncedEngineMessage::TeamControlLost(s) ))
-      | do_parse!(tag!("g") >> s: string_tail >> ( SyncedEngineMessage::TeamControlGained(s) ))
-      | do_parse!(tag!("t") >> t: be_u8 >> ( Taunt(t) ))
-      | do_parse!(tag!("w") >> w: be_u8 >> ( SetWeapon(w) ))
-      | do_parse!(tag!("~") >> s: be_u8 >> ( Slot(s) ))
-      | do_parse!(tag!("+") >> ( Heartbeat ))
-));
+fn synced_message(input: &[u8]) -> IResult<&[u8], SyncedEngineMessage> {
+    alt((
+        alt((
+            map(tag(b"L"), |_| Left(Press)),
+            map(tag(b"l"), |_| Left(Release)),
+            map(tag(b"R"), |_| Right(Press)),
+            map(tag(b"r"), |_| Right(Release)),
+            map(tag(b"U"), |_| Up(Press)),
+            map(tag(b"u"), |_| Up(Release)),
+            map(tag(b"D"), |_| Down(Press)),
+            map(tag(b"d"), |_| Down(Release)),
+            map(tag(b"Z"), |_| Precise(Press)),
+            map(tag(b"z"), |_| Precise(Release)),
+            map(tag(b"A"), |_| Attack(Press)),
+            map(tag(b"a"), |_| Attack(Release)),
+            map(tag(b"N"), |_| NextTurn),
+            map(tag(b"j"), |_| LongJump),
+            map(tag(b"J"), |_| HighJump),
+            map(tag(b"S"), |_| Switch),
+        )),
+        alt((
+            map(tag(b","), |_| Skip),
+            map(tag(b"1"), |_| Timer(1)),
+            map(tag(b"2"), |_| Timer(2)),
+            map(tag(b"3"), |_| Timer(3)),
+            map(tag(b"4"), |_| Timer(4)),
+            map(tag(b"5"), |_| Timer(5)),
+            map(tuple((tag(b"p"), be_i24, be_i24)), |(_, x, y)| Put(x, y)),
+            map(tuple((tag(b"P"), be_i24, be_i24)), |(_, x, y)| {
+                CursorMove(x, y)
+            }),
+            map(preceded(tag(b"f"), string_tail), TeamControlLost),
+            map(preceded(tag(b"g"), string_tail), TeamControlGained),
+            map(preceded(tag(b"t"), be_u8), Taunt),
+            map(preceded(tag(b"w"), be_u8), SetWeapon),
+            map(preceded(tag(b"~"), be_u8), Slot),
+            map(tag(b"+"), |_| Heartbeat),
+        )),
+    ))(input)
+}
 
-named!(unsynced_message<&[u8], UnsyncedEngineMessage>, alt!(
-        do_parse!(tag!("F") >> s: string_tail >> ( UnsyncedEngineMessage::TeamControlLost(s) ))
-      | do_parse!(tag!("G") >> s: string_tail >> ( UnsyncedEngineMessage::TeamControlGained(s) ))
-      | do_parse!(tag!("h") >> s: string_tail >> ( UnsyncedEngineMessage::HogSay(s) ))
-      | do_parse!(tag!("s") >> s: string_tail >> ( UnsyncedEngineMessage::ChatMessage(s)) )
-      | do_parse!(tag!("b") >> s: string_tail >> ( UnsyncedEngineMessage::TeamMessage(s)) ) // TODO: wtf is the format
-));
+fn unsynced_message(input: &[u8]) -> IResult<&[u8], UnsyncedEngineMessage> {
+    alt((
+        map(
+            preceded(tag(b"F"), string_tail),
+            UnsyncedEngineMessage::TeamControlLost,
+        ),
+        map(
+            preceded(tag(b"G"), string_tail),
+            UnsyncedEngineMessage::TeamControlGained,
+        ),
+        map(
+            preceded(tag(b"h"), string_tail),
+            UnsyncedEngineMessage::HogSay,
+        ),
+        map(
+            preceded(tag(b"s"), string_tail),
+            UnsyncedEngineMessage::ChatMessage,
+        ),
+        map(
+            preceded(tag(b"b"), string_tail),
+            UnsyncedEngineMessage::TeamMessage,
+        ),
+    ))(input)
+}
 
-named!(unordered_message<&[u8], UnorderedEngineMessage>, alt!(
-      do_parse!(tag!("?") >> ( Ping ))
-    | do_parse!(tag!("!") >> ( Pong ))
-    | do_parse!(tag!("E") >> s: string_tail >> ( UnorderedEngineMessage::Error(s)) )
-    | do_parse!(tag!("W") >> s: string_tail >> ( Warning(s)) )
-    | do_parse!(tag!("M") >> s: string_tail >> ( GameSetupChecksum(s)) )
-    | do_parse!(tag!("o") >> ( StopSyncing ))
-    | do_parse!(tag!("I") >> ( PauseToggled ))
-));
-
-named!(config_message<&[u8], ConfigEngineMessage>, alt!(
-    do_parse!(tag!("C") >> (ConfigRequest))
-    | do_parse!(tag!("eseed ") >> s: string_tail >> ( SetSeed(s)) )
-    | do_parse!(tag!("e$feature_size ") >> s: string_tail >> ( SetFeatureSize(s.parse::<u8>().unwrap())) )
-));
-
-named!(timestamped_message<&[u8], (SyncedEngineMessage, u16)>,
-    do_parse!(msg: length_value!(length_without_timestamp, terminated!(synced_message, eof_slice!()))
-        >> timestamp: be_u16
-        >> ((msg, timestamp))
-    )
-);
+fn unordered_message(input: &[u8]) -> IResult<&[u8], UnorderedEngineMessage> {
+    alt((
+        map(tag(b"?"), |_| Ping),
+        map(tag(b"!"), |_| Pong),
+        map(preceded(tag(b"E"), string_tail), Error),
+        map(preceded(tag(b"W"), string_tail), Warning),
+        map(preceded(tag(b"M"), string_tail), GameSetupChecksum),
+        map(tag(b"o"), |_| StopSyncing),
+        map(tag(b"I"), |_| PauseToggled),
+    ))(input)
+}
 
-named!(unwrapped_message<&[u8], EngineMessage>,
-    alt!(
-        map!(timestamped_message, |(m, t)| Synced(m, t as u32))
-        | do_parse!(tag!("#") >> (Synced(TimeWrap, 65535)))
-        | map!(unordered_message, |m| Unordered(m))
-        | map!(unsynced_message, |m| Unsynced(m))
-        | map!(config_message, |m| Config(m))
-        | unrecognized_message
-));
+fn config_message(input: &[u8]) -> IResult<&[u8], ConfigEngineMessage> {
+    alt((
+        map(tag(b"C"), |_| ConfigRequest),
+        map(preceded(tag(b"eseed "), string_tail), SetSeed),
+        map(preceded(tag(b"e$feature_size "), string_tail), |s| {
+            SetFeatureSize(s.parse().unwrap_or_default())
+        }),
+    ))(input)
+}
 
-named!(length_specifier<&[u8], u16>, alt!(
-    verify!(map!(take!(1), |a : &[u8]| a[0] as u16), |l| l < 64)
-    | map!(take!(2), |a| (a[0] as u16 - 64) * 256 + a[1] as u16 + 64)
-    )
-);
-
-named!(empty_message<&[u8], EngineMessage>,
-    do_parse!(tag!("\0") >> (Empty))
-);
+fn timestamped_message(input: &[u8]) -> IResult<&[u8], (SyncedEngineMessage, u16)> {
+    terminated(pair(synced_message, be_u16), eof_slice)(input)
+}
+fn unwrapped_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
+    alt((
+        map(timestamped_message, |(m, t)| {
+            EngineMessage::Synced(m, t as u32)
+        }),
+        map(tag(b"#"), |_| Synced(TimeWrap, 65535u32)),
+        map(unordered_message, Unordered),
+        map(unsynced_message, Unsynced),
+        map(config_message, Config),
+        unrecognized_message,
+    ))(input)
+}
 
-named!(non_empty_message<&[u8], EngineMessage>,
-    length_value!(length_specifier, terminated!(unwrapped_message, eof_slice!())));
+fn length_specifier(input: &[u8]) -> IResult<&[u8], u16> {
+    alt((
+        verify(map(take(1usize), |a: &[u8]| a[0] as u16), |&l| l < 64),
+        map(take(2usize), |a: &[u8]| {
+            (a[0] as u16 - 64) * 256 + a[1] as u16 + 64
+        }),
+    ))(input)
+}
 
-named!(message<&[u8], EngineMessage>, alt!(
-      empty_message
-    | non_empty_message
-    )
-);
+fn empty_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
+    map(tag(b"\0"), |_| Empty)(input)
+}
+
+fn non_empty_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
+    map_parser(length_data(length_specifier), unwrapped_message)(input)
+}
 
-named!(pub extract_messages<&[u8], Vec<EngineMessage> >, many0!(complete!(message)));
+fn message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
+    alt((empty_message, non_empty_message))(input)
+}
+
+pub fn extract_messages(input: &[u8]) -> IResult<&[u8], Vec<EngineMessage>> {
+    many0(complete(message))(input)
+}
 
 pub fn extract_message(buf: &[u8]) -> Option<(usize, EngineMessage)> {
     let parse_result = message(buf);
@@ -178,10 +221,13 @@
     #[test]
     fn parse_incorrect_messages() {
         assert_eq!(message(b"\x00"), Ok((&b""[..], Empty)));
-        assert_eq!(message(b"\x01\x00"), Ok((&b""[..], Unknown)));
+        assert_eq!(message(b"\x01\x00"), Ok((&b""[..], Unknown(vec![0]))));
 
         // garbage after correct message
-        assert_eq!(message(b"\x04La\x01\x02"), Ok((&b""[..], Unknown)));
+        assert_eq!(
+            message(b"\x04La\x01\x02"),
+            Ok((&b""[..], Unknown(vec![76, 97, 1, 2])))
+        );
     }
 
     #[test]
@@ -194,6 +240,9 @@
         assert_eq!(string_tail(b"abc"), Ok((&b""[..], String::from("abc"))));
 
         assert_eq!(extract_message(b"\x02#"), None);
+
+        assert_eq!(synced_message(b"L"), Ok((&b""[..], Left(Press))));
+
         assert_eq!(
             extract_message(b"\x01#"),
             Some((2, Synced(TimeWrap, 65535)))
--- a/rust/hedgewars-network-protocol/src/parser.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-network-protocol/src/parser.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -8,13 +8,13 @@
  */
 use nom::{
     branch::alt,
-    bytes::complete::{tag, tag_no_case, take_until, take_while},
-    character::complete::{newline, not_line_ending},
+    bytes::streaming::{tag, tag_no_case, take_until, take_while},
+    character::streaming::{newline, not_line_ending},
     combinator::{map, peek},
     error::{ErrorKind, ParseError},
     multi::separated_list0,
     sequence::{delimited, pair, preceded, terminated, tuple},
-    Err, IResult,
+    Err, IResult, Parser,
 };
 
 use std::{
@@ -127,7 +127,7 @@
     ))(input)
 }
 
-fn opt_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option<String>> {
+fn opt_arg(input: &[u8]) -> HwResult<Option<String>> {
     alt((
         map(peek(end_of_message), |_| None),
         map(preceded(tag("\n"), a_line), Some),
@@ -138,7 +138,7 @@
     preceded(tag(" "), take_while(|c| c == b' '))(input)
 }
 
-fn opt_space_arg<'a>(input: &'a [u8]) -> HwResult<'a, Option<String>> {
+fn opt_space_arg(input: &[u8]) -> HwResult<Option<String>> {
     alt((
         map(peek(end_of_message), |_| None),
         map(preceded(spaces, a_line), Some),
@@ -184,10 +184,10 @@
 }
 
 fn no_arg_message(input: &[u8]) -> HwResult<HwProtocolMessage> {
-    fn message<'a>(
-        name: &'a str,
+    fn message(
+        name: &str,
         msg: HwProtocolMessage,
-    ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> {
+    ) -> impl Fn(&[u8]) -> HwResult<HwProtocolMessage> + '_ {
         move |i| map(tag(name), |_| msg.clone())(i)
     }
 
@@ -207,14 +207,14 @@
 }
 
 fn single_arg_message(input: &[u8]) -> HwResult<HwProtocolMessage> {
-    fn message<'a, T, F, G>(
+    fn message<'a, T: 'a, F, G>(
         name: &'a str,
         parser: F,
         constructor: G,
-    ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwProtocolMessage>
+    ) -> impl FnMut(&'a [u8]) -> HwResult<HwProtocolMessage> + '_
     where
-        F: Fn(&[u8]) -> HwResult<T>,
-        G: Fn(T) -> HwProtocolMessage,
+        F: Parser<&'a [u8], T, HwProtocolError> + 'a,
+        G: FnMut(T) -> HwProtocolMessage + 'a,
     {
         map(preceded(tag(name), parser), constructor)
     }
@@ -239,10 +239,10 @@
 }
 
 fn cmd_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> {
-    fn cmd_no_arg<'a>(
-        name: &'a str,
+    fn cmd_no_arg(
+        name: &str,
         msg: HwProtocolMessage,
-    ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> {
+    ) -> impl Fn(&[u8]) -> HwResult<HwProtocolMessage> + '_ {
         move |i| map(tag_no_case(name), |_| msg.clone())(i)
     }
 
@@ -319,14 +319,14 @@
 }
 
 fn config_message<'a>(input: &'a [u8]) -> HwResult<'a, HwProtocolMessage> {
-    fn cfg_single_arg<'a, T, F, G>(
+    fn cfg_single_arg<'a, T: 'a, F, G>(
         name: &'a str,
         parser: F,
         constructor: G,
-    ) -> impl FnMut(&'a [u8]) -> HwResult<'a, GameCfg>
+    ) -> impl FnMut(&'a [u8]) -> HwResult<GameCfg> + '_
     where
-        F: Fn(&[u8]) -> HwResult<T>,
-        G: Fn(T) -> GameCfg,
+        F: Parser<&'a [u8], T, HwProtocolError> + 'a,
+        G: Fn(T) -> GameCfg + 'a,
     {
         map(preceded(pair(tag(name), newline), parser), constructor)
     }
@@ -521,14 +521,14 @@
 pub fn server_message(input: &[u8]) -> HwResult<HwServerMessage> {
     use HwServerMessage::*;
 
-    fn single_arg_message<'a, T, F, G>(
+    fn single_arg_message<'a, T: 'a, F, G>(
         name: &'a str,
         parser: F,
         constructor: G,
-    ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwServerMessage>
+    ) -> impl FnMut(&'a [u8]) -> HwResult<HwServerMessage> + '_
     where
-        F: Fn(&[u8]) -> HwResult<T>,
-        G: Fn(T) -> HwServerMessage,
+        F: Parser<&'a [u8], T, HwProtocolError> + 'a,
+        G: Fn(T) -> HwServerMessage + 'a,
     {
         map(
             preceded(terminated(tag(name), newline), parser),
@@ -539,9 +539,9 @@
     fn list_message<'a, G>(
         name: &'a str,
         constructor: G,
-    ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwServerMessage>
+    ) -> impl FnMut(&'a [u8]) -> HwResult<HwServerMessage> + '_
     where
-        G: Fn(Vec<String>) -> HwServerMessage,
+        G: Fn(Vec<String>) -> HwServerMessage + 'a,
     {
         map(
             preceded(
@@ -558,9 +558,9 @@
     fn string_and_list_message<'a, G>(
         name: &'a str,
         constructor: G,
-    ) -> impl FnMut(&'a [u8]) -> HwResult<'a, HwServerMessage>
+    ) -> impl FnMut(&'a [u8]) -> HwResult<HwServerMessage> + '_
     where
-        G: Fn(String, Vec<String>) -> HwServerMessage,
+        G: Fn(String, Vec<String>) -> HwServerMessage + 'a,
     {
         preceded(
             pair(tag(name), newline),
@@ -577,10 +577,10 @@
         )
     }
 
-    fn message<'a>(
-        name: &'a str,
+    fn message(
+        name: &str,
         msg: HwServerMessage,
-    ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwServerMessage> {
+    ) -> impl Fn(&[u8]) -> HwResult<HwServerMessage> + '_ {
         move |i| map(tag(name), |_| msg.clone())(i)
     }
 
--- a/rust/hedgewars-network-protocol/src/tests/parser.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-network-protocol/src/tests/parser.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -1,12 +1,22 @@
 use crate::{
     parser::HwProtocolError,
-    parser::{message, server_message},
+    parser::{malformed_message, message, server_message},
     types::GameCfg,
 };
 
 #[test]
 fn parse_test() {
     use crate::messages::HwProtocolMessage::*;
+    use nom::Err::Incomplete;
+
+    assert!(matches!(
+        dbg!(message(b"CHAT\nWhat the")),
+        Err(Incomplete(_))
+    ));
+    assert!(matches!(
+        dbg!(malformed_message(b"CHAT\nWhat the \xF0\x9F\xA6\x94\n\nBYE")),
+        Ok((b"BYE", _))
+    ));
 
     assert_eq!(message(b"PING\n\n"), Ok((&b""[..], Ping)));
     assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame)));
--- a/rust/hedgewars-server/Cargo.toml	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/Cargo.toml	Wed Aug 28 15:34:49 2024 +0200
@@ -25,7 +25,7 @@
 serde_derive = "1.0"
 sha1 = { version = "0.10.0", optional = true }
 slab = "0.4"
-tokio = { version = "1.16", features = ["full"]}
+tokio = { version = "1.36", features = ["full"]}
 tokio-native-tls = { version = "0.3", optional = true }
 
 hedgewars-network-protocol = { path = "../hedgewars-network-protocol" }
--- a/rust/hedgewars-server/src/core/anteroom.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/core/anteroom.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -32,7 +32,7 @@
 
 impl BanCollection {
     fn new() -> Self {
-        todo!("add nick bans");
+        //todo!("add nick bans");
         Self {
             ban_ips: vec![],
             ban_timeouts: vec![],
--- a/rust/hedgewars-server/src/core/client.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/core/client.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -30,7 +30,7 @@
 
 impl HwClient {
     pub fn new(id: ClientId, protocol_number: u16, nick: String) -> HwClient {
-        todo!("add quiet flag");
+        //todo!("add quiet flag");
         HwClient {
             id,
             nick,
--- a/rust/hedgewars-server/src/core/room.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/core/room.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -14,18 +14,25 @@
 pub const MAX_TEAMS_IN_ROOM: u8 = 8;
 pub const MAX_HEDGEHOGS_IN_ROOM: u8 = MAX_TEAMS_IN_ROOM * MAX_HEDGEHOGS_PER_TEAM;
 
+#[derive(Clone, Debug)]
+pub struct OwnedTeam {
+    pub owner_id: ClientId,
+    pub owner_nick: String,
+    pub info: TeamInfo,
+}
+
 fn client_teams_impl(
-    teams: &[(ClientId, TeamInfo)],
-    client_id: ClientId,
+    teams: &[OwnedTeam],
+    owner_id: ClientId,
 ) -> impl Iterator<Item = &TeamInfo> + Clone {
     teams
         .iter()
-        .filter(move |(id, _)| *id == client_id)
-        .map(|(_, t)| t)
+        .filter(move |team| team.owner_id == owner_id)
+        .map(|team| &team.info)
 }
 
 pub struct GameInfo {
-    pub original_teams: Vec<(ClientId, TeamInfo)>,
+    pub original_teams: Vec<OwnedTeam>,
     pub left_teams: Vec<String>,
     pub msg_log: Vec<String>,
     pub sync_msg: Option<String>,
@@ -34,7 +41,7 @@
 }
 
 impl GameInfo {
-    fn new(teams: Vec<(ClientId, TeamInfo)>, config: RoomConfig) -> GameInfo {
+    fn new(teams: Vec<OwnedTeam>, config: RoomConfig) -> GameInfo {
         GameInfo {
             left_teams: Vec::new(),
             msg_log: Vec::new(),
@@ -45,8 +52,18 @@
         }
     }
 
-    pub fn client_teams(&self, client_id: ClientId) -> impl Iterator<Item = &TeamInfo> + Clone {
-        client_teams_impl(&self.original_teams, client_id)
+    pub fn client_teams(&self, owner_id: ClientId) -> impl Iterator<Item = &TeamInfo> + Clone {
+        client_teams_impl(&self.original_teams, owner_id)
+    }
+
+    pub fn client_teams_by_nick<'a>(
+        &'a self,
+        owner_nick: &'a str,
+    ) -> impl Iterator<Item = &TeamInfo> + Clone + 'a {
+        self.original_teams
+            .iter()
+            .filter(move |team| team.owner_nick == owner_nick)
+            .map(|team| &team.info)
     }
 
     pub fn mark_left_teams<'a, I>(&mut self, team_names: I)
@@ -95,7 +112,7 @@
     pub default_hedgehog_number: u8,
     pub max_teams: u8,
     pub ready_players_number: u8,
-    pub teams: Vec<(ClientId, TeamInfo)>,
+    pub teams: Vec<OwnedTeam>,
     config: RoomConfig,
     pub voting: Option<Voting>,
     pub saves: HashMap<String, RoomSave>,
@@ -125,7 +142,10 @@
     }
 
     pub fn hedgehogs_number(&self) -> u8 {
-        self.teams.iter().map(|(_, t)| t.hedgehogs_number).sum()
+        self.teams
+            .iter()
+            .map(|team| team.info.hedgehogs_number)
+            .sum()
     }
 
     pub fn addable_hedgehogs(&self) -> u8 {
@@ -134,7 +154,7 @@
 
     pub fn add_team(
         &mut self,
-        owner_id: ClientId,
+        owner: &HwClient,
         mut team: TeamInfo,
         preserve_color: bool,
     ) -> &TeamInfo {
@@ -142,24 +162,32 @@
             team.color = iter::repeat(())
                 .enumerate()
                 .map(|(i, _)| i as u8)
-                .take(u8::max_value() as usize + 1)
-                .find(|i| self.teams.iter().all(|(_, t)| t.color != *i))
+                .take(u8::MAX as usize + 1)
+                .find(|i| self.teams.iter().all(|team| team.info.color != *i))
                 .unwrap_or(0u8)
         };
         team.hedgehogs_number = if self.teams.is_empty() {
             self.default_hedgehog_number
         } else {
             self.teams[0]
-                .1
+                .info
                 .hedgehogs_number
                 .min(self.addable_hedgehogs())
         };
-        self.teams.push((owner_id, team));
-        &self.teams.last().unwrap().1
+        self.teams.push(OwnedTeam {
+            owner_id: owner.id,
+            owner_nick: owner.nick.clone(),
+            info: team,
+        });
+        &self.teams.last().unwrap().info
     }
 
     pub fn remove_team(&mut self, team_name: &str) {
-        if let Some(index) = self.teams.iter().position(|(_, t)| t.name == team_name) {
+        if let Some(index) = self
+            .teams
+            .iter()
+            .position(|team| team.info.name == team_name)
+        {
             self.teams.remove(index);
         }
     }
@@ -169,9 +197,9 @@
         let teams = &mut self.teams;
 
         if teams.len() as u8 * n <= MAX_HEDGEHOGS_IN_ROOM {
-            for (_, team) in teams.iter_mut() {
-                team.hedgehogs_number = n;
-                names.push(team.name.clone())
+            for team in teams.iter_mut() {
+                team.info.hedgehogs_number = n;
+                names.push(team.info.name.clone())
             }
             self.default_hedgehog_number = n;
         }
@@ -190,8 +218,8 @@
     {
         self.teams
             .iter_mut()
-            .find(|(_, t)| f(t))
-            .map(|(id, t)| (*id, t))
+            .find(|team| f(&team.info))
+            .map(|team| (team.owner_id, &mut team.info))
     }
 
     pub fn find_team<F>(&self, f: F) -> Option<&TeamInfo>
@@ -200,18 +228,18 @@
     {
         self.teams
             .iter()
-            .find_map(|(_, t)| Some(t).filter(|t| f(&t)))
+            .find_map(|team| Some(&team.info).filter(|t| f(&t)))
     }
 
-    pub fn client_teams(&self, client_id: ClientId) -> impl Iterator<Item = &TeamInfo> {
-        client_teams_impl(&self.teams, client_id)
+    pub fn client_teams(&self, owner_id: ClientId) -> impl Iterator<Item = &TeamInfo> {
+        client_teams_impl(&self.teams, owner_id)
     }
 
     pub fn client_team_indices(&self, client_id: ClientId) -> Vec<u8> {
         self.teams
             .iter()
             .enumerate()
-            .filter(move |(_, (id, _))| *id == client_id)
+            .filter(move |(_, team)| team.owner_id == client_id)
             .map(|(i, _)| i as u8)
             .collect()
     }
@@ -219,15 +247,15 @@
     pub fn clan_team_owners(&self, color: u8) -> impl Iterator<Item = ClientId> + '_ {
         self.teams
             .iter()
-            .filter(move |(_, t)| t.color == color)
-            .map(|(id, _)| *id)
+            .filter(move |team| team.info.color == color)
+            .map(|team| team.owner_id)
     }
 
     pub fn find_team_owner(&self, team_name: &str) -> Option<(ClientId, &str)> {
         self.teams
             .iter()
-            .find(|(_, t)| t.name == team_name)
-            .map(|(id, t)| (*id, &t.name[..]))
+            .find(|team| team.info.name == team_name)
+            .map(|team| (team.owner_id, &team.info.name[..]))
     }
 
     pub fn find_team_color(&self, owner_id: ClientId) -> Option<u8> {
@@ -235,8 +263,8 @@
     }
 
     pub fn has_multiple_clans(&self) -> bool {
-        self.teams.iter().min_by_key(|(_, t)| t.color)
-            != self.teams.iter().max_by_key(|(_, t)| t.color)
+        let colors = self.teams.iter().map(|team| team.info.color);
+        colors.clone().min() != colors.max()
     }
 
     pub fn set_config(&mut self, cfg: GameCfg) {
--- a/rust/hedgewars-server/src/core/server.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/core/server.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -12,6 +12,7 @@
 
 use bitflags::*;
 use log::*;
+use rand::{self, seq::SliceRandom, thread_rng, Rng};
 use slab::Slab;
 use std::{borrow::BorrowMut, cmp::min, collections::HashSet, iter, mem::replace};
 
@@ -109,9 +110,18 @@
 }
 
 #[derive(Debug)]
+pub enum VoteEffect {
+    Kicked(ClientId, LeaveRoomResult),
+    Map(String),
+    Pause,
+    NewSeed(GameCfg),
+    HedgehogsPerTeam(u8, Vec<String>),
+}
+
+#[derive(Debug)]
 pub enum VoteResult {
     Submitted,
-    Succeeded(VoteType),
+    Succeeded(VoteEffect),
     Failed,
 }
 
@@ -189,7 +199,7 @@
 
 impl HwServer {
     pub fn new(clients_limit: usize, rooms_limit: usize) -> Self {
-        todo!("add reconnection IDs");
+        //todo!("add reconnection IDs");
         let rooms = Slab::with_capacity(rooms_limit);
         let clients = IndexSlab::with_capacity(clients_limit);
         let checkers = IndexSlab::new();
@@ -260,7 +270,7 @@
     }
 
     #[inline]
-    pub fn get_room_control(&mut self, client_id: ClientId) -> Option<HwRoomControl> {
+    pub fn get_room_control(&mut self, client_id: ClientId) -> HwRoomOrServer {
         HwRoomControl::new(self, client_id)
     }
 
@@ -335,7 +345,15 @@
         client_id: ClientId,
         room_id: RoomId,
         room_password: Option<&str>,
-    ) -> Result<(&HwClient, &HwRoom, impl Iterator<Item = &HwClient> + Clone), JoinRoomError> {
+    ) -> Result<
+        (
+            &HwClient,
+            Option<&HwClient>,
+            &HwRoom,
+            impl Iterator<Item = &HwClient> + Clone,
+        ),
+        JoinRoomError,
+    > {
         use JoinRoomError::*;
         let room = &mut self.rooms[room_id];
         let client = &mut self.clients[client_id];
@@ -349,15 +367,16 @@
             Err(WrongPassword)
         } else if room.is_join_restricted() {
             Err(Restricted)
-        } else if room.is_registration_required() {
+        } else if room.is_registration_required() && !client.is_registered() {
             Err(RegistrationRequired)
-        } else if room.players_number == u8::max_value() {
+        } else if room.players_number == u8::MAX {
             Err(Full)
         } else {
             move_to_room(client, room);
             let room_id = room.id;
             Ok((
                 &self.clients[client_id],
+                room.master_id.map(|id| &self.clients[id]),
                 &self.rooms[room_id],
                 self.iter_clients()
                     .filter(move |c| c.room_id == Some(room_id)),
@@ -371,7 +390,15 @@
         client_id: ClientId,
         room_name: &str,
         room_password: Option<&str>,
-    ) -> Result<(&HwClient, &HwRoom, impl Iterator<Item = &HwClient> + Clone), JoinRoomError> {
+    ) -> Result<
+        (
+            &HwClient,
+            Option<&HwClient>,
+            &HwRoom,
+            impl Iterator<Item = &HwClient> + Clone,
+        ),
+        JoinRoomError,
+    > {
         use JoinRoomError::*;
         let room = self.rooms.iter().find(|(_, r)| r.name == room_name);
         if let Some((_, room)) = room {
@@ -537,6 +564,21 @@
     }
 }
 
+pub enum HwRoomOrServer<'a> {
+    Room(HwRoomControl<'a>),
+    Server(&'a mut HwServer),
+}
+
+impl<'a> HwRoomOrServer<'a> {
+    #[inline]
+    pub fn into_room(self) -> Option<HwRoomControl<'a>> {
+        match self {
+            HwRoomOrServer::Room(control) => Some(control),
+            HwRoomOrServer::Server(_) => None,
+        }
+    }
+}
+
 pub struct HwRoomControl<'a> {
     server: &'a mut HwServer,
     client_id: ClientId,
@@ -546,23 +588,16 @@
 
 impl<'a> HwRoomControl<'a> {
     #[inline]
-    pub fn new(server: &'a mut HwServer, client_id: ClientId) -> Option<Self> {
+    pub fn new(server: &'a mut HwServer, client_id: ClientId) -> HwRoomOrServer {
         if let Some(room_id) = server.clients[client_id].room_id {
-            Some(Self {
+            HwRoomOrServer::Room(Self {
                 server,
                 client_id,
                 room_id,
                 is_room_removed: false,
             })
         } else {
-            None
-        }
-    }
-
-    #[inline]
-    pub fn cleanup_room(self) {
-        if self.is_room_removed {
-            self.server.rooms.remove(self.room_id);
+            HwRoomOrServer::Server(server)
         }
     }
 
@@ -604,13 +639,11 @@
         )
     }
 
-    pub fn change_client<'b: 'a>(self, client_id: ClientId) -> Option<HwRoomControl<'a>> {
-        let room_id = self.room_id;
-        HwRoomControl::new(self.server, client_id).filter(|c| c.room_id == room_id)
-    }
-
-    pub fn leave_room(&mut self) -> LeaveRoomResult {
-        let (client, room) = self.get_mut();
+    fn remove_from_room(&mut self, client_id: ClientId) -> LeaveRoomResult {
+        let (client, room) = self
+            .server
+            .client_and_room_mut(client_id)
+            .expect("Caller should have ensured the client is in this room");
         room.players_number -= 1;
         client.room_id = None;
 
@@ -620,7 +653,9 @@
         let was_in_game = client.is_in_game();
         let mut removed_teams = vec![];
 
-        if is_empty && !is_fixed {
+        self.is_room_removed = is_empty && !is_fixed;
+
+        if !self.is_room_removed {
             if client.is_ready() && room.ready_players_number > 0 {
                 room.ready_players_number -= 1;
             }
@@ -650,33 +685,29 @@
         client.set_is_ready(false);
         client.set_is_in_game(false);
 
-        if !is_fixed {
-            if room.players_number == 0 {
-                self.is_room_removed = true
-            } else if room.master_id == None {
-                let protocol_number = room.protocol_number;
-                let new_master_id = self.server.room_client_ids(self.room_id).next();
+        if !self.is_room_removed && room.master_id == None {
+            let protocol_number = room.protocol_number;
+            let new_master_id = self.server.room_client_ids(self.room_id).next();
+
+            if let Some(new_master_id) = new_master_id {
+                let room = self.room_mut();
+                room.master_id = Some(new_master_id);
+                let new_master = &mut self.server.clients[new_master_id];
+                new_master.set_is_master(true);
 
-                if let Some(new_master_id) = new_master_id {
-                    let room = self.room_mut();
-                    room.master_id = Some(new_master_id);
-                    let new_master = &mut self.server.clients[new_master_id];
-                    new_master.set_is_master(true);
+                if protocol_number < 42 {
+                    let nick = new_master.nick.clone();
+                    self.room_mut().name = nick;
+                }
 
-                    if protocol_number < 42 {
-                        let nick = new_master.nick.clone();
-                        self.room_mut().name = nick;
-                    }
-
-                    let room = self.room_mut();
-                    room.set_join_restriction(false);
-                    room.set_team_add_restriction(false);
-                    room.set_unregistered_players_restriction(true);
-                }
+                let room = self.room_mut();
+                room.set_join_restriction(false);
+                room.set_team_add_restriction(false);
+                room.set_unregistered_players_restriction(false);
             }
         }
 
-        if is_empty && !is_fixed {
+        if self.is_room_removed {
             LeaveRoomResult::RoomRemoved
         } else {
             LeaveRoomResult::RoomRemains {
@@ -689,6 +720,10 @@
         }
     }
 
+    pub fn leave_room(&mut self) -> LeaveRoomResult {
+        self.remove_from_room(self.client_id)
+    }
+
     pub fn change_master(
         &mut self,
         new_master_nick: String,
@@ -743,6 +778,40 @@
         }
     }
 
+    fn apply_vote(&mut self, kind: VoteType) -> Option<VoteEffect> {
+        match kind {
+            VoteType::Kick(nick) => {
+                if let Some(kicked_id) = self
+                    .server
+                    .find_client(&nick)
+                    .filter(|c| c.room_id == Some(self.room_id))
+                    .map(|c| c.id)
+                {
+                    let leave_result = self.remove_from_room(kicked_id);
+                    Some(VoteEffect::Kicked(kicked_id, leave_result))
+                } else {
+                    None
+                }
+            }
+            VoteType::Map(None) => None,
+            VoteType::Map(Some(name)) => self
+                .load_config(&name)
+                .map(|s| VoteEffect::Map(s.to_string())),
+            VoteType::Pause => Some(VoteEffect::Pause).filter(|_| self.toggle_pause()),
+            VoteType::NewSeed => {
+                let seed = thread_rng().gen_range(0..1_000_000_000).to_string();
+                let cfg = GameCfg::Seed(seed);
+                //todo!("Protocol backwards compatibility");
+                self.room_mut().set_config(cfg.clone());
+                Some(VoteEffect::NewSeed(cfg))
+            }
+            VoteType::HedgehogsPerTeam(number) => {
+                let nicks = self.set_hedgehogs_number(number);
+                Some(VoteEffect::HedgehogsPerTeam(number, nicks))
+            }
+        }
+    }
+
     pub fn vote(&mut self, vote: Vote) -> Result<VoteResult, VoteError> {
         use self::{VoteError::*, VoteResult::*};
         let client_id = self.client_id;
@@ -753,9 +822,14 @@
                 let pro = i.clone().filter(|(_, v)| *v).count();
                 let contra = i.filter(|(_, v)| !*v).count();
                 let success_quota = voting.voters.len() / 2 + 1;
+
                 if vote.is_forced && vote.is_pro || pro >= success_quota {
                     let voting = self.room_mut().voting.take().unwrap();
-                    Ok(Succeeded(voting.kind))
+                    if let Some(effect) = self.apply_vote(voting.kind) {
+                        Ok(Succeeded(effect))
+                    } else {
+                        Ok(Failed)
+                    }
                 } else if vote.is_forced && !vote.is_pro
                     || contra > voting.voters.len() - success_quota
                 {
@@ -884,7 +958,7 @@
             Err(Restricted)
         } else {
             info.owner = client.nick.clone();
-            let team = room.add_team(client.id, *info, client.protocol_number < 42);
+            let team = room.add_team(&client, *info, client.protocol_number < 42);
             client.teams_in_game += 1;
             client.clan = Some(team.color);
             Ok(team)
@@ -896,7 +970,7 @@
         let (client, room) = self.get_mut();
         match room.find_team_owner(team_name) {
             None => Err(NoTeam),
-            Some((id, _)) if id != client.id => Err(RemoveTeamError::TeamNotOwned),
+            Some((id, _)) if id != client.id => Err(TeamNotOwned),
             Some(_) => {
                 client.teams_in_game -= 1;
                 client.clan = room.find_team_color(client.id);
@@ -941,7 +1015,6 @@
                 }
                 cfg => cfg,
             };
-
             room.set_config(cfg);
             Ok(())
         }
@@ -1078,6 +1151,15 @@
     }
 }
 
+impl<'a> Drop for HwRoomControl<'a> {
+    #[inline]
+    fn drop(&mut self) {
+        if self.is_room_removed {
+            self.server.rooms.remove(self.room_id);
+        }
+    }
+}
+
 fn allocate_room(rooms: &mut Slab<HwRoom>) -> &mut HwRoom {
     let entry = rooms.vacant_entry();
     let room = HwRoom::new(entry.key());
@@ -1119,14 +1201,22 @@
     client.room_id = Some(room.id);
     client.set_is_in_game(room.game_info.is_some());
 
-    if let Some(ref mut info) = room.game_info {
-        let teams = info.client_teams(client.id);
-        client.teams_in_game = teams.clone().count() as u8;
-        client.clan = teams.clone().next().map(|t| t.color);
-        let team_names: Vec<_> = teams.map(|t| t.name.clone()).collect();
+    #[cfg(feature = "official-server")]
+    let can_rejoin = client.is_registered();
+
+    #[cfg(not(feature = "official-server"))]
+    let can_rejoin = true;
 
-        if !team_names.is_empty() {
-            info.left_teams.retain(|name| !team_names.contains(&name));
+    if can_rejoin {
+        if let Some(ref mut info) = room.game_info {
+            let teams = info.client_teams_by_nick(&client.nick);
+            client.teams_in_game = teams.clone().count() as u8;
+            client.clan = teams.clone().next().map(|t| t.color);
+            let team_names: Vec<_> = teams.map(|t| t.name.clone()).collect();
+
+            if !team_names.is_empty() {
+                info.left_teams.retain(|name| !team_names.contains(&name));
+            }
         }
     }
 }
--- a/rust/hedgewars-server/src/handlers.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/handlers.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -15,7 +15,7 @@
     core::{
         anteroom::HwAnteroom,
         room::RoomSave,
-        server::HwServer,
+        server::{HwRoomOrServer, HwServer},
         types::{ClientId, Replay, RoomId},
     },
     utils,
@@ -372,8 +372,10 @@
                         }
                     }
                     _ => match state.server.get_room_control(client_id) {
-                        None => inlobby::handle(&mut state.server, client_id, response, message),
-                        Some(control) => inroom::handle(control, response, message),
+                        HwRoomOrServer::Room(control) => inroom::handle(control, response, message),
+                        HwRoomOrServer::Server(server) => {
+                            inlobby::handle(server, client_id, response, message)
+                        }
                     },
                 }
             }
--- a/rust/hedgewars-server/src/handlers/common.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/handlers/common.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -2,6 +2,7 @@
     actions::{Destination, DestinationGroup},
     Response,
 };
+use crate::core::server::HwRoomOrServer;
 use crate::handlers::actions::ToPendingMessage;
 use crate::{
     core::{
@@ -9,7 +10,7 @@
         room::HwRoom,
         server::{
             EndGameResult, HwRoomControl, HwServer, JoinRoomError, LeaveRoomResult, StartGameError,
-            VoteError, VoteResult,
+            VoteEffect, VoteError, VoteResult,
         },
         types::{ClientId, RoomId},
     },
@@ -106,6 +107,7 @@
 
 pub fn get_room_join_data<'a, I: Iterator<Item = &'a HwClient> + Clone>(
     client: &HwClient,
+    master: Option<&HwClient>,
     room: &HwRoom,
     room_clients: I,
     response: &mut Response,
@@ -139,7 +141,15 @@
             .but_self(),
     );
     response.add(ClientFlags(add_flags(&[Flags::InRoom]), vec![nick.clone()]).send_all());
-    let nicks = room_clients.clone().map(|c| c.nick.clone()).collect();
+    let nicks = once(nick.clone())
+        .chain(
+            room_clients
+                .clone()
+                .filter(|c| c.id != client.id)
+                .map(|c| c.nick.clone()),
+        )
+        .collect();
+
     response.add(RoomJoined(nicks).send_self());
 
     let mut flag_selectors = [
@@ -211,25 +221,31 @@
             response.add(ForwardEngineMessage(vec![to_engine_msg(once(b'I'))]).send_self());
         }
 
-        for (_, original_team) in &info.original_teams {
-            if let Some(team) = room.find_team(|team| team.name == original_team.name) {
-                if team != original_team {
-                    response.add(TeamRemove(original_team.name.clone()).send_self());
+        for original_team in &info.original_teams {
+            if let Some(team) = room.find_team(|team| team.name == original_team.info.name) {
+                if *team != original_team.info {
+                    response.add(TeamRemove(original_team.info.name.clone()).send_self());
                     response.add(TeamAdd(team.to_protocol()).send_self());
                 }
             } else {
-                response.add(TeamRemove(original_team.name.clone()).send_self());
+                response.add(TeamRemove(original_team.info.name.clone()).send_self());
             }
         }
 
-        for (_, team) in &room.teams {
-            if !info.original_teams.iter().any(|(_, t)| t.name == team.name) {
-                response.add(TeamAdd(team.to_protocol()).send_self());
+        for team in &room.teams {
+            if !info
+                .original_teams
+                .iter()
+                .any(|original_team| original_team.info.name == team.info.name)
+            {
+                response.add(TeamAdd(team.info.to_protocol()).send_self());
             }
         }
 
         get_room_config_impl(room.config(), Destination::ToSelf, response);
     }
+
+    get_room_update(None, room, master, response);
 }
 
 pub fn get_room_join_error(error: JoinRoomError, response: &mut Response) {
@@ -327,8 +343,10 @@
 
             get_remove_teams_data(room.id, was_in_game, removed_teams, response);
 
+            let master = new_master.or(Some(client.id)).map(|id| server.client(id));
+
             response.add(
-                RoomUpdated(room.name.clone(), room.info(Some(&client)))
+                RoomUpdated(room.name.clone(), room.info(master))
                     .send_all()
                     .with_protocol(room.protocol_number),
             );
@@ -341,10 +359,14 @@
     let client = server.client(client_id);
     let nick = client.nick.clone();
 
-    if let Some(mut room_control) = server.get_room_control(client_id) {
-        let room_id = room_control.room().id;
-        let result = room_control.leave_room();
-        get_room_leave_result(server, server.room(room_id), &msg, result, response);
+    match server.get_room_control(client_id) {
+        HwRoomOrServer::Room(mut control) => {
+            let room_id = control.room().id;
+            let result = control.leave_room();
+            let server = control.server();
+            get_room_leave_result(server, server.room(room_id), &msg, result, response);
+        }
+        _ => (),
     }
 
     server.remove_client(client_id);
@@ -355,12 +377,12 @@
 }
 
 pub fn get_room_update(
-    room_name: Option<String>,
+    old_name: Option<String>,
     room: &HwRoom,
     master: Option<&HwClient>,
     response: &mut Response,
 ) {
-    let update_msg = RoomUpdated(room_name.unwrap_or(room.name.clone()), room.info(master));
+    let update_msg = RoomUpdated(old_name.unwrap_or(room.name.clone()), room.info(master));
     response.add(update_msg.send_all().with_protocol(room.protocol_number));
 }
 
@@ -403,7 +425,11 @@
         None => &room.teams,
     };
 
-    get_teams(current_teams.iter().map(|(_, t)| t), destination, response);
+    get_teams(
+        current_teams.iter().map(|team| &team.info),
+        destination,
+        response,
+    );
 }
 
 pub fn get_room_flags(
@@ -510,76 +536,58 @@
 }
 
 pub fn handle_vote(
-    mut room_control: HwRoomControl,
+    room_control: HwRoomControl,
     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);
+    get_vote_data(room_control.room().id, &result, response);
 
-    if let Ok(VoteResult::Succeeded(kind)) = result {
-        match kind {
-            VoteType::Kick(nick) => {
-                if let Some(kicked_client) = room_control.server().find_client(&nick) {
-                    let kicked_id = kicked_client.id;
-                    if let Some(mut room_control) = room_control.change_client(kicked_id) {
-                        response.add(Kicked.send(kicked_id));
-                        let result = room_control.leave_room();
-                        super::common::get_room_leave_result(
-                            room_control.server(),
-                            room_control.room(),
-                            "kicked",
-                            result,
-                            response,
-                        );
-                    }
-                }
+    if let Ok(VoteResult::Succeeded(effect)) = result {
+        match effect {
+            VoteEffect::Kicked(kicked_id, leave_result) => {
+                response.add(Kicked.send(kicked_id));
+                get_room_leave_result(
+                    room_control.server(),
+                    room_control.room(),
+                    "kicked",
+                    leave_result,
+                    response,
+                );
             }
-            VoteType::Map(None) => (),
-            VoteType::Map(Some(name)) => {
-                if let Some(location) = room_control.load_config(&name) {
-                    let msg = server_chat(location.to_string());
-                    let room = room_control.room();
-                    response.add(msg.send_all().in_room(room.id));
+            VoteEffect::Map(location) => {
+                let msg = server_chat(location.to_string());
+                let room = room_control.room();
+                response.add(msg.send_all().in_room(room.id));
 
-                    let room_master = room.master_id.map(|id| room_control.server().client(id));
+                let room_master = room.master_id.map(|id| room_control.server().client(id));
 
-                    super::common::get_room_update(None, room, room_master, response);
+                get_room_update(None, room, room_master, response);
 
-                    let room_destination = Destination::ToAll {
-                        group: DestinationGroup::Room(room.id),
-                        skip_self: false,
-                    };
-                    super::common::get_active_room_config(room, room_destination, response);
-                }
+                let room_destination = Destination::ToAll {
+                    group: DestinationGroup::Room(room.id),
+                    skip_self: false,
+                };
+                get_active_room_config(room, room_destination, response);
             }
-            VoteType::Pause => {
-                if room_control.toggle_pause() {
-                    response.add(
-                        server_chat("Pause toggled.".to_string())
-                            .send_all()
-                            .in_room(room_id),
-                    );
-                    response.add(
-                        ForwardEngineMessage(vec![to_engine_msg(once(b'I'))])
-                            .send_all()
-                            .in_room(room_id),
-                    );
-                }
+            VoteEffect::Pause => {
+                response.add(
+                    server_chat("Pause toggled.".to_string())
+                        .send_all()
+                        .in_room(room_id),
+                );
+                response.add(
+                    ForwardEngineMessage(vec![to_engine_msg(once(b'I'))])
+                        .send_all()
+                        .in_room(room_id),
+                );
             }
-            VoteType::NewSeed => {
-                let seed = thread_rng().gen_range(0..1_000_000_000).to_string();
-                let cfg = GameCfg::Seed(seed);
+            VoteEffect::NewSeed(cfg) => {
                 response.add(cfg.to_server_msg().send_all().in_room(room_id));
-                room_control
-                    .set_config(cfg)
-                    .expect("Apparently, you cannot just set room config");
             }
-            VoteType::HedgehogsPerTeam(number) => {
-                let nicks = room_control.set_hedgehogs_number(number);
+            VoteEffect::HedgehogsPerTeam(number, team_names) => {
                 response.extend(
-                    nicks
+                    team_names
                         .into_iter()
                         .map(|n| HedgehogsNumber(n, number).send_all().in_room(room_id)),
                 );
--- a/rust/hedgewars-server/src/handlers/inanteroom.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/handlers/inanteroom.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -60,6 +60,7 @@
     response: &mut super::Response,
     message: HwProtocolMessage,
 ) -> LoginResult {
+    //todo!("Handle parsing of empty nicks")
     match message {
         HwProtocolMessage::Quit(_) => {
             response.add(Bye("User quit".to_string()).send_self());
--- a/rust/hedgewars-server/src/handlers/inlobby.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/handlers/inlobby.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -26,7 +26,10 @@
 ) {
     use hedgewars_network_protocol::messages::HwProtocolMessage::*;
 
-    todo!("add kick/ban handlers");
+    //todo!("add kick/ban handlers");
+    //todo!("add command for forwarding lobby chat into rooms
+    //todo!("report player account age")
+    //todo!("port listing rooms for incompatible protocols"))
 
     match message {
         CreateRoom(name, password) => match server.create_room(client_id, name, password) {
@@ -49,7 +52,7 @@
             }
         },
         Chat(msg) => {
-            todo!("add client quiet flag");
+            //todo!("add client quiet flag");
             response.add(
                 ChatMsg {
                     nick: server.client(client_id).nick.clone(),
@@ -63,8 +66,8 @@
         JoinRoom(name, password) => {
             match server.join_room_by_name(client_id, &name, password.as_deref()) {
                 Err(error) => super::common::get_room_join_error(error, response),
-                Ok((client, room, room_clients)) => {
-                    super::common::get_room_join_data(client, room, room_clients, response)
+                Ok((client, master, room, room_clients)) => {
+                    super::common::get_room_join_data(client, master, room, room_clients, response)
                 }
             }
         }
@@ -73,8 +76,14 @@
                 if let Some(room_id) = client.room_id {
                     match server.join_room(client_id, room_id, None) {
                         Err(error) => super::common::get_room_join_error(error, response),
-                        Ok((client, room, room_clients)) => {
-                            super::common::get_room_join_data(client, room, room_clients, response)
+                        Ok((client, master, room, room_clients)) => {
+                            super::common::get_room_join_data(
+                                client,
+                                master,
+                                room,
+                                room_clients,
+                                response,
+                            )
                         }
                     }
                 } else {
--- a/rust/hedgewars-server/src/handlers/inroom.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/handlers/inroom.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -124,7 +124,6 @@
                 result,
                 response,
             );
-            room_control.cleanup_room();
         }
         Chat(msg) => {
             response.add(
@@ -334,6 +333,7 @@
             }
         }
         CallVote(None) => {
+            //todo!("implement ghost points")
             response.add(server_chat("Available callvote commands: kick <nickname>, map <name>, pause, newseed, hedgehogs <number>".to_string())
                 .send_self());
         }
@@ -495,6 +495,6 @@
                 response.warn("The player is not in your room.")
             }
         },
-        _ => warn!("Unimplemented!"),
+        message => warn!("Unimplemented: {:?}", message),
     }
 }
--- a/rust/hedgewars-server/src/main.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/main.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -27,7 +27,7 @@
     let args: Vec<String> = env::args().collect();
     let mut opts = Options::new();
 
-    todo!("Add options for cert paths");
+    //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/protocol.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/protocol.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -87,6 +87,10 @@
                 Err(nom::Err::Incomplete(_)) => {}
                 Err(nom::Err::Failure(e) | nom::Err::Error(e)) => {
                     debug!("Invalid message: {:?}", e);
+                    trace!(
+                        "Buffer content: {:?}",
+                        String::from_utf8_lossy(&self.buffer[..])
+                    );
                     self.recover();
                 }
             }
@@ -101,7 +105,13 @@
         use ProtocolError::*;
 
         loop {
-            if !self.buffer.has_remaining() {
+            let remaining = self.buffer.capacity() - self.buffer.len();
+            if remaining < 1024 {
+                self.buffer.reserve(2048 - remaining);
+            }
+
+            if !self.buffer.has_remaining() || self.is_recovering {
+                //todo!("ensure the buffer doesn't grow indefinitely")
                 match timeout(self.read_timeout, stream.read_buf(&mut self.buffer)).await {
                     Err(_) => return Err(Timeout),
                     Ok(Err(e)) => return Err(Network(Box::new(e))),
--- a/rust/hedgewars-server/src/server/demo.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/server/demo.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -135,7 +135,7 @@
         let mut teams = vec![];
         let mut hog_index = 7usize;
 
-        todo!("read messages from file");
+        //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	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/server/io.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -24,7 +24,7 @@
         let (core_tx, io_rx) = mpsc::channel();
         let (io_tx, core_rx) = mpsc::channel();
 
-        todo!("convert into an IO task");
+        //todo!("convert into an IO task");
 
         /*let mut db = Database::new("localhost");
 
--- a/rust/hedgewars-server/src/server/network.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hedgewars-server/src/server/network.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -30,11 +30,13 @@
 
 const PING_TIMEOUT: Duration = Duration::from_secs(15);
 
+#[derive(Debug)]
 enum ClientUpdateData {
     Message(HwProtocolMessage),
     Error(String),
 }
 
+#[derive(Debug)]
 struct ClientUpdate {
     client_id: ClientId,
     data: ClientUpdateData,
@@ -180,12 +182,14 @@
                 client_message = Self::read(&mut self.stream, &mut self.decoder) => {
                      match client_message {
                         Ok(message) => {
+                            //todo!("add flood stats");
                             if !sender.send(Message(message)).await {
                                 break;
                             }
                         }
                         Err(e) => {
-                            todo!("send cmdline errors");
+                            //todo!("send cmdline errors");
+                            //todo!("more graceful shutdown to prevent errors from explicitly closed clients")
                             sender.send(Error(format!("{}", e))).await;
                             if matches!(e, ProtocolError::Timeout) {
                                 Self::write(&mut self.stream, Bytes::from(HwServerMessage::Bye("Ping timeout".to_string()).to_raw_protocol())).await;
@@ -211,12 +215,12 @@
     tls: TlsListener,
     server_state: ServerState,
     clients: Slab<Sender<Bytes>>,
+    update_tx: Sender<ClientUpdate>,
+    update_rx: Receiver<ClientUpdate>,
 }
 
 impl NetworkLayer {
     pub async fn run(&mut self) {
-        let (update_tx, mut update_rx) = channel(128);
-
         async fn accept_plain_branch(
             layer: &mut NetworkLayer,
             value: (TcpStream, SocketAddr),
@@ -274,20 +278,20 @@
             }
         }
 
-        todo!("add the DB task");
-        todo!("add certfile watcher task");
+        //todo!("add the DB task");
+        //todo!("add certfile watcher task");
         loop {
             #[cfg(not(feature = "tls-connections"))]
             tokio::select! {
-                Ok(value) = self.listener.accept() => accept_plain_branch(self, value, update_tx.clone()).await,
-                client_message = update_rx.recv(), if !self.clients.is_empty() => client_message_branch(self, client_message).await
+                Ok(value) = self.listener.accept() => accept_plain_branch(self, value, self.update_tx.clone()).await,
+                client_message = self.update_rx.recv(), if !self.clients.is_empty() => client_message_branch(self, client_message).await
             }
 
             #[cfg(feature = "tls-connections")]
             tokio::select! {
-                Ok(value) = self.listener.accept() => accept_plain_branch(self, value, 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
+                Ok(value) = self.listener.accept() => accept_plain_branch(self, value, self.update_tx.clone()).await,
+                Ok(value) = self.tls.listener.accept() => accept_tls_branch(self, value, self.update_tx.clone()).await,
+                client_message = self.update_rx.recv(), if !self.clients.is_empty() => client_message_branch(self, client_message).await
             }
         }
     }
@@ -341,19 +345,24 @@
             return;
         }
 
+        for client_id in response.extract_removed_clients() {
+            if self.clients.contains(client_id) {
+                self.clients.remove(client_id);
+                if self.clients.is_empty() {
+                    let (update_tx, update_rx) = channel(128);
+                    self.update_rx = update_rx;
+                    self.update_tx = update_tx;
+                }
+            }
+            info!("Client {} removed", client_id);
+        }
+
         debug!("{} pending server messages", response.len());
         let output = response.extract_messages(&mut self.server_state.server);
         for (clients, message) in output {
             debug!("Message {:?} to {:?}", message, clients);
             Self::send_message(&mut self.clients, message, clients.iter().cloned()).await;
         }
-
-        for client_id in response.extract_removed_clients() {
-            if self.clients.contains(client_id) {
-                self.clients.remove(client_id);
-            }
-            info!("Client {} removed", client_id);
-        }
     }
 
     async fn send_message<I>(
@@ -427,6 +436,7 @@
         let server_state = ServerState::new(self.clients_capacity, self.rooms_capacity);
 
         let clients = Slab::with_capacity(self.clients_capacity);
+        let (update_tx, update_rx) = channel(128);
 
         NetworkLayer {
             listener: self.listener.expect("No listener provided"),
@@ -437,6 +447,8 @@
             },
             server_state,
             clients,
+            update_tx,
+            update_rx,
         }
     }
 }
--- a/rust/hwphysics/src/collision.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hwphysics/src/collision.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -6,10 +6,6 @@
 use integral_geometry::{Point, PotSize};
 use land2d::Land2D;
 
-pub fn fppoint_round(point: &FPPoint) -> Point {
-    Point::new(point.x().round(), point.y().round())
-}
-
 #[derive(PartialEq, Eq, Clone, Copy, Debug)]
 pub struct CircleBounds {
     pub center: FPPoint,
@@ -90,7 +86,7 @@
         position: &FPPoint,
     ) {
         self.pairs.push((contact_gear_id1, contact_gear_id2));
-        self.positions.push(fppoint_round(&position));
+        self.positions.push(Point::from_fppoint(&position));
     }
 
     pub fn clear(&mut self) {
--- a/rust/hwphysics/src/grid.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/hwphysics/src/grid.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -1,5 +1,5 @@
 use crate::{
-    collision::{fppoint_round, CircleBounds, DetectedCollisions},
+    collision::{CircleBounds, DetectedCollisions},
     common::GearId,
 };
 
@@ -63,7 +63,7 @@
     }
 
     fn bin_index(&self, position: &FPPoint) -> Point {
-        self.index.map(fppoint_round(position))
+        self.index.map(Point::from_fppoint(position))
     }
 
     fn get_bin(&mut self, index: Point) -> &mut GridBin {
--- a/rust/integral-geometry/src/lib.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/integral-geometry/src/lib.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -59,7 +59,7 @@
 
     #[inline]
     pub const fn rotate90(self) -> Self {
-        Point::new(self.y, -self.x)
+        Self::new(self.y, -self.x)
     }
 
     #[inline]
@@ -68,8 +68,8 @@
     }
 
     #[inline]
-    pub fn clamp(self, rect: &Rect) -> Point {
-        Point::new(rect.x_range().clamp(self.x), rect.y_range().clamp(self.y))
+    pub fn clamp(self, rect: &Rect) -> Self {
+        Self::new(rect.x_range().clamp(self.x), rect.y_range().clamp(self.y))
     }
 
     #[inline]
--- a/rust/lib-hedgewars-engine/Cargo.toml	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/lib-hedgewars-engine/Cargo.toml	Wed Aug 28 15:34:49 2024 +0200
@@ -20,6 +20,7 @@
 hwphysics = { path = "../hwphysics" }
 mapgen = { path = "../mapgen" }
 vec2d = { path = "../vec2d" }
+log = "0.4.21"
 
 [dev-dependencies]
 proptest = "0.9.2"
--- a/rust/lib-hedgewars-engine/src/instance.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/lib-hedgewars-engine/src/instance.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -35,6 +35,7 @@
         }
 
         world.init(template());
+        world.init_renderer();
 
         Self {
             world,
@@ -72,7 +73,7 @@
         for message in messages {
             println!("Processing message: {:?}", message);
             match message {
-                Unknown => println!("Unknown message"),
+                Unknown(data) => println!("Unknown message: {:?}", data),
                 Empty => println!("Empty message"),
                 Synced(_, _) => unimplemented!(),
                 Unsynced(_) => unimplemented!(),
--- a/rust/lib-hedgewars-engine/src/lib.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/lib-hedgewars-engine/src/lib.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -62,7 +62,9 @@
 }
 
 #[no_mangle]
-pub extern "C" fn simple_event(engine_state: &mut EngineInstance, event_type: SimpleEventType) {}
+pub extern "C" fn simple_event(engine_state: &mut EngineInstance, event_type: SimpleEventType) {
+    println!("{:?}", event_type);
+}
 
 #[no_mangle]
 pub extern "C" fn long_event(
@@ -152,6 +154,7 @@
         gl::Viewport(0, 0, width as i32, height as i32);
     }
     engine_state.world.create_renderer(width, height);
+    engine_state.world.init_renderer();
 }
 
 #[no_mangle]
--- a/rust/lib-hedgewars-engine/src/world.rs	Wed Aug 28 15:31:51 2024 +0200
+++ b/rust/lib-hedgewars-engine/src/world.rs	Wed Aug 28 15:34:49 2024 +0200
@@ -13,6 +13,7 @@
 };
 use lfprng::LaggedFibonacciPRNG;
 use std::path::{Path, PathBuf};
+use log::trace;
 
 use crate::render::{camera::Camera, GearEntry, GearRenderer, MapRenderer};
 
@@ -60,6 +61,12 @@
         self.gear_renderer = Some(GearRenderer::new(&self.data_path.as_path()));
         self.camera = Camera::with_size(Size::new(width as usize, height as usize));
 
+        if let Some(ref state) = self.game_state {
+            self.camera.position = state.land.play_box().center();
+        }
+    }
+
+    pub fn init_renderer(&mut self) {
         use mapgen::{theme::Theme, MapGenerator};
 
         if let Some(ref state) = self.game_state {
--- a/share/hedgewars/Data/Locale/missions_en.txt	Wed Aug 28 15:31:51 2024 +0200
+++ b/share/hedgewars/Data/Locale/missions_en.txt	Wed Aug 28 15:34:49 2024 +0200
@@ -20,16 +20,16 @@
 User_Mission_-_Diver.desc="This 'amphibious assault' thing is harder than it looks."
 
 User_Mission_-_Teamwork.name=Teamwork
-User_Mission_-_Teamwork.desc="A malfunctioning cyborg is guarding a valuable military secret. You need to lead a special ops team of two hedgehogs with the task to destroy the enemy in order to obtain the secret! It is absolutely critical for our future operations that both your hedgehogs survive."
+User_Mission_-_Teamwork.desc="A malfunctioning cyborg is guarding a valuable military secret. You need to lead a special ops team of two hedgehogs tasked with destroying the enemy in order to obtain it! It is absolutely critical for our future operations that both hedgehogs survive."
 
 User_Mission_-_Teamwork_2.name=Teamwork 2
-User_Mission_-_Teamwork_2.desc="We have located a secret outpost of the Cybernetic Empire and it is only guarded by a harmless watch bot. Lead your special ops team to destroy the watch bot so we can claim the base as ours. Like before, we need both hedgehogs to survive!"
+User_Mission_-_Teamwork_2.desc="We have located a secret outpost of the Cybernetic Empire and it is only guarded by a harmless watch bot. Lead your special ops team to destroy it so we can claim the base as ours. Like before, we need both hedgehogs to survive!"
 
 User_Mission_-_Spooky_Tree.name=Spooky Tree
 User_Mission_-_Spooky_Tree.desc="Lots of crates out here. I sure hope that bird ain't feeling hungry."
 
 User_Mission_-_Bamboo_Thicket.name=Bamboo Thicket
-User_Mission_-_Bamboo_Thicket.desc="A cyborg is terrorizing the bamboo thicket and attacks anyone in sight with a practically perfect accuracy. Plan ahead, move fast and take out the enemy quickly!"
+User_Mission_-_Bamboo_Thicket.desc="A cyborg is terrorizing the bamboo thicket and attacking anyone in sight, with practically perfect accuracy. Plan ahead, move fast and take out the enemy quickly!"
 
 User_Mission_-_That_Sinking_Feeling.name=That Sinking Feeling
 User_Mission_-_That_Sinking_Feeling.desc="The water is rising rapidly and time is limited. Many have tried and failed. Can you save them all?"
@@ -56,13 +56,13 @@
 Bazooka_Battlefield.desc="Your loyal hedgehogs have ambushed the enemy. Destroy them only with bazookas! But don't take too long, the water will rise soon."
 
 Tentacle_Terror.name=Tentacle Terror
-Tentacle_Terror.desc="Below a terrible monster, your enemy is hiding like a coward and will attack you with air strikes as soon you lose cover. Show him who's the real boss in Hell! But you need some devilish good roping skills to even stand a chance."
+Tentacle_Terror.desc="Below a terrible monster, your enemy is hiding like a coward and will attack you with air strikes as soon you lose cover. Show him who's the real boss in Hell! But you need some devilishly good roping skills to even stand a chance."
 
 ClimbHome.name=Climb Home
 ClimbHome.desc="You are far away from home and the water is rising. Climb as high as you can!"
 
 portal.name=Portal Mind Challenge
-portal.desc="Use the portal to move fast and far, use it to kill, use it with caution!"
+portal.desc="Use the portal to move fast and far. Use it to kill. Use it with caution!"
 
 Target_Practice_-_Bazooka_easy.name=Target Practice: Bazooka (easy)
 Target_Practice_-_Bazooka_easy.desc="Alright, soldier, blow those targets up as fast as you can!"
@@ -77,7 +77,7 @@
 Target_Practice_-_Shotgun.desc="Shoot first, ask questions later!"
 
 Basic_Training_-_Sniper_Rifle.name=Target Practice: Sniper Rifle
-Basic_Training_-_Sniper_Rifle.desc="This is the perfect shooting range for snipers! Destroy all targets as fast and accurate you can and become a legend!"
+Basic_Training_-_Sniper_Rifle.desc="This is the perfect shooting range for snipers! Destroy all targets as fast and as accurately as you can and become a legend!"
 
 Target_Practice_-_Homing_Bee.name=Target Practice: Homing Bee
 Target_Practice_-_Homing_Bee.desc="Using the homing bee is trickier than it seems."
@@ -89,7 +89,7 @@
 Target_Practice_-_Grenade_hard.desc="This is nothing for greenhorns! We will place the targets at some really tricky positions."
 
 Challenge_-_Speed_Shoppa_-_Hedgelove.name=Time Trial: Shoppa Love
-Challenge_-_Speed_Shoppa_-_Hedgelove.desc="Show your love to rope and collect a few crates on a small map."
+Challenge_-_Speed_Shoppa_-_Hedgelove.desc="Show your love for roping and collect a few crates on a small map."
 
 Challenge_-_Speed_Shoppa_-_Ropes.name=Time Trial: Ropes and Crates
 Challenge_-_Speed_Shoppa_-_Ropes.desc="Take your rope and collect all crates on this medium-sized map."