RingRacers/src/m_cond.c

3971 lines
97 KiB
C

// 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;
}