# HG changeset patch # User unC0Rr # Date 1724852089 -7200 # Node ID 2003b466b27953843d10df51cc349685073f7dfd # Parent 6a3dc15b78b9d806e8a9b892fcba1b01ca93da9c# Parent 9be943326d9c2ef066e56fb6ffa01cec28f3ff4e Merge default diff -r 6a3dc15b78b9 -r 2003b466b279 INSTALL.md --- 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 --------------- diff -r 6a3dc15b78b9 -r 2003b466b279 QTfrontend/CMakeLists.txt --- 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}) diff -r 6a3dc15b78b9 -r 2003b466b279 QTfrontend/net/newnetclient.cpp --- 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; diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/ClientIO.hs --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/HWProtoChecker.hs --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/HWProtoCore.hs --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/HWProtoLobbyState.hs --- 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 -------------------------------------- diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/HandlerUtils.hs --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/ServerState.hs --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/Votes.hs --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 gameServer/hedgewars-server.cabal --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uAI.pas --- 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; diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uAIAmmoTests.pas --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uGears.pas --- 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) diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uGearsList.pas --- 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; diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uIO.pas --- 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) + ')', diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uSound.pas --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uTypes.pas --- 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; diff -r 6a3dc15b78b9 -r 2003b466b279 hedgewars/uVariables.pas --- 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; diff -r 6a3dc15b78b9 -r 2003b466b279 project_files/hwc/rtl/system.c --- 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); } /* diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/CMakeLists.txt --- 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) diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/Page1.qml --- 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); + } + } } diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/Page1Form.ui.qml --- 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 + } } } diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/engine_instance.cpp --- 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(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(event_type), + static_cast(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(event_type), x, + y); } void EngineInstance::renderFrame() { render_frame(m_instance.get()); } diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/engine_instance.h --- 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) diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/engine_interface.h --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/game_view.cpp --- 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 + #include +#include +#include #include #include -#include + +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 m_gameView; + QPointer 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(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&& 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()); -} diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/game_view.h --- 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 - #include +#include #include -#include -#include #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 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 m_renderer; - bool m_windowChanged; QPointer m_engineInstance; QSize m_viewportSize; QPoint m_centerPoint; + QList> m_actions; + + void addAction(std::function&& action); }; #endif // GAMEVIEW_H diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/hwengine.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 #include -#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(); diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/main.cpp --- 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("Hedgewars.Engine", 1, 0, "HWEngine"); qmlRegisterType("Hedgewars.Engine", 1, 0, "GameView"); qmlRegisterType("Hedgewars.Engine", 1, 0, "NetSession"); - qmlRegisterUncreatableType("Hedgewars.Engine", 1, 0, - "EngineInstance", - "Create by HWEngine run methods"); + qmlRegisterUncreatableType( + "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(); diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/main.qml --- 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 diff -r 6a3dc15b78b9 -r 2003b466b279 qmlfrontend/players_model.cpp --- 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!" diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-checker/Cargo.toml --- 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"]} diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-checker/src/main.rs --- 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> { 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 { 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 diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-engine-messages/Cargo.toml --- 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" diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-engine-messages/src/messages.rs --- 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), Empty, Synced(SyncedEngineMessage, u32), Unsynced(UnsyncedEngineMessage), @@ -172,7 +172,7 @@ NextTurn => em![b'N'], Switch => em![b'S'], Timer(t) => vec![b'0' + t], - Slot(s) => vec![b'~' , *s], + Slot(s) => vec![b'~', *s], SetWeapon(s) => vec![b'~', *s], Put(x, y) => { let mut v = vec![b'p']; @@ -180,14 +180,14 @@ v.write_i24::(*y).unwrap(); v - }, + } CursorMove(x, y) => { let mut v = vec![b'P']; v.write_i24::(*x).unwrap(); v.write_i24::(*y).unwrap(); v - }, + } HighJump => em![b'J'], LongJump => em![b'j'], Skip => em![b','], @@ -242,7 +242,7 @@ fn to_unwrapped(&self) -> Vec { use self::EngineMessage::*; match self { - Unknown => unreachable!("you're not supposed to construct such messages"), + Unknown(_) => unreachable!("you're not supposed to construct such messages"), Empty => unreachable!("you're not supposed to construct such messages"), Synced(SyncedEngineMessage::TimeWrap, _) => vec![b'#', 0xff, 0xff], Synced(msg, timestamp) => { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-engine-messages/src/parser.rs --- 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::))) - } +fn eof_slice(i: I) -> IResult +where + I: nom::InputLength + Clone, +{ + if i.input_len() == 0 { + Ok((i.clone(), i)) + } else { + Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof))) } - ); -); +} +fn unrecognized_message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + map(rest, |i: &[u8]| Unknown(i.to_owned()))(input) +} -named!(unrecognized_message<&[u8], EngineMessage>, - do_parse!(rest >> (Unknown)) -); +fn string_tail(input: &[u8]) -> IResult<&[u8], String> { + map_res(rest, str::from_utf8)(input).map(|(i, s)| (i, s.to_owned())) +} -named!(string_tail<&[u8], String>, map!(map_res!(rest, str::from_utf8), String::from)); - -named!(length_without_timestamp<&[u8], usize>, - map_opt!(rest_len, |l| if l > 2 { Some(l - 2) } else { None } ) -); +fn length_without_timestamp(input: &[u8]) -> IResult<&[u8], usize> { + map_opt(rest_len, |l| if l > 2 { Some(l - 2) } else { None })(input) +} -named!(synced_message<&[u8], SyncedEngineMessage>, alt!( - do_parse!(tag!("L") >> (Left(Press))) - | do_parse!(tag!("l") >> ( Left(Release) )) - | do_parse!(tag!("R") >> ( Right(Press) )) - | do_parse!(tag!("r") >> ( Right(Release) )) - | do_parse!(tag!("U") >> ( Up(Press) )) - | do_parse!(tag!("u") >> ( Up(Release) )) - | do_parse!(tag!("D") >> ( Down(Press) )) - | do_parse!(tag!("d") >> ( Down(Release) )) - | do_parse!(tag!("Z") >> ( Precise(Press) )) - | do_parse!(tag!("z") >> ( Precise(Release) )) - | do_parse!(tag!("A") >> ( Attack(Press) )) - | do_parse!(tag!("a") >> ( Attack(Release) )) - | do_parse!(tag!("N") >> ( NextTurn )) - | do_parse!(tag!("j") >> ( LongJump )) - | do_parse!(tag!("J") >> ( HighJump )) - | do_parse!(tag!("S") >> ( Switch )) - | do_parse!(tag!(",") >> ( Skip )) - | do_parse!(tag!("1") >> ( Timer(1) )) - | do_parse!(tag!("2") >> ( Timer(2) )) - | do_parse!(tag!("3") >> ( Timer(3) )) - | do_parse!(tag!("4") >> ( Timer(4) )) - | do_parse!(tag!("5") >> ( Timer(5) )) - | do_parse!(tag!("p") >> x: be_i24 >> y: be_i24 >> ( Put(x, y) )) - | do_parse!(tag!("P") >> x: be_i24 >> y: be_i24 >> ( CursorMove(x, y) )) - | do_parse!(tag!("f") >> s: string_tail >> ( SyncedEngineMessage::TeamControlLost(s) )) - | do_parse!(tag!("g") >> s: string_tail >> ( SyncedEngineMessage::TeamControlGained(s) )) - | do_parse!(tag!("t") >> t: be_u8 >> ( Taunt(t) )) - | do_parse!(tag!("w") >> w: be_u8 >> ( SetWeapon(w) )) - | do_parse!(tag!("~") >> s: be_u8 >> ( Slot(s) )) - | do_parse!(tag!("+") >> ( Heartbeat )) -)); +fn synced_message(input: &[u8]) -> IResult<&[u8], SyncedEngineMessage> { + alt(( + alt(( + map(tag(b"L"), |_| Left(Press)), + map(tag(b"l"), |_| Left(Release)), + map(tag(b"R"), |_| Right(Press)), + map(tag(b"r"), |_| Right(Release)), + map(tag(b"U"), |_| Up(Press)), + map(tag(b"u"), |_| Up(Release)), + map(tag(b"D"), |_| Down(Press)), + map(tag(b"d"), |_| Down(Release)), + map(tag(b"Z"), |_| Precise(Press)), + map(tag(b"z"), |_| Precise(Release)), + map(tag(b"A"), |_| Attack(Press)), + map(tag(b"a"), |_| Attack(Release)), + map(tag(b"N"), |_| NextTurn), + map(tag(b"j"), |_| LongJump), + map(tag(b"J"), |_| HighJump), + map(tag(b"S"), |_| Switch), + )), + alt(( + map(tag(b","), |_| Skip), + map(tag(b"1"), |_| Timer(1)), + map(tag(b"2"), |_| Timer(2)), + map(tag(b"3"), |_| Timer(3)), + map(tag(b"4"), |_| Timer(4)), + map(tag(b"5"), |_| Timer(5)), + map(tuple((tag(b"p"), be_i24, be_i24)), |(_, x, y)| Put(x, y)), + map(tuple((tag(b"P"), be_i24, be_i24)), |(_, x, y)| { + CursorMove(x, y) + }), + map(preceded(tag(b"f"), string_tail), TeamControlLost), + map(preceded(tag(b"g"), string_tail), TeamControlGained), + map(preceded(tag(b"t"), be_u8), Taunt), + map(preceded(tag(b"w"), be_u8), SetWeapon), + map(preceded(tag(b"~"), be_u8), Slot), + map(tag(b"+"), |_| Heartbeat), + )), + ))(input) +} -named!(unsynced_message<&[u8], UnsyncedEngineMessage>, alt!( - do_parse!(tag!("F") >> s: string_tail >> ( UnsyncedEngineMessage::TeamControlLost(s) )) - | do_parse!(tag!("G") >> s: string_tail >> ( UnsyncedEngineMessage::TeamControlGained(s) )) - | do_parse!(tag!("h") >> s: string_tail >> ( UnsyncedEngineMessage::HogSay(s) )) - | do_parse!(tag!("s") >> s: string_tail >> ( UnsyncedEngineMessage::ChatMessage(s)) ) - | do_parse!(tag!("b") >> s: string_tail >> ( UnsyncedEngineMessage::TeamMessage(s)) ) // TODO: wtf is the format -)); +fn unsynced_message(input: &[u8]) -> IResult<&[u8], UnsyncedEngineMessage> { + alt(( + map( + preceded(tag(b"F"), string_tail), + UnsyncedEngineMessage::TeamControlLost, + ), + map( + preceded(tag(b"G"), string_tail), + UnsyncedEngineMessage::TeamControlGained, + ), + map( + preceded(tag(b"h"), string_tail), + UnsyncedEngineMessage::HogSay, + ), + map( + preceded(tag(b"s"), string_tail), + UnsyncedEngineMessage::ChatMessage, + ), + map( + preceded(tag(b"b"), string_tail), + UnsyncedEngineMessage::TeamMessage, + ), + ))(input) +} -named!(unordered_message<&[u8], UnorderedEngineMessage>, alt!( - do_parse!(tag!("?") >> ( Ping )) - | do_parse!(tag!("!") >> ( Pong )) - | do_parse!(tag!("E") >> s: string_tail >> ( UnorderedEngineMessage::Error(s)) ) - | do_parse!(tag!("W") >> s: string_tail >> ( Warning(s)) ) - | do_parse!(tag!("M") >> s: string_tail >> ( GameSetupChecksum(s)) ) - | do_parse!(tag!("o") >> ( StopSyncing )) - | do_parse!(tag!("I") >> ( PauseToggled )) -)); - -named!(config_message<&[u8], ConfigEngineMessage>, alt!( - do_parse!(tag!("C") >> (ConfigRequest)) - | do_parse!(tag!("eseed ") >> s: string_tail >> ( SetSeed(s)) ) - | do_parse!(tag!("e$feature_size ") >> s: string_tail >> ( SetFeatureSize(s.parse::().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 >, many0!(complete!(message))); +fn message(input: &[u8]) -> IResult<&[u8], EngineMessage> { + alt((empty_message, non_empty_message))(input) +} + +pub fn extract_messages(input: &[u8]) -> IResult<&[u8], Vec> { + many0(complete(message))(input) +} pub fn extract_message(buf: &[u8]) -> Option<(usize, EngineMessage)> { let parse_result = message(buf); @@ -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))) diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-network-protocol/src/parser.rs --- 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> { +fn opt_arg(input: &[u8]) -> HwResult> { 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> { +fn opt_space_arg(input: &[u8]) -> HwResult> { alt(( map(peek(end_of_message), |_| None), map(preceded(spaces, a_line), Some), @@ -184,10 +184,10 @@ } fn no_arg_message(input: &[u8]) -> HwResult { - fn message<'a>( - name: &'a str, + fn message( + name: &str, msg: HwProtocolMessage, - ) -> impl Fn(&'a [u8]) -> HwResult<'a, HwProtocolMessage> { + ) -> impl Fn(&[u8]) -> HwResult + '_ { move |i| map(tag(name), |_| msg.clone())(i) } @@ -207,14 +207,14 @@ } fn single_arg_message(input: &[u8]) -> HwResult { - 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 + '_ where - F: Fn(&[u8]) -> HwResult, - 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 + '_ { 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 + '_ where - F: Fn(&[u8]) -> HwResult, - 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 { 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 + '_ where - F: Fn(&[u8]) -> HwResult, - 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 + '_ where - G: Fn(Vec) -> HwServerMessage, + G: Fn(Vec) -> 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 + '_ where - G: Fn(String, Vec) -> HwServerMessage, + G: Fn(String, Vec) -> 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 + '_ { move |i| map(tag(name), |_| msg.clone())(i) } diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-network-protocol/src/tests/parser.rs --- 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))); diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/Cargo.toml --- 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" } diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/core/anteroom.rs --- 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![], diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/core/client.rs --- 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, diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/core/room.rs --- 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 + 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, pub left_teams: Vec, pub msg_log: Vec, pub sync_msg: Option, @@ -34,7 +41,7 @@ } impl GameInfo { - fn new(teams: Vec<(ClientId, TeamInfo)>, config: RoomConfig) -> GameInfo { + fn new(teams: Vec, 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 + Clone { - client_teams_impl(&self.original_teams, client_id) + pub fn client_teams(&self, owner_id: ClientId) -> impl Iterator + Clone { + client_teams_impl(&self.original_teams, owner_id) + } + + pub fn client_teams_by_nick<'a>( + &'a self, + owner_nick: &'a str, + ) -> impl Iterator + 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, config: RoomConfig, pub voting: Option, pub saves: HashMap, @@ -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(&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 { - client_teams_impl(&self.teams, client_id) + pub fn client_teams(&self, owner_id: ClientId) -> impl Iterator { + client_teams_impl(&self.teams, owner_id) } pub fn client_team_indices(&self, client_id: ClientId) -> Vec { 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 + '_ { 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 { @@ -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) { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/core/server.rs --- 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), +} + +#[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 { + 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 + Clone), JoinRoomError> { + ) -> Result< + ( + &HwClient, + Option<&HwClient>, + &HwRoom, + impl Iterator + 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 + Clone), JoinRoomError> { + ) -> Result< + ( + &HwClient, + Option<&HwClient>, + &HwRoom, + impl Iterator + 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> { + 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 { + 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> { - 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 { + match kind { + VoteType::Kick(nick) => { + if let Some(kicked_id) = self + .server + .find_client(&nick) + .filter(|c| c.room_id == Some(self.room_id)) + .map(|c| c.id) + { + let leave_result = self.remove_from_room(kicked_id); + Some(VoteEffect::Kicked(kicked_id, leave_result)) + } else { + None + } + } + VoteType::Map(None) => None, + VoteType::Map(Some(name)) => self + .load_config(&name) + .map(|s| VoteEffect::Map(s.to_string())), + VoteType::Pause => Some(VoteEffect::Pause).filter(|_| self.toggle_pause()), + VoteType::NewSeed => { + let seed = thread_rng().gen_range(0..1_000_000_000).to_string(); + let cfg = GameCfg::Seed(seed); + //todo!("Protocol backwards compatibility"); + self.room_mut().set_config(cfg.clone()); + Some(VoteEffect::NewSeed(cfg)) + } + VoteType::HedgehogsPerTeam(number) => { + let nicks = self.set_hedgehogs_number(number); + Some(VoteEffect::HedgehogsPerTeam(number, nicks)) + } + } + } + pub fn vote(&mut self, vote: Vote) -> Result { use self::{VoteError::*, VoteResult::*}; let client_id = self.client_id; @@ -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) -> &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)); + } } } } diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/handlers.rs --- 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) + } }, } } diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/handlers/common.rs --- 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 + 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, + old_name: Option, 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, 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)), ); diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/handlers/inanteroom.rs --- 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()); diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/handlers/inlobby.rs --- 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 { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/handlers/inroom.rs --- 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 , map , pause, newseed, hedgehogs ".to_string()) .send_self()); } @@ -495,6 +495,6 @@ response.warn("The player is not in your room.") } }, - _ => warn!("Unimplemented!"), + message => warn!("Unimplemented: {:?}", message), } } diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/main.rs --- 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 = 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..]) { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/protocol.rs --- 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))), diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/server/demo.rs --- 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)? { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/server/io.rs --- 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"); diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hedgewars-server/src/server/network.rs --- 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>, + update_tx: Sender, + update_rx: Receiver, } 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( @@ -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, } } } diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hwphysics/src/collision.rs --- 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) { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/hwphysics/src/grid.rs --- 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 { diff -r 6a3dc15b78b9 -r 2003b466b279 rust/integral-geometry/src/lib.rs --- 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] diff -r 6a3dc15b78b9 -r 2003b466b279 rust/lib-hedgewars-engine/Cargo.toml --- 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" diff -r 6a3dc15b78b9 -r 2003b466b279 rust/lib-hedgewars-engine/src/instance.rs --- 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!(), diff -r 6a3dc15b78b9 -r 2003b466b279 rust/lib-hedgewars-engine/src/lib.rs --- 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] diff -r 6a3dc15b78b9 -r 2003b466b279 rust/lib-hedgewars-engine/src/world.rs --- 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 { diff -r 6a3dc15b78b9 -r 2003b466b279 share/hedgewars/Data/Locale/missions_en.txt --- 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."