DEADSOFTWARE

Game: Add CSV stats and inter screenshots
authorfgsfds <pvt.fgsfds@gmail.com>
Mon, 23 Dec 2019 22:37:35 +0000 (01:37 +0300)
committerfgsfds <pvt.fgsfds@gmail.com>
Mon, 23 Dec 2019 22:37:35 +0000 (01:37 +0300)
src/game/g_game.pas
src/game/g_main.pas
src/game/g_options.pas
src/shared/utils.pas

index ca8ee633877789f8b162f376495952d0c8800025..686f15692b6a73af4f9c728254ef1948967cba24 100644 (file)
@@ -125,7 +125,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 = ''; StatShot: Boolean = False);
 procedure g_FatalError(Text: String);
 procedure g_SimpleError(Text: String);
 function  g_Game_IsTestMap(): Boolean;
@@ -220,6 +220,8 @@ const
   DEFAULT_PLAYERS = 1;
 {$ENDIF}
 
+  STATFILE_VERSION = $03;
+
 var
   gStdFont: DWORD;
   gGameSettings: TGameSettings;
@@ -373,11 +375,11 @@ uses
 {$IFDEF ENABLE_HOLMES}
   g_holmes,
 {$ENDIF}
-  e_texture, e_res, 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, g_system;
 
@@ -581,6 +583,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;
@@ -639,6 +644,55 @@ 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 version, datetime, server name, map name, game mode, time limit, score limit, dmflags, game time, number of 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, fname, 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, 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 := '';
@@ -898,6 +952,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));
@@ -993,6 +1048,18 @@ begin
             end;
 
           SortGameStat(CustomStat.PlayerStat);
+
+          if gSaveStats or gScreenshotStats 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');
@@ -1723,7 +1790,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>/<Ïðîáåë> èëè ïðîøëî äîñòàòî÷íî âðåìåíè:
@@ -2790,6 +2857,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 then
+  begin
+    g_TakeScreenShot('stats/' + StatFilename, True);
+    StatShotDone := True;
+  end;
 end;
 
 procedure DrawSingleStat();
@@ -7225,15 +7299,21 @@ begin
   end;
 end;
 
-procedure g_TakeScreenShot;
+procedure g_TakeScreenShot(Filename: string = ''; StatShot: Boolean = False);
   var s: TStream; t: TDateTime; dir, date, name: String;
 begin
   if e_NoGraphics then Exit;
   try
-    t := Now;
     dir := e_GetWriteableDir(ScreenshotDirs);
-    DateTimeToString(date, 'yyyy-mm-dd-hh-nn-ss', t);
-    name := e_CatPath(dir, 'screenshot-' + date + '.png');
+
+    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);
index c290df5e6b2176f743ae93748f919b73bd14add9..7a9785b6964cd331222219282a1f9b4310d4605f 100644 (file)
@@ -44,6 +44,7 @@ var
   CacheDirs: SSArray;
   ConfigDirs: SSArray;
   ScreenshotDirs: SSArray;
+  StatsDirs: SSArray;
   MapDownloadDirs: SSArray;
   WadDownloadDirs: SSArray;
 
@@ -345,6 +346,7 @@ begin
         AddDir(MapDownloadDirs, e_CatPath(rwdir, 'maps/downloads'));
         AddDir(WadDownloadDirs, e_CatPath(rwdir, 'wads/downloads'));
         AddDir(ScreenshotDirs, e_CatPath(rwdir, 'screenshots'));
+        AddDir(StatsDirs, e_CatPath(rwdir, 'stats'));
         (* RO *)
         AddDir(DataDirs, e_CatPath(rwdir, 'data'));
         AddDir(ModelDirs, e_CatPath(rwdir, 'data/models'));
@@ -392,6 +394,7 @@ begin
   AddDef(MapDownloadDirs, rwdirs, 'maps/downloads');
   AddDef(WadDownloadDirs, rwdirs, 'wads/downloads');
   AddDef(ScreenshotDirs, rwdirs, 'screenshots');
+  AddDef(StatsDirs, rwdirs, 'stats');
 
   for i := 0 to High(MapDirs) do
     AddDir(AllMapDirs, MapDirs[i]);
@@ -411,6 +414,10 @@ begin
       {$ENDIF}
     end
   end;
+  
+  // HACK: ensure the screenshots folder also has a stats subfolder in it
+  rwdir := e_GetWriteableDir(ScreenshotDirs, false);
+  if rwdir <> '' then CreateDir(rwdir + '/stats');
 end;
 
 procedure InitPrep;
@@ -453,6 +460,7 @@ begin
   PrintDirs('CacheDirs', CacheDirs);
   PrintDirs('ConfigDirs', ConfigDirs);
   PrintDirs('ScreenshotDirs', ScreenshotDirs);
+  PrintDirs('StatsDirs', StatsDirs);
   PrintDirs('MapDownloadDirs', MapDownloadDirs);
   PrintDirs('WadDownloadDirs', WadDownloadDirs);
 
index d523da8c511b83be4d6d6bc68af001e49598654d..d4f92e86617af3c35036b96df4f798c117de4c68 100644 (file)
@@ -60,6 +60,8 @@ var
   gRevertPlayers: Boolean;
   gLanguage: String;
   gAskLanguage: Boolean;
+  gSaveStats: Boolean = False;
+  gScreenshotStats: Boolean = False;
   gcMap: String;
   gcGameMode: String;
   gcTimeLimit: Word;
@@ -264,6 +266,7 @@ begin
   gDefaultMegawadStart := DF_Default_Megawad_Start;
   gBerserkAutoswitch := True;
   g_dbg_scale := 1.0;
+  gSaveStats := False;
 
   gAskLanguage := True;
   gLanguage := LANGUAGE_ENGLISH;
@@ -804,5 +807,6 @@ initialization
   conRegVar('sfs_fastmode', @wadoptFast, '', '');
   conRegVar('g_fast_screenshots', @e_FastScreenshots, '', '');
   conRegVar('g_default_megawad', @gDefaultMegawadStart, '', '');
-
+  conRegVar('g_save_stats', @gSaveStats, '', '');
+  conRegVar('g_screenshot_stats', @gScreenshotStats, '', '');
 end.
index 948ac697f8c948b37be295ec3db9aa1a432c2a16..f5f762c6e983c96a5dd7c54ad3fb68c82110d058 100644 (file)
@@ -74,6 +74,10 @@ function forceFilenameExt (const fn, ext: AnsiString): AnsiString;
 // rewrites slashes to '/'
 function fixSlashes (s: AnsiString): AnsiString;
 
+// replaces all the shitty characters with '_'
+// (everything except alphanumerics, '_', '.')
+function sanitizeFilename (s: AnsiString): AnsiString;
+
 function isAbsolutePath (const s: AnsiString): Boolean;
 function isRootPath (const s: AnsiString): Boolean;
 
@@ -353,6 +357,20 @@ begin
   {$ENDIF}
 end;
 
+// replaces all the shitty characters with '_'
+// (everything except alphanumerics, '_', '.')
+function sanitizeFilename (s: AnsiString): AnsiString;
+var
+  i: Integer;
+const
+  leaveChars: set of Char = [ '0'..'9', 'A'..'Z', 'a'..'z', '_', '.', #192..#255 ];
+  replaceWith: Char = '_';
+begin
+  result := s;
+  for i := 1 to length(result) do
+    if not (result[i] in leaveChars) then
+      result[i] := replaceWith;
+end;
 
 function isAbsolutePath (const s: AnsiString): Boolean;
 begin