First pass at assigning unique spraycans to level headers

Increments gamedata minor version, be aware
- M_AssignSpraycans
    - Called in M_FinaliseGameData.
    - Attaches a hardcoded set of colours to all race maps in cup order, stopping once we run out.
    - The colours are shuffled, with some "freebies" always at the head of the list.
    - Integrates partial lists pretty well.
    - In DEVELOP builds, I_Errors if it produces corrupted state.
- G_LoadGameData, G_SaveGameData
    - Save & Load is implemented for these assignments
This commit is contained in:
toaster 2023-08-19 15:06:36 +01:00
parent be1d3e49e8
commit 1d06637a38
10 changed files with 315 additions and 13 deletions

View file

@ -216,6 +216,13 @@ void clear_levels(void)
}
}
if (gamedata)
{
UINT16 i;
for (i = 1; i < MAXCANCOLORS; i++)
gamedata->spraycans[i].map = 0;
}
// Exit the current gamemap as a safeguard
if (Playing())
COM_BufAddText("exitgame"); // Command_ExitGame_f() but delayed

View file

@ -6538,6 +6538,7 @@ struct int_const_s const INT_CONST[] = {
// SKINCOLOR_ doesn't include these..!
{"MAXSKINCOLORS",MAXSKINCOLORS},
{"MAXCANCOLORS",MAXCANCOLORS},
{"FIRSTSUPERCOLOR",FIRSTSUPERCOLOR},
{"NUMSUPERCOLORS",NUMSUPERCOLORS},

View file

@ -346,7 +346,9 @@ typedef enum
SKINCOLOR_BLOSSOM,
SKINCOLOR_TAFFY,
FIRSTSUPERCOLOR,
MAXCANCOLORS,
FIRSTSUPERCOLOR = MAXCANCOLORS,
// Super special awesome Super flashing colors!
SKINCOLOR_SUPERSILVER1 = FIRSTSUPERCOLOR,

View file

@ -531,6 +531,9 @@ struct mapheader_t
UINT8 precutscenenum; ///< Cutscene number to play BEFORE a level starts.
UINT8 cutscenenum; ///< Cutscene number to use, 0 for none.
UINT32 _saveid; ///< Purely assistive in gamedata save processes
UINT16 cachedcan; ///< Cached Spraycan ID
// Lua information
UINT8 numCustomOptions; ///< Internal. For Lua custom value support.
customoption_t *customopts; ///< Custom options. Allocated dynamically for space reasons. Be careful.

View file

@ -4357,7 +4357,7 @@ void G_LoadGameSettings(void)
}
#define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual
#define GD_VERSIONMINOR 5 // Change every format update
#define GD_VERSIONMINOR 6 // Change every format update
typedef enum
{
@ -4678,10 +4678,17 @@ void G_LoadGameData(void)
}
}
UINT16 *tempmapidreferences = NULL;
numgamedatamapheaders = READUINT32(save.p);
if (numgamedatamapheaders)
{
tempmapidreferences = Z_Malloc(
numgamedatamapheaders * sizeof (UINT16),
PU_STATIC,
NULL
);
for (i = 0; i < numgamedatamapheaders; i++)
{
@ -4691,6 +4698,7 @@ void G_LoadGameData(void)
READSTRINGL(save.p, mapname, MAXMAPLUMPNAME);
mapnum = G_MapNumber(mapname);
tempmapidreferences[i] = (UINT16)mapnum;
recorddata_t dummyrecord;
@ -4735,6 +4743,49 @@ void G_LoadGameData(void)
}
}
if (versionMinor > 5)
{
UINT16 numgamedatacans = READUINT32(save.p);
if (numgamedatacans != MAXCANCOLORS - 1)
{
save.p += (1 + 4) * numgamedatacans;
}
else
{
for (i = 1; i < MAXCANCOLORS; i++)
{
gamedata->spraycans[i].got = (boolean)READUINT8(save.p);
gamedata->spraycans[i].map = 0;
UINT32 _saveid = READUINT32(save.p);
if (_saveid >= numgamedatamapheaders)
{
//CONS_Printf("LOAD - Color %s - id %u, map 0 (invalid id)\n", skincolors[i].name, _saveid);
continue;
}
UINT16 map = tempmapidreferences[_saveid];
if (map >= nummapheaders || !mapheaderinfo[map])
{
//CONS_Printf("LOAD - Color %s - id %u, map 0 (unloaded header)\n", skincolors[i].name, _saveid);
continue;
}
//CONS_Printf("LOAD - Color %s - id %u, map %d\n", skincolors[i].name, _saveid, map+1);
gamedata->spraycans[i].map = map+1;
mapheaderinfo[map]->cachedcan = i;
numgamedatacans--; // this one was successfully placed
}
gamedata->allspraycansplaced = (numgamedatacans == 0);
//CONS_Printf("CCC - all spray cans placed? %c\n", gamedata->allspraycansplaced ? 'Y' : 'N');
}
}
if (versionMinor > 1)
{
numgamedatacups = READUINT32(save.p);
@ -4862,6 +4913,8 @@ void G_LoadGameData(void)
if (tempskinreferences)
Z_Free(tempskinreferences);
if (tempmapidreferences)
Z_Free(tempmapidreferences);
// done
P_SaveBufferFree(&save);
@ -4983,12 +5036,16 @@ void G_SaveGameData(void)
for (i = 0; i < nummapheaders; i++)
{
// No spraycan attached.
if (mapheaderinfo[i]->cachedcan == 0
// It's safe to assume a level with no mapvisited will have no other data worth keeping, since you get MV_VISITED just for opening it.
if (!(mapheaderinfo[i]->records.mapvisited & MV_MAX))
&& !(mapheaderinfo[i]->records.mapvisited & MV_MAX))
{
mapheaderinfo[i]->_saveid = UINT32_MAX;
continue;
}
mapheaderinfo[i]->_saveid = numgamedatamapheaders;
numgamedatamapheaders++;
}
@ -5012,6 +5069,9 @@ void G_SaveGameData(void)
length += 4 + (numgamedatamapheaders * (MAXMAPLUMPNAME+1+4+4));
length += 4 + ((MAXCANCOLORS - 1) * (1 + 4));
UINT32 numgamedatacups = 0;
unloaded_cupheader_t *unloadedcup;
@ -5206,7 +5266,8 @@ void G_SaveGameData(void)
for (i = 0; i < nummapheaders; i++)
{
if (!(mapheaderinfo[i]->records.mapvisited & MV_MAX))
if (mapheaderinfo[i]->cachedcan == 0
&& !(mapheaderinfo[i]->records.mapvisited & MV_MAX))
continue;
WRITESTRINGL(save.p, mapheaderinfo[i]->lumpname, MAXMAPLUMPNAME);
@ -5247,6 +5308,27 @@ void G_SaveGameData(void)
}
}
WRITEUINT32(save.p, MAXCANCOLORS - 1); // 4
// (MAXCANCOLORS - 1) * (1 + 4)
for (i = 1; i < MAXCANCOLORS; i++)
{
WRITEUINT8(save.p, gamedata->spraycans[i].got);
UINT32 _saveid = UINT32_MAX;
UINT16 map = gamedata->spraycans[i].map;
if (map > 0 && map <= nummapheaders && mapheaderinfo[map - 1])
{
_saveid = mapheaderinfo[map - 1]->_saveid;
}
//CONS_Printf("SAVE - Color %s - id %u, map %d\n", skincolors[i].name, _saveid, map);
WRITEUINT32(save.p, _saveid);
}
WRITEUINT32(save.p, numgamedatacups); // 4

View file

@ -6049,6 +6049,26 @@ challengedesc:
static void M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y)
{
UINT8 lasttype = UINT8_MAX, curtype;
boolean start = false;
if (mapheaderinfo[mapnum]->cachedcan != 0 && mapheaderinfo[mapnum]->cachedcan < MAXCANCOLORS)
{
V_DrawSmallMappedPatch(x, y, 0, W_CachePatchName("GOTITA", PU_CACHE),
R_GetTranslationColormap(TC_RAINBOW, mapheaderinfo[mapnum]->cachedcan, GTC_MENUCACHE));
//V_DrawRightAlignedThinString(x - 2, y, 0, skincolors[mapheaderinfo[mapnum]->cachedcan].name);
x -= 8;
start = true;
}
// Shift over if emblem is of a different discipline
if (start)
x -= 4;
// M_GetLevelEmblems is ONE-indexed, urgh
mapnum++;
emblem_t *emblem = M_GetLevelEmblems(mapnum);
while (emblem)
@ -6091,7 +6111,7 @@ static void M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y)
if (gamedata->collected[emblem-emblemlocations])
V_DrawSmallMappedPatch(x, y, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_CACHE),
R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_MENUCACHE));
R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_MENUCACHE));
else
V_DrawSmallScaledPatch(x, y, 0, W_CachePatchName("NEEDIT", PU_CACHE));
@ -6246,7 +6266,7 @@ static void M_DrawStatsMaps(void)
}
}
M_DrawMapMedals(mnum+1, medalspos - 8, y);
M_DrawMapMedals(mnum, medalspos - 8, y);
if (mapheaderinfo[mnum]->menuttl[0])
{

View file

@ -621,14 +621,23 @@ void M_ClearStats(void)
void M_ClearSecrets(void)
{
INT32 i;
memset(gamedata->collected, 0, sizeof(gamedata->collected));
memset(gamedata->unlocked, 0, sizeof(gamedata->unlocked));
memset(gamedata->unlockpending, 0, sizeof(gamedata->unlockpending));
if (!dedicated)
memset(netUnlocked, 0, sizeof(netUnlocked));
memset(gamedata->achieved, 0, sizeof(gamedata->achieved));
for (i = 0; i < MAXEMBLEMS; ++i)
gamedata->collected[i] = false;
for (i = 0; i < MAXUNLOCKABLES; ++i)
gamedata->unlocked[i] = gamedata->unlockpending[i] = netUnlocked[i] = false;
for (i = 0; i < MAXCONDITIONSETS; ++i)
gamedata->achieved[i] = false;
gamedata->allspraycansplaced = false;
memset(gamedata->spraycans, 0, sizeof(gamedata->spraycans));
INT32 i;
for (i = 0; i < nummapheaders; i++)
{
if (!mapheaderinfo[i])
continue;
mapheaderinfo[i]->cachedcan = 0;
}
Z_Free(gamedata->challengegrid);
gamedata->challengegrid = NULL;
@ -640,10 +649,175 @@ void M_ClearSecrets(void)
gamedata->chaokeys = 3; // Start with 3 !!
}
// 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) // no need to swap on ==
{
i = M_RandomKey(len);
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)
if (gamedata->allspraycansplaced)
return;
// Init ordered list of skincolors
UINT16 tempcanlist[MAXCANCOLORS];
size_t listlen = 0;
// Todo one of these should be a freebie
UINT16 prependlist[] =
{
SKINCOLOR_RED,
SKINCOLOR_ORANGE,
SKINCOLOR_YELLOW,
SKINCOLOR_GREEN,
SKINCOLOR_BLUE,
SKINCOLOR_PURPLE,
0
};
UINT16 i;
for (i = 0; prependlist[i]; i++)
{
if (gamedata->spraycans[prependlist[i]].map > 0
&& gamedata->spraycans[prependlist[i]].map <= nummapheaders)
continue;
CONS_Printf("DDD - Prepending %d\n", prependlist[i]);
tempcanlist[listlen] = prependlist[i];
gamedata->spraycans[prependlist[i]].got = 2; // invalid set to detect in below loop, rather than having to iterate over prependlist again
listlen++;
}
size_t prepend = listlen;
for (i = 1; i < MAXCANCOLORS; i++)
{
if (gamedata->spraycans[i].map > 0
&& gamedata->spraycans[i].map <= nummapheaders)
continue;
if (gamedata->spraycans[i].got == 2)
{
// re-make valid, reject duplicating prepended
gamedata->spraycans[i].got = false;
continue;
}
CONS_Printf("DDD - Adding %d\n", i);
tempcanlist[listlen] = i;
listlen++;
}
if (!listlen)
goto cansdone;
if (prepend > 0)
{
// Swap the prepend for random order
M_Shuffle_UINT16(tempcanlist, prepend);
}
if (listlen > prepend)
{
// Swap everything else for random order
M_Shuffle_UINT16(tempcanlist + prepend, listlen - prepend);
}
i = 0;
cupheader_t *cup;
UINT16 level;
for (cup = kartcupheaders; cup; cup = cup->next)
{
UINT8 j;
for (j = 0; j < cup->numlevels; j++)
{
level = cup->cachedlevels[j];
if (level > nummapheaders)
continue;
if (mapheaderinfo[level]->cachedcan != 0)
continue;
gamedata->spraycans[tempcanlist[i]].map = level + 1;
mapheaderinfo[level]->cachedcan = tempcanlist[i];
if (++i < listlen)
continue;
goto cansdone;
}
}
for (level = 0; level < nummapheaders; level++)
{
if (!mapheaderinfo[level]
|| !(mapheaderinfo[level]->typeoflevel & TOL_RACE)
|| mapheaderinfo[level]->cachedcan != 0)
continue;
gamedata->spraycans[tempcanlist[i]].map = level + 1;
mapheaderinfo[level]->cachedcan = tempcanlist[i];
if (++i < listlen)
continue;
goto cansdone;
}
cansdone:
#ifdef PARANOIA
for (i = 1; i < MAXCANCOLORS; i++)
{
if (gamedata->spraycans[i].map == 0)
I_Error("CANPROBLEM - BAD MAP FOR CAN %d\n", i);
if (gamedata->spraycans[i].map > nummapheaders)
I_Error("CANPROBLEM - TOO BIG MAP FOR CAN %d\n", i);
if (mapheaderinfo[gamedata->spraycans[i].map-1]->cachedcan != i)
I_Error("CANPROBLEM - MAP AND CAN DISAGREE FOR %d (%d)\n", i, mapheaderinfo[gamedata->spraycans[i].map-1]->cachedcan);
}
for (i = 0; i < nummapheaders; i++)
{
if (!mapheaderinfo[i] || mapheaderinfo[i]->cachedcan == 0)
continue;
if (mapheaderinfo[i]->cachedcan > MAXCANCOLORS)
I_Error("MAPPROBLEM - BAD CAN FOR MAP %d\n", i);
if (gamedata->spraycans[mapheaderinfo[i]->cachedcan].map-1 != i)
I_Error("MAPPROBLEM - CAN AND MAP DISAGREE FOR %d (%d)\n", i, gamedata->spraycans[mapheaderinfo[i]->cachedcan].map-1);
}
#endif
gamedata->allspraycansplaced = true;
}
void M_FinaliseGameData(void)
{
//M_PopulateChallengeGrid(); -- This can be done lazily when we actually need it
// Place the spraycans, which CAN'T be done lazily.
M_AssignSpraycans();
// 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

View file

@ -251,6 +251,12 @@ typedef enum {
GDGT_MAX
} roundsplayed_t;
struct candata_t
{
UINT16 map;
boolean got;
};
// GAMEDATA STRUCTURE
// Everything that would get saved in gamedata.dat
struct gamedata_t
@ -272,6 +278,10 @@ struct gamedata_t
boolean unlocked[MAXUNLOCKABLES];
boolean unlockpending[MAXUNLOCKABLES];
// SPRAYCANS COLLECTED
boolean allspraycansplaced;
candata_t spraycans[MAXCANCOLORS];
// CHALLENGE GRID
UINT16 challengegridwidth;
UINT16 *challengegrid;

View file

@ -460,6 +460,8 @@ static void P_ClearSingleMapHeaderInfo(INT16 num)
mapheaderinfo[num]->justPlayed = 0;
mapheaderinfo[num]->anger = 0;
mapheaderinfo[num]->cachedcan = 0;
mapheaderinfo[num]->customopts = NULL;
mapheaderinfo[num]->numCustomOptions = 0;
}

View file

@ -238,6 +238,7 @@ TYPEDEF (condition_t);
TYPEDEF (conditionset_t);
TYPEDEF (emblem_t);
TYPEDEF (unlockable_t);
TYPEDEF (candata_t);
TYPEDEF (gamedata_t);
TYPEDEF (challengegridextradata_t);