// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by Vivian "toastergrl" Grannell. // Copyright (C) 2024 by Kart Krew. // Copyright (C) 2020 by Sonic Team Junior. // Copyright (C) 2016 by Kay "Kaito" Sinclaire. // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file m_cond.c /// \brief Challenges internals #include "m_cond.h" #include "m_random.h" // M_RandomKey #include "doomstat.h" #include "z_zone.h" #include "hu_stuff.h" // CEcho #include "v_video.h" // video flags #include "g_game.h" // record info #include "r_skins.h" // numskins #include "k_follower.h" #include "r_draw.h" // R_GetColorByName #include "s_sound.h" // S_StartSound #include "k_kart.h" // K_IsPLayerLosing #include "k_grandprix.h" // grandprixinfo #include "k_battle.h" // battleprisons #include "k_specialstage.h" // specialstageinfo #include "k_podium.h" #include "k_pwrlv.h" #include "k_profiles.h" gamedata_t *gamedata = NULL; boolean netUnlocked[MAXUNLOCKABLES]; // The meat of this system lies in condition sets conditionset_t conditionSets[MAXCONDITIONSETS]; // Emblem locations emblem_t emblemlocations[MAXEMBLEMS]; // Unlockables unlockable_t unlockables[MAXUNLOCKABLES]; // Highest used emblem ID INT32 numemblems = 0; // The challenge that will truly let the games begin. UINT16 gamestartchallenge = 600; // 601 // Create a new gamedata_t, for start-up void M_NewGameDataStruct(void) { gamedata = Z_Calloc(sizeof (gamedata_t), PU_STATIC, NULL); M_ClearSecrets(); G_ClearRecords(); } void M_PopulateChallengeGrid(void) { UINT16 i, j; UINT16 numunlocks = 0, nummajorunlocks = 0, numempty = 0; UINT16 selection[2][MAXUNLOCKABLES + (CHALLENGEGRIDHEIGHT-1)]; UINT16 majorcompact = 2; if (gamedata->challengegrid != NULL) { // todo tweak your grid if unlocks are changed return; } // Go through unlockables for (i = 0; i < MAXUNLOCKABLES; ++i) { if (!unlockables[i].conditionset) { continue; } if (unlockables[i].majorunlock) { selection[1][nummajorunlocks++] = i; //CONS_Printf(" found %d (LARGE)\n", selection[1][nummajorunlocks-1]); continue; } selection[0][numunlocks++] = i; //CONS_Printf(" found %d\n", selection[0][numunlocks-1]); } gamedata->challengegridwidth = 0; if (numunlocks + nummajorunlocks == 0) { return; } if (nummajorunlocks) { // Getting the number of 2-highs you can fit into two adjacent columns. UINT16 majorpad = (CHALLENGEGRIDHEIGHT/2); numempty = nummajorunlocks%majorpad; majorpad = (nummajorunlocks+(majorpad-1))/majorpad; gamedata->challengegridwidth = majorpad*2; numempty *= 4; #if (CHALLENGEGRIDHEIGHT % 2) // One extra empty per column. numempty += gamedata->challengegridwidth; #endif //CONS_Printf("%d major unlocks means width of %d, numempty of %d\n", nummajorunlocks, gamedata->challengegridwidth, numempty); } if (numunlocks > numempty) { // Getting the number of extra columns to store normal unlocks UINT16 temp = ((numunlocks - numempty) + (CHALLENGEGRIDHEIGHT-1))/CHALLENGEGRIDHEIGHT; gamedata->challengegridwidth += temp; majorcompact = 1; //CONS_Printf("%d normal unlocks means %d extra entries, additional width of %d\n", numunlocks, (numunlocks - numempty), temp); } else if (challengegridloops) { // Another case where offset large tiles are permitted. majorcompact = 1; } gamedata->challengegrid = Z_Malloc( (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT16)), PU_STATIC, NULL); if (!gamedata->challengegrid) { I_Error("M_PopulateChallengeGrid: was not able to allocate grid"); } for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); ++i) { gamedata->challengegrid[i] = MAXUNLOCKABLES; } // Attempt to place all large tiles first. if (nummajorunlocks) { // You lose one from CHALLENGEGRIDHEIGHT because it is impossible to place a 2-high tile on the bottom row. // You lose one from the width if it doesn't loop. // You divide by two if the grid is so compacted that large tiles can't be in offset columns. UINT16 numspots = (gamedata->challengegridwidth - (challengegridloops ? 0 : majorcompact)) * ((CHALLENGEGRIDHEIGHT-1) / majorcompact); // 0 is row, 1 is column INT16 *quickcheck = Z_Calloc(sizeof(INT16) * 2 * numspots, PU_STATIC, NULL); // Prepare the easy-grab spots. for (i = 0; i < numspots; i++) { quickcheck[i * 2 + 0] = i%(CHALLENGEGRIDHEIGHT-1); quickcheck[i * 2 + 1] = majorcompact * i/(CHALLENGEGRIDHEIGHT-1); } // Place in random valid locations. while (nummajorunlocks > 0 && numspots > 0) { INT16 row, col; j = M_RandomKey(numspots); row = quickcheck[j * 2 + 0]; col = quickcheck[j * 2 + 1]; // We always take from selection[1][] in order, but the PLACEMENT is still random. nummajorunlocks--; //CONS_Printf("--- %d (LARGE) placed at (%d, %d)\n", selection[1][nummajorunlocks], row, col); i = row + (col * CHALLENGEGRIDHEIGHT); gamedata->challengegrid[i] = gamedata->challengegrid[i+1] = selection[1][nummajorunlocks]; if (col == gamedata->challengegridwidth-1) { i = row; } else { i += CHALLENGEGRIDHEIGHT; } gamedata->challengegrid[i] = gamedata->challengegrid[i+1] = selection[1][nummajorunlocks]; if (nummajorunlocks == 0) { break; } for (i = 0; i < numspots; i++) { quickcheckagain: if (abs((quickcheck[i * 2 + 0]) - (row)) <= 1 // Row distance && (abs((quickcheck[i * 2 + 1]) - (col)) <= 1 // Column distance || (quickcheck[i * 2 + 1] == 0 && col == gamedata->challengegridwidth-1) // Wraparounds l->r || (quickcheck[i * 2 + 1] == gamedata->challengegridwidth-1 && col == 0))) // Wraparounds r->l { numspots--; // Remove from possible indicies if (i == numspots) break; // Shuffle remaining so we can keep on using M_RandomKey quickcheck[i * 2 + 0] = quickcheck[numspots * 2 + 0]; quickcheck[i * 2 + 1] = quickcheck[numspots * 2 + 1]; // Woah there - we've gotta check the one that just got put in our place. goto quickcheckagain; } continue; } } #if (CHALLENGEGRIDHEIGHT == 4) while (nummajorunlocks > 0) { UINT16 unlocktomoveup = MAXUNLOCKABLES; j = gamedata->challengegridwidth-1; // Attempt to fix our whoopsie. for (i = 0; i < j; i++) { if (gamedata->challengegrid[1 + (i*CHALLENGEGRIDHEIGHT)] != MAXUNLOCKABLES && gamedata->challengegrid[(i*CHALLENGEGRIDHEIGHT)] == MAXUNLOCKABLES) break; } if (i == j) { break; } unlocktomoveup = gamedata->challengegrid[1 + (i*CHALLENGEGRIDHEIGHT)]; if (i == 0 && challengegridloops && (gamedata->challengegrid [1 + (j*CHALLENGEGRIDHEIGHT)] == gamedata->challengegrid[1])) ; else { j = i + 1; } nummajorunlocks--; // Push one pair up. gamedata->challengegrid[(i*CHALLENGEGRIDHEIGHT)] = gamedata->challengegrid[(j*CHALLENGEGRIDHEIGHT)] = unlocktomoveup; // Wedge the remaining four underneath. gamedata->challengegrid[2 + (i*CHALLENGEGRIDHEIGHT)] = gamedata->challengegrid[2 + (j*CHALLENGEGRIDHEIGHT)] = selection[1][nummajorunlocks]; gamedata->challengegrid[3 + (i*CHALLENGEGRIDHEIGHT)] = gamedata->challengegrid[3 + (j*CHALLENGEGRIDHEIGHT)] = selection[1][nummajorunlocks]; } #endif if (nummajorunlocks > 0) { UINT16 widthtoprint = gamedata->challengegridwidth; Z_Free(gamedata->challengegrid); gamedata->challengegrid = NULL; I_Error("M_PopulateChallengeGrid: was not able to populate %d large tiles (width %d)", nummajorunlocks, widthtoprint); } } numempty = 0; // Space out empty entries to pepper into unlock list for (i = 0; i < gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; i++) { if (gamedata->challengegrid[i] < MAXUNLOCKABLES) { continue; } numempty++; } if (numunlocks > numempty) { gamedata->challengegridwidth = 0; Z_Free(gamedata->challengegrid); gamedata->challengegrid = NULL; I_Error("M_PopulateChallengeGrid: %d small unlocks vs %d empty spaces (%d gap)", numunlocks, numempty, (numunlocks-numempty)); } //CONS_Printf(" %d unlocks vs %d empty spaces\n", numunlocks, numempty); while (numunlocks < numempty) { //CONS_Printf(" adding empty)\n"); selection[0][numunlocks++] = MAXUNLOCKABLES; } // Fill the remaining spots with random ordinary unlocks (and empties). for (i = 0; i < gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; i++) { if (gamedata->challengegrid[i] < MAXUNLOCKABLES) { continue; } j = M_RandomKey(numunlocks); // Get an entry gamedata->challengegrid[i] = selection[0][j]; // Set that entry //CONS_Printf(" %d placed at (%d, %d)\n", selection[0][j], i/CHALLENGEGRIDHEIGHT, i%CHALLENGEGRIDHEIGHT); numunlocks--; // Remove from possible indicies selection[0][j] = selection[0][numunlocks]; // Shuffle remaining so we can keep on using M_RandomKey if (numunlocks == 0) { break; } } } void M_SanitiseChallengeGrid(void) { UINT8 seen[MAXUNLOCKABLES]; UINT16 empty[MAXUNLOCKABLES + (CHALLENGEGRIDHEIGHT-1)]; UINT16 i, j, numempty = 0; if (gamedata->challengegrid == NULL) return; memset(seen, 0, sizeof(seen)); // Go through all spots to identify duplicates and absences. for (j = 0; j < gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; j++) { i = gamedata->challengegrid[j]; if (i >= MAXUNLOCKABLES || !unlockables[i].conditionset) { empty[numempty++] = j; continue; } if (seen[i] != 5) // Arbitrary cap greater than 4 { seen[i]++; if (seen[i] == 1 || unlockables[i].majorunlock) { continue; } } empty[numempty++] = j; } // Go through unlockables to identify if any haven't been seen. for (i = 0; i < MAXUNLOCKABLES; ++i) { if (!unlockables[i].conditionset) { continue; } if (unlockables[i].majorunlock && seen[i] != 4) { // Probably not enough spots to retrofit. goto badgrid; } if (seen[i] != 0) { // Present on the challenge grid. continue; } if (numempty != 0) { // Small ones can be slotted in easy. j = empty[--numempty]; gamedata->challengegrid[j] = i; } // Nothing we can do to recover. goto badgrid; } // Fill the remaining spots with empties. while (numempty != 0) { j = empty[--numempty]; gamedata->challengegrid[j] = MAXUNLOCKABLES; } return; badgrid: // Just remove everything and let it get regenerated. Z_Free(gamedata->challengegrid); gamedata->challengegrid = NULL; gamedata->challengegridwidth = 0; } void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { UINT16 i, j, num, id, tempid, work; boolean idchange; if (gamedata->challengegrid == NULL) { return; } if (extradata == NULL) { return; } //CONS_Printf(" --- \n"); // Pre-wipe flags. for (i = 0; i < gamedata->challengegridwidth; i++) { for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) { id = (i * CHALLENGEGRIDHEIGHT) + j; num = gamedata->challengegrid[id]; if (num >= MAXUNLOCKABLES || unlockables[num].majorunlock == false || gamedata->unlocked[num] == true) { extradata[id].flags = CHE_NONE; continue; } // We only do this for locked large tiles, to reduce the // complexity of most standard tile challenge comparisons extradata[id].flags = CHE_ALLCLEAR; } } // Populate extra data. for (i = 0; i < gamedata->challengegridwidth; i++) { for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) { id = (i * CHALLENGEGRIDHEIGHT) + j; num = gamedata->challengegrid[id]; idchange = false; // Empty spots in the grid are always unconnected. if (num >= MAXUNLOCKABLES) { continue; } // Check the spot above. if (j > 0) { tempid = (i * CHALLENGEGRIDHEIGHT) + (j - 1); work = gamedata->challengegrid[tempid]; if (work == num) { extradata[id].flags = CHE_CONNECTEDUP; // Get the id to write extra hint data to. // This check is safe because extradata's order of population if (extradata[tempid].flags & CHE_CONNECTEDLEFT) { extradata[id].flags |= CHE_CONNECTEDLEFT; //CONS_Printf(" %d - %d above %d is invalid, check to left\n", num, tempid, id); if (i > 0) { tempid -= CHALLENGEGRIDHEIGHT; } else { tempid = ((gamedata->challengegridwidth - 1) * CHALLENGEGRIDHEIGHT) + j - 1; } } /*else CONS_Printf(" %d - %d above %d is valid\n", num, tempid, id);*/ id = tempid; idchange = true; if (extradata[id].flags == CHE_HINT) { // CHE_ALLCLEAR has already been removed, // and CHE_HINT has already been applied, // so nothing more needs to be done here. continue; } } else if (work < MAXUNLOCKABLES) { if (gamedata->unlocked[work] == true) { extradata[id].flags |= CHE_HINT; } else { extradata[id].flags &= ~CHE_ALLCLEAR; } } } // Check the spot to the left. { if (i > 0) { tempid = ((i - 1) * CHALLENGEGRIDHEIGHT) + j; } else { tempid = ((gamedata->challengegridwidth - 1) * CHALLENGEGRIDHEIGHT) + j; } work = gamedata->challengegrid[tempid]; if (work == num) { if (!idchange && (i > 0 || challengegridloops)) { //CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id); // If we haven't already updated our id, it's the one to our left. if (extradata[id].flags & CHE_HINT) { extradata[tempid].flags |= CHE_HINT; } if (!(extradata[id].flags & CHE_ALLCLEAR)) { extradata[tempid].flags &= ~CHE_ALLCLEAR; } extradata[id].flags = CHE_CONNECTEDLEFT; id = tempid; } /*else CONS_Printf(" %d - %d to left of %d is invalid\n", work, tempid, id);*/ } else if (work < MAXUNLOCKABLES) { if (gamedata->unlocked[work] == true) { extradata[id].flags |= CHE_HINT; } else { extradata[id].flags &= ~CHE_ALLCLEAR; } } } // Since we're not modifying id past this point, the conditions become much simpler. if (extradata[id].flags == CHE_HINT) { // CHE_ALLCLEAR has already been removed, // and CHE_HINT has already been applied, // so nothing more needs to be done here. continue; } // Check the spot below. if (j < CHALLENGEGRIDHEIGHT-1) { tempid = (i * CHALLENGEGRIDHEIGHT) + (j + 1); work = gamedata->challengegrid[tempid]; if (work == num) { ; } else if (work < MAXUNLOCKABLES) { if (gamedata->unlocked[work] == true) { extradata[id].flags |= CHE_HINT; } else { extradata[id].flags &= ~CHE_ALLCLEAR; } } } // Check the spot to the right. { if (i < (gamedata->challengegridwidth - 1)) { tempid = ((i + 1) * CHALLENGEGRIDHEIGHT) + j; } else { tempid = j; } work = gamedata->challengegrid[tempid]; if (work == num) { ; } else if (work < MAXUNLOCKABLES) { if (gamedata->unlocked[work] == true) { extradata[id].flags |= CHE_HINT; } else { extradata[id].flags &= ~CHE_ALLCLEAR; } } } } } } void M_AddRawCondition(UINT16 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2, char *stringvar) { condition_t *cond; UINT32 num, wnum; I_Assert(set < MAXCONDITIONSETS); wnum = conditionSets[set].numconditions; num = ++conditionSets[set].numconditions; conditionSets[set].condition = Z_Realloc(conditionSets[set].condition, sizeof(condition_t)*num, PU_STATIC, 0); cond = conditionSets[set].condition; cond[wnum].id = id; cond[wnum].type = c; cond[wnum].requirement = r; cond[wnum].extrainfo1 = x1; cond[wnum].extrainfo2 = x2; cond[wnum].stringvar = stringvar; } void M_ClearConditionSet(UINT16 set) { if (conditionSets[set].numconditions) { while (conditionSets[set].numconditions > 0) { --conditionSets[set].numconditions; Z_Free(conditionSets[set].condition[conditionSets[set].numconditions].stringvar); } Z_Free(conditionSets[set].condition); conditionSets[set].condition = NULL; } gamedata->achieved[set] = false; } // Clear ALL secrets. void M_ClearStats(void) { UINT16 i; gamedata->totalplaytime = 0; gamedata->totalnetgametime = 0; gamedata->timeattackingtotaltime = 0; gamedata->spbattackingtotaltime = 0; for (i = 0; i < GDGT_MAX; ++i) gamedata->modeplaytime[i] = 0; gamedata->totalmenutime = 0; gamedata->totaltimestaringatstatistics = 0; gamedata->totalrings = 0; gamedata->totaltumbletime = 0; for (i = 0; i < GDGT_MAX; ++i) gamedata->roundsplayed[i] = 0; gamedata->timesBeaten = 0; gamedata->everloadedaddon = false; gamedata->everfinishedcredits = false; gamedata->eversavedreplay = false; gamedata->everseenspecial = false; gamedata->evercrashed = false; gamedata->chaokeytutorial = false; gamedata->majorkeyskipattempted = false; gamedata->enteredtutorialchallenge = false; gamedata->finishedtutorialchallenge = false; gamedata->sealedswapalerted = false; gamedata->musicstate = GDMUSIC_NONE; gamedata->importprofilewins = false; // Skins only store stats, not progression metrics. Good to clear entirely here. for (i = 0; i < numskins; i++) { memset(&skins[i].records, 0, sizeof(skins[i].records)); } unloaded_skin_t *unloadedskin, *nextunloadedskin = NULL; for (unloadedskin = unloadedskins; unloadedskin; unloadedskin = nextunloadedskin) { nextunloadedskin = unloadedskin->next; Z_Free(unloadedskin); } unloadedskins = NULL; // We retain exclusively the most important stuff from maps. UINT8 restoremapvisited; recordtimes_t restoretimeattack; recordtimes_t restorespbattack; for (i = 0; i < nummapheaders; i++) { restoremapvisited = mapheaderinfo[i]->records.mapvisited; restoretimeattack = mapheaderinfo[i]->records.timeattack; restorespbattack = mapheaderinfo[i]->records.spbattack; memset(&mapheaderinfo[i]->records, 0, sizeof(recorddata_t)); mapheaderinfo[i]->records.mapvisited = restoremapvisited; mapheaderinfo[i]->records.timeattack = restoretimeattack; mapheaderinfo[i]->records.spbattack = restorespbattack; } unloaded_mapheader_t *unloadedmap; for (unloadedmap = unloadedmapheaders; unloadedmap; unloadedmap = unloadedmap->next) { restoremapvisited = unloadedmap->records.mapvisited; restoretimeattack = unloadedmap->records.timeattack; restorespbattack = unloadedmap->records.spbattack; memset(&unloadedmap->records, 0, sizeof(recorddata_t)); unloadedmap->records.mapvisited = restoremapvisited; unloadedmap->records.timeattack = restoretimeattack; unloadedmap->records.spbattack = restorespbattack; } } void M_ClearSecrets(void) { memset(gamedata->collected, 0, sizeof(gamedata->collected)); memset(gamedata->unlocked, 0, sizeof(gamedata->unlocked)); memset(gamedata->unlockpending, 0, sizeof(gamedata->unlockpending)); memset(netUnlocked, 0, sizeof(netUnlocked)); memset(gamedata->achieved, 0, sizeof(gamedata->achieved)); Z_Free(gamedata->spraycans); gamedata->spraycans = NULL; gamedata->numspraycans = 0; gamedata->gotspraycans = 0; Z_Free(gamedata->prisoneggpickups); gamedata->prisoneggpickups = NULL; gamedata->numprisoneggpickups = 0; gamedata->thisprisoneggpickup = MAXCONDITIONSETS; gamedata->thisprisoneggpickup_cached = NULL; gamedata->thisprisoneggpickupgrabbed = false; UINT16 i, j; for (i = 0; i < nummapheaders; i++) { if (!mapheaderinfo[i]) continue; mapheaderinfo[i]->records.mapvisited = 0; mapheaderinfo[i]->cache_spraycan = UINT16_MAX; mapheaderinfo[i]->cache_maplock = MAXUNLOCKABLES; for (j = 1; j < mapheaderinfo[i]->musname_size; j++) { mapheaderinfo[i]->cache_muslock[j-1] = MAXUNLOCKABLES; } } cupheader_t *cup; for (cup = kartcupheaders; cup; cup = cup->next) { cup->cache_cuplock = MAXUNLOCKABLES; } for (i = 0; i < numskincolors; i++) { skincolors[i].cache_spraycan = UINT16_MAX; } memset(gamedata->sealedswaps, 0, sizeof(gamedata->sealedswaps)); Z_Free(gamedata->challengegrid); gamedata->challengegrid = NULL; gamedata->challengegridwidth = 0; gamedata->pendingkeyrounds = 0; gamedata->pendingkeyroundoffset = 0; gamedata->keyspending = 0; gamedata->chaokeys = GDINIT_CHAOKEYS; gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; gamedata->gonerlevel = GDGONER_INIT; } // For lack of a better idea on where to put this static void M_Shuffle_UINT16(UINT16 *list, size_t len) { size_t i; UINT16 temp; while (len > 1) { i = M_RandomKey(len); if (i == --len) continue; temp = list[i]; list[i] = list[len]; list[len] = temp; } } static void M_AssignSpraycans(void) { // Very convenient I'm programming this on // the release date of "Bomb Rush Cyberfunk". // ~toast 180823 (committed a day later) // Init ordered list of skincolors UINT16 tempcanlist[MAXSKINCOLORS]; UINT16 listlen = 0, prependlen = 0; UINT32 i, j; conditionset_t *c; condition_t *cn; const UINT16 prependoffset = MAXSKINCOLORS-1; // None of the following accounts for cans being removed, only added... for (i = 0; i < MAXCONDITIONSETS; ++i) { c = &conditionSets[i]; if (!c->numconditions) continue; for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->type != UC_SPRAYCAN) continue; // G_LoadGamedata, G_SaveGameData doesn't support custom skincolors right now. if (cn->requirement >= SKINCOLOR_FIRSTFREESLOT) //numskincolors) continue; if (skincolors[cn->requirement].cache_spraycan != UINT16_MAX) continue; // Still invalid, just in case it isn't assigned one later skincolors[cn->requirement].cache_spraycan = UINT16_MAX-1; if (!cn->extrainfo1) { //CONS_Printf("DDD - Adding standard can color %d\n", cn->requirement); tempcanlist[listlen] = cn->requirement; listlen++; continue; } //CONS_Printf("DDD - Prepending early can color %d\n", cn->requirement); tempcanlist[prependoffset - prependlen] = cn->requirement; prependlen++; } } if (listlen) { // Swap the standard colours for random order M_Shuffle_UINT16(tempcanlist, listlen); } else if (!prependlen) { return; } if (prependlen) { // Swap the early colours for random order M_Shuffle_UINT16(tempcanlist + prependoffset - (prependlen - 1), prependlen); // Put at the front of the main list // (technically messes with the main order, but it // was LITERALLY just shuffled so it doesn't matter) i = 0; while (i < prependlen) { tempcanlist[listlen] = tempcanlist[i]; tempcanlist[i] = tempcanlist[prependoffset - i]; listlen++; i++; } } gamedata->spraycans = Z_Realloc( gamedata->spraycans, sizeof(candata_t) * (gamedata->numspraycans + listlen), PU_STATIC, NULL); for (i = 0; i < listlen; i++) { gamedata->spraycans[gamedata->numspraycans].map = NEXTMAP_INVALID; gamedata->spraycans[gamedata->numspraycans].col = tempcanlist[i]; skincolors[tempcanlist[i]].cache_spraycan = gamedata->numspraycans; gamedata->numspraycans++; } } static void M_InitPrisonEggPickups(void) { // Init ordered list of skincolors UINT16 temppickups[MAXCONDITIONSETS]; UINT16 listlen = 0; UINT32 i, j; conditionset_t *c; condition_t *cn; for (i = 0; i < MAXCONDITIONSETS; ++i) { // Optimisation - unlike Spray Cans, these are rebuilt every game launch/savedata wipe. // Therefore, we don't need to re-store the ones that have been achieved. if (gamedata->achieved[i]) continue; c = &conditionSets[i]; if (!c->numconditions) continue; for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->type != UC_PRISONEGGCD) continue; temppickups[listlen] = i; listlen++; break; } } if (!listlen) { return; } // This list doesn't need to be shuffled because it's always being randomly grabbed. // (Unlike Spray Cans, you don't know which CD you miss out on.) gamedata->prisoneggpickups = Z_Realloc( gamedata->prisoneggpickups, sizeof(UINT16) * listlen, PU_STATIC, NULL); while (gamedata->numprisoneggpickups < listlen) { gamedata->prisoneggpickups[gamedata->numprisoneggpickups] = temppickups[gamedata->numprisoneggpickups]; gamedata->numprisoneggpickups++; } M_UpdateNextPrisonEggPickup(); } void M_UpdateNextPrisonEggPickup(void) { UINT16 i, j, swap; conditionset_t *c; condition_t *cn; #ifdef DEVELOP extern consvar_t cv_debugprisoncd; #endif cacheprisoneggpickup: // Check if the current roll is fine gamedata->thisprisoneggpickup_cached = NULL; if (gamedata->thisprisoneggpickup < MAXCONDITIONSETS) { #ifdef DEVELOP if (cv_debugprisoncd.value) CONS_Printf("CACHE TEST: thisprisoneggpickup is set to %u\n", gamedata->thisprisoneggpickup); #endif if (gamedata->achieved[gamedata->thisprisoneggpickup] == false) { c = &conditionSets[gamedata->thisprisoneggpickup]; if (c->numconditions) { for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->type != UC_PRISONEGGCD) continue; if (cn->requirement < nummapheaders && M_MapLocked(cn->requirement+1)) continue; // Good! Attach the cache. gamedata->thisprisoneggpickup_cached = cn; #ifdef DEVELOP if (cv_debugprisoncd.value) CONS_Printf(" successfully set to cn!\n"); #endif break; } } } if (gamedata->thisprisoneggpickup_cached == NULL) { gamedata->thisprisoneggpickup = MAXCONDITIONSETS; gamedata->thisprisoneggpickupgrabbed = false; } } if (gamedata->numprisoneggpickups && gamedata->thisprisoneggpickup >= MAXCONDITIONSETS) { #ifdef DEVELOP if (cv_debugprisoncd.value) CONS_Printf(" Invalid thisprisoneggpickup, rolling a random one...\n"); #endif UINT16 gettableprisoneggpickups = 0; for (i = 0; i < gamedata->numprisoneggpickups; i++) { if (gamedata->achieved[gamedata->prisoneggpickups[i]] == false) { c = &conditionSets[gamedata->prisoneggpickups[i]]; if (c->numconditions) { for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->type != UC_PRISONEGGCD) continue; // Locked associated map? Keep in the rear end dimension! if (cn->requirement < nummapheaders && M_MapLocked(cn->requirement+1)) break; // not continue intentionally // Okay, this should be available. // Bring to the front! if (i != gettableprisoneggpickups) { swap = gamedata->prisoneggpickups[gettableprisoneggpickups]; gamedata->prisoneggpickups[gettableprisoneggpickups] = gamedata->prisoneggpickups[i]; gamedata->prisoneggpickups[i] = swap; } gettableprisoneggpickups++; break; } if (j < c->numconditions) continue; } } } if (gettableprisoneggpickups != 0) { gamedata->thisprisoneggpickup = gamedata->prisoneggpickups[ M_RandomKey(gettableprisoneggpickups) ]; #ifdef DEVELOP if (cv_debugprisoncd.value) CONS_Printf(" Selected %u, trying again...\n", gamedata->thisprisoneggpickup); #endif goto cacheprisoneggpickup; } } #ifdef DEVELOP if (cv_debugprisoncd.value) CONS_Printf("thisprisoneggpickup = %u (MAXCONDITIONSETS is %u)\n", gamedata->thisprisoneggpickup, MAXCONDITIONSETS); #if 0 // If all drops are collected, just force the first valid one. // THIS DOESN'T ACTUALLY WORK IF ALL PRISON PRIZES HAVE BEEN REDEEMED AND THE GAME IS RELAUNCHED, so it is not reliable enough to expose as a debugging tool if (cv_debugprisoncd.value && gamedata->thisprisoneggpickup_cached == NULL) { for (i = 0; gamedata->thisprisoneggpickup_cached == NULL && i < gamedata->numprisoneggpickups; i++) { c = &conditionSets[gamedata->prisoneggpickups[i]]; if (c->numconditions) { for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->type != UC_PRISONEGGCD) continue; gamedata->thisprisoneggpickup = gamedata->prisoneggpickups[i]; gamedata->thisprisoneggpickup_cached = cn; break; } } } } #endif #endif // DEVELOP } static void M_PrecacheLevelLocks(void) { UINT16 i, j; for (i = 0; i < MAXUNLOCKABLES; ++i) { switch (unlockables[i].type) { // SECRET_SKIN, SECRET_COLOR, SECRET_FOLLOWER are instantiated too late to use case SECRET_MAP: { UINT16 map = M_UnlockableMapNum(&unlockables[i]); if (map < nummapheaders && mapheaderinfo[map]) { if (mapheaderinfo[map]->cache_maplock != MAXUNLOCKABLES) CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_MAPs associated with Level %s\n", i+1, mapheaderinfo[map]->lumpname); mapheaderinfo[map]->cache_maplock = i; } break; } case SECRET_ALTMUSIC: { UINT16 map = M_UnlockableMapNum(&unlockables[i]); const char *tempstr = NULL; if (map < nummapheaders && mapheaderinfo[map]) { UINT8 greatersize = max(mapheaderinfo[map]->musname_size, mapheaderinfo[map]->encoremusname_size); for (j = 1; j < greatersize; j++) { if (mapheaderinfo[map]->cache_muslock[j - 1] != MAXUNLOCKABLES) { continue; } mapheaderinfo[map]->cache_muslock[j - 1] = i; UINT8 positionid = 0; if (mapheaderinfo[map]->cup) { for (positionid = 0; positionid < CUPCACHE_PODIUM; positionid++) { if (mapheaderinfo[map]->cup->cachedlevels[positionid] != map) continue; break; } if (positionid < CUPCACHE_PODIUM) { char prefix = 'R'; if (positionid >= CUPCACHE_BONUS) { positionid -= (CUPCACHE_BONUS); prefix = 'B'; } tempstr = va( "Music: %s CUP %c%u %c", mapheaderinfo[map]->cup->realname, prefix, positionid + 1, 'A' + j // :D ? ); } } if (tempstr == NULL) { UINT16 mapcheck; for (mapcheck = 0; mapcheck < map; mapcheck++) { if (!mapheaderinfo[mapcheck] || mapheaderinfo[mapcheck]->cup != NULL) continue; if (mapheaderinfo[mapcheck]->menuflags & LF2_HIDEINMENU) continue; if (((mapheaderinfo[mapcheck]->typeoflevel & TOL_TUTORIAL) == TOL_TUTORIAL) != ((mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) == TOL_TUTORIAL)) continue; // We don't check for locked, because the levels exist positionid++; } tempstr = va( "Music: %s #%u %c", (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) ? "Tutorial" : "Lost & Found", positionid + 1, 'A' + j // :D ? ); } break; } if (j == greatersize) CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_ALTMUSICs associated with Level %s\n", i+1, mapheaderinfo[map]->lumpname); } else { CONS_Alert(CONS_ERROR, "Unlockable %u: Invalid levelname %s for SECRET_ALTMUSIC\n", i+1, unlockables[i].stringVar); } if (tempstr == NULL) { tempstr = va("INVALID MUSIC UNLOCK %u", i+1); } strlcpy(unlockables[i].name, tempstr, sizeof (unlockables[i].name)); break; } case SECRET_CUP: { cupheader_t *cup = M_UnlockableCup(&unlockables[i]); if (cup) { if (cup->cache_cuplock != MAXUNLOCKABLES) CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_CUPs associated with Cup %s\n", i+1, cup->name); cup->cache_cuplock = i; break; } break; } default: break; } } } void M_FinaliseGameData(void) { //M_PopulateChallengeGrid(); -- This can be done lazily when we actually need it // Precache as many unlockables as is meaningfully feasible M_PrecacheLevelLocks(); // Place the spraycans, which CAN'T be done lazily. M_AssignSpraycans(); // You could probably do the Prison Egg Pickups lazily, but it'd be a lagspike mid-combat. M_InitPrisonEggPickups(); // Don't consider loaded until it's a success! // It used to do this much earlier, but this would cause the gamedata // to save over itself when it I_Errors from corruption, which can // accidentally delete players' legitimate data if the code ever has // any tiny mistakes! gamedata->loaded = true; // Silent update unlockables in case they're out of sync with conditions M_UpdateUnlockablesAndExtraEmblems(false, true); } void M_SetNetUnlocked(void) { UINT16 i; // Use your gamedata as baseline for (i = 0; i < MAXUNLOCKABLES; i++) { netUnlocked[i] = gamedata->unlocked[i]; } if (!dedicated) { return; } // Dedicated spoiler password - tournament mode equivalent. if (usedTourney) { for (i = 0; i < MAXUNLOCKABLES; i++) { if (unlockables[i].conditionset == CH_FURYBIKE) continue; netUnlocked[i] = true; } return; } // Okay, now it's dedicated first-week spoilerless behaviour. for (i = 0; i < MAXUNLOCKABLES; i++) { if (netUnlocked[i]) continue; switch (unlockables[i].type) { case SECRET_CUP: { // Give the first seven Cups for free. cupheader_t *cup = M_UnlockableCup(&unlockables[i]); if (cup && cup->id < 7) netUnlocked[i] = true; break; } case SECRET_ADDONS: { netUnlocked[i] = true; break; } default: { // Most stuff isn't given to dedis for free break; } } } } // ---------------------- // Condition set checking // ---------------------- void M_UpdateConditionSetsPending(void) { UINT32 i, j, k; conditionset_t *c; condition_t *cn; for (i = 0; i < MAXCONDITIONSETS; ++i) { c = &conditionSets[i]; if (!c->numconditions) continue; for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->stringvar == NULL) continue; switch (cn->type) { case UC_CHARACTERWINS: case UCRP_ISCHARACTER: case UCRP_MAKERETIRE: { cn->requirement = R_SkinAvailableEx(cn->stringvar, false); if (cn->requirement < 0) { CONS_Alert(CONS_WARNING, "UC TYPE %u: Invalid character %s for condition ID %d", cn->type, cn->stringvar, cn->id+1); continue; } Z_Free(cn->stringvar); cn->stringvar = NULL; break; } case UCRP_HASFOLLOWER: { // match deh_soc readfollower() for (k = 0; cn->stringvar[k]; k++) { if (cn->stringvar[k] == '_') cn->stringvar[k] = ' '; } cn->requirement = K_FollowerAvailable(cn->stringvar); if (cn->requirement < 0) { CONS_Alert(CONS_WARNING, "UC TYPE %u: Invalid character %s for condition ID %d", cn->type, cn->stringvar, cn->id+1); continue; } Z_Free(cn->stringvar); cn->stringvar = NULL; break; } case UCRP_WETPLAYER: { if (cn->extrainfo1) { char *l; for (l = cn->stringvar; *l != '\0'; l++) { *l = tolower(*l); } cn->extrainfo1 = 0; } break; } default: break; } } } } boolean M_NotFreePlay(void) { UINT8 i; UINT8 nump = 0; if (K_CanChangeRules(true) == false) { // Rounds with direction are never FREE PLAY. return true; } if (battleprisons) { // Prison Break is battle's FREE PLAY. return false; } for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] == false || players[i].spectator == true) { continue; } nump++; if (nump > 1) { return true; } } return false; } UINT16 M_CheckCupEmeralds(UINT8 difficulty) { if (difficulty == 0) return 0; if (difficulty >= KARTGP_MAX) difficulty = KARTGP_MASTER; cupheader_t *cup; UINT16 ret = 0, seen = 0; for (cup = kartcupheaders; cup; cup = cup->next) { // Don't use custom material if (cup->id >= basenumkartcupheaders) break; // Does it not *have* an emerald? if (cup->emeraldnum == 0 || cup->emeraldnum > 14) continue; UINT16 emerald = 1<<(cup->emeraldnum-1); // Only count the first reference. if (seen & emerald) continue; // We've seen it, prevent future repetitions. seen |= emerald; // Did you actually get it? if (cup->windata[difficulty].got_emerald == false) continue; // Wa hoo ! ret |= emerald; } return ret; } // See also M_GetConditionString boolean M_CheckCondition(condition_t *cn, player_t *player) { switch (cn->type) { case UC_NONE: return false; case UC_PLAYTIME: // Requires total playing time >= x return (gamedata->totalplaytime >= (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_TOTALTUMBLETIME: // Requires total tumbling time >= x return (gamedata->totaltumbletime >= (unsigned)cn->requirement); case UC_GAMECLEAR: // Requires game beaten >= x times return (gamedata->timesBeaten >= (unsigned)cn->requirement); case UC_OVERALLTIME: // Requires overall time <= x return (M_GotLowEnoughTime(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 case UC_MAPSPBATTACK: // Requires map x to be beaten in SPB Attack case UC_MAPMYSTICMELODY: // Mystic Melody on map x's Ancient Shrine { UINT8 mvtype = MV_VISITED; if (cn->type == UC_MAPBEATEN) mvtype = MV_BEATEN; else if (cn->type == UC_MAPENCORE) mvtype = MV_ENCORE; else if (cn->type == UC_MAPSPBATTACK) mvtype = MV_SPBATTACK; else if (cn->type == UC_MAPMYSTICMELODY) mvtype = MV_MYSTICMELODY; return ((cn->requirement < nummapheaders) && (mapheaderinfo[cn->requirement]) && ((mapheaderinfo[cn->requirement]->records.mapvisited & mvtype) == mvtype)); } case UC_MAPTIME: // Requires time on map <= x return (G_GetBestTime(cn->extrainfo1) <= (unsigned)cn->requirement); case UC_CHARACTERWINS: if (cn->requirement < 0) return false; return (skins[cn->requirement].records.wins >= (UINT32)cn->extrainfo1); case UC_ALLCUPRECORDS: { if (gamestate == GS_LEVEL) return false; // this one could be laggy with many cups available INT32 requiredid = cn->requirement; if (requiredid == -1) // stop at all basegame cup requiredid = basenumkartcupheaders; UINT8 difficulty = cn->extrainfo2; if (difficulty > KARTGP_MASTER) difficulty = KARTGP_MASTER; cupheader_t *cup; for (cup = kartcupheaders; cup; cup = cup->next) { // Ok, achieved up to the desired cup. if (cup->id == requiredid) return true; cupwindata_t *windata = &cup->windata[difficulty]; // Did you actually get it? if (windata->best_placement == 0) return false; // Sufficient placement? if (cn->extrainfo1 && windata->best_placement > cn->extrainfo1) return false; } // If we ended up here, check we were looking for all cups achieved. return (requiredid == basenumkartcupheaders); } case UC_ALLCHAOS: case UC_ALLSUPER: case UC_ALLEMERALDS: { UINT16 ret = 0; if (gamestate == GS_LEVEL) return false; // this one could be laggy with many cups available ret = M_CheckCupEmeralds(cn->requirement); if (cn->type == UC_ALLCHAOS) return ALLCHAOSEMERALDS(ret); if (cn->type == UC_ALLSUPER) return ALLSUPEREMERALDS(ret); return ALLEMERALDS(ret); } case UC_TOTALMEDALS: // Requires number of emblems >= x return (M_GotEnoughMedals(cn->requirement)); case UC_EMBLEM: // Requires emblem x to be obtained return gamedata->collected[cn->requirement-1]; case UC_UNLOCKABLE: // Requires unlockable x to be obtained return gamedata->unlocked[cn->requirement-1]; case UC_CONDITIONSET: // requires condition set x to already be achieved return M_Achieved(cn->requirement-1); case UC_UNLOCKPERCENT: { // Don't let netgame sessions intefere // or have this give a performance hit // (This is formulated this way to // perfectly eclipse M_CheckNetUnlockByID) if (netgame || demo.playback || Playing()) return false; UINT16 i, unlocked = cn->extrainfo2, total = 0; // Special case for maps if (cn->extrainfo1 == SECRET_MAP) { for (i = 0; i < basenummapheaders; i++) { if (!mapheaderinfo[i] || mapheaderinfo[i]->menuflags & (LF2_HIDEINMENU)) continue; total++; // Check for completion if ((mapheaderinfo[i]->menuflags & LF2_FINISHNEEDED) && !(mapheaderinfo[i]->records.mapvisited & MV_BEATEN)) continue; // Check for unlock if (M_MapLocked(i+1)) continue; unlocked++; } } // Special case for SECRET_COLOR else if (cn->extrainfo1 == SECRET_COLOR) { total = gamedata->numspraycans; unlocked = gamedata->gotspraycans; } // Special case for raw Challenge count else if (cn->extrainfo1 == SECRET_NONE) { for (i = 0; i < MAXUNLOCKABLES; i++) { if (unlockables[i].type == SECRET_NONE) continue; total++; if (M_Achieved(unlockables[i].conditionset - 1) == false) continue; unlocked++; } } else { for (i = 0; i < MAXUNLOCKABLES; i++) { if (unlockables[i].type != cn->extrainfo1) continue; total++; if (gamedata->unlocked[i] == false) continue; unlocked++; } } if (!total) return false; // No need to do a pesky divide return ((100 * unlocked) >= (total * cn->requirement)); } case UC_ADDON: return ((gamedata->everloadedaddon == true) && M_SecretUnlocked(SECRET_ADDONS, true)); case UC_CREDITS: return (gamedata->everfinishedcredits == true); case UC_REPLAY: return (gamedata->eversavedreplay == true); case UC_CRASH: if (gamedata->evercrashed) { if (gamedata->musicstate < GDMUSIC_LOSERCLUB) gamedata->musicstate = GDMUSIC_LOSERCLUB; return true; } return false; case UC_TUTORIALSKIP: return (gamedata->finishedtutorialchallenge == true); case UC_PASSWORD: return (cn->stringvar == NULL); case UC_SPRAYCAN: { if (cn->requirement <= 0 || cn->requirement >= numskincolors) return false; UINT16 can_id = skincolors[cn->requirement].cache_spraycan; if (can_id >= gamedata->numspraycans) return false; return (gamedata->spraycans[can_id].map < nummapheaders); } case UC_PRISONEGGCD: return ((gamedata->thisprisoneggpickupgrabbed == true) && (cn == gamedata->thisprisoneggpickup_cached)); // Just for string building case UC_AND: case UC_THEN: case UC_COMMA: case UC_DESCRIPTIONOVERRIDE: return true; case UCRP_PREFIX_GRANDPRIX: return (grandprixinfo.gp == true); case UCRP_PREFIX_BONUSROUND: return ((grandprixinfo.gp == true) && (grandprixinfo.eventmode == GPEVENT_BONUS)); case UCRP_PREFIX_TIMEATTACK: return (modeattacking != ATTACKING_NONE); case UCRP_PREFIX_PRISONBREAK: return ((gametyperules & GTR_PRISONS) && battleprisons); case UCRP_PREFIX_SEALEDSTAR: return (specialstageinfo.valid == true); case UCRP_PREFIX_ISMAP: case UCRP_ISMAP: return (gamemap == cn->requirement+1); case UCRP_ISCHARACTER: return ( player->roundconditions.switched_skin == false && player->skin == cn->requirement ); case UCRP_ISENGINECLASS: return (player->roundconditions.switched_skin == false && player->skin < numskins && R_GetEngineClass( skins[player->skin].kartspeed, skins[player->skin].kartweight, skins[player->skin].flags ) == (unsigned)cn->requirement); case UCRP_HASFOLLOWER: return (cn->requirement != -1 && player->followerskin == cn->requirement); case UCRP_ISDIFFICULTY: if (grandprixinfo.gp == false) return false; if (cn->requirement == KARTGP_MASTER) return (grandprixinfo.masterbots == true); return (grandprixinfo.gamespeed >= cn->requirement); case UCRP_ISGEAR: return (gamespeed >= cn->requirement); case UCRP_PODIUMCUP: if (grandprixinfo.gp == false || K_PodiumSequence() == false) return false; if (grandprixinfo.cup == NULL || ( cn->requirement != -1 // Any && grandprixinfo.cup->id != cn->requirement ) ) return false; if (cn->extrainfo2 != 0) return (K_PodiumGrade() >= cn->extrainfo1); if (cn->extrainfo1 != 0) return (player->position != 0 && player->position <= cn->extrainfo1); return true; case UCRP_PODIUMEMERALD: case UCRP_PODIUMPRIZE: return (grandprixinfo.gp == true && K_PodiumSequence() == true && grandprixinfo.rank.specialWon == true); case UCRP_PODIUMNOCONTINUES: return (grandprixinfo.gp == true && K_PodiumSequence() == true && grandprixinfo.rank.continuesUsed == 0); case UCRP_FINISHCOOL: return (player->exiting && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && !K_IsPlayerLosing(player)); case UCRP_FINISHPERFECT: return (player->exiting && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && (gamespeed != KARTSPEED_EASY) && (player->tally.active == true) && (player->tally.totalLaps > 0) // Only true if not Time Attack && (player->tally.laps >= player->tally.totalLaps)); case UCRP_FINISHALLPRISONS: return (battleprisons && !(player->pflags & PF_NOCONTEST) //&& M_NotFreePlay() && numtargets >= maptargets); case UCRP_SURVIVE: return (player->exiting && !(player->pflags & PF_NOCONTEST)); case UCRP_NOCONTEST: return (player->pflags & PF_NOCONTEST); case UCRP_SMASHUFO: return ( specialstageinfo.valid == true && ( P_MobjWasRemoved(specialstageinfo.ufo) || specialstageinfo.ufo->health <= 1 ) ); case UCRP_CHASEDBYSPB: // The PERFECT implementation would check spbplace, iterate over trackercap, etc. // But the game already has this handy-dandy SPB signal for us... // It's only MAYBE invalid in modded context. And mods can already cheat... return ((player->pflags & PF_RINGLOCK) == PF_RINGLOCK); case UCRP_MAPDESTROYOBJECTS: return ( gamemap == cn->requirement+1 && numchallengedestructibles == UINT16_MAX ); case UCRP_MAKERETIRE: { // You can't "make" someone retire in coop. if (K_Cooperative() == true) { return false; } // The following is basically UCRP_FINISHCOOL, // but without the M_NotFreePlay check since this // condition is already dependent on other players. if ((player->exiting && !(player->pflags & PF_NOCONTEST) && !K_IsPlayerLosing(player)) == false) { return false; } UINT8 i; for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] == false) { continue; } // This player is ME! if (player == players+i) { continue; } // This player didn't NO CONTEST. if (!(players[i].pflags & PF_NOCONTEST)) { continue; } // This player doesn't have the right skin. if (players[i].skin != cn->requirement) { continue; } // Okay, the right player is dead! break; } return (i != MAXPLAYERS); } case UCRP_FINISHPLACE: return (player->exiting && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && player->position != 0 && player->position <= cn->requirement); case UCRP_FINISHPLACEEXACT: return (player->exiting && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && player->position == cn->requirement); case UCRP_FINISHGRADE: return (player->exiting && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && (player->tally.active == true) && (player->tally.state >= TALLY_ST_GRADE_APPEAR) && (player->tally.state <= TALLY_ST_DONE) && (player->tally.rank >= cn->requirement)); case UCRP_FINISHTIME: return (player->exiting && !(player->pflags & PF_NOCONTEST) && (!battleprisons || numtargets >= maptargets) //&& M_NotFreePlay() && player->realtime <= (unsigned)cn->requirement); case UCRP_FINISHTIMEEXACT: return (player->exiting && !(player->pflags & PF_NOCONTEST) && (!battleprisons || numtargets >= maptargets) //&& M_NotFreePlay() && player->realtime/TICRATE == (unsigned)cn->requirement/TICRATE); case UCRP_FINISHTIMELEFT: return (timelimitintics && player->exiting && !(player->pflags & PF_NOCONTEST) && (!battleprisons || numtargets >= maptargets) && !K_CanChangeRules(false) // too easy to change cv_timelimit && player->realtime < timelimitintics && (timelimitintics + extratimeintics + secretextratime - player->realtime) >= (unsigned)cn->requirement); case UCRP_RINGS: return (player->hudrings >= cn->requirement); case UCRP_RINGSEXACT: return (player->hudrings == cn->requirement); case UCRP_SPEEDOMETER: return (player->roundconditions.maxspeed >= cn->requirement); case UCRP_DRAFTDURATION: return (player->roundconditions.continuousdraft_best >= ((tic_t)cn->requirement)*TICRATE); case UCRP_GROWCONSECUTIVEBEAMS: return (player->roundconditions.best_consecutive_grow_lasers >= cn->requirement); case UCRP_TRIGGER: // requires map trigger set return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); case UCRP_FALLOFF: return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) && player->roundconditions.fell_off == (cn->requirement == 1)); case UCRP_TOUCHOFFROAD: return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) && player->roundconditions.touched_offroad == (cn->requirement == 1)); case UCRP_TOUCHSNEAKERPANEL: return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) && player->roundconditions.touched_sneakerpanel == (cn->requirement == 1)); case UCRP_RINGDEBT: return (!(gametyperules & GTR_SPHERES) && (cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) && (player->roundconditions.debt_rings == (cn->requirement == 1))); case UCRP_FAULTED: return ((cn->requirement == 1 || player->latestlap >= 1) && (player->roundconditions.faulted == (cn->requirement == 1))); case UCRP_TRIPWIREHYUU: return (player->roundconditions.tripwire_hyuu); case UCRP_WHIPHYUU: return (player->roundconditions.whip_hyuu); case UCRP_SPBNEUTER: return (player->roundconditions.spb_neuter); case UCRP_LANDMINEDUNK: return (player->roundconditions.landmine_dunk); case UCRP_HITMIDAIR: return (player->roundconditions.hit_midair); case UCRP_HITDRAFTERLOOKBACK: return (player->roundconditions.hit_drafter_lookback); case UCRP_GIANTRACERSHRUNKENORBI: return (player->roundconditions.giant_foe_shrunken_orbi); case UCRP_RETURNMARKTOSENDER: return (player->roundconditions.returntosender_mark); case UCRP_TRACKHAZARD: { if (!(gametyperules & GTR_CIRCUIT)) { // Prison Break/Versus if (!player->exiting && cn->requirement == 0) return false; return (((player->roundconditions.hittrackhazard[0] & 1) == 1) == (cn->requirement == 1)); } INT16 requiredlap = cn->extrainfo1; if (requiredlap < 0) { // Prevents lowered numlaps from activating it // (this also handles exiting, for all-laps situations) requiredlap = max(mapheaderinfo[gamemap-1]->numlaps, numlaps); } // cn->requirement is used as an offset here // so if you need to get hit on lap x, the // condition can fire while that lap is active // but if you need to NOT get hit on lap X, // it only fires once the lap is complete if (player->latestlap <= (requiredlap - cn->requirement)) return false; UINT8 requiredbit = 1<<(requiredlap & 7); requiredlap /= 8; if (cn->extrainfo1 == -1) { if (cn->requirement == 0) { // The "don't get hit on any lap" check is trivial. for (; requiredlap > 0; requiredlap--) { if (player->roundconditions.hittrackhazard[requiredlap] != 0) return false; } return (player->roundconditions.hittrackhazard[0] == 0); } // The following is my attempt at a major optimisation. // The naive version was MAX_LAP bools, which is ridiculous. // Check the highest relevant byte for all necessary bits. // We only do this if an == 0xFF/0xFE check wouldn't satisfy. if (requiredbit != (1<<7)) { // Last bit MAYBE not needed, POSITION doesn't count. const UINT8 finalbit = (requiredlap == 0) ? 1 : 0; while (requiredbit != finalbit) { if (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit)) return false; requiredbit /= 2; } if (requiredlap == 0) return true; requiredlap--; } // All bytes between the top and the bottom need to be checked for saturation. for (; requiredlap > 0; requiredlap--) { if (player->roundconditions.hittrackhazard[requiredlap] != 0xFF) return false; } // Last bit not needed, POSITION doesn't count. return (player->roundconditions.hittrackhazard[0] == 0xFE); } return (((player->roundconditions.hittrackhazard[requiredlap] & requiredbit) == requiredbit) == (cn->requirement == 1)); } case UCRP_TARGETATTACKMETHOD: return (player->roundconditions.targetdamaging == (targetdamaging_t)cn->requirement); case UCRP_GACHABOMMISER: return ( player->roundconditions.targetdamaging == UFOD_GACHABOM && player->roundconditions.gachabom_miser != 0xFF ); case UCRP_WETPLAYER: return (((player->roundconditions.wet_player & cn->requirement) == 0) && !player->roundconditions.fell_off); // Levels with water tend to texture their pits as water too } return false; } static boolean M_CheckConditionSet(conditionset_t *c, player_t *player) { UINT32 i; UINT32 lastID = 0; condition_t *cn; boolean achievedSoFar = true; for (i = 0; i < c->numconditions; ++i) { cn = &c->condition[i]; // If the ID is changed and all previous statements of the same ID were true // then this condition has been successfully achieved if (lastID && lastID != cn->id && achievedSoFar) return true; // Skip future conditions with the same ID if one fails, for obvious reasons if (lastID && lastID == cn->id && !achievedSoFar) continue; // Skip entries that are JUST for string building if (cn->type == UC_AND || cn->type == UC_THEN || cn->type == UC_COMMA || cn->type == UC_DESCRIPTIONOVERRIDE) continue; lastID = cn->id; if ((player != NULL) != (cn->type >= UCRP_REQUIRESPLAYING)) { //CONS_Printf("skipping %s:%u:%u (%s)\n", sizeu1(c-conditionSets), cn->id, i, player ? "player exists" : "player does not exist"); achievedSoFar = false; continue; } achievedSoFar = M_CheckCondition(cn, player); //CONS_Printf("%s:%u:%u - %u is %s\n", sizeu1(c-conditionSets), cn->id, i, cn->type, achievedSoFar ? "true" : "false"); } return achievedSoFar; } static char *M_BuildConditionTitle(UINT16 map) { char *title, *ref; if ((!(mapheaderinfo[map]->menuflags & LF2_NOVISITNEEDED) // the following is intentionally not MV_BEATEN, just in case the title is for "Finish a round on X" && !(mapheaderinfo[map]->records.mapvisited & MV_VISITED)) || M_MapLocked(map+1)) return Z_StrDup("???"); if (mapheaderinfo[map]->menuttl[0]) { if (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) { // Intentionally not forced uppercase return Z_StrDup(va("the %s Tutorial", mapheaderinfo[map]->menuttl)); } title = ref = Z_StrDup(mapheaderinfo[map]->menuttl); } else { title = ref = G_BuildMapTitle(map+1); } if (!title) I_Error("M_BuildConditionTitle: out of memory"); while (*ref != '\0') { *ref = toupper(*ref); ref++; } return title; } static const char *M_GetConditionCharacter(INT32 skin, boolean directlyrequires) { // First we check for direct unlock. boolean permitname = R_SkinUsable(-1, skin, false); if (permitname == false && directlyrequires == false) { // If there's no direct unlock, we CAN check for if the // character is the Rival of somebody we DO have unlocked... UINT8 i, j; for (i = 0; i < numskins; i++) { if (i == skin) continue; if (R_SkinUsable(-1, i, false) == false) continue; for (j = 0; j < SKINRIVALS; j++) { const char *rivalname = skins[i].rivals[j]; INT32 rivalnum = R_SkinAvailableEx(rivalname, false); if (rivalnum != skin) continue; // We can see this character as a Rival! break; } if (j == SKINRIVALS) continue; // "break" our way up the nesting... break; } // We stopped before the end, we can see it! if (i != numskins) permitname = true; } return (permitname) ? skins[skin].realname : "???"; } static const char *M_GetNthType(UINT8 position) { if (position == 1) return "st"; if (position == 2) return "nd"; if (position == 3) return "rd"; return "th"; } // See also M_CheckCondition static const char *M_GetConditionString(condition_t *cn) { INT32 i; char *title = NULL; const char *work = NULL; // If this function returns NULL, it stops building the condition and just does ???'s. switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x return va("play the game for %i:%02i:%02i", G_TicsToHours(cn->requirement), G_TicsToMinutes(cn->requirement, false), G_TicsToSeconds(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 // Base gametypes && (cn->extrainfo1 != GDGT_CUSTOM || M_SecretUnlocked(SECRET_ADDONS, true) == false) // Custom is visible at 0 if addons are unlocked && gamedata->roundsplayed[cn->extrainfo1] == 0) work = " ???"; else switch (cn->extrainfo1) { case GDGT_RACE: work = " Race"; break; case GDGT_PRISONS: work = " Prison"; 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("clear %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,%03u,%03u Rings", (cn->requirement/1000000), (cn->requirement/1000)%1000, (cn->requirement%1000)); if (cn->requirement >= 1000) return va("collect %u,%03u Rings", (cn->requirement/1000), (cn->requirement%1000)); return va("collect %u Rings", cn->requirement); case UC_TOTALTUMBLETIME: return va("tumble through the air for %i:%02i.%02i", G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); case UC_GAMECLEAR: // Requires game beaten >= x times if (cn->requirement > 1) return va("beat the 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 case UC_MAPSPBATTACK: // Requires map x to be beaten in SPB Attack case UC_MAPMYSTICMELODY: // Mystic Melody on map x's Ancient Shrine { const char *prefix = ""; if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); title = M_BuildConditionTitle(cn->requirement); if (cn->type == UC_MAPSPBATTACK) prefix = (M_SecretUnlocked(SECRET_SPBATTACK, true) ? "SPB ATTACK: " : "???: "); else if (cn->type == UC_MAPENCORE) prefix = (M_SecretUnlocked(SECRET_ENCORE, true) ? "ENCORE MODE: " : "???: "); work = "finish a round on"; if (cn->type == UC_MAPVISITED) work = "visit"; else if (cn->type == UC_MAPSPBATTACK) work = "conquer"; else if (cn->type == UC_MAPMYSTICMELODY) work = "play a melody for the ancient shrine in"; work = va("%s%s %s", prefix, work, title); Z_Free(title); return work; } case UC_MAPTIME: // Requires time on map <= x { if (cn->extrainfo1 >= nummapheaders || !mapheaderinfo[cn->extrainfo1]) return va("INVALID MAP CONDITION \"%d:%d:%d\"", cn->type, cn->extrainfo1, cn->requirement); title = M_BuildConditionTitle(cn->extrainfo1); work = va("beat %s in %i:%02i.%02i", title, G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); Z_Free(title); return work; } case UC_CHARACTERWINS: { if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d:%d\"", cn->type, cn->requirement, cn->extrainfo1); work = M_GetConditionCharacter(cn->requirement, true); return va("win %d Round%s as %s", cn->extrainfo1, cn->extrainfo1 == 1 ? "" : "s", work); } case UC_ALLCUPRECORDS: { const char *completetype = "Complete", *orbetter = "", *specialtext = NULL, *speedtext = ""; if (cn->extrainfo1 == 0) ; else if (cn->extrainfo1 == 1) completetype = "get Gold over"; else { if (cn->extrainfo1 == 2) completetype = "get Silver"; else if (cn->extrainfo1 == 3) completetype = "get Bronze"; orbetter = " or better over"; } if (cn->extrainfo2 == KARTSPEED_NORMAL) { speedtext = " on Normal"; } else if (cn->extrainfo2 == KARTSPEED_HARD) { speedtext = " on Hard"; } else if (cn->extrainfo2 == KARTGP_MASTER) { if (M_SecretUnlocked(SECRET_MASTERMODE, true)) speedtext = " on Master"; else speedtext = " on ???"; } if (cn->requirement == -1) specialtext = "every Cup"; else if (M_CupSecondRowLocked() == true && cn->requirement+1 >= CUPMENU_COLUMNS) specialtext = "the first ??? Cups"; if (specialtext != NULL) return va("GRAND PRIX: %s%s %s%s", completetype, orbetter, specialtext, speedtext); return va("GRAND PRIX: %s%s the first %d Cups%s", completetype, orbetter, cn->requirement, speedtext); } case UC_ALLCHAOS: case UC_ALLSUPER: case UC_ALLEMERALDS: { const char *chaostext, *speedtext = ""; if (!gamedata->everseenspecial) return NULL; if (cn->type == UC_ALLCHAOS) chaostext = "7 Chaos"; else if (M_CupSecondRowLocked() == true) return NULL; else if (cn->type == UC_ALLSUPER) chaostext = "7 Super"; else chaostext = "14"; /*if (cn->requirement == KARTSPEED_NORMAL) -- Emeralds can not be collected on Easy { speedtext = " on Normal"; } else*/ if (cn->requirement == KARTSPEED_HARD) { speedtext = " on Hard"; } else if (cn->requirement == KARTGP_MASTER) { if (M_SecretUnlocked(SECRET_MASTERMODE, true)) speedtext = " on Master"; else speedtext = " on ???"; } return va("GRAND PRIX: collect all %s Emeralds%s", chaostext, speedtext); } 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; i = cn->requirement-1; checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel] || emblemlocations[i].type == ET_NONE) return va("INVALID MEDAL MAP \"%d:%d\"", cn->requirement, checkLevel); title = M_BuildConditionTitle(checkLevel); switch (emblemlocations[i].type) { case ET_MAP: work = ""; if (emblemlocations[i].flags & ME_SPBATTACK) work = (M_SecretUnlocked(SECRET_SPBATTACK, true) ? "SPB ATTACK: " : "???: "); else if (emblemlocations[i].flags & ME_ENCORE) work = (M_SecretUnlocked(SECRET_ENCORE, true) ? "ENCORE MODE: " : "???: "); work = va("%s%s %s", work, (emblemlocations[i].flags & ME_SPBATTACK) ? "conquer" : "finish a round on", title); break; case ET_TIME: if (emblemlocations[i].color <= 0 || emblemlocations[i].color >= numskincolors) { Z_Free(title); return va("INVALID MEDAL COLOR \"%d:%d\"", cn->requirement, checkLevel); } work = va("TIME ATTACK: get the %s Medal for %s", skincolors[emblemlocations[i].color].name, title); break; case ET_GLOBAL: { const char *astr, *colorstr, *medalstr; if (emblemlocations[i].flags & GE_NOTMEDAL) { astr = "a "; colorstr = ""; medalstr = "secret"; } else if (emblemlocations[i].color <= 0 || emblemlocations[i].color >= numskincolors) { Z_Free(title); return va("INVALID MEDAL COLOR \"%d:%d:%d\"", cn->requirement, emblemlocations[i].tag, checkLevel); } else { astr = "the "; colorstr = skincolors[emblemlocations[i].color].name; medalstr = " Medal"; } if (emblemlocations[i].flags & GE_TIMED) { work = va("%s: find %s%s%s before %i:%02i.%02i", title, astr, colorstr, medalstr, G_TicsToMinutes(emblemlocations[i].var, true), G_TicsToSeconds(emblemlocations[i].var), G_TicsToCentiseconds(emblemlocations[i].var)); } else { work = va("%s: find %s%s%s", title, astr, colorstr, medalstr); } break; } default: work = va("find a secret in %s", title); break; } Z_Free(title); return work; } case UC_UNLOCKABLE: // Requires unlockable x to be obtained return va("get %s", gamedata->unlocked[cn->requirement-1] ? unlockables[cn->requirement-1].name : "???"); case UC_UNLOCKPERCENT: { boolean checkavailable = false; switch (cn->extrainfo1) { case SECRET_NONE: work = "completion"; break; case SECRET_EXTRAMEDAL: work = "of Challenge Medals"; break; case SECRET_CUP: work = "of Cups"; break; case SECRET_MAP: work = "of Courses"; break; case SECRET_ALTMUSIC: work = "of alternate music"; checkavailable = true; break; case SECRET_SKIN: work = "of Characters"; checkavailable = true; break; case SECRET_FOLLOWER: work = "of Followers"; checkavailable = true; break; case SECRET_COLOR: work = (gamedata->gotspraycans == 0) ? "of ???" : "of Spray Cans"; //checkavailable = true; break; default: return va("INVALID CHALLENGE FOR PERCENT \"%d\"", cn->requirement); } if (checkavailable == true) { for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != cn->extrainfo1) continue; if (gamedata->unlocked[i] == false) continue; break; } if (i == MAXUNLOCKABLES) work = "of ???"; } return va("CHALLENGES: get %u%% %s", cn->requirement, work); } case UC_ADDON: if (!M_SecretUnlocked(SECRET_ADDONS, true)) return NULL; return "load a custom addon"; case UC_CREDITS: return "watch the developer credits all the way from start to finish"; case UC_REPLAY: return "save a replay after finishing a round"; case UC_CRASH: if (gamedata->evercrashed) return "re-launch the game after a crash"; return NULL; case UC_TUTORIALSKIP: return "successfully skip the Tutorial"; case UC_PASSWORD: return "enter a secret password"; case UC_SPRAYCAN: { if (cn->requirement <= 0 || cn->requirement >= numskincolors) return va("INVALID SPRAYCAN COLOR \"%d\"", cn->requirement); UINT16 can_id = skincolors[cn->requirement].cache_spraycan; if (can_id >= gamedata->numspraycans) return va("INVALID SPRAYCAN ID \"%d:%u\"", cn->requirement, skincolors[cn->requirement].cache_spraycan ); if (can_id == 0) return "grab a Spray Can"; // Special case for the head of the list if (gamedata->spraycans[0].map >= nummapheaders) return NULL; // Don't tease that there are many until you have one return va("grab %d Spray Cans", can_id + 1); } case UC_PRISONEGGCD: // :butterfly: "alternatively you could say 'grab a hot toooon' or 'smooth beeat'" return "GRAND PRIX: grab a certain prize from a random Prison Egg"; case UC_AND: return "&"; case UC_THEN: return "then"; case UC_COMMA: return ","; case UC_DESCRIPTIONOVERRIDE: return cn->stringvar; case UCRP_PREFIX_BONUSROUND: //return "BONUS ROUND:"; -- our final testers bounced off this, just fallthru to GRAND PRIX instead case UCRP_PREFIX_GRANDPRIX: return "GRAND PRIX:"; case UCRP_PREFIX_TIMEATTACK: if (!M_SecretUnlocked(SECRET_TIMEATTACK, true)) return NULL; return "TIME ATTACK:"; case UCRP_PREFIX_PRISONBREAK: return "PRISON BREAK:"; case UCRP_PREFIX_SEALEDSTAR: if (!gamedata->everseenspecial) return NULL; return "SEALED STARS:"; case UCRP_PREFIX_ISMAP: if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\":", cn->type, cn->requirement); title = M_BuildConditionTitle(cn->requirement); work = va("%s:", title); Z_Free(title); return work; case UCRP_ISMAP: if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); title = M_BuildConditionTitle(cn->requirement); work = va("on %s", title); Z_Free(title); return work; case UCRP_ISCHARACTER: if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d\"", cn->type, cn->requirement); work = M_GetConditionCharacter(cn->requirement, true); return va("as %s", work); case UCRP_ISENGINECLASS: return va("with engine class %c", 'A' + cn->requirement); case UCRP_HASFOLLOWER: if (cn->requirement < 0 || !followers[cn->requirement].name[0]) return va("INVALID FOLLOWER CONDITION \"%d:%d\"", cn->type, cn->requirement); work = (K_FollowerUsable(cn->requirement)) ? followers[cn->requirement].name : "???"; return va("with %s in tow", work); case UCRP_ISDIFFICULTY: { const char *speedtext = ""; if (cn->requirement == KARTSPEED_NORMAL) { speedtext = "on Normal"; } else if (cn->requirement == KARTSPEED_HARD) { speedtext = "on Hard"; } else if (cn->requirement == KARTGP_MASTER) { if (M_SecretUnlocked(SECRET_MASTERMODE, true)) speedtext = "on Master"; else speedtext = "on ???"; } return speedtext; } case UCRP_ISGEAR: return va("in Gear %d", cn->requirement + 1); case UCRP_PODIUMCUP: { cupheader_t *cup; const char *completetype = "complete", *orbetter = ""; if (cn->extrainfo2) { switch (cn->extrainfo1) { case GRADE_E: { completetype = "get grade E"; break; } case GRADE_D: { completetype = "get grade D"; break; } case GRADE_C: { completetype = "get grade C"; break; } case GRADE_B: { completetype = "get grade B"; break; } case GRADE_A: { completetype = "get grade A"; break; } case GRADE_S: { completetype = "get grade S"; break; } default: { break; } } if (cn->requirement < GRADE_S) orbetter = " or better in"; else orbetter = " in"; } else if (cn->extrainfo1 == 0) ; else if (cn->extrainfo1 == 1) completetype = "get Gold in"; else { if (cn->extrainfo1 == 2) completetype = "get Silver"; else if (cn->extrainfo1 == 3) completetype = "get Bronze"; orbetter = " or better in"; } if (cn->requirement == -1) { return va("%s%s any Cup", completetype, orbetter ); } for (cup = kartcupheaders; cup; cup = cup->next) { if (cup->id != cn->requirement) continue; return va("%s%s %s CUP", completetype, orbetter, (M_CupLocked(cup) ? "???" : cup->realname) ); } return va("INVALID CUP CONDITION \"%d:%d\"", cn->type, cn->requirement); } case UCRP_PODIUMEMERALD: if (!gamedata->everseenspecial) return "???"; return "collect the Emerald"; case UCRP_PODIUMPRIZE: if (!gamedata->everseenspecial) return "???"; return "collect the prize"; case UCRP_PODIUMNOCONTINUES: return "without using any continues"; case UCRP_FINISHCOOL: return "finish in good standing"; case UCRP_FINISHPERFECT: return "finish a perfect round"; case UCRP_FINISHALLPRISONS: return "break every Prison Egg"; case UCRP_SURVIVE: return "survive"; case UCRP_NOCONTEST: return "NO CONTEST"; case UCRP_SMASHUFO: if (!gamedata->everseenspecial) return NULL; return "smash the UFO Catcher"; case UCRP_CHASEDBYSPB: return "while chased by a Self-Propelled Bomb"; case UCRP_MAPDESTROYOBJECTS: { if (cn->stringvar == NULL) return va("INVALID DESTROY CONDITION \"%d\"", cn->type); title = M_BuildConditionTitle(cn->requirement); work = va("%s: destroy all the %s", title, cn->stringvar); Z_Free(title); return work; } case UCRP_MAKERETIRE: { if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d\"", cn->type, cn->requirement); work = M_GetConditionCharacter(cn->requirement, false); return va("make %s retire", work); } case UCRP_FINISHPLACE: case UCRP_FINISHPLACEEXACT: return va("finish in %d%s%s", cn->requirement, M_GetNthType(cn->requirement), ((cn->type == UCRP_FINISHPLACE && cn->requirement > 1) ? " or better" : "")); case UCRP_FINISHGRADE: { char gradeletter = '?'; const char *orbetter = ""; switch (cn->requirement) { case GRADE_E: { gradeletter = 'E'; break; } case GRADE_D: { gradeletter = 'D'; break; } case GRADE_C: { gradeletter = 'C'; break; } case GRADE_B: { gradeletter = 'B'; break; } case GRADE_A: { gradeletter = 'A'; break; } default: { break; } } if (cn->requirement < GRADE_A) orbetter = " or better"; return va("get grade %c%s", gradeletter, orbetter ); } case UCRP_FINISHTIME: return va("finish in %i:%02i.%02i", G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); case UCRP_FINISHTIMEEXACT: return va("finish in exactly %i:%02i.XX", G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement)); case UCRP_FINISHTIMELEFT: return va("finish with %i:%02i.%02i remaining", G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); case UCRP_RINGS: if (cn->requirement != 20) return va("with at least %d Rings", cn->requirement); // FALLTHRU case UCRP_RINGSEXACT: return va("with exactly %d Rings", cn->requirement); case UCRP_SPEEDOMETER: return va("reach %s%u%% on the speedometer", (cn->requirement == 999) ? "" : "at least ", cn->requirement ); case UCRP_DRAFTDURATION: return va("consistently tether off other racers for %u seconds", cn->requirement); case UCRP_GROWCONSECUTIVEBEAMS: return va("touch the blue beams from your own Shrink at least %u times before returning to normal size", cn->requirement); case UCRP_TRIGGER: return "do something special"; case UCRP_FALLOFF: return (cn->requirement == 1) ? "fall off the course" : "don't fall off the course"; case UCRP_TOUCHOFFROAD: return (cn->requirement == 1) ? "touch offroad" : "don't touch any offroad"; case UCRP_TOUCHSNEAKERPANEL: return (cn->requirement == 1) ? "touch a Sneaker Panel" : "don't touch any Sneaker Panels"; case UCRP_RINGDEBT: return (cn->requirement == 1) ? "go into Ring debt" : "don't go into Ring debt"; case UCRP_FAULTED: return (cn->requirement == 1) ? "FAULT during POSITION" : "don't FAULT during POSITION"; case UCRP_TRIPWIREHYUU: return "go through Tripwire while afflicted by Hyudoro"; case UCRP_WHIPHYUU: return "Insta-Whip a racer while afflicted by Hyudoro"; case UCRP_SPBNEUTER: return "shock a Self-Propelled Bomb into submission"; case UCRP_LANDMINEDUNK: return "dunk a Land Mine on another racer's head"; case UCRP_HITMIDAIR: return "hit another racer with a projectile while you're both in the air"; case UCRP_HITDRAFTERLOOKBACK: return "hit a racer tethering off you while looking back at them"; case UCRP_GIANTRACERSHRUNKENORBI: return "hit a giant racer with a shrunken Orbinaut"; case UCRP_RETURNMARKTOSENDER: return "when cursed with Eggmark, blow up the racer responsible"; case UCRP_TRACKHAZARD: { work = (cn->requirement == 1) ? "touch a course hazard" : "don't touch any course hazards"; if (cn->extrainfo1 == -1) return va("%s%s", work, (cn->requirement == 1) ? " on every lap" : ""); if (cn->extrainfo1 == -2) return va("%s on the final lap", work); if (cn->extrainfo1 == 0) return va("%s during POSITION", work); return va("%s on lap %u", work, cn->extrainfo1); } case UCRP_TARGETATTACKMETHOD: { work = NULL; switch (cn->requirement) { // See targetdamaging_t case UFOD_BOOST: work = "boost power"; break; case UFOD_WHIP: work = "Insta-Whip"; break; case UFOD_BANANA: work = "Bananas"; break; case UFOD_ORBINAUT: work = "Orbinauts"; break; case UFOD_JAWZ: work = "Jawz"; break; case UFOD_SPB: work = "Self-Propelled Bombs"; break; case UFOD_GACHABOM: work = "Gachabom"; break; default: break; } if (work == NULL) return va("INVALID ATTACK CONDITION \"%d:%d\"", cn->type, cn->requirement); return va("using only %s", work); } case UCRP_GACHABOMMISER: return "using exactly one Gachabom repeatedly"; case UCRP_WETPLAYER: return va("without %s %s", (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", (cn->stringvar) ? cn->stringvar : "water"); default: break; } // UC_MAPTRIGGER and UC_CONDITIONSET are explicitly very hard to support proper descriptions for return va("UNSUPPORTED CONDITION \"%d\"", cn->type); } char *M_BuildConditionSetString(UINT16 unlockid) { conditionset_t *c = NULL; UINT32 lastID = 0; condition_t *cn; size_t len = 1024, worklen; static char message[1024] = ""; const char *work = NULL; size_t i; UINT8 stopasap = 0; message[0] = '\0'; if (unlockid >= MAXUNLOCKABLES) { return NULL; } if (!unlockables[unlockid].conditionset) { return NULL; } if (gamedata->unlocked[unlockid] == true && M_Achieved(unlockables[unlockid].conditionset - 1) == false) { message[0] = '\x86'; // the following text will be grey message[1] = '\0'; len--; } c = &conditionSets[unlockables[unlockid].conditionset-1]; for (i = 0; i < c->numconditions; ++i) { cn = &c->condition[i]; if (i > 0) { worklen = 0; if (lastID != cn->id) { stopasap = 0; worklen = 6; strncat(message, " - OR ", len); } else if (stopasap == 0 && cn->type != UC_COMMA) { worklen = 1; strncat(message, " ", len); } len -= worklen; } lastID = cn->id; if (stopasap == 1) { // Secret challenge -- show unrelated condition IDs continue; } work = M_GetConditionString(cn); if (work == NULL) { stopasap = 1; if (message[0] && message[1]) work = "???"; else work = "(Find other secrets to learn about this...)"; } else if (cn->type == UC_DESCRIPTIONOVERRIDE) { stopasap = 2; } worklen = strlen(work); strncat(message, work, len); len -= worklen; if (stopasap == 2) { // Description override - hide all further ones break; } } if (message[0] == '\0') { return NULL; } // Valid sentence capitalisation handling. { // Finds the first : character, indicating the end of the prefix. for (i = 0; message[i]; i++) { if (message[i] != ':') continue; i++; break; } // Okay, now make the first non-whitespace character after this a capital. // Doesn't matter if !isalpha() - toupper is a no-op. // (If the first loop hit the string's end, the message[i] check keeps us safe) for (; message[i]; i++) { if ((message[i] & 0x80) || isspace(message[i])) continue; message[i] = toupper(message[i]); break; } // Also do this for the prefix. // This might seem redundant, but "the Controls Tutorial:" is a possible prefix! for (i = 0; message[i]; i++) { if ((message[i] & 0x80) || isspace(message[i])) continue; message[i] = toupper(message[i]); break; } } if (usedTourney && unlockables[unlockid].conditionset == CH_FURYBIKE && gamedata->unlocked[unlockid] == false) { strcpy(message, "Power shrouds this challenge in darkness... (Return here without Tournament Mode!)\0"); } // Finally, do a clean wordwrap! return V_ScaledWordWrap( DESCRIPTIONWIDTH << FRACBITS, FRACUNIT, FRACUNIT, FRACUNIT, 0, TINY_FONT, message ); } static boolean M_CheckUnlockConditions(player_t *player) { UINT32 i; conditionset_t *c; boolean ret; for (i = 0; i < MAXCONDITIONSETS; ++i) { c = &conditionSets[i]; if (!c->numconditions || gamedata->achieved[i]) continue; if ((gamedata->achieved[i] = (M_CheckConditionSet(c, player))) != true) continue; ret = true; } return ret; } boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall) { UINT16 i = 0, response = 0, newkeys = 0; if (!gamedata) { // Don't attempt to write/check anything. return false; } if (!loud) { // Just in case they aren't to sync // Done first so that emblems are ready before check M_CheckLevelEmblems(); M_CompletionEmblems(); doall = true; } if (gamedata->deferredconditioncheck == true) { // Handle deferred all-condition checks gamedata->deferredconditioncheck = false; doall = true; } if (doall) { response = M_CheckUnlockConditions(NULL); M_UpdateNextPrisonEggPickup(); if (gamedata->pendingkeyrounds == 0 || (gamedata->chaokeys >= GDMAX_CHAOKEYS)) { gamedata->keyspending = 0; } else while ((gamedata->keyspending + gamedata->chaokeys) < GDMAX_CHAOKEYS && ((gamedata->pendingkeyrounds + gamedata->pendingkeyroundoffset)/GDCONVERT_ROUNDSTOKEY) > gamedata->keyspending) { gamedata->keyspending++; newkeys++; response |= true; } } if (!demo.playback && Playing() && (gamestate == GS_LEVEL || K_PodiumSequence() == true)) { for (i = 0; i <= splitscreen; i++) { if (!playeringame[g_localplayers[i]]) continue; if (players[g_localplayers[i]].spectator) continue; if (!doall && players[g_localplayers[i]].roundconditions.checkthisframe == false) continue; response |= M_CheckUnlockConditions(&players[g_localplayers[i]]); players[g_localplayers[i]].roundconditions.checkthisframe = false; } } if (loud && response == 0) { return false; } response = 0; // Go through unlockables for (i = 0; i < MAXUNLOCKABLES; ++i) { if (gamedata->unlocked[i] || !unlockables[i].conditionset) { continue; } if (gamedata->unlocked[i] == true || gamedata->unlockpending[i] == true) { continue; } if (M_Achieved(unlockables[i].conditionset - 1) == false) { continue; } gamedata->unlockpending[i] = true; response++; } // Announce if (response != 0) { if (loud) { S_StartSound(NULL, sfx_achiev); } return true; } if (newkeys != 0) { if (loud) { S_StartSound(NULL, sfx_keygen); } return true; } return false; } UINT16 M_GetNextAchievedUnlock(boolean canskipchaokeys) { UINT16 i; // Go through unlockables for (i = 0; i < MAXUNLOCKABLES; ++i) { if (!unlockables[i].conditionset) { // Not worthy of consideration continue; } if (gamedata->unlocked[i] == true) { // Already unlocked, no need to engage continue; } if (gamedata->unlockpending[i] == false) { // Not unlocked AND not pending, which means chao keys can be used on something canskipchaokeys = false; continue; } return i; } if (canskipchaokeys == true) { // Okay, we're skipping chao keys - let's just insta-digest them. if (gamedata->chaokeys + gamedata->keyspending < GDMAX_CHAOKEYS) { gamedata->chaokeys += gamedata->keyspending; gamedata->pendingkeyroundoffset = (gamedata->pendingkeyroundoffset + gamedata->pendingkeyrounds) % GDCONVERT_ROUNDSTOKEY; } else { gamedata->chaokeys = GDMAX_CHAOKEYS; gamedata->pendingkeyroundoffset = 0; } gamedata->keyspending = 0; gamedata->pendingkeyrounds = 0; } else if (gamedata->keyspending != 0) { return PENDING_CHAOKEYS; } return MAXUNLOCKABLES; } // Emblem unlocking shit UINT16 M_CheckLevelEmblems(void) { INT32 i; INT32 valToReach; INT16 tag; INT16 levelnum; boolean res; UINT16 somethingUnlocked = 0; // Update Score, Time, Rings emblems for (i = 0; i < numemblems; ++i) { INT32 checkLevel; if (emblemlocations[i].type < ET_TIME || gamedata->collected[i]) continue; checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) continue; levelnum = checkLevel; valToReach = emblemlocations[i].var; tag = emblemlocations[i].tag; switch (emblemlocations[i].type) { case ET_TIME: // Requires time on map <= x if (tag > 0) { if (tag > mapheaderinfo[checkLevel]->ghostCount || mapheaderinfo[checkLevel]->ghostBrief[tag-1] == NULL) continue; res = (G_GetBestTime(levelnum) <= mapheaderinfo[checkLevel]->ghostBrief[tag-1]->time); } else if (tag < 0 && tag > AUTOMEDAL_MAX) { // Use auto medal times for emblem tags, see AUTOMEDAL_ in m_cond.h int index = -tag - 1; // 0 is Platinum, 3 is Bronze tic_t time = mapheaderinfo[checkLevel]->automedaltime[index]; res = (G_GetBestTime(levelnum) <= time); } else { res = (G_GetBestTime(levelnum) <= (unsigned)valToReach); } break; default: // unreachable but shuts the compiler up. continue; } gamedata->collected[i] = res; if (res) ++somethingUnlocked; } return somethingUnlocked; } UINT16 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separate print when awarding emblems and it's sorta different enough. { INT32 i; INT32 embtype; INT16 levelnum; boolean res; UINT16 somethingUnlocked = 0; UINT8 flags; for (i = 0; i < numemblems; ++i) { INT32 checkLevel; if (emblemlocations[i].type != ET_MAP || gamedata->collected[i]) continue; checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) continue; levelnum = checkLevel; embtype = emblemlocations[i].flags; flags = MV_BEATEN; if (embtype & ME_ENCORE) flags |= MV_ENCORE; if (embtype & ME_SPBATTACK) flags |= MV_SPBATTACK; res = ((mapheaderinfo[levelnum]->records.mapvisited & flags) == flags); gamedata->collected[i] = res; if (res) ++somethingUnlocked; } return somethingUnlocked; } // ------------------- // Quick unlock checks // ------------------- boolean M_GameTrulyStarted(void) { // Fail safe if (gamedata == NULL) return false; // Not set if (gamestartchallenge >= MAXUNLOCKABLES) return true; // An unfortunate sidestep, but sync is important. if (netgame) return true; // Okay, we can check to see if this challenge has been achieved. /*return ( gamedata->unlockpending[gamestartchallenge] || gamedata->unlocked[gamestartchallenge] );*/ // Actually, on second thought, let's let the Goner Setup play one last time // The above is used in M_StartControlPanel instead return (gamedata->gonerlevel == GDGONER_DONE); } boolean M_CheckNetUnlockByID(UINT16 unlockid) { if (unlockid >= MAXUNLOCKABLES || !unlockables[unlockid].conditionset) { return true; // default permit } if (netgame || demo.playback) { return netUnlocked[unlockid]; } return gamedata->unlocked[unlockid]; } boolean M_SecretUnlocked(INT32 type, boolean local) { INT32 i; #if 0 (void)type; (void)i; return false; // for quick testing #else for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != type) continue; if ((local && gamedata->unlocked[i]) || (!local && M_CheckNetUnlockByID(i))) continue; return false; } return true; #endif //if 0 } boolean M_CupLocked(cupheader_t *cup) { // No skipping over any part of your marathon. if (marathonmode) return false; if (!cup) return false; #if 0 // perfect uncached behaviour UINT16 i; for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_CUP) continue; if (M_UnlockableCup(&unlockables[i]) != cup) continue; return !M_CheckNetUnlockByID(i); } #else if (cup->cache_cuplock < MAXUNLOCKABLES) return !M_CheckNetUnlockByID(cup->cache_cuplock); #endif return false; } boolean M_CupSecondRowLocked(void) { // The following was pre-optimised for cached behaviour. // It would need a refactor if the cache system were to // change, maybe to iterate over unlockable_t instead. cupheader_t *cup; for (cup = kartcupheaders; cup; cup = cup->next) { // Only important for the second row. if ((cup->id % (CUPMENU_COLUMNS * CUPMENU_ROWS)) < CUPMENU_COLUMNS) continue; // Only important for ones that can be locked. if (cup->cache_cuplock == MAXUNLOCKABLES) continue; // If it's NOT unlocked, can't be used as proof of unlock. if (!M_CheckNetUnlockByID(cup->cache_cuplock)) continue; // Okay, at least one cup on the second row is unlocked! return false; } return true; } boolean M_MapLocked(UINT16 mapnum) { // No skipping over any part of your marathon. if (marathonmode) return false; if (mapnum == 0 || mapnum > nummapheaders) return false; if (!mapheaderinfo[mapnum-1]) return false; if (mapheaderinfo[mapnum-1]->cup) { return M_CupLocked(mapheaderinfo[mapnum-1]->cup); } #if 0 // perfect uncached behaviour UINT16 i; for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_MAP) continue; if (M_UnlockableMapNum(&unlockables[i]) != mapnum-1) continue; return !M_CheckNetUnlockByID(i); } #else if (mapheaderinfo[mapnum-1]->cache_maplock < MAXUNLOCKABLES) return !M_CheckNetUnlockByID(mapheaderinfo[mapnum-1]->cache_maplock); #endif return false; } INT32 M_CountMedals(boolean all, boolean extraonly) { INT32 found = 0, i; if (!extraonly) { for (i = 0; i < numemblems; ++i) { // Not init in SOC if (emblemlocations[i].type == ET_NONE) continue; // Not explicitly a medal if ((emblemlocations[i].type == ET_GLOBAL) && (emblemlocations[i].flags & GE_NOTMEDAL)) continue; // Not getting the counter, and not collected if (!all && !gamedata->collected[i]) continue; // Don't count Platinums in the overall count, so you can get 101% going for them if (all && (emblemlocations[i].type == ET_TIME) && (emblemlocations[i].tag == AUTOMEDAL_PLATINUM)) continue; // Relevant, add to da counter found++; } } // Above but for extramedals for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_EXTRAMEDAL) continue; if (!all && !gamedata->unlocked[i]) continue; found++; } return found; } // -------------------------------------- // Quick functions for calculating things // -------------------------------------- // Theoretically faster than using M_CountMedals() // Stops when it reaches the target number of medals. boolean M_GotEnoughMedals(INT32 number) { INT32 i, gottenmedals = 0; for (i = 0; i < numemblems; ++i) { // Not init in SOC if (emblemlocations[i].type == ET_NONE) continue; // Not explicitly a medal if ((emblemlocations[i].type == ET_GLOBAL) && (emblemlocations[i].flags & GE_NOTMEDAL)) continue; // Not collected if (!gamedata->collected[i]) continue; // Add to counter. Hit our threshold? if (++gottenmedals < number) continue; // We did! return true; } // Above but for extramedals for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_EXTRAMEDAL) continue; if (!gamedata->unlocked[i]) continue; if (++gottenmedals < number) continue; return true; } // Didn't hit our counter! return false; } boolean M_GotLowEnoughTime(INT32 tictime) { INT32 curtics = 0; INT32 i; for (i = 0; i < nummapheaders; ++i) { if (!mapheaderinfo[i] || (mapheaderinfo[i]->menuflags & LF2_NOTIMEATTACK)) continue; if (!mapheaderinfo[i]->records.timeattack.time) return false; if ((curtics += mapheaderinfo[i]->records.timeattack.time) > tictime) return false; } return true; } // Gets the skin number for a SECRET_SKIN unlockable. INT32 M_UnlockableSkinNum(unlockable_t *unlock) { if (unlock->type != SECRET_SKIN) { // This isn't a skin unlockable... return -1; } if (unlock->stringVar && unlock->stringVar[0]) { INT32 skinnum; if (unlock->stringVarCache != -1) { return unlock->stringVarCache; } // Get the skin from the string. skinnum = R_SkinAvailableEx(unlock->stringVar, false); if (skinnum != -1) { unlock->stringVarCache = skinnum; return skinnum; } } if (unlock->variable >= 0 && unlock->variable < numskins) { // Use the number directly. return unlock->variable; } // Invalid skin unlockable. return -1; } // Gets the skin number for a SECRET_FOLLOWER unlockable. INT32 M_UnlockableFollowerNum(unlockable_t *unlock) { if (unlock->type != SECRET_FOLLOWER) { // This isn't a follower unlockable... return -1; } if (unlock->stringVar && unlock->stringVar[0]) { INT32 skinnum; size_t i; char testname[SKINNAMESIZE+1]; if (unlock->stringVarCache != -1) { return unlock->stringVarCache; } // match deh_soc readfollower() for (i = 0; unlock->stringVar[i]; i++) { testname[i] = unlock->stringVar[i]; if (unlock->stringVar[i] == '_') testname[i] = ' '; } testname[i] = '\0'; // Get the skin from the string. skinnum = K_FollowerAvailable(testname); if (skinnum != -1) { unlock->stringVarCache = skinnum; return skinnum; } } if (unlock->variable >= 0 && unlock->variable < numfollowers) { // Use the number directly. return unlock->variable; } // Invalid follower unlockable. return -1; } INT32 M_UnlockableColorNum(unlockable_t *unlock) { if (unlock->type != SECRET_COLOR) { // This isn't a color unlockable... return -1; } if (unlock->stringVar && unlock->stringVar[0]) { skincolornum_t colornum = SKINCOLOR_NONE; if (unlock->stringVarCache != -1) { return unlock->stringVarCache; } // Get the skin from the string. colornum = R_GetColorByName(unlock->stringVar); if (colornum != SKINCOLOR_NONE) { unlock->stringVarCache = colornum; return colornum; } } if (unlock->variable > SKINCOLOR_NONE && unlock->variable < numskincolors) { // Use the number directly. return unlock->variable; } // Invalid color unlockable. return -1; } cupheader_t *M_UnlockableCup(unlockable_t *unlock) { cupheader_t *cup = kartcupheaders; INT16 val = unlock->variable-1; if (unlock->type != SECRET_CUP) { // This isn't a cup unlockable... return NULL; } if (unlock->stringVar && unlock->stringVar[0]) { if (unlock->stringVarCache == -1) { // Get the cup from the string. UINT32 hash = quickncasehash(unlock->stringVar, MAXCUPNAME); while (cup) { if (hash == cup->namehash && !strcmp(cup->name, unlock->stringVar)) break; cup = cup->next; } if (cup) { unlock->stringVarCache = cup->id; } return cup; } val = unlock->stringVarCache; } else if (val == -1) { return NULL; } // Use the number directly. while (cup) { if (cup->id == val) break; cup = cup->next; } return cup; } UINT16 M_UnlockableMapNum(unlockable_t *unlock) { if (unlock->type != SECRET_MAP && unlock->type != SECRET_ALTMUSIC) { // This isn't a map unlockable... return NEXTMAP_INVALID; } if (unlock->stringVar && unlock->stringVar[0]) { if (unlock->stringVarCache == -1) { INT32 result = G_MapNumber(unlock->stringVar); if (result >= nummapheaders) return result; unlock->stringVarCache = result; } return unlock->stringVarCache; } return NEXTMAP_INVALID; } // ---------------- // Misc Emblem shit // ---------------- UINT16 M_EmblemMapNum(emblem_t *emblem) { if (emblem->levelCache == NEXTMAP_INVALID) { UINT16 result = G_MapNumber(emblem->level); if (result >= nummapheaders) return result; emblem->levelCache = result; } return emblem->levelCache; } // Returns pointer to an emblem if an emblem exists for that level. // Pass -1 mapnum to continue from last emblem. // NULL if not found. // note that this goes in reverse!! emblem_t *M_GetLevelEmblems(INT32 mapnum) { static INT32 map = -1; static INT32 i = -1; if (mapnum > 0) { map = mapnum-1; i = numemblems; } while (--i >= 0) { if (emblemlocations[i].type == ET_NONE) continue; INT32 checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) continue; if (checkLevel != map) continue; return &emblemlocations[i]; } return NULL; } skincolornum_t M_GetEmblemColor(emblem_t *em) { if (!em || !em->color || em->color >= numskincolors) return SKINCOLOR_GOLD; return em->color; } const char *M_GetEmblemPatch(emblem_t *em, boolean big) { static char pnamebuf[7]; if (!big) strcpy(pnamebuf, "GOTITn"); else strcpy(pnamebuf, "EMBMn0"); I_Assert(em->sprite >= 'A' && em->sprite <= 'Z'); if (!big) pnamebuf[5] = em->sprite; else pnamebuf[4] = em->sprite; return pnamebuf; } boolean M_UseAlternateTitleScreen(void) { extern consvar_t cv_alttitle; return cv_alttitle.value && M_SecretUnlocked(SECRET_ALTTITLE, true); } INT32 M_GameDataGameType(INT32 lgametype, boolean lbattleprisons) { INT32 playtimemode = GDGT_CUSTOM; if (lgametype == GT_RACE) playtimemode = GDGT_RACE; else if (lgametype == GT_BATTLE) playtimemode = lbattleprisons ? GDGT_PRISONS : GDGT_BATTLE; else if (lgametype == GT_SPECIAL || lgametype == GT_VERSUS) playtimemode = GDGT_SPECIAL; return playtimemode; }