diff --git a/src/Sourcefile b/src/Sourcefile index 17a687d5d..dbea241ca 100644 --- a/src/Sourcefile +++ b/src/Sourcefile @@ -129,3 +129,4 @@ k_brightmap.c k_director.c k_follower.c k_mapuser.c +k_stats.c diff --git a/src/d_netcmd.c b/src/d_netcmd.c index d7ddd9fe6..fa14bbf1e 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -5826,12 +5826,8 @@ void Command_Retry_f(void) */ static void Command_Isgamemodified_f(void) { - if (majormods) - CONS_Printf("The game has been modified with major addons, so you cannot play Record Attack.\n"); - else if (savemoddata) - CONS_Printf("The game has been modified with an addon with its own save data, so you can play Record Attack and earn medals.\n"); - else if (modifiedgame) - CONS_Printf("The game has been modified with only minor addons. You can play Record Attack, earn medals and unlock extras.\n"); + if (majormods || modifiedgame) + CONS_Printf("The game has been modified, Record Attack data will be saved to a seperate savegame.\n"); else CONS_Printf("The game has not been modified. You can play Record Attack, earn medals and unlock extras.\n"); } diff --git a/src/deh_soc.c b/src/deh_soc.c index 85b1afda1..599367a9d 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2482,7 +2482,7 @@ void readsound(MYFILE *f, INT32 num) * \sa readmaincfg() * \author Graue */ -static boolean GoodDataFileName(const char *s) +/*static boolean GoodDataFileName(const char *s) { const char *p; const char *tail = ".dat"; @@ -2497,7 +2497,7 @@ static boolean GoodDataFileName(const char *s) if (fasticmp(s, "gamedata.dat")) return false; return true; -} +}*/ void reademblemdata(MYFILE *f, INT32 num) { @@ -3242,7 +3242,8 @@ void readmaincfg(MYFILE *f) else if (fastcmp(word, "GAMEDATA")) { - size_t filenamelen; + // just ignore it but dont throw a warning + /*size_t filenamelen; // Check the data filename so that mods // can't write arbitrary files. @@ -3271,7 +3272,7 @@ void readmaincfg(MYFILE *f) refreshdirmenu |= REFRESHDIR_GAMEDATA; gamedataadded = true; - titlechanged = true; + titlechanged = true;*/ } else if (fastcmp(word, "RESETDATA")) { diff --git a/src/doomstat.h b/src/doomstat.h index e310eb549..1006d7ff6 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -557,9 +557,6 @@ struct tolinfo_t extern tolinfo_t TYPEOFLEVEL[NUMTOLNAMES]; extern UINT32 lastcustomtol; -extern tic_t totalplaytime; -extern UINT32 matchesplayed; - extern UINT8 stagefailed; // Emeralds stored as bits to throw savegame hackers off. diff --git a/src/g_game.c b/src/g_game.c index 740938394..318cb486c 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -53,6 +53,7 @@ // SRB2kart #include "k_kart.h" +#include "k_stats.h" // SRB2kart #include "k_battle.h" #include "k_pwrlv.h" #include "k_color.h" @@ -196,8 +197,6 @@ UINT32 tokenlist; // List of tokens collected boolean gottoken; // Did you get a token? Used for end of act INT32 tokenbits; // Used for setting token bits -tic_t totalplaytime; -UINT32 matchesplayed; // SRB2Kart boolean gamedataloaded = false; // Temporary holding place for nights data for the current map @@ -673,6 +672,36 @@ static void G_UpdateRecordReplays(void) G_SaveGameData(); } +// kinda hacky way to do this, but this sets the game to use a seperate savefile if you have addons loaded +static void G_SetSaveGameModified(void) +{ + size_t filenamelen; + + if (savemoddata) + return; + + // save vanilla data just to be sure + G_SaveGameData(); + + savemoddata = true; + + strlcpy(gamedatafilename, "modkartdata.dat", sizeof (gamedatafilename)); + strlwr(gamedatafilename); + + // Also save a time attack folder + filenamelen = strlen(gamedatafilename)-4; // Strip off the extension + filenamelen = min(filenamelen, sizeof (timeattackfolder)); + memcpy(timeattackfolder, gamedatafilename, filenamelen); + timeattackfolder[min(filenamelen, sizeof (timeattackfolder) - 1)] = '\0'; + + strcpy(savegamename, timeattackfolder); + strlcat(savegamename, "%u.ssg", sizeof(savegamename)); + // can't use sprintf since there is %u in savegamename + strcatbf(savegamename, srb2home, PATHSEP); + + G_LoadGameData(); +} + // for consistency among messages: this modifies the game and removes savemoddata. void G_SetGameModified(boolean silent, boolean major) { @@ -687,8 +716,11 @@ void G_SetGameModified(boolean silent, boolean major) //savemoddata = false; -- there is literally no reason to do this anymore. majormods = true; + // should this only be done when you load a "major" gameplay modifieng addon? + G_SetSaveGameModified(); + if (!silent) - CONS_Alert(CONS_NOTICE, M_GetText("Game must be restarted to play Record Attack.\n")); + CONS_Alert(CONS_NOTICE, M_GetText("Record Attack data will be saved to seperate save.\n")); // If in record attack recording, cancel it. if (modeattacking) @@ -2198,7 +2230,7 @@ static inline void G_PlayerFinishLevel(INT32 player) { if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) { - matchesplayed++; + kartstats.matchesplayed++; if (M_UpdateUnlockablesAndExtraEmblems()) S_StartSound(NULL, sfx_ncitem); G_SaveGameData(); @@ -2952,6 +2984,9 @@ void G_DoReborn(INT32 playernum) G_SpawnPlayer(playernum, starpost); if (oldmo) G_ChangePlayerReferences(oldmo, players[playernum].mo); + + if (!demo.playback && playernum == consoleplayer) + kartstats.respawns++; } ACS_RunPlayerEnterScript(player); @@ -3999,9 +4034,13 @@ static void G_DoCompleted(void) if (metalrecording) G_StopMetalRecording(false); + K_StatRound(); + G_SetGamestate(GS_NULL); wipegamestate = GS_NULL; + K_StatRound(); + for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i]) @@ -4300,9 +4339,7 @@ void G_LoadGameData(void) // to new gamedata G_ClearRecords(); // main and nights records M_ClearSecrets(); // emblems, unlocks, maps visited, etc - - totalplaytime = 0; // total play time (separate from all) - matchesplayed = 0; // SRB2Kart: matches played & finished + K_EraseStats(); // stats for (i = 0; i < PWRLV_NUMTYPES; i++) // SRB2Kart: online rank system vspowerlevel[i] = PWRLVRECORD_START; @@ -4334,9 +4371,9 @@ void G_LoadGameData(void) P_SaveBufferFree(&save); I_Error("Game data is not for SRB2Kart v2.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder); } - - totalplaytime = READUINT32(save.p); - matchesplayed = READUINT32(save.p); + + // well no clue but dont think it would like reading garbage from vanilla files + K_ReadStats(&save, !savemoddata); for (i = 0; i < PWRLV_NUMTYPES; i++) { @@ -4473,21 +4510,10 @@ void G_SaveGameData(void) return; } -#if 0 - // SRB2Kart: Let players unlock stuff with addons. - if (modifiedgame && !savemoddata) - { - free(save.buffer); - save.p = save.buffer = NULL; - return; - } -#endif - // Version test WRITEUINT32(save.p, GD_VERSIONCHECK); // 4 - WRITEUINT32(save.p, totalplaytime); // 4 - WRITEUINT32(save.p, matchesplayed); // 4 + K_WriteStats(&save, !savemoddata); for (i = 0; i < PWRLV_NUMTYPES; i++) WRITEUINT16(save.p, vspowerlevel[i]); diff --git a/src/k_collide.c b/src/k_collide.c index fbdf2ed69..dff678846 100644 --- a/src/k_collide.c +++ b/src/k_collide.c @@ -2,6 +2,7 @@ /// \brief SRB2Kart item collision hooks #include "k_collide.h" +#include "k_stats.h" #include "doomstat.h" #include "doomtype.h" #include "p_mobj.h" @@ -800,6 +801,8 @@ boolean K_KitchenSinkCollide(mobj_t *t1, mobj_t *t2) if (t2->player->flashing > 0) return true; + K_StatPlayerSink(t1->player, P_MobjWasRemoved(t2->target) ? NULL : t2->target->player); + S_StartSound(NULL, sfx_cgot); //let all players hear it. HU_SetCEchoFlags(0); diff --git a/src/k_kart.c b/src/k_kart.c index e5cd00e1f..99b379b35 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -38,6 +38,8 @@ #include "m_cheat.h" // objectplacing #include "p_spec.h" + +#include "k_stats.h" #include "k_waypoint.h" #include "k_bot.h" #include "k_hud.h" @@ -3620,6 +3622,7 @@ void K_SpinPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 typ S_StartSound(player->mo, sfx_slip); } + K_StatPlayerHit(player, source ? source->player : NULL); player->spinouttimer = (3*TICRATE/2)+2; P_SetPlayerMobjState(player->mo, S_KART_SPINOUT); } diff --git a/src/k_stats.c b/src/k_stats.c new file mode 100644 index 000000000..dfdd142f1 --- /dev/null +++ b/src/k_stats.c @@ -0,0 +1,214 @@ +#include "k_stats.h" +#include "doomstat.h" +#include "g_game.h" +#include "byteptr.h" +#include "z_zone.h" + +kartstats_t kartstats = {0}; + +void K_StatTicker(void) +{ + if (demo.playback) + return; + + kartstats.totalplaytime++; + + if (netgame) + kartstats.onlineplaytime++; + else if (modeattacking) + kartstats.raplaytime++; + + if (gametype == GT_RACE) + kartstats.raceplaytime++; + else if (gametype == GT_BATTLE) + kartstats.battleplaytime++; + + // Should this also track splitscreen players? + if (!players[consoleplayer].spectator) + { + player_t *p = &players[consoleplayer]; + + if (p->kartstuff[k_position] == spbplace) + kartstats.spbtargettime++; + + if (max(p->kartstuff[k_spinouttimer], p->kartstuff[k_wipeoutslow]) > 0) + kartstats.spinouttime++; + } +} + +void K_StatPlayerHit(player_t *victim, player_t *source) +{ + if (demo.playback) + return; + + if (victim == &players[consoleplayer]) + { + if (victim == source) + kartstats.selfhits++; + } + else if (source == &players[consoleplayer]) + kartstats.hits++; +} + +void K_StatPlayerSink(player_t *victim, player_t *source) +{ + if (demo.playback) + return; + + if (victim == &players[consoleplayer]) + kartstats.sinked++; + else if (source == &players[consoleplayer]) + kartstats.sinks++; +} + +void K_StatRound(void) +{ + if (demo.playback) + return; + + int numplayers = 0; + + for (int i = 0; i < MAXPLAYERS; ++i) + { + if (playeringame[i] && !players[i].spectator) + ++numplayers; + } + + if (numplayers > 1 && !players[consoleplayer].spectator) + { + if (players[consoleplayer].kartstuff[k_position] == 1) + kartstats.totalwins++; + else if (players[consoleplayer].kartstuff[k_position] <= 3) // Should this check if there are more than 3 players in game? + kartstats.totalpodium++; + } +} + +void K_EraseStats(void) +{ + // The only field we want to remember + boolean vanilla = kartstats.vanilla; + + if (kartstats.copy) + Z_Free(kartstats.copy); + + memset(&kartstats, 0, sizeof(kartstats_t)); + + kartstats.vanilla = vanilla; +} + +static void K_ReadStatsCustom(savebuffer_t *save) +{ + kartstats.size = READUINT32(save->p); + kartstats.version = READUINT32(save->p); + + // So many similar-looking read's... scary... + kartstats.raplaytime = READUINT32(save->p); + kartstats.onlineplaytime = READUINT32(save->p); + kartstats.raceplaytime = READUINT32(save->p); + kartstats.battleplaytime = READUINT32(save->p); + kartstats.spbtargettime = READUINT32(save->p); + kartstats.spinouttime = READUINT32(save->p); + kartstats.totalwins = READUINT32(save->p); + kartstats.totalpodium = READUINT32(save->p); + kartstats.hits = READUINT32(save->p); + kartstats.selfhits = READUINT32(save->p); + kartstats.sinks = READUINT32(save->p); + kartstats.sinked = READUINT32(save->p); + kartstats.respawns = READUINT32(save->p); + + // If kartstats gets updated, uncomment this and read next fields after this early return. Do same on next updates, + // this way even data is read on different versions, it doesn't get corrupted (as long as fields aren't removed, which shouldn't happen) + //if (kartstats.version < 2) + // return; + // + //kartstats.somenewfield = READUINT32(save->p); +} + +void K_ReadStats(savebuffer_t *save, boolean vanilla) +{ + // Basically, free old kartstats.copy if needed and memset kartstats with zeros + K_EraseStats(); + + kartstats.vanilla = vanilla; + + kartstats.totalplaytime = READUINT32(save->p); + kartstats.matchesplayed = READUINT32(save->p); + + // Vanilla only has those 2 + if (vanilla) + return; + + // Save where block of custom stats starts + UINT8 *customblockstart = save->p; + + K_ReadStatsCustom(save); + + // Make a copy of entire custom stats block, so unread values will still be written back + kartstats.copy = Z_Malloc(kartstats.size, PU_STATIC, NULL); + memcpy(kartstats.copy, customblockstart, kartstats.size); + + // If there were extra fields we couldn't read, skip them + save->p = customblockstart + kartstats.size; +} + +static void K_WriteStatsCustom(savebuffer_t *save) +{ + // Update, if needed + kartstats.version = max(kartstats.version, KARTSTATSVERSION); + + WRITEUINT32(save->p, kartstats.size); // Note: if we will end up writing more that stored in there, this field in save file would be updated + WRITEUINT32(save->p, kartstats.version); + + // Version 1 + WRITEUINT32(save->p, kartstats.raplaytime); + WRITEUINT32(save->p, kartstats.onlineplaytime); + WRITEUINT32(save->p, kartstats.raceplaytime); + WRITEUINT32(save->p, kartstats.battleplaytime); + WRITEUINT32(save->p, kartstats.spbtargettime); + WRITEUINT32(save->p, kartstats.spinouttime); + WRITEUINT32(save->p, kartstats.totalwins); + WRITEUINT32(save->p, kartstats.totalpodium); + WRITEUINT32(save->p, kartstats.hits); + WRITEUINT32(save->p, kartstats.selfhits); + WRITEUINT32(save->p, kartstats.sinks); + WRITEUINT32(save->p, kartstats.sinked); + WRITEUINT32(save->p, kartstats.respawns); + + // No need for early returns, but please mark each new block of write's with version it corresponds to :3 + + // Version 2 + //WRITEUINT32(save->p, kartstats.somenewfield); +} + +void K_WriteStats(savebuffer_t *save, boolean vanilla) +{ + WRITEUINT32(save->p, kartstats.totalplaytime); + WRITEUINT32(save->p, kartstats.matchesplayed); + + // Vanilla only has those 2 + if (vanilla) + return; + + // Now need to be careful... We save start of custom stats block + UINT8 *customblockstart = save->p; + + // Write everything we know how to write + K_WriteStatsCustom(save); + + // Calculate size of data that we wrote + UINT32 size = (UINT32)(save->p - customblockstart); + + // Now, if we wrote less data than save originally had, that means our copy that we kept in K_ReadStats + // has stats from newer version that we need to append + if (size < kartstats.size) + { + I_Assert(kartstats.copy != NULL); // If kartstats.copy is null, kartstats.size **always** should be 0, this can only happen if we create new save + WRITEMEM(save->p, kartstats.copy + size, kartstats.size - size); + } + else if (size > kartstats.size) // No need to do anything if we wrote exactly same amount... + { + // ...but, if we wrote more, we need to update size field in header. **It's always located at start of block** + // So we can just: + WRITEUINT32(customblockstart, size); + } +} diff --git a/src/k_stats.h b/src/k_stats.h new file mode 100644 index 000000000..bd145de56 --- /dev/null +++ b/src/k_stats.h @@ -0,0 +1,66 @@ +#ifndef __K_STATS__ +#define __K_STATS__ + +#include "doomtype.h" +#include "d_player.h" +#include "p_saveg.h" + +#define KARTSTATSVERSION 1 + +typedef struct kartstats_s { + // Remember if stats we loaded are vanilla ones or not + boolean vanilla; + + // Vanilla + tic_t totalplaytime; + UINT32 matchesplayed; + + // Custom data, fields go in same order as they are written into game save + + // IMPORTANT: size **must be first**, so K_WriteStats can overwrite it (not that we're gonna change order, but just in case) + UINT32 size; // Size required for our stats. If older version reads save from newer one, it will just skip unread fields, avoiding reading corrupted values + UINT32 version; // Version, so we know which fields we actually can read + + tic_t raplaytime; + tic_t onlineplaytime; + tic_t raceplaytime; + tic_t battleplaytime; + tic_t spbtargettime; + tic_t spinouttime; + UINT32 totalwins; // 1st place + UINT32 totalpodium; // 2nd and 3rd place, *but not 1st* + UINT32 hits; + UINT32 selfhits; + UINT32 sinks; // Times hitting *others* by kitchen sink + UINT32 sinked; // Times *being hit* by kitchen sink + UINT32 respawns; + + UINT8 *copy; // Copy of stats block that has been read from file. In case we're older version that reads newer save, we will keep values that we couldn't read +} kartstats_t; + +extern kartstats_t kartstats; + +// Note: All stat-tracking functions check for demo.playback and early return if its true + +// Update global stats such as total play time, etc +void K_StatTicker(void); + +// Update hit-related stats (PlayerSpin, PlayerSquish, PlayerExplode) +void K_StatPlayerHit(player_t *victim, player_t *source); + +// Separate from stuff above +void K_StatPlayerSink(player_t *victim, player_t *source); + +// Update round-related stats (such as matchesplayed, totalwins, etc) +void K_StatRound(void); + +void K_EraseStats(void); + +// If vanilla is true, only read vanilla-supported fields. Everything else is initialized to 0 +void K_ReadStats(savebuffer_t *save, boolean vanilla); + +// Same as above, except it just doesn't write non-vanilla fields if vanilla is true +void K_WriteStats(savebuffer_t *save, boolean vanilla); + + +#endif diff --git a/src/m_cond.c b/src/m_cond.c index 64b580191..205087653 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -21,6 +21,7 @@ #include "r_skins.h" // numskins #include "r_draw.h" // R_GetColorByName #include "k_pwrlv.h" +#include "k_stats.h" // Map triggers for linedef executors // 32 triggers, one bit each @@ -174,9 +175,9 @@ UINT8 M_CheckCondition(condition_t *cn) switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x - return (totalplaytime >= (unsigned)cn->requirement); + return (kartstats.totalplaytime >= (unsigned)cn->requirement); case UC_MATCHESPLAYED: // Requires any level completed >= x times - return (matchesplayed >= (unsigned)cn->requirement); + return (kartstats.matchesplayed >= (unsigned)cn->requirement); case UC_POWERLEVEL: // Requires power level >= x on a certain gametype return (vspowerlevel[cn->extrainfo1] >= (unsigned)cn->requirement); case UC_GAMECLEAR: // Requires game beaten >= x times diff --git a/src/m_menu.c b/src/m_menu.c index 7597dee78..3e12b90ed 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -64,6 +64,7 @@ #include "k_hud.h" // SRB2kart #include "k_kart.h" // KartItemCVars #include "k_pwrlv.h" +#include "k_stats.h" // SRB2kart #include "d_player.h" // KITEM_ constants #include "k_color.h" #include "k_grandprix.h" @@ -2951,17 +2952,6 @@ boolean M_Responder(event_t *ev) if (routine) { - if (((currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_CALL - || (currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_SUBMENU) - && (currentMenu->menuitems[itemOn].status & IT_CALLTYPE)) - { - if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && majormods) - { - S_StartSound(NULL, sfx_menu1); - M_StartMessage(M_GetText("This cannot be done with complex addons\nor in a cheated game.\n\n(Press a key)\n"), NULL, MM_NOTHING); - return true; - } - } S_StartSound(NULL, sfx_menu1); switch (currentMenu->menuitems[itemOn].status & IT_TYPE) { @@ -5015,7 +5005,7 @@ static boolean M_AddonsRefresh(void) else if (majormods && !prevmajormods) { S_StartSound(NULL, sfx_s221); - message = va("%c%s\x80\nYou've loaded a gameplay-modifying addon.\n\nRecord Attack has been disabled, but you\ncan still play alone in local Multiplayer.\n\nIf you wish to play Record Attack mode, restart the game to disable loaded addons.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname); + message = va("%c%s\x80\nYou've loaded a gameplay-modifying addon.\n\nRecord Attack data will be saved to seperate save.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname); prevmajormods = majormods; } @@ -7534,10 +7524,29 @@ static void M_ChoosePlayer(INT32 choice) // STATISTICS MENU // =============== +static void M_DrawStatsMaps(void); +static void M_DrawStatsPlaytime(void); +static void M_DrawStatsExtra(void); // dunno how to name this one + static INT32 statsLocation; static INT32 statsMax; static INT16 *statsMapList = NULL; static INT16 statsMapListLen; +static UINT8 statsCurrentPage = 0; + +typedef struct statpage_s { + const char *title; + void (*drawer)(void); +} statpage_t; + +static statpage_t statsPages[] = { + { "Play Time Statistics", M_DrawStatsPlaytime, }, + { "Level Statistics", M_DrawStatsMaps, }, + { "Extra Statistics", M_DrawStatsExtra, }, +}; + +#define LENSTATSPAGES (sizeof(statsPages)/sizeof(statsPages[0])) +#define NUMSTATSPAGES (kartstats.vanilla ? 2 : LENSTATSPAGES) static void M_Statistics(INT32 choice) { @@ -7567,6 +7576,7 @@ static void M_Statistics(INT32 choice) statsMapList[j] = -1; statsMax = j - 11 + numextraemblems; statsLocation = 0; + statsCurrentPage = 0; if (statsMax < 0) statsMax = 0; @@ -7574,13 +7584,45 @@ static void M_Statistics(INT32 choice) M_SetupNextMenu(&SP_LevelStatsDef); } -static void M_DrawStatsMaps(int location) +static void M_DrawStatsMaps(void) { - INT32 y = 88, i = -1; + char beststr[40]; + tic_t besttime = 0; + INT32 mapsunfinished = 0; + + int location = statsLocation; + INT32 y = 62, i = -1, j; INT16 mnum; extraemblem_t *exemblem; boolean dotopname = true, dobottomarrow = (location < statsMax); + for (j = 0; j < nummapheaders; j++) + { + if (!mapheaderinfo[j] || (mapheaderinfo[j]->menuflags & LF2_NOTIMEATTACK)) + continue; + + if (!mapheaderinfo[j]->mainrecord || mapheaderinfo[j]->mainrecord->time <= 0) + { + mapsunfinished++; + continue; + } + + besttime += mapheaderinfo[j]->mainrecord->time; + } + + V_DrawString(20, 42, highlightflags, "Combined time records:"); + + sprintf(beststr, "%i:%02i:%02i.%02i", G_TicsToHours(besttime), G_TicsToMinutes(besttime, false), G_TicsToSeconds(besttime), G_TicsToCentiseconds(besttime)); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 42, (mapsunfinished ? warningflags : 0), beststr); + + if (mapsunfinished) + V_DrawRightAlignedString(BASEVIDWIDTH-16, 50, warningflags, va("(%d unfinished)", mapsunfinished)); + else + V_DrawRightAlignedString(BASEVIDWIDTH-16, 50, recommendedflags, "(complete)"); + + V_DrawString(32, 50, 0, va("x %d/%d", M_CountEmblems(), numemblems+numextraemblems)); + V_DrawSmallScaledPatch(20, 50, 0, W_CachePatchName("GOTITA", PU_STATIC)); + if (location) V_DrawCharacter(10, y-(skullAnimCounter/5), '\x1A' | highlightflags, false); // up arrow @@ -7669,58 +7711,78 @@ bottomarrow: '\x1B' | highlightflags, false); // down arrow } -static void M_DrawLevelStats(void) -{ - char beststr[40]; - - tic_t besttime = 0; - - INT32 i; - INT32 mapsunfinished = 0; - - M_DrawMenuTitle(); - - V_DrawString(20, 24, highlightflags, "Total Play Time:"); - V_DrawCenteredString(BASEVIDWIDTH/2, 32, 0, va("%i hours, %i minutes, %i seconds", - G_TicsToHours(totalplaytime), - G_TicsToMinutes(totalplaytime, false), - G_TicsToSeconds(totalplaytime))); - - V_DrawString(20, 42, highlightflags, "Total Matches:"); - V_DrawRightAlignedString(BASEVIDWIDTH-16, 42, 0, va("%i played", matchesplayed)); - - V_DrawString(20, 52, highlightflags, "Online Power Level:"); - V_DrawRightAlignedString(BASEVIDWIDTH-16, 52, 0, va("Race: %i", vspowerlevel[PWRLV_RACE])); - V_DrawRightAlignedString(BASEVIDWIDTH-16, 60, 0, va("Battle: %i", vspowerlevel[PWRLV_BATTLE])); - - for (i = 0; i < nummapheaders; i++) - { - if (!mapheaderinfo[i] || (mapheaderinfo[i]->menuflags & LF2_NOTIMEATTACK)) - continue; - - if (!mapheaderinfo[i]->mainrecord || mapheaderinfo[i]->mainrecord->time <= 0) - { - mapsunfinished++; - continue; - } - - besttime += mapheaderinfo[i]->mainrecord->time; +#define DRAWTIMESTAT(y, title, field) { \ + char timebuf[80]; \ + V_DrawString(20, (y), highlightflags, title); \ + tic_t timeval = kartstats.field; \ + snprintf(timebuf, 80, "%02i:%02i:%02i", G_TicsToHours(timeval), G_TicsToMinutes(timeval, false), G_TicsToSeconds(timeval)); \ + V_DrawRightAlignedString(BASEVIDWIDTH-16, (y), 0, timebuf); \ } - V_DrawString(20, 70, highlightflags, "Combined time records:"); +#define DRAWAMOUNTSTAT(y, title, field) { \ + V_DrawString(20, (y), highlightflags, title); \ + unsigned amountval = kartstats.field; \ + V_DrawRightAlignedString(BASEVIDWIDTH-16, (y), 0, va("%u", amountval)); \ + } - sprintf(beststr, "%i:%02i:%02i.%02i", G_TicsToHours(besttime), G_TicsToMinutes(besttime, false), G_TicsToSeconds(besttime), G_TicsToCentiseconds(besttime)); - V_DrawRightAlignedString(BASEVIDWIDTH-16, 70, (mapsunfinished ? warningflags : 0), beststr); +static void M_DrawStatsPlaytime(void) +{ + V_DrawString(20, 42, highlightflags, "Total Play Time:"); + V_DrawCenteredString(BASEVIDWIDTH/2, 52, 0, va("%i hours, %i minutes, %i seconds", + G_TicsToHours(kartstats.totalplaytime), + G_TicsToMinutes(kartstats.totalplaytime, false), + G_TicsToSeconds(kartstats.totalplaytime))); + V_DrawString(20, 62, highlightflags, "Total Matches:"); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 62, 0, va("%i played", kartstats.matchesplayed)); - if (mapsunfinished) - V_DrawRightAlignedString(BASEVIDWIDTH-16, 78, warningflags, va("(%d unfinished)", mapsunfinished)); - else - V_DrawRightAlignedString(BASEVIDWIDTH-16, 78, recommendedflags, "(complete)"); + V_DrawString(20, 82, highlightflags, "Online Power Level:"); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 92, 0, va("Race: %i", vspowerlevel[PWRLV_RACE])); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 102, 0, va("Battle: %i", vspowerlevel[PWRLV_BATTLE])); - V_DrawString(32, 78, V_ALLOWLOWERCASE, va("x %d/%d", M_CountEmblems(), numemblems+numextraemblems)); - V_DrawSmallScaledPatch(20, 78, 0, W_CachePatchName("GOTITA", PU_STATIC)); + // Nothing else to draw + if (kartstats.vanilla) + return; - M_DrawStatsMaps(statsLocation); + DRAWTIMESTAT(122, "RA Play Time:", raplaytime); + DRAWTIMESTAT(132, "Online Play Time:", onlineplaytime); + DRAWTIMESTAT(142, "Race Play Time:", raceplaytime); + DRAWTIMESTAT(152, "Battle Play Time:", battleplaytime); +} + +// Note: only available with non-vanilla stats loaded, so it doesn't check for that +static void M_DrawStatsExtra(void) +{ + DRAWTIMESTAT(42, "Time being SPB target:", spbtargettime); + DRAWTIMESTAT(52, "Time spent in spinout:", spinouttime); + + DRAWAMOUNTSTAT(72, "Total wins:", totalwins); + DRAWAMOUNTSTAT(82, "Total podium (2nd/3rd place):", totalpodium); + + DRAWAMOUNTSTAT(102, "Hits landed:", hits); + DRAWAMOUNTSTAT(112, "Self-hits landed:", selfhits); + + DRAWAMOUNTSTAT(132, "Sinks landed:", sinks); + DRAWAMOUNTSTAT(142, "Times hit by sink:", sinked); + + DRAWAMOUNTSTAT(162, "Total respawns:", respawns); +} + +#undef DRAWAMOUNTSTAT +#undef DRAWTIMESTAT + +static void M_DrawLevelStats(void) +{ + M_DrawMenuTitle(); + + V_DrawCenteredString(BASEVIDWIDTH/2, 28, highlightflags, statsPages[statsCurrentPage].title); + + INT32 w = V_StringWidth(statsPages[statsCurrentPage].title, highlightflags); + V_DrawCharacter(BASEVIDWIDTH/2 - w/2 - 10 - (skullAnimCounter/5), 28, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH/2 + w/2 + 2 + (skullAnimCounter/5), 28, + '\x1D' | highlightflags, false); // right arrow + + statsPages[statsCurrentPage].drawer(); } // Handle statistics. @@ -7731,23 +7793,46 @@ static void M_HandleLevelStats(INT32 choice) switch (choice) { case KEY_DOWNARROW: + if (statsCurrentPage != 1) // Must be on level stats page + break; S_StartSound(NULL, sfx_menu1); if (statsLocation < statsMax) ++statsLocation; break; case KEY_UPARROW: + if (statsCurrentPage != 1) // Must be on level stats page + break; S_StartSound(NULL, sfx_menu1); if (statsLocation) --statsLocation; break; + case KEY_RIGHTARROW: + S_StartSound(NULL, sfx_menu1); + statsCurrentPage++; + if (statsCurrentPage >= NUMSTATSPAGES) + statsCurrentPage = 0; + break; + + case KEY_LEFTARROW: + S_StartSound(NULL, sfx_menu1); + if (statsCurrentPage == 0) + statsCurrentPage = NUMSTATSPAGES-1; + else + --statsCurrentPage; + break; + case KEY_PGDN: + if (statsCurrentPage != 1) // Must be on level stats page + break; S_StartSound(NULL, sfx_menu1); statsLocation += (statsLocation+13 >= statsMax) ? statsMax-statsLocation : 13; break; case KEY_PGUP: + if (statsCurrentPage != 1) // Must be on level stats page + break; S_StartSound(NULL, sfx_menu1); statsLocation -= (statsLocation < 13) ? statsLocation : 13; break; @@ -10249,8 +10334,7 @@ static void M_EraseDataResponse(INT32 ch) if (erasecontext == 2) { // SRB2Kart: This actually needs to be done FIRST, so that you don't immediately regain playtime/matches secrets - totalplaytime = 0; - matchesplayed = 0; + K_EraseStats(); for (i = 0; i < PWRLV_NUMTYPES; i++) vspowerlevel[i] = PWRLVRECORD_START; F_StartIntro(); diff --git a/src/m_random.c b/src/m_random.c index 481fdb72b..fa4a00f63 100644 --- a/src/m_random.c +++ b/src/m_random.c @@ -14,7 +14,7 @@ #include "doomdef.h" #include "doomtype.h" -#include "doomstat.h" // totalplaytime +#include "k_stats.h" // kartstats.totalplaytime #include "m_random.h" #include "m_fixed.h" @@ -252,5 +252,5 @@ void P_SetRandSeedD(const char *rfile, INT32 rline, UINT32 seed) */ UINT32 M_RandomizedSeed(void) { - return ((totalplaytime & 0xFFFF) << 16)|M_RandomFixed(); + return ((kartstats.totalplaytime & 0xFFFF) << 16)|M_RandomFixed(); } diff --git a/src/p_map.c b/src/p_map.c index f65e23140..4ea3e6b12 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -25,7 +25,7 @@ #include "r_sky.h" #include "s_sound.h" #include "w_wad.h" - +#include "k_stats.h" #include "k_kart.h" // SRB2kart 011617 #include "k_collide.h" #include "hu_stuff.h" // SRB2kart diff --git a/src/p_tick.c b/src/p_tick.c index 691a717de..61d754dd7 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -22,6 +22,7 @@ #include "m_random.h" #include "lua_script.h" #include "lua_hook.h" +#include "k_stats.h" #include "m_perfstats.h" #include "i_system.h" // I_GetPreciseTime #include "i_video.h" @@ -746,8 +747,7 @@ void P_Ticker(boolean run) } // Keep track of how long they've been playing! - if (!demo.playback) // Don't increment if a demo is playing. - totalplaytime++; + K_StatTicker(); // formality so kitemcap gets updated properly each frame. P_RunKartItems();