DEADSOFTWARE

net: add 'an' as an alias for announce
[d2df-sdl.git] / src / game / g_game.pas
index ab843d3c8ca2892df532db9d556978f4e22a7539..d1246ba0c7dabdc027035501fe4fee5bd9bfc6a7 100644 (file)
@@ -1,9 +1,8 @@
-(* 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * the Free Software Foundation, version 3 of the License ONLY.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -22,7 +21,8 @@ uses
   SysUtils, Classes,
   MAPDEF,
   g_basic, g_player, e_graphics, g_res_downloader,
-  g_sound, g_gui, wadreader, md5, xprofiler;
+  g_sound, g_gui, utils, md5, mempool, xprofiler,
+  g_touch, g_weapons;
 
 type
   TGameSettings = record
@@ -31,6 +31,8 @@ type
     TimeLimit: Word;
     GoalLimit: Word;
     WarmupTime: Word;
+    SpawnInvul: Word;
+    ItemRespawnTime: Word;
     MaxLives: Byte;
     Options: LongWord;
     WAD: String;
@@ -49,11 +51,22 @@ type
     DEStr: String;
   end;
 
+  TChatSound = record
+    Sound: TPlayableSound;
+    Tags: Array of String;
+    FullWord: Boolean;
+  end;
+
   TPlayerSettings = record
     Name: String;
     Model: String;
     Color: TRGB;
     Team: Byte;
+    // ones below are sent only to the server
+    WeaponSwitch: Byte;
+    WeaponPreferences: Array[WP_FIRST..WP_LAST+1] of Byte;
+    SwitchToEmpty: Byte;
+    SkipFist: Byte;
   end;
 
   TMegaWADInfo = record
@@ -76,6 +89,7 @@ procedure g_Game_Free (freeTextures: Boolean=true);
 procedure g_Game_LoadData();
 procedure g_Game_FreeData();
 procedure g_Game_Update();
+procedure g_Game_PreUpdate();
 procedure g_Game_Draw();
 procedure g_Game_Quit();
 procedure g_Game_SetupScreenSize();
@@ -95,9 +109,8 @@ procedure g_Game_StartClient(Addr: String; Port: Word; PW: String);
 procedure g_Game_Restart();
 procedure g_Game_RestartLevel();
 procedure g_Game_RestartRound(NoMapRestart: Boolean = False);
-procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest);
-procedure g_Game_SaveOptions();
-function  g_Game_StartMap(Map: String; Force: Boolean = False; const oldMapPath: AnsiString=''): Boolean;
+function  g_Game_ClientWAD (NewWAD: String; const WHash: TMD5Digest): AnsiString;
+function  g_Game_StartMap(asMegawad: Boolean; Map: String; Force: Boolean = False; const oldMapPath: AnsiString=''): Boolean;
 procedure g_Game_ChangeMap(const MapPath: String);
 procedure g_Game_ExitLevel(const Map: AnsiString);
 function  g_Game_GetFirstMap(WAD: String): String;
@@ -114,25 +127,30 @@ procedure g_Game_PauseAllSounds(Enable: Boolean);
 procedure g_Game_StopAllSounds(all: Boolean);
 procedure g_Game_UpdateTriggerSounds();
 function  g_Game_GetMegaWADInfo(WAD: String): TMegaWADInfo;
+procedure g_Game_ChatSound(Text: String; Taunt: Boolean = True);
 procedure g_Game_Announce_GoodShot(SpawnerUID: Word);
 procedure g_Game_Announce_KillCombo(Param: Integer);
+procedure g_Game_Announce_BodyKill(SpawnerUID: Word);
 procedure g_Game_StartVote(Command, Initiator: string);
 procedure g_Game_CheckVote;
-procedure g_TakeScreenShot();
+procedure g_TakeScreenShot(Filename: string = '');
 procedure g_FatalError(Text: String);
 procedure g_SimpleError(Text: String);
 function  g_Game_IsTestMap(): Boolean;
 procedure g_Game_DeleteTestMap();
-procedure GameCVars(P: SArray);
-procedure GameCommands(P: SArray);
-procedure GameCheats(P: SArray);
-procedure DebugCommands(P: SArray);
+procedure GameCVars(P: SSArray);
+procedure PlayerSettingsCVars(P: SSArray);
+procedure SystemCommands(P: SSArray);
+procedure GameCommands(P: SSArray);
+procedure GameCheats(P: SSArray);
+procedure DebugCommands(P: SSArray);
 procedure g_Game_Process_Params;
 procedure g_Game_SetLoadingText(Text: String; Max: Integer; reWrite: Boolean);
-procedure g_Game_StepLoading();
+procedure g_Game_StepLoading(Value: Integer = -1);
 procedure g_Game_ClearLoading();
 procedure g_Game_SetDebugMode();
 procedure DrawLoadingStat();
+procedure DrawMenuBackground(tex: AnsiString);
 
 { procedure SetWinPause(Enable: Boolean); }
 
@@ -163,13 +181,19 @@ const
   EXIT_ENDLEVELSINGLE  = 4;
   EXIT_ENDLEVELCUSTOM  = 5;
 
-  GAME_OPTION_RESERVED     = 1;
-  GAME_OPTION_TEAMDAMAGE   = 2;
-  GAME_OPTION_ALLOWEXIT    = 4;
-  GAME_OPTION_WEAPONSTAY   = 8;
-  GAME_OPTION_MONSTERS     = 16;
-  GAME_OPTION_BOTVSPLAYER  = 32;
-  GAME_OPTION_BOTVSMONSTER = 64;
+  GAME_OPTION_RESERVED          = 1;
+  GAME_OPTION_TEAMDAMAGE        = 2;
+  GAME_OPTION_ALLOWEXIT         = 4;
+  GAME_OPTION_WEAPONSTAY        = 8;
+  GAME_OPTION_MONSTERS          = 16;
+  GAME_OPTION_BOTVSPLAYER       = 32;
+  GAME_OPTION_BOTVSMONSTER      = 64;
+  GAME_OPTION_DMKEYS            = 128;
+  GAME_OPTION_TEAMHITTRACE      = 256;
+  GAME_OPTION_TEAMHITPROJECTILE = 512;
+  GAME_OPTION_TEAMABSORBDAMAGE  = 1024;
+  GAME_OPTION_ALLOWDROPFLAG     = 2048;
+  GAME_OPTION_THROWFLAG         = 4096;
 
   STATE_NONE        = 0;
   STATE_MENU        = 1;
@@ -193,6 +217,7 @@ const
   DE_GLOBEVENT = 0;
   DE_BFGHIT    = 1;
   DE_KILLCOMBO = 2;
+  DE_BODYKILL  = 3;
 
   ANNOUNCE_NONE   = 0;
   ANNOUNCE_ME     = 1;
@@ -200,12 +225,19 @@ const
   ANNOUNCE_ALL    = 3;
 
   CONFIG_FILENAME = 'Doom2DF.cfg';
-  LOG_FILENAME = 'Doom2DF.log';
 
   TEST_MAP_NAME = '$$$_TEST_$$$';
 
   STD_PLAYER_MODEL = 'Doomer';
 
+{$IFDEF HEADLESS}
+  DEFAULT_PLAYERS = 0;
+{$ELSE}
+  DEFAULT_PLAYERS = 1;
+{$ENDIF}
+
+  STATFILE_VERSION = $03;
+
 var
   gStdFont: DWORD;
   gGameSettings: TGameSettings;
@@ -219,13 +251,20 @@ var
   gPlayer2: TPlayer = nil;
   gPlayerDrawn: TPlayer = nil;
   gTime: LongWord;
+  gLerpFactor: Single = 1.0;
   gSwitchGameMode: Byte = GM_DM;
   gHearPoint1, gHearPoint2: THearPoint;
   gSoundEffectsDF: Boolean = False;
   gSoundTriggerTime: Word = 0;
-  gAnnouncer: Byte = ANNOUNCE_NONE;
+  gAnnouncer: Integer = ANNOUNCE_NONE;
   goodsnd: array[0..3] of TPlayableSound;
   killsnd: array[0..3] of TPlayableSound;
+  hahasnd: array[0..2] of TPlayableSound;
+  sound_get_flag: array[0..1] of TPlayableSound;
+  sound_lost_flag: array[0..1] of TPlayableSound;
+  sound_ret_flag: array[0..1] of TPlayableSound;
+  sound_cap_flag: array[0..1] of TPlayableSound;
+  gBodyKillEvent: Integer = -1;
   gDefInterTime: ShortInt = -1;
   gInterEndTime: LongWord = 0;
   gInterTime: LongWord = 0;
@@ -234,10 +273,11 @@ var
   gTotalMonsters: Integer = 0;
   gPauseMain: Boolean = false;
   gPauseHolmes: Boolean = false;
-  gShowTime: Boolean = True;
+  gShowTime: Boolean = False;
   gShowFPS: Boolean = False;
   gShowGoals: Boolean = True;
   gShowStat: Boolean = True;
+  gShowPIDs: Boolean = False;
   gShowKillMsg: Boolean = True;
   gShowLives: Boolean = True;
   gShowPing: Boolean = False;
@@ -255,6 +295,10 @@ var
   gSpectViewTwo: Boolean = False;
   gSpectPID1: Integer = -1;
   gSpectPID2: Integer = -1;
+  gSpectAuto: Boolean = False;
+  gSpectAutoNext: LongWord;
+  gSpectAutoStepX: Integer;
+  gSpectAutoStepY: Integer;
   gMusic: TMusic = nil;
   gLoadGameMode: Boolean;
   gCheats: Boolean = False;
@@ -262,12 +306,10 @@ var
   gMapToDelete: String;
   gTempDelete: Boolean = False;
   gLastMap: Boolean = False;
-  gWinPosX, gWinPosY: Integer;
-  gWinSizeX, gWinSizeY: Integer;
-  gWinFrameX, gWinFrameY, gWinCaption: Integer;
-  gWinActive: Boolean = True; // by default window is active, lol
+  gScreenWidth: Word;
+  gScreenHeight: Word;
   gResolutionChange: Boolean = False;
-  gRC_Width, gRC_Height: Word;
+  gRC_Width, gRC_Height: Integer;
   gRC_FullScreen, gRC_Maximized: Boolean;
   gLanguageChange: Boolean = False;
   gDebugMode: Boolean = False;
@@ -303,6 +345,15 @@ var
   gVotesEnabled: Boolean = True;
   gEvents: Array of TGameEvent;
   gDelayedEvents: Array of TDelayedEvent;
+  gUseChatSounds: Boolean = True;
+  gChatSounds: Array of TChatSound;
+  gWeaponAction: Array [0..1, WP_FACT..WP_LACT] of Boolean; // [player, weapon_action]
+  gSelectWeapon: Array [0..1, WP_FIRST..WP_LAST] of Boolean; // [player, weapon]
+  gInterReadyCount: Integer = 0;
+
+  g_dbg_ignore_bounds: Boolean = false;
+  r_smallmap_h: Integer = 0; // 0: left; 1: center; 2: right
+  r_smallmap_v: Integer = 2; // 0: top; 1: center; 2: bottom
 
   // move button values:
   // bits 0-1: l/r state:
@@ -340,13 +391,21 @@ function gPause (): Boolean; inline;
 implementation
 
 uses
-  e_texture, g_textures, g_main, g_window, g_menu,
+{$INCLUDE ../nogl/noGLuses.inc}
+{$IFDEF ENABLE_HOLMES}
+  g_holmes,
+{$ENDIF}
+  e_texture, e_res, g_textures, g_window, g_menu,
   e_input, e_log, g_console, g_items, g_map, g_panel,
-  g_playermodel, g_gfx, g_options, g_weapons, Math,
+  g_playermodel, g_gfx, g_options, Math,
   g_triggers, g_monsters, e_sound, CONFIG,
-  g_language, g_net, SDL,
-  ENet, e_msg, g_netmsg, g_netmaster, GL, GLExt,
-  utils, sfs, g_holmes;
+  g_language, g_net, g_main, g_phys,
+  ENet, e_msg, g_netmsg, g_netmaster,
+  sfs, wadreader, g_system;
+
+
+var
+  hasPBarGfx: Boolean = false;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
@@ -513,6 +572,7 @@ type
     ShowCount: Integer;
     Msgs: Array of String;
     NextMsg: Word;
+    PBarWasHere: Boolean; // did we draw a progress bar for this message?
   end;
 
   TParamStrValue = record
@@ -532,7 +592,6 @@ var
   FPSCounter, UPSCounter: Word;
   FPSTime, UPSTime: LongWord;
   DataLoaded: Boolean = False;
-  LastScreenShot: Int64;
   IsDrawStat: Boolean = False;
   CustomStat: TEndCustomGameStat;
   SingleStat: TEndSingleGameStat;
@@ -540,8 +599,13 @@ var
   EndingGameCounter: Byte = 0;
   MessageText: String;
   MessageTime: Word;
-  MapList: SArray = nil;
+  MessageLineLength: Integer = 80;
+  MapList: SSArray = nil;
   MapIndex: Integer = -1;
+  InterReadyTime: Integer = -1;
+  StatShotDone: Boolean = False;
+  StatFilename: string = ''; // used by stat screenshot to save with the same name as the csv
+  StatDate: string = '';
   MegaWAD: record
     info: TMegaWADInfo;
     endpic: String;
@@ -563,7 +627,7 @@ var
   end;
   //InterPic: String;
   InterText: record
-    lines: SArray;
+    lines: SSArray;
     img: String;
     cur_line: Integer;
     cur_char: Integer;
@@ -600,6 +664,68 @@ begin
       end;
 end;
 
+// saves a shitty CSV containing the game stats passed to it
+procedure SaveGameStat(Stat: TEndCustomGameStat; Path: string);
+var 
+  s: TextFile;
+  dir, fname, map, mode, etime: String;
+  I: Integer;
+begin
+  try
+    dir := e_GetWriteableDir(StatsDirs);
+    // stats are placed in stats/yy/mm/dd/*.csv
+    fname := e_CatPath(dir, Path);
+    ForceDirectories(fname); // ensure yy/mm/dd exists within the stats dir
+    fname := e_CatPath(fname, StatFilename + '.csv');
+    AssignFile(s, fname);
+    try
+      Rewrite(s);
+      // line 1: stats ver, datetime, server name, map name, game mode, time limit, score limit, dmflags, game time, num players
+      if g_Game_IsNet then fname := NetServerName else fname := '';
+      map := g_ExtractWadNameNoPath(gMapInfo.Map) + ':/' + g_ExtractFileName(gMapInfo.Map);
+      mode := g_Game_ModeToText(Stat.GameMode);
+      etime := Format('%d:%.2d:%.2d', [
+        Stat.GameTime div 1000 div 3600,
+        (Stat.GameTime div 1000 div 60) mod 60,
+        Stat.GameTime div 1000 mod 60
+      ]);
+      WriteLn(s, 'stats_ver,datetime,server,map,mode,timelimit,scorelimit,dmflags,time,num_players');
+      WriteLn(s, Format('%d,%s,%s,%s,%s,%u,%u,%u,%s,%d', [
+        STATFILE_VERSION,
+        StatDate,
+        dquoteStr(fname),
+        dquoteStr(map),
+        mode,
+        gGameSettings.TimeLimit,
+        gGameSettings.GoalLimit,
+        gGameSettings.Options,
+        etime,
+        Length(Stat.PlayerStat)
+      ]));
+      // line 2: game specific shit
+      //   if it's a team game: red score, blue score
+      //   if it's a coop game: monsters killed, monsters total, secrets found, secrets total
+      //   otherwise nothing
+      if Stat.GameMode in [GM_TDM, GM_CTF] then
+        WriteLn(s, 
+          Format('red_score,blue_score' + LineEnding + '%d,%d', [Stat.TeamStat[TEAM_RED].Goals, Stat.TeamStat[TEAM_BLUE].Goals]))
+      else if Stat.GameMode in [GM_COOP, GM_SINGLE] then
+        WriteLn(s,
+          Format('mon_killed,mon_total,secrets_found,secrets_total' + LineEnding + '%d,%d,%d,%d',[gCoopMonstersKilled, gTotalMonsters, gCoopSecretsFound, gSecretsCount]));
+      // lines 3-...: team, player name, frags, deaths
+      WriteLn(s, 'team,name,frags,deaths');
+      for I := Low(Stat.PlayerStat) to High(Stat.PlayerStat) do
+        with Stat.PlayerStat[I] do
+          WriteLn(s, Format('%d,%s,%d,%d', [Team, dquoteStr(Name), Frags, Deaths]));
+    except
+      g_Console_Add(Format(_lc[I_CONSOLE_ERROR_WRITE], [fname]));
+    end;
+  except
+    g_Console_Add('could not create gamestats file "' + fname + '"');
+  end;
+  CloseFile(s);
+end;
+
 function g_Game_ModeToText(Mode: Byte): string;
 begin
   Result := '';
@@ -719,17 +845,17 @@ var
   cfg: TConfig;
   p: Pointer;
   {b, }len: Integer;
-  s: string;
+  s: AnsiString;
 begin
   g_Game_FreeWAD();
   gGameSettings.WAD := WAD;
   if not (gGameSettings.GameMode in [GM_COOP, GM_SINGLE]) then
     Exit;
 
-  MegaWAD.info := g_Game_GetMegaWADInfo(MapsDir + WAD);
+  MegaWAD.info := g_Game_GetMegaWADInfo(WAD);
 
   w := TWADFile.Create();
-  w.ReadFile(MapsDir + WAD);
+  w.ReadFile(WAD);
 
   if not w.GetResource('INTERSCRIPT', p, len) then
   begin
@@ -768,16 +894,16 @@ begin
   MegaWAD.endpic := cfg.ReadStr('megawad', 'endpic', '');
   if MegaWAD.endpic <> '' then
   begin
-    s := g_ExtractWadName(MegaWAD.endpic);
-    if s = '' then s := MapsDir+WAD else s := GameDir+'/wads/';
-    g_Texture_CreateWADEx('TEXTURE_endpic', s+MegaWAD.endpic);
+    TEXTUREFILTER := GL_LINEAR;
+    s := e_GetResourcePath(WadDirs, MegaWAD.endpic, WAD);
+    g_Texture_CreateWADEx('TEXTURE_endpic', s);
+    TEXTUREFILTER := GL_NEAREST;
   end;
   MegaWAD.endmus := cfg.ReadStr('megawad', 'endmus', 'Standart.wad:D2DMUS\ÊÎÍÅÖ');
   if MegaWAD.endmus <> '' then
   begin
-    s := g_ExtractWadName(MegaWAD.endmus);
-    if s = '' then s := MapsDir+WAD else s := GameDir+'/wads/';
-    g_Sound_CreateWADEx('MUSIC_endmus', s+MegaWAD.endmus, True);
+    s := e_GetResourcePath(WadDirs, MegaWAD.endmus, WAD);
+    g_Sound_CreateWADEx('MUSIC_endmus', s, True);
   end;
 
   cfg.Free();
@@ -849,7 +975,7 @@ begin
   gDelayedEvents[n].DENum := Num;
   gDelayedEvents[n].DEStr := Str;
   if DEType = DE_GLOBEVENT then
-    gDelayedEvents[n].Time := (GetTimer() {div 1000}) + Time
+    gDelayedEvents[n].Time := (sys_GetTicks() {div 1000}) + Time
   else
     gDelayedEvents[n].Time := gTime + Time;
   Result := n;
@@ -859,6 +985,7 @@ procedure EndGame();
 var
   a: Integer;
   FileName: string;
+  t: TDateTime;
 begin
   if g_Game_IsNet and g_Game_IsServer then
     MH_SEND_GameEvent(NET_EV_MAPEND, Byte(gMissionFailed));
@@ -907,6 +1034,7 @@ begin
               end
               else
                 slWaitStr := _lc[I_NET_SLIST_ERROR];
+              g_Serverlist_GenerateTable(slCurrent, slTable);
             end;
 
             g_Game_ExecuteEvent('ongameend');
@@ -941,6 +1069,7 @@ begin
               SetLength(CustomStat.PlayerStat, Length(CustomStat.PlayerStat)+1);
               with CustomStat.PlayerStat[High(CustomStat.PlayerStat)] do
               begin
+                Num := a;
                 Name := gPlayers[a].Name;
                 Frags := gPlayers[a].Frags;
                 Deaths := gPlayers[a].Death;
@@ -952,9 +1081,24 @@ begin
             end;
 
           SortGameStat(CustomStat.PlayerStat);
+
+          if (gSaveStats or gScreenshotStats) and (Length(CustomStat.PlayerStat) > 1) then
+          begin
+            t := Now;
+            if g_Game_IsNet then StatFilename := NetServerName else StatFilename := 'local';
+            StatDate := FormatDateTime('yymmdd_hhnnss', t);
+            StatFilename := StatFilename + '_' + CustomStat.Map + '_' + g_Game_ModeToText(CustomStat.GameMode);
+            StatFilename := sanitizeFilename(StatFilename) + '_' + StatDate;
+            if gSaveStats then
+              SaveGameStat(CustomStat, FormatDateTime('yyyy"/"mm"/"dd', t));
+          end;
+
+          StatShotDone := False;
         end;
 
         g_Game_ExecuteEvent('onmapend');
+        if not g_Game_IsClient then g_Player_ResetReady;
+        gInterReadyCount := 0;
 
       // Çàòóõàþùèé ýêðàí:
         EndingGameCounter := 255;
@@ -990,6 +1134,7 @@ begin
             gMusic.SetByName('MUSIC_INTERMUS');
             gMusic.Play();
             gState := STATE_INTERSINGLE;
+            e_UnpressAllKeys();
 
             g_Game_ExecuteEvent('oninter');
           end
@@ -1029,6 +1174,7 @@ var
   stat: TPlayerStatArray;
   wad, map: string;
   mapstr: string;
+  namestr: string;
 begin
   s1 := '';
   s2 := '';
@@ -1170,8 +1316,12 @@ begin
               gg := g;
               bb := b;
             end;
+            if gShowPIDs then
+              namestr := Format('[%5d] %s', [UID, Name])
+            else
+              namestr := Name;
             // Èìÿ
-            e_TextureFontPrintEx(x+16, _y, Name, gStdFont, rr, gg, bb, 1);
+            e_TextureFontPrintEx(x+16, _y, namestr, gStdFont, rr, gg, bb, 1);
             // Ïèíã/ïîòåðè
             e_TextureFontPrintEx(x+w1+16, _y, Format(_lc[I_GAME_PING_MS], [Ping, Loss]), gStdFont, rr, gg, bb, 1);
             // Ôðàãè
@@ -1206,11 +1356,15 @@ begin
           r := 255;
           g := 127;
         end;
+        if gShowPIDs then
+          namestr := Format('[%5d] %s', [UID, Name])
+        else
+          namestr := Name;
         // Öâåò èãðîêà
         e_DrawFillQuad(x+16, _y+4, x+32-1, _y+16+4-1, Color.R, Color.G, Color.B, 0);
         e_DrawQuad(x+16, _y+4, x+32-1, _y+16+4-1, 192, 192, 192);
         // Èìÿ
-        e_TextureFontPrintEx(x+16+16+8, _y+4, Name, gStdFont, r, g, 0, 1);
+        e_TextureFontPrintEx(x+16+16+8, _y+4, namestr, gStdFont, r, g, 0, 1);
         // Ïèíã/ïîòåðè
         e_TextureFontPrintEx(x+w1+16, _y+4, Format(_lc[I_GAME_PING_MS], [Ping, Loss]), gStdFont, r, g, 0, 1);
         // Ôðàãè
@@ -1225,6 +1379,10 @@ end;
 procedure g_Game_Init();
 var
   SR: TSearchRec;
+  knownFiles: array of AnsiString = nil;
+  found: Boolean;
+  wext, s: AnsiString;
+  f: Integer;
 begin
   gExit := 0;
   gMapToDelete := '';
@@ -1233,10 +1391,12 @@ begin
   sfsGCDisable(); // temporary disable removing of temporary volumes
 
   try
+    TEXTUREFILTER := GL_LINEAR;
     g_Texture_CreateWADEx('MENU_BACKGROUND', GameWAD+':TEXTURES\TITLE');
     g_Texture_CreateWADEx('INTER', GameWAD+':TEXTURES\INTER');
     g_Texture_CreateWADEx('ENDGAME_EN', GameWAD+':TEXTURES\ENDGAME_EN');
     g_Texture_CreateWADEx('ENDGAME_RU', GameWAD+':TEXTURES\ENDGAME_RU');
+    TEXTUREFILTER := GL_NEAREST;
 
     LoadStdFont('STDTXT', 'STDFONT', gStdFont);
     LoadFont('MENUTXT', 'MENUFONT', gMenuFont);
@@ -1252,32 +1412,47 @@ begin
     g_Game_SetLoadingText(_lc[I_LOAD_MODELS], 0, False);
     g_PlayerModel_LoadData();
 
-    if FindFirst(ModelsDir+'*.wad', faAnyFile, SR) = 0 then
-      repeat
-        if not g_PlayerModel_Load(ModelsDir+SR.Name) then
-          e_WriteLog(Format('Error loading model %s', [SR.Name]), TMsgType.Warning);
-      until FindNext(SR) <> 0;
-    FindClose(SR);
-
-    if FindFirst(ModelsDir+'*.pk3', faAnyFile, SR) = 0 then
-      repeat
-        if not g_PlayerModel_Load(ModelsDir+SR.Name) then
-          e_WriteLog(Format('Error loading model %s', [SR.Name]), TMsgType.Warning);
-      until FindNext(SR) <> 0;
-    FindClose(SR);
-
-    if FindFirst(ModelsDir+'*.zip', faAnyFile, SR) = 0 then
-      repeat
-        if not g_PlayerModel_Load(ModelsDir+SR.Name) then
-          e_WriteLog(Format('Error loading model %s', [SR.Name]), TMsgType.Warning);
-      until FindNext(SR) <> 0;
-    FindClose(SR);
+    // load models from all possible wad types, in all known directories
+    // this does a loosy job (linear search, ooph!), but meh
+    for wext in wadExtensions do
+    begin
+      for f := High(ModelDirs) downto Low(ModelDirs) do
+      begin
+        if (FindFirst(ModelDirs[f]+DirectorySeparator+'*'+wext, faAnyFile, SR) = 0) then
+        begin
+          repeat
+            found := false;
+            for s in knownFiles do
+            begin
+              if (strEquCI1251(forceFilenameExt(SR.Name, ''), forceFilenameExt(ExtractFileName(s), ''))) then
+              begin
+                found := true;
+                break;
+              end;
+            end;
+            if not found then
+            begin
+              SetLength(knownFiles, length(knownFiles)+1);
+              knownFiles[High(knownFiles)] := ModelDirs[f]+DirectorySeparator+SR.Name;
+            end;
+          until (FindNext(SR) <> 0);
+        end;
+        FindClose(SR);
+      end;
+    end;
+
+    if (length(knownFiles) = 0) then raise Exception.Create('no player models found!');
+
+    if (length(knownFiles) = 1) then e_LogWriteln('1 player model found.', TMsgType.Notify) else e_LogWritefln('%d player models found.', [Integer(length(knownFiles))], TMsgType.Notify);
+    for s in knownFiles do
+    begin
+      if not g_PlayerModel_Load(s) then e_LogWritefln('Error loading model "%s"', [s], TMsgType.Warning);
+    end;
 
     gGameOn := false;
     gPauseMain := false;
     gPauseHolmes := false;
     gTime := 0;
-    LastScreenShot := 0;
 
     {e_MouseInfo.Accel := 1.0;}
 
@@ -1287,11 +1462,13 @@ begin
     g_Game_SetLoadingText(_lc[I_LOAD_MUSIC], 0, False);
     g_Sound_CreateWADEx('MUSIC_INTERMUS', GameWAD+':MUSIC\INTERMUS', True);
     g_Sound_CreateWADEx('MUSIC_MENU', GameWAD+':MUSIC\MENU', True);
-    g_Sound_CreateWADEx('MUSIC_ROUNDMUS', GameWAD+':MUSIC\ROUNDMUS', True);
+    g_Sound_CreateWADEx('MUSIC_ROUNDMUS', GameWAD+':MUSIC\ROUNDMUS', True, True);
     g_Sound_CreateWADEx('MUSIC_STDENDMUS', GameWAD+':MUSIC\ENDMUS', True);
 
+{$IFNDEF HEADLESS}
     g_Game_SetLoadingText(_lc[I_LOAD_MENUS], 0, False);
     g_Menu_Init();
+{$ENDIF}
 
     gMusic := TMusic.Create();
     gMusic.SetByName('MUSIC_MENU');
@@ -1410,68 +1587,135 @@ begin
     Result := ids[(Length(ids) - 1 + idx) mod Length(ids)];
 end;
 
-function isKeyPressed (key1: Word; key2: Word): Boolean;
+function GetActivePlayerID_Random(Skip: Integer = -1): Integer;
+var
+  a, idx: Integer;
+  ids: Array of Word;
 begin
-  if (key1 <> 0) and e_KeyPressed(key1) then begin result := true; exit; end;
-  if (key2 <> 0) and e_KeyPressed(key2) then begin result := true; exit; end;
-  result := false;
+  Result := -1;
+  if gPlayers = nil then
+    Exit;
+  SetLength(ids, 0);
+  idx := -1;
+  for a := Low(gPlayers) to High(gPlayers) do
+    if IsActivePlayer(gPlayers[a]) then
+    begin
+      SetLength(ids, Length(ids) + 1);
+      ids[High(ids)] := gPlayers[a].UID;
+      if gPlayers[a].UID = Skip then
+        idx := High(ids);
+    end;
+  if Length(ids) = 0 then
+    Exit;
+  if Length(ids) = 1 then
+  begin
+    Result := ids[0];
+    Exit;
+  end;
+  Result := ids[Random(Length(ids))];
+  a := 10;
+  while (idx <> -1) and (Result = Skip) and (a > 0) do
+  begin
+    Result := ids[Random(Length(ids))];
+    Dec(a);
+  end;
 end;
 
-procedure processPlayerControls (plr: TPlayer; var ctrl: TPlayerControl; var MoveButton: Byte; p2hack: Boolean=false);
-var
-  time: Word;
-  strafeDir: Byte;
-  i: Integer;
+function GetRandomSpectMode(Current: Byte): Byte;
+label
+  retry;
+begin
+  Result := Current;
+retry:
+  case Random(7) of
+    0: Result := SPECT_STATS;
+    1: Result := SPECT_MAPVIEW;
+    2: Result := SPECT_MAPVIEW;
+    3: Result := SPECT_PLAYERS;
+    4: Result := SPECT_PLAYERS;
+    5: Result := SPECT_PLAYERS;
+    6: Result := SPECT_PLAYERS;
+  end;
+  if (Current in [SPECT_STATS, SPECT_MAPVIEW]) and (Current = Result) then
+    goto retry;
+end;
+
+procedure ProcessPlayerControls (plr: TPlayer; p: Integer; var MoveButton: Byte);
+  var
+    time: Word;
+    strafeDir: Byte;
+    i: Integer;
 begin
   if (plr = nil) then exit;
-  if (p2hack) then time := 1000 else time := 1;
+  if (p = 2) then time := 1000 else time := 1;
   strafeDir := MoveButton shr 4;
   MoveButton := MoveButton and $0F;
-  with ctrl do
+
+  if gPlayerAction[p, ACTION_MOVELEFT] and (not gPlayerAction[p, ACTION_MOVERIGHT]) then
+    MoveButton := 1 // Íàæàòà òîëüêî "Âëåâî"
+  else if (not gPlayerAction[p, ACTION_MOVELEFT]) and gPlayerAction[p, ACTION_MOVERIGHT] then
+    MoveButton := 2 // Íàæàòà òîëüêî "Âïðàâî"
+  else if (not gPlayerAction[p, ACTION_MOVELEFT]) and (not gPlayerAction[p, ACTION_MOVERIGHT]) then
+    MoveButton := 0; // Íå íàæàòû íè "Âëåâî", íè "Âïðàâî"
+
+  // Ñåé÷àñ èëè ðàíüøå áûëè íàæàòû "Âëåâî"/"Âïðàâî" => ïåðåäàåì èãðîêó:
+  if MoveButton = 1 then
+    plr.PressKey(KEY_LEFT, time)
+  else if MoveButton = 2 then
+    plr.PressKey(KEY_RIGHT, time);
+
+  // if we have "strafe" key, turn off old strafe mechanics
+  if gPlayerAction[p, ACTION_STRAFE] then
+  begin
+    // new strafe mechanics
+    if (strafeDir = 0) then
+      strafeDir := MoveButton; // start strafing
+    // now set direction according to strafe (reversed)
+    if (strafeDir = 2) then
+      plr.SetDirection(TDirection.D_LEFT)
+    else if (strafeDir = 1) then
+      plr.SetDirection(TDirection.D_RIGHT)
+  end
+  else
   begin
-         if isKeyPressed(KeyLeft, KeyLeft2) and (not isKeyPressed(KeyRight, KeyRight2)) then MoveButton := 1 // Íàæàòà òîëüêî "Âëåâî"
-    else if (not isKeyPressed(KeyLeft, KeyLeft2)) and isKeyPressed(KeyRight, KeyRight2) then MoveButton := 2 // Íàæàòà òîëüêî "Âïðàâî"
-    else if (not isKeyPressed(KeyLeft, KeyLeft2)) and (not isKeyPressed(KeyRight, KeyRight2)) then MoveButton := 0; // Íå íàæàòû íè "Âëåâî", íè "Âïðàâî"
+    strafeDir := 0; // not strafing anymore
+    // Ðàíüøå áûëà íàæàòà "Âïðàâî", à ñåé÷àñ "Âëåâî" => áåæèì âïðàâî, ñìîòðèì âëåâî:
+    if (MoveButton = 2) and gPlayerAction[p, ACTION_MOVELEFT] then
+      plr.SetDirection(TDirection.D_LEFT)
+    // Ðàíüøå áûëà íàæàòà "Âëåâî", à ñåé÷àñ "Âïðàâî" => áåæèì âëåâî, ñìîòðèì âïðàâî:
+    else if (MoveButton = 1) and gPlayerAction[p, ACTION_MOVERIGHT] then
+      plr.SetDirection(TDirection.D_RIGHT)
+    // ×òî-òî áûëî íàæàòî è íå èçìåíèëîñü => êóäà áåæèì, òóäà è ñìîòðèì:
+    else if MoveButton <> 0 then
+      plr.SetDirection(TDirection(MoveButton-1))
+  end;
 
-    // Ñåé÷àñ èëè ðàíüøå áûëè íàæàòû "Âëåâî"/"Âïðàâî" => ïåðåäàåì èãðîêó:
-         if MoveButton = 1 then plr.PressKey(KEY_LEFT, time)
-    else if MoveButton = 2 then plr.PressKey(KEY_RIGHT, time);
+  // fix movebutton state
+  MoveButton := MoveButton or (strafeDir shl 4);
 
-    // if we have "strafe" key, turn off old strafe mechanics
-    if isKeyPressed(KeyStrafe, KeyStrafe2) then
+  // Îñòàëüíûå êëàâèøè:
+  if gPlayerAction[p, ACTION_JUMP] then plr.PressKey(KEY_JUMP, time);
+  if gPlayerAction[p, ACTION_LOOKUP] then plr.PressKey(KEY_UP, time);
+  if gPlayerAction[p, ACTION_LOOKDOWN] then plr.PressKey(KEY_DOWN, time);
+  if gPlayerAction[p, ACTION_ATTACK] then plr.PressKey(KEY_FIRE);
+  if gPlayerAction[p, ACTION_ACTIVATE] then plr.PressKey(KEY_OPEN);
+
+  for i := WP_FACT to WP_LACT do
+  begin
+    if gWeaponAction[p, i] then
     begin
-      // new strafe mechanics
-      if (strafeDir = 0) then strafeDir := MoveButton; // start strafing
-      // now set direction according to strafe (reversed)
-           if (strafeDir = 2) then plr.SetDirection(TDirection.D_LEFT)
-      else if (strafeDir = 1) then plr.SetDirection(TDirection.D_RIGHT);
+      plr.ProcessWeaponAction(i);
+      gWeaponAction[p, i] := False
     end
-    else
-    begin
-      strafeDir := 0; // not strafing anymore
-      // Ðàíüøå áûëà íàæàòà "Âïðàâî", à ñåé÷àñ "Âëåâî" => áåæèì âïðàâî, ñìîòðèì âëåâî:
-           if (MoveButton = 2) and isKeyPressed(KeyLeft, KeyLeft2) then plr.SetDirection(TDirection.D_LEFT)
-      // Ðàíüøå áûëà íàæàòà "Âëåâî", à ñåé÷àñ "Âïðàâî" => áåæèì âëåâî, ñìîòðèì âïðàâî:
-      else if (MoveButton = 1) and isKeyPressed(KeyRight, KeyRight2) then plr.SetDirection(TDirection.D_RIGHT)
-      // ×òî-òî áûëî íàæàòî è íå èçìåíèëîñü => êóäà áåæèì, òóäà è ñìîòðèì:
-      else if MoveButton <> 0 then plr.SetDirection(TDirection(MoveButton-1));
-    end;
-
-    // fix movebutton state
-    MoveButton := MoveButton or (strafeDir shl 4);
-
-    // Îñòàëüíûå êëàâèøè:
-    if isKeyPressed(KeyJump, KeyJump2) then plr.PressKey(KEY_JUMP, time);
-    if isKeyPressed(KeyUp, KeyUp2) then plr.PressKey(KEY_UP, time);
-    if isKeyPressed(KeyDown, KeyDown2) then plr.PressKey(KEY_DOWN, time);
-    if isKeyPressed(KeyFire, KeyFire2) then plr.PressKey(KEY_FIRE);
-    if isKeyPressed(KeyNextWeapon, KeyNextWeapon2) then plr.PressKey(KEY_NEXTWEAPON);
-    if isKeyPressed(KeyPrevWeapon, KeyPrevWeapon2) then plr.PressKey(KEY_PREVWEAPON);
-    if isKeyPressed(KeyOpen, KeyOpen2) then plr.PressKey(KEY_OPEN);
+  end;
 
-    for i := 0 to High(KeyWeapon) do
-      if isKeyPressed(KeyWeapon[i], KeyWeapon2[i]) then
-        plr.QueueWeaponSwitch(i); // all choices are passed there, and god will take the best
+  for i := WP_FIRST to WP_LAST do
+  begin
+    if gSelectWeapon[p, i] then
+    begin
+      plr.QueueWeaponSwitch(i); // all choices are passed there, and god will take the best
+      gSelectWeapon[p, i] := False
+    end
   end;
 
   // HACK: add dynlight here
@@ -1490,6 +1734,25 @@ begin
   if gwin_has_stencil and g_playerLight then g_AddDynLight(plr.GameX+32, plr.GameY+40, 128, 1, 1, 0, 0.6);
 end;
 
+// HACK: don't have a "key was pressed" function
+procedure InterReady();
+begin
+  if InterReadyTime > gTime then Exit;
+  InterReadyTime := gTime + 3000;
+  MC_SEND_CheatRequest(NET_CHEAT_READY);
+end;
+
+procedure g_Game_PreUpdate();
+begin
+  // these are in separate PreUpdate functions because they can interact during Update()
+  // and are synced over the net
+  // we don't care that much about corpses and gibs
+  g_Player_PreUpdate();
+  g_Monsters_PreUpdate();
+  g_Items_PreUpdate();
+  g_Weapon_PreUpdate();
+end;
+
 procedure g_Game_Update();
 var
   Msg: g_gui.TMessage;
@@ -1523,10 +1786,22 @@ var
     if mon.gncNeedSend then MH_SEND_MonsterPos(mon.UID);
   end;
 
+  function sendItemPos (it: PItem): Boolean;
+  begin
+    result := false; // don't stop
+    if it.needSend then
+    begin
+      MH_SEND_ItemPos(it.myId);
+      it.needSend := False;
+    end;
+  end;
+
 var
   reliableUpdate: Boolean;
 begin
   g_ResetDynlights();
+  framePool.reset();
+
 // Ïîðà âûêëþ÷àòü èãðó:
   if gExit = EXIT_QUIT then
     Exit;
@@ -1538,8 +1813,8 @@ begin
       Exit;
   end;
 
-// ×èòàåì êëàâèàòóðó è äæîéñòèê, åñëè îêíî àêòèâíî:
-  e_PollInput();
+  // ×èòàåì êëàâèàòóðó è äæîéñòèê, åñëè îêíî àêòèâíî
+  // no need to, as we'll do it in event handler
 
 // Îáíîâëÿåì êîíñîëü (äâèæåíèå è ñîîáùåíèÿ):
   g_Console_Update();
@@ -1551,6 +1826,9 @@ begin
     Exit;
   end;
 
+  // process master server communications
+  g_Net_Slist_Pulse();
+
   case gState of
     STATE_INTERSINGLE, // Ñòàòèñòêà ïîñëå ïðîõîæäåíèÿ óðîâíÿ â Îäèíî÷íîé èãðå
     STATE_INTERCUSTOM, // Ñòàòèñòêà ïîñëå ïðîõîæäåíèÿ óðîâíÿ â Ñâîåé èãðå
@@ -1571,11 +1849,16 @@ begin
         if (not g_Game_IsClient) and
         (
           (
-            (e_KeyPressed(IK_RETURN) or e_KeyPressed(IK_KPRETURN) or e_KeyPressed(IK_SPACE))
+            (
+              e_KeyPressed(IK_RETURN) or e_KeyPressed(IK_KPRETURN) or e_KeyPressed(IK_SPACE) or
+              e_KeyPressed(VK_FIRE) or e_KeyPressed(VK_OPEN) or
+              e_KeyPressed(JOY0_ATTACK) or e_KeyPressed(JOY1_ATTACK) or
+              e_KeyPressed(JOY2_ATTACK) or e_KeyPressed(JOY3_ATTACK)
+            )
             and (not gJustChatted) and (not gConsoleShow) and (not gChatShow)
             and (g_ActiveWindow = nil)
           )
-          or (g_Game_IsNet and (gInterTime > gInterEndTime))
+          or (g_Game_IsNet and ((gInterTime > gInterEndTime) or ((gInterReadyCount >= NetClientCount) and (NetClientCount > 0))))
         )
         then
         begin // Íàæàëè <Enter>/<Ïðîáåë> èëè ïðîøëî äîñòàòî÷íî âðåìåíè:
@@ -1610,6 +1893,22 @@ begin
             end;
 
           Exit;
+        end
+        else if g_Game_IsClient and
+        (
+          (
+            e_KeyPressed(IK_RETURN) or e_KeyPressed(IK_KPRETURN) or e_KeyPressed(IK_SPACE) or
+            e_KeyPressed(VK_FIRE) or e_KeyPressed(VK_OPEN) or
+            e_KeyPressed(JOY0_ATTACK) or e_KeyPressed(JOY1_ATTACK) or
+            e_KeyPressed(JOY2_ATTACK) or e_KeyPressed(JOY3_ATTACK)
+          )
+          and (not gJustChatted) and (not gConsoleShow) and (not gChatShow)
+          and (g_ActiveWindow = nil)
+        )
+        then
+        begin
+          // ready / unready
+          InterReady();
         end;
 
         if gState = STATE_INTERTEXT then
@@ -1624,6 +1923,8 @@ begin
           // Çàêîí÷èëñÿ óðîâåíü â Ñâîåé èãðå:
             if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
               begin
+                gState := STATE_INTERCUSTOM;
+                InterReadyTime := -1;
                 if gLastMap and (gGameSettings.GameMode = GM_COOP) then
                 begin
                   g_Game_ExecuteEvent('onwadend');
@@ -1632,15 +1933,15 @@ begin
                 end
                 else
                   gMusic.SetByName('MUSIC_ROUNDMUS');
-
                 gMusic.Play();
-                gState := STATE_INTERCUSTOM;
+                e_UnpressAllKeys();
               end
             else // Çàêîí÷èëàñü ïîñëåäíÿÿ êàðòà â Îäèíî÷íîé èãðå
               begin
                 gMusic.SetByName('MUSIC_INTERMUS');
                 gMusic.Play();
                 gState := STATE_INTERSINGLE;
+                e_UnpressAllKeys();
               end;
             g_Game_ExecuteEvent('oninter');
           end
@@ -1658,32 +1959,12 @@ begin
       end;
 
     STATE_SLIST:
-        g_Serverlist_Control(slCurrent);
+        g_Serverlist_Control(slCurrent, slTable);
   end;
 
-  if g_Game_IsNet then
-    if not gConsoleShow then
-      if not gChatShow then
-      begin
-        if g_ActiveWindow = nil then
-        begin
-          if e_KeyPressed(gGameControls.GameControls.Chat) then
-            g_Console_Chat_Switch(False)
-          else if (e_KeyPressed(gGameControls.GameControls.TeamChat)) and
-                  (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
-            g_Console_Chat_Switch(True);
-        end;
-      end else
-        if not gChatEnter then
-          if (not e_KeyPressed(gGameControls.GameControls.Chat))
-             and (not e_KeyPressed(gGameControls.GameControls.TeamChat)) then
-            gChatEnter := True;
-
 // Ñòàòèñòèêà ïî Tab:
   if gGameOn then
-    IsDrawStat := (not gConsoleShow) and (not gChatShow) and
-                  (gGameSettings.GameType <> GT_SINGLE) and
-                  e_KeyPressed(gGameControls.GameControls.Stat);
+    IsDrawStat := (not gConsoleShow) and (not gChatShow) and (gGameSettings.GameType <> GT_SINGLE) and g_Console_Action(ACTION_SCORES);
 
 // Èãðà èäåò:
   if gGameOn and not gPause and (gState <> STATE_FOLD) then
@@ -1758,8 +2039,8 @@ begin
       if gPlayer2 <> nil then gPlayer2.ReleaseKeys();
       if (not gConsoleShow) and (not gChatShow) and (g_ActiveWindow = nil) then
       begin
-        processPlayerControls(gPlayer1, gGameControls.P1Control, P1MoveButton);
-        processPlayerControls(gPlayer2, gGameControls.P2Control, P2MoveButton, true);
+        ProcessPlayerControls(gPlayer1, 0, P1MoveButton);
+        ProcessPlayerControls(gPlayer2, 1, P2MoveButton);
       end  // if not console
       else
       begin
@@ -1774,7 +2055,7 @@ begin
     begin
       if not gSpectKeyPress then
       begin
-        if isKeyPressed(gGameControls.P1Control.KeyJump, gGameControls.P1Control.KeyJump2) then
+        if gPlayerAction[0, ACTION_JUMP] and (not gSpectAuto) then
         begin
           // switch spect mode
           case gSpectMode of
@@ -1785,78 +2066,144 @@ begin
           end;
           gSpectKeyPress := True;
         end;
-        if gSpectMode = SPECT_MAPVIEW then
+        if (gSpectMode = SPECT_MAPVIEW)
+           and (not gSpectAuto) then
         begin
-          if isKeyPressed(gGameControls.P1Control.KeyLeft, gGameControls.P1Control.KeyLeft2) then
+          if gPlayerAction[0, ACTION_MOVELEFT] then
             gSpectX := Max(gSpectX - gSpectStep, 0);
-          if isKeyPressed(gGameControls.P1Control.KeyRight, gGameControls.P1Control.KeyRight2) then
+          if gPlayerAction[0, ACTION_MOVERIGHT] then
             gSpectX := Min(gSpectX + gSpectStep, gMapInfo.Width - gScreenWidth);
-          if isKeyPressed(gGameControls.P1Control.KeyUp, gGameControls.P1Control.KeyUp2) then
+          if gPlayerAction[0, ACTION_LOOKUP] then
             gSpectY := Max(gSpectY - gSpectStep, 0);
-          if isKeyPressed(gGameControls.P1Control.KeyDown, gGameControls.P1Control.KeyDown2) then
+          if gPlayerAction[0, ACTION_LOOKDOWN] then
             gSpectY := Min(gSpectY + gSpectStep, gMapInfo.Height - gScreenHeight);
-          if isKeyPressed(gGameControls.P1Control.KeyPrevWeapon, gGameControls.P1Control.KeyPrevWeapon2) then
+          if gWeaponAction[0, WP_PREV] then
           begin
             // decrease step
             if gSpectStep > 4 then gSpectStep := gSpectStep shr 1;
-            gSpectKeyPress := True;
+            gWeaponAction[0, WP_PREV] := False;
           end;
-          if isKeyPressed(gGameControls.P1Control.KeyNextWeapon, gGameControls.P1Control.KeyNextWeapon2) then
+          if gWeaponAction[0, WP_NEXT] then
           begin
             // increase step
             if gSpectStep < 64 then gSpectStep := gSpectStep shl 1;
-            gSpectKeyPress := True;
+            gWeaponAction[0, WP_NEXT] := False;
           end;
         end;
-        if gSpectMode = SPECT_PLAYERS then
+        if (gSpectMode = SPECT_PLAYERS)
+           and (not gSpectAuto) then
         begin
-          if isKeyPressed(gGameControls.P1Control.KeyUp, gGameControls.P1Control.KeyUp2) then
+          if gPlayerAction[0, ACTION_LOOKUP] then
           begin
             // add second view
             gSpectViewTwo := True;
             gSpectKeyPress := True;
           end;
-          if isKeyPressed(gGameControls.P1Control.KeyDown, gGameControls.P1Control.KeyDown2) then
+          if gPlayerAction[0, ACTION_LOOKDOWN] then
           begin
             // remove second view
             gSpectViewTwo := False;
             gSpectKeyPress := True;
           end;
-          if isKeyPressed(gGameControls.P1Control.KeyLeft, gGameControls.P1Control.KeyLeft2) then
+          if gPlayerAction[0, ACTION_MOVELEFT] then
           begin
             // prev player (view 1)
             gSpectPID1 := GetActivePlayerID_Prev(gSpectPID1);
             gSpectKeyPress := True;
           end;
-          if isKeyPressed(gGameControls.P1Control.KeyRight, gGameControls.P1Control.KeyRight2) then
+          if gPlayerAction[0, ACTION_MOVERIGHT] then
           begin
             // next player (view 1)
             gSpectPID1 := GetActivePlayerID_Next(gSpectPID1);
             gSpectKeyPress := True;
           end;
-          if isKeyPressed(gGameControls.P1Control.KeyPrevWeapon, gGameControls.P1Control.KeyPrevWeapon2) then
+          if gWeaponAction[0, WP_PREV] then
           begin
             // prev player (view 2)
             gSpectPID2 := GetActivePlayerID_Prev(gSpectPID2);
-            gSpectKeyPress := True;
+            gWeaponAction[0, WP_PREV] := False;
           end;
-          if isKeyPressed(gGameControls.P1Control.KeyNextWeapon, gGameControls.P1Control.KeyNextWeapon2) then
+          if gWeaponAction[0, WP_NEXT] then
           begin
             // next player (view 2)
             gSpectPID2 := GetActivePlayerID_Next(gSpectPID2);
-            gSpectKeyPress := True;
+            gWeaponAction[0, WP_NEXT] := False;
           end;
         end;
+        if gPlayerAction[0, ACTION_ATTACK] then
+        begin
+          if (gSpectMode = SPECT_STATS) and (not gSpectAuto) then
+          begin
+            gSpectAuto := True;
+            gSpectAutoNext := 0;
+            gSpectViewTwo := False;
+            gSpectKeyPress := True;
+          end
+          else
+            if gSpectAuto then
+            begin
+              gSpectMode := SPECT_STATS;
+              gSpectAuto := False;
+              gSpectKeyPress := True;
+            end;
+        end;
       end
       else
-        if (not isKeyPressed(gGameControls.P1Control.KeyJump, gGameControls.P1Control.KeyJump2)) and
-           (not isKeyPressed(gGameControls.P1Control.KeyLeft, gGameControls.P1Control.KeyLeft2)) and
-           (not isKeyPressed(gGameControls.P1Control.KeyRight, gGameControls.P1Control.KeyRight2)) and
-           (not isKeyPressed(gGameControls.P1Control.KeyUp, gGameControls.P1Control.KeyUp2)) and
-           (not isKeyPressed(gGameControls.P1Control.KeyDown, gGameControls.P1Control.KeyDown2)) and
-           (not isKeyPressed(gGameControls.P1Control.KeyPrevWeapon, gGameControls.P1Control.KeyPrevWeapon2)) and
-           (not isKeyPressed(gGameControls.P1Control.KeyNextWeapon, gGameControls.P1Control.KeyNextWeapon2)) then
+        if (not gPlayerAction[0, ACTION_JUMP]) and
+           (not gPlayerAction[0, ACTION_ATTACK]) and
+           (not gPlayerAction[0, ACTION_MOVELEFT]) and
+           (not gPlayerAction[0, ACTION_MOVERIGHT]) and
+           (not gPlayerAction[0, ACTION_LOOKUP]) and
+           (not gPlayerAction[0, ACTION_LOOKDOWN]) then
           gSpectKeyPress := False;
+
+      if gSpectAuto then
+      begin
+        if gSpectMode = SPECT_MAPVIEW then
+        begin
+          i := Min(Max(gSpectX + gSpectAutoStepX, 0), gMapInfo.Width - gScreenWidth);
+          if i = gSpectX then
+            gSpectAutoNext := gTime
+          else
+            gSpectX := i;
+          i := Min(Max(gSpectY + gSpectAutoStepY, 0), gMapInfo.Height - gScreenHeight);
+          if i = gSpectY then
+            gSpectAutoNext := gTime
+          else
+            gSpectY := i;
+        end;
+        if gSpectAutoNext <= gTime then
+        begin
+          if gSpectAutoNext > 0 then
+          begin
+            gSpectMode := GetRandomSpectMode(gSpectMode);
+            case gSpectMode of
+              SPECT_MAPVIEW:
+              begin
+                gSpectX := Random(gMapInfo.Width - gScreenWidth);
+                gSpectY := Random(gMapInfo.Height - gScreenHeight);
+                gSpectAutoStepX := Random(9) - 4;
+                gSpectAutoStepY := Random(9) - 4;
+                if ((gSpectX < 800) and (gSpectAutoStepX < 0)) or
+                   ((gSpectX > gMapInfo.Width - gScreenWidth - 800) and (gSpectAutoStepX > 0)) then
+                  gSpectAutoStepX := gSpectAutoStepX * -1;
+                if ((gSpectY < 800) and (gSpectAutoStepY < 0)) or
+                   ((gSpectY > gMapInfo.Height - gScreenHeight - 800) and (gSpectAutoStepY > 0)) then
+                  gSpectAutoStepY := gSpectAutoStepY * -1;
+              end;
+              SPECT_PLAYERS:
+              begin
+                gSpectPID1 := GetActivePlayerID_Random(gSpectPID1);
+              end;
+            end;
+          end;
+          case gSpectMode of
+            SPECT_STATS:   gSpectAutoNext := gTime + (Random(3) + 5) * 1000;
+            SPECT_MAPVIEW: gSpectAutoNext := gTime + (Random(4) + 7) * 1000;
+            SPECT_PLAYERS: gSpectAutoNext := gTime + (Random(7) + 8) * 1000;
+          end;
+        end;
+      end;
     end;
 
   // Îáíîâëÿåì âñå îñòàëüíîå:
@@ -1907,6 +2254,18 @@ begin
 
         g_Mons_ForEach(sendMonsPos);
 
+        // update flags that aren't stationary
+        if gGameSettings.GameMode = GM_CTF then
+          for I := FLAG_RED to FLAG_BLUE do
+            if gFlags[I].NeedSend then
+            begin
+              gFlags[I].NeedSend := False;
+              MH_SEND_FlagPos(I);
+            end;
+
+        // update items that aren't stationary
+        g_Items_ForEachAlive(sendItemPos);
+
         if reliableUpdate then
         begin
           NetTimeToReliable := 0;
@@ -1926,19 +2285,18 @@ begin
       // send unexpected platform changes
       g_Map_NetSendInterestingPanels();
 
+      g_Net_Slist_ServerUpdate();
+      {
       if NetUseMaster then
       begin
-        if gTime >= NetTimeToMaster then
+        if (gTime >= NetTimeToMaster) or g_Net_Slist_IsConnectionInProgress then
         begin
-          if (NetMHost = nil) or (NetMPeer = nil) then
-          begin
-            if not g_Net_Slist_Connect then g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR]);
-          end;
-
+          if (not g_Net_Slist_IsConnectionActive) then g_Net_Slist_Connect(false); // non-blocking connection to the master
           g_Net_Slist_Update;
           NetTimeToMaster := gTime + NetMasterRate;
         end;
       end;
+      }
     end
     else if (NetMode = NET_CLIENT) then
     begin
@@ -1968,6 +2326,7 @@ begin
       e_WriteLog('Changing resolution', TMsgType.Notify);
       g_Game_ChangeResolution(gRC_Width, gRC_Height, gRC_FullScreen, gRC_Maximized);
       gResolutionChange := False;
+      g_ActiveWindow := nil;
     end;
 
   // Íóæíî ñìåíèòü ÿçûê:
@@ -1976,19 +2335,13 @@ begin
       //e_WriteLog('Read language file', MSG_NOTIFY);
       //g_Language_Load(DataDir + gLanguage + '.txt');
       g_Language_Set(gLanguage);
+{$IFNDEF HEADLESS}
       g_Menu_Reset();
+{$ENDIF}
       gLanguageChange := False;
     end;
   end;
 
-// Äåëàåì ñêðèíøîò (íå ÷àùå 200 ìèëëèñåêóíä):
-  if e_KeyPressed(gGameControls.GameControls.TakeScreenshot) then
-    if (GetTimer()-LastScreenShot) > 200000 div 1000 then
-    begin
-      g_TakeScreenShot();
-      LastScreenShot := GetTimer();
-    end;
-
 // Ãîðÿ÷àÿ êëàâèøà äëÿ âûçîâà ìåíþ âûõîäà èç èãðû (F10):
   if e_KeyPressed(IK_F10) and
      gGameOn and
@@ -1998,7 +2351,7 @@ begin
     KeyPress(IK_F10);
   end;
 
-  Time := GetTimer() {div 1000};
+  Time := sys_GetTicks() {div 1000};
 
 // Îáðàáîòêà îòëîæåííûõ ñîáûòèé:
   if gDelayedEvents <> nil then
@@ -2022,6 +2375,9 @@ begin
               if g_Game_IsNet and g_Game_IsServer then
                 MH_SEND_GameEvent(NET_EV_KILLCOMBO, gDelayedEvents[a].DENum);
             end;
+          DE_BODYKILL:
+            if gGameOn then
+              g_Game_Announce_BodyKill(gDelayedEvents[a].DENum);
         end;
         gDelayedEvents[a].Pending := False;
       end;
@@ -2042,7 +2398,69 @@ begin
   end;
 end;
 
+procedure g_Game_LoadChatSounds(Resource: string);
+var
+  WAD: TWADFile;
+  FileName, Snd: string;
+  p: Pointer;
+  len, cnt, tags, i, j: Integer;
+  cfg: TConfig;
+begin
+  FileName := g_ExtractWadName(Resource);
+
+  WAD := TWADFile.Create();
+  WAD.ReadFile(FileName);
+
+  if not WAD.GetResource(g_ExtractFilePathName(Resource), p, len) then
+  begin
+    gChatSounds := nil;
+    WAD.Free();
+    Exit;
+  end;
+
+  cfg := TConfig.CreateMem(p, len);
+  cnt := cfg.ReadInt('ChatSounds', 'Count', 0);
+
+  SetLength(gChatSounds, cnt);
+  for i := 0 to Length(gChatSounds) - 1 do
+  begin
+    gChatSounds[i].Sound := nil;
+    Snd := Trim(cfg.ReadStr(IntToStr(i), 'Sound', ''));
+    tags := cfg.ReadInt(IntToStr(i), 'Tags', 0);
+    if (Snd = '') or (Tags <= 0) then
+      continue;
+    g_Sound_CreateWADEx('SOUND_CHAT_MACRO' + IntToStr(i), GameWAD+':'+Snd);
+    gChatSounds[i].Sound := TPlayableSound.Create();
+    gChatSounds[i].Sound.SetByName('SOUND_CHAT_MACRO' + IntToStr(i));
+    SetLength(gChatSounds[i].Tags, tags);
+    for j := 0 to tags - 1 do
+      gChatSounds[i].Tags[j] := toLowerCase1251(cfg.ReadStr(IntToStr(i), 'Tag' + IntToStr(j), ''));
+    gChatSounds[i].FullWord := cfg.ReadBool(IntToStr(i), 'FullWord', False);
+  end;
+
+  cfg.Free();
+  WAD.Free();
+end;
+
+procedure g_Game_FreeChatSounds();
+var
+  i: Integer;
+begin
+  for i := 0 to Length(gChatSounds) - 1 do
+  begin
+    gChatSounds[i].Sound.Free();
+    g_Sound_Delete('SOUND_CHAT_MACRO' + IntToStr(i));
+  end;
+  SetLength(gChatSounds, 0);
+  gChatSounds := nil;
+end;
+
 procedure g_Game_LoadData();
+var
+  wl, hl: Integer;
+  wr, hr: Integer;
+  wb, hb: Integer;
+  wm, hm: Integer;
 begin
   if DataLoaded then Exit;
 
@@ -2054,23 +2472,55 @@ begin
   g_Texture_CreateWADEx('TEXTURE_PLAYER_HUDJET', GameWAD+':TEXTURES\JETBAR');
   g_Texture_CreateWADEx('TEXTURE_PLAYER_HUDBG', GameWAD+':TEXTURES\HUDBG');
   g_Texture_CreateWADEx('TEXTURE_PLAYER_ARMORHUD', GameWAD+':TEXTURES\ARMORHUD');
-  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG', GameWAD+':TEXTURES\FLAGHUD_RB', GameWAD+':TEXTURES\FLAGHUD_R_BASE');
-  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG_S', GameWAD+':TEXTURES\FLAGHUD_RS', GameWAD+':TEXTURES\FLAGHUD_R_STOLEN');
-  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG_D', GameWAD+':TEXTURES\FLAGHUD_RD', GameWAD+':TEXTURES\FLAGHUD_R_DROP');
-  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG', GameWAD+':TEXTURES\FLAGHUD_BB', GameWAD+':TEXTURES\FLAGHUD_B_BASE');
-  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG_S', GameWAD+':TEXTURES\FLAGHUD_BS', GameWAD+':TEXTURES\FLAGHUD_B_STOLEN');
-  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG_D', GameWAD+':TEXTURES\FLAGHUD_BD', GameWAD+':TEXTURES\FLAGHUD_B_DROP');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG', GameWAD+':TEXTURES\FLAGHUD_R_BASE');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG_S', GameWAD+':TEXTURES\FLAGHUD_R_STOLEN');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG_D', GameWAD+':TEXTURES\FLAGHUD_R_DROP');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG', GameWAD+':TEXTURES\FLAGHUD_B_BASE');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG_S', GameWAD+':TEXTURES\FLAGHUD_B_STOLEN');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG_D', GameWAD+':TEXTURES\FLAGHUD_B_DROP');
   g_Texture_CreateWADEx('TEXTURE_PLAYER_TALKBUBBLE', GameWAD+':TEXTURES\TALKBUBBLE');
   g_Texture_CreateWADEx('TEXTURE_PLAYER_INVULPENTA', GameWAD+':TEXTURES\PENTA');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_INDICATOR', GameWAD+':TEXTURES\PLRIND');
+
+  hasPBarGfx := true;
+  if not g_Texture_CreateWADEx('UI_GFX_PBAR_LEFT', GameWAD+':TEXTURES\LLEFT') then hasPBarGfx := false;
+  if not g_Texture_CreateWADEx('UI_GFX_PBAR_MARKER', GameWAD+':TEXTURES\LMARKER') then hasPBarGfx := false;
+  if not g_Texture_CreateWADEx('UI_GFX_PBAR_MIDDLE', GameWAD+':TEXTURES\LMIDDLE') then hasPBarGfx := false;
+  if not g_Texture_CreateWADEx('UI_GFX_PBAR_RIGHT', GameWAD+':TEXTURES\LRIGHT') then hasPBarGfx := false;
+
+  if hasPBarGfx then
+  begin
+    g_Texture_GetSize('UI_GFX_PBAR_LEFT', wl, hl);
+    g_Texture_GetSize('UI_GFX_PBAR_RIGHT', wr, hr);
+    g_Texture_GetSize('UI_GFX_PBAR_MIDDLE', wb, hb);
+    g_Texture_GetSize('UI_GFX_PBAR_MARKER', wm, hm);
+    if (wl > 0) and (hl > 0) and (wr > 0) and (hr = hl) and (wb > 0) and (hb = hl) and (wm > 0) and (hm > 0) and (hm <= hl) then
+    begin
+      // yay!
+    end
+    else
+    begin
+      hasPBarGfx := false;
+    end;
+  end;
+
   g_Frames_CreateWAD(nil, 'FRAMES_TELEPORT', GameWAD+':TEXTURES\TELEPORT', 64, 64, 10, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_PUNCH', GameWAD+':WEAPONS\PUNCH', 64, 64, 4, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_PUNCH_UP', GameWAD+':WEAPONS\PUNCH_UP', 64, 64, 4, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_PUNCH_DN', GameWAD+':WEAPONS\PUNCH_DN', 64, 64, 4, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_PUNCH_BERSERK', GameWAD+':WEAPONS\PUNCHB', 64, 64, 4, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_PUNCH_BERSERK_UP', GameWAD+':WEAPONS\PUNCHB_UP', 64, 64, 4, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_PUNCH_BERSERK_DN', GameWAD+':WEAPONS\PUNCHB_DN', 64, 64, 4, False);
   g_Sound_CreateWADEx('SOUND_GAME_TELEPORT', GameWAD+':SOUNDS\TELEPORT');
   g_Sound_CreateWADEx('SOUND_GAME_NOTELEPORT', GameWAD+':SOUNDS\NOTELEPORT');
+  g_Sound_CreateWADEx('SOUND_GAME_SECRET', GameWAD+':SOUNDS\SECRET');
   g_Sound_CreateWADEx('SOUND_GAME_DOOROPEN', GameWAD+':SOUNDS\DOOROPEN');
   g_Sound_CreateWADEx('SOUND_GAME_DOORCLOSE', GameWAD+':SOUNDS\DOORCLOSE');
   g_Sound_CreateWADEx('SOUND_GAME_BULK1', GameWAD+':SOUNDS\BULK1');
   g_Sound_CreateWADEx('SOUND_GAME_BULK2', GameWAD+':SOUNDS\BULK2');
   g_Sound_CreateWADEx('SOUND_GAME_BUBBLE1', GameWAD+':SOUNDS\BUBBLE1');
   g_Sound_CreateWADEx('SOUND_GAME_BUBBLE2', GameWAD+':SOUNDS\BUBBLE2');
+  g_Sound_CreateWADEx('SOUND_GAME_BURNING', GameWAD+':SOUNDS\BURNING');
   g_Sound_CreateWADEx('SOUND_GAME_SWITCH1', GameWAD+':SOUNDS\SWITCH1');
   g_Sound_CreateWADEx('SOUND_GAME_SWITCH0', GameWAD+':SOUNDS\SWITCH0');
   g_Sound_CreateWADEx('SOUND_GAME_RADIO', GameWAD+':SOUNDS\RADIO');
@@ -2082,6 +2532,17 @@ begin
   g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILL3X', GameWAD+':SOUNDS\KILL3X');
   g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILL4X', GameWAD+':SOUNDS\KILL4X');
   g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILLMX', GameWAD+':SOUNDS\KILLMX');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_MUHAHA1', GameWAD+':SOUNDS\MUHAHA1');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_MUHAHA2', GameWAD+':SOUNDS\MUHAHA2');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_MUHAHA3', GameWAD+':SOUNDS\MUHAHA3');
+  g_Sound_CreateWADEx('SOUND_CTF_GET1', GameWAD+':SOUNDS\GETFLAG1');
+  g_Sound_CreateWADEx('SOUND_CTF_GET2', GameWAD+':SOUNDS\GETFLAG2');
+  g_Sound_CreateWADEx('SOUND_CTF_LOST1', GameWAD+':SOUNDS\LOSTFLG1');
+  g_Sound_CreateWADEx('SOUND_CTF_LOST2', GameWAD+':SOUNDS\LOSTFLG2');
+  g_Sound_CreateWADEx('SOUND_CTF_RETURN1', GameWAD+':SOUNDS\RETFLAG1');
+  g_Sound_CreateWADEx('SOUND_CTF_RETURN2', GameWAD+':SOUNDS\RETFLAG2');
+  g_Sound_CreateWADEx('SOUND_CTF_CAPTURE1', GameWAD+':SOUNDS\CAPFLAG1');
+  g_Sound_CreateWADEx('SOUND_CTF_CAPTURE2', GameWAD+':SOUNDS\CAPFLAG2');
 
   goodsnd[0] := TPlayableSound.Create();
   goodsnd[1] := TPlayableSound.Create();
@@ -2103,6 +2564,34 @@ begin
   killsnd[2].SetByName('SOUND_ANNOUNCER_KILL4X');
   killsnd[3].SetByName('SOUND_ANNOUNCER_KILLMX');
 
+  hahasnd[0] := TPlayableSound.Create();
+  hahasnd[1] := TPlayableSound.Create();
+  hahasnd[2] := TPlayableSound.Create();
+
+  hahasnd[0].SetByName('SOUND_ANNOUNCER_MUHAHA1');
+  hahasnd[1].SetByName('SOUND_ANNOUNCER_MUHAHA2');
+  hahasnd[2].SetByName('SOUND_ANNOUNCER_MUHAHA3');
+
+  sound_get_flag[0] := TPlayableSound.Create();
+  sound_get_flag[1] := TPlayableSound.Create();
+  sound_lost_flag[0] := TPlayableSound.Create();
+  sound_lost_flag[1] := TPlayableSound.Create();
+  sound_ret_flag[0] := TPlayableSound.Create();
+  sound_ret_flag[1] := TPlayableSound.Create();
+  sound_cap_flag[0] := TPlayableSound.Create();
+  sound_cap_flag[1] := TPlayableSound.Create();
+
+  sound_get_flag[0].SetByName('SOUND_CTF_GET1');
+  sound_get_flag[1].SetByName('SOUND_CTF_GET2');
+  sound_lost_flag[0].SetByName('SOUND_CTF_LOST1');
+  sound_lost_flag[1].SetByName('SOUND_CTF_LOST2');
+  sound_ret_flag[0].SetByName('SOUND_CTF_RETURN1');
+  sound_ret_flag[1].SetByName('SOUND_CTF_RETURN2');
+  sound_cap_flag[0].SetByName('SOUND_CTF_CAPTURE1');
+  sound_cap_flag[1].SetByName('SOUND_CTF_CAPTURE2');
+
+  g_Game_LoadChatSounds(GameWAD+':CHATSND\SNDCFG');
+
   g_Game_SetLoadingText(_lc[I_LOAD_ITEMS_DATA], 0, False);
   g_Items_LoadData();
 
@@ -2137,14 +2626,22 @@ begin
   g_Texture_Delete('TEXTURE_PLAYER_TALKBUBBLE');
   g_Texture_Delete('TEXTURE_PLAYER_INVULPENTA');
   g_Frames_DeleteByName('FRAMES_TELEPORT');
+  g_Frames_DeleteByName('FRAMES_PUNCH');
+  g_Frames_DeleteByName('FRAMES_PUNCH_UP');
+  g_Frames_DeleteByName('FRAMES_PUNCH_DN');
+  g_Frames_DeleteByName('FRAMES_PUNCH_BERSERK');
+  g_Frames_DeleteByName('FRAMES_PUNCH_BERSERK_UP');
+  g_Frames_DeleteByName('FRAMES_PUNCH_BERSERK_DN');
   g_Sound_Delete('SOUND_GAME_TELEPORT');
   g_Sound_Delete('SOUND_GAME_NOTELEPORT');
+  g_Sound_Delete('SOUND_GAME_SECRET');
   g_Sound_Delete('SOUND_GAME_DOOROPEN');
   g_Sound_Delete('SOUND_GAME_DOORCLOSE');
   g_Sound_Delete('SOUND_GAME_BULK1');
   g_Sound_Delete('SOUND_GAME_BULK2');
   g_Sound_Delete('SOUND_GAME_BUBBLE1');
   g_Sound_Delete('SOUND_GAME_BUBBLE2');
+  g_Sound_Delete('SOUND_GAME_BURNING');
   g_Sound_Delete('SOUND_GAME_SWITCH1');
   g_Sound_Delete('SOUND_GAME_SWITCH0');
 
@@ -2168,6 +2665,34 @@ begin
   g_Sound_Delete('SOUND_ANNOUNCER_KILL4X');
   g_Sound_Delete('SOUND_ANNOUNCER_KILLMX');
 
+  hahasnd[0].Free();
+  hahasnd[1].Free();
+  hahasnd[2].Free();
+
+  g_Sound_Delete('SOUND_ANNOUNCER_MUHAHA1');
+  g_Sound_Delete('SOUND_ANNOUNCER_MUHAHA2');
+  g_Sound_Delete('SOUND_ANNOUNCER_MUHAHA3');
+
+  sound_get_flag[0].Free();
+  sound_get_flag[1].Free();
+  sound_lost_flag[0].Free();
+  sound_lost_flag[1].Free();
+  sound_ret_flag[0].Free();
+  sound_ret_flag[1].Free();
+  sound_cap_flag[0].Free();
+  sound_cap_flag[1].Free();
+
+  g_Sound_Delete('SOUND_CTF_GET1');
+  g_Sound_Delete('SOUND_CTF_GET2');
+  g_Sound_Delete('SOUND_CTF_LOST1');
+  g_Sound_Delete('SOUND_CTF_LOST2');
+  g_Sound_Delete('SOUND_CTF_RETURN1');
+  g_Sound_Delete('SOUND_CTF_RETURN2');
+  g_Sound_Delete('SOUND_CTF_CAPTURE1');
+  g_Sound_Delete('SOUND_CTF_CAPTURE2');
+
+  g_Game_FreeChatSounds();
+
   DataLoaded := False;
 end;
 
@@ -2182,8 +2707,9 @@ var
 begin
   e_TextureFontGetSize(gStdFont, ww2, hh2);
 
-  e_PollInput();
-  if e_KeyPressed(IK_TAB) then
+  sys_HandleInput;
+
+  if g_Console_Action(ACTION_SCORES) then
   begin
     if not gStatsPressed then
     begin
@@ -2369,9 +2895,12 @@ begin
               gg := g;
               bb := b;
             end;
-            e_TextureFontPrintEx(x+8, _y, Name, gStdFont, rr, gg, bb, 1);
-            e_TextureFontPrintEx(x+w1+8, _y, IntToStr(Frags), gStdFont, rr, gg, bb, 1);
-            e_TextureFontPrintEx(x+w1+w2+8, _y, IntToStr(Deaths), gStdFont, rr, gg, bb, 1);
+            if (gPlayers[Num] <> nil) and (gPlayers[Num].FReady) then
+              e_TextureFontPrintEx(x+16, _y, Name + ' *', gStdFont, rr, gg, bb, 1)
+            else
+              e_TextureFontPrintEx(x+16, _y, Name, gStdFont, rr, gg, bb, 1);
+            e_TextureFontPrintEx(x+w1+16, _y, IntToStr(Frags), gStdFont, rr, gg, bb, 1);
+            e_TextureFontPrintEx(x+w1+w2+16, _y, IntToStr(Deaths), gStdFont, rr, gg, bb, 1);
             _y := _y+24;
           end;
 
@@ -2396,12 +2925,22 @@ begin
         else
           r := 255;
 
-        e_TextureFontPrintEx(x+8+16+8, _y+4, Name, gStdFont, r, r, r, 1, True);
+        if (gPlayers[Num] <> nil) and (gPlayers[Num].FReady) then
+          e_TextureFontPrintEx(x+8+16+8, _y+4, Name + ' *', gStdFont, r, r, r, 1, True)
+        else
+          e_TextureFontPrintEx(x+8+16+8, _y+4, Name, gStdFont, r, r, r, 1, True);
         e_TextureFontPrintEx(x+w1+8+16+8, _y+4, IntToStr(Frags), gStdFont, r, r, r, 1, True);
         e_TextureFontPrintEx(x+w1+w2+8+16+8, _y+4, IntToStr(Deaths), gStdFont, r, r, r, 1, True);
         _y := _y+24;
       end;
   end;
+
+  // HACK: take stats screenshot immediately after the first frame of the stats showing
+  if gScreenshotStats and (not StatShotDone) and (Length(CustomStat.PlayerStat) > 1) then
+  begin
+    g_TakeScreenShot('stats/' + StatFilename);
+    StatShotDone := True;
+  end;
 end;
 
 procedure DrawSingleStat();
@@ -2503,13 +3042,124 @@ begin
 end;
 
 procedure DrawLoadingStat();
+  procedure drawRect (x, y, w, h: Integer);
+  begin
+    if (w < 1) or (h < 1) then exit;
+    glBegin(GL_QUADS);
+      glVertex2f(x+0.375, y+0.375);
+      glVertex2f(x+w+0.375, y+0.375);
+      glVertex2f(x+w+0.375, y+h+0.375);
+      glVertex2f(x+0.375, y+h+0.375);
+    glEnd();
+  end;
+
+  function drawPBar (cur, total: Integer; washere: Boolean): Boolean;
+  var
+    rectW, rectH: Integer;
+    x0, y0: Integer;
+    wdt: Integer;
+    wl, hl: Integer;
+    wr, hr: Integer;
+    wb, hb: Integer;
+    wm, hm: Integer;
+    idl, idr, idb, idm: LongWord;
+    f, my: Integer;
+  begin
+    result := false;
+    if (total < 1) then exit;
+    if (cur < 1) then exit; // don't blink
+    if (not washere) and (cur >= total) then exit; // don't blink
+    //if (cur < 0) then cur := 0;
+    //if (cur > total) then cur := total;
+    result := true;
+
+    if (hasPBarGfx) then
+    begin
+      g_Texture_Get('UI_GFX_PBAR_LEFT', idl);
+      g_Texture_GetSize('UI_GFX_PBAR_LEFT', wl, hl);
+      g_Texture_Get('UI_GFX_PBAR_RIGHT', idr);
+      g_Texture_GetSize('UI_GFX_PBAR_RIGHT', wr, hr);
+      g_Texture_Get('UI_GFX_PBAR_MIDDLE', idb);
+      g_Texture_GetSize('UI_GFX_PBAR_MIDDLE', wb, hb);
+      g_Texture_Get('UI_GFX_PBAR_MARKER', idm);
+      g_Texture_GetSize('UI_GFX_PBAR_MARKER', wm, hm);
+
+      //rectW := gScreenWidth-360;
+      rectW := trunc(624.0*gScreenWidth/1024.0);
+      rectH := hl;
+
+      x0 := (gScreenWidth-rectW) div 2;
+      y0 := gScreenHeight-rectH-64;
+      if (y0 < 2) then y0 := 2;
+
+      glEnable(GL_SCISSOR_TEST);
+
+      // left and right
+      glScissor(x0, gScreenHeight-y0-rectH, rectW, rectH);
+      e_DrawSize(idl, x0, y0, 0, true, false, wl, hl);
+      e_DrawSize(idr, x0+rectW-wr, y0, 0, true, false, wr, hr);
+
+      // body
+      glScissor(x0+wl, gScreenHeight-y0-rectH, rectW-wl-wr, rectH);
+      f := x0+wl;
+      while (f < x0+rectW) do
+      begin
+        e_DrawSize(idb, f, y0, 0, true, false, wb, hb);
+        f += wb;
+      end;
+
+      // filled part
+      wdt := (rectW-wl-wr)*cur div total;
+      if (wdt > rectW-wl-wr) then wdt := rectW-wr-wr;
+      if (wdt > 0) then
+      begin
+        my := y0; // don't be so smart, ketmar: +(rectH-wm) div 2;
+        glScissor(x0+wl, gScreenHeight-my-rectH, wdt, hm);
+        f := x0+wl;
+        while (wdt > 0) do
+        begin
+          e_DrawSize(idm, f, y0, 0, true, false, wm, hm);
+          f += wm;
+          wdt -= wm;
+        end;
+      end;
+
+      glScissor(0, 0, gScreenWidth, gScreenHeight);
+    end
+    else
+    begin
+      rectW := gScreenWidth-64;
+      rectH := 16;
+
+      x0 := (gScreenWidth-rectW) div 2;
+      y0 := gScreenHeight-rectH-64;
+      if (y0 < 2) then y0 := 2;
+
+      glDisable(GL_BLEND);
+      glDisable(GL_TEXTURE_2D);
+
+      //glClearColor(0, 0, 0, 0);
+      //glClear(GL_COLOR_BUFFER_BIT);
+
+      glColor4ub(127, 127, 127, 255);
+      drawRect(x0-2, y0-2, rectW+4, rectH+4);
+
+      glColor4ub(0, 0, 0, 255);
+      drawRect(x0-1, y0-1, rectW+2, rectH+2);
+
+      glColor4ub(127, 127, 127, 255);
+      wdt := rectW*cur div total;
+      if (wdt > rectW) then wdt := rectW;
+      drawRect(x0, y0, wdt, rectH);
+    end;
+  end;
+
 var
   ww, hh: Word;
   xx, yy, i: Integer;
   s: String;
 begin
-  if Length(LoadingStat.Msgs) = 0 then
-    Exit;
+  if (Length(LoadingStat.Msgs) = 0) then exit;
 
   e_CharFont_GetSize(gMenuFont, _lc[I_MENU_LOADING], ww, hh);
   yy := (gScreenHeight div 3);
@@ -2517,21 +3167,43 @@ begin
   xx := (gScreenWidth div 3);
 
   with LoadingStat do
+  begin
     for i := 0 to NextMsg-1 do
-      begin
-        if (i = (NextMsg-1)) and (MaxValue > 0) then
-          s := Format('%s:  %d/%d', [Msgs[i], CurValue, MaxValue])
-        else
-          s := Msgs[i];
+    begin
+      if (i = (NextMsg-1)) and (MaxValue > 0) then
+        s := Format('%s:  %d/%d', [Msgs[i], CurValue, MaxValue])
+      else
+        s := Msgs[i];
 
-        e_CharFont_PrintEx(gMenuSmallFont, xx, yy, s, _RGB(255, 0, 0));
-        yy := yy + LOADING_INTERLINE;
-      end;
+      e_CharFont_PrintEx(gMenuSmallFont, xx, yy, s, _RGB(255, 0, 0));
+      yy := yy + LOADING_INTERLINE;
+      PBarWasHere := drawPBar(CurValue, MaxValue, PBarWasHere);
+    end;
+  end;
 end;
 
-procedure DrawMinimap(p: TPlayer; RenderRect: e_graphics.TRect);
+procedure DrawMenuBackground(tex: AnsiString);
 var
-  a, aX, aY, aX2, aY2, Scale, ScaleSz: Integer;
+  w, h: Word;
+  ID: DWord;
+
+begin
+  if g_Texture_Get(tex, ID) then
+  begin
+    e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+    e_GetTextureSize(ID, @w, @h);
+    if w = h then
+      w := round(w * 1.333 * (gScreenHeight / h))
+    else
+      w := trunc(w * (gScreenHeight / h));
+    e_DrawSize(ID, (gScreenWidth - w) div 2, 0, 0, False, False, w, gScreenHeight);
+  end
+  else e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+end;
+
+procedure DrawMinimap(p: TPlayer; RenderRect: e_graphics.TRect);
+var
+  a, aX, aY, aX2, aY2, Scale, ScaleSz: Integer;
 
   function monDraw (mon: TMonster): Boolean;
   begin
@@ -2629,10 +3301,10 @@ begin
             aY2 := aY + aY2 - 1;
 
             case LiftType of
-              0: e_DrawFillQuad(aX, aY, aX2, aY2, 116,  72,  36, 0);
-              1: e_DrawFillQuad(aX, aY, aX2, aY2, 116, 124,  96, 0);
-              2: e_DrawFillQuad(aX, aY, aX2, aY2, 200,  80,   4, 0);
-              3: e_DrawFillQuad(aX, aY, aX2, aY2, 252, 140,  56, 0);
+              LIFTTYPE_UP:    e_DrawFillQuad(aX, aY, aX2, aY2, 116,  72,  36, 0);
+              LIFTTYPE_DOWN:  e_DrawFillQuad(aX, aY, aX2, aY2, 116, 124,  96, 0);
+              LIFTTYPE_LEFT:  e_DrawFillQuad(aX, aY, aX2, aY2, 200,  80,   4, 0);
+              LIFTTYPE_RIGHT: e_DrawFillQuad(aX, aY, aX2, aY2, 252, 140,  56, 0);
             end;
           end;
     end;
@@ -2776,7 +3448,6 @@ begin
    *     glBlendFunc(GL_DST_ALPHA, GL_ONE);
    *     draw all geometry up to and including walls (with alpha-testing, probably) -- this does lighting
    *)
-
   wassc := (glIsEnabled(GL_SCISSOR_TEST) <> 0);
   if wassc then glGetIntegerv(GL_SCISSOR_BOX, @scxywh[0]) else glGetIntegerv(GL_VIEWPORT, @scxywh[0]);
 
@@ -2807,7 +3478,7 @@ begin
     end
     else
     begin
-      glScissor(0, 0, gWinSizeX, gWinSizeY);
+      glScissor(0, 0, gScreenWidth, gScreenHeight);
     end;
     // no need to clear stencil buffer, light blitting will do it for us... but only for normal scale
     if (g_dbg_scale <> 1.0) then glClear(GL_STENCIL_BUFFER_BIT);
@@ -2882,7 +3553,7 @@ var
     tagmask: Integer;
     pan: TPanel;
   begin
-    profileFrameDraw.sectionBegin(profname);
+    if (profileFrameDraw <> nil) then profileFrameDraw.sectionBegin(profname);
     if gdbg_map_use_accel_render then
     begin
       tagmask := panelTypeToTag(panType);
@@ -2898,31 +3569,31 @@ var
     begin
       if doDraw then g_Map_DrawPanels(panType, hasAmbient, ambColor);
     end;
-    profileFrameDraw.sectionEnd();
+    if (profileFrameDraw <> nil) then profileFrameDraw.sectionEnd();
   end;
 
   procedure drawOther (profname: AnsiString; cb: TDrawCB);
   begin
-    profileFrameDraw.sectionBegin(profname);
+    if (profileFrameDraw <> nil) then profileFrameDraw.sectionBegin(profname);
     if assigned(cb) then cb();
-    profileFrameDraw.sectionEnd();
+    if (profileFrameDraw <> nil) then profileFrameDraw.sectionEnd();
   end;
 
 begin
-  profileFrameDraw.sectionBegin('total');
+  if (profileFrameDraw <> nil) then profileFrameDraw.sectionBegin('total');
 
   // our accelerated renderer will collect all panels to gDrawPanelList
   // we can use panel tag to render level parts (see GridTagXXX in g_map.pas)
-  profileFrameDraw.sectionBegin('collect');
+  if (profileFrameDraw <> nil) then profileFrameDraw.sectionBegin('collect');
   if gdbg_map_use_accel_render then
   begin
     g_Map_CollectDrawPanels(sX, sY, sWidth, sHeight);
   end;
-  profileFrameDraw.sectionEnd();
+  if (profileFrameDraw <> nil) then profileFrameDraw.sectionEnd();
 
-  profileFrameDraw.sectionBegin('skyback');
+  if (profileFrameDraw <> nil) then profileFrameDraw.sectionBegin('skyback');
   g_Map_DrawBack(backXOfs, backYOfs);
-  profileFrameDraw.sectionEnd();
+  if (profileFrameDraw <> nil) then profileFrameDraw.sectionEnd();
 
   if setTransMatrix then
   begin
@@ -2979,7 +3650,7 @@ begin
     g_Player_DrawHealth();
   end;
 
-  profileFrameDraw.mainEnd(); // map rendering
+  if (profileFrameDraw <> nil) then profileFrameDraw.mainEnd(); // map rendering
 end;
 
 
@@ -3007,7 +3678,8 @@ end;
 
 procedure DrawPlayer(p: TPlayer);
 var
-  px, py, a, b, c, d: Integer;
+  px, py, a, b, c, d, i, fX, fY: Integer;
+  camObj: TObj;
   //R: TRect;
 begin
   if (p = nil) or (p.FDummy) then
@@ -3019,16 +3691,18 @@ begin
   end;
 
   if (profileFrameDraw = nil) then profileFrameDraw := TProfiler.Create('RENDER', g_profile_history_size);
-  profileFrameDraw.mainBegin(g_profile_frame_draw);
+  if (profileFrameDraw <> nil) then profileFrameDraw.mainBegin(g_profile_frame_draw);
 
   gPlayerDrawn := p;
 
   glPushMatrix();
 
-  px := p.GameX + PLAYER_RECT_CX;
-  py := p.GameY + PLAYER_RECT_CY+p.Obj.slopeUpLeft;
+  camObj := p.getCameraObj();
+  camObj.lerp(gLerpFactor, fX, fY);
+  px := fX + PLAYER_RECT_CX;
+  py := fY + PLAYER_RECT_CY+nlerp(p.SlopeOld, camObj.slopeUpLeft, gLerpFactor);
 
-  if (g_dbg_scale = 1.0) then
+  if (g_dbg_scale = 1.0) and (not g_dbg_ignore_bounds) then
   begin
     if (px > (gPlayerScreenSize.X div 2)) then a := -px+(gPlayerScreenSize.X div 2) else a := 0;
     if (py > (gPlayerScreenSize.Y div 2)) then b := -py+(gPlayerScreenSize.Y div 2) else b := 0;
@@ -3057,72 +3731,104 @@ begin
     b := -py+(gPlayerScreenSize.Y div 2);
   end;
 
-  if p.IncCam <> 0 then
-  begin
-    if py > gMapInfo.Height-(gPlayerScreenSize.Y div 2) then
-    begin
-      if p.IncCam > 120-(py-(gMapInfo.Height-(gPlayerScreenSize.Y div 2))) then
-      begin
-        p.IncCam := 120-(py-(gMapInfo.Height-(gPlayerScreenSize.Y div 2)));
-      end;
-    end;
-
-    if py < gPlayerScreenSize.Y div 2 then
-    begin
-      if p.IncCam < -120+((gPlayerScreenSize.Y div 2)-py) then
-      begin
-        p.IncCam := -120+((gPlayerScreenSize.Y div 2)-py);
-      end;
-    end;
-
-    if p.IncCam < 0 then
-    begin
-      while (py+(gPlayerScreenSize.Y div 2)-p.IncCam > gMapInfo.Height) and (p.IncCam < 0) do p.IncCam := p.IncCam+1; //Inc(p.IncCam);
-    end;
+  sX := -a;
+  sY := -b;
+  sWidth := gPlayerScreenSize.X;
+  sHeight := gPlayerScreenSize.Y;
+  fixViewportForScale();
 
-    if p.IncCam > 0 then
-    begin
-      while (py-(gPlayerScreenSize.Y div 2)-p.IncCam < 0) and (p.IncCam > 0) do p.IncCam := p.IncCam-1; //Dec(p.IncCam);
-    end;
+  i := py - (sY + sHeight div 2);
+  if (p.IncCam > 0) then
+  begin
+    // clamp to level bounds
+    if (sY - p.IncCam < 0) then
+      p.IncCam := nclamp(sY, 0, 120);
+    // clamp around player position
+    if (i > 0) then
+      p.IncCam := nclamp(p.IncCam, 0, max(0, 120 - i));
+  end
+  else if (p.IncCam < 0) then
+  begin
+    // clamp to level bounds
+    if (sY + sHeight - p.IncCam > gMapInfo.Height) then
+      p.IncCam := nclamp(sY + sHeight - gMapInfo.Height, -120, 0);
+    // clamp around player position
+    if (i < 0) then
+      p.IncCam := nclamp(p.IncCam, min(0, -120 - i), 0);
   end;
 
-       if (px < gPlayerScreenSize.X div 2) or (gMapInfo.Width-gPlayerScreenSize.X <= 256) then c := 0
-  else if (px > gMapInfo.Width-(gPlayerScreenSize.X div 2)) then c := gBackSize.X-gPlayerScreenSize.X
-  else c := round((px-(gPlayerScreenSize.X div 2))/(gMapInfo.Width-gPlayerScreenSize.X)*(gBackSize.X-gPlayerScreenSize.X));
-
-       if (py-p.IncCam <= gPlayerScreenSize.Y div 2) or (gMapInfo.Height-gPlayerScreenSize.Y <= 256) then d := 0
-  else if (py-p.IncCam >= gMapInfo.Height-(gPlayerScreenSize.Y div 2)) then d := gBackSize.Y-gPlayerScreenSize.Y
-  else d := round((py-p.IncCam-(gPlayerScreenSize.Y div 2))/(gMapInfo.Height-gPlayerScreenSize.Y)*(gBackSize.Y-gPlayerScreenSize.Y));
+  sY := sY - nlerp(p.IncCamOld, p.IncCam, gLerpFactor);
 
-  sX := -a;
-  sY := -(b+p.IncCam);
-  sWidth := gPlayerScreenSize.X;
-  sHeight := gPlayerScreenSize.Y;
+  if (not g_dbg_ignore_bounds) then
+  begin
+    if (sX+sWidth > gMapInfo.Width) then sX := gMapInfo.Width-sWidth;
+    if (sY+sHeight > gMapInfo.Height) then sY := gMapInfo.Height-sHeight;
+    if (sX < 0) then sX := 0;
+    if (sY < 0) then sY := 0;
+  end;
 
-  //glTranslatef(a, b+p.IncCam, 0);
+  if (gBackSize.X <= gPlayerScreenSize.X) or (gMapInfo.Width <= sWidth) then c := 0 else c := trunc((gBackSize.X-gPlayerScreenSize.X)*sX/(gMapInfo.Width-sWidth));
+  if (gBackSize.Y <= gPlayerScreenSize.Y) or (gMapInfo.Height <= sHeight) then d := 0 else d := trunc((gBackSize.Y-gPlayerScreenSize.Y)*sY/(gMapInfo.Height-sHeight));
 
-  //if (p = gPlayer1) and (g_dbg_scale >= 1.0) then g_Holmes_plrViewSize(sWidth, sHeight);
+  //r_smallmap_h: 0: left; 1: center; 2: right
+  //r_smallmap_v: 0: top; 1: center; 2: bottom
+  // horiz small map?
+  if (gMapInfo.Width = sWidth) then
+  begin
+    sX := 0;
+  end
+  else if (gMapInfo.Width < sWidth) then
+  begin
+    case r_smallmap_h of
+      1: sX := -((sWidth-gMapInfo.Width) div 2); // center
+      2: sX := -(sWidth-gMapInfo.Width); // right
+      else sX := 0; // left
+    end;
+  end;
+  // vert small map?
+  if (gMapInfo.Height = sHeight) then
+  begin
+    sY := 0;
+  end
+  else if (gMapInfo.Height < sHeight) then
+  begin
+    case r_smallmap_v of
+      1: sY := -((sHeight-gMapInfo.Height) div 2); // center
+      2: sY := -(sHeight-gMapInfo.Height); // bottom
+      else sY := 0; // top
+    end;
+  end;
 
-  //conwritefln('OLD: (%s,%s)-(%s,%s)', [sX, sY, sWidth, sHeight]);
-  fixViewportForScale();
-  //conwritefln('     (%s,%s)-(%s,%s)', [sX, sY, sWidth, sHeight]);
   p.viewPortX := sX;
   p.viewPortY := sY;
   p.viewPortW := sWidth;
   p.viewPortH := sHeight;
 
+{$IFDEF ENABLE_HOLMES}
   if (p = gPlayer1) then
   begin
     g_Holmes_plrViewPos(sX, sY);
     g_Holmes_plrViewSize(sWidth, sHeight);
   end;
+{$ENDIF}
 
   renderMapInternal(-c, -d, true);
 
-  if p.FSpectator then
-    e_TextureFontPrintEx(p.GameX + PLAYER_RECT_CX - 4,
-                         p.GameY + PLAYER_RECT_CY - 4,
-                         'X', gStdFont, 255, 255, 255, 1, True);
+  if (gGameSettings.GameMode <> GM_SINGLE) and (gPlayerIndicator > 0) then
+    case gPlayerIndicator of
+      1:
+        p.DrawIndicator(_RGB(255, 255, 255));
+
+      2:
+        for i := 0 to High(gPlayers) do
+          if gPlayers[i] <> nil then
+            if gPlayers[i] = p then p.DrawIndicator(_RGB(255, 255, 255))
+            else if (gPlayers[i].Team = p.Team) and (gPlayers[i].Team <> TEAM_NONE) then
+              if gPlayerIndicatorStyle = 1 then
+                gPlayers[i].DrawIndicator(_RGB(192, 192, 192))
+              else gPlayers[i].DrawIndicator(gPlayers[i].GetColor);
+    end;
+
   {
   for a := 0 to High(gCollideMap) do
     for b := 0 to High(gCollideMap[a]) do
@@ -3157,9 +3863,9 @@ var
   px: Integer = -1;
   py: Integer = -1;
 begin
-  if g_profile_frame_draw then px := px-drawProfiles(px, py, profileFrameDraw);
-  if g_profile_collision then begin px := px-drawProfiles(px, py, profMapCollision); py -= calcProfilesHeight(profMonsLOS); end;
-  if g_profile_los then begin px := px-drawProfiles(px, py, profMonsLOS); py -= calcProfilesHeight(profMonsLOS); end;
+  if g_profile_frame_draw and (profileFrameDraw <> nil) then px := px-drawProfiles(px, py, profileFrameDraw);
+  if g_profile_collision and (profMapCollision <> nil) then begin px := px-drawProfiles(px, py, profMapCollision); py -= calcProfilesHeight(profMonsLOS); end;
+  if g_profile_los and (profMonsLOS <> nil) then begin px := px-drawProfiles(px, py, profMonsLOS); py -= calcProfilesHeight(profMonsLOS); end;
 end;
 
 procedure g_Game_Draw();
@@ -3174,7 +3880,7 @@ var
 begin
   if gExit = EXIT_QUIT then Exit;
 
-  Time := GetTimer() {div 1000};
+  Time := sys_GetTicks() {div 1000};
   FPSCounter := FPSCounter+1;
   if Time - FPSTime >= 1000 then
   begin
@@ -3183,6 +3889,9 @@ begin
     FPSTime := Time;
   end;
 
+  e_SetRendertarget(True);
+  e_SetViewPort(0, 0, gScreenWidth, gScreenHeight);
+
   if gGameOn or (gState = STATE_FOLD) then
   begin
     if (gPlayer1 <> nil) and (gPlayer2 <> nil) then
@@ -3257,15 +3966,15 @@ begin
       if plView1 <> nil then
       begin
         gHearPoint1.Active := True;
-        gHearPoint1.Coords.X := plView1.GameX;
-        gHearPoint1.Coords.Y := plView1.GameY;
+        gHearPoint1.Coords.X := plView1.GameX + PLAYER_RECT.Width;
+        gHearPoint1.Coords.Y := plView1.GameY + PLAYER_RECT.Height DIV 2;
       end else
         gHearPoint1.Active := False;
       if plView2 <> nil then
       begin
         gHearPoint2.Active := True;
-        gHearPoint2.Coords.X := plView2.GameX;
-        gHearPoint2.Coords.Y := plView2.GameY;
+        gHearPoint2.Coords.X := plView2.GameX + PLAYER_RECT.Width;
+        gHearPoint2.Coords.Y := plView2.GameY + PLAYER_RECT.Height DIV 2;
       end else
         gHearPoint2.Active := False;
 
@@ -3305,6 +4014,11 @@ begin
         e_DrawLine(2, 0, gScreenHeight div 2, gScreenWidth, gScreenHeight div 2, 0, 0, 0);
     end;
 
+{$IFDEF ENABLE_HOLMES}
+    // draw inspector
+    if (g_holmes_enabled) then g_Holmes_Draw();
+{$ENDIF}
+
     if MessageText <> '' then
     begin
       w := 0;
@@ -3318,9 +4032,10 @@ begin
                   Round(gScreenHeight / 2.75)-(h div 2), MessageText);
     end;
 
-    if IsDrawStat or (gSpectMode = 1) then DrawStat();
+    if IsDrawStat or (gSpectMode = SPECT_STATS) then
+      DrawStat();
 
-    if gSpectHUD and (not gChatShow) and (gSpectMode <> SPECT_NONE) then
+    if gSpectHUD and (not gChatShow) and (gSpectMode <> SPECT_NONE) and (not gSpectAuto) then
     begin
     // Draw spectator GUI
       ww := 0;
@@ -3335,6 +4050,11 @@ begin
           e_TextureFontPrintEx(0, gScreenHeight - (hh+2)*2, 'MODE: Watch Players', gStdFont, 255, 255, 255, 1);
       end;
       e_TextureFontPrintEx(2*ww, gScreenHeight - (hh+2), '< jump >', gStdFont, 255, 255, 255, 1);
+      if gSpectMode = SPECT_STATS then
+      begin
+        e_TextureFontPrintEx(16*ww, gScreenHeight - (hh+2)*2, 'Autoview', gStdFont, 255, 255, 255, 1);
+        e_TextureFontPrintEx(16*ww, gScreenHeight - (hh+2), '< fire >', gStdFont, 255, 255, 255, 1);
+      end;
       if gSpectMode = SPECT_MAPVIEW then
       begin
         e_TextureFontPrintEx(22*ww, gScreenHeight - (hh+2)*2, '[-]', gStdFont, 255, 255, 255, 1);
@@ -3377,11 +4097,7 @@ begin
   begin
     if (gState = STATE_MENU) then
     begin
-      if (g_ActiveWindow = nil) or (g_ActiveWindow.BackTexture = '') then
-      begin
-        if g_Texture_Get('MENU_BACKGROUND', ID) then e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
-        else e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
-      end;
+      if (g_ActiveWindow = nil) or (g_ActiveWindow.BackTexture = '') then DrawMenuBackground('MENU_BACKGROUND');
       // F3 at menu will show game loading dialog
       if e_KeyPressed(IK_F3) then g_Menu_Show_LoadMenu(true);
       if (g_ActiveWindow <> nil) then
@@ -3416,10 +4132,7 @@ begin
       else
         back := 'INTER';
 
-      if g_Texture_Get(back, ID) then
-        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
-      else
-        e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+      DrawMenuBackground(back);
 
       DrawCustomStat();
 
@@ -3440,10 +4153,7 @@ begin
       begin
         back := 'INTER';
 
-        if g_Texture_Get(back, ID) then
-          e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
-        else
-          e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+        DrawMenuBackground(back);
 
         DrawSingleStat();
 
@@ -3458,13 +4168,8 @@ begin
     if gState = STATE_ENDPIC then
     begin
       ID := DWORD(-1);
-      if not g_Texture_Get('TEXTURE_endpic', ID) then
-        g_Texture_Get(_lc[I_TEXTURE_ENDPIC], ID);
-
-      if ID <> DWORD(-1) then
-        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
-      else
-        e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+      if g_Texture_Get('TEXTURE_endpic', ID) then DrawMenuBackground('TEXTURE_endpic')
+      else DrawMenuBackground(_lc[I_TEXTURE_ENDPIC]);
 
       if g_ActiveWindow <> nil then
       begin
@@ -3475,13 +4180,14 @@ begin
 
     if gState = STATE_SLIST then
     begin
-      if g_Texture_Get('MENU_BACKGROUND', ID) then
-      begin
-        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight);
-        //e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
-        e_DarkenQuadWH(0, 0, gScreenWidth, gScreenHeight, 150);
-      end;
-      g_Serverlist_Draw(slCurrent);
+//      if g_Texture_Get('MENU_BACKGROUND', ID) then
+//      begin
+//        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight);
+//        //e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+//      end;
+      DrawMenuBackground('MENU_BACKGROUND');
+      e_DarkenQuadWH(0, 0, gScreenWidth, gScreenHeight, 150);
+      g_Serverlist_Draw(slCurrent, slTable);
     end;
   end;
 
@@ -3495,10 +4201,9 @@ begin
     g_ActiveWindow.Draw();
   end;
 
-  // draw inspector
-  if (g_holmes_enabled) then g_Holmes_Draw();
-
+{$IFNDEF HEADLESS}
   g_Console_Draw();
+{$ENDIF}
 
   if g_debug_Sounds and gGameOn then
   begin
@@ -3513,24 +4218,39 @@ begin
     e_TextureFontPrint(0, 16, Format('UPS: %d', [UPS]), gStdFont);
   end;
 
-  if gGameOn and gShowTime and (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT]) then
+  if gGameOn and gShowTime then
     drawTime(gScreenWidth-72, gScreenHeight-16);
 
   if gGameOn then drawProfilers();
 
+  // TODO: draw this after the FBO and remap mouse click coordinates
+
+{$IFDEF ENABLE_HOLMES}
   g_Holmes_DrawUI();
+{$ENDIF}
+
+  // blit framebuffer to screen
+
+  e_SetRendertarget(False);
+  e_SetViewPort(0, 0, gWinSizeX, gWinSizeY);
+  e_BlitFramebuffer(gWinSizeX, gWinSizeY);
+
+  // draw the overlay stuff on top of it
+
+  g_Touch_Draw;
 end;
 
 procedure g_Game_Quit();
 begin
   g_Game_StopAllSounds(True);
   gMusic.Free();
-  g_Game_SaveOptions();
   g_Game_FreeData();
   g_PlayerModel_FreeData();
   g_Texture_DeleteAll();
   g_Frames_DeleteAll();
-  g_Menu_Free();
+{$IFNDEF HEADLESS}
+  //g_Menu_Free(); //k8: this segfaults after resolution change; who cares?
+{$ENDIF}
 
   if NetInitDone then g_Net_Free;
 
@@ -3539,7 +4259,7 @@ begin
     g_Game_DeleteTestMap();
 
   gExit := EXIT_QUIT;
-  PushExitEvent();
+  sys_RequestQuit;
 end;
 
 procedure g_FatalError(Text: String);
@@ -3548,6 +4268,7 @@ begin
   e_WriteLog(Format(_lc[I_FATAL_ERROR], [Text]), TMsgType.Warning);
 
   gExit := EXIT_SIMPLE;
+  if gGameOn then EndGame;
 end;
 
 procedure g_SimpleError(Text: String);
@@ -3597,7 +4318,7 @@ end;
 
 procedure g_Game_ChangeResolution(newWidth, newHeight: Word; nowFull, nowMax: Boolean);
 begin
-  g_Window_SetSize(newWidth, newHeight, nowFull);
+  sys_SetDisplayMode(newWidth, newHeight, gBPP, nowFull, nowMax);
 end;
 
 procedure g_Game_AddPlayer(Team: Byte = TEAM_NONE);
@@ -3605,15 +4326,16 @@ begin
   if ((not gGameOn) and (gState <> STATE_INTERCUSTOM))
   or (not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT])) then
     Exit;
+
+  if (gGameSettings.MaxLives > 0) and (gLMSRespawn = LMS_RESPAWN_NONE) then
+    Exit;
+
   if gPlayer1 = nil then
   begin
     if g_Game_IsClient then
     begin
       if NetPlrUID1 > -1 then
-      begin
         MC_SEND_CheatRequest(NET_CHEAT_SPECTATE);
-        gPlayer1 := g_Player_Get(NetPlrUID1);
-      end;
       Exit;
     end;
 
@@ -3629,13 +4351,15 @@ begin
     else
     begin
       gPlayer1.Name := gPlayer1Settings.Name;
+      gPlayer1.WeapSwitchMode := gPlayer1Settings.WeaponSwitch;
+      gPlayer1.setWeaponPrefs(gPlayer1Settings.WeaponPreferences);
+      gPlayer1.SwitchToEmpty := gPlayer1Settings.SwitchToEmpty;
+      gPlayer1.SkipFist := gPlayer1Settings.SkipFist;
       g_Console_Add(Format(_lc[I_PLAYER_JOIN], [gPlayer1.Name]), True);
       if g_Game_IsServer and g_Game_IsNet then
         MH_SEND_PlayerCreate(gPlayer1.UID);
       gPlayer1.Respawn(False, True);
-
-      if g_Game_IsNet and NetUseMaster then
-        g_Net_Slist_Update;
+      g_Net_Slist_ServerPlayerComes();
     end;
 
     Exit;
@@ -3661,13 +4385,15 @@ begin
     else
     begin
       gPlayer2.Name := gPlayer2Settings.Name;
+      gPlayer2.WeapSwitchMode := gPlayer2Settings.WeaponSwitch;
+      gPlayer2.setWeaponPrefs(gPlayer2Settings.WeaponPreferences);
+      gPlayer2.SwitchToEmpty := gPlayer2Settings.SwitchToEmpty;
+      gPlayer2.SkipFist := gPlayer2Settings.SkipFist;
       g_Console_Add(Format(_lc[I_PLAYER_JOIN], [gPlayer2.Name]), True);
       if g_Game_IsServer and g_Game_IsNet then
         MH_SEND_PlayerCreate(gPlayer2.UID);
       gPlayer2.Respawn(False, True);
-
-      if g_Game_IsNet and NetUseMaster then
-        g_Net_Slist_Update;
+      g_Net_Slist_ServerPlayerComes();
     end;
 
     Exit;
@@ -3690,11 +4416,13 @@ begin
       Pl.Kill(K_SIMPLEKILL, 0, HIT_DISCON);
       g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [Pl.Name]), True);
       g_Player_Remove(Pl.UID);
-
-      if g_Game_IsNet and NetUseMaster then
-        g_Net_Slist_Update;
-    end else
+      g_Net_Slist_ServerPlayerLeaves();
+    end
+    else
+    begin
+      gSpectLatchPID2 := Pl.UID;
       gPlayer2 := nil;
+    end;
     Exit;
   end;
   Pl := gPlayer1;
@@ -3706,16 +4434,16 @@ begin
       Pl.Kill(K_SIMPLEKILL, 0, HIT_DISCON);
       g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [Pl.Name]), True);
       g_Player_Remove(Pl.UID);
-
-      if g_Game_IsNet and NetUseMaster then
-        g_Net_Slist_Update;
+      g_Net_Slist_ServerPlayerLeaves();
     end else
     begin
+      gSpectLatchPID1 := Pl.UID;
       gPlayer1 := nil;
       MC_SEND_CheatRequest(NET_CHEAT_SPECTATE);
     end;
     Exit;
   end;
+  g_Net_Slist_ServerPlayerLeaves();
 end;
 
 procedure g_Game_Spectate();
@@ -3734,6 +4462,7 @@ end;
 procedure g_Game_StartSingle(Map: String; TwoPlayers: Boolean; nPlayers: Byte);
 var
   i, nPl: Integer;
+  tmps: AnsiString;
 begin
   g_Game_Free();
 
@@ -3750,8 +4479,15 @@ begin
   gGameSettings.Options := gGameSettings.Options + GAME_OPTION_ALLOWEXIT;
   gGameSettings.Options := gGameSettings.Options + GAME_OPTION_MONSTERS;
   gGameSettings.Options := gGameSettings.Options + GAME_OPTION_BOTVSMONSTER;
+  gGameSettings.Options := gGameSettings.Options + GAME_OPTION_TEAMHITPROJECTILE;
+  gGameSettings.Options := gGameSettings.Options + GAME_OPTION_TEAMHITTRACE;
   gSwitchGameMode := GM_SINGLE;
 
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+  gSpectLatchPID1 := 0;
+  gSpectLatchPID2 := 0;
+
   g_Game_ExecuteEvent('ongamestart');
 
 // Óñòàíîâêà ðàçìåðîâ îêîí èãðîêîâ:
@@ -3768,6 +4504,10 @@ begin
   end;
 
   gPlayer1.Name := gPlayer1Settings.Name;
+  gPlayer1.WeapSwitchMode := gPlayer1Settings.WeaponSwitch;
+  gPlayer1.setWeaponPrefs(gPlayer1Settings.WeaponPreferences);
+  gPlayer1.SwitchToEmpty := gPlayer1Settings.SwitchToEmpty;
+  gPlayer1.SkipFist := gPlayer1Settings.SkipFist;
   nPl := 1;
 
 // Ñîçäàíèå âòîðîãî èãðîêà, åñëè åñòü:
@@ -3783,13 +4523,18 @@ begin
     end;
 
     gPlayer2.Name := gPlayer2Settings.Name;
+    gPlayer2.WeapSwitchMode := gPlayer2Settings.WeaponSwitch;
+    gPlayer2.setWeaponPrefs(gPlayer2Settings.WeaponPreferences);
+    gPlayer2.SwitchToEmpty := gPlayer2Settings.SwitchToEmpty;
+    gPlayer2.SkipFist := gPlayer2Settings.SkipFist;
     Inc(nPl);
   end;
 
 // Çàãðóçêà è çàïóñê êàðòû:
-  if not g_Game_StartMap(MAP, True) then
+  if not g_Game_StartMap(false{asMegawad}, MAP, True) then
   begin
-    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [gGameSettings.WAD + ':\' + MAP]));
+    if (Pos(':\', Map) > 0) or (Pos(':/', Map) > 0) then tmps := Map else tmps := gGameSettings.WAD + ':\' + MAP;
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [tmps]));
     Exit;
   end;
 
@@ -3830,6 +4575,11 @@ begin
   gAimLine := False;
   gShowMap := False;
 
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+  gSpectLatchPID1 := 0;
+  gSpectLatchPID2 := 0;
+
   g_Game_ExecuteEvent('ongamestart');
 
 // Óñòàíîâêà ðàçìåðîâ îêîí èãðîêîâ:
@@ -3856,6 +4606,10 @@ begin
     end;
 
     gPlayer1.Name := gPlayer1Settings.Name;
+    gPlayer1.WeapSwitchMode := gPlayer1Settings.WeaponSwitch;
+    gPlayer1.setWeaponPrefs(gPlayer1Settings.WeaponPreferences);
+    gPlayer1.SwitchToEmpty := gPlayer1Settings.SwitchToEmpty;
+    gPlayer1.SkipFist := gPlayer1Settings.SkipFist;
     Inc(nPl);
   end;
 
@@ -3872,11 +4626,15 @@ begin
     end;
 
     gPlayer2.Name := gPlayer2Settings.Name;
+    gPlayer2.WeapSwitchMode := gPlayer2Settings.WeaponSwitch;
+    gPlayer2.setWeaponPrefs(gPlayer2Settings.WeaponPreferences);
+    gPlayer2.SwitchToEmpty := gPlayer2Settings.SwitchToEmpty;
+    gPlayer2.SkipFist := gPlayer2Settings.SkipFist;
     Inc(nPl);
   end;
 
 // Çàãðóçêà è çàïóñê êàðòû:
-  if not g_Game_StartMap(Map, True) then
+  if not g_Game_StartMap(true{asMegawad}, Map, True) then
   begin
     g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [Map]));
     Exit;
@@ -3907,6 +4665,7 @@ procedure g_Game_StartServer(Map: String; GameMode: Byte;
                              IPAddr: LongWord; Port: Word);
 begin
   g_Game_Free();
+  g_Net_Slist_ServerClosed();
 
   e_WriteLog('Starting net game (server)...', TMsgType.Notify);
 
@@ -3928,6 +4687,11 @@ begin
   gAimLine := False;
   gShowMap := False;
 
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+  gSpectLatchPID1 := 0;
+  gSpectLatchPID2 := 0;
+
   g_Game_ExecuteEvent('ongamestart');
 
 // Óñòàíîâêà ðàçìåðîâ îêíà èãðîêà
@@ -3953,6 +4717,10 @@ begin
     end;
 
     gPlayer1.Name := gPlayer1Settings.Name;
+    gPlayer1.WeapSwitchMode := gPlayer1Settings.WeaponSwitch;
+    gPlayer1.setWeaponPrefs(gPlayer1Settings.WeaponPreferences);
+    gPlayer1.SwitchToEmpty := gPlayer1Settings.SwitchToEmpty;
+    gPlayer1.SkipFist := gPlayer1Settings.SkipFist;
   end;
 
   if nPlayers >= 2 then
@@ -3968,20 +4736,31 @@ begin
     end;
 
     gPlayer2.Name := gPlayer2Settings.Name;
+    gPlayer2.WeapSwitchMode := gPlayer2Settings.WeaponSwitch;
+    gPlayer2.setWeaponPrefs(gPlayer2Settings.WeaponPreferences);
+    gPlayer2.SwitchToEmpty := gPlayer2Settings.SwitchToEmpty;
+    gPlayer2.SkipFist := gPlayer2Settings.SkipFist;
   end;
 
+  g_Game_SetLoadingText(_lc[I_LOAD_HOST], 0, False);
+  if NetForwardPorts then
+    g_Game_SetLoadingText(_lc[I_LOAD_PORTS], 0, False);
+
 // Ñòàðòóåì ñåðâåð
   if not g_Net_Host(IPAddr, Port, NetMaxClients) then
   begin
-    g_FatalError(_lc[I_NET_MSG] + _lc[I_NET_ERR_HOST]);
+    g_FatalError(_lc[I_NET_MSG] + Format(_lc[I_NET_ERR_HOST], [Port]));
     Exit;
   end;
 
-  g_Net_Slist_Set(NetSlistIP, NetSlistPort);
+  g_Net_Slist_Set(NetMasterList);
+
+  g_Net_Slist_ServerStarted();
 
 // Çàãðóçêà è çàïóñê êàðòû:
-  if not g_Game_StartMap(Map, True) then
+  if not g_Game_StartMap(false{asMegawad}, Map, True) then
   begin
+    g_Net_Slist_ServerClosed();
     g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [Map]));
     Exit;
   end;
@@ -3993,6 +4772,7 @@ begin
       g_Map_GetPointCount(RESPAWNPOINT_RED)+
       g_Map_GetPointCount(RESPAWNPOINT_BLUE)) < 1 then
   begin
+    g_Net_Slist_ServerClosed();
     g_FatalError(_lc[I_GAME_ERROR_GET_SPAWN]);
     Exit;
   end;
@@ -4000,6 +4780,7 @@ begin
 // Íàñòðîéêè èãðîêîâ è áîòîâ:
   g_Player_Init();
 
+  g_Net_Slist_ServerMapStarted();
   NetState := NET_STATE_GAME;
 end;
 
@@ -4041,6 +4822,15 @@ begin
   NetState := NET_STATE_AUTH;
 
   g_Game_SetLoadingText(_lc[I_LOAD_CONNECT], 0, False);
+
+  // create (or update) map/resource databases
+  g_Res_CreateDatabases(true);
+
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+  gSpectLatchPID1 := 0;
+  gSpectLatchPID2 := 0;
+
 // Ñòàðòóåì êëèåíò
   if not g_Net_Connect(Addr, Port) then
   begin
@@ -4056,14 +4846,27 @@ begin
   OuterLoop := True;
   while OuterLoop do
   begin
-    while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+    // fuck! https://www.mail-archive.com/enet-discuss@cubik.org/msg00852.html
+    // tl;dr: on shitdows, we can get -1 sometimes, and it is *NOT* a failure.
+    //        thank you, enet. let's ignore failures altogether then.
+    while (enet_host_service(NetHost, @NetEvent, 50) > 0) do
     begin
       if (NetEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
       begin
+        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then
+        begin
+          // ignore all download packets, they're processed by separate code
+          enet_packet_destroy(NetEvent.packet);
+          continue;
+        end;
         Ptr := NetEvent.packet^.data;
         if not InMsg.Init(Ptr, NetEvent.packet^.dataLength, True) then
+        begin
+          enet_packet_destroy(NetEvent.packet);
           continue;
+        end;
 
+        InMsg.ReadLongWord(); // skip size
         MID := InMsg.ReadByte();
 
         if (MID = NET_MSG_INFO) and (State = 0) then
@@ -4084,11 +4887,11 @@ begin
           gGameSettings.Options := InMsg.ReadLongWord();
           T := InMsg.ReadLongWord();
 
-          newResPath := g_Res_SearchSameWAD(MapsDir, WadName, gWADHash);
-          if newResPath = '' then
+          //newResPath := g_Res_SearchSameWAD(MapsDir, WadName, gWADHash);
+          //if newResPath = '' then
           begin
-            g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
-            newResPath := g_Res_DownloadWAD(WadName);
+            //g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
+            newResPath := g_Res_DownloadMapWAD(ExtractFileName(WadName), gWADHash);
             if newResPath = '' then
             begin
               g_FatalError(_lc[I_NET_ERR_HASH]);
@@ -4096,8 +4899,10 @@ begin
               NetState := NET_STATE_NONE;
               Exit;
             end;
+            e_LogWritefln('using downloaded map wad [%s] for [%s]`', [newResPath, WadName], TMsgType.Notify);
           end;
-          newResPath := ExtractRelativePath(MapsDir, newResPath);
+          //newResPath := ExtractRelativePath(MapsDir, newResPath);
+
 
           gPlayer1 := g_Player_Get(g_Player_Create(gPlayer1Settings.Model,
                                                    gPlayer1Settings.Color,
@@ -4113,10 +4918,14 @@ begin
           end;
 
           gPlayer1.Name := gPlayer1Settings.Name;
+          gPlayer1.WeapSwitchMode := gPlayer1Settings.WeaponSwitch;
+          gPlayer1.setWeaponPrefs(gPlayer1Settings.WeaponPreferences);
+          gPlayer1.SwitchToEmpty := gPlayer1Settings.SwitchToEmpty;
+          gPlayer1.SkipFist := gPlayer1Settings.SkipFist;
           gPlayer1.UID := NetPlrUID1;
           gPlayer1.Reset(True);
 
-          if not g_Game_StartMap(newResPath + ':\' + Map, True) then
+          if not g_Game_StartMap(false{asMegawad}, newResPath + ':\' + Map, True) then
           begin
             g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [WadName + ':\' + Map]));
 
@@ -4136,6 +4945,7 @@ begin
           enet_packet_destroy(NetEvent.packet);
       end
       else
+      begin
         if (NetEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
         begin
           State := 0;
@@ -4145,13 +4955,12 @@ begin
           OuterLoop := False;
           Break;
         end;
+      end;
     end;
 
     ProcessLoading(true);
 
-    e_PollInput();
-
-    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
+    if g_Net_UserRequestExit() then
     begin
       State := 0;
       break;
@@ -4165,19 +4974,14 @@ begin
     Exit;
   end;
 
-  gLMSRespawn := LMS_RESPAWN_NONE;
-  gLMSRespawnTime := 0;
-
   g_Player_Init();
   NetState := NET_STATE_GAME;
   MC_SEND_FullStateRequest;
   e_WriteLog('NET: Connection successful.', TMsgType.Notify);
 end;
 
-procedure g_Game_SaveOptions();
-begin
-  g_Options_Write_Video(GameDir+'/'+CONFIG_FILENAME);
-end;
+var
+  lastAsMegaWad: Boolean = false;
 
 procedure g_Game_ChangeMap(const MapPath: String);
 var
@@ -4192,7 +4996,7 @@ begin
     Force := False;
     gExitByTrigger := False;
   end;
-  if not g_Game_StartMap(MapPath, Force) then
+  if not g_Game_StartMap(lastAsMegaWad, MapPath, Force) then
     g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [MapPath]));
 end;
 
@@ -4203,17 +5007,19 @@ begin
   if g_Game_IsClient then
     Exit;
   map := g_ExtractFileName(gMapInfo.Map);
+  e_LogWritefln('g_Game_Restart: map = "%s" gCurrentMapFileName = "%s"', [map, gCurrentMapFileName]);
 
   MessageTime := 0;
   gGameOn := False;
   g_Game_ClearLoading();
-  g_Game_StartMap(Map, True, gCurrentMapFileName);
+  g_Game_StartMap(lastAsMegaWad, Map, True, gCurrentMapFileName);
 end;
 
-function g_Game_StartMap(Map: String; Force: Boolean = False; const oldMapPath: AnsiString=''): Boolean;
+function g_Game_StartMap (asMegawad: Boolean; Map: String; Force: Boolean = False; const oldMapPath: AnsiString=''): Boolean;
 var
   NewWAD, ResName: String;
   I: Integer;
+  nws: AnsiString;
 begin
   g_Map_Free((Map <> gCurrentMapFileName) and (oldMapPath <> gCurrentMapFileName));
   g_Player_RemoveAllCorpses();
@@ -4231,21 +5037,61 @@ begin
 
   g_Player_ResetTeams();
 
+  lastAsMegaWad := asMegawad;
   if isWadPath(Map) then
   begin
     NewWAD := g_ExtractWadName(Map);
     ResName := g_ExtractFileName(Map);
     if g_Game_IsServer then
     begin
-      gWADHash := MD5File(MapsDir + NewWAD);
-      g_Game_LoadWAD(NewWAD);
-    end else
-      // hash recieved in MC_RECV_GameEvent -> NET_EV_MAPSTART
-      g_Game_ClientWAD(NewWAD, gWADHash);
-  end else
+      nws := findDiskWad(NewWAD);
+      //writeln('000: Map=[', Map, ']; nws=[', nws, ']; NewWAD=[', NewWAD, ']');
+      if (asMegawad) then
+      begin
+        if (length(nws) = 0) then nws := e_FindWad(MegawadDirs, NewWAD);
+        if (length(nws) = 0) then nws := e_FindWad(MapDirs, NewWAD);
+      end
+      else
+      begin
+        if (length(nws) = 0) then nws := e_FindWad(MapDirs, NewWAD);
+        if (length(nws) = 0) then nws := e_FindWad(MegawadDirs, NewWAD);
+      end;
+      //if (length(nws) = 0) then nws := e_FindWad(MapDownloadDirs, NewWAD);
+      //writeln('001: Map=[', Map, ']; nws=[', nws, ']; NewWAD=[', NewWAD, ']');
+      //nws := NewWAD;
+      if (length(nws) = 0) then
+      begin
+        ResName := ''; // failed
+      end
+      else
+      begin
+        NewWAD := nws;
+        if (g_Game_IsNet) then gWADHash := MD5File(nws);
+        //writeln('********: nws=', nws, ' : Map=', Map, ' : nw=', NewWAD, ' : resname=', ResName);
+        g_Game_LoadWAD(NewWAD);
+      end;
+    end
+    else
+    begin
+      // hash received in MC_RECV_GameEvent -> NET_EV_MAPSTART
+      NewWAD := g_Game_ClientWAD(NewWAD, gWADHash);
+    end;
+  end
+  else
+  begin
+    NewWAD := gGameSettings.WAD;
     ResName := Map;
+  end;
 
-  Result := g_Map_Load(MapsDir + gGameSettings.WAD + ':\' + ResName);
+  gTime := 0;
+
+  //writeln('********: gsw=', gGameSettings.WAD, '; rn=', ResName);
+  result := false;
+  if (ResName <> '') and (NewWAD <> '') then
+  begin
+    //result := g_Map_Load(gGameSettings.WAD + ':\' + ResName);
+    result := g_Map_Load(NewWAD+':\'+ResName);
+  end;
   if Result then
     begin
       g_Player_ResetAll(Force or gLastMap, gGameSettings.GameType = GT_SINGLE);
@@ -4275,12 +5121,11 @@ begin
   gExit := 0;
   gPauseMain := false;
   gPauseHolmes := false;
-  gTime := 0;
   NetTimeToUpdate := 1;
   NetTimeToReliable := 0;
   NetTimeToMaster := NetMasterRate;
-  gLMSRespawn := LMS_RESPAWN_NONE;
-  gLMSRespawnTime := 0;
+  gSpectLatchPID1 := 0;
+  gSpectLatchPID2 := 0;
   gMissionFailed := False;
   gNextMap := '';
 
@@ -4298,15 +5143,21 @@ begin
 
   g_Game_SpectateCenterView();
 
-  if (gGameSettings.MaxLives > 0) and (gGameSettings.WarmupTime > 0) then
+  if g_Game_IsServer then
   begin
-    gLMSRespawn := LMS_RESPAWN_WARMUP;
-    gLMSRespawnTime := gTime + gGameSettings.WarmupTime*1000;
-    gLMSSoftSpawn := True;
-    if NetMode = NET_SERVER then
-      MH_SEND_GameEvent(NET_EV_LMS_WARMUP, (gLMSRespawnTime - gTime) div 1000)
+    if (gGameSettings.MaxLives > 0) and (gGameSettings.WarmupTime > 0) then
+    begin
+      gLMSRespawn := LMS_RESPAWN_WARMUP;
+      gLMSRespawnTime := gTime + gGameSettings.WarmupTime*1000;
+      gLMSSoftSpawn := True;
+      if g_Game_IsNet then
+        MH_SEND_GameEvent(NET_EV_LMS_WARMUP, gLMSRespawnTime - gTime);
+    end
     else
-      g_Console_Add(Format(_lc[I_MSG_WARMUP_START], [(gLMSRespawnTime - gTime) div 1000]), True);
+    begin
+      gLMSRespawn := LMS_RESPAWN_NONE;
+      gLMSRespawnTime := 0;
+    end;
   end;
 
   if NetMode = NET_SERVER then
@@ -4314,14 +5165,7 @@ begin
     MH_SEND_GameEvent(NET_EV_MAPSTART, gGameSettings.GameMode, Map);
 
   // Ìàñòåðñåðâåð
-    if NetUseMaster then
-    begin
-      if (NetMHost = nil) or (NetMPeer = nil) then
-        if not g_Net_Slist_Connect then
-          g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR]);
-
-      g_Net_Slist_Update;
-    end;
+    g_Net_Slist_ServerMapStarted();
 
     if NetClients <> nil then
       for I := 0 to High(NetClients) do
@@ -4350,11 +5194,11 @@ begin
   g_Game_ExecuteEvent('onmapstart');
 end;
 
-procedure SetFirstLevel();
+procedure SetFirstLevel;
 begin
   gNextMap := '';
 
-  MapList := g_Map_GetMapsList(MapsDir + gGameSettings.WAD);
+  MapList := g_Map_GetMapsList(gGameSettings.WAD);
   if MapList = nil then
     Exit;
 
@@ -4382,7 +5226,7 @@ begin
     if gGameSettings.GameMode = GM_COOP then
       g_Player_RememberAll;
 
-    if not g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + gNextMap) then
+    if not g_Map_Exist(gGameSettings.WAD + ':\' + gNextMap) then
     begin
       gLastMap := True;
       if gGameSettings.GameMode = GM_COOP then
@@ -4391,7 +5235,7 @@ begin
       gStatsPressed := True;
       gNextMap := 'MAP01';
 
-      if not g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + gNextMap) then
+      if not g_Map_Exist(gGameSettings.WAD + ':\' + gNextMap) then
         g_Game_NextLevel;
 
       if g_Game_IsNet then
@@ -4417,19 +5261,36 @@ begin
   gNextMap := Map;
 end;
 
-procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest);
+function g_Game_ClientWAD (NewWAD: String; const WHash: TMD5Digest): AnsiString;
 var
-  gWAD: String;
+  gWAD{, xwad}: String;
 begin
-  if LowerCase(NewWAD) = LowerCase(gGameSettings.WAD) then
-    Exit;
-  if not g_Game_IsClient then
+  result := NewWAD;
+  if not g_Game_IsClient then Exit;
+  //e_LogWritefln('*** g_Game_ClientWAD: `%s`', [NewWAD]);
+
+  gWAD := g_Res_DownloadMapWAD(ExtractFileName(NewWAD), WHash);
+  if gWAD = '' then
+  begin
+    result := '';
+    g_Game_Free();
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_WAD], [ExtractFileName(NewWAD)]));
     Exit;
+  end;
+
+  e_LogWritefln('using downloaded client map wad [%s] for [%s]', [gWAD, NewWAD], TMsgType.Notify);
+  NewWAD := gWAD;
+
+  g_Game_LoadWAD(NewWAD);
+  result := NewWAD;
+
+  {
+  if LowerCase(NewWAD) = LowerCase(gGameSettings.WAD) then Exit;
   gWAD := g_Res_SearchSameWAD(MapsDir, ExtractFileName(NewWAD), WHash);
   if gWAD = '' then
   begin
     g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
-    gWAD := g_Res_DownloadWAD(ExtractFileName(NewWAD));
+    gWAD := g_Res_DownloadMapWAD(ExtractFileName(NewWAD), WHash);
     if gWAD = '' then
     begin
       g_Game_Free();
@@ -4439,18 +5300,12 @@ begin
   end;
   NewWAD := ExtractRelativePath(MapsDir, gWAD);
   g_Game_LoadWAD(NewWAD);
+  }
 end;
 
 procedure g_Game_RestartRound(NoMapRestart: Boolean = False);
 var
   i, n, nb, nr: Integer;
-
-  function monRespawn (mon: TMonster): Boolean;
-  begin
-    result := false; // don't stop
-    if not mon.FNoRespawn then mon.Respawn();
-  end;
-
 begin
   if not g_Game_IsServer then Exit;
   if gLMSRespawn = LMS_RESPAWN_NONE then Exit;
@@ -4476,12 +5331,14 @@ begin
         else if gPlayers[i].Team = TEAM_BLUE then Inc(nb)
       end;
 
-  if (n < 2) or ((gGameSettings.GameMode = GM_TDM) and ((nr = 0) or (nb = 0))) then
+  if (n < 1) or ((gGameSettings.GameMode = GM_TDM) and ((nr = 0) or (nb = 0))) then
   begin
     // wait a second until the fuckers finally decide to join
     gLMSRespawn := LMS_RESPAWN_WARMUP;
-    gLMSRespawnTime := gTime + 1000;
+    gLMSRespawnTime := gTime + gGameSettings.WarmupTime*1000;
     gLMSSoftSpawn := NoMapRestart;
+    if g_Game_IsNet then
+      MH_SEND_GameEvent(NET_EV_LMS_WARMUP, gLMSRespawnTime - gTime);
     Exit;
   end;
 
@@ -4511,17 +5368,14 @@ begin
       gPlayers[i].Frags := 0;
       gPlayers[i].RecallState;
     end;
-    if (gPlayer1 = nil) and (gLMSPID1 > 0) then
-      gPlayer1 := g_Player_Get(gLMSPID1);
-    if (gPlayer2 = nil) and (gLMSPID2 > 0) then
-      gPlayer2 := g_Player_Get(gLMSPID2);
+    if (gPlayer1 = nil) and (gSpectLatchPID1 > 0) then
+      gPlayer1 := g_Player_Get(gSpectLatchPID1);
+    if (gPlayer2 = nil) and (gSpectLatchPID2 > 0) then
+      gPlayer2 := g_Player_Get(gSpectLatchPID2);
   end;
 
   g_Items_RestartRound();
 
-
-  g_Mons_ForEach(monRespawn);
-
   gLMSSoftSpawn := False;
 end;
 
@@ -4549,7 +5403,7 @@ var
 begin
   Result := '';
 
-  MapList := g_Map_GetMapsList(MapsDir + gGameSettings.WAD);
+  MapList := g_Map_GetMapsList(gGameSettings.WAD);
   if MapList = nil then
     Exit;
 
@@ -4571,7 +5425,7 @@ begin
     else
       Result := MapList[MapIndex + 1];
 
-    if not g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + Result) then Result := Map;
+    if not g_Map_Exist(gGameSettings.WAD + ':\' + Result) then Result := Map;
   end;
 
   MapList := nil;
@@ -4603,7 +5457,7 @@ var
   WadName: string;
 {
   WAD: TWADFile;
-  MapList: SArray;
+  MapList: SSArray;
   time: Integer;
 }
 begin
@@ -4656,81 +5510,66 @@ begin
   if gTempDelete then DeleteFile(WadName);
 end;
 
-procedure GameCVars(P: SArray);
+procedure GameCVars(P: SSArray);
 var
   a, b: Integer;
   stat: TPlayerStatArray;
-  cmd, s: string;
-  config: TConfig;
-begin
-  stat := nil;
-  cmd := LowerCase(P[0]);
-  if cmd = 'r_showfps' then
-  begin
-    if (Length(P) > 1) and
-       ((P[1] = '1') or (P[1] = '0')) then
-      gShowFPS := (P[1][1] = '1');
+  cmd: string;
 
-    if gShowFPS then
-      g_Console_Add(_lc[I_MSG_SHOW_FPS_ON])
-    else
-      g_Console_Add(_lc[I_MSG_SHOW_FPS_OFF]);
-  end
-  else if (cmd = 'g_friendlyfire') and not g_Game_IsClient then
+  procedure ParseGameFlag(Flag: LongWord; OffMsg, OnMsg: TStrings_Locale; OnMapChange: Boolean = False);
+  var
+    x: Boolean;
   begin
-    with gGameSettings do
+    if Length(P) > 1 then
     begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-      begin
-        if (P[1][1] = '1') then
-          Options := Options or GAME_OPTION_TEAMDAMAGE
-        else
-          Options := Options and (not GAME_OPTION_TEAMDAMAGE);
-      end;
+      x := P[1] = '1';
 
-      if (LongBool(Options and GAME_OPTION_TEAMDAMAGE)) then
-        g_Console_Add(_lc[I_MSG_FRIENDLY_FIRE_ON])
+      if x then
+        gsGameFlags := gsGameFlags or Flag
       else
-        g_Console_Add(_lc[I_MSG_FRIENDLY_FIRE_OFF]);
+        gsGameFlags := gsGameFlags and (not Flag);
 
-      if g_Game_IsNet then MH_SEND_GameSettings;
-    end;
-  end
-  else if (cmd = 'g_weaponstay') and not g_Game_IsClient then
-  begin
-    with gGameSettings do
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
+      if g_Game_IsServer then
       begin
-        if (P[1][1] = '1') then
-          Options := Options or GAME_OPTION_WEAPONSTAY
+        if x then
+          gGameSettings.Options := gGameSettings.Options or Flag
         else
-          Options := Options and (not GAME_OPTION_WEAPONSTAY);
+          gGameSettings.Options := gGameSettings.Options and (not Flag);
+        if g_Game_IsNet then MH_SEND_GameSettings;
       end;
+    end;
 
-      if (LongBool(Options and GAME_OPTION_WEAPONSTAY)) then
-        g_Console_Add(_lc[I_MSG_WEAPONSTAY_ON])
-      else
-        g_Console_Add(_lc[I_MSG_WEAPONSTAY_OFF]);
+    if LongBool(gsGameFlags and Flag) then
+      g_Console_Add(_lc[OnMsg])
+    else
+      g_Console_Add(_lc[OffMsg]);
 
-      if g_Game_IsNet then MH_SEND_GameSettings;
-    end;
-  end
-  else if cmd = 'g_gamemode' then
+    if OnMapChange and g_Game_IsServer then
+      g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+  end;
+
+begin
+  stat := nil;
+  cmd := LowerCase(P[0]);
+
+  if cmd = 'g_gamemode' then
   begin
-    a := g_Game_TextToMode(P[1]);
-    if a = GM_SINGLE then a := GM_COOP;
-    if (Length(P) > 1) and (a <> GM_NONE) and (not g_Game_IsClient) then
+    if (Length(P) > 1) then
     begin
-      gSwitchGameMode := a;
-      if (gGameOn and (gGameSettings.GameMode = GM_SINGLE)) or
-         (gState = STATE_INTERSINGLE) then
-        gSwitchGameMode := GM_SINGLE;
-      if not gGameOn then
-        gGameSettings.GameMode := gSwitchGameMode;
+      a := g_Game_TextToMode(P[1]);
+      if a = GM_SINGLE then a := GM_COOP;
+      gsGameMode := g_Game_ModeToText(a);
+      if g_Game_IsServer then
+      begin
+        gSwitchGameMode := a;
+        if (gGameOn and (gGameSettings.GameMode = GM_SINGLE)) or
+           (gState = STATE_INTERSINGLE) then
+          gSwitchGameMode := GM_SINGLE;
+        if not gGameOn then
+          gGameSettings.GameMode := gSwitchGameMode;
+      end;
     end;
+
     if gSwitchGameMode = gGameSettings.GameMode then
       g_Console_Add(Format(_lc[I_MSG_GAMEMODE_CURRENT],
                           [g_Game_ModeToText(gGameSettings.GameMode)]))
@@ -4739,453 +5578,536 @@ begin
                           [g_Game_ModeToText(gGameSettings.GameMode),
                            g_Game_ModeToText(gSwitchGameMode)]));
   end
-  else if (cmd = 'g_allow_exit') and not g_Game_IsClient then
+  else if cmd = 'g_friendlyfire' then
+  begin
+    ParseGameFlag(GAME_OPTION_TEAMDAMAGE, I_MSG_FRIENDLY_FIRE_OFF, I_MSG_FRIENDLY_FIRE_ON);
+  end
+  else if cmd = 'g_friendly_absorb_damage' then
+  begin
+    ParseGameFlag(GAME_OPTION_TEAMABSORBDAMAGE, I_MSG_FRIENDLY_ABSORB_DAMAGE_OFF, I_MSG_FRIENDLY_ABSORB_DAMAGE_ON);
+  end
+  else if cmd = 'g_friendly_hit_trace' then
+  begin
+    ParseGameFlag(GAME_OPTION_TEAMHITTRACE, I_MSG_FRIENDLY_HIT_TRACE_OFF, I_MSG_FRIENDLY_HIT_TRACE_ON);
+  end
+  else if cmd = 'g_friendly_hit_projectile' then
+  begin
+    ParseGameFlag(GAME_OPTION_TEAMHITPROJECTILE, I_MSG_FRIENDLY_PROJECT_TRACE_OFF, I_MSG_FRIENDLY_PROJECT_TRACE_ON);
+  end
+  else if cmd = 'g_weaponstay' then
+  begin
+    ParseGameFlag(GAME_OPTION_WEAPONSTAY, I_MSG_WEAPONSTAY_OFF, I_MSG_WEAPONSTAY_ON);
+  end
+  else if cmd = 'g_allow_exit' then
+  begin
+    ParseGameFlag(GAME_OPTION_ALLOWEXIT, I_MSG_ALLOWEXIT_OFF, I_MSG_ALLOWEXIT_ON, True);
+  end
+  else if cmd = 'g_allow_monsters' then
+  begin
+    ParseGameFlag(GAME_OPTION_MONSTERS, I_MSG_ALLOWMON_OFF, I_MSG_ALLOWMON_ON, True);
+  end
+  else if cmd = 'g_allow_dropflag' then
+  begin
+    ParseGameFlag(GAME_OPTION_ALLOWDROPFLAG, I_MSG_ALLOWDROPFLAG_OFF, I_MSG_ALLOWDROPFLAG_ON);
+  end
+  else if cmd = 'g_throw_flag' then
+  begin
+    ParseGameFlag(GAME_OPTION_THROWFLAG, I_MSG_THROWFLAG_OFF, I_MSG_THROWFLAG_ON);
+  end
+  else if cmd = 'g_bot_vsplayers' then
+  begin
+    ParseGameFlag(GAME_OPTION_BOTVSPLAYER, I_MSG_BOTSVSPLAYERS_OFF, I_MSG_BOTSVSPLAYERS_ON);
+  end
+  else if cmd = 'g_bot_vsmonsters' then
+  begin
+    ParseGameFlag(GAME_OPTION_BOTVSMONSTER, I_MSG_BOTSVSMONSTERS_OFF, I_MSG_BOTSVSMONSTERS_ON);
+  end
+  else if cmd = 'g_dm_keys' then
+  begin
+    ParseGameFlag(GAME_OPTION_DMKEYS, I_MSG_DMKEYS_OFF, I_MSG_DMKEYS_ON, True);
+  end
+  else if cmd = 'g_gameflags' then
   begin
-    with gGameSettings do
+    if Length(P) > 1 then
     begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
+      gsGameFlags := StrToDWordDef(P[1], gsGameFlags);
+      if g_Game_IsServer then
       begin
-        if (P[1][1] = '1') then
-          Options := Options or GAME_OPTION_ALLOWEXIT
-        else
-          Options := Options and (not GAME_OPTION_ALLOWEXIT);
+        gGameSettings.Options := gsGameFlags;
+        if g_Game_IsNet then MH_SEND_GameSettings;
       end;
-
-      if (LongBool(Options and GAME_OPTION_ALLOWEXIT)) then
-        g_Console_Add(_lc[I_MSG_ALLOWEXIT_ON])
-      else
-        g_Console_Add(_lc[I_MSG_ALLOWEXIT_OFF]);
-      g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
-
-      if g_Game_IsNet then MH_SEND_GameSettings;
     end;
+
+    g_Console_Add(Format('%s %u', [cmd, gsGameFlags]));
   end
-  else if (cmd = 'g_allow_monsters') and not g_Game_IsClient then
+  else if cmd = 'g_warmup_time' then
   begin
-    with gGameSettings do
+    if Length(P) > 1 then
     begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
+      gsWarmupTime := nclamp(StrToIntDef(P[1], gsWarmupTime), 0, $FFFF);
+      if g_Game_IsServer then
       begin
-        if (P[1][1] = '1') then
-          Options := Options or GAME_OPTION_MONSTERS
-        else
-          Options := Options and (not GAME_OPTION_MONSTERS);
+        gGameSettings.WarmupTime := gsWarmupTime;
+        // extend warmup if it's already going
+        if gLMSRespawn = LMS_RESPAWN_WARMUP then
+        begin
+          gLMSRespawnTime := gTime + gsWarmupTime * 1000;
+          if g_Game_IsNet then MH_SEND_GameEvent(NET_EV_LMS_WARMUP, gLMSRespawnTime - gTime);
+        end;
+        if g_Game_IsNet then MH_SEND_GameSettings;
       end;
-
-      if (LongBool(Options and GAME_OPTION_MONSTERS)) then
-        g_Console_Add(_lc[I_MSG_ALLOWMON_ON])
-      else
-        g_Console_Add(_lc[I_MSG_ALLOWMON_OFF]);
-      g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
-
-      if g_Game_IsNet then MH_SEND_GameSettings;
     end;
-  end
-  else if (cmd = 'g_bot_vsplayers') and not g_Game_IsClient then
-  begin
-    with gGameSettings do
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-      begin
-        if (P[1][1] = '1') then
-          Options := Options or GAME_OPTION_BOTVSPLAYER
-        else
-          Options := Options and (not GAME_OPTION_BOTVSPLAYER);
-      end;
 
-      if (LongBool(Options and GAME_OPTION_BOTVSPLAYER)) then
-        g_Console_Add(_lc[I_MSG_BOTSVSPLAYERS_ON])
-      else
-        g_Console_Add(_lc[I_MSG_BOTSVSPLAYERS_OFF]);
-
-      if g_Game_IsNet then MH_SEND_GameSettings;
-    end;
+    g_Console_Add(Format(_lc[I_MSG_WARMUP], [Integer(gsWarmupTime)]));
+    if g_Game_IsServer then g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
   end
-  else if (cmd = 'g_bot_vsmonsters') and not g_Game_IsClient then
+  else if cmd = 'g_spawn_invul' then
   begin
-    with gGameSettings do
+    if Length(P) > 1 then
     begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
+      gsSpawnInvul := nclamp(StrToIntDef(P[1], gsSpawnInvul), 0, $FFFF);
+      if g_Game_IsServer then
       begin
-        if (P[1][1] = '1') then
-          Options := Options or GAME_OPTION_BOTVSMONSTER
-        else
-          Options := Options and (not GAME_OPTION_BOTVSMONSTER);
+        gGameSettings.SpawnInvul := gsSpawnInvul;
+        if g_Game_IsNet then MH_SEND_GameSettings;
       end;
-
-      if (LongBool(Options and GAME_OPTION_BOTVSMONSTER)) then
-        g_Console_Add(_lc[I_MSG_BOTSVSMONSTERS_ON])
-      else
-        g_Console_Add(_lc[I_MSG_BOTSVSMONSTERS_OFF]);
-
-      if g_Game_IsNet then MH_SEND_GameSettings;
     end;
+
+    g_Console_Add(Format('%s %d', [cmd, Integer(gsSpawnInvul)]));
   end
-  else if (cmd = 'g_warmuptime') and not g_Game_IsClient then
+  else if cmd = 'g_item_respawn_time' then
   begin
     if Length(P) > 1 then
     begin
-      if StrToIntDef(P[1], gGameSettings.WarmupTime) = 0 then
-        gGameSettings.WarmupTime := 30
-      else
-        gGameSettings.WarmupTime := StrToIntDef(P[1], gGameSettings.WarmupTime);
+      gsItemRespawnTime := nclamp(StrToIntDef(P[1], gsItemRespawnTime), 0, $FFFF);
+      if g_Game_IsServer then
+      begin
+        gGameSettings.ItemRespawnTime := gsItemRespawnTime;
+        if g_Game_IsNet then MH_SEND_GameSettings;
+      end;
     end;
 
-    g_Console_Add(Format(_lc[I_MSG_WARMUP],
-                 [gGameSettings.WarmupTime]));
-    g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+    g_Console_Add(Format('%s %d', [cmd, Integer(gsItemRespawnTime)]));
+    if g_Game_IsServer then g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
   end
-  else if cmd = 'net_interp' then
+  else if cmd = 'sv_intertime' then
   begin
     if (Length(P) > 1) then
-      NetInterpLevel := StrToIntDef(P[1], NetInterpLevel);
+      gDefInterTime := Min(Max(StrToIntDef(P[1], gDefInterTime), -1), 120);
 
-    g_Console_Add('net_interp = ' + IntToStr(NetInterpLevel));
-    config := TConfig.CreateFile(GameDir+'/'+CONFIG_FILENAME);
-    config.WriteInt('Client', 'InterpolationSteps', NetInterpLevel);
-    config.SaveFile(GameDir+'/'+CONFIG_FILENAME);
-    config.Free();
+    g_Console_Add(cmd + ' = ' + IntToStr(gDefInterTime));
   end
-  else if cmd = 'net_forceplayerupdate' then
+  else if cmd = 'g_max_particles' then
   begin
-    if (Length(P) > 1) and
-       ((P[1] = '1') or (P[1] = '0')) then
-      NetForcePlayerUpdate := (P[1][1] = '1');
-
-    if NetForcePlayerUpdate then
-      g_Console_Add('net_forceplayerupdate = 1')
+    if Length(p) = 2 then
+    begin
+      a := Max(0, StrToIntDef(p[1], 0));
+      g_GFX_SetMax(a)
+    end
+    else if Length(p) = 1 then
+    begin
+      e_LogWritefln('%s', [g_GFX_GetMax()])
+    end
     else
-      g_Console_Add('net_forceplayerupdate = 0');
-    config := TConfig.CreateFile(GameDir+'/'+CONFIG_FILENAME);
-    config.WriteBool('Client', 'ForcePlayerUpdate', NetForcePlayerUpdate);
-    config.SaveFile(GameDir+'/'+CONFIG_FILENAME);
-    config.Free();
+    begin
+      e_LogWritefln('usage: %s <n>', [cmd])
+    end
   end
-  else if cmd = 'net_predictself' then
+  else if cmd = 'g_max_shells' then
   begin
-    if (Length(P) > 1) and
-       ((P[1] = '1') or (P[1] = '0')) then
-      NetPredictSelf := (P[1][1] = '1');
-
-    if NetPredictSelf then
-      g_Console_Add('net_predictself = 1')
+    if Length(p) = 2 then
+    begin
+      a := Max(0, StrToIntDef(p[1], 0));
+      g_Shells_SetMax(a)
+    end
+    else if Length(p) = 1 then
+    begin
+      e_LogWritefln('%s', [g_Shells_GetMax()])
+    end
     else
-      g_Console_Add('net_predictself = 0');
-    config := TConfig.CreateFile(GameDir+'/'+CONFIG_FILENAME);
-    config.WriteBool('Client', 'PredictSelf', NetPredictSelf);
-    config.SaveFile(GameDir+'/'+CONFIG_FILENAME);
-    config.Free();
+    begin
+      e_LogWritefln('usage: %s <n>', [cmd])
+    end
   end
-  else if cmd = 'sv_name' then
+  else if cmd = 'g_max_gibs' then
   begin
-    if (Length(P) > 1) and (Length(P[1]) > 0) then
+    if Length(p) = 2 then
     begin
-      NetServerName := P[1];
-      if Length(NetServerName) > 64 then
-        SetLength(NetServerName, 64);
-      if g_Game_IsServer and g_Game_IsNet and NetUseMaster then
-        g_Net_Slist_Update;
-    end;
-
-    g_Console_Add(cmd + ' = "' + NetServerName + '"');
+      a := Max(0, StrToIntDef(p[1], 0));
+      g_Gibs_SetMax(a)
+    end
+    else if Length(p) = 1 then
+    begin
+      e_LogWritefln('%s', [g_Gibs_GetMax()])
+    end
+    else
+    begin
+      e_LogWritefln('usage: %s <n>', [cmd])
+    end
   end
-  else if cmd = 'sv_passwd' then
+  else if cmd = 'g_max_corpses' then
   begin
-    if (Length(P) > 1) and (Length(P[1]) > 0) then
+    if Length(p) = 2 then
     begin
-      NetPassword := P[1];
-      if Length(NetPassword) > 24 then
-        SetLength(NetPassword, 24);
-      if g_Game_IsServer and g_Game_IsNet and NetUseMaster then
-        g_Net_Slist_Update;
-    end;
-
-    g_Console_Add(cmd + ' = "' + AnsiLowerCase(NetPassword) + '"');
+      a := Max(0, StrToIntDef(p[1], 0));
+      g_Corpses_SetMax(a)
+    end
+    else if Length(p) = 1 then
+    begin
+      e_LogWritefln('%s', [g_Corpses_GetMax()])
+    end
+    else
+    begin
+      e_LogWritefln('usage: %s <n>', [cmd])
+    end
   end
-  else if cmd = 'sv_maxplrs' then
+  else if cmd = 'g_scorelimit' then
   begin
-    if (Length(P) > 1) then
+    if Length(P) > 1 then
     begin
-      NetMaxClients := Min(Max(StrToIntDef(P[1], NetMaxClients), 1), NET_MAXCLIENTS);
-      if g_Game_IsServer and g_Game_IsNet then
+      gsGoalLimit := nclamp(StrToIntDef(P[1], gsGoalLimit), 0, $FFFF);
+
+      if g_Game_IsServer then
       begin
         b := 0;
-        for a := 0 to High(NetClients) do
-          if NetClients[a].Used then
-          begin
-            Inc(b);
-            if b > NetMaxClients then
-            begin
-              s := g_Player_Get(NetClients[a].Player).Name;
-              enet_peer_disconnect(NetClients[a].Peer, NET_DISC_FULL);
-              g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
-              MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
-            end;
-          end;
-        if NetUseMaster then
-          g_Net_Slist_Update;
+        if gGameSettings.GameMode = GM_DM then
+        begin // DM
+          stat := g_Player_GetStats();
+          if stat <> nil then
+            for a := 0 to High(stat) do
+              if stat[a].Frags > b then
+                b := stat[a].Frags;
+        end
+        else // TDM/CTF
+          b := Max(gTeamStat[TEAM_RED].Goals, gTeamStat[TEAM_BLUE].Goals);
+
+        // if someone has a higher score, set it to that instead
+        gsGoalLimit := max(gsGoalLimit, b);
+        gGameSettings.GoalLimit := gsGoalLimit;
+        if g_Game_IsNet then MH_SEND_GameSettings;
       end;
     end;
 
-    g_Console_Add(cmd + ' = ' + IntToStr(NetMaxClients));
+    g_Console_Add(Format(_lc[I_MSG_SCORE_LIMIT], [Integer(gsGoalLimit)]));
   end
-  else if cmd = 'sv_public' then
+  else if cmd = 'g_timelimit' then
   begin
-    if (Length(P) > 1) then
+    if Length(P) > 1 then
     begin
-      NetUseMaster := StrToIntDef(P[1], Byte(NetUseMaster)) > 0;
-      if g_Game_IsServer and g_Game_IsNet then
-        if NetUseMaster then
-        begin
-          if NetMPeer = nil then
-            if not g_Net_Slist_Connect() then
-              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR]);
-          g_Net_Slist_Update();
-        end
-        else
-          if NetMPeer <> nil then
-            g_Net_Slist_Disconnect();
+      gsTimeLimit := nclamp(StrToIntDef(P[1], gsTimeLimit), 0, $FFFF);
+      if g_Game_IsServer then
+      begin
+        gGameSettings.TimeLimit := gsTimeLimit;
+        if g_Game_IsNet then MH_SEND_GameSettings;
+      end;
     end;
-
-    g_Console_Add(cmd + ' = ' + IntToStr(Byte(NetUseMaster)));
+    g_Console_Add(Format(_lc[I_MSG_TIME_LIMIT],
+                         [gsTimeLimit div 3600,
+                         (gsTimeLimit div 60) mod 60,
+                          gsTimeLimit mod 60]));
   end
-  else if cmd = 'sv_intertime' then
+  else if cmd = 'g_maxlives' then
   begin
-    if (Length(P) > 1) then
-      gDefInterTime := Min(Max(StrToIntDef(P[1], gDefInterTime), -1), 120);
+    if Length(P) > 1 then
+    begin
+      gsMaxLives := nclamp(StrToIntDef(P[1], gsMaxLives), 0, $FFFF);
+      if g_Game_IsServer then
+      begin
+        gGameSettings.MaxLives := gsMaxLives;
+        if g_Game_IsNet then MH_SEND_GameSettings;
+      end;
+    end;
 
-    g_Console_Add(cmd + ' = ' + IntToStr(gDefInterTime));
-  end
-  else if cmd = 'p1_name' then
+    g_Console_Add(Format(_lc[I_MSG_LIVES], [Integer(gsMaxLives)]));
+  end;
+end;
+
+procedure PlayerSettingsCVars(P: SSArray);
+var
+  cmd: string;
+  team: Byte;
+
+  function ParseTeam(s: string): Byte;
   begin
-    if (Length(P) > 1) and gGameOn then
-    begin
-      if g_Game_IsClient then
+    result := 0;
+    case s of
+      'red', '1':  result := TEAM_RED;
+      'blue', '2': result := TEAM_BLUE;
+      else         result := TEAM_NONE;
+    end;
+  end;
+begin
+  cmd := LowerCase(P[0]);
+  case cmd of
+    'p1_name':
       begin
-        gPlayer1Settings.Name := b_Text_Unformat(P[1]);
-        MC_SEND_PlayerSettings;
-      end
-      else
-        if gPlayer1 <> nil then
+        if (Length(P) > 1) then
         begin
-          gPlayer1.Name := b_Text_Unformat(P[1]);
-          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
-        end
-        else
           gPlayer1Settings.Name := b_Text_Unformat(P[1]);
-    end;
-  end
-  else if cmd = 'p2_name' then
-  begin
-    if (Length(P) > 1) and gGameOn then
-    begin
-      if g_Game_IsClient then
+          if g_Game_IsClient then
+            MC_SEND_PlayerSettings
+          else if gGameOn and (gPlayer1 <> nil) then
+          begin
+            gPlayer1.Name := b_Text_Unformat(P[1]);
+            if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+          end;
+        end;
+      end;
+    'p2_name':
       begin
-        gPlayer2Settings.Name := b_Text_Unformat(P[1]);
-        MC_SEND_PlayerSettings;
-      end
-      else
-        if gPlayer2 <> nil then
+        if (Length(P) > 1) then
         begin
-          gPlayer2.Name := b_Text_Unformat(P[1]);
-          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
-        end
-        else
           gPlayer2Settings.Name := b_Text_Unformat(P[1]);
-    end;
-  end
-  else if cmd = 'p1_color' then
-  begin
-    if Length(P) > 3 then
-      if g_Game_IsClient then
+          if g_Game_IsClient then
+            MC_SEND_PlayerSettings
+          else if gGameOn and (gPlayer2 <> nil) then
+          begin
+            gPlayer2.Name := b_Text_Unformat(P[1]);
+            if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
+          end;
+        end;
+      end;
+    'p1_color':
       begin
-        gPlayer1Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
-                                       EnsureRange(StrToIntDef(P[2], 0), 0, 255),
-                                       EnsureRange(StrToIntDef(P[3], 0), 0, 255));
-        MC_SEND_PlayerSettings;
-      end
-      else
-        if gPlayer1 <> nil then
+        if Length(P) > 3 then
         begin
-          gPlayer1.Model.SetColor(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
-                                  EnsureRange(StrToIntDef(P[2], 0), 0, 255),
-                                  EnsureRange(StrToIntDef(P[3], 0), 0, 255));
-          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
-        end
-        else
           gPlayer1Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
                                          EnsureRange(StrToIntDef(P[2], 0), 0, 255),
                                          EnsureRange(StrToIntDef(P[3], 0), 0, 255));
-  end
-  else if (cmd = 'p2_color') and not g_Game_IsNet then
-  begin
-    if Length(P) > 3 then
-      if g_Game_IsClient then
+          if g_Game_IsClient then
+            MC_SEND_PlayerSettings
+          else if gGameOn and (gPlayer1 <> nil) then
+          begin
+            gPlayer1.SetColor(gPlayer1Settings.Color);
+            if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+          end;
+        end;
+      end;
+    'p2_color':
       begin
-        gPlayer2Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
-                                       EnsureRange(StrToIntDef(P[2], 0), 0, 255),
-                                       EnsureRange(StrToIntDef(P[3], 0), 0, 255));
-        MC_SEND_PlayerSettings;
-      end
-      else
-        if gPlayer2 <> nil then
+        if Length(P) > 3 then
         begin
-          gPlayer2.Model.SetColor(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
-                                  EnsureRange(StrToIntDef(P[2], 0), 0, 255),
-                                  EnsureRange(StrToIntDef(P[3], 0), 0, 255));
-          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
-        end
-        else
           gPlayer2Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
                                          EnsureRange(StrToIntDef(P[2], 0), 0, 255),
                                          EnsureRange(StrToIntDef(P[3], 0), 0, 255));
-  end
-  else if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
-  begin
-    if cmd = 'r_showtime' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gShowTime := (P[1][1] = '1');
-
-      if gShowTime then
-        g_Console_Add(_lc[I_MSG_TIME_ON])
-      else
-        g_Console_Add(_lc[I_MSG_TIME_OFF]);
-    end
-    else if cmd = 'r_showscore' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gShowGoals := (P[1][1] = '1');
-
-      if gShowGoals then
-        g_Console_Add(_lc[I_MSG_SCORE_ON])
-      else
-        g_Console_Add(_lc[I_MSG_SCORE_OFF]);
-    end
-    else if cmd = 'r_showstat' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gShowStat := (P[1][1] = '1');
-
-      if gShowStat then
-        g_Console_Add(_lc[I_MSG_STATS_ON])
-      else
-        g_Console_Add(_lc[I_MSG_STATS_OFF]);
-    end
-    else if cmd = 'r_showkillmsg' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gShowKillMsg := (P[1][1] = '1');
-
-      if gShowKillMsg then
-        g_Console_Add(_lc[I_MSG_KILL_MSGS_ON])
-      else
-        g_Console_Add(_lc[I_MSG_KILL_MSGS_OFF]);
-    end
-    else if cmd = 'r_showlives' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gShowLives := (P[1][1] = '1');
-
-      if gShowLives then
-        g_Console_Add(_lc[I_MSG_LIVES_ON])
-      else
-        g_Console_Add(_lc[I_MSG_LIVES_OFF]);
-    end
-    else if cmd = 'r_showspect' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gSpectHUD := (P[1][1] = '1');
-
-      if gSpectHUD then
-        g_Console_Add(_lc[I_MSG_SPECT_HUD_ON])
-      else
-        g_Console_Add(_lc[I_MSG_SPECT_HUD_OFF]);
-    end
-    else if cmd = 'r_showping' then
-    begin
-      if (Length(P) > 1) and
-         ((P[1] = '1') or (P[1] = '0')) then
-        gShowPing := (P[1][1] = '1');
-
-      if gShowPing then
-        g_Console_Add(_lc[I_MSG_PING_ON])
-      else
-        g_Console_Add(_lc[I_MSG_PING_OFF]);
-    end
-    else if (cmd = 'g_scorelimit') and not g_Game_IsClient then
-    begin
-      if Length(P) > 1 then
+          if g_Game_IsClient then
+            MC_SEND_PlayerSettings
+          else if gGameOn and (gPlayer2 <> nil) then
+          begin
+            gPlayer2.SetColor(gPlayer2Settings.Color);
+            if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
+          end;
+        end;
+      end;
+    'p1_model':
       begin
-        if StrToIntDef(P[1], gGameSettings.GoalLimit) = 0 then
-          gGameSettings.GoalLimit := 0
-        else
+        if (Length(P) > 1) then
+        begin
+          gPlayer1Settings.Model := P[1];
+          if g_Game_IsClient then
+            MC_SEND_PlayerSettings
+          else if gGameOn and (gPlayer1 <> nil) then
           begin
-            b := 0;
-
-            if gGameSettings.GameMode = GM_DM then
-              begin // DM
-                stat := g_Player_GetStats();
-                if stat <> nil then
-                  for a := 0 to High(stat) do
-                    if stat[a].Frags > b then
-                      b := stat[a].Frags;
-              end
-            else // TDM/CTF
-              b := Max(gTeamStat[TEAM_RED].Goals, gTeamStat[TEAM_BLUE].Goals);
-
-            gGameSettings.GoalLimit := Max(StrToIntDef(P[1], gGameSettings.GoalLimit), b);
+            gPlayer1.FActualModelName := gPlayer1Settings.Model;
+            gPlayer1.SetModel(gPlayer1Settings.Model);
+            if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
           end;
-
-        if g_Game_IsNet then MH_SEND_GameSettings;
+        end;
       end;
-
-      g_Console_Add(Format(_lc[I_MSG_SCORE_LIMIT], [gGameSettings.GoalLimit]));
-    end
-    else if (cmd = 'g_timelimit') and not g_Game_IsClient then
-    begin
-      if (Length(P) > 1) and (StrToIntDef(P[1], -1) >= 0) then
-        gGameSettings.TimeLimit := StrToIntDef(P[1], -1);
-
-      g_Console_Add(Format(_lc[I_MSG_TIME_LIMIT],
-                           [gGameSettings.TimeLimit div 3600,
-                           (gGameSettings.TimeLimit div 60) mod 60,
-                            gGameSettings.TimeLimit mod 60]));
-      if g_Game_IsNet then MH_SEND_GameSettings;
-    end
-    else if (cmd = 'g_maxlives') and not g_Game_IsClient then
-    begin
-      if Length(P) > 1 then
+    'p2_model':
       begin
-        if StrToIntDef(P[1], gGameSettings.MaxLives) = 0 then
-          gGameSettings.MaxLives := 0
-        else
+        if (Length(P) > 1) then
         begin
-          b := 0;
-          stat := g_Player_GetStats();
-          if stat <> nil then
-            for a := 0 to High(stat) do
-              if stat[a].Lives > b then
-                b := stat[a].Lives;
-          gGameSettings.MaxLives :=
-            Max(StrToIntDef(P[1], gGameSettings.MaxLives), b);
+          gPlayer2Settings.Model := P[1];
+          if g_Game_IsClient then
+            MC_SEND_PlayerSettings
+          else if gGameOn and (gPlayer2 <> nil) then
+          begin
+            gPlayer2.FActualModelName := gPlayer2Settings.Model;
+            gPlayer2.SetModel(gPlayer2Settings.Model);
+            if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
+          end;
         end;
       end;
-
-      g_Console_Add(Format(_lc[I_MSG_LIVES],
-                           [gGameSettings.MaxLives]));
-      if g_Game_IsNet then MH_SEND_GameSettings;
-    end;
+    'p1_team':
+      begin
+        // TODO: switch teams if in game or store this separately
+        if (Length(P) > 1) then
+        begin
+          team := ParseTeam(P[1]);
+          if team = TEAM_NONE then
+            g_Console_Add('expected ''red'', ''blue'', 1 or 2')
+          else if not gGameOn and not g_Game_IsNet then
+            gPlayer1Settings.Team := team
+          else
+            g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+        end;
+      end;
+    'p2_team':
+      begin
+        // TODO: switch teams if in game or store this separately
+        if (Length(P) > 1) then
+        begin
+          team := ParseTeam(P[1]);
+          if team = TEAM_NONE then
+            g_Console_Add('expected ''red'', ''blue'', 1 or 2')
+          else if not gGameOn and not g_Game_IsNet then
+            gPlayer2Settings.Team := team
+          else
+            g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+        end;
+      end;
+    'p1_autoswitch':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponSwitch := EnsureRange(StrTointDef(P[1], 0), 0, 2);
+        end;
+    'p2_autoswitch':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponSwitch := EnsureRange(StrTointDef(P[1], 0), 0, 2);
+        end;
+    'p1_switch_empty':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.SwitchToEmpty := EnsureRange(StrTointDef(P[1], 0), 0, 1);
+        end;
+    'p2_switch_empty':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.SwitchToEmpty := EnsureRange(StrTointDef(P[1], 0), 0, 1);
+        end;
+    'p1_skip_fist':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.SkipFist := EnsureRange(StrTointDef(P[1], 0), 0, 1);
+        end;
+    'p2_skip_fist':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.SkipFist := EnsureRange(StrTointDef(P[1], 0), 0, 1);
+        end;
+    'p1_priority_kastet':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_KASTET] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1); 
+        end;
+    'p2_priority_kastet':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_KASTET] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1); 
+      end;        
+    'p1_priority_saw':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_SAW] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+        end;
+    'p2_priority_saw':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_SAW] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_pistol':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_KASTET] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1); 
+        end;
+    'p2_priority_pistol':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_KASTET] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1); 
+      end;         
+    'p1_priority_shotgun1':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_SHOTGUN1] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_shotgun1':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_SHOTGUN1] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_shotgun2':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_SHOTGUN2] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_shotgun2':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_SHOTGUN2] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_chaingun':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_CHAINGUN] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_chaingun':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_CHAINGUN] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_rocketlauncher':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_ROCKETLAUNCHER] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_rocketlauncher':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_ROCKETLAUNCHER] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_plasma':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_PLASMA] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_plasma':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_PLASMA] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_bfg':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_BFG] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_bfg':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_BFG] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_super':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_SUPERPULEMET] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_super':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_SUPERPULEMET] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p1_priority_flamethrower':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WEAPON_FLAMETHROWER] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;
+    'p2_priority_flamethrower':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WEAPON_FLAMETHROWER] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1);
+      end;      
+    'p1_priority_berserk':
+      begin
+        if (Length(P) = 2) then
+          gPlayer1Settings.WeaponPreferences[WP_LAST+1] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1); 
+        end;
+    'p2_priority_berserk':
+      begin
+        if (Length(P) = 2) then
+          gPlayer2Settings.WeaponPreferences[WP_LAST+1] := EnsureRange(StrToIntDef(P[1], WP_FIRST), WP_FIRST, WP_LAST+1); 
+      end;                                                                                  
   end;
 end;
 
@@ -5203,7 +6125,7 @@ begin
   e_LogWriteLn ('^=======================^');
 end;
 
-procedure DebugCommands(P: SArray);
+procedure DebugCommands(P: SSArray);
 var
   a, b: Integer;
   cmd: string;
@@ -5216,11 +6138,8 @@ begin
     cmd := LowerCase(P[0]);
     if cmd = 'd_window' then
     begin
-      g_Console_Add(Format('gWinPosX = %d, gWinPosY %d', [gWinPosX, gWinPosY]));
-      g_Console_Add(Format('gWinRealPosX = %d, gWinRealPosY %d', [gWinRealPosX, gWinRealPosY]));
       g_Console_Add(Format('gScreenWidth = %d, gScreenHeight = %d', [gScreenWidth, gScreenHeight]));
-      g_Console_Add(Format('gWinSizeX = %d, gWinSizeY = %d', [gWinSizeX, gWinSizeY]));
-      g_Console_Add(Format('Frame X = %d, Y = %d, Caption Y = %d', [gWinFrameX, gWinFrameY, gWinCaption]));
+      g_Console_Add(Format('gScreenWidth = %d, gScreenHeight = %d', [gScreenWidth, gScreenHeight]));
     end
     else if cmd = 'd_sounds' then
     begin
@@ -5324,11 +6243,6 @@ begin
 
       g_Console_Add(Format(cmd + ' is %d', [Byte(g_Debug_Player)]));
     end
-    else if (cmd = 'd_joy') then
-    begin
-      for a := 1 to 8 do
-        g_Console_Add(e_JoystickStateToString(a));
-    end
     else if (cmd = 'd_mem') then
     begin
       PrintHeapStats();
@@ -5339,7 +6253,7 @@ begin
 end;
 
 
-procedure GameCheats(P: SArray);
+procedure GameCheats(P: SSArray);
 var
   cmd: string;
   f, a: Integer;
@@ -5554,7 +6468,7 @@ begin
   end;
 end;
 
-procedure GameCommands(P: SArray);
+procedure GameCommands(P: SSArray);
 var
   a, b: Integer;
   s, pw: String;
@@ -5565,18 +6479,12 @@ var
   prt: Word;
   nm: Boolean;
   listen: LongWord;
+  found: Boolean;
 begin
 // Îáùèå êîìàíäû:
   cmd := LowerCase(P[0]);
   chstr := '';
-  if (cmd = 'quit') or
-     (cmd = 'exit') then
-  begin
-    g_Game_Free();
-    g_Game_Quit();
-    Exit;
-  end
-  else if cmd = 'pause' then
+  if cmd = 'pause' then
   begin
     if (g_ActiveWindow = nil) then
       g_Game_Pause(not gPauseMain);
@@ -5619,8 +6527,7 @@ begin
         enet_peer_disconnect(pl^.Peer, NET_DISC_KICK);
         g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
         MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
-        if NetUseMaster then
-          g_Net_Slist_Update;
+        g_Net_Slist_ServerPlayerLeaves();
       end else if gPlayers <> nil then
         for a := Low(gPlayers) to High(gPlayers) do
           if gPlayers[a] <> nil then
@@ -5633,8 +6540,7 @@ begin
               gPlayers[a].Kill(K_SIMPLEKILL, 0, HIT_DISCON);
               g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [gPlayers[a].Name]), True);
               g_Player_Remove(gPlayers[a].UID);
-              if NetUseMaster then
-                g_Net_Slist_Update;
+              g_Net_Slist_ServerPlayerLeaves();
               // Åñëè íå ïåðåìåøàòü, ïðè äîáàâëåíèè íîâûõ áîòîâ ïîÿâÿòñÿ ñòàðûå
               g_Bot_MixNames();
             end;
@@ -5665,13 +6571,40 @@ begin
           enet_peer_disconnect(NetClients[a].Peer, NET_DISC_KICK);
           g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
           MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
-          if NetUseMaster then
-            g_Net_Slist_Update;
+          g_Net_Slist_ServerPlayerLeaves();
         end;
       end;
     end else
       g_Console_Add(_lc[I_MSG_SERVERONLY]);
   end
+  else if cmd = 'kick_pid' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('kick_pid <player ID>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('kick_pid <player ID>');
+        Exit;
+      end;
+
+      a := StrToIntDef(P[1], 0);
+      pl := g_Net_Client_ByPlayer(a);
+      if (pl <> nil) and pl^.Used and (pl^.Peer <> nil) then
+      begin
+        s := g_Net_ClientName_ByID(pl^.ID);
+        enet_peer_disconnect(pl^.Peer, NET_DISC_KICK);
+        g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
+        MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
+        g_Net_Slist_ServerPlayerLeaves();
+      end;
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
   else if cmd = 'ban' then
   begin
     if g_Game_IsServer and g_Game_IsNet then
@@ -5695,8 +6628,7 @@ begin
         enet_peer_disconnect(pl^.Peer, NET_DISC_TEMPBAN);
         g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
         MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
-        if NetUseMaster then
-          g_Net_Slist_Update;
+        g_Net_Slist_ServerPlayerLeaves();
       end else
         g_Console_Add(Format(_lc[I_NET_ERR_NAME404], [P[1]]));
     end else
@@ -5726,12 +6658,40 @@ begin
           enet_peer_disconnect(NetClients[a].Peer, NET_DISC_TEMPBAN);
           g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
           MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
-          if NetUseMaster then
-            g_Net_Slist_Update;
+          g_Net_Slist_ServerPlayerLeaves();
         end;
     end else
       g_Console_Add(_lc[I_MSG_SERVERONLY]);
   end
+  else if cmd = 'ban_pid' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('ban_pid <player ID>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('ban_pid <player ID>');
+        Exit;
+      end;
+
+      a := StrToIntDef(P[1], 0);
+      pl := g_Net_Client_ByPlayer(a);
+      if (pl <> nil) and pl^.Used and (pl^.Peer <> nil) then
+      begin
+        s := g_Net_ClientName_ByID(pl^.ID);
+        g_Net_BanHost(pl^.Peer^.address.host, False);
+        enet_peer_disconnect(pl^.Peer, NET_DISC_TEMPBAN);
+        g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
+        MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
+        g_Net_Slist_ServerPlayerLeaves();
+      end;
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
   else if cmd = 'permban' then
   begin
     if g_Game_IsServer and g_Game_IsNet then
@@ -5756,8 +6716,7 @@ begin
         g_Net_SaveBanList();
         g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
         MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
-        if NetUseMaster then
-          g_Net_Slist_Update;
+        g_Net_Slist_ServerPlayerLeaves();
       end else
         g_Console_Add(Format(_lc[I_NET_ERR_NAME404], [P[1]]));
     end else
@@ -5788,12 +6747,62 @@ begin
           g_Net_SaveBanList();
           g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
           MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
-          if NetUseMaster then
-            g_Net_Slist_Update;
+          g_Net_Slist_ServerPlayerLeaves();
         end;
     end else
       g_Console_Add(_lc[I_MSG_SERVERONLY]);
   end
+  else if cmd = 'permban_pid' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('permban_pid <player ID>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('permban_pid <player ID>');
+        Exit;
+      end;
+
+      a := StrToIntDef(P[1], 0);
+      pl := g_Net_Client_ByPlayer(a);
+      if (pl <> nil) and pl^.Used and (pl^.Peer <> nil) then
+      begin
+        s := g_Net_ClientName_ByID(pl^.ID);
+        g_Net_BanHost(pl^.Peer^.address.host);
+        enet_peer_disconnect(pl^.Peer, NET_DISC_BAN);
+        g_Net_SaveBanList();
+        g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
+        MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
+        g_Net_Slist_ServerPlayerLeaves();
+      end;
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'permban_ip' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('permban_ip <IP address>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('permban_ip <IP address>');
+        Exit;
+      end;
+
+      g_Net_BanHost(P[1]);
+      g_Net_SaveBanList();
+      g_Console_Add(Format(_lc[I_PLAYER_BAN], [P[1]]));
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
   else if cmd = 'unban' then
   begin
     if g_Game_IsServer and g_Game_IsNet then
@@ -5889,7 +6898,9 @@ begin
   else if (cmd = 'addbot') or
      (cmd = 'bot_add') then
   begin
-    if Length(P) > 1 then
+    if Length(P) > 2 then
+      g_Bot_Add(TEAM_NONE, StrToIntDef(P[1], 2), StrToIntDef(P[2], 100))
+    else if Length(P) > 1 then
       g_Bot_Add(TEAM_NONE, StrToIntDef(P[1], 2))
     else
       g_Bot_Add(TEAM_NONE, 2);
@@ -5897,10 +6908,14 @@ begin
   else if cmd = 'bot_addlist' then
   begin
     if Length(P) > 1 then
+    begin
       if Length(P) = 2 then
         g_Bot_AddList(TEAM_NONE, P[1], StrToIntDef(P[1], -1))
+      else if Length(P) = 3 then
+        g_Bot_AddList(TEAM_NONE, P[1], StrToIntDef(P[1], -1), StrToIntDef(P[2], 100))
       else
         g_Bot_AddList(IfThen(P[2] = 'red', TEAM_RED, TEAM_BLUE), P[1], StrToIntDef(P[1], -1));
+    end;
   end
   else if cmd = 'bot_removeall' then
     g_Bot_RemoveAll()
@@ -5961,6 +6976,34 @@ begin
     end else
       g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
   end
+  else if (cmd = 'an') or (cmd = 'announce') then
+  begin
+    if g_Game_IsNet then
+    begin
+      if Length(P) > 1 then
+      begin
+        for a := 1 to High(P) do
+          chstr := chstr + P[a] + ' ';
+
+        if Length(chstr) > 200 then SetLength(chstr, 200);
+
+        if Length(chstr) < 1 then
+        begin
+          g_Console_Add('announce <text>');
+          Exit;
+        end;
+
+        chstr := 'centerprint 100 ' + b_Text_Format(chstr);
+        if g_Game_IsClient then
+          MC_SEND_RCONCommand(chstr)
+        else
+          g_Console_Process(chstr, True);
+      end
+      else
+        g_Console_Add('announce <text>');
+    end else
+      g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+  end
   else if cmd = 'game' then
   begin
     if gGameSettings.GameType <> GT_NONE then
@@ -5973,26 +7016,31 @@ begin
       g_Console_Add(cmd + ' <WAD> [MAP] [# players]');
       Exit;
     end;
-    // Èãðà åù¸ íå çàïóùåíà, ñíà÷àëà íàì íàäî çàãðóçèòü êàêîé-òî WAD
-    P[1] := addWadExtension(P[1]);
-    if FileExists(MapsDir + P[1]) then
-    begin
-      // Åñëè êàðòà íå óêàçàíà, áåð¸ì ïåðâóþ êàðòó â ôàéëå
+    // game not started yet, load fist map from some wad
+    found := false;
+    s := addWadExtension(P[1]);
+    found := e_FindResource(AllMapDirs, s);
+    P[1] := s;
+    if found then
+    begin
+      P[1] := ExpandFileName(P[1]);
+      // if map not choosed then set first map
       if Length(P) < 3 then
       begin
         SetLength(P, 3);
-        P[2] := g_Game_GetFirstMap(MapsDir + P[1]);
+        P[2] := g_Game_GetFirstMap(P[1]);
       end;
 
       s := P[1] + ':\' + UpperCase(P[2]);
 
-      if g_Map_Exist(MapsDir + s) then
+      if g_Map_Exist(s) then
       begin
-        // Çàïóñêàåì ñâîþ èãðó
+        // start game
         g_Game_Free();
         with gGameSettings do
         begin
-          GameMode := g_Game_TextToMode(gcGameMode);
+          Options := gsGameFlags;
+          GameMode := g_Game_TextToMode(gsGameMode);
           if gSwitchGameMode <> GM_NONE then
             GameMode := gSwitchGameMode;
           if GameMode = GM_NONE then GameMode := GM_DM;
@@ -6008,7 +7056,7 @@ begin
         if P[2] = '' then
           g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[1]]))
         else
-          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [UpperCase(P[2])]));
+          g_Console_Add(Format(_lc[I_MSG_NO_MAP_FALLBACK], [UpperCase(P[2]), P[1]]));
     end else
       g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
   end
@@ -6028,43 +7076,47 @@ begin
       Exit;
     prt := StrToIntDef(P[2], 25666);
 
-    P[3] := addWadExtension(P[3]);
-    if FileExists(MapsDir + P[3]) then
+    s := addWadExtension(P[3]);
+    found := e_FindResource(AllMapDirs, s);
+    P[3] := s;
+    if found then
     begin
-      // Åñëè êàðòà íå óêàçàíà, áåð¸ì ïåðâóþ êàðòó â ôàéëå
+      // get first map in wad, if not specified
       if Length(P) < 5 then
       begin
         SetLength(P, 5);
-        P[4] := g_Game_GetFirstMap(MapsDir + P[1]);
+        P[4] := g_Game_GetFirstMap(P[1]);
       end;
-
       s := P[3] + ':\' + UpperCase(P[4]);
-
-      if g_Map_Exist(MapsDir + s) then
+      if g_Map_Exist(s) then
       begin
-        // Çàïóñêàåì ñâîþ èãðó
+        // start game
         g_Game_Free();
         with gGameSettings do
         begin
-          GameMode := g_Game_TextToMode(gcGameMode);
-          if gSwitchGameMode <> GM_NONE then
-            GameMode := gSwitchGameMode;
+          Options := gsGameFlags;
+          GameMode := g_Game_TextToMode(gsGameMode);
+          if gSwitchGameMode <> GM_NONE then GameMode := gSwitchGameMode;
           if GameMode = GM_NONE then GameMode := GM_DM;
           if GameMode = GM_SINGLE then GameMode := GM_COOP;
           b := 0;
           if Length(P) >= 6 then
             b := StrToIntDef(P[5], 0);
-          g_Game_StartServer(s, GameMode, TimeLimit,
-                             GoalLimit, MaxLives, Options, b, listen, prt);
-        end;
+          g_Game_StartServer(s, GameMode, TimeLimit, GoalLimit, MaxLives, Options, b, listen, prt)
+        end
       end
       else
+      begin
         if P[4] = '' then
           g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[3]]))
         else
-          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [UpperCase(P[4])]));
-    end else
-      g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[3]]));
+          g_Console_Add(Format(_lc[I_MSG_NO_MAP_FALLBACK], [UpperCase(P[4]), P[3]]))
+      end
+    end
+    else
+    begin
+      g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[3]]))
+    end
   end
   else if cmd = 'map' then
   begin
@@ -6073,91 +7125,126 @@ begin
       if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
       begin
         g_Console_Add(cmd + ' <MAP>');
-        g_Console_Add(cmd + ' <WAD> [MAP]');
-      end else
-        g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
-    end else
+        g_Console_Add(cmd + ' <WAD> [MAP]')
+      end
+      else
+      begin
+        g_Console_Add(_lc[I_MSG_GM_UNAVAIL])
+      end
+    end
+    else
+    begin
       if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
       begin
-        // Èä¸ò ñâîÿ èãðà èëè ñåðâåð
         if Length(P) < 3 then
         begin
-          // Ïåðâûé ïàðàìåòð - ëèáî êàðòà, ëèáî èìÿ WAD ôàéëà
+          // first param is map or wad
           s := UpperCase(P[1]);
-          if g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + s) then
-          begin // Êàðòà íàøëàñü
+          if g_Map_Exist(gGameSettings.WAD + ':\' + s) then
+          begin
             gExitByTrigger := False;
             if gGameOn then
-            begin // Èä¸ò èãðà - çàâåðøàåì óðîâåíü
+            begin
+              // already in game, finish current map
               gNextMap := s;
               gExit := EXIT_ENDLEVELCUSTOM;
             end
-            else // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
-              g_Game_ChangeMap(s);
-          end else
+            else
+            begin
+              // intermission, so change map immediately
+              g_Game_ChangeMap(s)
+            end
+          end
+          else
           begin
-            g_Console_Add(Format(_lc[I_MSG_NO_MAP], [s]));
-            // Òàêîé êàðòû íåò, èùåì WAD ôàéë
-            P[1] := addWadExtension(P[1]);
-            if FileExists(MapsDir + P[1]) then
+            s := P[1];
+            found := e_FindResource(AllMapDirs, s);
+            P[1] := s;
+            g_Console_Add(Format(_lc[I_MSG_NO_MAP_FALLBACK], [s, 'WAD ' + P[1]]));
+            if found then
             begin
-              // Ïàðàìåòðà êàðòû íåò, ïîýòîìó ñòàâèì ïåðâóþ èç ôàéëà
+              // no such map, found wad
+              pw := P[1];
               SetLength(P, 3);
-              P[2] := g_Game_GetFirstMap(MapsDir + P[1]);
-
+              P[1] := ExpandFileName(pw);
+              P[2] := g_Game_GetFirstMap(P[1]);
               s := P[1] + ':\' + P[2];
-
-              if g_Map_Exist(MapsDir + s) then
+              if g_Map_Exist(s) then
               begin
                 gExitByTrigger := False;
                 if gGameOn then
-                begin // Èä¸ò èãðà - çàâåðøàåì óðîâåíü
+                begin
+                  // already in game, finish current map
                   gNextMap := s;
-                  gExit := EXIT_ENDLEVELCUSTOM;
+                  gExit := EXIT_ENDLEVELCUSTOM
+                end
+                else
+                begin
+                  // intermission, so change map immediately
+                  g_Game_ChangeMap(s)
                 end
-                else // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
-                  g_Game_ChangeMap(s);
-              end else
+              end
+              else
+              begin
                 if P[2] = '' then
                   g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[1]]))
                 else
-                  g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
-            end else
-              g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
+                  g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]))
+              end
+            end
+            else
+            begin
+              g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]))
+            end
           end;
-        end else
+        end
+        else
         begin
-          // Óêàçàíî äâà ïàðàìåòðà, çíà÷èò ïåðâûé - WAD ôàéë, à âòîðîé - êàðòà
-          P[1] := addWadExtension(P[1]);
-          if FileExists(MapsDir + P[1]) then
+          s := addWadExtension(P[1]);
+          found := e_FindResource(AllMapDirs, s);
+          P[1] := s;
+          if found then
           begin
-            // Íàøëè WAD ôàéë
             P[2] := UpperCase(P[2]);
             s := P[1] + ':\' + P[2];
-
-            if g_Map_Exist(MapsDir + s) then
-            begin // Íàøëè êàðòó
+            if g_Map_Exist(s) then
+            begin
               gExitByTrigger := False;
               if gGameOn then
-              begin // Èä¸ò èãðà - çàâåðøàåì óðîâåíü
+              begin
                 gNextMap := s;
-                gExit := EXIT_ENDLEVELCUSTOM;
+                gExit := EXIT_ENDLEVELCUSTOM
               end
-              else // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
-                g_Game_ChangeMap(s);
-            end else
-              g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
-          end else
-            g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
-        end;
-      end else
-        g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+              else
+              begin
+                g_Game_ChangeMap(s)
+              end
+            end
+            else
+            begin
+              g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]))
+            end
+          end
+          else
+          begin
+            g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]))
+          end
+        end
+      end
+      else
+      begin
+        g_Console_Add(_lc[I_MSG_GM_UNAVAIL])
+      end
+    end
   end
   else if cmd = 'nextmap' then
   begin
     if not(gGameOn or (gState = STATE_INTERCUSTOM)) then
+    begin
       g_Console_Add(_lc[I_MSG_NOT_GAME])
-    else begin
+    end
+    else
+    begin
       nm := True;
       if Length(P) = 1 then
       begin
@@ -6165,113 +7252,148 @@ begin
         begin
           g_Console_Add(cmd + ' <MAP>');
           g_Console_Add(cmd + ' <WAD> [MAP]');
-        end else begin
+        end
+        else
+        begin
           nm := False;
           g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
         end;
-      end else
+      end
+      else
       begin
         nm := False;
         if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
         begin
           if Length(P) < 3 then
           begin
-            // Ïåðâûé ïàðàìåòð - ëèáî êàðòà, ëèáî èìÿ WAD ôàéëà
+            // first param is map or wad
             s := UpperCase(P[1]);
-            if g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + s) then
-            begin // Êàðòà íàøëàñü
+            if g_Map_Exist(gGameSettings.WAD + ':\' + s) then
+            begin
+              // map founded
               gExitByTrigger := False;
               gNextMap := s;
               nm := True;
-            end else
+            end
+            else
             begin
-              g_Console_Add(Format(_lc[I_MSG_NO_MAP], [s]));
-              // Òàêîé êàðòû íåò, èùåì WAD ôàéë
-              P[1] := addWadExtension(P[1]);
-              if FileExists(MapsDir + P[1]) then
+              // no such map, found wad
+              pw := addWadExtension(P[1]);
+              found := e_FindResource(MapDirs, pw);
+              if not found then
+                found := e_FindResource(WadDirs, pw);
+              P[1] := pw;
+              g_Console_Add(Format(_lc[I_MSG_NO_MAP_FALLBACK], [s, P[1]]));
+              if found then
               begin
-                // Ïàðàìåòðà êàðòû íåò, ïîýòîìó ñòàâèì ïåðâóþ èç ôàéëà
+                // map not specified, select first map
                 SetLength(P, 3);
-                P[2] := g_Game_GetFirstMap(MapsDir + P[1]);
-
+                P[2] := g_Game_GetFirstMap(P[1]);
                 s := P[1] + ':\' + P[2];
-
-                if g_Map_Exist(MapsDir + s) then
-                begin // Óñòàíàâëèâàåì êàðòó
+                if g_Map_Exist(s) then
+                begin
                   gExitByTrigger := False;
                   gNextMap := s;
-                  nm := True;
-                end else
+                  nm := True
+                end
+                else
+                begin
                   if P[2] = '' then
                     g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[1]]))
                   else
-                    g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
-              end else
-                g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
-            end;
-          end else
+                    g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]))
+                end
+              end
+              else
+              begin
+                g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]))
+              end
+            end
+          end
+          else
           begin
-            // Óêàçàíî äâà ïàðàìåòðà, çíà÷èò ïåðâûé - WAD ôàéë, à âòîðîé - êàðòà
-            P[1] := addWadExtension(P[1]);
-            if FileExists(MapsDir + P[1]) then
+            // specified two params wad + map
+            pw := addWadExtension(P[1]);
+            found := e_FindResource(MapDirs, pw);
+            if not found then
+              found := e_FindResource(MapDirs, pw);
+            P[1] := pw;
+            if found then
             begin
-              // Íàøëè WAD ôàéë
               P[2] := UpperCase(P[2]);
               s := P[1] + ':\' + P[2];
-
-              if g_Map_Exist(MapsDir + s) then
-              begin // Íàøëè êàðòó
+              if g_Map_Exist(s) then
+              begin
                 gExitByTrigger := False;
                 gNextMap := s;
-                nm := True;
-              end else
-                g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
-            end else
-              g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
-          end;
-        end else
-          g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+                nm := True
+              end
+              else
+              begin
+                g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]))
+              end
+            end
+            else
+            begin
+              g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]))
+            end
+          end
+        end
+        else
+        begin
+          g_Console_Add(_lc[I_MSG_GM_UNAVAIL])
+        end
       end;
       if nm then
+      begin
         if gNextMap = '' then
           g_Console_Add(_lc[I_MSG_NEXTMAP_UNSET])
         else
-          g_Console_Add(Format(_lc[I_MSG_NEXTMAP_SET], [gNextMap]));
-    end;
+          g_Console_Add(Format(_lc[I_MSG_NEXTMAP_SET], [gNextMap]))
+      end
+    end
   end
   else if (cmd = 'endmap') or (cmd = 'goodbye') then
   begin
     if not gGameOn then
+    begin
       g_Console_Add(_lc[I_MSG_NOT_GAME])
+    end
     else
+    begin
       if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
       begin
         gExitByTrigger := False;
-        // Ñëåäóþùàÿ êàðòà íå çàäàíà, ïðîáóåì íàéòè òðèããåð Âûõîä
+        // next map not specified, try to find trigger EXIT
         if (gNextMap = '') and (gTriggers <> nil) then
+        begin
           for a := 0 to High(gTriggers) do
+          begin
             if gTriggers[a].TriggerType = TRIGGER_EXIT then
             begin
               gExitByTrigger := True;
               //gNextMap := gTriggers[a].Data.MapName;
               gNextMap := gTriggers[a].tgcMap;
-              Break;
-            end;
-        // Èùåì ñëåäóþùóþ êàðòó â WAD ôàéëå
+              Break
+            end
+          end
+        end;
         if gNextMap = '' then
           gNextMap := g_Game_GetNextMap();
-        // Ïðîâåðÿåì, íå çàäàí ëè WAD ôàéë ðåñóðñíîé ñòðîêîé
         if not isWadPath(gNextMap) then
           s := gGameSettings.WAD + ':\' + gNextMap
         else
           s := gNextMap;
-        // Åñëè êàðòà íàéäåíà, âûõîäèì ñ óðîâíÿ
-        if g_Map_Exist(MapsDir + s) then
+        if g_Map_Exist(s) then
           gExit := EXIT_ENDLEVELCUSTOM
         else
-          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [gNextMap]));
-      end else
-        g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [gNextMap]))
+      end
+      else
+      begin
+        g_Console_Add(_lc[I_MSG_GM_UNAVAIL])
+      end
+    end
   end
   else if (cmd = 'event') then
   begin
@@ -6322,6 +7444,76 @@ begin
       end;
     end;
   end
+  else if cmd = 'screenshot' then
+  begin
+    g_TakeScreenShot()
+  end
+  else if (cmd = 'weapnext') or (cmd = 'weapprev') then
+  begin
+    a := 1 - (ord(cmd[5]) - ord('n'));
+    if a = -1 then
+      gWeaponAction[0, WP_PREV] := True;
+    if a = 1 then
+      gWeaponAction[0, WP_NEXT] := True;
+  end
+  else if cmd = 'weapon' then
+  begin
+    if Length(p) = 2 then
+    begin
+      a := WP_FIRST + StrToIntDef(p[1], 0) - 1;
+      if (a >= WP_FIRST) and (a <= WP_LAST) then
+        gSelectWeapon[0, a] := True
+    end
+  end
+  else if (cmd = 'p1_weapnext') or (cmd = 'p1_weapprev')
+       or (cmd = 'p2_weapnext') or (cmd = 'p2_weapprev') then
+  begin
+    a := 1 - (ord(cmd[8]) - ord('n'));
+    b := ord(cmd[2]) - ord('1');
+    if a = -1 then
+      gWeaponAction[b, WP_PREV] := True;
+    if a = 1 then
+      gWeaponAction[b, WP_NEXT] := True;
+  end
+  else if (cmd = 'p1_weapon') or (cmd = 'p2_weapon') then
+  begin
+    if Length(p) = 2 then
+    begin
+      a := WP_FIRST + StrToIntDef(p[1], 0) - 1;
+      b := ord(cmd[2]) - ord('1');
+      if (a >= WP_FIRST) and (a <= WP_LAST) then
+        gSelectWeapon[b, a] := True
+    end
+  end
+  else if (cmd = 'p1_weapbest') or (cmd = 'p2_weapbest') then
+  begin
+    b := ord(cmd[2]) - ord('1');
+    if b = 0 then
+      gSelectWeapon[b, gPlayer1.GetMorePrefered()] := True
+    else
+      gSelectWeapon[b, gPlayer2.GetMorePrefered()] := True;
+  end
+  else if (cmd = 'dropflag') then
+  begin
+    if g_Game_IsServer then
+    begin
+      if gPlayer2 <> nil then gPlayer2.TryDropFlag();
+      if gPlayer1 <> nil then gPlayer1.TryDropFlag();
+    end
+    else
+      MC_SEND_CheatRequest(NET_CHEAT_DROPFLAG);
+  end
+  else if (cmd = 'p1_dropflag') or (cmd = 'p2_dropflag') then
+  begin
+    b := ord(cmd[2]) - ord('1');
+    if g_Game_IsServer then
+    begin
+      if (b = 1) and (gPlayer2 <> nil) then gPlayer2.TryDropFlag()
+      else if (b = 0) and (gPlayer1 <> nil) then gPlayer1.TryDropFlag();
+    end
+    else
+      MC_SEND_CheatRequest(NET_CHEAT_DROPFLAG);
+  end
 // Êîìàíäû Ñâîåé èãðû:
   else if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
   begin
@@ -6398,6 +7590,30 @@ begin
       end else
         g_Console_Add(_lc[I_MSG_SERVERONLY]);
     end
+    else if cmd = 'centerprint' then
+    begin
+      if (Length(P) > 2) and (P[1] <> '') then
+      begin
+        chstr := '';
+        for a := 2 to High(P) do
+          chstr := chstr + P[a] + ' ';
+
+        if Length(chstr) > 200 then SetLength(chstr, 200);
+
+        if Length(chstr) < 1 then
+        begin
+          g_Console_Add('centerprint <timeout> <text>');
+          Exit;
+        end;
+
+        a := StrToIntDef(P[1], 100);
+        chstr := b_Text_Format(chstr);
+        g_Game_Message(chstr, a);
+        if g_Game_IsNet and g_Game_IsServer then
+          MH_SEND_GameEvent(NET_EV_BIGTEXT, a, chstr);
+      end
+      else g_Console_Add('centerprint <timeout> <text>');
+    end
     else if (cmd = 'overtime') and not g_Game_IsClient then
     begin
       if (Length(P) = 1) or (StrToIntDef(P[1], -1) <= 0) then
@@ -6503,47 +7719,101 @@ begin
   end;
 end;
 
-procedure g_TakeScreenShot();
+procedure SystemCommands(P: SSArray);
 var
-  a: Word;
-  FileName: string;
-  ssdir, t: string;
-  st: TStream;
-  ok: Boolean;
+  cmd: string;
 begin
-  if e_NoGraphics then Exit;
-  ssdir := GameDir+'/screenshots';
-  if not findFileCI(ssdir, true) then
-  begin
-    // try to create dir
-    try
-      CreateDir(ssdir);
-    except
-    end;
-    if not findFileCI(ssdir, true) then exit; // alas
-  end;
-  try
-    for a := 1 to High(Word) do
-    begin
-      FileName := Format(ssdir+'screenshot%.3d.png', [a]);
-      t := FileName;
-      if findFileCI(t, true) then continue;
-      if not findFileCI(FileName) then
-      begin
-        ok := false;
-        st := createDiskFile(FileName);
-        try
-          e_MakeScreenshot(st, gScreenWidth, gScreenHeight);
-          ok := true;
-        finally
-          st.Free();
+  cmd := LowerCase(P[0]);
+  case cmd of
+    'exit', 'quit':
+      begin
+        g_Game_Free();
+        g_Game_Quit();
+      end;
+    'r_reset':
+      begin
+        gRC_Width := Max(1, gRC_Width);
+        gRC_Height := Max(1, gRC_Height);
+        gBPP := Max(1, gBPP);
+        if sys_SetDisplayMode(gRC_Width, gRC_Height, gBPP, gRC_FullScreen, gRC_Maximized) = True then
+          e_LogWriteln('resolution changed')
+        else
+          e_LogWriteln('resolution not changed');
+        sys_EnableVSync(gVSync);
+      end;
+    'r_maxfps':
+      begin
+        if Length(p) = 2 then
+        begin
+          gMaxFPS := StrToIntDef(p[1], gMaxFPS);
+          if gMaxFPS > 0 then
+            gFrameTime := 1000 div gMaxFPS
+          else
+            gFrameTime := 0;
         end;
-        if not ok then try DeleteFile(FileName); except end else g_Console_Add(Format(_lc[I_CONSOLE_SCREENSHOT], [ExtractFileName(FileName)]));
-        break;
+        e_LogWritefln('r_maxfps %d', [gMaxFPS]);
+      end;
+    'g_language':
+      begin
+        if Length(p) = 2 then
+        begin
+          gAskLanguage := true;
+          gLanguage := LANGUAGE_ENGLISH;
+          case LowerCase(p[1]) of
+            'english':
+               begin
+                 gAskLanguage := false;
+                 gLanguage := LANGUAGE_ENGLISH;
+               end;
+            'russian':
+               begin
+                 gAskLanguage := false;
+                 gLanguage := LANGUAGE_RUSSIAN;
+               end;
+            'ask':
+               begin
+                 gAskLanguage := true;
+                 gLanguage := LANGUAGE_ENGLISH;
+               end;
+          end;
+          g_Language_Set(gLanguage);
+        end
+        else
+        begin
+          e_LogWritefln('usage: %s <English|Russian|Ask>', [cmd]);
+        end
       end;
+  end;
+end;
+
+procedure g_TakeScreenShot(Filename: string = '');
+  var s: TStream; t: TDateTime; dir, date, name: String;
+begin
+  if e_NoGraphics then Exit;
+  try
+    dir := e_GetWriteableDir(ScreenshotDirs);
+
+    if Filename = '' then
+    begin
+      t := Now;
+      DateTimeToString(date, 'yyyy-mm-dd-hh-nn-ss', t);
+      Filename := 'screenshot-' + date;
     end;
+    
+    name := e_CatPath(dir, Filename + '.png');
+    s := createDiskFile(name);
+    try
+      e_MakeScreenshot(s, gWinSizeX, gWinSizeX);
+      s.Free;
+      g_Console_Add(Format(_lc[I_CONSOLE_SCREENSHOT], [name]))
+    except
+      g_Console_Add(Format(_lc[I_CONSOLE_ERROR_WRITE], [name]));
+      s.Free;
+      DeleteFile(name)
+    end
   except
-  end;
+    g_Console_Add('oh shit, i can''t create screenshot!')
+  end
 end;
 
 procedure g_Game_InGameMenu(Show: Boolean);
@@ -6648,27 +7918,14 @@ begin
     e_StopChannels();
 end;
 
-procedure g_Game_UpdateTriggerSounds();
-var
-  i: Integer;
+procedure g_Game_UpdateTriggerSounds;
+  var i: Integer;
 begin
   if gTriggers <> nil then
     for i := 0 to High(gTriggers) do
       with gTriggers[i] do
-        if (TriggerType = TRIGGER_SOUND) and
-           (Sound <> nil) and
-           (tgcLocal) and
-           Sound.IsPlaying() then
-        begin
-          if ((gPlayer1 <> nil) and g_CollidePoint(gPlayer1.GameX, gPlayer1.GameY, X, Y, Width, Height)) or
-             ((gPlayer2 <> nil) and g_CollidePoint(gPlayer2.GameX, gPlayer2.GameY, X, Y, Width, Height)) then
-          begin
-            Sound.SetPan(0.5 - tgcPan/255.0);
-            Sound.SetVolume(tgcVolume/255.0);
-          end
-          else
-            Sound.SetCoords(X+(Width div 2), Y+(Height div 2), tgcVolume/255.0);
-        end;
+        if (TriggerType = TRIGGER_SOUND) and (Sound <> nil) and tgcLocal and Sound.IsPlaying() then
+          Sound.SetCoordsRect(X, Y, Width, Height, tgcVolume / 255.0)
 end;
 
 function g_Game_IsWatchedPlayer(UID: Word): Boolean;
@@ -6734,10 +7991,76 @@ end;
 
 procedure g_Game_Message(Msg: string; Time: Word);
 begin
-  MessageText := b_Text_Format(Msg);
+  MessageLineLength := (gScreenWidth - 204) div e_CharFont_GetMaxWidth(gMenuFont);
+  MessageText := b_Text_Wrap(b_Text_Format(Msg), MessageLineLength);
   MessageTime := Time;
 end;
 
+procedure g_Game_ChatSound(Text: String; Taunt: Boolean = True);
+const
+  punct: Array[0..13] of String =
+  ('.', ',', ':', ';', '!', '?', '(', ')', '''', '"', '/', '\', '*', '^');
+var
+  i, j: Integer;
+  ok: Boolean;
+  fpText: String;
+
+  function IsPunctuation(S: String): Boolean;
+  var
+    i: Integer;
+  begin
+    Result := False;
+    if Length(S) <> 1 then
+      Exit;
+    for i := Low(punct) to High(punct) do
+      if S = punct[i] then
+      begin
+        Result := True;
+        break;
+      end;
+  end;
+  function FilterPunctuation(S: String): String;
+  var
+    i: Integer;
+  begin
+    for i := Low(punct) to High(punct) do
+      S := StringReplace(S, punct[i], ' ', [rfReplaceAll]);
+    Result := S;
+  end;
+begin
+  ok := False;
+
+  if gUseChatSounds and Taunt and (gChatSounds <> nil) and (Pos(': ', Text) > 0) then
+  begin
+    // remove player name
+    Delete(Text, 1, Pos(': ', Text) + 2 - 1);
+    // for FullWord check
+    Text := toLowerCase1251(' ' + Text + ' ');
+    fpText := FilterPunctuation(Text);
+
+    for i := 0 to Length(gChatSounds) - 1 do
+    begin
+      ok := True;
+      for j := 0 to Length(gChatSounds[i].Tags) - 1 do
+      begin
+        if gChatSounds[i].FullWord and (not IsPunctuation(gChatSounds[i].Tags[j])) then
+          ok := Pos(' ' + gChatSounds[i].Tags[j] + ' ', fpText) > 0
+        else
+          ok := Pos(gChatSounds[i].Tags[j], Text) > 0;
+        if not ok then
+          break;
+      end;
+      if ok then
+      begin
+        gChatSounds[i].Sound.Play();
+        break;
+      end;
+    end;
+  end;
+  if not ok then
+    g_Sound_PlayEx('SOUND_GAME_RADIO');
+end;
+
 procedure g_Game_Announce_GoodShot(SpawnerUID: Word);
 var
   a: Integer;
@@ -6810,6 +8133,24 @@ begin
   killsnd[n].Play();
 end;
 
+procedure g_Game_Announce_BodyKill(SpawnerUID: Word);
+var
+  a: Integer;
+begin
+  case gAnnouncer of
+    ANNOUNCE_NONE:
+      Exit;
+    ANNOUNCE_ME:
+      if not g_Game_IsWatchedPlayer(SpawnerUID) then
+        Exit;
+  end;
+  for a := 0 to 2 do
+    if hahasnd[a].IsPlaying() then
+      Exit;
+
+  hahasnd[Random(3)].Play();
+end;
+
 procedure g_Game_StartVote(Command, Initiator: string);
 var
   Need: Integer;
@@ -6951,6 +8292,7 @@ begin
     CurValue := 0;
     MaxValue := Max;
     ShowCount := 0;
+    PBarWasHere := false;
   end;
 
   g_ActiveWindow := nil;
@@ -6958,13 +8300,18 @@ begin
   ProcessLoading(true);
 end;
 
-procedure g_Game_StepLoading();
+procedure g_Game_StepLoading(Value: Integer = -1);
 begin
   with LoadingStat do
   begin
-    Inc(CurValue);
-    Inc(ShowCount);
-    if (ShowCount > LOADING_SHOW_STEP) then
+    if Value = -1 then
+    begin
+      Inc(CurValue);
+      Inc(ShowCount);
+    end
+    else
+      CurValue := Value;
+    if (ShowCount > LOADING_SHOW_STEP) or (Value > -1) then
     begin
       ShowCount := 0;
       ProcessLoading();
@@ -6987,6 +8334,7 @@ begin
     for len := Low(Msgs) to High(Msgs) do
       Msgs[len] := '';
     NextMsg := 0;
+    PBarWasHere := false;
   end;
 end;
 
@@ -7000,9 +8348,9 @@ begin
   while i <= ParamCount do
   begin
     s := ParamStr(i);
-    if (s[1] = '-') and (Length(s) > 1) then
+    if (Length(s) > 1) and (s[1] = '-') then
     begin
-      if (s[2] = '-') and (Length(s) > 2) then
+      if (Length(s) > 2) and (s[2] = '-') then
         begin // Îäèíî÷íûé ïàðàìåòð
           SetLength(pars, Length(pars) + 1);
           with pars[High(pars)] do
@@ -7127,40 +8475,47 @@ begin
   // Options:
     s := Find_Param_Value(pars, '-opt');
     if (s = '') then
-      Opt := GAME_OPTION_ALLOWEXIT or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER
+      Opt := gsGameFlags
     else
       Opt := StrToIntDef(s, 0);
-    if Opt = 0 then
-      Opt := GAME_OPTION_ALLOWEXIT or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER;
 
   // Close after map:
     s := Find_Param_Value(pars, '--close');
     if (s <> '') then
       gMapOnce := True;
 
+  // Override map to test:
+    s := LowerCase(Find_Param_Value(pars, '-testmap'));
+    if s <> '' then
+    begin
+      if e_IsValidResourceName(s) then
+        e_FindResource(AllMapDirs, s);
+      gTestMap := ExpandFileName(s);
+    end;
+
   // Delete test map after play:
     s := Find_Param_Value(pars, '--testdelete');
     if (s <> '') then
     begin
-      gMapToDelete := MapsDir + map;
+      //gMapToDelete := MapsDir + map;
       e_WriteLog('"--testdelete" is deprecated, use --tempdelete.', TMsgType.Fatal);
       Halt(1);
     end;
 
   // Delete temporary WAD after play:
     s := Find_Param_Value(pars, '--tempdelete');
-    if (s <> '') then
+    if (s <> '') and (gTestMap <> '') then
     begin
-      gMapToDelete := MapsDir + map;
+      gMapToDelete := gTestMap;
       gTempDelete := True;
     end;
 
   // Number of players:
     s := Find_Param_Value(pars, '-pl');
     if (s = '') then
-      n := 1
+      n := DEFAULT_PLAYERS
     else
-      n := StrToIntDef(s, 1);
+      n := StrToIntDef(s, DEFAULT_PLAYERS);
 
   // Start:
     s := Find_Param_Value(pars, '-port');
@@ -7174,8 +8529,8 @@ begin
   s := Find_Param_Value(pars, '-exec');
   if s <> '' then
   begin
-    if not isWadPath(s) then
-      s := GameDir + '/' + s;
+//    if not isWadPath(s) then
+//      s := GameDir + '/' + s;
 
     {$I-}
     AssignFile(F, s);
@@ -7228,10 +8583,28 @@ begin
   conRegVar('los_enabled', @gmon_dbg_los_enabled, 'enable/disable monster LOS calculations', 'monster LOS', true);
   conRegVar('mon_think', @gmon_debug_think, 'enable/disable monster thinking', 'monster thinking', true);
 
+{$IFDEF ENABLE_HOLMES}
   conRegVar('dbg_holmes', @g_holmes_enabled, 'enable/disable Holmes', 'Holmes', true);
+{$ENDIF}
 
-  conRegVar('dbg_scale', @g_dbg_scale, 0.01, 100.0, 'experimental deBUG scale mode', '',  false);
+  conRegVar('dbg_ignore_level_bounds', @g_dbg_ignore_bounds, 'ignore level bounds', '',  false);
+
+  conRegVar('r_scale', @g_dbg_scale, 0.01, 100.0, 'render scale', '',  false);
+  conRegVar('r_resolution_scale', @r_pixel_scale, 0.01, 100.0, 'upscale factor', '', false);
 
   conRegVar('light_enabled', @gwin_k8_enable_light_experiments, 'enable/disable dynamic lighting', 'lighting');
   conRegVar('light_player_halo', @g_playerLight, 'enable/disable player halo', 'player light halo');
+
+  conRegVar('r_smallmap_align_h', @r_smallmap_h, 'halign: 0: left; 1: center; 2: right', 'horizontal aligning of small maps');
+  conRegVar('r_smallmap_align_v', @r_smallmap_v, 'valign: 0: top; 1: center; 2: bottom', 'vertial aligning of small maps');
+
+  conRegVar('r_showfps', @gShowFPS, 'draw fps counter', 'draw fps counter');
+  conRegVar('r_showtime', @gShowTime, 'show game time', 'show game time');
+  conRegVar('r_showping', @gShowPing, 'show ping', 'show ping');
+  conRegVar('r_showscore', @gShowGoals, 'show score', 'show score');
+  conRegVar('r_showkillmsg', @gShowKillMsg, 'show kill log', 'show kill log');
+  conRegVar('r_showlives', @gShowLives, 'show lives', 'show lives');
+  conRegVar('r_showspect', @gSpectHUD, 'show spectator hud', 'show spectator hud');
+  conRegVar('r_showstat', @gShowStat, 'show stats', 'show stats');
+  conRegVar('r_showpids', @gShowPIDs, 'show PIDs', 'show PIDs');
 end.