diff --git a/src/deh_soc.c b/src/deh_soc.c index 42dde0a2f..7298491e7 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2363,13 +2363,44 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) return; } - if ((offset=0) || fastcmp(params[0], "PLAYTIME") - || (++offset && fastcmp(params[0], "MATCHESPLAYED"))) + if (fastcmp(params[0], "PLAYTIME")) { PARAMCHECK(1); ty = UC_PLAYTIME + offset; re = atoi(params[1]); } + else if (fastcmp(params[0], "ROUNDSPLAYED")) + { + PARAMCHECK(1); + ty = UC_ROUNDSPLAYED; + re = atoi(params[1]); + x1 = GDGT_MAX; + + if (re == 0) + { + deh_warning("Rounds played requirement is %d for condition ID %d", re, id+1); + return; + } + + if (params[2]) + { + if (fastcmp(params[2], "RACE")) + x1 = GDGT_RACE; + else if (fastcmp(params[2], "BATTLE")) + x1 = GDGT_BATTLE; + else if (fastcmp(params[2], "CAPSULE")) + x1 = GDGT_CAPSULES; + else if (fastcmp(params[2], "SPECIAL")) + x1 = GDGT_SPECIAL; + else if (fastcmp(params[2], "CUSTOM")) + x1 = GDGT_CUSTOM; + else + { + deh_warning("gametype requirement \"%s\" invalid for condition ID %d", params[2], id+1); + return; + } + } + } else if (fastcmp(params[0], "TOTALRINGS")) { PARAMCHECK(1); diff --git a/src/g_game.c b/src/g_game.c index 80a187cfc..597624021 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -3994,8 +3994,18 @@ static void G_DoCompleted(void) if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) { + UINT8 roundtype = GDGT_CUSTOM; + + if (gametype == GT_RACE) + roundtype = GDGT_RACE; + else if (gametype == GT_BATTLE) + roundtype = (battlecapsules ? GDGT_CAPSULES : GDGT_BATTLE); + else if (gametype == GT_SPECIAL || gametype == GT_VERSUS) + roundtype = GDGT_SPECIAL; + + gamedata->roundsplayed[roundtype]++; + // Done before forced addition of PF_NOCONTEST to make UCRP_NOCONTEST harder to achieve - gamedata->matchesplayed++; M_UpdateUnlockablesAndExtraEmblems(true); gamedata->deferredsave = true; } @@ -4348,12 +4358,9 @@ void G_LoadGameData(void) // to new gamedata // see also M_EraseDataResponse G_ClearRecords(); // records + M_ClearStats(); // statistics M_ClearSecrets(); // emblems, unlocks, maps visited, etc - gamedata->totalplaytime = 0; - gamedata->matchesplayed = 0; - gamedata->totalrings = 0; - if (M_CheckParm("-nodata")) { // Don't load at all. @@ -4398,16 +4405,24 @@ void G_LoadGameData(void) } gamedata->totalplaytime = READUINT32(save.p); - gamedata->matchesplayed = READUINT32(save.p); if (versionMinor > 1) { gamedata->totalrings = READUINT32(save.p); + for (i = 0; i < GDGT_MAX; i++) + { + gamedata->roundsplayed[i] = READUINT32(save.p); + } + gamedata->crashflags = READUINT8(save.p); if (gamedata->crashflags & GDCRASH_LAST) gamedata->crashflags |= GDCRASH_ANY; } + else + { + save.p += 4; // no direct equivalent to matchesplayed + } { // Quick & dirty hash for what mod this save file is for. @@ -4574,7 +4589,7 @@ void G_SaveGameData(boolean dirty) return; } - length = (4+1+4+4+4+1+4+(MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+4+4+2); + length = (4+1+4+4+(4*GDGT_MAX)+1+4+(MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+4+4+2); if (gamedata->challengegrid) { length += gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; @@ -4592,9 +4607,13 @@ void G_SaveGameData(boolean dirty) WRITEUINT32(save.p, GD_VERSIONCHECK); // 4 WRITEUINT8(save.p, GD_VERSIONMINOR); // 1 WRITEUINT32(save.p, gamedata->totalplaytime); // 4 - WRITEUINT32(save.p, gamedata->matchesplayed); // 4 WRITEUINT32(save.p, gamedata->totalrings); // 4 + for (i = 0; i < GDGT_MAX; i++) // 4 * GDGT_MAX + { + WRITEUINT32(save.p, gamedata->roundsplayed[i]); + } + { UINT8 crashflags = (gamedata->crashflags & GDCRASH_ANY); if (dirty) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index bb6786cec..750903ee7 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5523,10 +5523,30 @@ void M_DrawStatistics(void) sprintf(beststr, "%u", gamedata->totalrings); } V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 32, V_6WIDTHSPACE, va("%s collected", beststr)); - beststr[0] = 0; - V_DrawThinString(20, 42, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Total Matches:"); - V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 42, V_6WIDTHSPACE, va("%i played", gamedata->matchesplayed)); + beststr[0] = 0; + V_DrawThinString(20, 42, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Total Rounds:"); + + strcat(beststr, va("%u Race", gamedata->roundsplayed[GDGT_RACE])); + + if (gamedata->roundsplayed[GDGT_CAPSULES] > 0) + { + strcat(beststr, va(", %u Capsule", gamedata->roundsplayed[GDGT_CAPSULES])); + } + + strcat(beststr, va(", %u Battle", gamedata->roundsplayed[GDGT_BATTLE])); + + if (gamedata->roundsplayed[GDGT_SPECIAL] > 0) + { + strcat(beststr, va(", %u Special", gamedata->roundsplayed[GDGT_SPECIAL])); + } + + if (gamedata->roundsplayed[GDGT_CUSTOM] > 0) + { + strcat(beststr, va(", %u Custom", gamedata->roundsplayed[GDGT_CUSTOM])); + } + + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 42, V_6WIDTHSPACE, beststr); if (!statisticsmenu.maplist) { diff --git a/src/m_cond.c b/src/m_cond.c index f41f348c6..b57908bb1 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -524,6 +524,17 @@ void M_ClearConditionSet(UINT8 set) } // Clear ALL secrets. +void M_ClearStats(void) +{ + UINT8 i; + gamedata->totalplaytime = 0; + gamedata->totalrings = 0; + for (i = 0; i < GDGT_MAX; ++i) + gamedata->roundsplayed[i] = 0; + gamedata->timesBeaten = 0; + gamedata->crashflags = 0; +} + void M_ClearSecrets(void) { INT32 i; @@ -544,9 +555,6 @@ void M_ClearSecrets(void) gamedata->challengegrid = NULL; gamedata->challengegridwidth = 0; - gamedata->timesBeaten = 0; - gamedata->crashflags = 0; - // Re-unlock any always unlocked things M_UpdateUnlockablesAndExtraEmblems(false); } @@ -627,8 +635,22 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) { case UC_PLAYTIME: // Requires total playing time >= x return (gamedata->totalplaytime >= (unsigned)cn->requirement); - case UC_MATCHESPLAYED: // Requires any level completed >= x times - return (gamedata->matchesplayed >= (unsigned)cn->requirement); + case UC_ROUNDSPLAYED: // Requires any level completed >= x times + { + if (cn->extrainfo1 == GDGT_MAX) + { + UINT8 i; + UINT32 sum = 0; + + for (i = 0; i < GDGT_MAX; i++) + { + sum += gamedata->roundsplayed[i]; + } + + return (sum >= (unsigned)cn->requirement); + } + return (gamedata->roundsplayed[cn->extrainfo1] >= (unsigned)cn->requirement); + } case UC_TOTALRINGS: // Requires grabbing >= x rings return (gamedata->totalrings >= (unsigned)cn->requirement); case UC_POWERLEVEL: // Requires power level >= x on a certain gametype @@ -834,33 +856,70 @@ static const char *M_GetConditionString(condition_t *cn) switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x + return va("Play for %i:%02i:%02i", G_TicsToHours(cn->requirement), G_TicsToMinutes(cn->requirement, false), G_TicsToSeconds(cn->requirement)); - case UC_MATCHESPLAYED: // Requires any level completed >= x times - return va("Play %d Rounds", cn->requirement); + + case UC_ROUNDSPLAYED: // Requires any level completed >= x times + + if (cn->extrainfo1 == GDGT_MAX) + work = ""; + else if (cn->extrainfo1 != GDGT_RACE + && cn->extrainfo1 != GDGT_BATTLE + && cn->extrainfo1 != GDGT_CUSTOM + && gamedata->roundsplayed[cn->extrainfo1] == 0) + work = " ???"; + else switch (cn->extrainfo1) + { + case GDGT_RACE: + work = " Race"; + break; + case GDGT_CAPSULES: + work = " Capsule"; + break; + case GDGT_BATTLE: + work = " Battle"; + break; + case GDGT_SPECIAL: + work = " Special"; + break; + case GDGT_CUSTOM: + work = " custom gametype"; + break; + default: + return va("INVALID GAMETYPE CONDITION \"%d:%d:%d\"", cn->type, cn->extrainfo1, cn->requirement); + } + + return va("Play %d%s Round%s", cn->requirement, work, + (cn->requirement == 1 ? "" : "s")); + case UC_TOTALRINGS: // Requires collecting >= x rings if (cn->requirement >= 1000000) return va("Collect %u,%u,%u Rings", (cn->requirement/1000000), (cn->requirement/1000)%1000, (cn->requirement%1000)); if (cn->requirement >= 1000) return va("Collect %u,%u Rings", (cn->requirement/1000), (cn->requirement%1000)); return va("Collect %u Rings", cn->requirement); + case UC_POWERLEVEL: // Requires power level >= x on a certain gametype return va("Get a PWR of %d in %s", cn->requirement, (cn->extrainfo1 == PWRLV_RACE) ? "Race" : "Battle"); + case UC_GAMECLEAR: // Requires game beaten >= x times if (cn->requirement > 1) return va("Beat game %d times", cn->requirement); else return va("Beat the game"); + case UC_OVERALLTIME: // Requires overall time <= x return va("Get overall time of %i:%02i:%02i", G_TicsToHours(cn->requirement), G_TicsToMinutes(cn->requirement, false), G_TicsToSeconds(cn->requirement)); + case UC_MAPVISITED: // Requires map x to be visited case UC_MAPBEATEN: // Requires map x to be beaten case UC_MAPENCORE: // Requires map x to be beaten in encore @@ -876,6 +935,7 @@ static const char *M_GetConditionString(condition_t *cn) Z_Free(title); return work; } + case UC_MAPTIME: // Requires time on map <= x { if (cn->extrainfo1 >= nummapheaders || !mapheaderinfo[cn->extrainfo1]) @@ -890,8 +950,10 @@ static const char *M_GetConditionString(condition_t *cn) Z_Free(title); return work; } + case UC_TOTALMEDALS: // Requires number of emblems >= x return va("Get %d medals", cn->requirement); + case UC_EMBLEM: // Requires emblem x to be obtained { INT32 checkLevel; @@ -987,7 +1049,9 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_PREFIX_BREAKTHECAPSULES: return "BREAK THE CAPSULES:"; case UCRP_PREFIX_SEALEDSTAR: - return "SEALED STAR:"; + if (gamedata->roundsplayed[GDGT_SPECIAL] == 0) + return NULL; + return "SEALED STARS:"; case UCRP_PREFIX_ISMAP: if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) diff --git a/src/m_cond.h b/src/m_cond.h index f19f9c93b..65fee4632 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -29,7 +29,7 @@ extern "C" { typedef enum { UC_PLAYTIME, // PLAYTIME [tics] - UC_MATCHESPLAYED, // SRB2Kart: MATCHESPLAYED [x played] + UC_ROUNDSPLAYED, // ROUNDSPLAYED [x played] UC_TOTALRINGS, // TOTALRINGS [x collected] UC_POWERLEVEL, // SRB2Kart: POWERLEVEL [power level to reach] [gametype, "0" for race, "1" for battle] UC_GAMECLEAR, // GAMECLEAR @@ -191,6 +191,15 @@ typedef enum // This is the largest number of 9s that will fit in UINT32. #define GDMAX_RINGS 999999999 +typedef enum { + GDGT_RACE, + GDGT_BATTLE, + GDGT_CAPSULES, + GDGT_SPECIAL, + GDGT_CUSTOM, + GDGT_MAX +} roundsplayed_t; + // GAMEDATA STRUCTURE // Everything that would get saved in gamedata.dat struct gamedata_t @@ -218,7 +227,7 @@ struct gamedata_t // PLAY TIME UINT32 totalplaytime; - UINT32 matchesplayed; + UINT32 roundsplayed[GDGT_MAX]; UINT32 totalrings; // Funny @@ -267,6 +276,7 @@ void M_UpdateConditionSetsPending(void); // Clearing secrets void M_ClearConditionSet(UINT8 set); void M_ClearSecrets(void); +void M_ClearStats(void); // Updating conditions and unlockables boolean M_CheckCondition(condition_t *cn, player_t *player); diff --git a/src/menus/options-data-erase-1.c b/src/menus/options-data-erase-1.c index d301f29f4..f3a977f7f 100644 --- a/src/menus/options-data-erase-1.c +++ b/src/menus/options-data-erase-1.c @@ -6,27 +6,31 @@ #include "../m_cond.h" // Condition Sets #include "../f_finale.h" +#define EC_CHALLENGES 0x01 +#define EC_STATISTICS 0x02 +#define EC_TIMEATTACK 0x04 +#define EC_ALLGAME (EC_CHALLENGES|EC_STATISTICS|EC_TIMEATTACK) + menuitem_t OPTIONS_DataErase[] = { + {IT_STRING | IT_CALL, "Erase Challenges Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_CHALLENGES, 0}, + + {IT_STRING | IT_CALL, "Erase Statistics Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_STATISTICS, 0}, {IT_STRING | IT_CALL, "Erase Time Attack Data", "Be careful! What's deleted is gone forever!", - NULL, {.routine = M_EraseData}, 0, 0}, + NULL, {.routine = M_EraseData}, EC_TIMEATTACK, 0}, - {IT_STRING | IT_CALL, "Erase Unlockable Data", "Be careful! What's deleted is gone forever!", - NULL, {.routine = M_EraseData}, 0, 0}, + {IT_STRING | IT_CALL, "\x85\x45rase all Game Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_ALLGAME, 0}, {IT_SPACE | IT_NOTHING, NULL, NULL, NULL, {NULL}, 0, 0}, - {IT_STRING | IT_CALL, "Erase Profile Data...", "Select a Profile to erase.", + {IT_STRING | IT_CALL, "Erase a Profile...", "Select a Profile to erase.", NULL, {.routine = M_CheckProfileData}, 0, 0}, - {IT_SPACE | IT_NOTHING, NULL, NULL, - NULL, {NULL}, 0, 0}, - - {IT_STRING | IT_CALL, "\x85\x45rase all Data", "Be careful! What's deleted is gone forever!", - NULL, {.routine = M_EraseData}, 0, 0}, - }; menu_t OPTIONS_DataEraseDef = { @@ -54,16 +58,12 @@ static void M_EraseDataResponse(INT32 ch) // Delete the data // see also G_LoadGameData - if (optionsmenu.erasecontext == 2) - { - // SRB2Kart: This actually needs to be done FIRST, so that you don't immediately regain playtime/matches secrets - gamedata->totalplaytime = 0; - gamedata->matchesplayed = 0; - gamedata->totalrings = 0; - } - if (optionsmenu.erasecontext != 1) + // We do these in backwards order to prevent things from being immediately re-unlocked. + if (optionsmenu.erasecontext & EC_TIMEATTACK) G_ClearRecords(); - if (optionsmenu.erasecontext != 0) + if (optionsmenu.erasecontext & EC_STATISTICS) + M_ClearStats(); + if (optionsmenu.erasecontext & EC_CHALLENGES) M_ClearSecrets(); F_StartIntro(); @@ -73,15 +73,21 @@ static void M_EraseDataResponse(INT32 ch) void M_EraseData(INT32 choice) { const char *eschoice, *esstr = M_GetText("Are you sure you want to erase\n%s?\n\nPress (A) to confirm or (B) to cancel\n"); + (void)choice; - optionsmenu.erasecontext = (UINT8)choice; + optionsmenu.erasecontext = (UINT8)currentMenu->menuitems[itemOn].mvar1; - if (choice == 0) + if (optionsmenu.erasecontext == EC_CHALLENGES) + eschoice = M_GetText("Challenges data"); + else if (optionsmenu.erasecontext == EC_STATISTICS) + eschoice = M_GetText("Statistics data"); + else if (optionsmenu.erasecontext == EC_TIMEATTACK) eschoice = M_GetText("Time Attack data"); - else if (choice == 1) - eschoice = M_GetText("Secrets data"); - else + else if (optionsmenu.erasecontext == EC_ALLGAME) eschoice = M_GetText("ALL game data"); + else + eschoice = va("[misconfigured erasecontext %d]", optionsmenu.erasecontext); + M_StartMessage(va(esstr, eschoice), FUNCPTRCAST(M_EraseDataResponse), MM_YESNO); }