diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f31a9bd26..121cb9925 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -173,6 +173,7 @@ set(SRB2_CORE_GAME_SOURCES p_user.c k_director.c k_kart.c + k_stats.c i_time.c p_local.h @@ -187,6 +188,7 @@ set(SRB2_CORE_GAME_SOURCES p_tick.h k_director.h k_kart.h + k_stats.h ) if(NOT (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) diff --git a/src/Makefile b/src/Makefile index 3b090873f..caaa36f4c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -506,6 +506,7 @@ OBJS:=$(i_main_o) \ $(OBJDIR)/st_stuff.o \ $(OBJDIR)/k_director.o \ $(OBJDIR)/k_kart.o \ + $(OBJDIR)/k_stats.o \ $(OBJDIR)/m_aatree.o \ $(OBJDIR)/m_anigif.o \ $(OBJDIR)/m_argv.o \ diff --git a/src/d_netcmd.c b/src/d_netcmd.c index b01f87e5a..3a2b90369 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -5801,12 +5801,8 @@ static void Fishcake_OnChange(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/dehacked.c b/src/dehacked.c index e4b4028ef..9acf2006b 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -1756,7 +1756,7 @@ static void readsound(MYFILE *f, INT32 num, const char *savesfxnames[]) * \sa readmaincfg() * \author Graue */ -static boolean GoodDataFileName(const char *s) +/*static boolean GoodDataFileName(const char *s) { const char *p; const char *tail = ".dat"; @@ -1776,7 +1776,7 @@ static boolean GoodDataFileName(const char *s) if (fasticmp(s, "online.dat")) return false; // SRB2Kart online replay folder return true; -} +}*/ static void reademblemdata(MYFILE *f, INT32 num) { @@ -2554,10 +2554,10 @@ static void readmaincfg(MYFILE *f) { maxXtraLife = (UINT8)get_number(word2); } - 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. @@ -2581,7 +2581,7 @@ static void readmaincfg(MYFILE *f) // can't use sprintf since there is %u in savegamename strcatbf(savegamename, srb2home, PATHSEP); - refreshdirmenu |= REFRESHDIR_GAMEDATA; + refreshdirmenu |= REFRESHDIR_GAMEDATA;*/ } else if (fastcmp(word, "RESETDATA")) { diff --git a/src/doomstat.h b/src/doomstat.h index f70f97059..7e83eab04 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -338,9 +338,6 @@ enum GameType // SRB2Kart // String names for gametypes extern const char *Gametype_Names[NUMGAMETYPES]; -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 f12a72620..73ca41898 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -52,6 +52,7 @@ #include "md5.h" // demo checksums #include "k_director.h" // SRB2kart #include "k_kart.h" // SRB2kart +#include "k_stats.h" // SRB2kart #include "r_fps.h" // frame interpolation/uncapped #ifdef HAVE_DISCORDRPC @@ -175,8 +176,6 @@ INT32 tokenbits; // Used for setting token bits // Old Special Stage INT32 sstimer; // Time allotted in the special stage -tic_t totalplaytime; -UINT32 matchesplayed; // SRB2Kart boolean gamedataloaded = false; // Time attack data for levels @@ -600,6 +599,36 @@ tic_t G_GetBestTime(INT16 map) return mainrecords[map-1]->time; } +// 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(true); + + 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) { @@ -614,8 +643,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 a seperate save file.\n")); // If in record attack recording, cancel it. if (modeattacking) @@ -2011,7 +2043,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(true)) S_StartSound(NULL, sfx_ncitem); G_SaveGameData(true); @@ -2684,6 +2716,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++; } } @@ -3113,6 +3148,8 @@ static void G_DoCompleted(void) if (metalrecording) G_StopMetalRecording(); + K_StatRound(); + for (i = 0; i < MAXPLAYERS; i++) if (playeringame[i]) { @@ -3482,8 +3519,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 if (M_CheckParm("-nodata")) return; // Don't load. @@ -3512,8 +3548,8 @@ void G_LoadGameData(void) I_Error("Game data is from another version of SRB2.\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); modded = READUINT8(save.p); @@ -3606,6 +3642,8 @@ void G_SaveGameData(boolean force) INT32 i, j; UINT8 btemp; savebuffer_t save; + (void)force; + char backupfile[MAX_WADPATH+4]; if (!gamedataloaded) return; // If never loaded (-nodata), don't save @@ -3617,18 +3655,27 @@ void G_SaveGameData(boolean force) return; } - if (majormods && !force) + // Create backup of the save data + snprintf(backupfile, sizeof(backupfile), "%s.bak", gamedatafilename); + backupfile[sizeof(backupfile) - 1] = '\0'; + + FILE *gamedata = fopen(gamedatafilename, "r"); + + if (gamedata != NULL) { - free(save.buffer); - save.p = save.buffer = NULL; - return; + fclose(gamedata); + + if (!FIL_CopyFile(gamedatafilename, backupfile)) + { + CONS_Alert(CONS_WARNING,"Failed to create a backup of save data. Will not attempt to write to save data\n"); + return; + } } // Version test WRITEUINT32(save.p, 0xFCAFE211); - WRITEUINT32(save.p, totalplaytime); - WRITEUINT32(save.p, matchesplayed); + K_WriteStats(&save, !savemoddata); btemp = (UINT8)(savemoddata); // what used to be here was profoundly dunderheaded WRITEUINT8(save.p, btemp); diff --git a/src/k_kart.c b/src/k_kart.c index 209f083e1..259ea8a3a 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -34,6 +34,7 @@ #include "m_cond.h" #include "k_director.h" #include "k_kart.h" +#include "k_stats.h" #include "f_finale.h" #include "lua_hud.h" // For Lua hud checks #include "lua_hook.h" // For MobjDamage and ShouldDamage @@ -2518,6 +2519,8 @@ void K_SpinPlayer(player_t *player, mobj_t *source, INT32 type, mobj_t *inflicto if (LUA_HookPlayerSpin(player, inflictor, source)) // Let Lua do its thing or overwrite if it wants to. Make sure to let any possible instashield happen because we didn't get "damaged" in this case. return; + K_StatPlayerHit(player, source ? source->player : NULL); + if (source && source != player->mo && source->player) K_PlayHitEmSound(source, player->mo); diff --git a/src/k_stats.c b/src/k_stats.c new file mode 100644 index 000000000..538b4e3e3 --- /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 (G_RaceGametype()) + kartstats.raceplaytime++; + else + 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 b777e7d22..f9d871969 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -21,6 +21,7 @@ #include "r_things.h" // numskins //#include "r_draw.h" // R_GetColorByName #include "k_kart.h" // K_GetKartColorByName +#include "k_stats.h" // Map triggers for linedef executors // 32 triggers, one bit each @@ -286,9 +287,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_GAMECLEAR: // Requires game beaten >= x times return (timesBeaten >= (unsigned)cn->requirement); case UC_ALLEMERALDS: // Requires game beaten with all 7 emeralds >= x times @@ -384,9 +385,7 @@ UINT8 M_UpdateUnlockablesAndExtraEmblems(boolean force) INT32 i; char cechoText[992] = ""; UINT8 cechoLines = 0; - - if (majormods && !force) // SRB2Kart: for enabling unlocks online in modified servers - return false; + (void)force; M_CheckUnlockConditions(); diff --git a/src/m_menu.c b/src/m_menu.c index fbe7e0ffb..09ed017c5 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -70,6 +70,7 @@ #include "st_stuff.h" #include "i_sound.h" #include "k_kart.h" // SRB2kart +#include "k_stats.h" // SRB2kart #include "d_player.h" // KITEM_ constants #include "i_joy.h" // for joystick menu controls @@ -474,6 +475,12 @@ static void Dummystaff_OnChange(void); // crap to force hud to show when in saturns hud options boolean forceshowhud = false; +// smol text indicating if game is modified +// so ppl dont wonder where their ra times went and stuff +#define SHOWMODDEDGAME \ + if (savemoddata) \ + V_DrawThinString(0, 0, V_REDMAP|V_SNAPTOTOP|V_SNAPTOLEFT|V_TRANSLUCENT|V_ALLOWLOWERCASE, ("Modified Game")); + // ========================================================================== // CONSOLE VARIABLES AND THEIR POSSIBLE VALUES GO HERE. // ========================================================================== @@ -4099,17 +4106,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) { @@ -6193,7 +6189,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 a seperate save file.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname); prevmajormods = majormods; } @@ -8615,9 +8611,28 @@ static void M_Credits(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[NUMMAPS+1]; +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) { @@ -8644,6 +8659,7 @@ static void M_Statistics(INT32 choice) statsMapList[j] = -1; statsMax = j - 11 + numextraemblems; statsLocation = 0; + statsCurrentPage = 0; if (statsMax < 0) statsMax = 0; @@ -8651,13 +8667,47 @@ static void M_Statistics(INT32 choice) M_SetupNextMenu(&SP_LevelStatsDef); } -static void M_DrawStatsMaps(int location) +static void M_DrawStatsMaps(void) { - INT32 y = 80, 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); + SHOWMODDEDGAME + + for (j = 0; j < NUMMAPS; j++) + { + if (!mapheaderinfo[j] || !(mapheaderinfo[j]->menuflags & LF2_RECORDATTACK)) + continue; + + if (!mainrecords[j] || mainrecords[j]->time <= 0) + { + mapsunfinished++; + continue; + } + + besttime += mainrecords[j]->time; + } + + V_DrawString(20, 42, highlightflags|MENUCAPS, "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|MENUCAPS, va("(%d unfinished)", mapsunfinished)); + else + V_DrawRightAlignedString(BASEVIDWIDTH-16, 50, recommendedflags|MENUCAPS, "(complete)"); + + V_DrawString(32, 50, MENUCAPS, 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 @@ -8746,53 +8796,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|MENUCAPS, "Total Play Time:"); - V_DrawCenteredString(BASEVIDWIDTH/2, 32, MENUCAPS, va("%i hours, %i minutes, %i seconds", - G_TicsToHours(totalplaytime), - G_TicsToMinutes(totalplaytime, false), - G_TicsToSeconds(totalplaytime))); - V_DrawString(20, 42, highlightflags|MENUCAPS, "Total Matches:"); - V_DrawRightAlignedString(BASEVIDWIDTH-16, 42, MENUCAPS, va("%i played", matchesplayed)); - - for (i = 0; i < NUMMAPS; i++) - { - if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_RECORDATTACK)) - continue; - - if (!mainrecords[i] || mainrecords[i]->time <= 0) - { - mapsunfinished++; - continue; - } - - besttime += mainrecords[i]->time; +#define DRAWTIMESTAT(y, title, field) { \ + char timebuf[80]; \ + V_DrawString(20, (y), highlightflags|MENUCAPS, 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), MENUCAPS, timebuf); \ } - V_DrawString(20, 62, highlightflags|MENUCAPS, "Combined time records:"); +#define DRAWAMOUNTSTAT(y, title, field) { \ + V_DrawString(20, (y), highlightflags|MENUCAPS, title); \ + unsigned amountval = kartstats.field; \ + V_DrawRightAlignedString(BASEVIDWIDTH-16, (y), MENUCAPS, 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, 62, (mapsunfinished ? warningflags : 0), beststr); +static void M_DrawStatsPlaytime(void) +{ + V_DrawString(20, 42, highlightflags|MENUCAPS, "Total Play Time:"); + V_DrawCenteredString(BASEVIDWIDTH/2, 52, MENUCAPS, 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|MENUCAPS, "Total Matches:"); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 62, MENUCAPS, va("%i played", kartstats.matchesplayed)); - if (mapsunfinished) - V_DrawRightAlignedString(BASEVIDWIDTH-16, 70, warningflags|MENUCAPS, va("(%d unfinished)", mapsunfinished)); - else - V_DrawRightAlignedString(BASEVIDWIDTH-16, 70, recommendedflags|MENUCAPS, "(complete)"); + // Nothing else to draw + if (kartstats.vanilla) + return; - V_DrawString(32, 70, MENUCAPS, va("x %d/%d", M_CountEmblems(), numemblems+numextraemblems)); - V_DrawSmallScaledPatch(20, 70, 0, W_CachePatchName("GOTITA", PU_STATIC)); + SHOWMODDEDGAME - M_DrawStatsMaps(statsLocation); + DRAWTIMESTAT(82, "RA Play Time:", raplaytime); + DRAWTIMESTAT(92, "Online Play Time:", onlineplaytime); + DRAWTIMESTAT(102, "Race Play Time:", raceplaytime); + DRAWTIMESTAT(112, "Battle Play Time:", battleplaytime); +} + +// Note: only available with non-vanilla stats loaded, so it doesn't check for that +static void M_DrawStatsExtra(void) +{ + SHOWMODDEDGAME + + 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|MENUCAPS, statsPages[statsCurrentPage].title); + + INT32 w = V_StringWidth(statsPages[statsCurrentPage].title, highlightflags|MENUCAPS); + 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. @@ -8803,23 +8878,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; @@ -8860,6 +8958,8 @@ void M_DrawTimeAttackMenu(void) x = currentMenu->x; y = currentMenu->y; + SHOWMODDEDGAME + // Character face! if (W_CheckNumForName(skins[cv_chooseskin.value-1].facewant) != LUMPERROR) { @@ -11745,8 +11845,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(); F_StartIntro(); } if (erasecontext != 1) diff --git a/src/m_random.c b/src/m_random.c index 197f37239..9b4396cbd 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 df03bcb76..e60142443 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -26,6 +26,7 @@ #include "s_sound.h" #include "w_wad.h" #include "k_kart.h" // SRB2kart 011617 +#include "k_stats.h" #include "hu_stuff.h" // SRB2kart #include "i_system.h" // SRB2kart @@ -719,6 +720,8 @@ static boolean PIT_CheckThing(mobj_t *thing) if (thing->type == MT_PLAYER) { + K_StatPlayerSink(thing->player, P_MobjWasRemoved(tmthing->target) ? NULL : tmthing->target->player); + S_StartSound(NULL, sfx_bsnipe); //let all players hear it. HU_SetCEchoFlags(0); HU_SetCEchoDuration(5); @@ -986,6 +989,8 @@ static boolean PIT_CheckThing(mobj_t *thing) if ((thing->target == tmthing) && (thing->threshold > 0)) return true; + K_StatPlayerSink(tmthing->player, P_MobjWasRemoved(thing->target) ? NULL : thing->target->player); + S_StartSound(NULL, sfx_cgot); //let all players hear it. HU_SetCEchoFlags(0); HU_SetCEchoDuration(5); diff --git a/src/p_setup.c b/src/p_setup.c index 3b2ed7714..362cd71d0 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -988,7 +988,7 @@ static inline void P_SpawnEmblems(void) static void P_SpawnSecretItems(boolean loademblems) { // Now let's spawn those funky emblem things! Tails 12-08-2002 - if (netgame || multiplayer || majormods) // No cheating!! + if (netgame || multiplayer) // No cheating!! return; if (loademblems) @@ -2976,7 +2976,7 @@ boolean P_SetupLevel(boolean skipprecip, boolean reloadinggamestate) nextmapoverride = 0; skipstats = false; - if (!(netgame || multiplayer) && !majormods) + if (!(netgame || multiplayer)) mapvisited[gamemap-1] |= MV_VISITED; levelloading = false; diff --git a/src/p_tick.c b/src/p_tick.c index 6996b4d4d..34bc28847 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -25,6 +25,7 @@ #include "lua_hook.h" #include "k_director.h" #include "k_kart.h" +#include "k_stats.h" #include "i_system.h" #include "r_main.h" #include "r_fps.h" @@ -610,8 +611,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(); if (run) { diff --git a/src/y_inter.c b/src/y_inter.c index 61f4a069b..cce19ad7f 100644 --- a/src/y_inter.c +++ b/src/y_inter.c @@ -844,7 +844,7 @@ void Y_StartIntermission(void) } case int_race: // (time-only race) { - if (!majormods && !multiplayer && !demo.playback) // remove this once we have a proper time attack screen + if (!multiplayer && !demo.playback) // remove this once we have a proper time attack screen { // Update visitation flags mapvisited[gamemap-1] |= MV_BEATEN;