gamedata->roundsplayed: Split into multiple roughly-gametype-aligned categories

- Race, Capsule, Battle, Special, Custom
- All categories can now be used for Rounds Played condition
- UCRP_SEALEDSTAR now hides if you haven't beaten a single Sealed Star yet
In addition, introduce M_ClearStats
- As more statistics get added, clearing them manually in G_LoadGameData and M_EraseDataResponse is just going to get annoying
- Change around the options on the Erase Data screen to
    - Make it clear that erasing all game data won't clear your Profiles
    - Add an option to clear stats by themselves, rather than only permitting via complete gamedata wipe
    - Move to the "Challenges" terminology over SRB2's "Secrets" terminology
- Move some entries that were previously handled in M_ClearSecrets into M_ClearStats
This commit is contained in:
toaster 2023-03-04 20:09:29 +00:00
parent 22f9467e71
commit e994b920c6
6 changed files with 197 additions and 47 deletions

View file

@ -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);

View file

@ -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)

View file

@ -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)
{

View file

@ -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])

View file

@ -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 <x times>
@ -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);

View file

@ -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);
}