DEADSOFTWARE

added optional framebuffer and resolution scaling
[d2df-sdl.git] / src / game / g_game.pas
index e6429bc23bbaa28f9776ab1f7a3d872a5fb35791..8d784c2a2be634127f934150244bae03af5e5cb2 100644 (file)
@@ -2,8 +2,7 @@
  *
  * 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
@@ -32,6 +31,7 @@ type
     TimeLimit: Word;
     GoalLimit: Word;
     WarmupTime: Word;
+    SpawnInvul: Word;
     MaxLives: Byte;
     Options: LongWord;
     WAD: String;
@@ -102,9 +102,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;
@@ -127,7 +126,7 @@ 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;
@@ -180,6 +179,8 @@ const
   GAME_OPTION_MONSTERS     = 16;
   GAME_OPTION_BOTVSPLAYER  = 32;
   GAME_OPTION_BOTVSMONSTER = 64;
+  GAME_OPTION_DMKEYS       = 128;
+  GAME_OPTION_RESPAWNITEMS = 256;
 
   STATE_NONE        = 0;
   STATE_MENU        = 1;
@@ -222,6 +223,8 @@ const
   DEFAULT_PLAYERS = 1;
 {$ENDIF}
 
+  STATFILE_VERSION = $03;
+
 var
   gStdFont: DWORD;
   gGameSettings: TGameSettings;
@@ -239,7 +242,7 @@ var
   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;
@@ -288,12 +291,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;
@@ -378,13 +379,13 @@ uses
 {$IFDEF ENABLE_HOLMES}
   g_holmes,
 {$ENDIF}
-  e_texture, g_textures, g_main, g_window, g_menu,
+  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, Math,
   g_triggers, g_monsters, e_sound, CONFIG,
-  g_language, g_net,
+  g_language, g_net, g_main,
   ENet, e_msg, g_netmsg, g_netmaster,
-  sfs, wadreader;
+  sfs, wadreader, g_system;
 
 
 var
@@ -586,6 +587,9 @@ var
   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;
@@ -644,6 +648,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 := '';
@@ -763,17 +829,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
@@ -812,18 +878,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/';
     TEXTUREFILTER := GL_LINEAR;
-    g_Texture_CreateWADEx('TEXTURE_endpic', s+MegaWAD.endpic);
+    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();
@@ -895,7 +959,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;
@@ -905,6 +969,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));
@@ -1000,6 +1065,19 @@ 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');
@@ -1276,6 +1354,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 := '';
@@ -1305,26 +1387,42 @@ 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;
@@ -1342,8 +1440,10 @@ begin
     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');
@@ -1676,6 +1776,9 @@ begin
     Exit;
   end;
 
+  // process master server communications
+  g_Net_Slist_Pulse();
+
   case gState of
     STATE_INTERSINGLE, // Ñòàòèñòêà ïîñëå ïðîõîæäåíèÿ óðîâíÿ â Îäèíî÷íîé èãðå
     STATE_INTERCUSTOM, // Ñòàòèñòêà ïîñëå ïðîõîæäåíèÿ óðîâíÿ â Ñâîåé èãðå
@@ -1705,7 +1808,7 @@ begin
             and (not gJustChatted) and (not gConsoleShow) and (not gChatShow)
             and (g_ActiveWindow = nil)
           )
-          or (g_Game_IsNet and ((gInterTime > gInterEndTime) or (gInterReadyCount >= NetClientCount)))
+          or (g_Game_IsNet and ((gInterTime > gInterEndTime) or ((gInterReadyCount >= NetClientCount) and (NetClientCount > 0))))
         )
         then
         begin // Íàæàëè <Enter>/<Ïðîáåë> èëè ïðîøëî äîñòàòî÷íî âðåìåíè:
@@ -2123,19 +2226,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
@@ -2174,7 +2276,9 @@ 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;
@@ -2188,7 +2292,7 @@ begin
     KeyPress(IK_F10);
   end;
 
-  Time := GetTimer() {div 1000};
+  Time := sys_GetTicks() {div 1000};
 
 // Îáðàáîòêà îòëîæåííûõ ñîáûòèé:
   if gDelayedEvents <> nil then
@@ -2544,7 +2648,7 @@ var
 begin
   e_TextureFontGetSize(gStdFont, ww2, hh2);
 
-  g_ProcessMessages();
+  sys_HandleInput;
 
   if g_Console_Action(ACTION_SCORES) then
   begin
@@ -2771,6 +2875,13 @@ begin
         _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();
@@ -3308,7 +3419,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);
@@ -3508,7 +3619,7 @@ end;
 
 procedure DrawPlayer(p: TPlayer);
 var
-  px, py, a, b, c, d: Integer;
+  px, py, a, b, c, d, i: Integer;
   //R: TRect;
 begin
   if (p = nil) or (p.FDummy) then
@@ -3558,67 +3669,45 @@ 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;
-
-    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;
-  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));
-
   sX := -a;
-  sY := -(b+p.IncCam);
+  sY := -b;
   sWidth := gPlayerScreenSize.X;
   sHeight := gPlayerScreenSize.Y;
+  fixViewportForScale();
 
-  //glTranslatef(a, b+p.IncCam, 0);
-
-  //if (p = gPlayer1) and (g_dbg_scale >= 1.0) then g_Holmes_plrViewSize(sWidth, sHeight);
+  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;
 
-  //conwritefln('OLD: (%s,%s)-(%s,%s)', [sX, sY, sWidth, sHeight]);
-  fixViewportForScale();
-  //conwritefln('     (%s,%s)-(%s,%s)', [sX, sY, sWidth, sHeight]);
+  sY := sY - p.IncCam;
 
-  if (g_dbg_scale <> 1.0) and (not g_dbg_ignore_bounds) then
+  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;
-
-    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));
   end;
 
+  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));
+
   //r_smallmap_h: 0: left; 1: center; 2: right
   //r_smallmap_v: 0: top; 1: center; 2: bottom
   // horiz small map?
@@ -3663,8 +3752,21 @@ begin
 
   renderMapInternal(-c, -d, true);
 
-  if (gGameSettings.GameMode <> GM_SINGLE) and gPlayerIndicator then
-    p.DrawIndicator();
+  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;
+
   if p.FSpectator then
     e_TextureFontPrintEx(p.GameX + PLAYER_RECT_CX - 4,
                          p.GameY + PLAYER_RECT_CY - 4,
@@ -3720,7 +3822,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
@@ -4037,7 +4139,9 @@ begin
     g_ActiveWindow.Draw();
   end;
 
+{$IFNDEF HEADLESS}
   g_Console_Draw();
+{$ENDIF}
 
   if g_debug_Sounds and gGameOn then
   begin
@@ -4068,12 +4172,13 @@ 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();
+{$IFNDEF HEADLESS}
   //g_Menu_Free(); //k8: this segfaults after resolution change; who cares?
+{$ENDIF}
 
   if NetInitDone then g_Net_Free;
 
@@ -4082,7 +4187,7 @@ begin
     g_Game_DeleteTestMap();
 
   gExit := EXIT_QUIT;
-  PushExitEvent();
+  sys_RequestQuit;
 end;
 
 procedure g_FatalError(Text: String);
@@ -4140,7 +4245,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);
@@ -4176,9 +4281,7 @@ begin
       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;
@@ -4208,9 +4311,7 @@ begin
       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;
@@ -4233,9 +4334,7 @@ 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
       gPlayer2 := nil;
     Exit;
@@ -4249,9 +4348,7 @@ 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
       gPlayer1 := nil;
@@ -4259,6 +4356,7 @@ begin
     end;
     Exit;
   end;
+  g_Net_Slist_ServerPlayerLeaves();
 end;
 
 procedure g_Game_Spectate();
@@ -4331,7 +4429,7 @@ begin
   end;
 
 // Çàãðóçêà è çàïóñê êàðòû:
-  if not g_Game_StartMap(MAP, True) then
+  if not g_Game_StartMap(false{asMegawad}, MAP, True) then
   begin
     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]));
@@ -4421,7 +4519,7 @@ begin
   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;
@@ -4452,6 +4550,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);
 
@@ -4522,15 +4621,18 @@ begin
 // Ñòàðòóåì ñåðâåð
   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(NetSlistIP, NetSlistPort, NetSlistList);
+
+  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;
@@ -4542,6 +4644,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;
@@ -4549,6 +4652,7 @@ begin
 // Íàñòðîéêè èãðîêîâ è áîòîâ:
   g_Player_Init();
 
+  g_Net_Slist_ServerMapStarted();
   NetState := NET_STATE_GAME;
 end;
 
@@ -4590,6 +4694,10 @@ begin
   NetState := NET_STATE_AUTH;
 
   g_Game_SetLoadingText(_lc[I_LOAD_CONNECT], 0, False);
+
+  // create (or update) map/resource databases
+  g_Res_CreateDatabases(true);
+
 // Ñòàðòóåì êëèåíò
   if not g_Net_Connect(Addr, Port) then
   begin
@@ -4605,13 +4713,25 @@ 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();
@@ -4634,11 +4754,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]);
@@ -4646,8 +4766,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,
@@ -4666,7 +4788,7 @@ begin
           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]));
 
@@ -4701,8 +4823,7 @@ begin
 
     ProcessLoading(true);
 
-    if e_KeyPressed(IK_SPACE) or e_KeyPressed(IK_ESCAPE) or e_KeyPressed(VK_ESCAPE) or
-       e_KeyPressed(JOY0_JUMP) or e_KeyPressed(JOY1_JUMP) or e_KeyPressed(JOY2_JUMP) or e_KeyPressed(JOY3_JUMP) then
+    if g_Net_UserRequestExit() then
     begin
       State := 0;
       break;
@@ -4725,10 +4846,8 @@ begin
   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
@@ -4743,7 +4862,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;
 
@@ -4754,17 +4873,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();
@@ -4782,21 +4903,59 @@ 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
+      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
-      g_Game_ClientWAD(NewWAD, gWADHash);
-  end else
+      NewWAD := g_Game_ClientWAD(NewWAD, gWADHash);
+    end;
+  end
+  else
+  begin
+    NewWAD := gGameSettings.WAD;
     ResName := Map;
+  end;
 
-  Result := g_Map_Load(MapsDir + gGameSettings.WAD + ':\' + ResName);
+  //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);
@@ -4865,14 +5024,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
@@ -4901,11 +5053,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;
 
@@ -4933,7 +5085,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
@@ -4942,7 +5094,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
@@ -4968,19 +5120,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();
@@ -4990,6 +5159,7 @@ begin
   end;
   NewWAD := ExtractRelativePath(MapsDir, gWAD);
   g_Game_LoadWAD(NewWAD);
+  }
 end;
 
 procedure g_Game_RestartRound(NoMapRestart: Boolean = False);
@@ -5100,7 +5270,7 @@ var
 begin
   Result := '';
 
-  MapList := g_Map_GetMapsList(MapsDir + gGameSettings.WAD);
+  MapList := g_Map_GetMapsList(gGameSettings.WAD);
   if MapList = nil then
     Exit;
 
@@ -5122,7 +5292,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;
@@ -5365,6 +5535,48 @@ begin
       if g_Game_IsNet then MH_SEND_GameSettings;
     end;
   end
+  else if (cmd = 'g_dm_keys') 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_DMKEYS
+        else
+          Options := Options and (not GAME_OPTION_DMKEYS);
+      end;
+
+      if (LongBool(Options and GAME_OPTION_DMKEYS)) then
+        g_Console_Add(_lc[I_MSG_DMKEYS_ON])
+      else
+        g_Console_Add(_lc[I_MSG_DMKEYS_OFF]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if (cmd = 'g_respawn_items') 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_RESPAWNITEMS
+        else
+          Options := Options and (not GAME_OPTION_RESPAWNITEMS);
+      end;
+
+      if (LongBool(Options and GAME_OPTION_RESPAWNITEMS)) then
+        g_Console_Add(_lc[I_MSG_RESPAWNITEMS_ON])
+      else
+        g_Console_Add(_lc[I_MSG_RESPAWNITEMS_OFF]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
   else if (cmd = 'g_warmuptime') and not g_Game_IsClient then
   begin
     if Length(P) > 1 then
@@ -5379,31 +5591,52 @@ begin
                  [gGameSettings.WarmupTime]));
     g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
   end
+  else if (cmd = 'g_spawn_invul') and not g_Game_IsClient then
+  begin
+    if Length(P) > 1 then
+    begin
+      if StrToIntDef(P[1], gGameSettings.SpawnInvul) = 0 then
+        gGameSettings.SpawnInvul := 0
+      else
+        gGameSettings.SpawnInvul := StrToIntDef(P[1], gGameSettings.SpawnInvul);
+    end;
+
+    g_Console_Add(Format(_lc[I_MSG_SPAWNINVUL],
+                 [gGameSettings.SpawnInvul]));
+    g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+  end
   else if cmd = 'net_interp' then
   begin
     if (Length(P) > 1) then
       NetInterpLevel := StrToIntDef(P[1], NetInterpLevel);
-
     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();
+    s := e_GetWriteableDir(ConfigDirs);
+    if s <> '' then
+    begin
+      config := TConfig.CreateFile(s + '/' + CONFIG_FILENAME);
+      config.WriteInt('Client', 'InterpolationSteps', NetInterpLevel);
+      config.SaveFile(s + '/' + CONFIG_FILENAME);
+      config.Free
+    end
   end
   else if cmd = 'net_forceplayerupdate' then
   begin
-    if (Length(P) > 1) and
-       ((P[1] = '1') or (P[1] = '0')) then
+    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')
     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();
+
+    s := e_GetWriteableDir(ConfigDirs);
+    if s <> '' then
+    begin
+      config := TConfig.CreateFile(s + '/' + CONFIG_FILENAME);
+      config.WriteBool('Client', 'ForcePlayerUpdate', NetForcePlayerUpdate);
+      config.SaveFile(s + '/' + CONFIG_FILENAME);
+      config.Free
+    end
   end
   else if cmd = 'net_predictself' then
   begin
@@ -5415,10 +5648,15 @@ begin
       g_Console_Add('net_predictself = 1')
     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();
+
+    s := e_GetWriteableDir(ConfigDirs);
+    if s <> '' then
+    begin
+      config := TConfig.CreateFile(s + '/' + CONFIG_FILENAME);
+      config.WriteBool('Client', 'PredictSelf', NetPredictSelf);
+      config.SaveFile(s + '/' + CONFIG_FILENAME);
+      config.Free
+    end
   end
   else if cmd = 'sv_name' then
   begin
@@ -5427,8 +5665,7 @@ 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;
+      g_Net_Slist_ServerRenamed();
     end;
 
     g_Console_Add(cmd + ' = "' + NetServerName + '"');
@@ -5440,8 +5677,7 @@ 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;
+      g_Net_Slist_ServerRenamed();
     end;
 
     g_Console_Add(cmd + ' = "' + AnsiLowerCase(NetPassword) + '"');
@@ -5455,6 +5691,7 @@ begin
       begin
         b := 0;
         for a := 0 to High(NetClients) do
+        begin
           if NetClients[a].Used then
           begin
             Inc(b);
@@ -5466,8 +5703,8 @@ begin
               MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
             end;
           end;
-        if NetUseMaster then
-          g_Net_Slist_Update;
+        end;
+        g_Net_Slist_ServerRenamed();
       end;
     end;
 
@@ -5478,17 +5715,7 @@ begin
     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();
+      if NetUseMaster then g_Net_Slist_Public() else g_Net_Slist_Private();
     end;
 
     g_Console_Add(cmd + ' = ' + IntToStr(Byte(NetUseMaster)));
@@ -5745,11 +5972,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
@@ -6089,6 +6313,7 @@ var
   prt: Word;
   nm: Boolean;
   listen: LongWord;
+  found: Boolean;
 begin
 // Îáùèå êîìàíäû:
   cmd := LowerCase(P[0]);
@@ -6143,8 +6368,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
@@ -6157,8 +6381,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;
@@ -6189,8 +6412,7 @@ 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
@@ -6219,8 +6441,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
@@ -6250,8 +6471,7 @@ 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]);
@@ -6280,8 +6500,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
@@ -6312,8 +6531,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;
     end else
       g_Console_Add(_lc[I_MSG_SERVERONLY]);
@@ -6503,22 +6721,26 @@ begin
       g_Console_Add(cmd + ' <WAD> [MAP] [# players]');
       Exit;
     end;
-    // Èãðà åù¸ íå çàïóùåíà, ñíà÷àëà íàì íàäî çàãðóçèòü êàêîé-òî WAD
-    P[1] := addWadExtension(P[1]);
-    if FileExists(MapsDir + P[1]) then
+    // 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
@@ -6558,43 +6780,46 @@ 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;
+          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_FALLBACK], [UpperCase(P[4]), P[3]]));
-    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
@@ -6603,92 +6828,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
-            // Òàêîé êàðòû íåò, èùåì WAD ôàéë
-            pw := findDiskWad(MapsDir + P[1]);
+            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 FileExists(pw) then
+            if found then
             begin
-              // Ïàðàìåòðà êàðòû íåò, ïîýòîìó ñòàâèì ïåðâóþ èç ôàéëà
+              // no such map, found wad
+              pw := P[1];
               SetLength(P, 3);
-              P[1] := ExtractRelativePath(MapsDir, pw);
-              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 // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
-                  g_Game_ChangeMap(s);
-              end else
+                else
+                begin
+                  // intermission, so change map immediately
+                  g_Game_ChangeMap(s)
+                end
+              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
@@ -6696,113 +6955,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
-              // Òàêîé êàðòû íåò, èùåì WAD ôàéë
-              P[1] := addWadExtension(P[1]);
+              // 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 FileExists(MapsDir + P[1]) then
+              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
@@ -6952,6 +7246,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
@@ -7057,47 +7375,34 @@ begin
   end;
 end;
 
-procedure g_TakeScreenShot();
-var
-  a: Word;
-  FileName: string;
-  ssdir, t: string;
-  st: TStream;
-  ok: Boolean;
+procedure g_TakeScreenShot(Filename: string = '');
+  var s: TStream; t: TDateTime; dir, date, name: 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();
-        end;
-        if not ok then try DeleteFile(FileName); except end else g_Console_Add(Format(_lc[I_CONSOLE_SCREENSHOT], [ExtractFileName(FileName)]));
-        break;
-      end;
+    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, gScreenWidth, gScreenHeight);
+      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);
@@ -7774,13 +8079,17 @@ begin
   // Override map to test:
     s := LowerCase(Find_Param_Value(pars, '-testmap'));
     if s <> '' then
-      gTestMap := MapsDir + s;
+    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;
@@ -7812,8 +8121,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);
@@ -7873,6 +8182,7 @@ begin
   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');