DEADSOFTWARE

oops; gTime fwp sync timeout was too small
[d2df-sdl.git] / src / game / g_netmsg.pas
index 213669e57aca2a89a5a93814f88e735080dc2264..9563b8083f9fd7d1f7955a35fb0a36a96200cf35 100644 (file)
@@ -1,4 +1,4 @@
-(* Copyright (C)  DooM 2D:Forever Developers
+(* Copyright (C)  Doom 2D: Forever Developers
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,6 +16,8 @@
 {$INCLUDE ../shared/a_modes.inc}
 unit g_netmsg;
 
+{.$DEFINE K8_XXX_WEAPON_DEBUG}
+
 interface
 
 uses e_msg, g_net, g_triggers, Classes, SysUtils, md5;
@@ -52,8 +54,8 @@ const
   NET_MSG_MSHOT  = 134;
   NET_MSG_MDEL   = 135;
 
-  NET_MSG_PSTATE = 141;
-  NET_MSG_PTEX   = 142;
+  NET_MSG_PSTATE   = 141;
+  NET_MSG_PTEX     = 142;
 
   NET_MSG_TSOUND = 151;
   NET_MSG_TMUSIC = 152;
@@ -168,8 +170,8 @@ procedure MH_SEND_PlayerSettings(PID: Word; Mdl: string = ''; ID: Integer = NET_
 procedure MH_SEND_ItemSpawn(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
 procedure MH_SEND_ItemDestroy(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
 // PANEL
-procedure MH_SEND_PanelTexture(PType: Word; PID: LongWord; AnimLoop: Byte; ID: Integer = NET_EVERYONE);
-procedure MH_SEND_PanelState(PType: Word; PID: LongWord; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PanelTexture(PGUID: Integer; AnimLoop: Byte; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PanelState(PGUID: Integer; ID: Integer = NET_EVERYONE);
 // MONSTER
 procedure MH_SEND_MonsterSpawn(UID: Word; ID: Integer = NET_EVERYONE);
 procedure MH_SEND_MonsterPos(UID: Word; ID: Integer = NET_EVERYONE);
@@ -384,7 +386,7 @@ begin
 
   g_Console_Add(Format(_lc[I_PLAYER_JOIN], [PName]), True);
   e_WriteLog('NET: Client ' + PName + ' [' + IntToStr(C^.ID) +
-             '] connected. Assigned player #' + IntToStr(PID) + '.', MSG_NOTIFY);
+             '] connected. Assigned player #' + IntToStr(PID) + '.', TMsgType.Notify);
 
   MH_SEND_Info(C^.ID);
 
@@ -430,12 +432,13 @@ end;
 
 function  MH_RECV_PlayerPos(C: pTNetClient; var M: TMsg): Word;
 var
-  Dir, i: Byte;
-  WeaponSelect: Word;
+  Dir{, i}: Byte;
+  //WeaponSelect: Word;
   PID: Word;
   kByte: Word;
   Pl: TPlayer;
   GT: LongWord;
+  newweapon: Byte;
 begin
   Result := 0;
   if not gGameOn then Exit;
@@ -453,13 +456,22 @@ begin
     NetTime := GT;
     kByte := M.ReadWord();
     Dir := M.ReadByte();
-    WeaponSelect := M.ReadWord();
+    //WeaponSelect := M.ReadWord();
+    newweapon := M.ReadByte();
+    if (newweapon <> CurrWeap) then
+    begin
+{$IFDEF K8_XXX_WEAPON_DEBUG}
+      writeln('HOST PLRPOS: got: currweap=', CurrWeap, '; curfrm=', gTime, '; netweap=', newweapon, '; oldweap=', CurrWeap);
+{$ENDIF}
+      //NetForceWeap := newweapon;
+      SetWeapon(newweapon);
+    end;
     //e_WriteLog(Format('R:ws=%d', [WeaponSelect]), MSG_WARNING);
     if Direction <> TDirection(Dir) then
       JustTeleported := False;
 
     SetDirection(TDirection(Dir));
-    ReleaseKeys;
+    ReleaseKeysNoWeapon();
 
     if kByte = NET_KEY_CHAT then
     begin
@@ -474,9 +486,10 @@ begin
     if LongBool(kByte and NET_KEY_JUMP) then PressKey(KEY_JUMP, 10000);
     if LongBool(kByte and NET_KEY_FIRE) then PressKey(KEY_FIRE, 10000);
     if LongBool(kByte and NET_KEY_OPEN) then PressKey(KEY_OPEN, 10000);
-    if LongBool(kByte and NET_KEY_NW) then PressKey(KEY_NEXTWEAPON, 10000);
-    if LongBool(kByte and NET_KEY_PW) then PressKey(KEY_PREVWEAPON, 10000);
+    //if LongBool(kByte and NET_KEY_NW) then PressKey(KEY_NEXTWEAPON, 10000);
+    //if LongBool(kByte and NET_KEY_PW) then PressKey(KEY_PREVWEAPON, 10000);
 
+    (*
     for i := 0 to 15 do
     begin
       if (WeaponSelect and Word(1 shl i)) <> 0 then
@@ -485,6 +498,7 @@ begin
         QueueWeaponSwitch(i);
       end;
     end;
+    *)
   end;
 
   // MH_SEND_PlayerPos(False, PID, C^.ID);
@@ -634,12 +648,19 @@ procedure MH_SEND_Everything(CreatePlayers: Boolean = False; ID: Integer = NET_E
     MH_SEND_ItemSpawn(True, it.myid, ID);
   end;
 
-  function sendMonSpawn (monidx: Integer; mon: TMonster): Boolean;
+  function sendMonSpawn (mon: TMonster): Boolean;
   begin
     result := false; // don't stop
     MH_SEND_MonsterSpawn(mon.UID, ID);
   end;
 
+  function sendPanelState (pan: TPanel): Boolean;
+  begin
+    result := false; // don't stop
+    MH_SEND_PanelState(pan.guid, ID); // anyway, to sync mplats
+    if (pan.CanChangeTexture) then MH_SEND_PanelTexture(pan.guid, pan.LastAnimLoop, ID);
+  end;
+
 var
   I: Integer;
 begin
@@ -654,86 +675,38 @@ begin
         MH_SEND_PlayerStats(gPlayers[I].UID, ID);
 
         if (gPlayers[I].Flag <> FLAG_NONE) and (gGameSettings.GameMode = GM_CTF) then
+        begin
           MH_SEND_FlagEvent(FLAG_STATE_CAPTURED, gPlayers[I].Flag, gPlayers[I].UID, True, ID);
+        end;
       end;
     end;
   end;
 
   g_Items_ForEachAlive(sendItemRespawn, true); // backwards
   g_Mons_ForEach(sendMonSpawn);
-
-  if gWalls <> nil then
-    for I := Low(gWalls) to High(gWalls) do
-      if gWalls[I] <> nil then
-        with gWalls[I] do
-        begin
-          if Door then
-            MH_SEND_PanelState(PanelType, I, ID);
-
-          if GetTextureCount > 1 then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
-        end;
-
-  if gLifts <> nil then
-    for I := Low(gLifts) to High(gLifts) do
-      if gLifts[I] <> nil then
-        with gLifts[I] do
-          MH_SEND_PanelState(PanelType, I, ID);
-
-  if gRenderForegrounds <> nil then
-    for I := Low(gRenderForegrounds) to High(gRenderForegrounds) do
-      if gRenderForegrounds[I] <> nil then
-        with gRenderForegrounds[I] do
-        begin
-          if (GetTextureCount > 1) then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
-          if Moved then
-            MH_SEND_PanelState(PanelType, I, ID);
-        end;
-  if gRenderBackgrounds <> nil then
-    for I := Low(gRenderBackgrounds) to High(gRenderBackgrounds) do
-      if gRenderBackgrounds[I] <> nil then
-        with gRenderBackgrounds[I] do
-        begin
-          if (GetTextureCount > 1) then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
-          if Moved then
-            MH_SEND_PanelState(PanelType, I, ID);
-        end;
-  if gWater <> nil then
-    for I := Low(gWater) to High(gWater) do
-      if gWater[I] <> nil then
-        with gWater[I] do
-          if GetTextureCount > 1 then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
-  if gAcid1 <> nil then
-    for I := Low(gAcid1) to High(gAcid1) do
-      if gAcid1[I] <> nil then
-        with gAcid1[I] do
-          if GetTextureCount > 1 then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
-  if gAcid2 <> nil then
-    for I := Low(gAcid2) to High(gAcid2) do
-      if gAcid2[I] <> nil then
-        with gAcid2[I] do
-          if GetTextureCount > 1 then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
-  if gSteps <> nil then
-    for I := Low(gSteps) to High(gSteps) do
-      if gSteps[I] <> nil then
-        with gSteps[I] do
-          if GetTextureCount > 1 then
-            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+  g_Map_ForEachPanel(sendPanelState);
 
   if gTriggers <> nil then
+  begin
     for I := Low(gTriggers) to High(gTriggers) do
+    begin
       if gTriggers[I].TriggerType = TRIGGER_SOUND then
+      begin
         MH_SEND_TriggerSound(gTriggers[I], ID);
+      end;
+    end;
+  end;
 
   if Shots <> nil then
+  begin
     for I := Low(Shots) to High(Shots) do
+    begin
       if Shots[i].ShotType in [6, 7, 8] then
+      begin
         MH_SEND_CreateShot(i, ID);
+      end;
+    end;
+  end;
 
   MH_SEND_TriggerMusic(ID);
 
@@ -742,16 +715,16 @@ begin
 
   if gGameSettings.GameMode = GM_CTF then
   begin
-    if gFlags[FLAG_RED].State <> FLAG_STATE_CAPTURED then
-      MH_SEND_FlagEvent(gFlags[FLAG_RED].State, FLAG_RED, 0, True, ID);
-    if gFlags[FLAG_BLUE].State <> FLAG_STATE_CAPTURED then
-      MH_SEND_FlagEvent(gFlags[FLAG_BLUE].State, FLAG_BLUE, 0, True, ID);
+    if gFlags[FLAG_RED].State <> FLAG_STATE_CAPTURED then MH_SEND_FlagEvent(gFlags[FLAG_RED].State, FLAG_RED, 0, True, ID);
+    if gFlags[FLAG_BLUE].State <> FLAG_STATE_CAPTURED then MH_SEND_FlagEvent(gFlags[FLAG_BLUE].State, FLAG_BLUE, 0, True, ID);
   end;
 
   if CreatePlayers and (ID >= 0) then NetClients[ID].State := NET_STATE_GAME;
 
   if gLMSRespawn > LMS_RESPAWN_NONE then
+  begin
     MH_SEND_GameEvent(NET_EV_LMS_WARMUP, (gLMSRespawnTime - gTime) div 1000, 'N', ID);
+  end;
 end;
 
 procedure MH_SEND_Info(ID: Byte);
@@ -818,8 +791,8 @@ begin
     if Mode = NET_CHAT_PLAYER then
     begin
       g_Console_Add(Txt, True);
-      e_WriteLog('[Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
-      g_Sound_PlayEx('SOUND_GAME_RADIO');
+      e_WriteLog('[Chat] ' + b_Text_Unformat(Txt), TMsgType.Notify);
+      g_Game_ChatSound(b_Text_Unformat(Txt));
     end
     else
     if Mode = NET_CHAT_TEAM then
@@ -828,14 +801,14 @@ begin
         if (gPlayer1.Team = TEAM_RED) and (Team = TEAM_RED) then
         begin
           g_Console_Add(#18'[Team] '#2 + Txt, True);
-          e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
-          g_Sound_PlayEx('SOUND_GAME_RADIO');
+          e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), TMsgType.Notify);
+          g_Game_ChatSound(b_Text_Unformat(Txt));
         end
         else if (gPlayer1.Team = TEAM_BLUE) and (Team = TEAM_BLUE) then
         begin
           g_Console_Add(#20'[Team] '#2 + Txt, True);
-          e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
-          g_Sound_PlayEx('SOUND_GAME_RADIO');
+          e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), TMsgType.Notify);
+          g_Game_ChatSound(b_Text_Unformat(Txt));
         end;
       end;
   end
@@ -843,8 +816,8 @@ begin
   begin
     Name := g_Net_ClientName_ByID(ID);
     g_Console_Add('-> ' + Name + ': ' + Txt, True);
-    e_WriteLog('[Tell ' + Name + '] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
-    g_Sound_PlayEx('SOUND_GAME_RADIO');
+    e_WriteLog('[Tell ' + Name + '] ' + b_Text_Unformat(Txt), TMsgType.Notify);
+    g_Game_ChatSound(b_Text_Unformat(Txt), False);
   end;
 end;
 
@@ -955,7 +928,7 @@ begin
   NetOut.Write(EvStr);
   NetOut.Write(Byte(gLastMap));
   NetOut.Write(gTime);
-  if (EvType = NET_EV_MAPSTART) and (Pos(':\', EvStr) > 0) then
+  if (EvType = NET_EV_MAPSTART) and isWadPath(EvStr) then
   begin
     NetOut.Write(Byte(1));
     NetOut.Write(gWADHash);
@@ -1048,7 +1021,7 @@ begin
     end;
 
     NetOut.Write(kByte);
-    if Direction = D_LEFT then NetOut.Write(Byte(0)) else NetOut.Write(Byte(1));
+    if Direction = TDirection.D_LEFT then NetOut.Write(Byte(0)) else NetOut.Write(Byte(1));
     NetOut.Write(GameX);
     NetOut.Write(GameY);
     NetOut.Write(GameVelX);
@@ -1064,6 +1037,7 @@ procedure MH_SEND_PlayerStats(PID: Word; ID: Integer = NET_EVERYONE);
 var
   P: TPlayer;
   I: Integer;
+  ww: Word;
 begin
   P := g_Player_Get(PID);
   if P = nil then Exit;
@@ -1073,7 +1047,7 @@ begin
 
   with P do
   begin
-    NetOut.Write(Byte(Live));
+    NetOut.Write(Byte(alive));
     NetOut.Write(Byte(GodMode));
     NetOut.Write(Health);
     NetOut.Write(Armor);
@@ -1082,8 +1056,10 @@ begin
     NetOut.Write(Lives);
     NetOut.Write(Team);
 
-    for I := WP_FIRST to WP_LAST do
-      NetOut.Write(Byte(FWeapon[I]));
+    // collect all weapons in one word
+    ww := 0;
+    for I := WP_FIRST to WP_LAST do if (FWeapon[I]) then ww := ww or (1 shl (i-WP_FIRST));
+    NetOut.Write(Word(ww));
 
     for I := A_BULLETS to A_HIGH do
       NetOut.Write(FAmmo[I]);
@@ -1094,22 +1070,30 @@ begin
     for I := MR_SUIT to MR_MAX do
       NetOut.Write(LongWord(FMegaRulez[I]));
 
-    NetOut.Write(Byte(R_ITEM_BACKPACK in FRulez));
-    NetOut.Write(Byte(R_KEY_RED in FRulez));
-    NetOut.Write(Byte(R_KEY_GREEN in FRulez));
-    NetOut.Write(Byte(R_KEY_BLUE in FRulez));
-    NetOut.Write(Byte(R_BERSERK in FRulez));
+    // collect all special flags in one byte
+    ww := 0;
+    if (R_ITEM_BACKPACK in FRulez) then ww := ww or $01;
+    if (R_KEY_RED in FRulez) then ww := ww or $02;
+    if (R_KEY_GREEN in FRulez) then ww := ww or $04;
+    if (R_KEY_BLUE in FRulez) then ww := ww or $08;
+    if (R_BERSERK in FRulez) then ww := ww or $10;
+    NetOut.Write(Byte(ww));
 
     NetOut.Write(Frags);
     NetOut.Write(Death);
 
-    NetOut.Write(CurrWeap);
+    //NetOut.Write(CurrWeap);
+    NetOut.Write(Byte(CurrWeap));
+
+    // other flags
+    ww := 0;
+    if (FSpectator) then ww := ww or $01;
+    if (FGhost) then ww := ww or $02;
+    if (FPhysics) then ww := ww or $04;
+    if (FNoRespawn) then ww := ww or $08;
+    if (FJetpack) then ww := ww or $10;
+    NetOut.Write(Byte(ww));
 
-    NetOut.Write(Byte(FSpectator));
-    NetOut.Write(Byte(FGhost));
-    NetOut.Write(Byte(FPhysics));
-    NetOut.Write(Byte(FNoRespawn));
-    NetOut.Write(Byte(FJetpack));
     NetOut.Write(FFireTime);
   end;
 
@@ -1189,13 +1173,16 @@ end;
 procedure MH_SEND_ItemSpawn(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
 var
   it: PItem;
+  tt: Byte;
 begin
   it := g_Items_ByIdx(IID);
 
   NetOut.Write(Byte(NET_MSG_ISPAWN));
   NetOut.Write(IID);
   NetOut.Write(Byte(Quiet));
-  NetOut.Write(it.ItemType);
+  tt := it.ItemType;
+  if it.dropped then tt := tt or $80;
+  NetOut.Write(tt);
   NetOut.Write(Byte(it.Fall));
   NetOut.Write(Byte(it.Respawnable));
   NetOut.Write(it.Obj.X);
@@ -1217,34 +1204,17 @@ end;
 
 // PANEL
 
-procedure MH_SEND_PanelTexture(PType: Word; PID: LongWord; AnimLoop: Byte; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PanelTexture(PGUID: Integer; AnimLoop: Byte; ID: Integer = NET_EVERYONE);
 var
   TP: TPanel;
 begin
-  case PType of
-    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
-      TP := gWalls[PID];
-    PANEL_FORE:
-      TP := gRenderForegrounds[PID];
-    PANEL_BACK:
-      TP := gRenderBackgrounds[PID];
-    PANEL_WATER:
-      TP := gWater[PID];
-    PANEL_ACID1:
-      TP := gAcid1[PID];
-    PANEL_ACID2:
-      TP := gAcid2[PID];
-    PANEL_STEP:
-      TP := gSteps[PID];
-    else
-      Exit;
-  end;
+  TP := g_Map_PanelByGUID(PGUID);
+  if (TP = nil) then exit;
 
   with TP do
   begin
     NetOut.Write(Byte(NET_MSG_PTEX));
-    NetOut.Write(PType);
-    NetOut.Write(PID);
+    NetOut.Write(LongWord(PGUID));
     NetOut.Write(FCurTexture);
     NetOut.Write(FCurFrame);
     NetOut.Write(FCurFrameCount);
@@ -1254,36 +1224,36 @@ begin
   g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
 end;
 
-procedure MH_SEND_PanelState(PType: Word; PID: LongWord; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PanelState(PGUID: Integer; ID: Integer = NET_EVERYONE);
 var
   TP: TPanel;
+  mpflags: Byte = 0;
 begin
-  case PType of
-    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
-      TP := gWalls[PID];
-    PANEL_LIFTUP, PANEL_LIFTDOWN, PANEL_LIFTLEFT, PANEL_LIFTRIGHT:
-      TP := gLifts[PID];
-    PANEL_BACK:
-    begin
-      TP := gRenderBackgrounds[PID];
-      TP.Moved := True;
-    end;
-    PANEL_FORE:
-    begin
-      TP := gRenderForegrounds[PID];
-      TP.Moved := True;
-    end;
-    else
-      Exit;
-  end;
+  TP := g_Map_PanelByGUID(PGUID);
+  if (TP = nil) then exit;
 
   NetOut.Write(Byte(NET_MSG_PSTATE));
-  NetOut.Write(PType);
-  NetOut.Write(PID);
+  NetOut.Write(LongWord(PGUID));
   NetOut.Write(Byte(TP.Enabled));
   NetOut.Write(TP.LiftType);
   NetOut.Write(TP.X);
   NetOut.Write(TP.Y);
+  NetOut.Write(Word(TP.Width));
+  NetOut.Write(Word(TP.Height));
+  // mplats
+  NetOut.Write(LongInt(TP.movingSpeedX));
+  NetOut.Write(LongInt(TP.movingSpeedY));
+  NetOut.Write(LongInt(TP.movingStartX));
+  NetOut.Write(LongInt(TP.movingStartY));
+  NetOut.Write(LongInt(TP.movingEndX));
+  NetOut.Write(LongInt(TP.movingEndY));
+  NetOut.Write(LongInt(TP.sizeSpeedX));
+  NetOut.Write(LongInt(TP.sizeSpeedY));
+  NetOut.Write(LongInt(TP.sizeEndX));
+  NetOut.Write(LongInt(TP.sizeEndY));
+  if TP.movingActive then mpflags := mpflags or 1;
+  if TP.moveOnce then mpflags := mpflags or 2;
+  NetOut.Write(Byte(mpflags));
 
   g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
 end;
@@ -1464,8 +1434,8 @@ begin
     if Mode = NET_CHAT_PLAYER then
     begin
       g_Console_Add(Txt, True);
-      e_WriteLog('[Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
-      g_Sound_PlayEx('SOUND_GAME_RADIO');
+      e_WriteLog('[Chat] ' + b_Text_Unformat(Txt), TMsgType.Notify);
+      g_Game_ChatSound(b_Text_Unformat(Txt));
     end else
     if (Mode = NET_CHAT_TEAM) and (gPlayer1 <> nil) then
     begin
@@ -1473,8 +1443,8 @@ begin
         g_Console_Add(b_Text_Format('\r[Team] ') + Txt, True);
       if gPlayer1.Team = TEAM_BLUE then
         g_Console_Add(b_Text_Format('\b[Team] ') + Txt, True);
-      e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
-      g_Sound_PlayEx('SOUND_GAME_RADIO');
+      e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), TMsgType.Notify);
+      g_Game_ChatSound(b_Text_Unformat(Txt));
     end;
   end else
     g_Console_Add(Txt, True);
@@ -1726,7 +1696,7 @@ begin
       gWADHash := EvHash;
       if not g_Game_StartMap(EvStr, True) then
       begin
-        if Pos(':\', EvStr) = 0 then
+        if not isWadPath(EvStr) then
           g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [gGameSettings.WAD + ':\' + EvStr]))
         else
           g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [EvStr]));
@@ -2051,7 +2021,7 @@ begin
   end;
 
   g_Console_Add(Format(_lc[I_PLAYER_JOIN], [PName]), True);
-  e_WriteLog('NET: Player ' + PName + ' [' + IntToStr(PID) + '] added.', MSG_NOTIFY);
+  e_WriteLog('NET: Player ' + PName + ' [' + IntToStr(PID) + '] added.', TMsgType.Notify);
   Result := PID;
 end;
 
@@ -2073,6 +2043,8 @@ begin
     Exit;
   end;
   gTime := GT;
+  if g_Game_IsClient and (gPlayer1 <> nil) and (gPlayer1.NetForceWeapFIdx >= gTime+15) then gPlayer1.NetForceWeapFIdx := 0;
+  if g_Game_IsClient and (gPlayer2 <> nil) and (gPlayer2.NetForceWeapFIdx >= gTime+15) then gPlayer2.NetForceWeapFIdx := 0;
 
   PID := M.ReadWord();
   Pl := g_Player_Get(PID);
@@ -2091,7 +2063,7 @@ begin
     TmpX := M.ReadLongInt();
     TmpY := M.ReadLongInt();
 
-    ReleaseKeys;
+    ReleaseKeysNoWeapon;
 
     if (kByte = NET_KEY_CHAT) then
       PressKey(KEY_CHAT, 10000)
@@ -2123,6 +2095,8 @@ var
   I: Integer;
   OldJet: Boolean;
   NewTeam: Byte;
+  ww: Word;
+  newweapon: Byte;
 begin
   PID := M.ReadWord();
   Pl := g_Player_Get(PID);
@@ -2132,7 +2106,7 @@ begin
 
   with Pl do
   begin
-    Live := (M.ReadByte() <> 0);
+    alive := (M.ReadByte() <> 0);
     GodMode := (M.ReadByte() <> 0);
     Health := M.ReadLongInt();
     Armor := M.ReadLongInt();
@@ -2141,8 +2115,12 @@ begin
     Lives := M.ReadByte();
     NewTeam := M.ReadByte();
 
+    ww := M.ReadWord();
     for I := WP_FIRST to WP_LAST do
-      FWeapon[I] := (M.ReadByte() <> 0);
+    begin
+      FWeapon[I] := ((ww and $01) <> 0);
+      ww := ww shr 1;
+    end;
 
     for I := A_BULLETS to A_HIGH do
       FAmmo[I] := M.ReadWord();
@@ -2154,23 +2132,32 @@ begin
       FMegaRulez[I] := M.ReadLongWord();
 
     FRulez := [];
-    if (M.ReadByte() <> 0) then
-      FRulez := FRulez + [R_ITEM_BACKPACK];
-    if (M.ReadByte() <> 0) then
-      FRulez := FRulez + [R_KEY_RED];
-    if (M.ReadByte() <> 0) then
-      FRulez := FRulez + [R_KEY_GREEN];
-    if (M.ReadByte() <> 0) then
-      FRulez := FRulez + [R_KEY_BLUE];
-    if (M.ReadByte() <> 0) then
-      FRulez := FRulez + [R_BERSERK];
+    // unpack special flags
+    ww := M.ReadByte();
+    if ((ww and $01) <> 0) then FRulez := FRulez+[R_ITEM_BACKPACK];
+    if ((ww and $02) <> 0) then FRulez := FRulez+[R_KEY_RED];
+    if ((ww and $04) <> 0) then FRulez := FRulez+[R_KEY_GREEN];
+    if ((ww and $08) <> 0) then FRulez := FRulez+[R_KEY_BLUE];
+    if ((ww and $10) <> 0) then FRulez := FRulez+[R_BERSERK];
 
     Frags := M.ReadLongInt();
     Death := M.ReadLongInt();
 
-    SetWeapon(M.ReadByte());
+    newweapon := M.ReadByte();
+{$IFDEF K8_XXX_WEAPON_DEBUG}
+    writeln('CLIENT PLRSTATS: got: currweap=', CurrWeap, '; curfrm=', gTime, '; netweap=', newweapon, '; lastnwfrm=', NetForceWeapFIdx);
+{$ENDIF}
+    if (gTime >= NetForceWeapFIdx) then
+    begin
+      //NetForceWeap := newweapon;
+      SetWeapon(newweapon);
+    end;
+    //SetWeapon(M.ReadByte());
+
+    // other flags
+    ww := M.ReadByte();
 
-    FSpectator := M.ReadByte() <> 0;
+    FSpectator := ((ww and $01) <> 0);
     if FSpectator then
     begin
       if Pl = gPlayer1 then
@@ -2191,11 +2178,12 @@ begin
       if (gPlayer2 = nil) and (gLMSPID2 > 0) then
         gPlayer2 := g_Player_Get(gLMSPID2);
     end;
-    FGhost := M.ReadByte() <> 0;
-    FPhysics := M.ReadByte() <> 0;
-    FNoRespawn := M.ReadByte() <> 0;
+
+    FGhost := ((ww and $02) <> 0);
+    FPhysics := ((ww and $04) <> 0);
+    FNoRespawn := ((ww and $08) <> 0);
     OldJet := FJetpack;
-    FJetpack := M.ReadByte() <> 0;
+    FJetpack := ((ww and $10) <> 0);
     FFireTime := M.ReadLongInt();
     if OldJet and not FJetpack then
       JetpackOff
@@ -2269,7 +2257,7 @@ begin
   if Pl = nil then Exit;
 
   g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [Pl.Name]), True);
-  e_WriteLog('NET: Player ' + Pl.Name + ' [' + IntToStr(PID) + '] removed.', MSG_NOTIFY);
+  e_WriteLog('NET: Player ' + Pl.Name + ' [' + IntToStr(PID) + '] removed.', TMsgType.Notify);
 
   g_Player_Remove(PID);
 
@@ -2298,7 +2286,7 @@ begin
   SHID := M.ReadLongInt();
 
   with Pl do
-    if Live then NetFire(Weap, X, Y, AX, AY, SHID);
+    if alive then NetFire(Weap, X, Y, AX, AY, SHID);
 end;
 
 procedure MC_RECV_PlayerSettings(var M: TMsg);
@@ -2364,7 +2352,8 @@ begin
   VX := M.ReadLongInt();
   VY := M.ReadLongInt();
 
-  g_Items_Create(X, Y, T, Fall, False, False, ID);
+  g_Items_Create(X, Y, T and $7F, Fall, False, False, ID);
+  if ((T and $80) <> 0) then g_Items_SetDrop(ID);
 
   it := g_Items_ByIdx(ID);
   it.Obj.Vel.X := VX;
@@ -2403,94 +2392,89 @@ end;
 procedure MC_RECV_PanelTexture(var M: TMsg);
 var
   TP: TPanel;
-  PType: Word;
-  ID: LongWord;
+  PGUID: Integer;
   Tex, Fr: Integer;
   Loop, Cnt: Byte;
 begin
   if not gGameOn then Exit;
-  PType := M.ReadWord();
-  ID := M.ReadLongWord();
+
+  PGUID := Integer(M.ReadLongWord());
   Tex := M.ReadLongInt();
   Fr := M.ReadLongInt();
   Cnt := M.ReadByte();
   Loop := M.ReadByte();
 
-  TP := nil;
-
-  case PType of
-    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
-     if gWalls <> nil then
-      TP := gWalls[ID];
-    PANEL_FORE:
-     if gRenderForegrounds <> nil then
-      TP := gRenderForegrounds[ID];
-    PANEL_BACK:
-     if gRenderBackgrounds <> nil then
-      TP := gRenderBackgrounds[ID];
-    PANEL_WATER:
-     if gWater <> nil then
-      TP := gWater[ID];
-    PANEL_ACID1:
-     if gAcid1 <> nil then
-      TP := gAcid1[ID];
-    PANEL_ACID2:
-     if gAcid2 <> nil then
-      TP := gAcid2[ID];
-    PANEL_STEP:
-     if gSteps <> nil then
-      TP := gSteps[ID];
-    else
-      Exit;
+  TP := g_Map_PanelByGUID(PGUID);
+  if (TP <> nil) then
+  begin
+    // switch texture
+    TP.SetTexture(Tex, Loop);
+    TP.SetFrame(Fr, Cnt);
   end;
-
-  if TP <> nil then
-    if Loop = 0 then
-    begin    // switch texture
-      TP.SetTexture(Tex, Loop);
-      TP.SetFrame(Fr, Cnt);
-    end else // looped or non-looped animation
-      TP.NextTexture(Loop);
 end;
 
 procedure MC_RECV_PanelState(var M: TMsg);
 var
-  ID: LongWord;
+  PGUID: Integer;
   E: Boolean;
   Lift: Byte;
-  PType: Word;
-  X, Y: Integer;
+  X, Y, W, H: Integer;
+  TP: TPanel;
+  speedX, speedY, startX, startY, endX, endY: Integer;
+  sizeSpX, sizeSpY, sizeEX, sizeEY: Integer;
+  mpflags: Byte;
 begin
   if not gGameOn then Exit;
-  PType := M.ReadWord();
-  ID := M.ReadLongWord();
+
+  PGUID := Integer(M.ReadLongWord());
   E := (M.ReadByte() <> 0);
   Lift := M.ReadByte();
   X := M.ReadLongInt();
   Y := M.ReadLongInt();
-
-  case PType of
-    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
-      if E then
-        g_Map_EnableWall(ID)
-      else
-        g_Map_DisableWall(ID);
-
-    PANEL_LIFTUP, PANEL_LIFTDOWN, PANEL_LIFTLEFT, PANEL_LIFTRIGHT:
-      g_Map_SetLift(ID, Lift);
-
-    PANEL_BACK:
-    begin
-      gRenderBackgrounds[ID].X := X;
-      gRenderBackgrounds[ID].Y := Y;
-    end;
-
-    PANEL_FORE:
-    begin
-      gRenderForegrounds[ID].X := X;
-      gRenderForegrounds[ID].Y := Y;
-    end;      
-  end;
+  W := M.ReadWord();
+  H := M.ReadWord();
+  // mplats
+  speedX := M.ReadLongInt();
+  speedY := M.ReadLongInt();
+  startX := M.ReadLongInt();
+  startY := M.ReadLongInt();
+  endX := M.ReadLongInt();
+  endY := M.ReadLongInt();
+  sizeSpX := M.ReadLongInt();
+  sizeSpY := M.ReadLongInt();
+  sizeEX := M.ReadLongInt();
+  sizeEY := M.ReadLongInt();
+  mpflags := M.ReadByte(); // bit0: TP.movingActive; bit1: TP.moveOnce
+
+  TP := g_Map_PanelByGUID(PGUID);
+  if (TP = nil) then exit;
+
+  // update lifts state
+  if TP.isGLift then g_Map_SetLiftGUID(PGUID, Lift);
+
+  // update enabled/disabled state for all panels
+  if E then g_Map_EnableWallGUID(PGUID) else g_Map_DisableWallGUID(PGUID);
+
+  // update panel position, as it can be moved (mplat)
+  TP.X := X;
+  TP.Y := Y;
+  TP.Width := W;
+  TP.Height := H;
+  // update mplat state
+  TP.movingSpeedX := speedX;
+  TP.movingSpeedY := speedY;
+  TP.movingStartX := startX;
+  TP.movingStartY := startY;
+  TP.movingEndX := endX;
+  TP.movingEndY := endY;
+  TP.sizeSpeedX := sizeSpX;
+  TP.sizeSpeedY := sizeSpY;
+  TP.sizeEndX := sizeEX;
+  TP.sizeEndY := sizeEY;
+  TP.movingActive := ((mpflags and 1) <> 0);
+  TP.moveOnce := ((mpflags and 2) <> 0);
+  // notify panel of it's position/size change, so it can fix other internal structures
+  TP.positionChanged();
 end;
 
 // TRIGGERS
@@ -2515,16 +2499,19 @@ begin
       if gTriggers[I].ClientID = SID then
         with gTriggers[I] do
         begin
-          if SPlaying then
+          if Sound <> nil then
           begin
-            if Data.Local then
-              Sound.PlayVolumeAt(X+(Width div 2), Y+(Height div 2), Data.Volume/255.0)
+            if SPlaying then
+            begin
+              if tgcLocal then
+                Sound.PlayVolumeAt(X+(Width div 2), Y+(Height div 2), tgcVolume/255.0)
+              else
+                Sound.PlayPanVolume((tgcPan-127.0)/128.0, tgcVolume/255.0);
+              Sound.SetPosition(SPos);
+            end
             else
-              Sound.PlayPanVolume((Data.Pan-127.0)/128.0, Data.Volume/255.0);
-            Sound.SetPosition(SPos);
-          end
-          else
-            if Sound.IsPlaying then Sound.Stop;
+              if Sound.IsPlaying then Sound.Stop;
+          end;
 
           SoundPlayCount := SCount;
         end;
@@ -2593,10 +2580,6 @@ begin
 
   with Mon do
   begin
-    GameX := X;
-    GameY := Y;
-    GameVelX := VX;
-    GameVelY := VY;
 
     MonsterAnim := MAnim;
     MonsterTargetUID := MTarg;
@@ -2608,7 +2591,9 @@ begin
 
     SetState(MState);
 
-    positionChanged(); // this updates spatial accelerators
+    setPosition(X, Y); // this will call positionChanged();
+    GameVelX := VX;
+    GameVelY := VY;
   end;
 end;
 
@@ -2616,6 +2601,7 @@ procedure MC_RECV_MonsterPos(var M: TMsg);
 var
   Mon: TMonster;
   ID: Word;
+  X, Y: Integer;
 begin
   ID := M.ReadWord();
   Mon := g_Monsters_ByUID(ID);
@@ -2624,12 +2610,12 @@ begin
 
   with Mon do
   begin
-    GameX := M.ReadLongInt();
-    GameY := M.ReadLongInt();
+    X := M.ReadLongInt();
+    Y := M.ReadLongInt();
+    Mon.setPosition(X, Y); // this will call `positionChanged()`
     GameVelX := M.ReadLongInt();
     GameVelY := M.ReadLongInt();
     GameDirection := TDirection(M.ReadByte());
-    positionChanged(); // this updates spatial accelerators
   end;
 end;
 
@@ -2661,16 +2647,11 @@ begin
 
     if MonsterState <> MState then
     begin
-      if (MState = MONSTATE_GO) and (MonsterState = MONSTATE_SLEEP) then
-        WakeUpSound;
-      if (MState = MONSTATE_DIE) then
-        DieSound;
-      if (MState = MONSTATE_PAIN) then
-        MakeBloodSimple(Min(200, MonsterPain));
-      if (MState = MONSTATE_ATTACK) then
-        kick(nil);
-      if (MState = MONSTATE_DEAD) then
-        SetDeadAnim;
+      if (MState = MONSTATE_GO) and (MonsterState = MONSTATE_SLEEP) then WakeUpSound();
+      if (MState = MONSTATE_DIE) then DieSound();
+      if (MState = MONSTATE_PAIN) then MakeBloodSimple(Min(200, MonsterPain));
+      if (MState = MONSTATE_ATTACK) then kick(nil);
+      if (MState = MONSTATE_DEAD) then SetDeadAnim();
 
       SetState(MState, MFAnm);
     end;
@@ -2784,7 +2765,7 @@ var
   kByte: Word;
   Predict: Boolean;
   strafeDir: Byte;
-  WeaponSelect: Word = 0;
+  //WeaponSelect: Word = 0;
   I: Integer;
 begin
   if not gGameOn then Exit;
@@ -2810,17 +2791,20 @@ begin
         // new strafe mechanics
         if (strafeDir = 0) then strafeDir := P1MoveButton; // start strafing
         // now set direction according to strafe (reversed)
-             if (strafeDir = 2) then gPlayer1.SetDirection(D_LEFT)
-        else if (strafeDir = 1) then gPlayer1.SetDirection(D_RIGHT);
+             if (strafeDir = 2) then gPlayer1.SetDirection(TDirection.D_LEFT)
+        else if (strafeDir = 1) then gPlayer1.SetDirection(TDirection.D_RIGHT);
       end
       else
       begin
-             if (P1MoveButton = 2) and isKeyPressed(KeyLeft, KeyLeft2) then gPlayer1.SetDirection(D_LEFT)
-        else if (P1MoveButton = 1) and isKeyPressed(KeyRight, KeyRight2) then gPlayer1.SetDirection(D_RIGHT)
+             if (P1MoveButton = 2) and isKeyPressed(KeyLeft, KeyLeft2) then gPlayer1.SetDirection(TDirection.D_LEFT)
+        else if (P1MoveButton = 1) and isKeyPressed(KeyRight, KeyRight2) then gPlayer1.SetDirection(TDirection.D_RIGHT)
         else if P1MoveButton <> 0 then gPlayer1.SetDirection(TDirection(P1MoveButton-1));
       end;
 
       gPlayer1.ReleaseKeys;
+      gPlayer1.weaponSwitchKeysStateChange(-1, isKeyPressed(KeyNextWeapon, KeyNextWeapon2));
+      gPlayer1.weaponSwitchKeysStateChange(-2, isKeyPressed(KeyPrevWeapon, KeyPrevWeapon2));
+
       if P1MoveButton = 1 then
       begin
         kByte := kByte or NET_KEY_LEFT;
@@ -2848,11 +2832,25 @@ begin
       end;
       if isKeyPressed(KeyFire, KeyFire2) then kByte := kByte or NET_KEY_FIRE;
       if isKeyPressed(KeyOpen, KeyOpen2) then kByte := kByte or NET_KEY_OPEN;
-      if isKeyPressed(KeyNextWeapon, KeyNextWeapon2) then kByte := kByte or NET_KEY_NW;
-      if isKeyPressed(KeyPrevWeapon, KeyPrevWeapon2) then kByte := kByte or NET_KEY_PW;
+      // do not send weapon switch keys, `MH_SEND_PlayerStats()` will send changed weapon anyway
+      if isKeyPressed(KeyNextWeapon, KeyNextWeapon2) and gPlayer1.isWeaponSwitchKeyReleased(-1) then gPlayer1.PressKey(KEY_NEXTWEAPON); //kByte := kByte or NET_KEY_NW;
+      if isKeyPressed(KeyPrevWeapon, KeyPrevWeapon2) and gPlayer1.isWeaponSwitchKeyReleased(-2) then gPlayer1.PressKey(KEY_PREVWEAPON); //kByte := kByte or NET_KEY_PW;
       for I := 0 to High(KeyWeapon) do
+      begin
         if isKeyPressed(KeyWeapon[I], KeyWeapon2[I]) then
-          WeaponSelect := WeaponSelect or Word(1 shl I);
+        begin
+          gPlayer1.weaponSwitchKeysStateChange(i, true);
+          if gPlayer1.isWeaponSwitchKeyReleased(i) then
+          begin
+            gPlayer1.QueueWeaponSwitch(i); // all choices are passed there, and god will take the best
+            //WeaponSelect := WeaponSelect or Word(1 shl I);
+          end;
+        end
+        else
+        begin
+          gPlayer1.weaponSwitchKeysStateChange(i, false);
+        end;
+      end;
     end;
     // fix movebutton state
     P1MoveButton := P1MoveButton or (strafeDir shl 4);
@@ -2860,14 +2858,24 @@ begin
   else
     kByte := NET_KEY_CHAT;
 
+  gPlayer1.weaponSwitchKeysShiftNewStates();
+
   NetOut.Write(Byte(NET_MSG_PLRPOS));
   NetOut.Write(gTime);
   NetOut.Write(kByte);
   NetOut.Write(Byte(gPlayer1.Direction));
-  NetOut.Write(WeaponSelect);
+  NetOut.Write(Byte(gPlayer1.CurrWeap));
+{$IFDEF K8_XXX_WEAPON_DEBUG}
+  if (kByte and NET_KEY_FIRE) <> 0 then writeln('FIRE: CurrWeap=', gPlayer1.CurrWeap);
+{$ENDIF}
+  //e_WriteLog(Format('S:nwp=%d; nwfidx=%d', [Integer(gPlayer1.NetForceWeap), Integer(gPlayer1.NetForceWeapFIdx)]), TMsgType.Warning);
+  //NetOut.Write(WeaponSelect);
   //e_WriteLog(Format('S:ws=%d', [WeaponSelect]), MSG_WARNING);
   g_Net_Client_Send(True, NET_CHAN_PLAYERPOS);
 
+  // force player weapon
+  //{if (gPlayer1.NetForceWeapFIdx < gTime) then} gPlayer1.SetWeapon(gPlayer1.NetForceWeap);
+
   //kBytePrev := kByte;
   //kDirPrev := gPlayer1.Direction;
 end;
@@ -2929,15 +2937,15 @@ var
   FileStream : TStream;
   fname: string;
 begin
-  e_WriteLog(Format('NETWORK: looking for file "%s"', [FileName]), MSG_NOTIFY);
+  e_WriteLog(Format('NETWORK: looking for file "%s"', [FileName]), TMsgType.Notify);
   fname := findDiskWad(FileName);
   if length(fname) = 0 then
   begin
-    e_WriteLog(Format('NETWORK: file "%s" not found!', [FileName]), MSG_FATALERROR);
+    e_WriteLog(Format('NETWORK: file "%s" not found!', [FileName]), TMsgType.Fatal);
     SetLength(Result, 0);
     exit;
   end;
-  e_WriteLog(Format('NETWORK: found file "%s"', [fname]), MSG_NOTIFY);
+  e_WriteLog(Format('NETWORK: found file "%s"', [fname]), TMsgType.Notify);
   Result := nil;
   FileStream := openDiskFileRO(fname);
   try
@@ -3076,10 +3084,10 @@ var
   mapDataMsg: TMapDataMsg;
 begin
   e_WriteLog('NET: Received map request from ' +
-             DecodeIPV4(C.Peer.address.host), MSG_NOTIFY);
+             DecodeIPV4(C^.Peer.address.host), TMsgType.Notify);
 
   mapDataMsg := CreateMapDataMsg(MapsDir + gGameSettings.WAD, gExternalResources);
-  peer := NetClients[C.ID].Peer;
+  peer := NetClients[C^.ID].Peer;
 
   MapDataMsgToBytes(payload, mapDataMsg);
   g_Net_SendData(payload, peer, True, NET_CHAN_DOWNLOAD);
@@ -3098,15 +3106,15 @@ var
 begin
   FileName := ExtractFileName(M.ReadString());
   e_WriteLog('NET: Received res request: ' + FileName +
-             ' from ' + DecodeIPV4(C.Peer.address.host), MSG_NOTIFY);
+             ' from ' + DecodeIPV4(C^.Peer.address.host), TMsgType.Notify);
 
   if not IsValidFilePath(FileName) then
   begin
-    e_WriteLog('Invalid filename: ' + FileName, MSG_WARNING);
+    e_WriteLog('Invalid filename: ' + FileName, TMsgType.Warning);
     exit;
   end;
 
-  peer := NetClients[C.ID].Peer;
+  peer := NetClients[C^.ID].Peer;
 
   if gExternalResources.IndexOf(FileName) > -1 then
   begin