From 74ed8fd1d9bcc60f885241c643ae602550f7f064 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Tue, 12 Nov 2019 22:00:33 +0100 Subject: [PATCH] - added GZDoom's statistics code Not hooked up yet. --- source/CMakeLists.txt | 2 + source/build/include/baselayer.h | 8 + source/common/statistics.cpp | 545 ++++++++++++++++++++++++++++++ source/common/utility/strnatcmp.c | 176 ++++++++++ source/common/utility/strnatcmp.h | 39 +++ source/duke3d/src/duke3d.h | 1 + source/duke3d/src/screens.cpp | 9 + source/rr/src/duke3d.h | 1 + source/rr/src/screens.cpp | 7 + 9 files changed, 788 insertions(+) create mode 100644 source/common/statistics.cpp create mode 100644 source/common/utility/strnatcmp.c create mode 100644 source/common/utility/strnatcmp.h diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 5287abcf0..2cad63503 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -652,6 +652,7 @@ set( FASTMATH_SOURCES # The rest is only here because it is C, not C++ glad/src/glad.c + common/utility/strnatcmp.c # Another bit of cruft just to make S(hit)DL happy... sdlappicon.cpp @@ -743,6 +744,7 @@ set (PCH_SOURCES common/initfs.cpp common/openaudio.cpp common/optionmenu/optionmenu.cpp + common/statistics.cpp common/2d/v_2ddrawer.cpp common/2d/v_draw.cpp diff --git a/source/build/include/baselayer.h b/source/build/include/baselayer.h index 41e59a3f8..2e0cb6ce2 100644 --- a/source/build/include/baselayer.h +++ b/source/build/include/baselayer.h @@ -161,6 +161,13 @@ void wm_setapptitle(const char *name); #include "print.h" +struct GameStats +{ + int kill, tkill; + int secret, tsecret; + int timesecnd; +}; + struct GameInterface { virtual ~GameInterface() {} @@ -171,6 +178,7 @@ struct GameInterface virtual void set_hud_scale(int size) = 0; virtual bool mouseInactiveConditional(bool condition) { return condition; } virtual FString statFPS() { return "FPS display not available"; } + virtual GameStats getStats() { return {}; } }; extern GameInterface* gi; diff --git a/source/common/statistics.cpp b/source/common/statistics.cpp new file mode 100644 index 000000000..321b20fa1 --- /dev/null +++ b/source/common/statistics.cpp @@ -0,0 +1,545 @@ +/* +** +** statistics.cpp +** Save game statistics to a file +** +**--------------------------------------------------------------------------- +** Copyright 2010 Christoph Oelckers +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#include +#include + +#include "strnatcmp.h" +#include "c_dispatch.h" +#include "m_png.h" +#include "filesystem.h" +#include "cmdlib.h" +#include "stats.h" +#include "c_cvars.h" +#include "sc_man.h" +#include "baselayer.h" + +CVAR(Int, savestatistics, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) +CVAR(String, statfile, "demolitionstat.txt", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) + +//========================================================================== +// +// Global statistics data +// +//========================================================================== + +// This struct is used to track statistics data in game +struct OneLevel +{ + int totalkills = 0, killcount = 0; + int totalsecrets = 0, secretcount = 0; + int leveltime = 0; + FString Levelname; +}; + +// Current game's statistics +static TArray LevelData; +static char StartEpisode[MAX_PATH]; +static int StartSkill; +static char LevelName[MAX_PATH]; + +// The statistics for one level +struct FLevelStatistics +{ + char info[60]; + short skill; + short playerclass; + char name[24]; + int timeneeded; +}; + +// Statistics for one episode playthrough +struct FSessionStatistics : public FLevelStatistics +{ + TArray levelstats; +}; + +// Collected statistics for one episode +struct FStatistics +{ + TArray stats; + FString epi_name; + FString epi_header; +}; + +// All statistics ever collected +static TArray EpisodeStatistics; + +//========================================================================== +// +// Initializes statistics data from external file +// +//========================================================================== + +static void ParseStatistics(const char *fn, TArray &statlist) +{ + statlist.Clear(); + try + { + FScanner sc; + sc.OpenFile(fn); + + while (sc.GetString()) + { + FStatistics &ep_entry = statlist[statlist.Reserve(1)]; + + ep_entry.epi_header = sc.String; + sc.MustGetString(); + ep_entry.epi_name = sc.String; + + sc.MustGetStringName("{"); + while (!sc.CheckString("}")) + { + FSessionStatistics &session = ep_entry.stats[ep_entry.stats.Reserve(1)]; + + sc.MustGetString(); + sc.MustGetString(); + strncpy(session.name, sc.String, 24); + sc.MustGetString(); + strncpy(session.info, sc.String, 60); + + int h,m,s; + sc.MustGetString(); + sscanf(sc.String, "%d:%d:%d", &h, &m, &s); + session.timeneeded= ((((h*60)+m)*60)+s); + + sc.MustGetNumber(); + session.skill=sc.Number; + if (sc.CheckString("{")) + { + while (!sc.CheckString("}")) + { + FLevelStatistics &lstats = session.levelstats[session.levelstats.Reserve(1)]; + + sc.MustGetString(); + strncpy(lstats.name, sc.String, 24); + sc.MustGetString(); + strncpy(lstats.info, sc.String, 60); + + int h,m,s; + sc.MustGetString(); + sscanf(sc.String, "%d:%d:%d", &h, &m, &s); + lstats.timeneeded= ((((h*60)+m)*60)+s); + + lstats.skill = 0; + } + } + } + } + } + catch(std::runtime_error &) + { + } +} + + +// ==================================================================== +// +// Reads the statistics file +// +// ==================================================================== + +void ReadStatistics() +{ + ParseStatistics(statfile, EpisodeStatistics); +} + +// ==================================================================== +// +// Saves the statistics file +// Sorting helpers. +// +// ==================================================================== + +int compare_episode_names(const void *a, const void *b) +{ + FStatistics *A = (FStatistics*)a; + FStatistics *B = (FStatistics*)b; + + return strnatcasecmp(A->epi_header, B->epi_header); +} + +int compare_level_names(const void *a, const void *b) +{ + FLevelStatistics *A = (FLevelStatistics*)a; + FLevelStatistics *B = (FLevelStatistics*)b; + + return strnatcasecmp(A->name, B->name); +} + +int compare_dates(const void *a, const void *b) +{ + FLevelStatistics *A = (FLevelStatistics*)a; + FLevelStatistics *B = (FLevelStatistics*)b; + char *p; + + int aday = strtol(A->name, &p, 10); + int amonth = strtol(p+1, &p, 10); + int ayear = strtol(p+1, &p, 10); + int av = aday + 100 * amonth + 2000*ayear; + + int bday = strtol(B->name, &p, 10); + int bmonth = strtol(p+1, &p, 10); + int byear = strtol(p+1, &p, 10); + int bv = bday + 100 * bmonth + 2000*byear; + + return av-bv; +} + + +// ==================================================================== +// +// Main save routine +// +// ==================================================================== + +inline int hours(int v) { return v / (60*60); } +inline int minutes(int v) { return (v % (60*60)) / (60); } +inline int seconds(int v) { return (v % (60)); } + +static void SaveStatistics(const char *fn, TArray &statlist) +{ + unsigned int j; + + FileWriter *fw = FileWriter::Open(fn); + if (fw == nullptr) return; + + qsort(&statlist[0], statlist.Size(), sizeof(statlist[0]), compare_episode_names); + for(unsigned i=0;iPrintf("%s \"%s\"\n{\n", ep_stats.epi_header.GetChars(), ep_stats.epi_name.GetChars()); + for(j=0;jinfo[0]>0) + { + fw->Printf("\t%2i. %10s \"%-33s\" %02d:%02d:%02d %i\n", j+1, sst->name, sst->info, + hours(sst->timeneeded), minutes(sst->timeneeded), seconds(sst->timeneeded), sst->skill); + + TArray &ls = sst->levelstats; + if (ls.Size() > 0) + { + fw->Printf("\t{\n"); + + qsort(&ls[0], ls.Size(), sizeof(ls[0]), compare_level_names); + + for(unsigned k=0;kPrintf("\t\t%-8s \"%-33s\" %02d:%02d:%02d\n", ls[k].name, ls[k].info, + hours(ls[k].timeneeded), minutes(ls[k].timeneeded), seconds(ls[k].timeneeded)); + } + fw->Printf("\t}\n"); + } + } + } + fw->Printf("}\n\n"); + } + delete fw; +} + + +// ==================================================================== +// +// Gets list for current episode +// +// ==================================================================== +static FStatistics *GetStatisticsList(TArray &statlist, const char *section, const char *fullname) +{ + for(unsigned int i=0;iepi_header = section; + stats->epi_name = fullname; + return stats; +} + +// ==================================================================== +// +// Adds a statistics entry +// +// ==================================================================== +static FSessionStatistics *StatisticsEntry(FStatistics *stats, const char *text, int playtime) +{ + FSessionStatistics s; + time_t clock; + struct tm *lt; + + time (&clock); + lt = localtime (&clock); + + if (lt != NULL) + snprintf(s.name, countof(s.name), "%02d.%02d.%04d",lt->tm_mday, lt->tm_mon+1, lt->tm_year+1900); + else + strcpy(s.name,"00.00.0000"); + + s.skill=StartSkill; + strcpy(s.info, text); + s.timeneeded=playtime; + + stats->stats.Push(s); + return &stats->stats[stats->stats.Size()-1]; +} + +// ==================================================================== +// +// Adds a statistics entry +// +// ==================================================================== +static void LevelStatEntry(FSessionStatistics *es, const char *level, const char *text, int playtime) +{ + FLevelStatistics s; + time_t clock; + struct tm *lt; + + time (&clock); + lt = localtime (&clock); + + strcpy(s.name, level); + strcpy(s.info, text); + s.timeneeded=playtime; + es->levelstats.Push(s); +} + + + +//========================================================================== +// +// STAT_StartNewGame: called when a new game starts. Sets the current episode +// +//========================================================================== + +void STAT_StartNewGame(const char *episode, int skill) +{ + strncpy(StartEpisode, episode, MAX_PATH); + StartSkill = skill; + LevelData.Clear(); +} + +void STAT_NewLevel(const char* mapname) +{ + strncpy(LevelName, mapname, MAX_PATH); +} + +//========================================================================== +// +// Store the current level's statistics +// +//========================================================================== + +static void StoreLevelStats() +{ + unsigned int i; + + for(i=0;igetStats(); + LevelData[i].totalkills = stat.tkill; + LevelData[i].killcount = stat.kill; + LevelData[i].totalsecrets = stat.tsecret; + LevelData[i].secretcount = stat.secret; + LevelData[i].leveltime = stat.timesecnd; +} + +//========================================================================== +// +// STAT_ChangeLevel: called when the level changes or the current statistics are +// requested +// +//========================================================================== + +void STAT_Update(bool endofgame) +{ + const char* fn = "?"; + // record the current level's stats. + StoreLevelStats(); + + if (savestatistics == 1 && endofgame) + { + if (LevelData.Size() == 0) + { + auto lump = fileSystem.FindFile(LevelName); + if (lump >= 0) + { + int file = fileSystem.GetFileContainer(lump); + fn = fileSystem.GetResourceFileName(file); + } + } + FString section = ExtractFileBase(fn) + "." + LevelData[0].Levelname; + section.ToUpper(); + FStatistics* sl = GetStatisticsList(EpisodeStatistics, section, StartEpisode); + + int statvals[] = { 0,0,0,0, 0 }; + FString infostring; + int validlevels = LevelData.Size(); + for (unsigned i = 0; i < LevelData.Size(); i++) + { + statvals[0] += LevelData[i].killcount; + statvals[1] += LevelData[i].totalkills; + statvals[2] += LevelData[i].secretcount; + statvals[3] += LevelData[i].totalsecrets; + statvals[4] += LevelData[i].leveltime; + } + + infostring.Format("%4d/%4d, %3d/%3d, %2d", statvals[0], statvals[1], statvals[2], statvals[3], validlevels); + FSessionStatistics* es = StatisticsEntry(sl, infostring, statvals[4]); + + for (unsigned i = 0; i < LevelData.Size(); i++) + { + FString lsection = LevelData[i].Levelname; + lsection.ToUpper(); + infostring.Format("%4d/%4d, %3d/%3d", LevelData[i].killcount, LevelData[i].totalkills, LevelData[i].secretcount, LevelData[i].totalsecrets); + LevelStatEntry(es, lsection, infostring, LevelData[i].leveltime); + } + SaveStatistics(statfile, EpisodeStatistics); + } +} + + +//========================================================================== +// +// saves statistics info to savegames +// +//========================================================================== + +void SaveOneLevel(FileWriter& fil, OneLevel& l) +{ + fil.Write(&l.totalkills, 4); + fil.Write(&l.killcount, 4); + fil.Write(&l.totalsecrets, 4); + fil.Write(&l.secretcount, 4); + fil.Write(&l.leveltime, 4); + uint8_t siz = l.Levelname.Len(); + fil.Write(&siz, 1); + fil.Write(l.Levelname.GetChars(), siz); +} + +void ReadOneLevel(FileReader& fil, OneLevel& l) +{ + fil.Read(&l.totalkills, 4); + fil.Read(&l.killcount, 4); + fil.Read(&l.totalsecrets, 4); + fil.Read(&l.secretcount, 4); + fil.Read(&l.leveltime, 4); + uint8_t siz; + fil.Read(&siz, 1); + auto p = l.Levelname.LockNewBuffer(siz); + fil.Read(p, siz); + l.Levelname.UnlockBuffer(); +} + +void SaveStatistics(FileWriter& fil) +{ + fil.Write("STAT", 4); + fil.Write(LevelName, MAX_PATH); + fil.Write(&StartEpisode, MAX_PATH); + fil.Write(&StartSkill, 4); + int p = LevelData.Size(); + fil.Write(&p, 4); + for (auto& lev : LevelData) + { + SaveOneLevel(fil, lev); + } + fil.Write("TATS", 4); +} + +bool ReadStatistics(FileReader& fil) +{ + char id[4]; + + fil.Read(id, 4); + if (memcmp(id, "STAT", 4)) return false; + fil.Read(LevelName, MAX_PATH); + fil.Read(&StartEpisode, MAX_PATH); + fil.Read(&StartSkill, 4); + int p; + fil.Read(&p, 4); + LevelData.Resize(p); + for (auto& lev : LevelData) + { + ReadOneLevel(fil, lev); + } + fil.Read(id, 4); + if (memcmp(id, "TATS", 4)) return false; + return true; +} + + +//========================================================================== +// +// show statistics +// +//========================================================================== + +FString GetStatString() +{ + FString compose; + for(unsigned i = 0; i < LevelData.Size(); i++) + { + OneLevel *l = &LevelData[i]; + compose.AppendFormat("Level %s - Kills: %d/%d - Secrets: %d/%d - Time: %d:%02d\n", + l->Levelname.GetChars(), l->killcount, l->totalkills, l->secretcount, l->totalsecrets, + l->leveltime/(60), (l->leveltime)%60); + } + return compose; +} + +CCMD(printstats) +{ + StoreLevelStats(); // Refresh the current level's results. + Printf("%s", GetStatString().GetChars()); +} + +ADD_STAT(statistics) +{ + StoreLevelStats(); // Refresh the current level's results. + return GetStatString(); +} + diff --git a/source/common/utility/strnatcmp.c b/source/common/utility/strnatcmp.c new file mode 100644 index 000000000..58fa5571a --- /dev/null +++ b/source/common/utility/strnatcmp.c @@ -0,0 +1,176 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + + +/* partial change history: + * + * 2004-10-10 mbp: Lift out character type dependencies into macros. + * + * Eric Sosman pointed out that ctype functions take a parameter whose + * value must be that of an unsigned int, even on platforms that have + * negative chars in their default char type. + */ + +#include +#include + +#include "strnatcmp.h" + + +/* These are defined as macros to make it easier to adapt this code to + * different characters types or comparison functions. */ +static __inline int +nat_isdigit(nat_char a) +{ + return isdigit((unsigned char) a); +} + + +static __inline int +nat_isspace(nat_char a) +{ + return isspace((unsigned char) a); +} + + +static __inline nat_char +nat_toupper(nat_char a) +{ + return toupper((unsigned char) a); +} + + + +static int +compare_right(nat_char const *a, nat_char const *b) +{ + int bias = 0; + + /* The longest run of digits wins. That aside, the greatest + value wins, but we can't know that it will until we've scanned + both numbers to know that they have the same magnitude, so we + remember it in BIAS. */ + for (;; a++, b++) { + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return bias; + else if (!nat_isdigit(*a)) + return -1; + else if (!nat_isdigit(*b)) + return +1; + else if (*a < *b) { + if (!bias) + bias = -1; + } else if (*a > *b) { + if (!bias) + bias = +1; + } else if (!*a && !*b) + return bias; + } + + return 0; +} + + +static int +compare_left(nat_char const *a, nat_char const *b) +{ + /* Compare two left-aligned numbers: the first to have a + different value wins. */ + for (;; a++, b++) { + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return 0; + else if (!nat_isdigit(*a)) + return -1; + else if (!nat_isdigit(*b)) + return +1; + else if (*a < *b) + return -1; + else if (*a > *b) + return +1; + } + + return 0; +} + + +static int strnatcmp0(nat_char const *a, nat_char const *b, int fold_case) +{ + int ai, bi; + nat_char ca, cb; + int fractional, result; + + assert(a && b); + ai = bi = 0; + while (1) { + ca = a[ai]; cb = b[bi]; + + /* skip over leading spaces or zeros */ + while (nat_isspace(ca)) + ca = a[++ai]; + + while (nat_isspace(cb)) + cb = b[++bi]; + + /* process run of digits */ + if (nat_isdigit(ca) && nat_isdigit(cb)) { + fractional = (ca == '0' || cb == '0'); + + if (fractional) { + if ((result = compare_left(a+ai, b+bi)) != 0) + return result; + } else { + if ((result = compare_right(a+ai, b+bi)) != 0) + return result; + } + } + + if (!ca && !cb) { + /* The strings compare the same. Perhaps the caller + will want to call strcmp to break the tie. */ + return 0; + } + + if (fold_case) { + ca = nat_toupper(ca); + cb = nat_toupper(cb); + } + + if (ca < cb) + return -1; + else if (ca > cb) + return +1; + + ++ai; ++bi; + } +} + + + +int strnatcmp(nat_char const *a, nat_char const *b) { + return strnatcmp0(a, b, 0); +} + + +/* Compare, recognizing numeric string and ignoring case. */ +int strnatcasecmp(nat_char const *a, nat_char const *b) { + return strnatcmp0(a, b, 1); +} diff --git a/source/common/utility/strnatcmp.h b/source/common/utility/strnatcmp.h new file mode 100644 index 000000000..7d7462349 --- /dev/null +++ b/source/common/utility/strnatcmp.h @@ -0,0 +1,39 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* CUSTOMIZATION SECTION + * + * You can change this typedef, but must then also change the inline + * functions in strnatcmp.c */ +typedef char nat_char; + +int strnatcmp(nat_char const *a, nat_char const *b); +int strnatcasecmp(nat_char const *a, nat_char const *b); + +#ifdef __cplusplus +} +#endif diff --git a/source/duke3d/src/duke3d.h b/source/duke3d/src/duke3d.h index d3be7d5e5..b8c1c5771 100644 --- a/source/duke3d/src/duke3d.h +++ b/source/duke3d/src/duke3d.h @@ -153,6 +153,7 @@ struct GameInterface : ::GameInterface void set_hud_scale(int size) override; bool mouseInactiveConditional(bool condition) override; FString statFPS() override; + GameStats getStats() override; }; END_DUKE_NS diff --git a/source/duke3d/src/screens.cpp b/source/duke3d/src/screens.cpp index c57072127..3b2dfe562 100644 --- a/source/duke3d/src/screens.cpp +++ b/source/duke3d/src/screens.cpp @@ -1180,6 +1180,15 @@ void G_DisplayRest(int32_t smoothratio) VM_OnEvent(EVENT_DISPLAYEND, g_player[screenpeek].ps->i, screenpeek); } +GameStats GameInterface::getStats() +{ + GameStats stats; + DukePlayer_t* p = g_player[myconnectindex].ps; + return { p->actors_killed, p->max_actors_killed, p->secret_rooms, p->max_secret_rooms, p->player_par / REALGAMETICSPERSEC }; +} + + + void G_FadePalette(int32_t r, int32_t g, int32_t b, int32_t e) { if (ud.screenfade == 0) diff --git a/source/rr/src/duke3d.h b/source/rr/src/duke3d.h index e9ab843cf..4b3e27367 100644 --- a/source/rr/src/duke3d.h +++ b/source/rr/src/duke3d.h @@ -157,6 +157,7 @@ struct GameInterface : ::GameInterface void set_hud_scale(int size) override; bool mouseInactiveConditional(bool condition) override; FString statFPS() override; + GameStats getStats() override; }; END_RR_NS diff --git a/source/rr/src/screens.cpp b/source/rr/src/screens.cpp index 91214bd32..87bbf83ba 100644 --- a/source/rr/src/screens.cpp +++ b/source/rr/src/screens.cpp @@ -751,6 +751,13 @@ FString GameInterface::statFPS() return output; } +GameStats GameInterface::getStats() +{ + GameStats stats; + DukePlayer_t* p = g_player[myconnectindex].ps; + return { p->actors_killed, p->max_actors_killed, p->secret_rooms, p->max_secret_rooms, p->player_par / REALGAMETICSPERSEC }; +} + #undef FPS_COLOR void G_DisplayRest(int32_t smoothratio)