Sealed Star re-ordering (resolves #606)

- If emerald not yet collected on that cup, pick the first uncollected emerald, then get the cup's CUPCACHE_SPECIAL with that ID to pick the stage
- Already collected emeralds retain their swappage across gamedata saves
- Returns to normal order if you get all 7 OR Special Mode is unlocked (chao key? debug? password in modded games? sky's the limit)
- Pops up a Message from the Stars telling you the gems have been returned to their natural place
- Add-ons will always use their dedicated sealed star, since it's unordered material

If it weren't so last minute I could have a better solution for GP Backups, but right now what I've gone for is it always trusts whatever G_GPCupIntoRoundQueue does AS LONG AS THE COURSE ISN'T THE ONE YOU'RE RELOADING INTO. If it IS, then it checks to see if it's exactly what's been saved, and complains (with the generic error message, unfortunately) if it isn't.
This commit is contained in:
toaster 2024-03-01 23:22:28 +00:00
parent b00255e333
commit e8523b69f0
13 changed files with 333 additions and 43 deletions

View file

@ -3932,15 +3932,68 @@ void G_GPCupIntoRoundQueue(cupheader_t *cup, UINT8 setgametype, boolean setencor
// At the end of the Cup is a Rank-restricted treat.
// So we append it to the end of the roundqueue.
// (as long as it exists, of course!)
cupLevelNum = cup->cachedlevels[CUPCACHE_SPECIAL];
if (cupLevelNum < nummapheaders)
{
G_MapIntoRoundQueue(
cupLevelNum,
G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel),
setencore, // if this isn't correct, Got_Mapcmd will fix it
true // Rank-restricted!
);
// Of course, this last minute game design tweak
// has to make things a little complicated. We
// basically just make sure they're dispensed
// at the intended difficulty sequence until
// you've got them all, at which point they
// become their intended order permanently.
// ~toast 010324
cupheader_t *emeraldcup = NULL;
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| cup->id >= basenumkartcupheaders // custom content
|| M_SecretUnlocked(SECRET_SPECIALATTACK, false)) // true order
{
// Standard order.
emeraldcup = cup;
}
else
{
// Determine order from sealedswaps.
for (i = 0; (i < GDMAX_SEALEDSWAPS && gamedata->sealedswaps[i]); i++)
{
if (gamedata->sealedswaps[i] != grandprixinfo.cup)
continue;
// Repeat visit, grab the same ID.
break;
}
// If there's pending stars, get them from the associated cup order.
if (i < GDMAX_SEALEDSWAPS)
{
emeraldcup = kartcupheaders;
while (emeraldcup)
{
if (emeraldcup->id >= basenumkartcupheaders)
{
emeraldcup = NULL;
break;
}
if (emeraldcup->emeraldnum == i+1)
break;
emeraldcup = emeraldcup->next;
}
}
}
if (emeraldcup)
{
cupLevelNum = emeraldcup->cachedlevels[CUPCACHE_SPECIAL];
if (cupLevelNum < nummapheaders)
{
G_MapIntoRoundQueue(
cupLevelNum,
G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel),
setencore, // if this isn't correct, Got_Mapcmd will fix it
true // Rank-restricted!
);
}
}
}
if (roundqueue.size == 0)

View file

@ -64,6 +64,7 @@ void srb2::save_ng_gamedata()
ng.milestones.majorkeyskipattempted = gamedata->majorkeyskipattempted;
ng.milestones.finishedtutorialchallenge = gamedata->finishedtutorialchallenge;
ng.milestones.enteredtutorialchallenge = gamedata->enteredtutorialchallenge;
ng.milestones.sealedswapalerted = gamedata->sealedswapalerted;
ng.milestones.gonerlevel = gamedata->gonerlevel;
ng.prisons.thisprisoneggpickup = gamedata->thisprisoneggpickup;
ng.prisons.prisoneggstothispickup = gamedata->prisoneggstothispickup;
@ -176,7 +177,7 @@ void srb2::save_ng_gamedata()
}
for (auto cup = kartcupheaders; cup; cup = cup->next)
{
if (cup->windata[0].best_placement == 0)
if (cup->windata[0].best_placement == 0 && cup->windata[1].got_emerald == false)
{
continue;
}
@ -229,6 +230,17 @@ void srb2::save_ng_gamedata()
ng.cups[cupdata.name] = std::move(cupdata);
}
for (int i = 0; (i < GDMAX_SEALEDSWAPS && gamedata->sealedswaps[i]); i++)
{
srb2::GamedataSealedSwapJson sealedswap {};
cupheader_t* cup = gamedata->sealedswaps[i];
sealedswap.name = std::string(cup->name);
ng.sealedswaps.emplace_back(std::move(sealedswap));
}
std::string gamedataname_s {gamedatafilename};
fs::path savepath {fmt::format("{}/{}", srb2home, gamedataname_s)};
fs::path tmpsavepath {fmt::format("{}/{}.tmp", srb2home, gamedataname_s)};
@ -418,6 +430,7 @@ void srb2::load_ng_gamedata()
gamedata->majorkeyskipattempted = js.milestones.majorkeyskipattempted;
gamedata->finishedtutorialchallenge = js.milestones.finishedtutorialchallenge;
gamedata->enteredtutorialchallenge = js.milestones.enteredtutorialchallenge;
gamedata->sealedswapalerted = js.milestones.sealedswapalerted;
gamedata->gonerlevel = js.milestones.gonerlevel;
gamedata->thisprisoneggpickup = js.prisons.thisprisoneggpickup;
gamedata->prisoneggstothispickup = js.prisons.prisoneggstothispickup;
@ -720,6 +733,33 @@ void srb2::load_ng_gamedata()
}
}
size_t sealedswaps_size = js.sealedswaps.size();
for (size_t i = 0; i < std::min((size_t)GDMAX_SEALEDSWAPS, sealedswaps_size); i++)
{
cupheader_t* cup = nullptr;
// Find BASE cups only
for (cup = kartcupheaders; cup; cup = cup->next)
{
if (cup->id >= basenumkartcupheaders)
{
cup = NULL;
break;
}
std::string cupname = std::string(cup->name);
if (cupname == js.sealedswaps[i].name)
{
break;
}
}
if (cup)
{
gamedata->sealedswaps[i] = cup;
}
}
M_FinaliseGameData();
}

View file

@ -70,6 +70,7 @@ struct GamedataMilestonesJson final
bool majorkeyskipattempted;
bool finishedtutorialchallenge;
bool enteredtutorialchallenge;
bool sealedswapalerted;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
GamedataMilestonesJson,
@ -81,7 +82,8 @@ struct GamedataMilestonesJson final
chaokeytutorial,
majorkeyskipattempted,
finishedtutorialchallenge,
enteredtutorialchallenge
enteredtutorialchallenge,
sealedswapalerted
)
};
@ -184,6 +186,13 @@ struct GamedataCupJson final
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataCupJson, name, records)
};
struct GamedataSealedSwapJson final
{
std::string name;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataSealedSwapJson, name)
};
struct GamedataJson final
{
GamedataPlaytimeJson playtime;
@ -203,6 +212,7 @@ struct GamedataJson final
std::unordered_map<std::string, GamedataMapJson> maps;
std::vector<GamedataSprayCanJson> spraycans;
std::unordered_map<std::string, GamedataCupJson> cups;
std::vector<GamedataSealedSwapJson> sealedswaps;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
GamedataJson,
@ -222,7 +232,8 @@ struct GamedataJson final
skins,
maps,
spraycans,
cups
cups,
sealedswaps
)
};

View file

@ -680,6 +680,8 @@ void M_Init(void);
void M_PlayMenuJam(void);
boolean M_ConsiderSealedSwapAlert(void);
void M_OpenVirtualKeyboard(boolean gamepad);
void M_MenuTypingInput(INT32 key);
@ -1347,6 +1349,8 @@ extern struct challengesmenu_s {
boolean chaokeyadd, keywasadded;
UINT8 chaokeyhold;
boolean considersealedswapalert;
boolean requestflip;
UINT16 unlockcount[CMC_MAX];

View file

@ -2823,6 +2823,57 @@ fixed_t M_DrawCupWinData(INT32 rankx, INT32 ranky, cupheader_t *cup, UINT8 diffi
rankx += 19 - (rankw / 2);
cupwindata_t *windata = &(cup->windata[difficulty]);
UINT8 emeraldnum = UINT8_MAX;
if (!noemerald)
{
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| cup->id >= basenumkartcupheaders // custom content
|| M_SecretUnlocked(SECRET_SPECIALATTACK, true)) // true order
{
// Standard order.
if (windata->got_emerald == true)
{
emeraldnum = cup->emeraldnum;
}
}
else
{
// Determine order from sealedswaps.
UINT8 i;
for (i = 0; (i < GDMAX_SEALEDSWAPS && gamedata->sealedswaps[i]); i++)
{
if (gamedata->sealedswaps[i] != cup)
continue;
break;
}
// If there's pending stars, get them from the associated cup order.
if (i < GDMAX_SEALEDSWAPS)
{
cupheader_t *emeraldcup = kartcupheaders;
while (emeraldcup)
{
if (emeraldcup->id >= basenumkartcupheaders)
{
emeraldcup = NULL;
break;
}
if (emeraldcup->emeraldnum == i+1)
{
if (emeraldcup->windata[difficulty].got_emerald == true)
emeraldnum = i+1;
break;
}
emeraldcup = emeraldcup->next;
}
}
}
}
if (windata->best_placement == 0)
{
if (statsmode)
@ -2832,14 +2883,13 @@ fixed_t M_DrawCupWinData(INT32 rankx, INT32 ranky, cupheader_t *cup, UINT8 diffi
V_DrawCharacter((14-4)/2 + rankx, ranky, '.' | V_GRAYMAP, false);
rankx += 14 + 1;
V_DrawCharacter((12-4)/2 + rankx, ranky, '.' | V_GRAYMAP, false);
if (!noemerald)
{
rankx += 12 + 1;
V_DrawCharacter((12-4)/2 + rankx, ranky, '.' | V_GRAYMAP, false);
}
}
return rankw;
else
{
rankx += 14 + 1;
}
goto windataemeraldmaybe;
}
UINT8 *colormap = NULL;
@ -2934,13 +2984,15 @@ fixed_t M_DrawCupWinData(INT32 rankx, INT32 ranky, cupheader_t *cup, UINT8 diffi
if (charPat)
V_DrawFixedPatch((rankx)*FRACUNIT, (ranky)*FRACUNIT, FRACUNIT, 0, charPat, colormap);
windataemeraldmaybe:
rankx += 12 + 1;
if (noemerald)
;
else if (windata->got_emerald == true)
else if (emeraldnum != UINT8_MAX)
{
if (cup->emeraldnum == 0)
if (emeraldnum == 0)
V_DrawCharacter(rankx+2, ranky+2, '+', false);
else
{
@ -2948,14 +3000,14 @@ fixed_t M_DrawCupWinData(INT32 rankx, INT32 ranky, cupheader_t *cup, UINT8 diffi
if (!flash)
{
UINT16 col = SKINCOLOR_CHAOSEMERALD1 + (cup->emeraldnum-1) % 7;
UINT16 col = SKINCOLOR_CHAOSEMERALD1 + (emeraldnum-1) % 7;
colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE);
}
const char *emname = va(
"%sMAP%c",
(cup->emeraldnum > 7) ? "SUP" : "EME",
(emeraldnum > 7) ? "SUP" : "EME",
colormap ? '\0' : 'B'
);
@ -3116,7 +3168,7 @@ void M_DrawCupSelect(void)
y += 44; //(8 + 100) - (20 + 44)
}
if (windata && windata->best_placement != 0)
if (windata)
{
M_DrawCupWinData(
x,

View file

@ -560,6 +560,35 @@ void M_PlayMenuJam(void)
#undef IsCurrentlyPlaying
boolean M_ConsiderSealedSwapAlert(void)
{
if (gamedata->sealedswapalerted == true)
return false;
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| M_SecretUnlocked(SECRET_SPECIALATTACK, true)) // true order
{
gamedata->sealedswapalerted = true;
// Don't make a message if no Sealed Stars have yet been found.
if (gamedata->everseenspecial == false)
return false;
M_StartMessage(
"Message from the Stars",
"As if called by fate, the Emeralds you've\n"
"collected return to their rightful places...\n"
"\n"
"The Sealed Stars are now ordered via Cups!\n",
NULL, MM_NOTHING, NULL, NULL
);
return true;
}
return false;
}
void M_ValidateRestoreMenu(void)
{
if (restoreMenu == NULL || restoreMenu == &MAIN_GonerDef)
@ -629,6 +658,11 @@ menu_t *M_SpecificMenuRestore(menu_t *torestore)
M_SetupPlayMenu(-1);
PLAY_CharSelectDef.prevMenu = &MainDef;
if (torestore != &MISC_ChallengesDef)
{
M_ConsiderSealedSwapAlert();
}
return torestore;
}

View file

@ -83,6 +83,7 @@ static struct podiumData_s
sfxenum_t gradeVoice;
cupheader_t *cup;
UINT8 emeraldnum;
boolean fastForward;
@ -102,6 +103,7 @@ void podiumData_s::Init(void)
{
rank = grandprixinfo.rank;
cup = grandprixinfo.cup;
emeraldnum = cup->emeraldnum;
}
else
{
@ -119,6 +121,7 @@ void podiumData_s::Init(void)
cup = cup->next;
}
emeraldnum = 0;
memset(&rank, 0, sizeof(gpRank_t));
rank.skin = players[consoleplayer].skin;
@ -628,12 +631,7 @@ void podiumData_s::Draw(void)
case GPEVENT_SPECIAL:
{
srb2::Draw drawer_emerald = drawer_gametype;
UINT8 emeraldNum = 0;
if (cup != nullptr)
{
emeraldNum = cup->emeraldnum;
}
UINT8 emeraldNum = g_podiumData.emeraldnum;
boolean useWhiteFrame = ((leveltime & 1) || !dta->gotSpecialPrize);
patch_t *emeraldPatch = nullptr;
@ -844,12 +842,7 @@ void podiumData_s::Draw(void)
if (rank.specialWon == true)
{
UINT8 emeraldNum = 0;
if (cup != nullptr)
{
emeraldNum = cup->emeraldnum;
}
UINT8 emeraldNum = g_podiumData.emeraldnum;
const boolean emeraldBlink = (leveltime & 1);
patch_t *emeraldOverlay = nullptr;
@ -1263,6 +1256,60 @@ void K_ResetCeremony(void)
return;
}
cupheader_t *emeraldcup = NULL;
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| grandprixinfo.cup->id >= basenumkartcupheaders // custom content
|| M_SecretUnlocked(SECRET_SPECIALATTACK, false)) // true order
{
// Standard order.
emeraldcup = grandprixinfo.cup;
}
else
{
// Determine order from sealedswaps.
for (i = 0; i < GDMAX_SEALEDSWAPS; i++)
{
if (gamedata->sealedswaps[i] == NULL)
{
if (g_podiumData.rank.specialWon == true)
{
// First visit! Mark it off.
gamedata->sealedswaps[i] = grandprixinfo.cup;
}
break;
}
if (gamedata->sealedswaps[i] != grandprixinfo.cup)
continue;
// Repeat visit, grab the same ID.
break;
}
// If there's pending stars, apply them to the new cup order.
if (i < GDMAX_SEALEDSWAPS)
{
emeraldcup = kartcupheaders;
while (emeraldcup)
{
if (emeraldcup->id >= basenumkartcupheaders)
{
emeraldcup = NULL;
break;
}
if (emeraldcup->emeraldnum == i+1)
break;
emeraldcup = emeraldcup->next;
}
g_podiumData.emeraldnum = i+1;
}
}
// Write grade, position, and emerald-having-ness for later sessions!
i = (grandprixinfo.masterbots) ? KARTGP_MASTER : grandprixinfo.gamespeed;
@ -1292,9 +1339,9 @@ void K_ResetCeremony(void)
anymerit = true;
}
if (g_podiumData.rank.specialWon == true)
if (g_podiumData.rank.specialWon == true && emeraldcup)
{
grandprixinfo.cup->windata[i].got_emerald = true;
emeraldcup->windata[i].got_emerald = true;
anymerit = true;
}

View file

@ -96,6 +96,7 @@ static UINT8 cheatf_warp(void)
if (success)
{
gamedata->gonerlevel = GDGONER_DONE;
gamedata->sealedswapalerted = true;
G_SetUsedCheats();
}
@ -231,6 +232,7 @@ static UINT8 cheatf_devmode(void)
}
gamedata->gonerlevel = GDGONER_DONE;
gamedata->sealedswapalerted = true;
M_ClearMenus(true);

View file

@ -666,6 +666,7 @@ void M_ClearStats(void)
gamedata->majorkeyskipattempted = false;
gamedata->enteredtutorialchallenge = false;
gamedata->finishedtutorialchallenge = false;
gamedata->sealedswapalerted = false;
gamedata->musicstate = GDMUSIC_NONE;
gamedata->importprofilewins = false;
@ -720,6 +721,8 @@ void M_ClearSecrets(void)
skincolors[i].cache_spraycan = UINT16_MAX;
}
memset(gamedata->sealedswaps, 0, sizeof(gamedata->sealedswaps));
Z_Free(gamedata->challengegrid);
gamedata->challengegrid = NULL;
gamedata->challengegridwidth = 0;

View file

@ -291,6 +291,7 @@ typedef enum {
// This is the largest number of 9s that will fit in UINT32 and UINT16 respectively.
#define GDMAX_RINGS 999999999
#define GDMAX_CHAOKEYS 9999
#define GDMAX_SEALEDSWAPS 7
#define GDCONVERT_ROUNDSTOKEY 14
@ -371,12 +372,15 @@ struct gamedata_t
UINT32 totalrings;
UINT32 totaltumbletime;
// Chao Key condition bypass
// CHAO KEYS AND THEIR GENERATION
UINT32 pendingkeyrounds;
UINT8 pendingkeyroundoffset;
UINT16 keyspending;
UINT16 chaokeys;
// EMERALD REMAPPING
cupheader_t *sealedswaps[GDMAX_SEALEDSWAPS];
// SPECIFIC SPECIAL EVENTS
boolean everloadedaddon;
boolean everfinishedcredits;
@ -387,6 +391,7 @@ struct gamedata_t
boolean majorkeyskipattempted;
boolean enteredtutorialchallenge;
boolean finishedtutorialchallenge;
boolean sealedswapalerted;
gdmusic_t musicstate;
UINT8 gonerlevel;

View file

@ -335,6 +335,7 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
challengesmenu.requestnew = false;
challengesmenu.chaokeyadd = false;
challengesmenu.keywasadded = false;
challengesmenu.considersealedswapalert = false;
challengesmenu.chaokeyhold = 0;
challengesmenu.currentunlock = MAXUNLOCKABLES;
challengesmenu.unlockcondition = NULL;
@ -668,10 +669,15 @@ void M_ChallengesTick(void)
if (challengesmenu.currentunlock < MAXUNLOCKABLES
&& challengesmenu.unlockanim == UNLOCKTIME)
{
unlockable_t *ref = &unlockables[challengesmenu.currentunlock];
// Unlock animation... also tied directly to the actual unlock!
gamedata->unlocked[challengesmenu.currentunlock] = true;
M_UpdateUnlockablesAndExtraEmblems(true, true);
if (ref->type == SECRET_SPECIALATTACK)
challengesmenu.considersealedswapalert = true;
// Update shown description just in case..?
if (challengesmenu.unlockcondition)
Z_Free(challengesmenu.unlockcondition);
@ -682,12 +688,10 @@ void M_ChallengesTick(void)
if (challengesmenu.extradata)
{
unlockable_t *ref;
UINT16 bombcolor;
M_UpdateChallengeGridExtraData(challengesmenu.extradata);
ref = &unlockables[challengesmenu.currentunlock];
bombcolor = SKINCOLOR_NONE;
if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors)
@ -747,7 +751,14 @@ void M_ChallengesTick(void)
// Play music the moment control returns.
M_PlayMenuJam();
if (gamedata->chaokeytutorial == false
if (challengesmenu.considersealedswapalert == true
&& M_ConsiderSealedSwapAlert() == true)
{
// No keygen tutorial in this case...
// not ideal but at least unlikely to
// get at same time?? :V
}
else if (gamedata->chaokeytutorial == false
&& challengesmenu.keywasadded == true)
{
M_ChallengesTutorial(CCTUTORIAL_KEYGEN);

View file

@ -6280,6 +6280,14 @@ static boolean P_UnArchiveSPGame(savebuffer_t *save)
{
UINT32 val = READUINT32(save->p);
if (roundqueue.entries[i].rankrestricted && roundqueue.position != i+1)
{
// If this is a Sealed Star that hasn't yet been
// reached, don't be picky about divergance. Just
// use the base game without question. ~toast 010324
continue;
}
mapnum = roundqueue.entries[i].mapnum;
if (mapnum < nummapheaders && mapheaderinfo[mapnum] != NULL)
{

View file

@ -296,9 +296,29 @@ UINT8 P_GetNextEmerald(void)
{
cupheader_t *cup = NULL;
if (grandprixinfo.gp == true)
if (grandprixinfo.gp == true && grandprixinfo.cup)
{
cup = grandprixinfo.cup;
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| grandprixinfo.cup->id >= basenumkartcupheaders // custom content
|| M_SecretUnlocked(SECRET_SPECIALATTACK, false)) // true order
{
cup = grandprixinfo.cup;
}
else
{
// Determine order from sealedswaps.
UINT8 i;
for (i = 0; (i < GDMAX_SEALEDSWAPS && gamedata->sealedswaps[i]); i++)
{
if (gamedata->sealedswaps[i] != grandprixinfo.cup)
continue;
// Repeat visit, grab the same ID.
break;
}
return i+1;
}
}
if (cup == NULL)