diff --git a/src/acs/call-funcs.cpp b/src/acs/call-funcs.cpp index 4cbdcdecc..119238cbe 100644 --- a/src/acs/call-funcs.cpp +++ b/src/acs/call-funcs.cpp @@ -1173,6 +1173,7 @@ bool CallFunc_HaveUnlockableTrigger(ACSVM::Thread *thread, const ACSVM::Word *ar { UINT8 id = 0; bool unlocked = false; + auto info = &static_cast(thread)->info; (void)argC; @@ -1182,9 +1183,11 @@ bool CallFunc_HaveUnlockableTrigger(ACSVM::Thread *thread, const ACSVM::Word *ar { CONS_Printf("Bad unlockable trigger ID %d\n", id); } - else + else if ((info != NULL) + && (info->mo != NULL && P_MobjWasRemoved(info->mo) == false) + && (info->mo->player != NULL)) { - unlocked = (unlocktriggers & (1 << id)); + unlocked = (info->mo->player->roundconditions.unlocktriggers & (1 << id)); } thread->dataStk.push(unlocked); @@ -1354,7 +1357,7 @@ bool CallFunc_BreakTheCapsules(ACSVM::Thread *thread, const ACSVM::Word *argV, A (void)argV; (void)argC; - thread->dataStk.push(battlecapsules); + thread->dataStk.push(battleprisons); return false; } diff --git a/src/command.c b/src/command.c index a3062a478..6cacd3bcd 100644 --- a/src/command.c +++ b/src/command.c @@ -2242,6 +2242,11 @@ void CV_AddValue(consvar_t *var, INT32 increment) if (var->PossibleValue == kartspeed_cons_t) max++; // Accommodate KARTSPEED_AUTO } + else if (var->PossibleValue == gpdifficulty_cons_t + && !M_SecretUnlocked(SECRET_MASTERMODE, false)) + { + max = KARTSPEED_HARD+1; + } } #ifdef PARANOIA if (currentindice == -1) diff --git a/src/command.h b/src/command.h index d1ddfa337..8ad27bfbc 100644 --- a/src/command.h +++ b/src/command.h @@ -174,11 +174,7 @@ extern CV_PossibleValue_t CV_Unsigned[]; extern CV_PossibleValue_t CV_Natural[]; // SRB2kart -#define KARTSPEED_AUTO -1 -#define KARTSPEED_EASY 0 -#define KARTSPEED_NORMAL 1 -#define KARTSPEED_HARD 2 -#define KARTGP_MASTER 3 // Not a speed setting, gives the hardest speed with maxed out bots +// the KARTSPEED and KARTGP were previously defined here, but moved to doomstat to avoid circular dependencies extern CV_PossibleValue_t kartspeed_cons_t[], dummykartspeed_cons_t[], gpdifficulty_cons_t[]; extern consvar_t cv_execversion; diff --git a/src/d_clisrv.c b/src/d_clisrv.c index ad4adb7eb..080aa1cd1 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -824,7 +824,7 @@ static boolean CL_SendJoin(void) for (; i < MAXSPLITSCREENPLAYERS; i++) strncpy(netbuffer->u.clientcfg.names[i], va("Player %c", 'A' + i), MAXPLAYERNAME); - memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false), MAXAVAILABILITY*sizeof(UINT8)); + memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, false), MAXAVAILABILITY*sizeof(UINT8)); return HSendPacket(servernode, false, 0, sizeof (clientconfig_pak)); } @@ -3723,7 +3723,7 @@ static void Got_RemovePlayer(UINT8 **p, INT32 playernum) static void Got_AddBot(UINT8 **p, INT32 playernum) { INT16 newplayernum; - UINT8 i, skinnum = 0; + UINT8 skinnum = 0; UINT8 difficulty = DIFFICULTBOT; if (playernum != serverplayer && !IsPlayerAdmin(playernum)) @@ -3753,14 +3753,8 @@ static void Got_AddBot(UINT8 **p, INT32 playernum) playernode[newplayernum] = servernode; - // todo find a way to have all auto unlocked for dedicated - if (playeringame[0]) - { - for (i = 0; i < MAXAVAILABILITY; i++) - { - players[newplayernum].availabilities[i] = players[0].availabilities[i]; - } - } + // this will permit unlocks + memcpy(&players[newplayernum].availabilities, R_GetSkinAvailabilities(false, true), MAXAVAILABILITY*sizeof(UINT8)); players[newplayernum].splitscreenindex = 0; players[newplayernum].bot = true; @@ -3933,7 +3927,7 @@ boolean SV_SpawnServer(void) // strictly speaking, i'm not convinced the following is necessary // but I'm not confident enough to remove it entirely in case it breaks something { - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false); + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, false); SINT8 node = 0; for (; node < MAXNETNODES; node++) result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, cv_playername[0].zstring, cv_playername[1].zstring, cv_playername[2].zstring, cv_playername[3].zstring); diff --git a/src/d_main.c b/src/d_main.c index 1910630bb..7b75ff8d0 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -1045,6 +1045,9 @@ void D_ClearState(void) cursongcredit.def = NULL; S_StopSounds(); + if (gamedata && gamedata->deferredsave) + G_SaveGameData(); + G_SetGamestate(GS_NULL); wipegamestate = GS_NULL; } @@ -1920,23 +1923,32 @@ void D_SRB2Main(void) newskill = (INT16)j; } - if (grandprixinfo.gp == true) + // Invalidate if locked. + if ((newskill >= KARTSPEED_HARD && !M_SecretUnlocked(SECRET_HARDSPEED, true)) + || (newskill >= KARTGP_MASTER && !M_SecretUnlocked(SECRET_MASTERMODE, true))) { - if (newskill == KARTGP_MASTER) - { - grandprixinfo.masterbots = true; - newskill = KARTSPEED_HARD; - } - - grandprixinfo.gamespeed = newskill; - } - else if (newskill == KARTGP_MASTER) - { - newskill = KARTSPEED_HARD; + newskill = -1; } if (newskill != -1) + { + if (grandprixinfo.gp == true) + { + if (newskill == KARTGP_MASTER) + { + grandprixinfo.masterbots = true; + newskill = KARTSPEED_HARD; + } + + grandprixinfo.gamespeed = newskill; + } + else if (newskill == KARTGP_MASTER) + { + newskill = KARTSPEED_HARD; + } + CV_SetValue(&cv_kartspeed, newskill); + } } if (server && (dedicated || !M_CheckParm("+map"))) @@ -1946,7 +1958,7 @@ void D_SRB2Main(void) I_Error("Can't get first map of gametype\n"); } - if (M_MapLocked(pstartmap)) + if (pstartmap != 1 && M_MapLocked(pstartmap)) { G_SetUsedCheats(); } diff --git a/src/d_netcmd.c b/src/d_netcmd.c index ee5f2f40a..fac8b476f 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2811,7 +2811,7 @@ static void Command_Map_f(void) return; } - if (M_MapLocked(newmapnum)) + if (/*newmapnum != 1 &&*/ M_MapLocked(newmapnum)) { ischeating = true; } diff --git a/src/d_player.h b/src/d_player.h index 2dd19dd44..c343a5701 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -333,6 +333,29 @@ struct botvars_t tic_t spindashconfirm; // When high enough, they will try spindashing }; +// player_t struct for round-specific condition tracking + +struct roundconditions_t +{ + // Reduce the number of checks by only updating when this is true + boolean checkthisframe; + + // Trivial Yes/no events across multiple UCRP's + boolean fell_off; + boolean touched_offroad; + boolean touched_sneakerpanel; + boolean debt_rings; + boolean tripwire_hyuu; + boolean spb_neuter; + boolean landmine_dunk; + boolean hit_midair; + + mobjeflag_t wet_player; + + // 32 triggers, one bit each, for map execution + UINT32 unlocktriggers; +}; + // player_t struct for all skybox variables struct skybox_t { mobj_t * viewpoint; @@ -695,6 +718,7 @@ struct player_t #endif sonicloopvars_t loop; + roundconditions_t roundconditions; }; #ifdef __cplusplus diff --git a/src/deh_soc.c b/src/deh_soc.c index 5c281db50..f286a7ce1 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2280,16 +2280,24 @@ void readunlockable(MYFILE *f, INT32 num) unlockables[num].type = SECRET_FOLLOWER; else if (fastcmp(word2, "HARDSPEED")) unlockables[num].type = SECRET_HARDSPEED; + else if (fastcmp(word2, "MASTERMODE")) + unlockables[num].type = SECRET_MASTERMODE; else if (fastcmp(word2, "ENCORE")) unlockables[num].type = SECRET_ENCORE; - else if (fastcmp(word2, "LEGACYBOXRUMMAGE")) - unlockables[num].type = SECRET_LEGACYBOXRUMMAGE; else if (fastcmp(word2, "TIMEATTACK")) unlockables[num].type = SECRET_TIMEATTACK; - else if (fastcmp(word2, "BREAKTHECAPSULES")) - unlockables[num].type = SECRET_BREAKTHECAPSULES; + else if (fastcmp(word2, "PRISONBREAK")) + unlockables[num].type = SECRET_PRISONBREAK; else if (fastcmp(word2, "SPECIALATTACK")) unlockables[num].type = SECRET_SPECIALATTACK; + else if (fastcmp(word2, "SPBATTACK")) + unlockables[num].type = SECRET_SPBATTACK; + else if (fastcmp(word2, "ONLINE")) + unlockables[num].type = SECRET_ONLINE; + else if (fastcmp(word2, "ADDONS")) + unlockables[num].type = SECRET_ADDONS; + else if (fastcmp(word2, "EGGTV")) + unlockables[num].type = SECRET_EGGTV; else if (fastcmp(word2, "SOUNDTEST")) unlockables[num].type = SECRET_SOUNDTEST; else if (fastcmp(word2, "ALTTITLE")) @@ -2328,18 +2336,23 @@ void readunlockable(MYFILE *f, INT32 num) static void readcondition(UINT8 set, UINT32 id, char *word2) { INT32 i; - char *params[4]; // condition, requirement, extra info, extra info + char *params[5]; // condition, requirement, extra info, extra info, stringvar char *spos; + char *stringvar = NULL; conditiontype_t ty; - INT32 re; + INT32 re = 0; INT16 x1 = 0, x2 = 0; INT32 offset = 0; +#if 0 + char *endpos = word2 + strlen(word2); +#endif + spos = strtok(word2, " "); - for (i = 0; i < 4; ++i) + for (i = 0; i < 5; ++i) { if (spos != NULL) { @@ -2356,13 +2369,56 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) return; } - if (fastcmp(params[0], "PLAYTIME") - || (++offset && fastcmp(params[0], "MATCHESPLAYED"))) + if (fastcmp(params[0], "PLAYTIME")) { PARAMCHECK(1); ty = UC_PLAYTIME + offset; re = atoi(params[1]); } + else if (fastcmp(params[0], "ROUNDSPLAYED")) + { + PARAMCHECK(1); + ty = UC_ROUNDSPLAYED; + re = atoi(params[1]); + x1 = GDGT_MAX; + + if (re == 0) + { + deh_warning("Rounds played requirement is %d for condition ID %d", re, id+1); + return; + } + + if (params[2]) + { + if (fastcmp(params[2], "RACE")) + x1 = GDGT_RACE; + else if (fastcmp(params[2], "BATTLE")) + x1 = GDGT_BATTLE; + else if (fastcmp(params[2], "PRISONS")) + x1 = GDGT_PRISONS; + else if (fastcmp(params[2], "SPECIAL")) + x1 = GDGT_SPECIAL; + else if (fastcmp(params[2], "CUSTOM")) + x1 = GDGT_CUSTOM; + else + { + deh_warning("gametype requirement \"%s\" invalid for condition ID %d", params[2], id+1); + return; + } + } + } + else if (fastcmp(params[0], "TOTALRINGS")) + { + PARAMCHECK(1); + ty = UC_TOTALRINGS; + re = atoi(params[1]); + + if (re < 2 || re > GDMAX_RINGS) + { + deh_warning("Total Rings requirement %d out of range (%d - %d) for condition ID %d", re, 2, GDMAX_RINGS, id+1); + return; + } + } else if (fastcmp(params[0], "POWERLEVEL")) { PARAMCHECK(2); @@ -2394,7 +2450,9 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) re = atoi(params[1]); } else if ((offset=0) || fastcmp(params[0], "MAPVISITED") - || (++offset && fastcmp(params[0], "MAPBEATEN"))) + || (++offset && fastcmp(params[0], "MAPBEATEN")) + || (++offset && fastcmp(params[0], "MAPENCORE")) + || (++offset && fastcmp(params[0], "MAPSPBATTACK"))) { PARAMCHECK(1); ty = UC_MAPVISITED + offset; @@ -2419,17 +2477,26 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) return; } } - else if (fastcmp(params[0], "TRIGGER")) + else if ((offset=0) || fastcmp(params[0], "ALLCHAOS") + || (++offset && fastcmp(params[0], "ALLSUPER")) + || (++offset && fastcmp(params[0], "ALLEMERALDS"))) { - PARAMCHECK(1); - ty = UC_TRIGGER; - re = atoi(params[1]); - - // constrained by 32 bits - if (re < 0 || re > 31) + //PARAMCHECK(1); + ty = UC_ALLCHAOS + offset; + re = KARTSPEED_NORMAL; + if (params[1]) { - deh_warning("Trigger ID %d out of range (0 - 31) for condition ID %d", re, id+1); - return; + if (fastcmp(params[1], "NORMAL")) + ; + else if (fastcmp(params[1], "HARD")) + x1 = KARTSPEED_HARD; + else if (fastcmp(params[1], "MASTER")) + x1 = KARTGP_MASTER; + else + { + deh_warning("gamespeed requirement \"%s\" invalid for condition ID %d", params[1], id+1); + return; + } } } else if (fastcmp(params[0], "TOTALMEDALS")) @@ -2474,13 +2541,273 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) return; } } + else if ((offset=0) || fastcmp(params[0], "ADDON") + || (++offset && fastcmp(params[0], "REPLAY")) + || (++offset && fastcmp(params[0], "CRASH"))) + { + //PARAMCHECK(1); + ty = UC_ADDON + offset; + } + else if ((offset=0) || fastcmp(params[0], "AND") + || (++offset && fastcmp(params[0], "COMMA"))) + { + //PARAMCHECK(1); + ty = UC_AND + offset; + } + else if ((offset=0) || fastcmp(params[0], "PREFIX_GRANDPRIX") + || (++offset && fastcmp(params[0], "PREFIX_BONUSROUND")) + || (++offset && fastcmp(params[0], "PREFIX_TIMEATTACK")) + || (++offset && fastcmp(params[0], "PREFIX_PRISONBREAK")) + || (++offset && fastcmp(params[0], "PREFIX_SEALEDSTAR"))) + { + //PARAMCHECK(1); + ty = UCRP_PREFIX_GRANDPRIX + offset; + } + else if ((offset=0) || fastcmp(params[0], "PREFIX_ISMAP") + || (++offset && fastcmp(params[0], "ISMAP"))) + { + PARAMCHECK(1); + ty = UCRP_PREFIX_ISMAP + offset; + re = G_MapNumber(params[1]); + + if (re >= nummapheaders) + { + deh_warning("Invalid level %s for condition ID %d", params[1], id+1); + return; + } + } + else if (fastcmp(params[0], "ISCHARACTER")) + { + PARAMCHECK(1); + ty = UCRP_ISCHARACTER; +#if 0 + { + re = R_SkinAvailable(params[1]); + + if (re < 0) + { + deh_warning("Invalid character %s for condition ID %d", params[1], id+1); + return; + } + } +#else + { + stringvar = Z_StrDup(params[1]); + re = -1; + } +#endif + } + else if (fastcmp(params[0], "ISENGINECLASS")) + { + PARAMCHECK(1); + ty = UCRP_ISENGINECLASS; + if (!params[1][1] + && params[1][0] >= 'A' && params[1][0] <= 'J') + { + re = params[1][0] - 'A'; + } + else + { + deh_warning("engine class requirement \"%s\" invalid for condition ID %d", params[1], id+1); + return; + } + } + else if (fastcmp(params[0], "ISDIFFICULTY")) + { + //PARAMCHECK(1); + ty = UCRP_ISDIFFICULTY; + re = KARTSPEED_NORMAL; + if (params[1]) + { + if (fastcmp(params[1], "NORMAL")) + ; + else if (fastcmp(params[1], "HARD")) + x1 = KARTSPEED_HARD; + else if (fastcmp(params[1], "MASTER")) + x1 = KARTGP_MASTER; + else + { + deh_warning("gamespeed requirement \"%s\" invalid for condition ID %d", params[1], id+1); + return; + } + } + } + else if (fastcmp(params[0], "PODIUMCUP")) + { + PARAMCHECK(1); + ty = UCRP_PODIUMCUP; + { + cupheader_t *cup = kartcupheaders; + while (cup) + { + if (!strcmp(cup->name, params[1])) + break; + cup = cup->next; + } + + if (!cup) + { + deh_warning("Invalid cup %s for condition ID %d", params[1], id+1); + return; + } + + re = cup->id; + } + + if (params[2]) + { + if (params[2][0] && !params[2][1]) + { + x2 = 1; + + switch (params[2][0]) + { + case 'E': { x1 = GRADE_E; break; } + case 'D': { x1 = GRADE_D; break; } + case 'C': { x1 = GRADE_C; break; } + case 'B': { x1 = GRADE_B; break; } + case 'A': { x1 = GRADE_A; break; } + case 'S': { x1 = GRADE_S; break; } + default: + deh_warning("Invalid grade %s for condition ID %d", params[2], id+1); + return; + } + } + else if ((offset=0) || fastcmp(params[2], "GOLD") + || (++offset && fastcmp(params[2], "SILVER")) + || (++offset && fastcmp(params[2], "BRONZE"))) + { + x1 = offset + 1; + } + else + { + deh_warning("Invalid cup result %s for condition ID %d", params[2], id+1); + return; + } + + } + } + else if ((offset=0) || fastcmp(params[0], "PODIUMEMERALD") + || (++offset && fastcmp(params[0], "PODIUMPRIZE"))) + { + //PARAMCHECK(1); + ty = UCRP_PODIUMEMERALD + offset; + } + else if ((offset=0) || fastcmp(params[0], "FINISHCOOL") + || (++offset && fastcmp(params[0], "FINISHALLPRISONS")) + || (++offset && fastcmp(params[0], "NOCONTEST"))) + { + //PARAMCHECK(1); + ty = UCRP_FINISHCOOL + offset; + } + else if ((offset=0) || fastcmp(params[0], "FINISHPLACE") + || (++offset && fastcmp(params[0], "FINISHPLACEEXACT"))) + { + PARAMCHECK(1); + ty = UCRP_FINISHPLACE + offset; + re = atoi(params[1]); + + if (re < 1 || re > MAXPLAYERS) + { + deh_warning("Invalid place %s for condition ID %d", params[1], id+1); + return; + } + } + else if ((offset=0) || fastcmp(params[0], "FINISHTIME") + || (++offset && fastcmp(params[0], "FINISHTIMEEXACT")) + || (++offset && fastcmp(params[0], "FINISHTIMELEFT"))) + { + PARAMCHECK(1); + ty = UCRP_FINISHTIME + offset; + re = get_number(params[1]); + + if (re < 0) + { + deh_warning("Invalid time %s for condition ID %d", params[1], id+1); + return; + } + } + else if (fastcmp(params[0], "TRIGGER")) + { + PARAMCHECK(2); // strictly speaking at LEAST two + ty = UCRP_TRIGGER; + re = atoi(params[1]); + + // constrained by 32 bits + if (re < 0 || re > 31) + { + deh_warning("Trigger ID %d out of range (0 - 31) for condition ID %d", re, id+1); + return; + } + + // The following undid the effects of strtok. + // Unfortunately, there is no way it can reasonably undo the effects of strupr. + // If we want custom descriptions for map execution triggers, we're gonna need a different method. +#if 0 + // undo affect of strtok + i = 5; + // so spos will still be the strtok from earlier + while (i >= 2) + { + if (!spos) + continue; + while (*spos != '\0') + spos++; + if (spos < endpos) + *spos = ' '; + spos = params[--i]; + } +#endif + + stringvar = Z_StrDup(params[2]); + } + else if ((offset=0) || fastcmp(params[0], "FALLOFF") + || (++offset && fastcmp(params[0], "TOUCHOFFROAD")) + || (++offset && fastcmp(params[0], "TOUCHSNEAKERPANEL")) + || (++offset && fastcmp(params[0], "RINGDEBT"))) + { + PARAMCHECK(1); + ty = UCRP_FALLOFF + offset; + re = 1; + + if (params[1][0] == 'F' || params[1][0] == 'N' || params[1][0] == '0') + re = 0; + } + else if ((offset=0) || fastcmp(params[0], "TRIPWIREHYUU") + || (++offset && fastcmp(params[0], "SPBNEUTER")) + || (++offset && fastcmp(params[0], "LANDMINEDUNK")) + || (++offset && fastcmp(params[0], "HITMIDAIR"))) + { + //PARAMCHECK(1); + ty = UCRP_TRIPWIREHYUU + offset; + } + else if (fastcmp(params[0], "WETPLAYER")) + { + PARAMCHECK(1); + ty = UCRP_WETPLAYER; + re = MFE_UNDERWATER; + x1 = 1; + + if (params[2]) + { + if (fastcmp(params[2], "STRICT")) + re |= MFE_TOUCHWATER; + else + { + deh_warning("liquid strictness requirement \"%s\" invalid for condition ID %d", params[2], id+1); + return; + } + } + + stringvar = Z_StrDup(params[1]); + } else { deh_warning("Invalid condition name %s for condition ID %d", params[0], id+1); return; } - M_AddRawCondition(set, (UINT8)id, ty, re, x1, x2); + M_AddRawCondition(set, (UINT8)id, ty, re, x1, x2, stringvar); } void readconditionset(MYFILE *f, UINT8 setnum) @@ -2633,6 +2960,10 @@ void readmaincfg(MYFILE *f, boolean mainfile) clear_emblems(); //clear_levels(); doClearLevels = true; + + G_ClearRecords(); + M_ClearStats(); + M_ClearSecrets(); } #ifndef DEVELOP else if (!mainfile && !gamedataadded) @@ -3101,7 +3432,16 @@ void readcupheader(MYFILE *f, cupheader_t *cup) i = atoi(word2); // used for numerical settings strupr(word2); - if (fastcmp(word, "ICON")) + if (fastcmp(word, "MONITOR")) + { + if (i > 0 && i < 10) + cup->monitor = i; + else if (!word2[0] || word2[1] != '\0' || word2[0] == '0') + deh_warning("%s Cup: Invalid monitor type \"%s\" (should be 1-9 or A-Z)\n", cup->name, word2); + else + cup->monitor = (word2[0] - 'A') + 10; + } + else if (fastcmp(word, "ICON")) { deh_strlcpy(cup->icon, word2, sizeof(cup->icon), va("%s Cup: icon", cup->name)); diff --git a/src/deh_tables.c b/src/deh_tables.c index 66271a5f4..ea5991732 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -5837,7 +5837,7 @@ const char *const GAMETYPERULE_LIST[] = { "KARMA", "ITEMARROWS", - "CAPSULES", + "PRISONS", "CATCHER", "ROLLINGSTART", "SPECIALSTART", diff --git a/src/dehacked.c b/src/dehacked.c index 7a916c950..9e50a9f42 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -486,6 +486,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) { cup = Z_Calloc(sizeof (cupheader_t), PU_STATIC, NULL); cup->id = numkartcupheaders; + cup->monitor = 1; deh_strlcpy(cup->name, word2, sizeof(cup->name), va("Cup header %s: name", word2)); if (prev != NULL) diff --git a/src/doomstat.h b/src/doomstat.h index d79f3497f..2bf01f261 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -120,6 +120,30 @@ struct recorddata_t //UINT16 rings; ///< Rings when the level was finished. }; +#define KARTSPEED_AUTO -1 +#define KARTSPEED_EASY 0 +#define KARTSPEED_NORMAL 1 +#define KARTSPEED_HARD 2 +#define KARTGP_MASTER 3 // Not a speed setting, gives the hardest speed with maxed out bots +#define KARTGP_MAX 4 + +typedef enum +{ + GRADE_E, + GRADE_D, + GRADE_C, + GRADE_B, + GRADE_A, + GRADE_S +} gp_rank_e; + +struct cupwindata_t +{ + UINT8 best_placement; + gp_rank_e best_grade; + boolean got_emerald; +}; + // mapvisited is now a set of flags that says what we've done in the map. #define MV_VISITED (1) #define MV_BEATEN (1<<1) @@ -352,6 +376,7 @@ struct customoption_t struct cupheader_t { UINT16 id; ///< Cup ID + UINT8 monitor; ///< Monitor graphic 1-9 or A-Z char name[15]; ///< Cup title (14 chars) char icon[9]; ///< Name of the icon patch char *levellist[CUPCACHE_MAX]; ///< List of levels that belong to this cup @@ -359,6 +384,7 @@ struct cupheader_t UINT8 numlevels; ///< Number of levels defined in levellist UINT8 numbonus; ///< Number of bonus stages defined UINT8 emeraldnum; ///< ID of Emerald to use for special stage (1-7 for Chaos Emeralds, 8-14 for Super Emeralds, 0 for no emerald) + cupwindata_t windata[4]; ///< Data for cup visitation cupheader_t *next; ///< Next cup in linked list }; @@ -528,7 +554,7 @@ enum GameTypeRules GTR_ITEMARROWS = 1<<9, // Show item box arrows above players // Bonus gametype rules - GTR_CAPSULES = 1<<10, // Can enter Break The Capsules mode + GTR_PRISONS = 1<<10, // Can enter Prison Break mode GTR_CATCHER = 1<<11, // UFO Catcher (only works with GTR_CIRCUIT) GTR_ROLLINGSTART = 1<<12, // Rolling start (only works with GTR_CIRCUIT) GTR_SPECIALSTART = 1<<13, // White fade instant start diff --git a/src/f_finale.c b/src/f_finale.c index c53650f62..3e595b61c 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -310,6 +310,8 @@ void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean reset void F_StartIntro(void) { + cursongcredit.def = NULL; + if (gamestate) { F_WipeStartScreen(); @@ -1090,7 +1092,7 @@ void F_GameEvaluationTicker(void) { ++gamedata->timesBeaten; - M_UpdateUnlockablesAndExtraEmblems(true); + M_UpdateUnlockablesAndExtraEmblems(true, true); G_SaveGameData(); } else diff --git a/src/g_demo.c b/src/g_demo.c index 7785763f7..7c3385091 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -26,6 +26,7 @@ #include "g_game.h" #include "g_demo.h" #include "m_misc.h" +#include "m_cond.h" #include "k_menu.h" #include "m_argv.h" #include "hu_stuff.h" @@ -2240,7 +2241,7 @@ static void G_SaveDemoSkins(UINT8 **pp) { char skin[16]; UINT8 i; - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true); + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true, false); WRITEUINT8((*pp), numskins); for (i = 0; i < numskins; i++) @@ -4189,7 +4190,15 @@ void G_SaveDemo(void) if (!modeattacking) { if (demo.savemode == DSM_SAVED) + { CONS_Printf(M_GetText("Demo %s recorded\n"), demoname); + if (gamedata->eversavedreplay == false) + { + gamedata->eversavedreplay = true; + M_UpdateUnlockablesAndExtraEmblems(true, true); + G_SaveGameData(); + } + } else CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname); } diff --git a/src/g_game.c b/src/g_game.c index 34d74cfe3..bb803209b 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -465,6 +465,8 @@ void G_AllocMainRecordData(INT16 i) void G_ClearRecords(void) { INT16 i; + cupheader_t *cup; + for (i = 0; i < nummapheaders; ++i) { if (mapheaderinfo[i]->mainrecord) @@ -473,6 +475,11 @@ void G_ClearRecords(void) mapheaderinfo[i]->mainrecord = NULL; } } + + for (cup = kartcupheaders; cup; cup = cup->next) + { + memset(&cup->windata, 0, sizeof(cup->windata)); + } } // For easy retrieval of records @@ -662,8 +669,8 @@ void G_UpdateRecords(void) S_StartSound(NULL, sfx_ncitem); } - M_UpdateUnlockablesAndExtraEmblems(true); - G_SaveGameData(); + M_UpdateUnlockablesAndExtraEmblems(true, true); + gamedata->deferredsave = true; } // @@ -2367,19 +2374,6 @@ static inline void G_PlayerFinishLevel(INT32 player) p->starpostnum = 0; memset(&p->respawn, 0, sizeof (p->respawn)); - - // SRB2kart: Increment the "matches played" counter. - if (player == consoleplayer) - { - if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) - { - gamedata->matchesplayed++; - M_UpdateUnlockablesAndExtraEmblems(true); - G_SaveGameData(); - } - - legitimateexit = false; - } } // @@ -2454,6 +2448,9 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) INT32 kickstartaccel; boolean enteredGame; + roundconditions_t roundconditions; + boolean saveroundconditions; + score = players[player].score; lives = players[player].lives; ctfteam = players[player].ctfteam; @@ -2544,6 +2541,8 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) khudfinish = 0; khudcardanimation = 0; starpostnum = 0; + + saveroundconditions = false; } else { @@ -2592,6 +2591,9 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) starpostnum = players[player].starpostnum; pflags |= (players[player].pflags & (PF_STASIS|PF_ELIMINATED|PF_NOCONTEST|PF_FAULT|PF_LOSTLIFE)); + + memcpy(&roundconditions, &players[player].roundconditions, sizeof (roundconditions)); + saveroundconditions = true; } if (!betweenmaps) @@ -2676,6 +2678,9 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) memcpy(&p->itemRoulette, &itemRoulette, sizeof (p->itemRoulette)); memcpy(&p->respawn, &respawn, sizeof (p->respawn)); + if (saveroundconditions) + memcpy(&p->roundconditions, &roundconditions, sizeof (p->roundconditions)); + if (follower) P_RemoveMobj(follower); @@ -3302,7 +3307,7 @@ static gametype_t defaultgametypes[] = { "Battle", "GT_BATTLE", - GTR_SPHERES|GTR_BUMPERS|GTR_PAPERITEMS|GTR_POWERSTONES|GTR_KARMA|GTR_ITEMARROWS|GTR_CAPSULES|GTR_BATTLESTARTS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_CLOSERPLAYERS, + GTR_SPHERES|GTR_BUMPERS|GTR_PAPERITEMS|GTR_POWERSTONES|GTR_KARMA|GTR_ITEMARROWS|GTR_PRISONS|GTR_BATTLESTARTS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_CLOSERPLAYERS, TOL_BATTLE, int_scoreortimeattack, 0, @@ -3866,7 +3871,7 @@ static void G_UpdateVisited(void) if ((earnedEmblems = M_CompletionEmblems())) CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : ""); - M_UpdateUnlockablesAndExtraEmblems(true); + M_UpdateUnlockablesAndExtraEmblems(true, true); G_SaveGameData(); } @@ -3946,7 +3951,7 @@ static void G_GetNextMap(void) { gp_rank_e grade = K_CalculateGPGrade(&grandprixinfo.rank); - if (grade >= GRADE_A) // On A rank pace? Then you get a chance for S rank! + if (grade >= GRADE_A && grandprixinfo.gamespeed >= KARTSPEED_NORMAL) // On A rank pace? Then you get a chance for S rank! { const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[CUPCACHE_SPECIAL]; if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum]) @@ -3954,6 +3959,13 @@ static void G_GetNextMap(void) grandprixinfo.eventmode = GPEVENT_SPECIAL; nextmap = cupLevelNum; newgametype = G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel); + + if (gamedata->everseenspecial == false) + { + gamedata->everseenspecial = true; + M_UpdateUnlockablesAndExtraEmblems(true, true); + G_SaveGameData(); + } } } } @@ -4001,7 +4013,7 @@ static void G_GetNextMap(void) } else { - nextmap = prevmap; // Prevent uninitialised use + nextmap = 0; // Prevent uninitialised use -- go to TEST RUN, it's very obvious } grandprixinfo.roundnum++; @@ -4164,6 +4176,33 @@ static void G_DoCompleted(void) if (modeattacking && pausedelay) pausedelay = 0; + // We do this here so Challenges-related sounds aren't insta-killed + S_StopSounds(); + + if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) + { + UINT8 roundtype = GDGT_CUSTOM; + + if (gametype == GT_RACE) + roundtype = GDGT_RACE; + else if (gametype == GT_BATTLE) + roundtype = (battleprisons ? GDGT_PRISONS : GDGT_BATTLE); + else if (gametype == GT_SPECIAL || gametype == GT_VERSUS) + roundtype = GDGT_SPECIAL; + + gamedata->roundsplayed[roundtype]++; + gamedata->pendingkeyrounds++; + + // Done before forced addition of PF_NOCONTEST to make UCRP_NOCONTEST harder to achieve + M_UpdateUnlockablesAndExtraEmblems(true, true); + gamedata->deferredsave = true; + } + + if (gamedata->deferredsave) + G_SaveGameData(); + + legitimateexit = false; + gameaction = ga_nothing; if (metalplayback) @@ -4174,7 +4213,7 @@ static void G_DoCompleted(void) G_SetGamestate(GS_NULL); wipegamestate = GS_NULL; - grandprixinfo.rank.capsules += numtargets; + grandprixinfo.rank.prisons += numtargets; grandprixinfo.rank.position = MAXPLAYERS; for (i = 0; i < MAXPLAYERS; i++) @@ -4218,8 +4257,6 @@ static void G_DoCompleted(void) if (automapactive) AM_Stop(); - S_StopSounds(); - prevmap = (INT16)(gamemap-1); if (!demo.playback) @@ -4486,7 +4523,7 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 1 // Change every format update +#define GD_VERSIONMINOR 2 // Change every format update static const char *G_GameDataFolder(void) { @@ -4509,36 +4546,36 @@ void G_LoadGameData(void) //For records UINT32 numgamedatamapheaders; + UINT32 numgamedatacups; // Stop saving, until we successfully load it again. gamedata->loaded = false; // Clear things so previously read gamedata doesn't transfer // to new gamedata + // see also M_EraseDataResponse G_ClearRecords(); // records + M_ClearStats(); // statistics M_ClearSecrets(); // emblems, unlocks, maps visited, etc - gamedata->totalplaytime = 0; // total play time (separate from all) - gamedata->matchesplayed = 0; // SRB2Kart: matches played & finished - if (M_CheckParm("-nodata")) { // Don't load at all. + // The following used to be in M_ClearSecrets, but that was silly. + M_UpdateUnlockablesAndExtraEmblems(false, true); return; } if (M_CheckParm("-resetdata")) { // Don't load, but do save. (essentially, reset) - gamedata->loaded = true; - return; + goto finalisegamedata; } if (P_SaveBufferFromFile(&save, va(pandf, srb2home, gamedatafilename)) == false) { // No gamedata. We can save a new one. - gamedata->loaded = true; - return; + goto finalisegamedata; } // Version check @@ -4559,13 +4596,44 @@ void G_LoadGameData(void) P_SaveBufferFree(&save); I_Error("Game data is from the future! (expected %d, got %d)\nRename or delete %s (maybe in %s) and try again.", GD_VERSIONMINOR, versionMinor, gamedatafilename, gdfolder); } - if (versionMinor == 0) + if ((versionMinor == 0 || versionMinor == 1) +#ifdef DEVELOP + || M_CheckParm("-resetchallengegrid") +#endif + ) { gridunusable = true; } + if (versionMinor > 1) + { + gamedata->evercrashed = (boolean)READUINT8(save.p); + } + gamedata->totalplaytime = READUINT32(save.p); - gamedata->matchesplayed = READUINT32(save.p); + + if (versionMinor > 1) + { + gamedata->totalrings = READUINT32(save.p); + + for (i = 0; i < GDGT_MAX; i++) + { + gamedata->roundsplayed[i] = READUINT32(save.p); + } + + gamedata->pendingkeyrounds = READUINT32(save.p); + gamedata->pendingkeyroundoffset = READUINT8(save.p); + gamedata->keyspending = READUINT8(save.p); + gamedata->chaokeys = READUINT16(save.p); + + gamedata->everloadedaddon = (boolean)READUINT8(save.p); + gamedata->eversavedreplay = (boolean)READUINT8(save.p); + gamedata->everseenspecial = (boolean)READUINT8(save.p); + } + else + { + save.p += 4; // no direct equivalent to matchesplayed + } { // Quick & dirty hash for what mod this save file is for. @@ -4672,7 +4740,7 @@ void G_LoadGameData(void) G_AllocMainRecordData((INT16)i); mapheaderinfo[i]->mainrecord->time = rectime; mapheaderinfo[i]->mainrecord->lap = reclap; - CONS_Printf("ID %d, Time = %d, Lap = %d\n", i, rectime/35, reclap/35); + //CONS_Printf("ID %d, Time = %d, Lap = %d\n", i, rectime/35, reclap/35); } } else @@ -4683,19 +4751,62 @@ void G_LoadGameData(void) } } + if (versionMinor > 1) + { + numgamedatacups = READUINT32(save.p); + + for (i = 0; i < numgamedatacups; i++) + { + char cupname[16]; + cupheader_t *cup; + + // Find the relevant cup. + READSTRINGN(save.p, cupname, sizeof(cupname)); + for (cup = kartcupheaders; cup; cup = cup->next) + { + if (strcmp(cup->name, cupname)) + continue; + break; + } + + // Digest its data... + for (j = 0; j < KARTGP_MAX; j++) + { + rtemp = READUINT8(save.p); + + // ...but only record it if we actually found the associated cup. + if (cup) + { + cup->windata[j].best_placement = (rtemp & 0x0F); + cup->windata[j].best_grade = (rtemp & 0x70)>>4; + if (rtemp & 0x80) + { + if (j == 0) + goto datacorrupt; + + cup->windata[j].got_emerald = true; + } + } + } + } + } + // done P_SaveBufferFree(&save); - // 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 the corruption landing point below, - // which can accidentally delete players' legitimate data if the code ever has any tiny mistakes! - gamedata->loaded = true; + finalisegamedata: + { + // 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 the corruption landing point below, + // 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); + // Silent update unlockables in case they're out of sync with conditions + M_UpdateUnlockablesAndExtraEmblems(false, true); - return; + return; + } // Landing point for corrupt gamedata datacorrupt: @@ -4710,18 +4821,46 @@ void G_LoadGameData(void) } } +// G_DirtyGameData +// Modifies the gamedata as little as possible to maintain safety in a crash event, while still recording it. +void G_DirtyGameData(void) +{ + FILE *handle = NULL; + const UINT8 writebytesource = true; + + if (gamedata) + gamedata->evercrashed = true; + + //if (FIL_WriteFileOK(name)) + handle = fopen(va(pandf, srb2home, gamedatafilename), "r+"); + + if (!handle) + return; + + // Write a dirty byte immediately after the gamedata check + minor version. + if (fseek(handle, 5, SEEK_SET) != -1) + fwrite(&writebytesource, 1, 1, handle); + + fclose(handle); + + return; +} + // G_SaveGameData // Saves the main data file, which stores information such as emblems found, etc. void G_SaveGameData(void) { size_t length; - INT32 i, j; + INT32 i, j, numcups; + cupheader_t *cup; UINT8 btemp; savebuffer_t save = {0}; if (gamedata == NULL || !gamedata->loaded) return; // If never loaded (-nodata), don't save + gamedata->deferredsave = false; + if (usedCheats) { #ifdef DEVELOP @@ -4730,12 +4869,27 @@ void G_SaveGameData(void) return; } - length = (4+1+4+4+1+(MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+4+4+2); + length = (4+1+1+ + 4+4+ + (4*GDGT_MAX)+ + 4+1+1+2+ + 1+1+1+ + 4+ + (MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+ + 4+2); + if (gamedata->challengegrid) { length += gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; } - length += nummapheaders * (MAXMAPLUMPNAME+1+4+4); + length += 4 + (nummapheaders * (MAXMAPLUMPNAME+1+4+4)); + + numcups = 0; + for (cup = kartcupheaders; cup; cup = cup->next) + { + numcups++; + } + length += 4 + (numcups * (4+16)); if (P_SaveBufferAlloc(&save, length) == false) { @@ -4747,8 +4901,30 @@ void G_SaveGameData(void) WRITEUINT32(save.p, GD_VERSIONCHECK); // 4 WRITEUINT8(save.p, GD_VERSIONMINOR); // 1 + + // Crash dirtiness + // cannot move, see G_DirtyGameData + WRITEUINT8(save.p, gamedata->evercrashed); // 1 + + // Statistics + WRITEUINT32(save.p, gamedata->totalplaytime); // 4 - WRITEUINT32(save.p, gamedata->matchesplayed); // 4 + WRITEUINT32(save.p, gamedata->totalrings); // 4 + + for (i = 0; i < GDGT_MAX; i++) // 4 * GDGT_MAX + { + WRITEUINT32(save.p, gamedata->roundsplayed[i]); + } + + WRITEUINT32(save.p, gamedata->pendingkeyrounds); // 4 + WRITEUINT8(save.p, gamedata->pendingkeyroundoffset); // 1 + WRITEUINT8(save.p, gamedata->keyspending); // 1 + WRITEUINT16(save.p, gamedata->chaokeys); // 2 + + WRITEUINT8(save.p, gamedata->everloadedaddon); // 1 + WRITEUINT8(save.p, gamedata->eversavedreplay); // 1 + WRITEUINT8(save.p, gamedata->everseenspecial); // 1 + WRITEUINT32(save.p, quickncasehash(timeattackfolder, 64)); // To save space, use one bit per collected/achieved/unlocked flag @@ -4808,7 +4984,7 @@ void G_SaveGameData(void) for (i = 0; i < nummapheaders; i++) // nummapheaders * (255+1+4+4) { - // For figuring out which header to assing it to on load + // For figuring out which header to assign it to on load WRITESTRINGN(save.p, mapheaderinfo[i]->lumpname, MAXMAPLUMPNAME); WRITEUINT8(save.p, (mapheaderinfo[i]->mapvisited & MV_MAX)); @@ -4825,6 +5001,24 @@ void G_SaveGameData(void) } } + WRITEUINT32(save.p, numcups); // 4 + + for (cup = kartcupheaders; cup; cup = cup->next) + { + // For figuring out which header to assign it to on load + WRITESTRINGN(save.p, cup->name, 16); + + for (i = 0; i < KARTGP_MAX; i++) + { + btemp = min(cup->windata[i].best_placement, 0x0F); + btemp |= (cup->windata[i].best_grade<<4); + if (i != 0 && cup->windata[i].got_emerald == true) + btemp |= 0x80; + + WRITEUINT8(save.p, btemp); // 4 * numcups + } + } + length = save.p - save.buffer; FIL_WriteFile(va(pandf, srb2home, gamedatafilename), save.buffer, length); @@ -5175,9 +5369,6 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr } } - // Reset unlockable triggers - unlocktriggers = 0; - // clear itemfinder, just in case if (!dedicated) // except in dedicated servers, where it is not registered and can actually I_Error debug builds CV_StealthSetValue(&cv_itemfinder, 0); diff --git a/src/g_game.h b/src/g_game.h index ed38b4402..3f122f6c4 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -176,6 +176,7 @@ boolean G_IsTitleCardAvailable(void); void G_LoadGame(UINT32 slot, INT16 mapoverride); void G_SaveGameData(void); +void G_DirtyGameData(void); void G_SaveGame(UINT32 slot, INT16 mapnum); diff --git a/src/hu_stuff.c b/src/hu_stuff.c index 44264b48f..5a8069414 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -2474,8 +2474,8 @@ static void HU_DrawRankings(void) // draw the current gametype in the lower right if (grandprixinfo.gp == true) V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Grand Prix"); - else if (battlecapsules) - V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Capsules"); + else if (battleprisons) + V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Prisons"); else if (gametype >= 0 && gametype < numgametypes) V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, gametypes[gametype]->name); @@ -2533,11 +2533,11 @@ static void HU_DrawRankings(void) } // Right hand side - if (battlecapsules == true) + if (battleprisons == true) { if (numtargets < maptargets) { - V_DrawCenteredString(256, 8, 0, "CAPSULES"); + V_DrawCenteredString(256, 8, 0, "PRISONS"); V_DrawCenteredString(256, 16, hilicol, va("%d", maptargets - numtargets)); } } diff --git a/src/k_battle.c b/src/k_battle.c index 77a292cce..31052cb6f 100644 --- a/src/k_battle.c +++ b/src/k_battle.c @@ -26,7 +26,7 @@ struct battleovertime battleovertime; // Capsules mode enabled for this map? -boolean battlecapsules = false; +boolean battleprisons = false; // box respawning in battle mode INT32 nummapboxes = 0; @@ -38,8 +38,8 @@ UINT8 numtargets = 0; // Capsules busted INT32 K_StartingBumperCount(void) { - if (battlecapsules) - return 0; // always 1 hit in Break the Capsules + if (battleprisons) + return 0; // always 1 hit in Prison Break return cv_kartbumpers.value; } @@ -143,7 +143,7 @@ void K_CheckBumpers(void) if (numingame <= 1) { - if ((gametyperules & GTR_CAPSULES) && (K_CanChangeRules(true) == true)) + if ((gametyperules & GTR_PRISONS) && (K_CanChangeRules(true) == true)) { // Reset map to turn on battle capsules if (server) @@ -343,7 +343,7 @@ void K_RunPaperItemSpawners(void) UINT8 pcount = 0; INT16 i; - if (battlecapsules) + if (battleprisons) { // Gametype uses paper items, but this specific expression doesn't return; @@ -771,7 +771,7 @@ void K_BattleInit(boolean singleplayercontext) { size_t i; - if ((gametyperules & GTR_CAPSULES) && singleplayercontext && !battlecapsules) + if ((gametyperules & GTR_PRISONS) && singleplayercontext && !battleprisons) { mapthing_t *mt = mapthings; for (i = 0; i < nummapthings; i++, mt++) @@ -782,7 +782,7 @@ void K_BattleInit(boolean singleplayercontext) maptargets++; } - battlecapsules = true; + battleprisons = true; } if (gametyperules & GTR_BUMPERS) diff --git a/src/k_battle.h b/src/k_battle.h index d1927f256..4e1dda2c8 100644 --- a/src/k_battle.h +++ b/src/k_battle.h @@ -18,7 +18,7 @@ extern struct battleovertime fixed_t x, y, z; ///< Position to center on } battleovertime; -extern boolean battlecapsules; +extern boolean battleprisons; extern INT32 nummapboxes, numgotboxes; // keep track of spawned battle mode items extern UINT8 maptargets, numtargets; diff --git a/src/k_boss.c b/src/k_boss.c index ff10231ea..7b0a8fe96 100644 --- a/src/k_boss.c +++ b/src/k_boss.c @@ -1,7 +1,7 @@ // SONIC ROBO BLAST 2 KART //----------------------------------------------------------------------------- -// Copyright (C) 2018-2022 by Viv "toaster" Grannell -// Copyright (C) 2018-2022 by Kart Krew +// Copyright (C) 2018-2023 by Vivian "toastergrl" Grannell +// Copyright (C) 2018-2023 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. diff --git a/src/k_boss.h b/src/k_boss.h index 2ce43adea..8e41e0cdf 100644 --- a/src/k_boss.h +++ b/src/k_boss.h @@ -1,7 +1,7 @@ // SONIC ROBO BLAST 2 KART //----------------------------------------------------------------------------- -// Copyright (C) 2018-2022 by Viv "toaster" Grannell -// Copyright (C) 2018-2022 by Kart Krew +// Copyright (C) 2018-2023 by Vivian "toastergrl" Grannell +// Copyright (C) 2018-2023 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. diff --git a/src/k_bot.c b/src/k_bot.c index b76423438..3e490031a 100644 --- a/src/k_bot.c +++ b/src/k_bot.c @@ -108,13 +108,15 @@ boolean K_AddBot(UINT8 skin, UINT8 difficulty, UINT8 *p) --------------------------------------------------*/ void K_UpdateMatchRaceBots(void) { + const UINT8 defaultbotskin = R_BotDefaultSkin(); const UINT8 difficulty = cv_kartbot.value; UINT8 pmax = min((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), cv_maxconnections.value); UINT8 numplayers = 0; UINT8 numbots = 0; UINT8 numwaiting = 0; SINT8 wantedbots = 0; - boolean skinusable[MAXSKINS]; + UINT8 usableskins = 0; + UINT8 grabskins[MAXSKINS+1]; UINT8 i; if (!server) @@ -122,18 +124,12 @@ void K_UpdateMatchRaceBots(void) return; } - // init usable bot skins list - for (i = 0; i < MAXSKINS; i++) + // Init usable bot skins list + for (i = 0; i < numskins; i++) { - if (i < numskins) - { - skinusable[i] = true; - } - else - { - skinusable[i] = false; - } + grabskins[usableskins++] = i; } + grabskins[usableskins] = MAXSKINS; if (cv_maxplayers.value > 0) { @@ -146,7 +142,7 @@ void K_UpdateMatchRaceBots(void) { if (!players[i].spectator) { - skinusable[players[i].skin] = false; + grabskins[players[i].skin] = MAXSKINS; if (players[i].bot) { @@ -185,48 +181,42 @@ void K_UpdateMatchRaceBots(void) { // We require MORE bots! UINT8 newplayernum = 0; - boolean usedallskins = false; if (dedicated) { newplayernum = 1; } + // Rearrange usable bot skins list to prevent gaps for randomised selection + for (i = 0; i < usableskins; i++) + { + if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true))) + continue; + while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins], true))) + { + usableskins--; + } + grabskins[i] = grabskins[usableskins]; + grabskins[usableskins] = MAXSKINS; + } + while (numbots < wantedbots) { - UINT8 skin = M_RandomKey(numskins); + UINT8 skinnum = defaultbotskin; - if (usedallskins == false) + if (usableskins > 0) { - UINT8 loops = 0; - - while (!skinusable[skin]) - { - if (loops >= numskins) - { - // no more skins, stick to our first choice - usedallskins = true; - break; - } - - skin++; - - if (skin >= numskins) - { - skin = 0; - } - - loops++; - } + UINT8 index = M_RandomKey(usableskins); + skinnum = grabskins[index]; + grabskins[index] = grabskins[--usableskins]; } - if (!K_AddBot(skin, difficulty, &newplayernum)) + if (!K_AddBot(skinnum, difficulty, &newplayernum)) { // Not enough player slots to add the bot, break the loop. break; } - skinusable[skin] = false; numbots++; } } diff --git a/src/k_collide.c b/src/k_collide.c index 1f3b3e318..5d089c67f 100644 --- a/src/k_collide.c +++ b/src/k_collide.c @@ -406,7 +406,15 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2) // Banana snipe! if (t1->health > 1) + { + if (t1->target && t1->target->player) + { + t1->target->player->roundconditions.landmine_dunk = true; + t1->target->player->roundconditions.checkthisframe = true; + } + S_StartSound(t2, sfx_bsnipe); + } if (t2->player->flamedash && t2->player->itemtype == KITEM_FLAMESHIELD) { diff --git a/src/k_grandprix.c b/src/k_grandprix.c index bc4779386..61044aa68 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -95,25 +95,6 @@ INT16 K_CalculateGPRankPoints(UINT8 position, UINT8 numplayers) return points; } -/*-------------------------------------------------- - UINT8 K_BotDefaultSkin(void) - - See header file for description. ---------------------------------------------------*/ -UINT8 K_BotDefaultSkin(void) -{ - const char *defaultbotskinname = "eggrobo"; - INT32 defaultbotskin = R_SkinAvailable(defaultbotskinname); - - if (defaultbotskin == -1) - { - // This shouldn't happen, but just in case - defaultbotskin = 0; - } - - return (UINT8)defaultbotskin; -} - /*-------------------------------------------------- UINT8 K_GetGPPlayerCount(UINT8 humans) @@ -139,7 +120,7 @@ UINT8 K_GetGPPlayerCount(UINT8 humans) --------------------------------------------------*/ void K_InitGrandPrixBots(void) { - const UINT8 defaultbotskin = K_BotDefaultSkin(); + const UINT8 defaultbotskin = R_BotDefaultSkin(); const UINT8 startingdifficulty = K_BotStartingDifficulty(grandprixinfo.gamespeed); UINT8 difficultylevels[MAXPLAYERS]; @@ -532,7 +513,7 @@ void K_IncreaseBotDifficulty(player_t *bot) --------------------------------------------------*/ void K_RetireBots(void) { - const UINT8 defaultbotskin = K_BotDefaultSkin(); + const UINT8 defaultbotskin = R_BotDefaultSkin(); SINT8 newDifficulty; UINT8 usableskins; diff --git a/src/k_grandprix.h b/src/k_grandprix.h index efa251246..9bfc982e5 100644 --- a/src/k_grandprix.h +++ b/src/k_grandprix.h @@ -75,16 +75,6 @@ UINT8 K_BotStartingDifficulty(SINT8 value); INT16 K_CalculateGPRankPoints(UINT8 position, UINT8 numplayers); -/*-------------------------------------------------- - UINT8 K_BotDefaultSkin(void); - - Returns the skin number of the skin the game - uses as a fallback option. ---------------------------------------------------*/ - -UINT8 K_BotDefaultSkin(void); - - /*-------------------------------------------------- UINT8 K_GetGPPlayerCount(UINT8 humans) diff --git a/src/k_hud.c b/src/k_hud.c index 61578e8d7..710c28058 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -2855,7 +2855,7 @@ static void K_drawKartBumpersOrKarma(void) V_DrawScaledPatch(fx-2 + (flipflag ? (SHORT(kp_ringstickersplit[1]->width) - 3) : 0), fy, V_HUDTRANS|V_SLIDEIN|splitflags|flipflag, kp_ringstickersplit[0]); V_DrawScaledPatch(fx+22, fy, V_HUDTRANS|V_SLIDEIN|splitflags, frameslash); - if (battlecapsules) + if (battleprisons) { V_DrawMappedPatch(fx+1, fy-2, V_HUDTRANS|V_SLIDEIN|splitflags, kp_rankcapsule, NULL); @@ -2911,7 +2911,7 @@ static void K_drawKartBumpersOrKarma(void) } else { - if (battlecapsules) + if (battleprisons) { if (numtargets > 9 && maptargets > 9) V_DrawMappedPatch(LAPS_X, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_capsulestickerwide, NULL); @@ -3801,7 +3801,7 @@ static void K_drawKartMinimap(void) workingPic = kp_capsuleminimap[(mobj->extravalue1 != 0 ? 1 : 0)]; break; case MT_CDUFO: - if (battlecapsules) //!battleprisons + if (battleprisons) workingPic = kp_capsuleminimap[2]; break; default: @@ -4244,7 +4244,7 @@ static void K_drawBattleFullscreen(void) if (K_IsPlayerLosing(stplyr)) p = kp_battlelose; - else if (stplyr->position == 1 && (!battlecapsules || numtargets >= maptargets)) + else if (stplyr->position == 1 && (!battleprisons || numtargets >= maptargets)) p = kp_battlewin; V_DrawFixedPatch(x<pflags & PF_NOCONTEST) return true; - if (battlecapsules && numtargets == 0) + if (battleprisons && numtargets == 0) return true; // Didn't even TRY? if (player->position == 1) @@ -558,7 +558,7 @@ boolean K_TimeAttackRules(void) return true; } - if (battlecapsules == true) + if (battleprisons == true) { // Break the Capsules always uses Time Attack // rules, since you can bring 2-4 players in @@ -1129,6 +1129,13 @@ static void K_UpdateOffroad(player_t *player) if (player->offroad > offroadstrength) player->offroad = offroadstrength; + + if (player->roundconditions.touched_offroad == false + && player->offroad > (2*offroadstrength) / TICRATE) + { + player->roundconditions.touched_offroad = true; + player->roundconditions.checkthisframe = true; + } } else player->offroad = 0; @@ -4223,7 +4230,15 @@ void K_ApplyTripWire(player_t *player, tripwirestate_t state) K_TumblePlayer(player, NULL, NULL); if (state == TRIPSTATE_PASSED) + { S_StartSound(player->mo, sfx_ssa015); + if (player->roundconditions.tripwire_hyuu == false + && player->hyudorotimer > 0) + { + player->roundconditions.tripwire_hyuu = true; + player->roundconditions.checkthisframe = true; + } + } else if (state == TRIPSTATE_BLOCKED) { S_StartSound(player->mo, sfx_kc40); @@ -5823,6 +5838,13 @@ void K_DoSneaker(player_t *player, INT32 type) { const fixed_t intendedboost = FRACUNIT/2; + if (player->roundconditions.touched_sneakerpanel == false + && player->floorboost != 0) + { + player->roundconditions.touched_sneakerpanel = true; + player->roundconditions.checkthisframe = true; + } + if (player->floorboost == 0 || player->floorboost == 3) { const sfxenum_t normalsfx = sfx_cdfm01; @@ -7067,7 +7089,7 @@ static void K_UpdateEngineSounds(player_t *player) const UINT16 buttons = K_GetKartButtons(player); - INT32 class, s, w; // engine class number + INT32 class; // engine class number UINT8 volume = 255; fixed_t volumedampen = FRACUNIT; @@ -7082,17 +7104,7 @@ static void K_UpdateEngineSounds(player_t *player) return; } - s = (player->kartspeed - 1) / 3; - w = (player->kartweight - 1) / 3; - -#define LOCKSTAT(stat) \ - if (stat < 0) { stat = 0; } \ - if (stat > 2) { stat = 2; } - LOCKSTAT(s); - LOCKSTAT(w); -#undef LOCKSTAT - - class = s + (3*w); + class = R_GetEngineClass(player->kartspeed, player->kartweight, 0); // there are no unique sounds for ENGINECLASS_J #if 0 if ((leveltime % 8) != ((player-players) % 8)) // Per-player offset, to make engines sound distinct! @@ -11603,7 +11615,7 @@ tic_t K_TimeLimitForGametype(void) // Grand Prix if (!K_CanChangeRules(true)) { - if (battlecapsules) + if (battleprisons) { return 20*TICRATE; } @@ -11617,7 +11629,7 @@ tic_t K_TimeLimitForGametype(void) } // No time limit for Break the Capsules FREE PLAY - if (battlecapsules) + if (battleprisons) { return 0; } @@ -11668,7 +11680,7 @@ UINT32 K_PointLimitForGametype(void) boolean K_Cooperative(void) { - if (battlecapsules) + if (battleprisons) { return true; } diff --git a/src/k_menu.h b/src/k_menu.h index 17c8d4665..cff331600 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -698,6 +698,7 @@ boolean M_CharacterSelectHandler(INT32 choice); void M_CharacterSelectTick(void); boolean M_CharacterSelectQuit(void); +void M_SetupPlayMenu(INT32 choice); void M_SetupGametypeMenu(INT32 choice); void M_SetupRaceMenu(INT32 choice); @@ -727,13 +728,16 @@ extern struct levellist_s { SINT8 cursor; UINT16 y; UINT16 dest; - INT16 choosemap; + UINT16 choosemap; + UINT16 mapcount; UINT8 newgametype; UINT8 guessgt; levelsearch_t levelsearch; boolean netgame; // Start the game in an actual server } levellist; +extern cupheader_t dummy_lostandfound; + boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch); UINT16 M_CountLevelsToShowInList(levelsearch_t *levelsearch); UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch); @@ -776,6 +780,7 @@ extern struct mpmenu_s { // See M_OptSelectTick, it'll make more sense there. Sorry if this is a bit of a mess! UINT8 room; + boolean roomforced; tic_t ticker; UINT8 servernum; @@ -1133,7 +1138,9 @@ void M_DrawAddons(void); #define CC_UNLOCKED 1 #define CC_TALLY 2 #define CC_ANIM 3 -#define CC_MAX 4 +#define CC_CHAOANIM 4 +#define CC_CHAONOPE 5 +#define CC_MAX 6 #define TILEFLIP_MAX 16 @@ -1144,7 +1151,6 @@ extern struct timeattackmenu_s { } timeattackmenu; - // Keep track of some pause menu data for visual goodness. extern struct challengesmenu_s { @@ -1163,6 +1169,7 @@ extern struct challengesmenu_s { boolean pending; boolean requestnew; + boolean chaokeyadd; boolean requestflip; @@ -1180,6 +1187,7 @@ boolean M_ChallengesInputs(INT32 ch); extern struct statisticsmenu_s { INT32 location; INT32 nummaps; + INT32 numextramedals; INT32 maxscroll; UINT16 *maplist; } statisticsmenu; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index cb9bbc1f7..39144a6d8 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -791,6 +791,8 @@ void M_DrawKartGamemodeMenu(void) for (i = 0; i < currentMenu->numitems; i++) { + INT32 type; + if (currentMenu->menuitems[i].status == IT_DISABLED) { continue; @@ -807,9 +809,12 @@ void M_DrawKartGamemodeMenu(void) } } - switch (currentMenu->menuitems[i].status & IT_DISPLAY) + type = (currentMenu->menuitems[i].status & IT_DISPLAY); + + switch (type) { case IT_STRING: + case IT_TRANSTEXT2: { UINT8 *colormap = NULL; @@ -823,7 +828,13 @@ void M_DrawKartGamemodeMenu(void) } V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, 0, W_CachePatchName("MENUPLTR", PU_CACHE), colormap); - V_DrawGamemodeString(x + 16, y - 3, V_ALLOWLOWERCASE, colormap, currentMenu->menuitems[i].text); + V_DrawGamemodeString(x + 16, y - 3, + (type == IT_TRANSTEXT2 + ? V_TRANSLUCENT + : 0 + )|V_ALLOWLOWERCASE, + colormap, + currentMenu->menuitems[i].text); } break; } @@ -1470,23 +1481,31 @@ static void M_DrawCharSelectPreview(UINT8 num) if (p->showextra == true) { + INT32 randomskin = 0; switch (p->mdepth) { - case CSSTEP_CHARS: // Character Select grid - V_DrawThinString(x-3, y+2, V_6WIDTHSPACE, va("Speed %u - Weight %u", p->gridx+1, p->gridy+1)); - break; case CSSTEP_ALTS: // Select clone case CSSTEP_READY: if (p->clonenum < setup_chargrid[p->gridx][p->gridy].numskins && setup_chargrid[p->gridx][p->gridy].skinlist[p->clonenum] < numskins) { - V_DrawThinString(x-3, y+2, V_6WIDTHSPACE, + V_DrawThinString(x-3, y+12, V_6WIDTHSPACE, skins[setup_chargrid[p->gridx][p->gridy].skinlist[p->clonenum]].name); + randomskin = (skins[setup_chargrid[p->gridx][p->gridy].skinlist[p->clonenum]].flags & SF_IRONMAN); } else { - V_DrawThinString(x-3, y+2, V_6WIDTHSPACE, va("BAD CLONENUM %u", p->clonenum)); + V_DrawThinString(x-3, y+12, V_6WIDTHSPACE, va("BAD CLONENUM %u", p->clonenum)); } + /* FALLTHRU */ + case CSSTEP_CHARS: // Character Select grid + V_DrawThinString(x-3, y+2, V_6WIDTHSPACE, va("Class %c (s %c - w %c)", + ('A' + R_GetEngineClass(p->gridx+1, p->gridy+1, randomskin)), + (randomskin + ? '?' : ('1'+p->gridx)), + (randomskin + ? '?' : ('1'+p->gridy)) + )); break; case CSSTEP_COLORS: // Select color if (p->color < numskincolors) @@ -2066,7 +2085,11 @@ static void M_DrawCupTitle(INT16 y, levelsearch_t *levelsearch) V_DrawScaledPatch(0, y, 0, W_CachePatchName("MENUHINT", PU_CACHE)); - if (levelsearch->cup) + if (levelsearch->cup == &dummy_lostandfound) + { + V_DrawCenteredLSTitleLowString(BASEVIDWIDTH/2, y+6, 0, "Lost and Found"); + } + else if (levelsearch->cup) { boolean unlocked = (M_GetFirstLevelInList(&temp, levelsearch) != NEXTMAP_INVALID); UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE); @@ -2095,6 +2118,8 @@ static void M_DrawCupTitle(INT16 y, levelsearch_t *levelsearch) void M_DrawCupSelect(void) { UINT8 i, j, temp = 0; + UINT8 *colormap = NULL; + cupwindata_t *windata = NULL; levelsearch_t templevelsearch = levellist.levelsearch; // full copy for (i = 0; i < CUPMENU_COLUMNS; i++) @@ -2105,28 +2130,82 @@ void M_DrawCupSelect(void) patch_t *patch = NULL; INT16 x, y; INT16 icony = 7; + char status = 'A'; + char monitor; + INT32 rankx = 0; if (!cupgrid.builtgrid[id]) break; templevelsearch.cup = cupgrid.builtgrid[id]; - /*if (templevelsearch.cup->emeraldnum == 0) - patch = W_CachePatchName("CUPMON3A", PU_CACHE); - else*/ if (templevelsearch.cup->emeraldnum > 7) + if (cupgrid.grandprix + && (cv_dummygpdifficulty.value >= 0 && cv_dummygpdifficulty.value < KARTGP_MAX)) { - patch = W_CachePatchName("CUPMON2A", PU_CACHE); - icony = 5; + UINT16 col = SKINCOLOR_NONE; + + windata = &templevelsearch.cup->windata[cv_dummygpdifficulty.value]; + + switch (windata->best_placement) + { + case 0: + break; + case 1: + col = SKINCOLOR_GOLD; + status = 'B'; + break; + case 2: + col = SKINCOLOR_SILVER; + status = 'B'; + break; + case 3: + col = SKINCOLOR_BRONZE; + status = 'B'; + break; + default: + col = SKINCOLOR_BEIGE; + break; + } + + if (col != SKINCOLOR_NONE) + colormap = R_GetTranslationColormap(TC_RAINBOW, col, GTC_MENUCACHE); + else + colormap = NULL; + } + + if (templevelsearch.cup == &dummy_lostandfound) + { + // No cup? Lost and found! + monitor = '0'; } else - patch = W_CachePatchName("CUPMON1A", PU_CACHE); + { + if (templevelsearch.cup->monitor < 10) + { + monitor = '0' + templevelsearch.cup->monitor; + + if (monitor == '2') + { + icony = 5; + rankx = 2; + } + } + else + { + monitor = 'A' + (templevelsearch.cup->monitor - 10); + } + } + + patch = W_CachePatchName(va("CUPMON%c%c", monitor, status), PU_CACHE); x = 14 + (i*42); y = 20 + (j*44) - (30*menutransition.tics); - V_DrawScaledPatch(x, y, 0, patch); + V_DrawFixedPatch((x)*FRACUNIT, (y)<icon, PU_CACHE)); V_DrawScaledPatch(x + 8, y + icony, 0, W_CachePatchName("CUPBOX", PU_CACHE)); + + if (!windata) + ; + else if (windata->best_placement != 0) + { + char gradeChar = '?'; + + switch (windata->best_grade) + { + case GRADE_E: { gradeChar = 'E'; break; } + case GRADE_D: { gradeChar = 'D'; break; } + case GRADE_C: { gradeChar = 'C'; break; } + case GRADE_B: { gradeChar = 'B'; break; } + case GRADE_A: { gradeChar = 'A'; break; } + case GRADE_S: { gradeChar = 'S'; break; } + default: { break; } + } + + V_DrawCharacter(x + 5 + rankx, y + icony + 14, gradeChar, false); // rank + + if (windata->got_emerald == true) + { + if (templevelsearch.cup->emeraldnum == 0) + V_DrawCharacter(x + 26 - rankx, y + icony + 14, '*', false); // rank + else + { + UINT16 col = SKINCOLOR_CHAOSEMERALD1 + (templevelsearch.cup->emeraldnum-1) % 7; + colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + + V_DrawFixedPatch((x + 26 - rankx)*FRACUNIT, (y + icony + 13)*FRACUNIT, FRACUNIT, 0, W_CachePatchName("K_EMERC", PU_CACHE), colormap); + } + } + } } } } @@ -2320,7 +2432,7 @@ void M_DrawLevelSelect(void) void M_DrawTimeAttack(void) { - INT16 map = levellist.choosemap; + UINT16 map = levellist.choosemap; INT16 t = (48*menutransition.tics); INT16 leftedge = 149+t+16; INT16 rightedge = 149+t+155; @@ -2366,7 +2478,7 @@ void M_DrawTimeAttack(void) K_drawKartTimestamp(timerec, 162+t, timeheight+6, 0, 1); // SPB Attack control hint + menu overlay - if (levellist.newgametype == GT_RACE && levellist.levelsearch.timeattack == true) + if (levellist.newgametype == GT_RACE && levellist.levelsearch.timeattack == true && M_SecretUnlocked(SECRET_SPBATTACK, true)) { const UINT8 anim_duration = 16; const UINT8 anim = (timeattackmenu.ticker % (anim_duration * 2)) < anim_duration; @@ -2379,10 +2491,9 @@ void M_DrawTimeAttack(void) else V_DrawScaledPatch(buttonx + 35, buttony - 3, V_SNAPTOLEFT, W_CachePatchName("TLB_IB", PU_CACHE)); - if (timeattackmenu.ticker > (timeattackmenu.spbflicker + TICRATE/6) || timeattackmenu.ticker % 2) + if ((timeattackmenu.spbflicker == 0 || timeattackmenu.ticker % 2) == (cv_dummyspbattack.value == 1)) { - if (cv_dummyspbattack.value) - V_DrawMappedPatch(buttonx + 7, buttony - 1, 0, W_CachePatchName("K_SPBATK", PU_CACHE), R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_MENUCACHE)); + V_DrawMappedPatch(buttonx + 7, buttony - 1, 0, W_CachePatchName("K_SPBATK", PU_CACHE), R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_MENUCACHE)); } } @@ -2776,8 +2887,12 @@ void M_DrawMPRoomSelect(void) // Draw buttons: - V_DrawFixedPatch(160< 2) { stat = 2; } + LOCKSTAT(s); + LOCKSTAT(w); + #undef LOCKSTAT + + V_DrawFill(4+16 + (s*5), (BASEVIDHEIGHT-(4+16)) + (w*5), 6, 6, 0); + } } break; } @@ -4968,14 +5146,27 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) UINT16 col = K_GetEffectiveFollowerColor(followers[fskin].defaultcolor, cv_playercolor[0].value); colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); M_DrawFollowerSprite(x - 16, y, fskin, false, 0, colormap, NULL); + + if (followers[fskin].category < numfollowercategories) + { + V_DrawFixedPatch(4*FRACUNIT, (BASEVIDHEIGHT-(4+16))*FRACUNIT, + FRACUNIT, + 0, W_CachePatchName(followercategories[followers[fskin].category].icon, PU_CACHE), + NULL); + } } break; } case SECRET_CUP: { levelsearch_t templevelsearch; + UINT32 i, id, maxid, offset; + cupheader_t *temp = M_UnlockableCup(ref); - templevelsearch.cup = M_UnlockableCup(ref); + if (!temp) + break; + + templevelsearch.cup = temp; templevelsearch.typeoflevel = G_TOLFlag(GT_RACE)|G_TOLFlag(GT_BATTLE); templevelsearch.cupmode = true; templevelsearch.timeattack = false; @@ -4983,17 +5174,87 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) M_DrawCupPreview(146, &templevelsearch); + maxid = id = (temp->id % 14); + offset = (temp->id - id) * 2; + while (temp && maxid < 14) + { + maxid++; + temp = temp->next; + } + + V_DrawFadeFill(4, (BASEVIDHEIGHT-(4+16)), 28 + offset, 16, 0, 31, challengetransparentstrength); + + for (i = 0; i < offset; i += 4) + { + V_DrawFill(4+1 + i, (BASEVIDHEIGHT-(4+16))+3, 2, 2, 15); + V_DrawFill(4+1 + i, (BASEVIDHEIGHT-(4+16))+8+3, 2, 2, 15); + } + + for (i = 0; i < 7; i++) + { + if (templevelsearch.cup && id == i) + { + V_DrawFill(offset + 4 + (i*4), (BASEVIDHEIGHT-(4+16)), 4, 8, 0); + } + else if (i < maxid) + { + V_DrawFill(offset + 4+1 + (i*4), (BASEVIDHEIGHT-(4+16))+3, 2, 2, 0); + } + + if (templevelsearch.cup && (templevelsearch.cup->id % 14) == i+7) + { + V_DrawFill(offset + 4 + (i*4), (BASEVIDHEIGHT-(4+16))+8, 4, 8, 0); + } + else if (i+7 < maxid) + { + V_DrawFill(offset + 4+1 + (i*4), (BASEVIDHEIGHT-(4+16))+8+3, 2, 2, 0); + } + } + break; } case SECRET_MAP: { + const char *gtname = "INVALID HEADER"; UINT16 mapnum = M_UnlockableMapNum(ref); + K_DrawMapThumbnail( - (x-30)<typeoflevel); + + if (guessgt == -1) + { + // No Time Attack support, so specify... + gtname = "Match Race/Online"; + } + else + { + if (guessgt == GT_VERSUS) + { + // Fudge since there's no Versus-specific menu right now... + guessgt = GT_SPECIAL; + } + + if (guessgt == GT_SPECIAL && !M_SecretUnlocked(SECRET_SPECIALATTACK, true)) + { + gtname = "???"; + } + else + { + gtname = gametypes[guessgt]->name; + } + } + } + + V_DrawThinString(1, BASEVIDHEIGHT-(9+3), V_ALLOWLOWERCASE|V_6WIDTHSPACE, gtname); + break; } case SECRET_ENCORE: @@ -5016,7 +5277,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) specialmap = tamapcache; break; } - case SECRET_BREAKTHECAPSULES: + case SECRET_PRISONBREAK: { static UINT16 btcmapcache = NEXTMAP_INVALID; if (btcmapcache > nummapheaders) @@ -5036,6 +5297,16 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) specialmap = sscmapcache; break; } + case SECRET_SPBATTACK: + { + static UINT16 spbmapcache = NEXTMAP_INVALID; + if (spbmapcache > nummapheaders) + { + spbmapcache = G_RandMap(G_TOLFlag(GT_RACE), -1, 2, 0, false, NULL); + } + specialmap = spbmapcache; + break; + } case SECRET_HARDSPEED: { static UINT16 hardmapcache = NEXTMAP_INVALID; @@ -5046,12 +5317,31 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) specialmap = hardmapcache; break; } + case SECRET_MASTERMODE: + { + static UINT16 mastermapcache = NEXTMAP_INVALID; + if (mastermapcache > nummapheaders) + { + mastermapcache = G_RandMap(G_TOLFlag(GT_RACE), -1, 2, 0, false, NULL); + } + specialmap = mastermapcache; + break; + } + case SECRET_ONLINE: + { + V_DrawFixedPatch(-3*FRACUNIT, (y-40)*FRACUNIT, + FRACUNIT, + 0, W_CachePatchName("EGGASTLA", PU_CACHE), + NULL); + break; + } case SECRET_ALTTITLE: { x = 8; y = BASEVIDHEIGHT-16; V_DrawGamemodeString(x, y - 32, V_ALLOWLOWERCASE, R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_PLAGUE, GTC_MENUCACHE), cv_alttitle.string); V_DrawThinString(x, y, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Press (A)"); + break; } default: { @@ -5079,6 +5369,13 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) V_DrawFixedPatch((x+40)<type == SECRET_SPBATTACK) + { + V_DrawFixedPatch((x+40-25)<type == SECRET_HARDSPEED) { V_DrawFixedPatch((x+40-25)<type == SECRET_MASTERMODE) + { + V_DrawFixedPatch((x+40-25)<chaokeys > 9) + { + offs -= 6; + if (gamedata->chaokeys > 99) + offs -= 2; // as far as we can go + } + + V_DrawFixedPatch((8+offs)*FRACUNIT, 5*FRACUNIT, FRACUNIT, 0, key, NULL); + V_DrawKartString((27+offs), 9-challengesmenu.unlockcount[CC_CHAOANIM], 0, va("%u", gamedata->chaokeys)); + + offs = challengekeybarwidth; + if (gamedata->chaokeys < GDMAX_CHAOKEYS) + offs = ((gamedata->pendingkeyroundoffset * challengekeybarwidth)/GDCONVERT_ROUNDSTOKEY); + + if (offs > 0) + V_DrawFill(1, 25, offs, 2, 0); + if (offs < challengekeybarwidth) + V_DrawFadeFill(1+offs, 25, challengekeybarwidth-offs, 2, 0, 31, challengetransparentstrength); + } + // Tally { str = va("%d/%d", @@ -5275,6 +5607,7 @@ challengedesc: #undef challengetransparentstrength #undef challengesgridstep +#undef challengekeybarwidth // Statistics menu @@ -5302,6 +5635,17 @@ static void M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y) curtype = 2; break; } + case ET_MAP: + { + if (((emblem->flags & ME_ENCORE) && !M_SecretUnlocked(SECRET_ENCORE, true)) + || ((emblem->flags & ME_SPBATTACK) && !M_SecretUnlocked(SECRET_SPBATTACK, true))) + { + emblem = M_GetLevelEmblems(-1); + continue; + } + curtype = 0; + break; + } default: curtype = 0; break; @@ -5334,27 +5678,53 @@ static void M_DrawStatsMaps(void) V_DrawCharacter(10, y-(skullAnimCounter/5), '\x1A' | highlightflags, false); // up arrow - while (statisticsmenu.maplist[++i] != NEXTMAP_INVALID) + while ((mnum = statisticsmenu.maplist[++i]) != NEXTMAP_INVALID) { if (location) { --location; continue; } - else if (dotopname) + + if (dotopname || mnum >= nummapheaders) { - V_DrawThinString(20, y, V_6WIDTHSPACE|highlightflags, "LEVEL NAME"); - V_DrawRightAlignedThinString(BASEVIDWIDTH-20, y, V_6WIDTHSPACE|highlightflags, "MEDALS"); + if (mnum >= nummapheaders) + { + mnum = statisticsmenu.maplist[1+i]; + if (mnum >= nummapheaders) + mnum = statisticsmenu.maplist[i-1]; + } + + if (mnum < nummapheaders) + { + const char *str; + + if (mapheaderinfo[mnum]->cup) + str = va("%s CUP", mapheaderinfo[mnum]->cup->name); + else + str = "LOST AND FOUND"; + + V_DrawThinString(20, y, V_6WIDTHSPACE|highlightflags, str); + } + + if (dotopname) + { + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, y, V_6WIDTHSPACE|highlightflags, "MEDALS"); + dotopname = false; + } + y += STATSSTEP; - dotopname = false; + if (y >= BASEVIDHEIGHT-STATSSTEP) + goto bottomarrow; + + continue; } - mnum = statisticsmenu.maplist[i]+1; - M_DrawMapMedals(mnum, 291, y); + M_DrawMapMedals(mnum+1, 291, y); { - char *title = G_BuildMapTitle(mnum); - V_DrawThinString(20, y, V_6WIDTHSPACE, title); + char *title = G_BuildMapTitle(mnum+1); + V_DrawThinString(24, y, V_6WIDTHSPACE, title); Z_Free(title); } @@ -5363,15 +5733,12 @@ static void M_DrawStatsMaps(void) if (y >= BASEVIDHEIGHT-STATSSTEP) goto bottomarrow; } - if (dotopname && !location) - { - V_DrawString(20, y, V_6WIDTHSPACE|highlightflags, "LEVEL NAME"); - V_DrawString(256, y, V_6WIDTHSPACE|highlightflags, "MEDALS"); - y += STATSSTEP; - } - else if (location) + if (location) --location; + if (statisticsmenu.numextramedals == 0) + goto bottomarrow; + // Extra Emblem headers for (i = 0; i < 2; ++i) { @@ -5410,7 +5777,6 @@ static void M_DrawStatsMaps(void) continue; } - if (i >= 0) { if (gamedata->unlocked[i]) { @@ -5425,7 +5791,7 @@ static void M_DrawStatsMaps(void) V_DrawSmallScaledPatch(291, y+1, V_6WIDTHSPACE, W_CachePatchName("NEEDIT", PU_CACHE)); } - V_DrawThinString(20, y, V_6WIDTHSPACE, va("%s", unlockables[i].name)); + V_DrawThinString(24, y, V_6WIDTHSPACE, va("%s", unlockables[i].name)); } y += STATSSTEP; @@ -5441,7 +5807,7 @@ bottomarrow: void M_DrawStatistics(void) { - char beststr[40]; + char beststr[256]; tic_t besttime = 0; @@ -5453,14 +5819,71 @@ void M_DrawStatistics(void) V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL); } + beststr[0] = 0; V_DrawThinString(20, 22, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Total Play Time:"); - V_DrawCenteredThinString(BASEVIDWIDTH/2, 32, V_6WIDTHSPACE, - va("%i hours, %i minutes, %i seconds", - G_TicsToHours(gamedata->totalplaytime), - G_TicsToMinutes(gamedata->totalplaytime, false), - G_TicsToSeconds(gamedata->totalplaytime))); - V_DrawThinString(20, 42, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Total Matches:"); - V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 42, V_6WIDTHSPACE, va("%i played", gamedata->matchesplayed)); + besttime = G_TicsToHours(gamedata->totalplaytime); + if (besttime) + { + if (besttime >= 24) + { + strcat(beststr, va("%u day%s, ", besttime/24, (besttime < 48 ? "" : "s"))); + besttime %= 24; + } + + strcat(beststr, va("%u hour%s, ", besttime, (besttime == 1 ? "" : "s"))); + } + besttime = G_TicsToMinutes(gamedata->totalplaytime, false); + if (besttime) + { + strcat(beststr, va("%u minute%s, ", besttime, (besttime == 1 ? "" : "s"))); + } + besttime = G_TicsToSeconds(gamedata->totalplaytime); + strcat(beststr, va("%i second%s", besttime, (besttime == 1 ? "" : "s"))); + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 22, V_6WIDTHSPACE, beststr); + beststr[0] = 0; + + V_DrawThinString(20, 32, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Total Rings:"); + if (gamedata->totalrings > GDMAX_RINGS) + { + sprintf(beststr, "%c999,999,999+", '\x82'); + } + else if (gamedata->totalrings >= 1000000) + { + sprintf(beststr, "%u,%03u,%03u", (gamedata->totalrings/1000000), (gamedata->totalrings/1000)%1000, (gamedata->totalrings%1000)); + } + else if (gamedata->totalrings >= 1000) + { + sprintf(beststr, "%u,%03u", (gamedata->totalrings/1000), (gamedata->totalrings%1000)); + } + else + { + sprintf(beststr, "%u", gamedata->totalrings); + } + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 32, V_6WIDTHSPACE, va("%s collected", beststr)); + + beststr[0] = 0; + V_DrawThinString(20, 42, V_6WIDTHSPACE|V_ALLOWLOWERCASE|highlightflags, "Total Rounds:"); + + strcat(beststr, va("%u Race", gamedata->roundsplayed[GDGT_RACE])); + + if (gamedata->roundsplayed[GDGT_PRISONS] > 0) + { + strcat(beststr, va(", %u Prisons", gamedata->roundsplayed[GDGT_PRISONS])); + } + + strcat(beststr, va(", %u Battle", gamedata->roundsplayed[GDGT_BATTLE])); + + if (gamedata->roundsplayed[GDGT_SPECIAL] > 0) + { + strcat(beststr, va(", %u Special", gamedata->roundsplayed[GDGT_SPECIAL])); + } + + if (gamedata->roundsplayed[GDGT_CUSTOM] > 0) + { + strcat(beststr, va(", %u Custom", gamedata->roundsplayed[GDGT_CUSTOM])); + } + + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 42, V_6WIDTHSPACE, beststr); if (!statisticsmenu.maplist) { @@ -5468,6 +5891,8 @@ void M_DrawStatistics(void) return; } + besttime = 0; + for (i = 0; i < nummapheaders; i++) { if (!mapheaderinfo[i] || (mapheaderinfo[i]->menuflags & (LF2_NOTIMEATTACK|LF2_HIDEINSTATS|LF2_HIDEINMENU))) diff --git a/src/k_menufunc.c b/src/k_menufunc.c index 038ed8047..2ed924289 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -15,6 +15,7 @@ #include "v_video.h" #include "f_finale.h" #include "m_misc.h" +#include "m_cond.h" #ifdef PC_DOS #include // for snprintf @@ -369,9 +370,17 @@ boolean M_Responder(event_t *ev) void M_PlayMenuJam(void) { menu_t *refMenu = (menuactive ? currentMenu : restoreMenu); + static boolean loserclubpermitted = false; + boolean loserclub = (loserclubpermitted && (gamedata->musicflags & GDMUSIC_LOSERCLUB)); if (challengesmenu.pending) + { + S_StopMusic(); + cursongcredit.def = NULL; + + loserclubpermitted = true; return; + } if (Playing()) return; @@ -382,15 +391,27 @@ void M_PlayMenuJam(void) { S_StopMusic(); cursongcredit.def = NULL; + return; } - else + else if (!loserclub) { if (NotCurrentlyPlaying(refMenu->music)) { S_ChangeMusicInternal(refMenu->music, true); S_ShowMusicCredit(); } + return; } + } + + if (loserclub) + { + if (refMenu != NULL && NotCurrentlyPlaying("LOSERC")) + { + S_ChangeMusicInternal("LOSERC", true); + S_ShowMusicCredit(); + } + return; } @@ -478,6 +499,7 @@ menu_t *M_SpecificMenuRestore(menu_t *torestore) } // One last catch. + M_SetupPlayMenu(-1); PLAY_CharSelectDef.prevMenu = &MainDef; return torestore; @@ -945,7 +967,7 @@ static void M_HandleMenuInput(void) { if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && majormods) { - M_StartMessage(M_GetText("This cannot be done with complex addons\nor in a cheated game.\n\nPress (B)\n"), NULL, MM_NOTHING); + M_StartMessage(M_GetText("This cannot be done with complex addons\nor in a cheated game.\n\nPress (B)"), NULL, MM_NOTHING); return; } } diff --git a/src/k_podium.c b/src/k_podium.c index 7f00834d9..519c654e3 100644 --- a/src/k_podium.c +++ b/src/k_podium.c @@ -13,7 +13,6 @@ #include "k_podium.h" #include "doomdef.h" -#include "doomstat.h" #include "d_main.h" #include "d_netcmd.h" #include "f_finale.h" @@ -69,6 +68,31 @@ boolean K_PodiumSequence(void) return (gamestate == GS_CEREMONY); } +/*-------------------------------------------------- + boolean K_PodiumRanking(void) + + See header file for description. +--------------------------------------------------*/ +boolean K_PodiumRanking(void) +{ + return (gamestate == GS_CEREMONY && podiumData.ranking == true); +} + +/*-------------------------------------------------- + boolean K_PodiumGrade(void) + + See header file for description. +--------------------------------------------------*/ +gp_rank_e K_PodiumGrade(void) +{ + if (K_PodiumSequence() == false) + { + return 0; + } + + return podiumData.grade; +} + /*-------------------------------------------------- UINT8 K_GetPodiumPosition(player_t *player) @@ -264,6 +288,10 @@ void K_FinishCeremony(void) } podiumData.ranking = true; + + // Play the noise now + M_UpdateUnlockablesAndExtraEmblems(true, true); + G_SaveGameData(); } /*-------------------------------------------------- @@ -273,6 +301,8 @@ void K_FinishCeremony(void) --------------------------------------------------*/ void K_ResetCeremony(void) { + UINT8 i; + memset(&podiumData, 0, sizeof(struct podiumData_s)); if (K_PodiumSequence() == false) @@ -280,8 +310,36 @@ void K_ResetCeremony(void) return; } + // Establish rank and grade for this play session. podiumData.rank = grandprixinfo.rank; podiumData.grade = K_CalculateGPGrade(&podiumData.rank); + + if (!grandprixinfo.cup) + { + return; + } + + // Write grade, position, and emerald-having-ness for later sessions! + i = (grandprixinfo.masterbots) ? KARTGP_MASTER : grandprixinfo.gamespeed; + + if ((grandprixinfo.cup->windata[i].best_placement == 0) // First run + || (podiumData.rank.position < grandprixinfo.cup->windata[i].best_placement)) // Later, better run + { + grandprixinfo.cup->windata[i].best_placement = podiumData.rank.position; + + // The following will not occour in unmodified builds, but pre-emptively sanitise gamedata if someone just changes MAXPLAYERS and calls it a day + if (grandprixinfo.cup->windata[i].best_placement > 0x0F) + grandprixinfo.cup->windata[i].best_placement = 0x0F; + } + + if (podiumData.grade > grandprixinfo.cup->windata[i].best_grade) + grandprixinfo.cup->windata[i].best_grade = podiumData.grade; + + if (i != KARTSPEED_EASY && podiumData.rank.specialWon == true) + grandprixinfo.cup->windata[i].got_emerald = true; + + // Save before playing the noise + G_SaveGameData(); } /*-------------------------------------------------- @@ -458,7 +516,7 @@ void K_CeremonyDrawer(void) case 5: { V_DrawString(x, y, V_ALLOWLOWERCASE, - va("CAPSULES: %d / %d", podiumData.rank.capsules, podiumData.rank.totalCapsules) + va("PRISONS: %d / %d", podiumData.rank.prisons, podiumData.rank.totalPrisons) ); break; } diff --git a/src/k_podium.h b/src/k_podium.h index 08357dd02..d7b04e3e4 100644 --- a/src/k_podium.h +++ b/src/k_podium.h @@ -14,6 +14,7 @@ #define __K_PODIUM__ #include "doomtype.h" +#include "doomstat.h" // gp_rank_e #include "d_event.h" #include "p_mobj.h" @@ -37,6 +38,37 @@ extern "C" { boolean K_PodiumSequence(void); +/*-------------------------------------------------- + boolean K_PodiumRanking(void); + + Returns whenver or not we are in the podium + final state. + + Input Arguments:- + N/A + + Return:- + true if we're in GS_CEREMONY, otherwise false. +--------------------------------------------------*/ + +boolean K_PodiumRanking(void); + + +/*-------------------------------------------------- + boolean K_PodiumGrade(void) + + Returns the podium grade. + + Input Arguments:- + N/A + + Return:- + gp_rank_e constant if we're in GS_CEREMONY, otherwise 0. +--------------------------------------------------*/ + +gp_rank_e K_PodiumGrade(void); + + /*-------------------------------------------------- UINT8 K_GetPodiumPosition(player_t *player); diff --git a/src/k_pwrlv.c b/src/k_pwrlv.c index db46a9266..aa98cbcc3 100644 --- a/src/k_pwrlv.c +++ b/src/k_pwrlv.c @@ -397,6 +397,7 @@ void K_CashInPowerLevels(void) { SINT8 powerType = K_UsingPowerLevels(); UINT8 i; + boolean gamedataupdate; //CONS_Printf("\n========\n"); //CONS_Printf("Cashing in power level changes...\n"); @@ -417,14 +418,19 @@ void K_CashInPowerLevels(void) { pr->powerlevels[powerType] = clientpowerlevels[i][powerType]; - M_UpdateUnlockablesAndExtraEmblems(true); - G_SaveGameData(); + gamedataupdate = true; } } clientPowerAdd[i] = 0; } + if (gamedataupdate) + { + M_UpdateUnlockablesAndExtraEmblems(true, true); + G_SaveGameData(); + } + //CONS_Printf("========\n"); } @@ -637,7 +643,7 @@ void K_PlayerForfeit(UINT8 playerNum, boolean pointLoss) { pr->powerlevels[powerType] = yourPower + inc; - M_UpdateUnlockablesAndExtraEmblems(true); + M_UpdateUnlockablesAndExtraEmblems(true, true); G_SaveGameData(); } } diff --git a/src/k_rank.c b/src/k_rank.c index 7bf277e82..2419804e8 100644 --- a/src/k_rank.c +++ b/src/k_rank.c @@ -304,7 +304,12 @@ void K_InitGrandPrixRank(gpRank_t *rankData) const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[i]; if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum] != NULL) { - laps += mapheaderinfo[cupLevelNum]->numlaps; + if (!cv_gptest.value) + { + laps += mapheaderinfo[cupLevelNum]->numlaps; + continue; + } + laps++; } } @@ -335,7 +340,7 @@ void K_InitGrandPrixRank(gpRank_t *rankData) continue; } - rankData->totalCapsules += RankCapsules_CountFromMap(virt); + rankData->totalPrisons += RankCapsules_CountFromMap(virt); vres_Free(virt); } } @@ -360,9 +365,9 @@ gp_rank_e K_CalculateGPGrade(gpRank_t *rankData) const INT32 positionWeight = 150; const INT32 pointsWeight = 100; const INT32 lapsWeight = 100; - const INT32 capsulesWeight = 100; + const INT32 prisonsWeight = 100; const INT32 ringsWeight = 50; - const INT32 total = positionWeight + pointsWeight + lapsWeight + capsulesWeight + ringsWeight; + const INT32 total = positionWeight + pointsWeight + lapsWeight + prisonsWeight + ringsWeight; const INT32 continuesPenalty = 20; INT32 ours = 0; @@ -385,9 +390,9 @@ gp_rank_e K_CalculateGPGrade(gpRank_t *rankData) ours += (rankData->laps * lapsWeight) / rankData->totalLaps; } - if (rankData->totalCapsules > 0) + if (rankData->totalPrisons > 0) { - ours += (rankData->capsules * capsulesWeight) / rankData->totalCapsules; + ours += (rankData->prisons * prisonsWeight) / rankData->totalPrisons; } if (rankData->totalRings > 0) diff --git a/src/k_rank.h b/src/k_rank.h index cc675db17..eb2c420cd 100644 --- a/src/k_rank.h +++ b/src/k_rank.h @@ -35,8 +35,8 @@ struct gpRank_t UINT32 continuesUsed; - UINT32 capsules; - UINT32 totalCapsules; + UINT32 prisons; + UINT32 totalPrisons; UINT32 rings; UINT32 totalRings; @@ -44,15 +44,7 @@ struct gpRank_t boolean specialWon; }; -typedef enum -{ - GRADE_E, - GRADE_D, - GRADE_C, - GRADE_B, - GRADE_A, - GRADE_S -} gp_rank_e; +// gp_rank_e was once defined here, but moved to doomstat.h to prevent circular dependency // 3rd place is neutral, anything below is a penalty #define RANK_NEUTRAL_POSITION (3) diff --git a/src/k_roulette.c b/src/k_roulette.c index a265ec1fd..89d265b68 100644 --- a/src/k_roulette.c +++ b/src/k_roulette.c @@ -1230,7 +1230,7 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet kartitems_t *presetlist = K_KartItemReelTimeAttack; // If the objective is not to go fast, it's to cause serious damage. - if (gametyperules & GTR_CAPSULES) + if (gametyperules & GTR_PRISONS) { presetlist = K_KartItemReelBreakTheCapsules; } diff --git a/src/m_cond.c b/src/m_cond.c index 8ff27ba24..9fc0d0805 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1,6 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- -// Copyright (C) 2022-2023 by Vivian "toaster" Grannell. +// Copyright (C) 2022-2023 by Vivian "toastergrl" Grannell. // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh. // Copyright (C) 2012-2020 by Sonic Team Junior. // @@ -25,16 +25,17 @@ #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]; -// Map triggers for linedef executors -// 32 triggers, one bit each -UINT32 unlocktriggers; - // The meat of this system lies in condition sets conditionset_t conditionSets[MAXCONDITIONSETS]; @@ -208,9 +209,55 @@ quickcheckagain: } } +#if (CHALLENGEGRIDHEIGHT == 4) + while (nummajorunlocks > 0) + { + UINT8 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) { - I_Error("M_PopulateChallengeGrid: was not able to populate %d large tiles (width %d)", nummajorunlocks, gamedata->challengegridwidth); + 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); } } @@ -228,6 +275,10 @@ quickcheckagain: 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)); } @@ -427,7 +478,7 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) } } -void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2) +void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2, char *stringvar) { condition_t *cond; UINT32 num, wnum; @@ -446,20 +497,42 @@ void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1 cond[wnum].requirement = r; cond[wnum].extrainfo1 = x1; cond[wnum].extrainfo2 = x2; + cond[wnum].stringvar = stringvar; } void M_ClearConditionSet(UINT8 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; - conditionSets[set].numconditions = 0; } gamedata->achieved[set] = false; } // Clear ALL secrets. +void M_ClearStats(void) +{ + UINT8 i; + gamedata->totalplaytime = 0; + gamedata->totalrings = 0; + for (i = 0; i < GDGT_MAX; ++i) + gamedata->roundsplayed[i] = 0; + gamedata->timesBeaten = 0; + + gamedata->everloadedaddon = false; + gamedata->eversavedreplay = false; + gamedata->everseenspecial = false; + gamedata->evercrashed = false; + gamedata->musicflags = 0; +} + void M_ClearSecrets(void) { INT32 i; @@ -480,28 +553,131 @@ void M_ClearSecrets(void) gamedata->challengegrid = NULL; gamedata->challengegridwidth = 0; - gamedata->timesBeaten = 0; - - // Re-unlock any always unlocked things - M_UpdateUnlockablesAndExtraEmblems(false); + gamedata->pendingkeyrounds = 0; + gamedata->pendingkeyroundoffset = 0; + gamedata->keyspending = 0; + gamedata->chaokeys = 3; // Start with 3 !! } // ---------------------- // Condition set checking // ---------------------- +void M_UpdateConditionSetsPending(void) +{ + UINT32 i, j; + 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 UCRP_ISCHARACTER: + { + cn->requirement = R_SkinAvailable(cn->stringvar); + + if (cn->requirement < 0) + { + CONS_Alert(CONS_WARNING, "UCRP_ISCHARACTER: Invalid character %s for condition ID %d", cn->stringvar, cn->id+1); + return; + } + + 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; + } + } + + + } +} + +static boolean M_NotFreePlay(player_t *player) +{ + UINT8 i; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false || players[i].spectator == true) + { + continue; + } + + if (player == &players[i]) + { + continue; + } + + return true; + } + + return false; +} + // See also M_GetConditionString -UINT8 M_CheckCondition(condition_t *cn) +boolean M_CheckCondition(condition_t *cn, player_t *player) { switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x return (gamedata->totalplaytime >= (unsigned)cn->requirement); - case UC_MATCHESPLAYED: // Requires any level completed >= x times - return (gamedata->matchesplayed >= (unsigned)cn->requirement); + case UC_ROUNDSPLAYED: // Requires any level completed >= x times + { + if (cn->extrainfo1 == GDGT_MAX) + { + UINT8 i; + UINT32 sum = 0; + + for (i = 0; i < GDGT_MAX; i++) + { + sum += gamedata->roundsplayed[i]; + } + + return (sum >= (unsigned)cn->requirement); + } + return (gamedata->roundsplayed[cn->extrainfo1] >= (unsigned)cn->requirement); + } + case UC_TOTALRINGS: // Requires grabbing >= x rings + return (gamedata->totalrings >= (unsigned)cn->requirement); case UC_POWERLEVEL: // Requires power level >= x on a certain gametype { UINT8 i; + + if (gamestate == GS_LEVEL) + return false; // this one could be laggy with many profiles available + for (i = PROFILE_GUEST; i < PR_GetNumProfiles(); i++) { profile_t *p = PR_GetProfile(i); @@ -521,12 +697,15 @@ UINT8 M_CheckCondition(condition_t *cn) 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 { 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; return ((cn->requirement < nummapheaders) && (mapheaderinfo[cn->requirement]) @@ -534,8 +713,43 @@ UINT8 M_CheckCondition(condition_t *cn) } case UC_MAPTIME: // Requires time on map <= x return (G_GetBestTime(cn->extrainfo1) <= (unsigned)cn->requirement); - case UC_TRIGGER: // requires map trigger set - return !!(unlocktriggers & (1 << cn->requirement)); + + case UC_ALLCHAOS: + case UC_ALLSUPER: + case UC_ALLEMERALDS: + { + cupheader_t *cup; + UINT16 ret = 0; + UINT8 i; + + if (gamestate == GS_LEVEL) + return false; // this one could be laggy with many cups available + + for (cup = kartcupheaders; cup; cup = cup->next) + { + if (cup->emeraldnum == 0) + continue; + + i = cn->requirement; + for (i = cn->requirement; i < KARTGP_MAX; i++) + { + if (cup->windata[i].got_emerald == true) + break; + } + + if (i == KARTGP_MAX) + continue; + + ret |= 1<<(cup->emeraldnum-1); + } + + 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 @@ -544,16 +758,147 @@ UINT8 M_CheckCondition(condition_t *cn) 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_ADDON: + return ((gamedata->everloadedaddon == true) + && M_SecretUnlocked(SECRET_ADDONS, true)); + case UC_REPLAY: + return (gamedata->eversavedreplay == true); + case UC_CRASH: + if (gamedata->evercrashed) + { + gamedata->musicflags |= GDMUSIC_LOSERCLUB; + return true; + } + return false; + + // Just for string building + case UC_AND: + case UC_COMMA: + 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->skin == cn->requirement); + case UCRP_ISENGINECLASS: + return (player->skin < numskins + && R_GetEngineClass( + skins[player->skin].kartspeed, + skins[player->skin].kartweight, + skins[player->skin].flags + ) == (unsigned)cn->requirement); + case UCRP_ISDIFFICULTY: + if (grandprixinfo.gp == false) + return (gamespeed >= cn->requirement); + if (cn->requirement == KARTGP_MASTER) + return (grandprixinfo.masterbots == true); + return (grandprixinfo.gamespeed >= cn->requirement); + + case UCRP_PODIUMCUP: + if (K_PodiumRanking() == false) + return false; + if (grandprixinfo.cup == NULL + || grandprixinfo.cup->id != cn->requirement) + return false; + if (cn->extrainfo2) + return (K_PodiumGrade() >= (unsigned)cn->requirement); + if (cn->extrainfo1 != 0) + return (player->position != 0 + && player->position <= cn->extrainfo1); + return true; + case UCRP_PODIUMEMERALD: + case UCRP_PODIUMPRIZE: + return (K_PodiumRanking() == true + && grandprixinfo.rank.specialWon == true); + + case UCRP_FINISHCOOL: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay(player) + && !K_IsPlayerLosing(player)); + case UCRP_FINISHALLPRISONS: + return (battleprisons + && !(player->pflags & PF_NOCONTEST) + //&& M_NotFreePlay(player) + && numtargets >= maptargets); + case UCRP_NOCONTEST: + return (player->pflags & PF_NOCONTEST); + case UCRP_FINISHPLACE: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay(player) + && player->position != 0 + && player->position <= cn->requirement); + case UCRP_FINISHPLACEEXACT: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay(player) + && player->position == cn->requirement); + case UCRP_FINISHTIME: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + //&& M_NotFreePlay(player) + && player->realtime <= (unsigned)cn->requirement); + case UCRP_FINISHTIMEEXACT: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + //&& M_NotFreePlay(player) + && player->realtime/TICRATE == (unsigned)cn->requirement/TICRATE); + case UCRP_FINISHTIMELEFT: + return (timelimitintics + && player->exiting + && !(player->pflags & PF_NOCONTEST) + && !K_CanChangeRules(false) // too easy to change cv_timelimit + && player->realtime < timelimitintics + && (timelimitintics + extratimeintics + secretextratime - player->realtime) >= (unsigned)cn->requirement); + + case UCRP_TRIGGER: // requires map trigger set + return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); + + case UCRP_FALLOFF: + return (player->roundconditions.fell_off == (cn->requirement == 1)); + case UCRP_TOUCHOFFROAD: + return (player->roundconditions.touched_offroad == (cn->requirement == 1)); + case UCRP_TOUCHSNEAKERPANEL: + return (player->roundconditions.touched_sneakerpanel == (cn->requirement == 1)); + case UCRP_RINGDEBT: + return (!(gametyperules & GTR_SPHERES) && (player->roundconditions.debt_rings == (cn->requirement == 1))); + + case UCRP_TRIPWIREHYUU: + return (player->roundconditions.tripwire_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_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 UINT8 M_CheckConditionSet(conditionset_t *c) +static boolean M_CheckConditionSet(conditionset_t *c, player_t *player) { UINT32 i; UINT32 lastID = 0; condition_t *cn; - UINT8 achievedSoFar = true; + boolean achievedSoFar = true; for (i = 0; i < c->numconditions; ++i) { @@ -565,11 +910,24 @@ static UINT8 M_CheckConditionSet(conditionset_t *c) return true; // Skip future conditions with the same ID if one fails, for obvious reasons - else if (lastID && lastID == cn->id && !achievedSoFar) + if (lastID && lastID == cn->id && !achievedSoFar) + continue; + + // Skip entries that are JUST for string building + if (cn->type == UC_AND || cn->type == UC_COMMA) continue; lastID = cn->id; - achievedSoFar = M_CheckCondition(cn); + + 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; @@ -599,6 +957,17 @@ static char *M_BuildConditionTitle(UINT16 map) return title; } +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) { @@ -606,54 +975,115 @@ static const char *M_GetConditionString(condition_t *cn) char *title = NULL; const char *work = NULL; + // If this function returns NULL, it stops building the condition and just does ???'s. + #define BUILDCONDITIONTITLE(i) (M_BuildConditionTitle(i)) switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x - return va("Play for %i:%02i:%02i", + + return va("play for %i:%02i:%02i", G_TicsToHours(cn->requirement), G_TicsToMinutes(cn->requirement, false), G_TicsToSeconds(cn->requirement)); - case UC_MATCHESPLAYED: // Requires any level completed >= x times - return va("Play %d matches", 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("play %d%s Round%s", cn->requirement, work, + (cn->requirement == 1 ? "" : "s")); + + case UC_TOTALRINGS: // Requires collecting >= x rings + if (cn->requirement >= 1000000) + return va("collect %u,%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_POWERLEVEL: // Requires power level >= x on a certain gametype - return va("Get a PWR of %d in %s", cn->requirement, + return va("get a PWR of %d in %s", cn->requirement, (cn->extrainfo1 == PWRLV_RACE) ? "Race" : "Battle"); + case UC_GAMECLEAR: // Requires game beaten >= x times if (cn->requirement > 1) - return va("Beat game %d times", cn->requirement); + return va("beat game %d times", cn->requirement); else - return va("Beat the game"); + return va("beat the game"); + case UC_OVERALLTIME: // Requires overall time <= x - return va("Get overall time of %i:%02i:%02i", + 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 { + const char *prefix = ""; + if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); title = BUILDCONDITIONTITLE(cn->requirement); - work = va("%s %s%s", - (cn->type == UC_MAPVISITED) ? "Visit" : "Finish a round on", + + 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"; + + work = va("%s%s %s%s", + prefix, + work, title, (cn->type == UC_MAPENCORE) ? " in Encore Mode" : ""); 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 = BUILDCONDITIONTITLE(cn->extrainfo1); - work = va("Beat %s in %i:%02i.%02i", title, + work = va("beat %s in %i:%02i.%02i", title, G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); @@ -661,8 +1091,49 @@ static const char *M_GetConditionString(condition_t *cn) Z_Free(title); return work; } + + case UC_ALLCHAOS: + case UC_ALLSUPER: + case UC_ALLEMERALDS: + { + const char *chaostext, *speedtext = "", *orbetter = ""; + + if (!gamedata->everseenspecial) + return NULL; + + if (cn->type == UC_ALLCHAOS) + chaostext = "7 Chaos"; + else if (cn->type == UC_ALLSUPER) + chaostext = "7 Super"; + else + chaostext = "14"; + + if (cn->requirement == KARTSPEED_NORMAL) + { + speedtext = " on Normal difficulty"; + //if (M_SecretUnlocked(SECRET_HARDSPEED, true)) + orbetter = " or better"; + } + else if (cn->requirement == KARTSPEED_HARD) + { + speedtext = " on Hard difficulty"; + if (M_SecretUnlocked(SECRET_MASTERMODE, true)) + orbetter = " or better"; + } + else if (cn->requirement == KARTGP_MASTER) + { + if (M_SecretUnlocked(SECRET_MASTERMODE, true)) + speedtext = " on Master difficulty"; + else + speedtext = " on ???"; + } + + return va("collect all %s Emeralds%s%s", chaostext, speedtext, orbetter); + } + case UC_TOTALMEDALS: // Requires number of emblems >= x - return va("Get %d medals", cn->requirement); + return va("get %d medals", cn->requirement); + case UC_EMBLEM: // Requires emblem x to be obtained { INT32 checkLevel; @@ -677,7 +1148,16 @@ static const char *M_GetConditionString(condition_t *cn) switch (emblemlocations[i].type) { case ET_MAP: - work = va("Beat %s", title); + 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) @@ -685,7 +1165,7 @@ static const char *M_GetConditionString(condition_t *cn) Z_Free(title); return va("INVALID MEDAL COLOR \"%d:%d\"", cn->requirement, checkLevel); } - work = va("Get the %s Medal for %s", skincolors[emblemlocations[i].color].name, title); + work = va("TIME ATTACK: get the %s Medal for %s", skincolors[emblemlocations[i].color].name, title); break; case ET_GLOBAL: { @@ -711,21 +1191,21 @@ static const char *M_GetConditionString(condition_t *cn) if (emblemlocations[i].flags & GE_TIMED) { - work = va("Find %s%s%s in %s before %i:%02i.%02i", - astr, colorstr, medalstr, title, + 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("Find %s%s%s in %s", - astr, colorstr, medalstr, title); + work = va("%s: find %s%s%s", + title, astr, colorstr, medalstr); } break; } default: - work = va("Find a secret in %s", title); + work = va("find a secret in %s", title); break; } @@ -733,10 +1213,192 @@ static const char *M_GetConditionString(condition_t *cn) return work; } case UC_UNLOCKABLE: // Requires unlockable x to be obtained - return va("Get \"%s\"", + return va("get \"%s\"", gamedata->unlocked[cn->requirement-1] ? unlockables[cn->requirement-1].name : "???"); + + case UC_ADDON: + if (!M_SecretUnlocked(SECRET_ADDONS, true)) + return NULL; + return "load a custom addon into \"Dr. Robotnik's Ring Racers\""; + case UC_REPLAY: + return "save a replay after finishing a round"; + case UC_CRASH: + if (gamedata->evercrashed) + return "launch \"Dr. Robotnik's Ring Racers\" again after a game crash"; + return NULL; + + case UC_AND: + return "&"; + case UC_COMMA: + return ","; + + case UCRP_PREFIX_GRANDPRIX: + return "GRAND PRIX:"; + case UCRP_PREFIX_BONUSROUND: + return "BONUS ROUND:"; + 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 = 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 = 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); + return va("as %s", skins[cn->requirement].realname); + case UCRP_ISENGINECLASS: + return va("with engine class %c", 'A' + cn->requirement); + case UCRP_ISDIFFICULTY: + { + const char *speedtext = ""; + + if (cn->requirement == KARTSPEED_NORMAL) + { + speedtext = "on Normal difficulty or better"; + } + else if (cn->requirement == KARTSPEED_HARD) + { + speedtext = "on Hard difficulty"; + } + else if (cn->requirement == KARTGP_MASTER) + { + if (M_SecretUnlocked(SECRET_MASTERMODE, true)) + speedtext = "on Master difficulty"; + else + speedtext = "on ???"; + } + + return speedtext; + } + + case UCRP_PODIUMCUP: + { + cupheader_t *cup; + const char *completetype = "complete", *orbetter = ""; + + if (cn->extrainfo2) + { + switch (cn->requirement) + { + 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"; + } + + for (cup = kartcupheaders; cup; cup = cup->next) + { + if (cup->id != cn->requirement) + continue; + return va("%s%s %s CUP", completetype, orbetter, cup->name); + } + 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_FINISHCOOL: + return "finish in good standing"; + case UCRP_FINISHALLPRISONS: + return "break every prison"; + case UCRP_NOCONTEST: + return "NO CONTEST"; + 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_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_TRIGGER: + return cn->stringvar; + + case UCRP_FALLOFF: + return (cn->requirement == 1) ? "fall off the course" : "without falling off"; + case UCRP_TOUCHOFFROAD: + return (cn->requirement == 1) ? "touch offroad" : "without touching any offroad"; + case UCRP_TOUCHSNEAKERPANEL: + return (cn->requirement == 1) ? "touch a Sneaker Panel" : "without touching any Sneaker Panels"; + case UCRP_RINGDEBT: + return (cn->requirement == 1) ? "go into Ring debt" : "without going into Ring debt"; + + case UCRP_TRIPWIREHYUU: + return "go through Tripwire after getting snared by Hyudoro"; + case UCRP_SPBNEUTER: + return "shock a Self Propelled Bomb into submission"; + case UCRP_LANDMINEDUNK: + return "dunk a Landmine on another racer's head"; + case UCRP_HITMIDAIR: + return "hit another racer with a projectile while you're both in the air"; + + case UCRP_WETPLAYER: + return va("without %s %s", + (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", + cn->stringvar); + default: break; } @@ -746,20 +1408,16 @@ static const char *M_GetConditionString(condition_t *cn) #undef BUILDCONDITIONTITLE } -//#define ACHIEVEDBRITE - char *M_BuildConditionSetString(UINT8 unlockid) { conditionset_t *c = NULL; UINT32 lastID = 0; condition_t *cn; -#ifdef ACHIEVEDBRITE - boolean achieved = false; -#endif size_t len = 1024, worklen; static char message[1024] = ""; const char *work = NULL; - size_t max = 0, start = 0, strlines = 0, i; + size_t max = 0, maxatstart = 0, start = 0, i; + boolean stopasap = false; message[0] = '\0'; @@ -773,51 +1431,51 @@ char *M_BuildConditionSetString(UINT8 unlockid) 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) + if (i > 0 && (cn->type != UC_COMMA)) { - worklen = 3; - if (lastID == cn->id) + if (lastID != cn->id) { - strncat(message, "\n& ", len); + worklen = 4; + strncat(message, "\nOR ", len); } else { - strncat(message, "\nOR ", len); - worklen++; + worklen = 1; + strncat(message, " ", len); } len -= worklen; } + lastID = cn->id; -#ifdef ACHIEVEDBRITE - achieved = M_CheckCondition(cn); - - if (achieved) - { - strncat(message, "\0x82", len); - len--; - } -#endif - work = M_GetConditionString(cn); + if (work == NULL) + { + stopasap = true; + work = "???"; + } worklen = strlen(work); strncat(message, work, len); len -= worklen; -#ifdef ACHIEVEDBRITE - if (achieved) + if (stopasap) { - strncat(message, "\0x80", len); - len--; + break; } -#endif } // Rudementary word wrapping. @@ -828,12 +1486,13 @@ char *M_BuildConditionSetString(UINT8 unlockid) { start = i; max += 4; + maxatstart = max; } else if (message[i] == '\n') { - strlines = i; start = 0; max = 0; + maxatstart = 0; continue; } else if (message[i] & 0x80) @@ -845,19 +1504,45 @@ char *M_BuildConditionSetString(UINT8 unlockid) if (max >= DESCRIPTIONWIDTH && start > 0) { message[start] = '\n'; - max -= (start-strlines)*8; - strlines = start; + max -= maxatstart; start = 0; } } + // 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; + } + + // If we didn't find a prefix, just start from the first character again. + if (!message[i]) + i = 0; + + // Okay, now make the first non-whitespace character after the prefix a capital. + // Doesn't matter if !isalpha() - toupper is a no-op. + for (; message[i]; i++) + { + if ((message[i] & 0x80) || isspace(message[i])) + continue; + message[i] = toupper(message[i]); + break; + } + } + return message; } -static void M_CheckUnlockConditions(void) +static boolean M_CheckUnlockConditions(player_t *player) { INT32 i; conditionset_t *c; + boolean ret; for (i = 0; i < MAXCONDITIONSETS; ++i) { @@ -865,14 +1550,24 @@ static void M_CheckUnlockConditions(void) if (!c->numconditions || gamedata->achieved[i]) continue; - gamedata->achieved[i] = (M_CheckConditionSet(c)); + if ((gamedata->achieved[i] = (M_CheckConditionSet(c, player))) != true) + continue; + + ret = true; } + + return ret; } -boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) +boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall) { - INT32 i; - UINT8 response = 0; + UINT16 i = 0, response = 0, newkeys = 0; + + if (!gamedata) + { + // Don't attempt to write/check anything. + return false; + } if (!loud) { @@ -880,9 +1575,55 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) // Done first so that emblems are ready before check M_CheckLevelEmblems(); M_CompletionEmblems(); + doall = true; } - M_CheckUnlockConditions(); + if (gamedata->deferredconditioncheck == true) + { + // Handle deferred all-condition checks + gamedata->deferredconditioncheck = false; + doall = true; + } + + if (doall) + { + response = M_CheckUnlockConditions(NULL); + + 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_PodiumRanking() == 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) @@ -907,8 +1648,10 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) response++; } + response += newkeys; + // Announce - if (response) + if (response != 0) { if (loud) { @@ -919,7 +1662,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) return false; } -UINT8 M_GetNextAchievedUnlock(void) +UINT16 M_GetNextAchievedUnlock(void) { UINT8 i; @@ -944,6 +1687,11 @@ UINT8 M_GetNextAchievedUnlock(void) return i; } + if (gamedata->keyspending != 0) + { + return PENDING_CHAOKEYS; + } + return MAXUNLOCKABLES; } @@ -1115,7 +1863,7 @@ boolean M_CupLocked(cupheader_t *cup) return false; } -boolean M_MapLocked(INT32 mapnum) +boolean M_MapLocked(UINT16 mapnum) { UINT8 i; @@ -1128,7 +1876,7 @@ boolean M_MapLocked(INT32 mapnum) if (marathonmode) return false; - if (!mapnum || mapnum > nummapheaders) + if (mapnum == 0 || mapnum > nummapheaders) return false; if (!mapheaderinfo[mapnum-1]) diff --git a/src/m_cond.h b/src/m_cond.h index c279572f7..f65bba15b 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -1,6 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- -// Copyright (C) 2022-2023 by Vivian "toaster" Grannell. +// Copyright (C) 2022-2023 by Vivian "toastergrl" Grannell. // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh. // Copyright (C) 2012-2020 by Sonic Team Junior. // @@ -29,19 +29,81 @@ extern "C" { typedef enum { UC_PLAYTIME, // PLAYTIME [tics] - UC_MATCHESPLAYED, // SRB2Kart: MATCHESPLAYED [x played] + UC_ROUNDSPLAYED, // ROUNDSPLAYED [x played] + UC_TOTALRINGS, // TOTALRINGS [x collected] + UC_POWERLEVEL, // SRB2Kart: POWERLEVEL [power level to reach] [gametype, "0" for race, "1" for battle] + UC_GAMECLEAR, // GAMECLEAR UC_OVERALLTIME, // OVERALLTIME [time to beat, tics] - UC_MAPVISITED, // MAPVISITED [map number] - UC_MAPBEATEN, // MAPBEATEN [map number] - UC_MAPENCORE, // MAPENCORE [map number] - UC_MAPTIME, // MAPTIME [map number] [time to beat, tics] - UC_TRIGGER, // TRIGGER [trigger number] + + UC_MAPVISITED, // MAPVISITED [map] + UC_MAPBEATEN, // MAPBEATEN [map] + UC_MAPENCORE, // MAPENCORE [map] + UC_MAPSPBATTACK, // MAPSPBATTACK [map] + UC_MAPTIME, // MAPTIME [map] [time to beat, tics] + + UC_ALLCHAOS, // ALLCHAOS [minimum difficulty] + UC_ALLSUPER, // ALLSUPER [minimum difficulty] + UC_ALLEMERALDS, // ALLEMERALDS [minimum difficulty] + UC_TOTALMEDALS, // TOTALMEDALS [number of emblems] UC_EMBLEM, // EMBLEM [emblem number] + UC_UNLOCKABLE, // UNLOCKABLE [unlockable number] UC_CONDITIONSET, // CONDITIONSET [condition set number] + + UC_ADDON, // Ever loaded a custom file? + UC_REPLAY, // Save a replay + UC_CRASH, // Hee ho ! + + // Just for string building + UC_AND, + UC_COMMA, + + UCRP_REQUIRESPLAYING, // All conditions below this can only be checked if (Playing() && gamestate == GS_LEVEL). + + UCRP_PREFIX_GRANDPRIX = UCRP_REQUIRESPLAYING, // GRAND PRIX: + UCRP_PREFIX_BONUSROUND, // BONUS ROUND: + UCRP_PREFIX_TIMEATTACK, // TIME ATTACK: + UCRP_PREFIX_PRISONBREAK, // PRISON BREAK: + UCRP_PREFIX_SEALEDSTAR, // SEALED STAR: + + UCRP_PREFIX_ISMAP, // name of [map]: + UCRP_ISMAP, // gamemap == [map] + + UCRP_ISCHARACTER, // character == [skin] + UCRP_ISENGINECLASS, // engine class [class] + UCRP_ISDIFFICULTY, // difficulty >= [difficulty] + + UCRP_PODIUMCUP, // cup == [cup] [optional: >= grade OR place] + UCRP_PODIUMEMERALD, // Get to podium sequence with that cup's emerald + UCRP_PODIUMPRIZE, // Get to podium sequence with that cup's bonus (alternate string version of UCRP_PODIUMEMERALD + + UCRP_FINISHCOOL, // Finish in good standing + UCRP_FINISHALLPRISONS, // Break all prisons + UCRP_NOCONTEST, // No Contest + + UCRP_FINISHPLACE, // Finish at least [place] + UCRP_FINISHPLACEEXACT, // Finish at [place] exactly + + UCRP_FINISHTIME, // Finish <= [time, tics] + UCRP_FINISHTIMEEXACT, // Finish == [time, tics] + UCRP_FINISHTIMELEFT, // Finish with at least [time, tics] to spare + + UCRP_TRIGGER, // Map execution trigger [id] + + UCRP_FALLOFF, // Fall off (or don't) + UCRP_TOUCHOFFROAD, // Touch offroad (or don't) + UCRP_TOUCHSNEAKERPANEL, // Either touch sneaker panel (or don't) + UCRP_RINGDEBT, // Go into debt (or don't) + + UCRP_TRIPWIREHYUU, // Go through tripwire with Hyudoro + UCRP_SPBNEUTER, // Kill an SPB with Lightning + UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... + UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem + + UCRP_WETPLAYER, // Don't touch [fluid] } conditiontype_t; // Condition Set information @@ -55,6 +117,7 @@ struct condition_t INT32 requirement; /// <- The requirement for this variable. INT16 extrainfo1; /// <- Extra information for the condition when needed. INT16 extrainfo2; /// <- Extra information for the condition when needed. + char *stringvar; /// <- Extra z-allocated string for the condition when needed }; struct conditionset_t { @@ -120,13 +183,19 @@ typedef enum // Difficulty restrictions SECRET_HARDSPEED, // Permit Hard gamespeed + SECRET_MASTERMODE, // Permit Master Mode bots in GP SECRET_ENCORE, // Permit Encore option - SECRET_LEGACYBOXRUMMAGE, // Permit the Legacy Box for record attack, etc // Menu restrictions SECRET_TIMEATTACK, // Permit Time attack - SECRET_BREAKTHECAPSULES, // Permit SP Capsule attack + SECRET_PRISONBREAK, // Permit SP Prison attack SECRET_SPECIALATTACK, // Permit Special attack (You're blue now!) + SECRET_SPBATTACK, // Permit SPB mode of Time attack + + // Option restrictions + SECRET_ONLINE, // Permit netplay (ankle-high barrier to jumping in the deep end) + SECRET_ADDONS, // Permit menu addfile + SECRET_EGGTV, // Permit replay playback menu SECRET_SOUNDTEST, // Permit Sound Test SECRET_ALTTITLE, // Permit alternate titlescreen @@ -148,12 +217,35 @@ typedef enum #endif #define challengegridloops (gamedata->challengegridwidth >= CHALLENGEGRIDLOOPWIDTH) +#define GDMUSIC_LOSERCLUB 0x01 + +// This is the largest number of 9s that will fit in UINT32 and UINT16 respectively. +#define GDMAX_RINGS 999999999 +#define GDMAX_CHAOKEYS 9999 + +#ifdef DEVELOP +#define GDCONVERT_ROUNDSTOKEY 20 +#else +#define GDCONVERT_ROUNDSTOKEY 50 +#endif + +typedef enum { + GDGT_RACE, + GDGT_BATTLE, + GDGT_PRISONS, + GDGT_SPECIAL, + GDGT_CUSTOM, + GDGT_MAX +} roundsplayed_t; + // GAMEDATA STRUCTURE // Everything that would get saved in gamedata.dat struct gamedata_t { // WHENEVER OR NOT WE'RE READY TO SAVE boolean loaded; + boolean deferredsave; + boolean deferredconditioncheck; // CONDITION SETS ACHIEVED boolean achieved[MAXCONDITIONSETS]; @@ -174,7 +266,21 @@ struct gamedata_t // PLAY TIME UINT32 totalplaytime; - UINT32 matchesplayed; + UINT32 roundsplayed[GDGT_MAX]; + UINT32 totalrings; + + // Chao Key condition bypass + UINT32 pendingkeyrounds; + UINT8 pendingkeyroundoffset; + UINT8 keyspending; + UINT16 chaokeys; + + // SPECIFIC SPECIAL EVENTS + boolean everloadedaddon; + boolean eversavedreplay; + boolean everseenspecial; + boolean evercrashed; + UINT8 musicflags; }; extern gamedata_t *gamedata; @@ -188,8 +294,6 @@ extern unlockable_t unlockables[MAXUNLOCKABLES]; extern INT32 numemblems; -extern UINT32 unlocktriggers; - void M_NewGameDataStruct(void); // Challenges menu stuff @@ -213,16 +317,21 @@ char *M_BuildConditionSetString(UINT8 unlockid); #define DESCRIPTIONWIDTH 170 // Condition set setup -void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2); +void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2, char *stringvar); +void M_UpdateConditionSetsPending(void); // Clearing secrets void M_ClearConditionSet(UINT8 set); void M_ClearSecrets(void); +void M_ClearStats(void); // Updating conditions and unlockables -UINT8 M_CheckCondition(condition_t *cn); -boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud); -UINT8 M_GetNextAchievedUnlock(void); +boolean M_CheckCondition(condition_t *cn, player_t *player); +boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall); + +#define PENDING_CHAOKEYS (UINT16_MAX-1) +UINT16 M_GetNextAchievedUnlock(void); + UINT8 M_CheckLevelEmblems(void); UINT8 M_CompletionEmblems(void); @@ -230,7 +339,7 @@ UINT8 M_CompletionEmblems(void); boolean M_CheckNetUnlockByID(UINT8 unlockid); boolean M_SecretUnlocked(INT32 type, boolean local); boolean M_CupLocked(cupheader_t *cup); -boolean M_MapLocked(INT32 mapnum); +boolean M_MapLocked(UINT16 mapnum); INT32 M_CountMedals(boolean all, boolean extraonly); // Emblem shit diff --git a/src/menus/extras-1.c b/src/menus/extras-1.c index 34b0cad6c..2ed90301a 100644 --- a/src/menus/extras-1.c +++ b/src/menus/extras-1.c @@ -2,22 +2,26 @@ /// \brief Extras Menu #include "../k_menu.h" +#include "../m_cond.h" #include "../s_sound.h" menuitem_t EXTRAS_Main[] = { + // The following has NULL strings for text and tooltip. + // These are populated in M_InitExtras depending on unlock state. + // (This is legal - they're (const char)*'s, not const (char*)'s. - {IT_STRING | IT_CALL, "Addons", "Add files to customize your experience.", + {IT_STRING | IT_CALL, NULL, NULL, NULL, {.routine = M_Addons}, 0, 0}, {IT_STRING | IT_CALL, "Challenges", "View the requirements for some of the secret content you can unlock!", NULL, {.routine = M_Challenges}, 0, 0}, - {IT_STRING | IT_CALL, "Replay Hut", "Play the replays you've saved throughout your many races & battles!", - NULL, {.routine = M_ReplayHut}, 0, 0}, - {IT_STRING | IT_CALL, "Statistics", "Look back on some of your greatest achievements such as your playtime and wins!", NULL, {.routine = M_Statistics}, 0, 0}, + + {IT_STRING | IT_CALL, NULL, NULL, + NULL, {.routine = M_ReplayHut}, 0, 0}, }; // the extras menu essentially reuses the options menu stuff @@ -54,6 +58,40 @@ void M_InitExtras(INT32 choice) extrasmenu.textx = 0; extrasmenu.texty = 0; + // Addons + if (M_SecretUnlocked(SECRET_ADDONS, true)) + { + EXTRAS_Main[0].status = IT_STRING | IT_CALL; + EXTRAS_Main[0].text = "Addons"; + EXTRAS_Main[0].tooltip = "Add files to customize your experience."; + } + else + { + EXTRAS_Main[0].status = IT_STRING | IT_TRANSTEXT; + EXTRAS_Main[0].text = EXTRAS_Main[0].tooltip = "???"; + if (EXTRAS_MainDef.lastOn == 0) + { + EXTRAS_MainDef.lastOn = 1; + } + } + + // Egg TV + if (M_SecretUnlocked(SECRET_EGGTV, true)) + { + EXTRAS_Main[3].status = IT_STRING | IT_CALL; + EXTRAS_Main[3].text = "Egg TV"; + EXTRAS_Main[3].tooltip = "Watch the replays you've saved throughout your many races & battles!"; + } + else + { + EXTRAS_Main[3].status = IT_STRING | IT_TRANSTEXT; + EXTRAS_Main[3].text = EXTRAS_Main[3].tooltip = "???"; + if (EXTRAS_MainDef.lastOn == 3) + { + EXTRAS_MainDef.lastOn = 2; + } + } + M_SetupNextMenu(&EXTRAS_MainDef, false); } diff --git a/src/menus/extras-addons.c b/src/menus/extras-addons.c index b3949c4fc..a63c35e7f 100644 --- a/src/menus/extras-addons.c +++ b/src/menus/extras-addons.c @@ -341,7 +341,7 @@ void M_HandleAddons(INT32 choice) //MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED); if (currentMenu->prevMenu) - M_SetupNextMenu(currentMenu->prevMenu, false); + M_SetupNextMenu(M_InterruptMenuWithChallenges(currentMenu->prevMenu), false); else M_ClearMenus(true); diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 17ced0502..c5cda4bc8 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -49,17 +49,59 @@ menu_t MISC_StatisticsDef = { struct challengesmenu_s challengesmenu; -static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh) +static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh) { UINT8 i; SINT8 work; + if (unlockid >= MAXUNLOCKABLES && gamedata->pendingkeyrounds > 0 + && (gamedata->chaokeys < GDMAX_CHAOKEYS)) + challengesmenu.chaokeyadd = true; + + if (fresh && unlockid >= MAXUNLOCKABLES) + { + UINT8 selection[MAXUNLOCKABLES]; + UINT8 numunlocks = 0; + + // Get a random available unlockable. + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (!unlockables[i].conditionset) + { + continue; + } + + if (!gamedata->unlocked[i]) + { + continue; + } + + selection[numunlocks++] = i; + } + + if (!numunlocks) + { + // ...OK, get a random unlockable. + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (!unlockables[i].conditionset) + { + continue; + } + + selection[numunlocks++] = i; + } + } + + unlockid = selection[M_RandomKey(numunlocks)]; + } + if (unlockid >= MAXUNLOCKABLES) return; challengesmenu.currentunlock = unlockid; challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); - challengesmenu.unlockanim = 0; + challengesmenu.unlockanim = (challengesmenu.pending && !challengesmenu.chaokeyadd ? 0 : MAXUNLOCKTIME); if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL) return; @@ -161,12 +203,16 @@ static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh) menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) { - UINT8 i; - UINT16 newunlock = M_GetNextAchievedUnlock(); + UINT16 i, newunlock; - M_UpdateUnlockablesAndExtraEmblems(false); + if (Playing()) + return desiredmenu; - if ((challengesmenu.pending = (newunlock < MAXUNLOCKABLES))) + M_UpdateUnlockablesAndExtraEmblems(false, true); + + newunlock = M_GetNextAchievedUnlock(); + + if ((challengesmenu.pending = (newunlock != MAXUNLOCKABLES))) { S_StopMusic(); MISC_ChallengesDef.prevMenu = desiredmenu; @@ -177,6 +223,7 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) challengesmenu.ticker = 0; challengesmenu.requestflip = false; challengesmenu.requestnew = false; + challengesmenu.chaokeyadd = false; challengesmenu.currentunlock = MAXUNLOCKABLES; challengesmenu.unlockcondition = NULL; @@ -210,6 +257,9 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) if (challengesmenu.pending) M_ChallengesAutoFocus(newunlock, true); + else if (newunlock >= MAXUNLOCKABLES && gamedata->pendingkeyrounds > 0 + && (gamedata->chaokeys < GDMAX_CHAOKEYS)) + challengesmenu.chaokeyadd = true; return &MISC_ChallengesDef; } @@ -219,7 +269,6 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) void M_Challenges(INT32 choice) { - UINT8 i; (void)choice; M_InterruptMenuWithChallenges(NULL); @@ -227,40 +276,7 @@ void M_Challenges(INT32 choice) if (gamedata->challengegrid != NULL && !challengesmenu.pending) { - UINT8 selection[MAXUNLOCKABLES]; - UINT8 numunlocks = 0; - - // Get a random available unlockable. - for (i = 0; i < MAXUNLOCKABLES; i++) - { - if (!unlockables[i].conditionset) - { - continue; - } - - if (!gamedata->unlocked[i]) - { - continue; - } - - selection[numunlocks++] = i; - } - - if (!numunlocks) - { - // ...OK, get a random unlockable. - for (i = 0; i < MAXUNLOCKABLES; i++) - { - if (!unlockables[i].conditionset) - { - continue; - } - - selection[numunlocks++] = i; - } - } - - M_ChallengesAutoFocus(selection[M_RandomKey(numunlocks)], true); + M_ChallengesAutoFocus(UINT16_MAX, true); } M_SetupNextMenu(&MISC_ChallengesDef, false); @@ -270,7 +286,7 @@ void M_ChallengesTick(void) { const UINT8 pid = 0; UINT16 i; - UINT8 newunlock = MAXUNLOCKABLES; + UINT16 newunlock = MAXUNLOCKABLES; // Ticking challengesmenu.ticker++; @@ -280,8 +296,11 @@ void M_ChallengesTick(void) if (setup_explosions[i].tics > 0) setup_explosions[i].tics--; } - if (challengesmenu.unlockcount[CC_ANIM] > 0) - challengesmenu.unlockcount[CC_ANIM]--; + for (i = CC_ANIM; i < CC_MAX; i++) + { + if (challengesmenu.unlockcount[i] > 0) + challengesmenu.unlockcount[i]--; + } M_CupSelectTick(); // Update tile flip state. @@ -307,109 +326,158 @@ void M_ChallengesTick(void) } } - if (challengesmenu.pending) + if (challengesmenu.pending && challengesmenu.fade < 5) { - // Pending mode. + // Fade increase. + challengesmenu.fade++; + } + else if (challengesmenu.chaokeyadd == true) + { + if (challengesmenu.ticker <= 5) + ; // recreate the slight delay the unlock fades provide + else if (gamedata->pendingkeyrounds == 0) + { + gamedata->keyspending = 0; + gamedata->pendingkeyroundoffset %= GDCONVERT_ROUNDSTOKEY; - if (challengesmenu.requestnew) - { - // The menu apparatus is requesting a new unlock. - challengesmenu.requestnew = false; - if ((newunlock = M_GetNextAchievedUnlock()) < MAXUNLOCKABLES) - { - // We got one! - M_ChallengesAutoFocus(newunlock, false); - } - else - { - // All done! Let's save the unlocks we've busted open. - challengesmenu.pending = false; - G_SaveGameData(); - } + challengesmenu.chaokeyadd = false; + challengesmenu.requestnew = true; } - else if (challengesmenu.fade < 5) + else if (gamedata->chaokeys >= GDMAX_CHAOKEYS) { - // Fade increase. - challengesmenu.fade++; + // The above condition will run on the next tic because of this set + gamedata->pendingkeyrounds = 0; + gamedata->pendingkeyroundoffset = 0; } else { - // Unlock sequence. - tic_t nexttime = M_MenuExtraHeld(pid) ? (UNLOCKTIME*2) : MAXUNLOCKTIME; + UINT32 keyexchange = gamedata->keyspending; - if (++challengesmenu.unlockanim >= nexttime) + if (keyexchange > gamedata->pendingkeyrounds) { - challengesmenu.requestnew = true; + keyexchange = 1; + } + else if (keyexchange >= GDCONVERT_ROUNDSTOKEY/2) + { + keyexchange = GDCONVERT_ROUNDSTOKEY/2; } - if (challengesmenu.currentunlock < MAXUNLOCKABLES - && challengesmenu.unlockanim == UNLOCKTIME) + keyexchange |= 1; // guarantee an odd delta for the sake of the sound + + gamedata->pendingkeyrounds -= keyexchange; + gamedata->pendingkeyroundoffset += keyexchange; + + if (!(gamedata->pendingkeyrounds & 1)) { - // Unlock animation... also tied directly to the actual unlock! - gamedata->unlocked[challengesmenu.currentunlock] = true; - M_UpdateUnlockablesAndExtraEmblems(true); + S_StartSound(NULL, sfx_ptally); + } - // Update shown description just in case..? - challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); + if (gamedata->pendingkeyroundoffset >= GDCONVERT_ROUNDSTOKEY) + { + gamedata->pendingkeyroundoffset %= GDCONVERT_ROUNDSTOKEY; - challengesmenu.unlockcount[CC_TALLY]++; - challengesmenu.unlockcount[CC_ANIM]++; - - if (challengesmenu.extradata) + if (gamedata->keyspending > 0) { - unlockable_t *ref; - UINT16 bombcolor; - - M_UpdateChallengeGridExtraData(challengesmenu.extradata); - - ref = &unlockables[challengesmenu.currentunlock]; - bombcolor = SKINCOLOR_NONE; - - if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) - { - bombcolor = ref->color; - } - else switch (ref->type) - { - case SECRET_SKIN: - { - INT32 skin = M_UnlockableSkinNum(ref); - if (skin != -1) - { - bombcolor = skins[skin].prefcolor; - } - break; - } - case SECRET_FOLLOWER: - { - INT32 skin = M_UnlockableFollowerNum(ref); - if (skin != -1) - { - bombcolor = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); - } - break; - } - default: - break; - } - - if (bombcolor == SKINCOLOR_NONE) - { - bombcolor = cv_playercolor[0].value; - } - - i = (ref->majorunlock && M_RandomChance(FRACUNIT/2)) ? 1 : 0; - M_SetupReadyExplosions(false, challengesmenu.hilix, challengesmenu.hiliy+i, bombcolor); - if (ref->majorunlock) - { - M_SetupReadyExplosions(false, challengesmenu.hilix+1, challengesmenu.hiliy+(1-i), bombcolor); - } - - S_StartSound(NULL, sfx_s3k4e); + S_StartSound(NULL, sfx_achiev); + gamedata->keyspending--; + gamedata->chaokeys++; + challengesmenu.unlockcount[CC_CHAOANIM]++; } } } } + else if (challengesmenu.requestnew) + { + // The menu apparatus is requesting a new unlock. + challengesmenu.requestnew = false; + if ((newunlock = M_GetNextAchievedUnlock()) != MAXUNLOCKABLES) + { + // We got one! + M_ChallengesAutoFocus(newunlock, false); + } + else + { + // All done! Let's save the unlocks we've busted open. + challengesmenu.pending = challengesmenu.chaokeyadd = false; + G_SaveGameData(); + } + } + else if (challengesmenu.pending) + { + tic_t nexttime = M_MenuExtraHeld(pid) ? (UNLOCKTIME*2) : MAXUNLOCKTIME; + + if (++challengesmenu.unlockanim >= nexttime) + { + challengesmenu.requestnew = true; + } + + if (challengesmenu.currentunlock < MAXUNLOCKABLES + && challengesmenu.unlockanim == UNLOCKTIME) + { + // Unlock animation... also tied directly to the actual unlock! + gamedata->unlocked[challengesmenu.currentunlock] = true; + M_UpdateUnlockablesAndExtraEmblems(true, true); + + // Update shown description just in case..? + challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); + + challengesmenu.unlockcount[CC_TALLY]++; + challengesmenu.unlockcount[CC_ANIM]++; + + if (challengesmenu.extradata) + { + unlockable_t *ref; + UINT16 bombcolor; + + M_UpdateChallengeGridExtraData(challengesmenu.extradata); + + ref = &unlockables[challengesmenu.currentunlock]; + bombcolor = SKINCOLOR_NONE; + + if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) + { + bombcolor = ref->color; + } + else switch (ref->type) + { + case SECRET_SKIN: + { + INT32 skin = M_UnlockableSkinNum(ref); + if (skin != -1) + { + bombcolor = skins[skin].prefcolor; + } + break; + } + case SECRET_FOLLOWER: + { + INT32 skin = M_UnlockableFollowerNum(ref); + if (skin != -1) + { + bombcolor = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); + } + break; + } + default: + break; + } + + if (bombcolor == SKINCOLOR_NONE) + { + bombcolor = cv_playercolor[0].value; + } + + i = (ref->majorunlock && M_RandomChance(FRACUNIT/2)) ? 1 : 0; + M_SetupReadyExplosions(false, challengesmenu.hilix, challengesmenu.hiliy+i, bombcolor); + if (ref->majorunlock) + { + M_SetupReadyExplosions(false, challengesmenu.hilix+1, challengesmenu.hiliy+(1-i), bombcolor); + } + + S_StartSound(NULL, sfx_s3k4e); + } + } + } else { @@ -441,28 +509,57 @@ boolean M_ChallengesInputs(INT32 ch) const boolean move = (menucmd[pid].dpad_ud != 0 || menucmd[pid].dpad_lr != 0); (void) ch; - if (challengesmenu.fade) + if (challengesmenu.fade || challengesmenu.chaokeyadd) { ; } -#ifdef DEVELOP - else if (M_MenuExtraPressed(pid) && challengesmenu.extradata) // debugging + else if (M_MenuExtraPressed(pid) + && challengesmenu.extradata) { - if (challengesmenu.currentunlock < MAXUNLOCKABLES) - { - Z_Free(gamedata->challengegrid); - gamedata->challengegrid = NULL; - gamedata->challengegridwidth = 0; - M_PopulateChallengeGrid(); - M_UpdateChallengeGridExtraData(challengesmenu.extradata); + i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy; - M_ChallengesAutoFocus(challengesmenu.currentunlock, true); + if (challengesmenu.currentunlock < MAXUNLOCKABLES + && !gamedata->unlocked[challengesmenu.currentunlock] + && !unlockables[challengesmenu.currentunlock].majorunlock + && ((challengesmenu.extradata[i].flags & CHE_HINT) + || (challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY] == 0)) + && gamedata->chaokeys > 0) + { + gamedata->chaokeys--; + challengesmenu.unlockcount[CC_CHAOANIM]++; + + S_StartSound(NULL, sfx_chchng); challengesmenu.pending = true; + M_ChallengesAutoFocus(challengesmenu.currentunlock, false); + } + else + { + challengesmenu.unlockcount[CC_CHAONOPE] = 6; + S_StartSound(NULL, sfx_s3k7b); //sfx_s3kb2 +#if 0 // debugging + if (challengesmenu.currentunlock < MAXUNLOCKABLES) + { + if (gamedata->unlocked[challengesmenu.currentunlock] && challengesmenu.unlockanim >= UNLOCKTIME) + { + if (challengesmenu.unlockcount[CC_TALLY] > 0) + challengesmenu.unlockcount[CC_TALLY]--; + else + challengesmenu.unlockcount[CC_UNLOCKED]--; + } + + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = NULL; + gamedata->challengegridwidth = 0; + M_PopulateChallengeGrid(); + M_UpdateChallengeGridExtraData(challengesmenu.extradata); + challengesmenu.pending = true; + M_ChallengesAutoFocus(challengesmenu.currentunlock, true); + } +#endif } return true; } -#endif else { if (M_MenuBackPressed(pid) || start) diff --git a/src/menus/extras-statistics.c b/src/menus/extras-statistics.c index 97f4b3192..356bf46b2 100644 --- a/src/menus/extras-statistics.c +++ b/src/menus/extras-statistics.c @@ -8,37 +8,80 @@ struct statisticsmenu_s statisticsmenu; +static boolean M_StatisticsAddMap(UINT16 map, cupheader_t *cup, boolean *headerexists) +{ + if (!mapheaderinfo[map]) + return false; + + if (mapheaderinfo[map]->cup != cup) + return false; + + // Check for no visibility + if (mapheaderinfo[map]->menuflags & (LF2_NOTIMEATTACK|LF2_HIDEINSTATS|LF2_HIDEINMENU)) + return false; + + // No TEST RUN, as that's another exception to Time Attack too + if (!mapheaderinfo[map]->typeoflevel) + return false; + + // Check for completion + if ((mapheaderinfo[map]->menuflags & LF2_FINISHNEEDED) + && !(mapheaderinfo[map]->mapvisited & MV_BEATEN)) + return false; + + // Check for unlock + if (M_MapLocked(map+1)) + return false; + + if (*headerexists == false) + { + statisticsmenu.maplist[statisticsmenu.nummaps++] = NEXTMAP_TITLE; // cheeky hack + *headerexists = true; + } + + statisticsmenu.maplist[statisticsmenu.nummaps++] = map; + return true; +} + void M_Statistics(INT32 choice) { - UINT16 i = 0; + cupheader_t *cup; + UINT16 i; + boolean headerexists; (void)choice; - statisticsmenu.maplist = Z_Malloc(sizeof(UINT16) * nummapheaders, PU_STATIC, NULL); + statisticsmenu.maplist = Z_Malloc(sizeof(UINT16) * (nummapheaders+1 + numkartcupheaders), PU_STATIC, NULL); statisticsmenu.nummaps = 0; + for (cup = kartcupheaders; cup; cup = cup->next) + { + headerexists = false; + + if (M_CupLocked(cup)) + continue; + + for (i = 0; i < CUPCACHE_MAX; i++) + { + if (cup->cachedlevels[i] >= nummapheaders) + continue; + + M_StatisticsAddMap(cup->cachedlevels[i], cup, &headerexists); + } + } + + headerexists = false; + for (i = 0; i < nummapheaders; i++) { - if (!mapheaderinfo[i]) - continue; - - // Check for no visibility + legacy box - if (mapheaderinfo[i]->menuflags & (LF2_NOTIMEATTACK|LF2_HIDEINSTATS|LF2_HIDEINMENU)) - continue; - - // Check for completion - if ((mapheaderinfo[i]->menuflags & LF2_FINISHNEEDED) - && !(mapheaderinfo[i]->mapvisited & MV_BEATEN)) - continue; - - // Check for unlock - if (M_MapLocked(i+1)) - continue; - - statisticsmenu.maplist[statisticsmenu.nummaps++] = i; + M_StatisticsAddMap(i, NULL, &headerexists); } + + if ((i = statisticsmenu.numextramedals = M_CountMedals(true, true)) != 0) + i += 2; + statisticsmenu.maplist[statisticsmenu.nummaps] = NEXTMAP_INVALID; - statisticsmenu.maxscroll = (statisticsmenu.nummaps + M_CountMedals(true, true) + 2) - 10; + statisticsmenu.maxscroll = (statisticsmenu.nummaps + i) - 11; statisticsmenu.location = 0; if (statisticsmenu.maxscroll < 0) diff --git a/src/menus/options-1.c b/src/menus/options-1.c index b853c89a6..bc55f41e6 100644 --- a/src/menus/options-1.c +++ b/src/menus/options-1.c @@ -91,6 +91,9 @@ void M_InitOptions(INT32 choice) (M_SecretUnlocked(SECRET_ENCORE, false) ? (IT_STRING | IT_CVAR) : IT_DISABLED); } + OPTIONS_DataDef.menuitems[dopt_addon].status = (M_SecretUnlocked(SECRET_ADDONS, true) + ? (IT_STRING | IT_SUBMENU) + : (IT_TRANSTEXT2 | IT_SPACE)); OPTIONS_DataDef.menuitems[dopt_erase].status = (gamestate == GS_MENU ? (IT_STRING | IT_SUBMENU) : (IT_TRANSTEXT2 | IT_SPACE)); diff --git a/src/menus/options-data-erase-1.c b/src/menus/options-data-erase-1.c index f40e106c6..faf893d32 100644 --- a/src/menus/options-data-erase-1.c +++ b/src/menus/options-data-erase-1.c @@ -6,27 +6,31 @@ #include "../m_cond.h" // Condition Sets #include "../f_finale.h" +#define EC_CHALLENGES 0x01 +#define EC_STATISTICS 0x02 +#define EC_TIMEATTACK 0x04 +#define EC_ALLGAME (EC_CHALLENGES|EC_STATISTICS|EC_TIMEATTACK) + menuitem_t OPTIONS_DataErase[] = { + {IT_STRING | IT_CALL, "Erase Challenges Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_CHALLENGES, 0}, - {IT_STRING | IT_CALL, "Erase Time Attack Data", "Be careful! What's deleted is gone forever!", - NULL, {.routine = M_EraseData}, 0, 0}, + {IT_STRING | IT_CALL, "Erase Statistics Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_STATISTICS, 0}, - {IT_STRING | IT_CALL, "Erase Unlockable Data", "Be careful! What's deleted is gone forever!", - NULL, {.routine = M_EraseData}, 0, 0}, + {IT_STRING | IT_CALL, "Erase GP & Time Attack Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_TIMEATTACK, 0}, + + {IT_STRING | IT_CALL, "\x85\x45rase all Game Data", "Be careful! What's deleted is gone forever!", + NULL, {.routine = M_EraseData}, EC_ALLGAME, 0}, {IT_SPACE | IT_NOTHING, NULL, NULL, NULL, {NULL}, 0, 0}, - {IT_STRING | IT_CALL, "Erase Profile Data...", "Select a Profile to erase.", + {IT_STRING | IT_CALL, "Erase a Profile...", "Select a Profile to erase.", NULL, {.routine = M_CheckProfileData}, 0, 0}, - {IT_SPACE | IT_NOTHING, NULL, NULL, - NULL, {NULL}, 0, 0}, - - {IT_STRING | IT_CALL, "\x85\x45rase all Data", "Be careful! What's deleted is gone forever!", - NULL, {.routine = M_EraseData}, 0, 0}, - }; menu_t OPTIONS_DataEraseDef = { @@ -53,17 +57,17 @@ static void M_EraseDataResponse(INT32 ch) S_StartSound(NULL, sfx_itrole); // bweh heh heh // Delete the data - if (optionsmenu.erasecontext == 2) - { - // SRB2Kart: This actually needs to be done FIRST, so that you don't immediately regain playtime/matches secrets - gamedata->totalplaytime = 0; - gamedata->matchesplayed = 0; - } - if (optionsmenu.erasecontext != 1) + // see also G_LoadGameData + // We do these in backwards order to prevent things from being immediately re-unlocked. + if (optionsmenu.erasecontext & EC_TIMEATTACK) G_ClearRecords(); - if (optionsmenu.erasecontext != 0) + if (optionsmenu.erasecontext & EC_STATISTICS) + M_ClearStats(); + if (optionsmenu.erasecontext & EC_CHALLENGES) M_ClearSecrets(); + M_UpdateUnlockablesAndExtraEmblems(false, true); + F_StartIntro(); M_ClearMenus(true); } @@ -71,15 +75,21 @@ static void M_EraseDataResponse(INT32 ch) void M_EraseData(INT32 choice) { const char *eschoice, *esstr = M_GetText("Are you sure you want to erase\n%s?\n\nPress (A) to confirm or (B) to cancel\n"); + (void)choice; - optionsmenu.erasecontext = (UINT8)choice; + optionsmenu.erasecontext = (UINT8)currentMenu->menuitems[itemOn].mvar1; - if (choice == 0) - eschoice = M_GetText("Time Attack data"); - else if (choice == 1) - eschoice = M_GetText("Secrets data"); - else + if (optionsmenu.erasecontext == EC_CHALLENGES) + eschoice = M_GetText("Challenges data"); + else if (optionsmenu.erasecontext == EC_STATISTICS) + eschoice = M_GetText("Statistics data"); + else if (optionsmenu.erasecontext == EC_TIMEATTACK) + eschoice = M_GetText("GP & Time Attack data"); + else if (optionsmenu.erasecontext == EC_ALLGAME) eschoice = M_GetText("ALL game data"); + else + eschoice = "[misconfigured erasecontext]"; + M_StartMessage(va(esstr, eschoice), FUNCPTRCAST(M_EraseDataResponse), MM_YESNO); } diff --git a/src/menus/play-1.c b/src/menus/play-1.c index d4c8ee099..53ca72dc3 100644 --- a/src/menus/play-1.c +++ b/src/menus/play-1.c @@ -2,16 +2,42 @@ /// \brief Play Menu #include "../k_menu.h" +#include "../m_cond.h" menuitem_t PLAY_MainMenu[] = { {IT_STRING | IT_CALL, "Local Play", "Play only on this computer.", NULL, {.routine = M_SetupGametypeMenu}, 0, 0}, - {IT_STRING | IT_CALL, "Online", "Connect to other computers.", + {IT_STRING | IT_CALL, "Online", NULL, NULL, {.routine = M_MPOptSelectInit}, /*M_MPRoomSelectInit,*/ 0, 0}, {IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0}, }; menu_t PLAY_MainDef = KARTGAMEMODEMENU(PLAY_MainMenu, &PLAY_CharSelectDef); + +void M_SetupPlayMenu(INT32 choice) +{ +#ifdef TESTERS + (void)choice; +#else + if (choice != -1) + PLAY_MainDef.prevMenu = currentMenu; + + // Enable/disable online play. + if (!M_SecretUnlocked(SECRET_ONLINE, true)) + { + PLAY_MainMenu[1].status = IT_TRANSTEXT2 | IT_CALL; + PLAY_MainMenu[1].tooltip = "You'll need experience to play over the internet!"; + } + else + { + PLAY_MainMenu[1].status = IT_STRING | IT_CALL; + PLAY_MainMenu[1].tooltip = "Connect to other computers over the internet."; + } + + if (choice != -1) + M_SetupNextMenu(&PLAY_MainDef, false); +#endif +} diff --git a/src/menus/play-char-select.c b/src/menus/play-char-select.c index 2c041b636..8c0d36601 100644 --- a/src/menus/play-char-select.c +++ b/src/menus/play-char-select.c @@ -5,6 +5,7 @@ #include "../r_skins.h" #include "../s_sound.h" #include "../k_grandprix.h" // K_CanChangeRules +#include "../m_cond.h" // Condition Sets menuitem_t PLAY_CharSelect[] = { @@ -1503,7 +1504,7 @@ void M_CharacterSelectTick(void) #if defined (TESTERS) M_MPOptSelectInit(0); #else - M_SetupNextMenu(&PLAY_MainDef, false); + M_SetupPlayMenu(0); #endif } diff --git a/src/menus/play-local-1.c b/src/menus/play-local-1.c index edcfc2bc0..21f127e0a 100644 --- a/src/menus/play-local-1.c +++ b/src/menus/play-local-1.c @@ -12,7 +12,7 @@ menuitem_t PLAY_GamemodesMenu[] = {IT_STRING | IT_CALL, "Battle", "It's last kart standing in this free-for-all!", "MENIMG00", {.routine = M_LevelSelectInit}, 0, GT_BATTLE}, - {IT_STRING | IT_CALL, "Capsules", "Bust up all of the capsules in record time!", + {IT_STRING | IT_CALL, "Prisons", "Bust up all of the Prison Eggs in record time!", NULL, {.routine = M_LevelSelectInit}, 1, GT_BATTLE}, {IT_STRING | IT_CALL, "Special", "Strike your target and secure the prize!", @@ -42,7 +42,7 @@ void M_SetupGametypeMenu(INT32 choice) { boolean anyunlocked = false; - if (M_SecretUnlocked(SECRET_BREAKTHECAPSULES, true)) + if (M_SecretUnlocked(SECRET_PRISONBREAK, true)) { // Re-add Capsules PLAY_GamemodesMenu[2].status = IT_STRING | IT_CALL; @@ -59,7 +59,7 @@ void M_SetupGametypeMenu(INT32 choice) if (!anyunlocked) { // Only one non-Back entry, let's skip straight to Race. - M_SetupRaceMenu(0); + M_SetupRaceMenu(choice); return; } } diff --git a/src/menus/play-local-race-difficulty.c b/src/menus/play-local-race-difficulty.c index 30e6436fc..db9704be2 100644 --- a/src/menus/play-local-race-difficulty.c +++ b/src/menus/play-local-race-difficulty.c @@ -89,7 +89,7 @@ void M_SetupDifficultyOptions(INT32 choice) PLAY_RaceDifficulty[drace_mapselect].status = IT_STRING|IT_CALL; // Level Select (Match Race) PLAY_RaceDifficultyDef.lastOn = drace_mapselect; // Select map select by default. - if (M_SecretUnlocked(SECRET_ENCORE, false)) + if (M_SecretUnlocked(SECRET_ENCORE, true)) { PLAY_RaceDifficulty[drace_encore].status = IT_STRING2|IT_CVAR; // Encore on/off } diff --git a/src/menus/play-local-race-time-attack.c b/src/menus/play-local-race-time-attack.c index abdff5762..4c51a2a6c 100644 --- a/src/menus/play-local-race-time-attack.c +++ b/src/menus/play-local-race-time-attack.c @@ -9,6 +9,7 @@ #include "../d_main.h" // srb2home #include "../m_misc.h" // M_MkdirEach #include "../z_zone.h" // Z_StrDup/Z_Free +#include "../m_cond.h" static void CV_SPBAttackChanged(void) { @@ -21,7 +22,11 @@ struct timeattackmenu_s timeattackmenu; void M_TimeAttackTick(void) { - timeattackmenu.ticker++; + timeattackmenu.ticker++; + if (timeattackmenu.spbflicker > 0) + { + timeattackmenu.spbflicker--; + } } boolean M_TimeAttackInputs(INT32 ch) @@ -30,10 +35,10 @@ boolean M_TimeAttackInputs(INT32 ch) const boolean buttonR = M_MenuButtonPressed(pid, MBT_R); (void) ch; - if (buttonR && levellist.newgametype == GT_RACE) + if (buttonR && levellist.newgametype == GT_RACE && M_SecretUnlocked(SECRET_SPBATTACK, true)) { CV_AddValue(&cv_dummyspbattack, 1); - timeattackmenu.spbflicker = timeattackmenu.ticker; + timeattackmenu.spbflicker = TICRATE/6; if (cv_dummyspbattack.value) { S_StartSound(NULL, sfx_s3k9f); @@ -231,6 +236,9 @@ void M_PrepareTimeAttack(INT32 choice) } } + if (levellist.levelsearch.timeattack == false || levellist.newgametype != GT_RACE || !M_SecretUnlocked(SECRET_SPBATTACK, true)) + CV_SetValue(&cv_dummyspbattack, 0); + // Time-sticker Medals G_UpdateTimeStickerMedals(levellist.choosemap, false); diff --git a/src/menus/play-online-1.c b/src/menus/play-online-1.c index e48154648..e146926b1 100644 --- a/src/menus/play-online-1.c +++ b/src/menus/play-online-1.c @@ -2,6 +2,8 @@ /// \brief MULTIPLAYER OPTION SELECT #include "../k_menu.h" +#include "../m_cond.h" +#include "../s_sound.h" #if defined (TESTERS) #define IT_STRING_CALL_NOTESTERS IT_DISABLED @@ -62,6 +64,15 @@ void M_MPOptSelectInit(INT32 choice) INT16 arrcpy[3][3] = {{0,68,0}, {0,12,0}, {0,74,0}}; const UINT32 forbidden = GTR_FORBIDMP; +#ifndef TESTERS + if (choice != -1 && !M_SecretUnlocked(SECRET_ONLINE, true)) + { + M_StartMessage("Online play is ""\x8B""not yet unlocked""\x80"".\n\nYou'll want experience in ""\x8B""Grand Prix""\x80""\nbefore even thinking about facing\nopponents from across the world.\n\nPress (B)", NULL, MM_NOTHING); + S_StartSound(NULL, sfx_s3k36); + return; + } +#endif + mpmenu.modechoice = 0; mpmenu.ticker = 0; diff --git a/src/menus/play-online-room-select.c b/src/menus/play-online-room-select.c index 349421d94..b1364f020 100644 --- a/src/menus/play-online-room-select.c +++ b/src/menus/play-online-room-select.c @@ -3,6 +3,7 @@ #include "../k_menu.h" #include "../s_sound.h" +#include "../m_cond.h" menuitem_t PLAY_MP_RoomSelect[] = { @@ -30,23 +31,23 @@ void M_MPRoomSelect(INT32 choice) const UINT8 pid = 0; (void) choice; - if (menucmd[pid].dpad_lr) - { - mpmenu.room = (!mpmenu.room) ? 1 : 0; - S_StartSound(NULL, sfx_s3k5b); - M_SetMenuDelay(pid); - } - else if (M_MenuBackPressed(pid)) + if (M_MenuBackPressed(pid)) { M_GoBack(0); M_SetMenuDelay(pid); } - else if (M_MenuConfirmPressed(pid)) { M_ServersMenu(0); M_SetMenuDelay(pid); } + else if (mpmenu.roomforced == false + && menucmd[pid].dpad_lr != 0) + { + mpmenu.room ^= 1; + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } } void M_MPRoomSelectTick(void) @@ -57,7 +58,8 @@ void M_MPRoomSelectTick(void) void M_MPRoomSelectInit(INT32 choice) { (void)choice; - mpmenu.room = 0; + mpmenu.room = (modifiedgame == true) ? 1 : 0; + mpmenu.roomforced = ((modifiedgame == true) || (!M_SecretUnlocked(SECRET_ADDONS, true))); mpmenu.ticker = 0; mpmenu.servernum = 0; mpmenu.scrolln = 0; diff --git a/src/menus/transient/cup-select.c b/src/menus/transient/cup-select.c index 23a85e4e0..cca546f9e 100644 --- a/src/menus/transient/cup-select.c +++ b/src/menus/transient/cup-select.c @@ -84,7 +84,7 @@ void M_CupSelectHandler(INT32 choice) if (M_MenuConfirmPressed(pid) /*|| M_MenuButtonPressed(pid, MBT_START)*/) { - INT16 count; + UINT16 count; cupheader_t *newcup = cupgrid.builtgrid[CUPMENU_CURSORID]; cupheader_t *oldcup = levellist.levelsearch.cup; @@ -94,7 +94,7 @@ void M_CupSelectHandler(INT32 choice) count = M_CountLevelsToShowInList(&levellist.levelsearch); if ((!newcup) - || (count <= 0) + || (count == 0) || (cupgrid.grandprix == true && newcup->cachedlevels[0] == NEXTMAP_INVALID)) { S_StartSound(NULL, sfx_s3kb2); @@ -161,7 +161,7 @@ void M_CupSelectHandler(INT32 choice) restoreMenu = &PLAY_CupSelectDef; } - else if (count == 1) + else if (count == 1 && levellist.levelsearch.timeattack == true) { PLAY_TimeAttackDef.transitionID = currentMenu->transitionID+1; M_LevelSelected(0); @@ -174,6 +174,7 @@ void M_CupSelectHandler(INT32 choice) levellist.cursor = 0; } + levellist.mapcount = count; M_LevelSelectScrollDest(); levellist.y = levellist.dest; diff --git a/src/menus/transient/level-select.c b/src/menus/transient/level-select.c index 20442ceb6..11a62e944 100644 --- a/src/menus/transient/level-select.c +++ b/src/menus/transient/level-select.c @@ -9,6 +9,8 @@ #include "../../f_finale.h" // F_WipeStartScreen #include "../../v_video.h" +cupheader_t dummy_lostandfound; + menuitem_t PLAY_LevelSelect[] = { {IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, {.routine = M_LevelSelectHandler}, 0, 0}, @@ -57,8 +59,9 @@ boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch) if (mapheaderinfo[mapnum]->lumpnum == LUMPERROR) return false; - // Check for TOL - if (!(mapheaderinfo[mapnum]->typeoflevel & levelsearch->typeoflevel)) + // Check for TOL (permits TEST RUN outside of time attack) + if ((levelsearch->timeattack || mapheaderinfo[mapnum]->typeoflevel) + && !(mapheaderinfo[mapnum]->typeoflevel & levelsearch->typeoflevel)) return false; // Should the map be hidden? @@ -70,10 +73,14 @@ boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch) return false; // Don't permit cup when no cup requested (also no dupes in time attack) - if (levelsearch->cupmode - && (levelsearch->timeattack || !levelsearch->cup) - && mapheaderinfo[mapnum]->cup != levelsearch->cup) - return false; + if (levelsearch->cupmode) + { + cupheader_t *cup = (levelsearch->cup == &dummy_lostandfound) ? NULL : levelsearch->cup; + + if ((!cup || levelsearch->timeattack) + && mapheaderinfo[mapnum]->cup != cup) + return false; + } // Finally, the most complex check: does the map have lock conditions? if (levelsearch->checklocked) @@ -97,9 +104,9 @@ UINT16 M_CountLevelsToShowInList(levelsearch_t *levelsearch) INT16 i, count = 0; if (!levelsearch) - return false; + return 0; - if (levelsearch->cup) + if (levelsearch->cup && levelsearch->cup != &dummy_lostandfound) { if (levelsearch->checklocked && M_CupLocked(levelsearch->cup)) return 0; @@ -126,9 +133,9 @@ UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch) INT16 mapnum = NEXTMAP_INVALID; if (!levelsearch) - return false; + return NEXTMAP_INVALID; - if (levelsearch->cup) + if (levelsearch->cup && levelsearch->cup != &dummy_lostandfound) { if (levelsearch->checklocked && M_CupLocked(levelsearch->cup)) { @@ -151,6 +158,9 @@ UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch) for (mapnum = 0; mapnum < nummapheaders; mapnum++) if (M_CanShowLevelInList(mapnum, levelsearch)) break; + + if (mapnum >= nummapheaders) + mapnum = NEXTMAP_INVALID; } return mapnum; @@ -159,9 +169,9 @@ UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch) UINT16 M_GetNextLevelInList(UINT16 mapnum, UINT8 *i, levelsearch_t *levelsearch) { if (!levelsearch) - return false; + return NEXTMAP_INVALID; - if (levelsearch->cup) + if (levelsearch->cup && levelsearch->cup != &dummy_lostandfound) { mapnum = NEXTMAP_INVALID; (*i)++; @@ -185,14 +195,14 @@ UINT16 M_GetNextLevelInList(UINT16 mapnum, UINT8 *i, levelsearch_t *levelsearch) void M_LevelSelectScrollDest(void) { - UINT16 m = M_CountLevelsToShowInList(&levellist.levelsearch)-1; + UINT16 m = levellist.mapcount-1; levellist.dest = (6*levellist.cursor); if (levellist.dest < 3) levellist.dest = 3; - if (levellist.dest > (6*m)-3) + if (m && levellist.dest > (6*m)-3) levellist.dest = (6*m)-3; } @@ -209,6 +219,9 @@ boolean M_LevelListFromGametype(INT16 gt) { cupgrid.cappages = 0; cupgrid.builtgrid = NULL; + dummy_lostandfound.cachedlevels[0] = NEXTMAP_INVALID; + + first = false; } levellist.newgametype = gt; @@ -227,11 +240,8 @@ boolean M_LevelListFromGametype(INT16 gt) levellist.levelsearch.cupmode = (!(gametypes[gt]->rules & GTR_NOCUPSELECT)); - first = false; - } - - if (levellist.levelsearch.timeattack == false || levellist.newgametype != GT_RACE) CV_SetValue(&cv_dummyspbattack, 0); + } // Obviously go to Cup Select in gametypes that have cups. // Use a really long level select in gametypes that don't use cups. @@ -266,6 +276,31 @@ boolean M_LevelListFromGametype(INT16 gt) } memset(cupgrid.builtgrid, 0, cupgrid.cappages * pagelen); + // The following doubles the size of the buffer if necessary. +#define GRID_INSERTCUP \ + if ((currentid * sizeof(cupheader_t*)) >= cupgrid.cappages * pagelen) \ + { \ + const size_t firstlen = cupgrid.cappages * pagelen; \ + cupgrid.builtgrid = Z_Realloc(cupgrid.builtgrid, \ + firstlen * 2, \ + PU_STATIC, NULL); \ + \ + if (!cupgrid.builtgrid) \ + { \ + I_Error("M_LevelListFromGametype: Not enough memory to reallocate builtgrid"); \ + } \ + \ + cupgrid.cappages *= 2; \ + } \ + \ + cupgrid.builtgrid[currentid] = templevelsearch.cup; + +#define GRID_FOCUSCUP \ + cupgrid.x = currentid % CUPMENU_COLUMNS; \ + cupgrid.y = (currentid / CUPMENU_COLUMNS) % CUPMENU_ROWS; \ + cupgrid.pageno = currentid / (CUPMENU_COLUMNS * CUPMENU_ROWS); \ + currentvalid = true; + while (templevelsearch.cup) { templevelsearch.checklocked = false; @@ -278,23 +313,7 @@ boolean M_LevelListFromGametype(INT16 gt) foundany = true; - if ((currentid * sizeof(cupheader_t*)) >= cupgrid.cappages * pagelen) - { - // Double the size of the buffer, and clear the other stuff. - const size_t firstlen = cupgrid.cappages * pagelen; - cupgrid.builtgrid = Z_Realloc(cupgrid.builtgrid, - firstlen * 2, - PU_STATIC, NULL); - - if (!cupgrid.builtgrid) - { - I_Error("M_LevelListFromGametype: Not enough memory to reallocate builtgrid"); - } - - cupgrid.cappages *= 2; - } - - cupgrid.builtgrid[currentid] = templevelsearch.cup; + GRID_INSERTCUP; templevelsearch.checklocked = true; if (M_GetFirstLevelInList(&temp, &templevelsearch) != NEXTMAP_INVALID) @@ -305,10 +324,7 @@ boolean M_LevelListFromGametype(INT16 gt) ? (mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->cup == templevelsearch.cup) : (gt == -1 && levellist.levelsearch.cup == templevelsearch.cup)) { - cupgrid.x = currentid % CUPMENU_COLUMNS; - cupgrid.y = (currentid / CUPMENU_COLUMNS) % CUPMENU_ROWS; - cupgrid.pageno = currentid / (CUPMENU_COLUMNS * CUPMENU_ROWS); - currentvalid = true; + GRID_FOCUSCUP; } } @@ -316,6 +332,34 @@ boolean M_LevelListFromGametype(INT16 gt) templevelsearch.cup = templevelsearch.cup->next; } + // Lost and found, a simplified version of the above loop. + if (cupgrid.grandprix == false) + { + templevelsearch.cup = &dummy_lostandfound; + templevelsearch.checklocked = true; + + if (M_GetFirstLevelInList(&temp, &levellist.levelsearch) != NEXTMAP_INVALID) + { + foundany = true; + GRID_INSERTCUP; + highestunlockedid = currentid; + + if (Playing() + ? (mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->cup == NULL) + : (gt == -1 && levellist.levelsearch.cup == templevelsearch.cup)) + { + GRID_FOCUSCUP; + } + + currentid++; + } + + templevelsearch.cup = NULL; + } + +#undef GRID_INSERTCUP +#undef GRID_FOCUSCUP + if (foundany == false) { return false; @@ -356,6 +400,7 @@ boolean M_LevelListFromGametype(INT16 gt) levellist.levelsearch.cup = NULL; } + levellist.mapcount = M_CountLevelsToShowInList(&levellist.levelsearch); M_LevelSelectScrollDest(); levellist.y = levellist.dest; @@ -518,7 +563,6 @@ void M_LevelSelected(INT16 add) void M_LevelSelectHandler(INT32 choice) { - INT16 maxlevels = M_CountLevelsToShowInList(&levellist.levelsearch); const UINT8 pid = 0; (void)choice; @@ -531,7 +575,7 @@ void M_LevelSelectHandler(INT32 choice) if (menucmd[pid].dpad_ud > 0) { levellist.cursor++; - if (levellist.cursor >= maxlevels) + if (levellist.cursor >= levellist.mapcount) levellist.cursor = 0; S_StartSound(NULL, sfx_s3k5b); M_SetMenuDelay(pid); @@ -540,7 +584,7 @@ void M_LevelSelectHandler(INT32 choice) { levellist.cursor--; if (levellist.cursor < 0) - levellist.cursor = maxlevels-1; + levellist.cursor = levellist.mapcount-1; S_StartSound(NULL, sfx_s3k5b); M_SetMenuDelay(pid); } diff --git a/src/menus/transient/message-box.c b/src/menus/transient/message-box.c index 42591352d..605949b68 100644 --- a/src/menus/transient/message-box.c +++ b/src/menus/transient/message-box.c @@ -27,7 +27,7 @@ static inline size_t M_StringHeight(const char *string) void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtype) { const UINT8 pid = 0; - size_t max = 0, start = 0, strlines = 0, i; + size_t max = 0, maxatstart = 0, start = 0, strlines, i; static char *message = NULL; Z_Free(message); message = Z_StrDup(string); @@ -41,12 +41,13 @@ void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtyp { start = i; max += 4; + maxatstart = max; } else if (message[i] == '\n') { - strlines = i; start = 0; max = 0; + maxatstart = 0; continue; } else if (message[i] & 0x80) @@ -58,8 +59,7 @@ void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtyp if (max >= BASEVIDWIDTH && start > 0) { message[start] = '\n'; - max -= (start-strlines)*8; - strlines = start; + max -= maxatstart; start = 0; } } diff --git a/src/menus/transient/pause-game.c b/src/menus/transient/pause-game.c index 18b19c169..ff6c5c3b4 100644 --- a/src/menus/transient/pause-game.c +++ b/src/menus/transient/pause-game.c @@ -3,6 +3,7 @@ #include "../../k_menu.h" #include "../../k_grandprix.h" // K_CanChangeRules +#include "../../m_cond.h" #include "../../s_sound.h" // ESC pause menu @@ -136,7 +137,11 @@ void M_OpenPauseMenu(void) PAUSE_Main[mpause_switchmap].status = IT_STRING | IT_CALL; PAUSE_Main[mpause_restartmap].status = IT_STRING | IT_CALL; - PAUSE_Main[mpause_addons].status = IT_STRING | IT_CALL; + + if (M_SecretUnlocked(SECRET_ADDONS, true)) + { + PAUSE_Main[mpause_addons].status = IT_STRING | IT_CALL; + } } } else if (!netgame && !demo.playback) diff --git a/src/p_inter.c b/src/p_inter.c index 433e4e704..0a37235db 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -534,9 +534,9 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) if (P_IsLocalPlayer(player) && !gamedata->collected[special->health-1]) { gamedata->collected[special->health-1] = gotcollected = true; - if (!M_UpdateUnlockablesAndExtraEmblems(true)) + if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) S_StartSound(NULL, sfx_ncitem); - G_SaveGameData(); + gamedata->deferredsave = true; } if (netgame) @@ -636,7 +636,7 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) { (void)target; - if (!battlecapsules) // !battleprisons + if (!battleprisons) return; if ((gametyperules & GTR_POINTLIMIT) && (source && source->player)) @@ -845,7 +845,7 @@ void P_CheckPointLimit(void) if (!(gametyperules & GTR_POINTLIMIT)) return; - if (battlecapsules) + if (battleprisons) return; // pointlimit is nonzero, check if it's been reached by this player @@ -1953,6 +1953,13 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, switch (type) { case DMG_DEATHPIT: + // Fell off the stage + if (player->roundconditions.fell_off == false) + { + player->roundconditions.fell_off = true; + player->roundconditions.checkthisframe = true; + } + if (gametyperules & GTR_BUMPERS) { player->mo->health--; @@ -2087,6 +2094,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da player_t *player; player_t *playerInflictor; boolean force = false; + boolean spbpop = false; INT32 laglength = 6; @@ -2125,6 +2133,16 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da return false; break; + case MT_SPB: + spbpop = (damagetype & DMG_TYPEMASK) == DMG_VOLTAGE; + if (spbpop && source && source->player + && source->player->roundconditions.spb_neuter == false) + { + source->player->roundconditions.spb_neuter = true; + source->player->roundconditions.checkthisframe = true; + } + break; + default: break; } @@ -2143,7 +2161,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da if (!force) { - if (!(target->type == MT_SPB && (damagetype & DMG_TYPEMASK) == DMG_VOLTAGE)) + if (!spbpop) { if (!(target->flags & MF_SHOOTABLE)) return false; // shouldn't happen... @@ -2190,6 +2208,18 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da } } + if (inflictor && source && source->player) + { + if (source->player->roundconditions.hit_midair == false + && K_IsMissileOrKartItem(source) + && target->player->airtime > TICRATE/2 + && source->player->airtime > TICRATE/2) + { + source->player->roundconditions.hit_midair = true; + source->player->roundconditions.checkthisframe = true; + } + } + // Instant-Death if ((damagetype & DMG_DEATHMASK)) { diff --git a/src/p_mobj.c b/src/p_mobj.c index 2c4527fdd..e0a71242c 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -3427,6 +3427,20 @@ void P_MobjCheckWater(mobj_t *mobj) { return; } + + if (!(p->roundconditions.wet_player & MFE_TOUCHWATER) + && (mobj->eflags & MFE_TOUCHWATER)) + { + p->roundconditions.wet_player |= MFE_TOUCHWATER; + p->roundconditions.checkthisframe = true; + } + + if (!(p->roundconditions.wet_player & MFE_UNDERWATER) + && (mobj->eflags & MFE_UNDERWATER)) + { + p->roundconditions.wet_player |= MFE_UNDERWATER; + p->roundconditions.checkthisframe = true; + } } if (mobj->flags & MF_APPLYTERRAIN) @@ -9381,7 +9395,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) { if (gametyperules & GTR_PAPERITEMS) { - if (battlecapsules == true) + if (battleprisons == true) { ; } diff --git a/src/p_saveg.c b/src/p_saveg.c index eca813e50..fdbcb48de 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -488,6 +488,10 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEFIXED(save->p, players[i].loop.shift.x); WRITEFIXED(save->p, players[i].loop.shift.y); WRITEUINT8(save->p, players[i].loop.flip); + + // ACS has read access to this, so it has to be net-communicated. + // It is the ONLY roundcondition that is sent over the wire and I'd like it to stay that way. + WRITEUINT32(save->p, players[i].roundconditions.unlocktriggers); } } @@ -876,6 +880,10 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].loop.shift.y = READFIXED(save->p); players[i].loop.flip = READUINT8(save->p); + // ACS has read access to this, so it has to be net-communicated. + // It is the ONLY roundcondition that is sent over the wire and I'd like it to stay that way. + players[i].roundconditions.unlocktriggers = READUINT32(save->p); + //players[i].viewheight = P_GetPlayerViewHeight(players[i]); // scale cannot be factored in at this point } } @@ -4997,7 +5005,7 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending) // SRB2kart WRITEINT32(save->p, numgotboxes); WRITEUINT8(save->p, numtargets); - WRITEUINT8(save->p, battlecapsules); + WRITEUINT8(save->p, battleprisons); WRITEUINT8(save->p, gamespeed); WRITEUINT8(save->p, numlaps); @@ -5166,7 +5174,7 @@ static inline boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading) // SRB2kart numgotboxes = READINT32(save->p); numtargets = READUINT8(save->p); - battlecapsules = (boolean)READUINT8(save->p); + battleprisons = (boolean)READUINT8(save->p); gamespeed = READUINT8(save->p); numlaps = READUINT8(save->p); diff --git a/src/p_setup.c b/src/p_setup.c index 7e95d7455..40a68144a 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -7071,7 +7071,7 @@ static void P_InitLevelSettings(void) nummaprings = 0; nummapboxes = numgotboxes = 0; maptargets = numtargets = 0; - battlecapsules = false; + battleprisons = false; // emerald hunt hunt1 = hunt2 = hunt3 = NULL; @@ -7501,6 +7501,7 @@ static void P_InitGametype(void) // Started a game? Move on to the next jam when you go back to the title screen CV_SetValue(&cv_menujam_update, 1); + gamedata->musicflags = 0; } struct minimapinfo minimapinfo; @@ -7993,7 +7994,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) { mapheaderinfo[gamemap-1]->mapvisited |= MV_VISITED; - M_UpdateUnlockablesAndExtraEmblems(true); + M_UpdateUnlockablesAndExtraEmblems(true, true); G_SaveGameData(); } diff --git a/src/p_spec.c b/src/p_spec.c index c321c80b8..79fde7570 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -1541,17 +1541,18 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller return false; break; case 317: + if (actor && actor->player) { // Unlockable triggers required INT32 trigid = triggerline->args[1]; - if ((modifiedgame && !savemoddata) || (netgame || multiplayer)) - return false; - else if (trigid < 0 || trigid > 31) // limited by 32 bit variable + if (trigid < 0 || trigid > 31) // limited by 32 bit variable { CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger (sidedef %hu): bad trigger ID %d\n", triggerline->sidenum[0], trigid); return false; } - else if (!(unlocktriggers & (1 << trigid))) + else if (!(actor && actor->player)) + return false; + else if (!(actor->player->roundconditions.unlocktriggers & (1 << trigid))) return false; } break; @@ -2111,6 +2112,12 @@ static void K_HandleLapIncrement(player_t *player) } lastLowestLap = lowestLap; + + if (P_IsLocalPlayer(player)) + { + player->roundconditions.checkthisframe = true; + gamedata->deferredconditioncheck = true; + } } else if (player->starpostnum) { @@ -3420,29 +3427,23 @@ boolean P_ProcessSpecial(activator_t *activator, INT16 special, INT32 *args, cha break; case 441: // Trigger unlockable - if (!(demo.playback || netgame || multiplayer)) { INT32 trigid = args[0]; if (trigid < 0 || trigid > 31) // limited by 32 bit variable CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger: bad trigger ID %d\n", trigid); - else + else if (mo && mo->player) { UINT32 flag = 1 << trigid; - if (unlocktriggers & flag) + if (mo->player->roundconditions.unlocktriggers & flag) { // Execute one time only break; } - unlocktriggers |= flag; - - // Unlocked something? - if (M_UpdateUnlockablesAndExtraEmblems(true)) - { - G_SaveGameData(); // only save if unlocked something - } + mo->player->roundconditions.unlocktriggers |= flag; + mo->player->roundconditions.checkthisframe = true; } } break; diff --git a/src/p_tick.c b/src/p_tick.c index eb733affe..32881d68e 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -708,6 +708,11 @@ void P_Ticker(boolean run) } ps_playerthink_time = I_GetPreciseTime() - ps_playerthink_time; + + // TODO would this be laggy with more conditions in play... + if (((!demo.playback && leveltime > introtime && M_UpdateUnlockablesAndExtraEmblems(true, false)) + || (gamedata && gamedata->deferredsave))) + G_SaveGameData(); } // Keep track of how long they've been playing! diff --git a/src/p_user.c b/src/p_user.c index 98e5b92c2..823a65dc4 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -509,6 +509,11 @@ INT32 P_GivePlayerRings(player_t *player, INT32 num_rings) if ((gametyperules & GTR_SPHERES)) // No rings in Battle Mode return 0; + if (gamedata && num_rings > 0 && P_IsLocalPlayer(player) && gamedata->totalrings <= GDMAX_RINGS) + { + gamedata->totalrings += num_rings; + } + test = player->rings + num_rings; if (test > 20) // Caps at 20 rings, sorry! num_rings -= (test-20); @@ -517,6 +522,12 @@ INT32 P_GivePlayerRings(player_t *player, INT32 num_rings) player->rings += num_rings; + if (player->roundconditions.debt_rings == false && player->rings < 0) + { + player->roundconditions.debt_rings = true; + player->roundconditions.checkthisframe = true; + } + return num_rings; } @@ -1268,7 +1279,11 @@ void P_DoPlayerExit(player_t *player) return; if (P_IsLocalPlayer(player) && (!player->spectator && !demo.playback)) + { legitimateexit = true; + player->roundconditions.checkthisframe = true; + gamedata->deferredconditioncheck = true; + } if (G_GametypeUsesLives() && losing) { @@ -3807,6 +3822,8 @@ void P_DoTimeOver(player_t *player) if (P_IsLocalPlayer(player) && !demo.playback) { legitimateexit = true; // SRB2kart: losing a race is still seeing it through to the end :p + player->roundconditions.checkthisframe = true; + gamedata->deferredconditioncheck = true; } if (netgame && !player->bot && !(gametyperules & GTR_BOSS)) diff --git a/src/r_skins.c b/src/r_skins.c index d6294fa0d..7623f2232 100644 --- a/src/r_skins.c +++ b/src/r_skins.c @@ -1,5 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- +// Copyright (C) 2016-2023 by Vivian "toastergrl" Grannell // Copyright (C) 1993-1996 by id Software, Inc. // Copyright (C) 1998-2000 by DooM Legacy Team. // Copyright (C) 1999-2020 by Sonic Team Junior. @@ -129,6 +130,27 @@ static void Sk_SetDefaultValue(skin_t *skin) skin->soundsid[S_sfx[i].skinsound] = i; } +// Grab the default skin +UINT8 R_BotDefaultSkin(void) +{ + static INT32 defaultbotskin = -1; + + if (defaultbotskin == -1) + { + const char *defaultbotskinname = "eggrobo"; + + defaultbotskin = R_SkinAvailable(defaultbotskinname); + + if (defaultbotskin == -1) + { + // This shouldn't happen, but just in case + defaultbotskin = 0; + } + } + + return (UINT8)defaultbotskin; +} + // // Initialize the basic skins // @@ -156,13 +178,15 @@ void R_InitSkins(void) R_LoadSpriteInfoLumps(i, wadfiles[i]->numlumps); } ST_ReloadSkinFaceGraphics(); + M_UpdateConditionSetsPending(); } -UINT8 *R_GetSkinAvailabilities(boolean demolock) +UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots) { UINT8 i, shif, byte; INT32 skinid; static UINT8 responsebuffer[MAXAVAILABILITY]; + UINT8 defaultbotskin = R_BotDefaultSkin(); memset(&responsebuffer, 0, sizeof(responsebuffer)); @@ -171,15 +195,17 @@ UINT8 *R_GetSkinAvailabilities(boolean demolock) if (unlockables[i].type != SECRET_SKIN) continue; - // NEVER EVER EVER M_CheckNetUnlockByID - if (gamedata->unlocked[i] != true && !demolock) - continue; - skinid = M_UnlockableSkinNum(&unlockables[i]); if (skinid < 0 || skinid >= MAXSKINS) continue; + if ((forbots + ? (M_CheckNetUnlockByID(i) || skinid == defaultbotskin) // Assert the host's lock. + : gamedata->unlocked[i]) // Assert the local lock. + != true && !demolock) + continue; + shif = (skinid % 8); byte = (skinid / 8); @@ -250,8 +276,11 @@ boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins) return !!(players[playernum].availabilities[byte] & (1 << shif)); } + // Use the host's if it's checking general state + if (playernum == -1) + return M_CheckNetUnlockByID(i); + // Use the unlockables table directly - // NOTE: M_CheckNetUnlockByID would be correct in many circumstances... but not all. TODO figure out how to discern. return (boolean)(gamedata->unlocked[i]); } @@ -270,6 +299,25 @@ INT32 R_SkinAvailable(const char *name) return -1; } +// Returns engine class dependent on skin properties +engineclass_t R_GetEngineClass(SINT8 speed, SINT8 weight, skinflags_t flags) +{ + if (flags & SF_IRONMAN) + return ENGINECLASS_J; + + speed = (speed - 1) / 3; + weight = (weight - 1) / 3; + +#define LOCKSTAT(stat) \ + if (stat < 0) { stat = 0; } \ + if (stat > 2) { stat = 2; } + LOCKSTAT(speed); + LOCKSTAT(weight); +#undef LOCKSTAT + + return (speed + (3*weight)); +} + // Auxillary function that actually sets the skin static void SetSkin(player_t *player, INT32 skinnum) { diff --git a/src/r_skins.h b/src/r_skins.h index fe29282e7..2319326fd 100644 --- a/src/r_skins.h +++ b/src/r_skins.h @@ -1,5 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- +// Copyright (C) 2016-2023 by Vivian "toastergrl" Grannell // Copyright (C) 1993-1996 by id Software, Inc. // Copyright (C) 1998-2000 by DooM Legacy Team. // Copyright (C) 1999-2020 by Sonic Team Junior. @@ -74,6 +75,25 @@ enum facepatches { NUMFACES }; +typedef enum { + ENGINECLASS_A, + ENGINECLASS_B, + ENGINECLASS_C, + + ENGINECLASS_D, + ENGINECLASS_E, + ENGINECLASS_F, + + ENGINECLASS_G, + ENGINECLASS_H, + ENGINECLASS_I, + + ENGINECLASS_J + +} engineclass_t; + +engineclass_t R_GetEngineClass(SINT8 speed, SINT8 weight, skinflags_t flags); + /// Externs extern INT32 numskins; extern skin_t skins[MAXSKINS]; @@ -91,8 +111,9 @@ void ClearFakePlayerSkin(player_t* player); boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins); INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight, UINT32 flags, boolean unlock); -UINT8 *R_GetSkinAvailabilities(boolean demolock); +UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots); INT32 R_SkinAvailable(const char *name); +UINT8 R_BotDefaultSkin(void); void R_PatchSkins(UINT16 wadnum, boolean mainfile); void R_AddSkins(UINT16 wadnum, boolean mainfile); diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c index 852b8c8c5..92938e3f7 100644 --- a/src/sdl/i_system.c +++ b/src/sdl/i_system.c @@ -328,6 +328,7 @@ FUNCNORETURN static ATTRNORETURN void signal_handler(INT32 num) { D_QuitNetGame(); // Fix server freezes CL_AbortDownloadResume(); + G_DirtyGameData(); #ifdef UNIXBACKTRACE write_backtrace(num); #endif @@ -724,6 +725,8 @@ static void I_RegisterSignals (void) #ifdef NEWSIGNALHANDLER static void signal_handler_child(INT32 num) { + G_DirtyGameData(); + #ifdef UNIXBACKTRACE write_backtrace(num); #endif @@ -1542,6 +1545,7 @@ void I_Error(const char *error, ...) if (errorcount == 8) { M_SaveConfig(NULL); + G_DirtyGameData(); // done first in case an error is in G_SaveGameData G_SaveGameData(); } if (errorcount > 20) @@ -1573,6 +1577,7 @@ void I_Error(const char *error, ...) M_SaveConfig(NULL); // save game config, cvars.. D_SaveBan(); // save the ban list + G_DirtyGameData(); // done first in case an error is in G_SaveGameData G_SaveGameData(); // Tails 12-08-2002 // Shutdown. Here might be other errors. diff --git a/src/sdl12/i_system.c b/src/sdl12/i_system.c index 7c0abb998..409339ded 100644 --- a/src/sdl12/i_system.c +++ b/src/sdl12/i_system.c @@ -357,6 +357,7 @@ static void signal_handler(INT32 num) sigmsg = sigdef; } + G_DirtyGameData(); I_OutputMsg("signal_handler() error: %s\n", sigmsg); signal(num, SIG_DFL); //default signal action raise(num); @@ -3083,6 +3084,7 @@ void I_Error(const char *error, ...) if (errorcount == 9) { M_SaveConfig(NULL); + G_DirtyGameData(); // done first in case an error is in G_SaveGameData G_SaveGameData(); } if (errorcount > 20) @@ -3147,6 +3149,7 @@ void I_Error(const char *error, ...) #ifndef NONET D_SaveBan(); // save the ban list #endif + G_DirtyGameData(); // done first in case an error is in G_SaveGameData G_SaveGameData(); // Tails 12-08-2002 // Shutdown. Here might be other errors. diff --git a/src/typedef.h b/src/typedef.h index ae61dea9a..0615e7034 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -44,6 +44,7 @@ TYPEDEF (discordRequest_t); // d_player.h TYPEDEF (respawnvars_t); TYPEDEF (botvars_t); +TYPEDEF (roundconditions_t); TYPEDEF (skybox_t); TYPEDEF (itemroulette_t); TYPEDEF (altview_t); @@ -107,6 +108,7 @@ TYPEDEF (skincolor_t); // doomstat.h TYPEDEF (precipprops_t); TYPEDEF (recorddata_t); +TYPEDEF (cupwindata_t); TYPEDEF (scene_t); TYPEDEF (cutscene_t); TYPEDEF (textpage_t); diff --git a/src/w_wad.c b/src/w_wad.c index 635d41caf..ee7d76754 100644 --- a/src/w_wad.c +++ b/src/w_wad.c @@ -49,6 +49,7 @@ #include "fastcmp.h" #include "g_game.h" // G_LoadGameData +#include "m_cond.h" // gamedata itself #include "filesrch.h" #include "i_video.h" // rendermode @@ -813,6 +814,14 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup) } #endif + // Do this immediately before anything of consequence that invalidates gamedata can happen. + if ((mainfile == false) && (gamedata != NULL) && (gamedata->everloadedaddon == false)) + { + gamedata->everloadedaddon = true; + M_UpdateUnlockablesAndExtraEmblems(true, true); + G_SaveGameData(); + } + switch(type = ResourceFileDetect(filename)) { case RET_SOC: diff --git a/src/win32ce/win_sys.c b/src/win32ce/win_sys.c index b261a7a50..0185561a2 100644 --- a/src/win32ce/win_sys.c +++ b/src/win32ce/win_sys.c @@ -469,6 +469,7 @@ static void signal_handler(int num) char sigdef[64]; D_QuitNetGame(); // Fix server freezes + G_DirtyGameData(); I_ShutdownSystem(); switch (num) @@ -607,6 +608,7 @@ void I_Error(const char *error, ...) if (errorcount == 7) { M_SaveConfig(NULL); + G_DirtyGameData(); // done first in case an error is in G_SaveGameData G_SaveGameData(); } if (errorcount > 20) @@ -636,6 +638,7 @@ void I_Error(const char *error, ...) if (!errorcount) { M_SaveConfig(NULL); // save game config, cvars.. + G_DirtyGameData(); // done first in case an error is in G_SaveGameData G_SaveGameData(); } @@ -726,7 +729,7 @@ void I_Quit(void) G_CheckDemoStatus(); M_SaveConfig(NULL); // save game config, cvars.. - G_SaveGameData(); + G_SaveGameData(); // undirty your save // maybe it needs that the ticcount continues, // or something else that will be finished by I_ShutdownSystem(),