diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 61e5ff86f..318c9cb7e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -132,9 +132,12 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 k_roulette.c ) -if("${CMAKE_COMPILER_IS_GNUCC}" AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows" AND NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}" AND NOT "${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}") - # On MinGW with internal libraries, link the standard library statically - target_link_options(SRB2SDL2 PRIVATE "-static") +if("${CMAKE_COMPILER_IS_GNUCC}" AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows") + target_link_options(SRB2SDL2 PRIVATE "-Wl,--disable-dynamicbase") + if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}" AND NOT "${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}") + # On MinGW with internal libraries, link the standard library statically + target_link_options(SRB2SDL2 PRIVATE "-static") + endif() endif() target_compile_features(SRB2SDL2 PRIVATE c_std_11 cxx_std_17) @@ -149,7 +152,11 @@ set(SRB2_CONFIG_USEASM OFF CACHE BOOL set(SRB2_CONFIG_YASM OFF CACHE BOOL "Use YASM in place of NASM.") set(SRB2_CONFIG_DEV_BUILD OFF CACHE BOOL - "Compile a development build of SRB2Kart.") + "Compile a development build of Dr Robotnik's Ring Racers.") +set(SRB2_CONFIG_TESTERS OFF CACHE BOOL + "Compile a build for testers.") +set(SRB2_CONFIG_HOSTTESTERS OFF CACHE BOOL + "Compile a build to host netgames for testers builds.") add_subdirectory(blua) @@ -233,7 +240,7 @@ if(${SRB2_CONFIG_HAVE_ZLIB}) set(SRB2_HAVE_ZLIB ON) target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_ZLIB) else() - message(WARNING "You have specified that ZLIB is available but it was not found. SRB2Kart may not compile correctly.") + message(WARNING "You have specified that ZLIB is available but it was not found. Dr Robotnik's Ring Racers may not compile correctly.") endif() endif() @@ -256,7 +263,7 @@ if(${SRB2_CONFIG_HAVE_PNG} AND ${SRB2_CONFIG_HAVE_ZLIB}) target_compile_definitions(SRB2SDL2 PRIVATE -D_LARGEFILE64_SOURCE) target_sources(SRB2SDL2 PRIVATE apng.c) else() - message(WARNING "You have specified that PNG is available but it was not found. SRB2Kart may not compile correctly.") + message(WARNING "You have specified that PNG is available but it was not found. Dr Robotnik's Ring Racers may not compile correctly.") endif() endif() endif() @@ -482,6 +489,12 @@ target_compile_definitions(SRB2SDL2 PRIVATE -DCMAKECONFIG) if(SRB2_CONFIG_DEBUGMODE) target_compile_definitions(SRB2SDL2 PRIVATE -DZDEBUG -DPARANOIA -DRANGECHECK -DPACKETDROP) endif() +if(SRB2_CONFIG_TESTERS) + target_compile_definitions(SRB2SDL2 PRIVATE -DTESTERS) +endif() +if(SRB2_CONFIG_HOSTTESTERS) + target_compile_definitions(SRB2SDL2 PRIVATE -DHOSTTESTERS) +endif() if(SRB2_CONFIG_MOBJCONSISTANCY) target_compile_definitions(SRB2SDL2 PRIVATE -DMOBJCONSISTANCY) endif() diff --git a/src/command.c b/src/command.c index eb1204108..88d11105a 100644 --- a/src/command.c +++ b/src/command.c @@ -84,7 +84,7 @@ CV_PossibleValue_t CV_Natural[] = {{1, "MIN"}, {999999999, "MAX"}, {0, NULL}}; #else #define VALUE "Off" #endif -consvar_t cv_cheats = CVAR_INIT ("cheats", VALUE, CV_NETVAR|CV_CALL, CV_OnOff, CV_CheatsChanged); +consvar_t cv_cheats = CVAR_INIT ("cheats", VALUE, CV_NETVAR|CV_CALL|CV_NOINIT, CV_OnOff, CV_CheatsChanged); #undef VALUE // SRB2kart @@ -1481,7 +1481,7 @@ boolean CV_CompleteValue(consvar_t *var, const char **valstrp, INT32 *intval) { v = R_SkinAvailable(valstr); - if (!R_SkinUsable(-1, v)) + if (!R_SkinUsable(-1, v, false)) v = -1; goto finish; @@ -1966,13 +1966,13 @@ static void CV_SetCVar(consvar_t *var, const char *value, boolean stealth) return; } - if (var == &cv_kartencore && !M_SecretUnlocked(SECRET_ENCORE)) + if (var == &cv_kartencore && !M_SecretUnlocked(SECRET_ENCORE, false)) { CONS_Printf(M_GetText("You haven't unlocked Encore Mode yet!\n")); return; } - if (var == &cv_kartspeed && !M_SecretUnlocked(SECRET_HARDSPEED)) + if (var == &cv_kartspeed && !M_SecretUnlocked(SECRET_HARDSPEED, false)) { if (!stricmp(value, "Hard") || atoi(value) >= KARTSPEED_HARD) { @@ -1984,7 +1984,7 @@ static void CV_SetCVar(consvar_t *var, const char *value, boolean stealth) if (var == &cv_forceskin) { INT32 skin = R_SkinAvailable(value); - if ((stricmp(value, "None")) && ((skin == -1) || !R_SkinUsable(-1, skin))) + if ((stricmp(value, "None")) && ((skin == -1) || !R_SkinUsable(-1, skin, false))) { CONS_Printf("Please provide a valid skin name (\"None\" disables).\n"); return; @@ -2109,7 +2109,7 @@ void CV_AddValue(consvar_t *var, INT32 increment) else if (newvalue >= numskins) newvalue = -1; } while ((oldvalue != newvalue) - && !(R_SkinUsable(-1, newvalue))); + && !(R_SkinUsable(-1, newvalue, false))); } else newvalue = var->value + increment; @@ -2227,7 +2227,7 @@ void CV_AddValue(consvar_t *var, INT32 increment) || var->PossibleValue == dummykartspeed_cons_t || var->PossibleValue == gpdifficulty_cons_t) { - if (!M_SecretUnlocked(SECRET_HARDSPEED)) + if (!M_SecretUnlocked(SECRET_HARDSPEED, false)) { max = KARTSPEED_NORMAL+1; if (var->PossibleValue == kartspeed_cons_t) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 6ce85db27..27a489916 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -57,6 +57,7 @@ #include "k_boss.h" #include "doomstat.h" #include "s_sound.h" // sfx_syfail +#include "m_cond.h" // netUnlocked // cl loading screen #include "v_video.h" @@ -824,6 +825,8 @@ 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)); + return HSendPacket(servernode, false, 0, sizeof (clientconfig_pak)); } @@ -3461,6 +3464,11 @@ void SV_ResetServer(void) CV_RevertNetVars(); + // Copy our unlocks to a place where net material can grab at/overwrite them safely. + // (permits all unlocks in dedicated) + for (i = 0; i < MAXUNLOCKABLES; i++) + netUnlocked[i] = (dedicated || gamedata->unlocked[i]); + DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n"); } @@ -3565,8 +3573,8 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum) return; } - node = (UINT8)READUINT8(*p); - newplayernum = (UINT8)READUINT8(*p); + node = READUINT8(*p); + newplayernum = READUINT8(*p); CONS_Debug(DBG_NETPLAY, "addplayer: %d %d\n", node, newplayernum); @@ -3587,8 +3595,13 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum) READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME); - console = (UINT8)READUINT8(*p); - splitscreenplayer = (UINT8)READUINT8(*p); + console = READUINT8(*p); + splitscreenplayer = READUINT8(*p); + + for (i = 0; i < MAXAVAILABILITY; i++) + { + newplayer->availabilities[i] = READUINT8(*p); + } // the server is creating my player if (node == mynode) @@ -3690,7 +3703,7 @@ static void Got_RemovePlayer(UINT8 **p, INT32 playernum) static void Got_AddBot(UINT8 **p, INT32 playernum) { INT16 newplayernum; - UINT8 skinnum = 0; + UINT8 i, skinnum = 0; UINT8 difficulty = DIFFICULTBOT; if (playernum != serverplayer && !IsPlayerAdmin(playernum)) @@ -3704,9 +3717,9 @@ static void Got_AddBot(UINT8 **p, INT32 playernum) return; } - newplayernum = (UINT8)READUINT8(*p); - skinnum = (UINT8)READUINT8(*p); - difficulty = (UINT8)READUINT8(*p); + newplayernum = READUINT8(*p); + skinnum = READUINT8(*p); + difficulty = READUINT8(*p); CONS_Debug(DBG_NETPLAY, "addbot: %d\n", newplayernum); @@ -3720,6 +3733,15 @@ 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]; + } + } + players[newplayernum].splitscreenindex = 0; players[newplayernum].bot = true; players[newplayernum].botvars.difficulty = difficulty; @@ -3737,10 +3759,10 @@ static void Got_AddBot(UINT8 **p, INT32 playernum) LUA_HookInt(newplayernum, HOOK(PlayerJoin)); } -static boolean SV_AddWaitingPlayers(SINT8 node, const char *name, const char *name2, const char *name3, const char *name4) +static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities, const char *name, const char *name2, const char *name3, const char *name4) { - INT32 n, newplayernum; - UINT8 buf[4 + MAXPLAYERNAME]; + INT32 n, newplayernum, i; + UINT8 buf[4 + MAXPLAYERNAME + MAXAVAILABILITY]; UINT8 *buf_p = buf; boolean newplayer = false; @@ -3823,6 +3845,11 @@ static boolean SV_AddWaitingPlayers(SINT8 node, const char *name, const char *na WRITEUINT8(buf_p, nodetoplayer[node]); // consoleplayer WRITEUINT8(buf_p, playerpernode[node]); // splitscreen num + for (i = 0; i < MAXAVAILABILITY; i++) + { + WRITEUINT8(buf_p, availabilities[i]); + } + playerpernode[node]++; SendNetXCmd(XD_ADDPLAYER, buf, buf_p - buf); @@ -3886,9 +3913,10 @@ 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); SINT8 node = 0; for (; node < MAXNETNODES; node++) - result |= SV_AddWaitingPlayers(node, cv_playername[0].zstring, cv_playername[1].zstring, cv_playername[2].zstring, cv_playername[3].zstring); + result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, cv_playername[0].zstring, cv_playername[1].zstring, cv_playername[2].zstring, cv_playername[3].zstring); } return result; #endif @@ -3971,6 +3999,7 @@ static void HandleConnect(SINT8 node) { char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1]; INT32 i; + UINT8 availabilitiesbuffer[MAXAVAILABILITY]; // Sal: Dedicated mode is INCREDIBLY hacked together. // If a server filled out, then it'd overwrite the host and turn everyone into weird husks..... @@ -4077,6 +4106,8 @@ static void HandleConnect(SINT8 node) } } + memcpy(availabilitiesbuffer, netbuffer->u.clientcfg.availabilities, sizeof(availabilitiesbuffer)); + // client authorised to join nodewaiting[node] = (UINT8)(netbuffer->u.clientcfg.localplayers - playerpernode[node]); if (!nodeingame[node]) @@ -4111,7 +4142,8 @@ static void HandleConnect(SINT8 node) SV_SendSaveGame(node, false); // send a complete game state DEBFILE("send savegame\n"); } - SV_AddWaitingPlayers(node, names[0], names[1], names[2], names[3]); + + SV_AddWaitingPlayers(node, availabilitiesbuffer, names[0], names[1], names[2], names[3]); joindelay += cv_joindelay.value * TICRATE; player_joining = true; } diff --git a/src/d_clisrv.h b/src/d_clisrv.h index 42393da1b..e8763f681 100644 --- a/src/d_clisrv.h +++ b/src/d_clisrv.h @@ -246,6 +246,7 @@ struct clientconfig_pak UINT8 localplayers; // number of splitscreen players UINT8 mode; char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME]; + UINT8 availabilities[MAXAVAILABILITY]; } ATTRPACK; #define SV_SPEEDMASK 0x03 // used to send kartspeed diff --git a/src/d_main.c b/src/d_main.c index d668aadae..8ba822e21 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -953,11 +953,6 @@ void D_StartTitle(void) for (i = 0; i < MAXPLAYERS; i++) CL_ClearPlayer(i); - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - players[g_localplayers[i]].availabilities = R_GetSkinAvailabilities(); - } - splitscreen = 0; SplitScreen_OnChange(); @@ -1151,6 +1146,10 @@ static void IdentifyVersion(void) #ifdef USE_PATCH_FILE D_AddFile(startupiwads, va(pandf,srb2waddir,PATCHNAME)); #endif +#define UNLOCKTESTING +#if defined(DEVELOP) && defined(UNLOCKTESTING) + D_AddFile(startupiwads, va(pandf,srb2waddir,"unlocks.pk3")); +#endif //// #undef TEXTURESNAME #undef MAPSNAME @@ -1385,6 +1384,8 @@ void D_SRB2Main(void) Z_Init(); CON_SetLoadingProgress(LOADED_ZINIT); + M_NewGameDataStruct(); + // Do this up here so that WADs loaded through the command line can use ExecCfg COM_Init(); @@ -1456,6 +1457,9 @@ void D_SRB2Main(void) #ifdef USE_PATCH_FILE mainwads++; // scripts.pk3 #endif +#ifdef UNLOCKTESTING + mainwads++; // unlocks.pk3 +#endif #endif //ifndef DEVELOP diff --git a/src/d_netcmd.c b/src/d_netcmd.c index ab4a070a1..9679b5631 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -383,12 +383,14 @@ consvar_t cv_items[NUMKARTRESULTS-1] = { CVAR_INIT ("kitchensink", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("droptarget", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("gardentop", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("gachabom", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("dualsneaker", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("triplesneaker", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("triplebanana", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("tripleorbinaut", "On", CV_NETVAR, CV_OnOff, NULL), CVAR_INIT ("quadorbinaut", "On", CV_NETVAR, CV_OnOff, NULL), - CVAR_INIT ("dualjawz", "On", CV_NETVAR, CV_OnOff, NULL) + CVAR_INIT ("dualjawz", "On", CV_NETVAR, CV_OnOff, NULL), + CVAR_INIT ("triplegachabom", "On", CV_NETVAR, CV_OnOff, NULL) }; consvar_t cv_kartspeed = CVAR_INIT ("gamespeed", "Auto", CV_NETVAR|CV_CALL|CV_NOINIT, kartspeed_cons_t, KartSpeed_OnChange); @@ -467,6 +469,7 @@ consvar_t cv_autobalance = CVAR_INIT ("autobalance", "Off", CV_SAVE|CV_NETVAR|CV consvar_t cv_teamscramble = CVAR_INIT ("teamscramble", "Off", CV_SAVE|CV_NETVAR|CV_CALL|CV_NOINIT, teamscramble_cons_t, TeamScramble_OnChange); consvar_t cv_scrambleonchange = CVAR_INIT ("scrambleonchange", "Off", CV_SAVE|CV_NETVAR, teamscramble_cons_t, NULL); +consvar_t cv_alttitle = CVAR_INIT ("alttitle", "Off", CV_CALL|CV_NOSHOWHELP|CV_NOINIT|CV_SAVE, CV_OnOff, AltTitle_OnChange); consvar_t cv_itemfinder = CVAR_INIT ("itemfinder", "Off", CV_CALL|CV_NOSHOWHELP, CV_OnOff, ItemFinder_OnChange); // Scoring type options @@ -939,6 +942,7 @@ void D_RegisterClientCommands(void) CV_RegisterVar(&cv_mindelay); // HUD + CV_RegisterVar(&cv_alttitle); CV_RegisterVar(&cv_itemfinder); CV_RegisterVar(&cv_showinputjoy); @@ -1408,26 +1412,29 @@ boolean CanChangeSkinWhilePlaying(INT32 playernum) static void ForceAllSkins(INT32 forcedskin) { - INT32 i, j; + INT32 i; + + if (demo.playback) + return; // DXD_SKIN should handle all changes for us + for (i = 0; i < MAXPLAYERS; ++i) { if (!playeringame[i]) continue; SetPlayerSkinByNum(i, forcedskin); + } - // If it's me (or my brother (or my sister (or my trusty pet dog))), set appropriate skin value in cv_skin - if (dedicated) // But don't do this for dedicated servers, of course. + if (dedicated) + return; + + // If it's me (or my brother (or my sister (or my trusty pet dog))), set appropriate skin value in cv_skin + for (i = 0; i <= splitscreen; i++) + { + if (!playeringame[g_localplayers[i]]) continue; - for (j = 0; j <= splitscreen; j++) - { - if (i == g_localplayers[j]) - { - CV_StealthSet(&cv_skin[j], skins[forcedskin].name); - break; - } - } + CV_StealthSet(&cv_skin[i], skins[forcedskin].name); } } @@ -1478,6 +1485,8 @@ static void SendNameAndColor(UINT8 n) char buf[MAXPLAYERNAME+12]; char *p; + UINT16 i = 0; + if (splitscreen < n) return; // can happen if skin4/color4/name4 changed @@ -1495,9 +1504,10 @@ static void SendNameAndColor(UINT8 n) CV_StealthSetValue(&cv_playercolor[n], skins[player->skin].prefcolor); else if (skincolors[atoi(cv_playercolor[n].defaultvalue)].accessible) CV_StealthSet(&cv_playercolor[n], cv_playercolor[n].defaultvalue); - else { - UINT16 i = 0; - while (ifollowercolor) return; - player->availabilities = R_GetSkinAvailabilities(); - // We'll handle it later if we're not playing. if (!Playing()) return; @@ -1532,7 +1540,8 @@ static void SendNameAndColor(UINT8 n) K_KartResetPlayerColor(player); - if ((foundskin = R_SkinAvailable(cv_skin[n].string)) != -1 && R_SkinUsable(playernum, foundskin)) + foundskin = R_SkinAvailable(cv_skin[n].string); + if (foundskin != -1 && R_SkinUsable(playernum, foundskin, false)) { SetPlayerSkin(playernum, cv_skin[n].string); CV_StealthSet(&cv_skin[n], skins[foundskin].name); @@ -1550,6 +1559,8 @@ static void SendNameAndColor(UINT8 n) // Update follower for local games: foundskin = K_FollowerAvailable(cv_follower[n].string); + if (!K_FollowerUsable(foundskin)) + foundskin = -1; CV_StealthSet(&cv_follower[n], (foundskin == -1) ? "None" : followers[foundskin].name); cv_follower[n].value = foundskin; K_SetFollowerByNum(playernum, foundskin); @@ -1577,14 +1588,14 @@ static void SendNameAndColor(UINT8 n) // check if player has the skin loaded (cv_skin may have // the name of a skin that was available in the previous game) cv_skin[n].value = R_SkinAvailable(cv_skin[n].string); - if ((cv_skin[n].value < 0) || !R_SkinUsable(playernum, cv_skin[n].value)) + if ((cv_skin[n].value < 0) || !R_SkinUsable(playernum, cv_skin[n].value, false)) { CV_StealthSet(&cv_skin[n], DEFAULTSKIN); cv_skin[n].value = 0; } cv_follower[n].value = K_FollowerAvailable(cv_follower[n].string); - if (cv_follower[n].value < 0) + if (cv_follower[n].value < 0 || !K_FollowerUsable(cv_follower[n].value)) { CV_StealthSet(&cv_follower[n], "None"); cv_follower[n].value = -1; @@ -1592,7 +1603,6 @@ static void SendNameAndColor(UINT8 n) // Finally write out the complete packet and send it off. WRITESTRINGN(p, cv_playername[n].zstring, MAXPLAYERNAME); - WRITEUINT32(p, (UINT32)player->availabilities); WRITEUINT16(p, (UINT16)cv_playercolor[n].value); WRITEUINT8(p, (UINT8)cv_skin[n].value); WRITEINT16(p, (INT16)cv_follower[n].value); @@ -1634,11 +1644,9 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum) } READSTRINGN(*cp, name, MAXPLAYERNAME); - p->availabilities = READUINT32(*cp); color = READUINT16(*cp); skin = READUINT8(*cp); follower = READINT16(*cp); - //CONS_Printf("Recieved follower id %d\n", follower); followercolor = READUINT16(*cp); // set name @@ -1659,56 +1667,20 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum) { boolean kick = false; - // team colors - if (G_GametypeHasTeams()) - { - if (p->ctfteam == 1 && p->skincolor != skincolor_redteam) - kick = true; - else if (p->ctfteam == 2 && p->skincolor != skincolor_blueteam) - kick = true; - } - // don't allow inaccessible colors if (skincolors[p->skincolor].accessible == false) kick = true; - // availabilities - for (i = 0; i < MAXSKINS; i++) - { - UINT32 playerhasunlocked = (p->availabilities & (1 << i)); - boolean islocked = false; - UINT8 j; - - for (j = 0; j < MAXUNLOCKABLES; j++) - { - if (unlockables[j].type != SECRET_SKIN) - continue; - - if (unlockables[j].variable == i) - { - islocked = true; - break; - } - } - - if (islocked == false && playerhasunlocked == true) - { - // hacked client that enabled every bit - kick = true; - break; - } - } - if (kick) { - CONS_Alert(CONS_WARNING, M_GetText("Illegal color change received from %s (team: %d), color: %d)\n"), player_names[playernum], p->ctfteam, p->skincolor); + CONS_Alert(CONS_WARNING, M_GetText("Illegal color change received from %s, color: %d)\n"), player_names[playernum], p->skincolor); SendKick(playernum, KICK_MSG_CON_FAIL); return; } } // set skin - if (cv_forceskin.value >= 0 && (netgame || multiplayer)) // Server wants everyone to use the same player + if (cv_forceskin.value >= 0 && K_CanChangeRules(true)) // Server wants everyone to use the same player { const INT32 forcedskin = cv_forceskin.value; SetPlayerSkinByNum(playernum, forcedskin); @@ -2890,7 +2862,7 @@ static void Command_Map_f(void) { newencoremode = !newencoremode; - if (!M_SecretUnlocked(SECRET_ENCORE) && newencoremode == true && !usingcheats) + if (!M_SecretUnlocked(SECRET_ENCORE, false) && newencoremode == true && !usingcheats) { CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n")); return; @@ -4877,12 +4849,25 @@ FUNCNORETURN static ATTRNORETURN void Command_Quit_f(void) I_Quit(); } +void AltTitle_OnChange(void) +{ + if (!cv_alttitle.value) + return; // it's fine. + + if (!M_SecretUnlocked(SECRET_ALTTITLE, true)) + { + CONS_Printf(M_GetText("You haven't earned this yet.\n")); + CV_StealthSetValue(&cv_itemfinder, 0); + return; + } +} + void ItemFinder_OnChange(void) { if (!cv_itemfinder.value) return; // it's fine. - if (!M_SecretUnlocked(SECRET_ITEMFINDER)) + if (!M_SecretUnlocked(SECRET_ITEMFINDER, true)) { CONS_Printf(M_GetText("You haven't earned this yet.\n")); CV_StealthSetValue(&cv_itemfinder, 0); @@ -5732,11 +5717,6 @@ void Command_ExitGame_f(void) for (i = 0; i < MAXPLAYERS; i++) CL_ClearPlayer(i); - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - players[g_localplayers[i]].availabilities = R_GetSkinAvailabilities(); - } - splitscreen = 0; SplitScreen_OnChange(); diff --git a/src/d_netcmd.h b/src/d_netcmd.h index e0b969ffd..ee1fb6ec7 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -94,7 +94,7 @@ extern consvar_t cv_kartdebugnodes, cv_kartdebugcolorize, cv_kartdebugdirector; extern consvar_t cv_spbtest, cv_gptest, cv_reducevfx; extern consvar_t cv_kartdebugwaypoints, cv_kartdebugbotpredict; -extern consvar_t cv_itemfinder; +extern consvar_t cv_alttitle, cv_itemfinder; extern consvar_t cv_inttime, cv_advancemap; extern consvar_t cv_overtime; @@ -238,6 +238,7 @@ boolean IsPlayerAdmin(INT32 playernum); void SetAdminPlayer(INT32 playernum); void ClearAdminPlayers(void); void RemoveAdminPlayer(INT32 playernum); +void AltTitle_OnChange(void); void ItemFinder_OnChange(void); void D_SetPassword(const char *pw); diff --git a/src/d_player.h b/src/d_player.h index 81a80d641..265bd13cb 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -154,7 +154,8 @@ Run this macro, then #undef FOREACH afterward FOREACH (SUPERRING, 19),\ FOREACH (KITCHENSINK, 20),\ FOREACH (DROPTARGET, 21),\ - FOREACH (GARDENTOP, 22) + FOREACH (GARDENTOP, 22),\ + FOREACH (GACHABOM, 23) typedef enum { @@ -171,6 +172,7 @@ typedef enum KRITEM_TRIPLEORBINAUT, KRITEM_QUADORBINAUT, KRITEM_DUALJAWZ, + KRITEM_TRIPLEGACHABOM, NUMKARTRESULTS } kartitems_t; @@ -421,7 +423,7 @@ struct player_t UINT16 skincolor; INT32 skin; - UINT32 availabilities; + UINT8 availabilities[MAXAVAILABILITY]; UINT8 fakeskin; // ironman UINT8 lastfakeskin; @@ -464,6 +466,7 @@ struct player_t UINT16 spinouttimer; // Spin-out from a banana peel or oil slick (was "pw_bananacam") UINT8 spinouttype; // Determines the mode of spinout/wipeout, see kartspinoutflags_t UINT8 instashield; // Instashield no-damage animation timer + INT32 invulnhitlag; // Numbers of tics of hitlag added this tic for "potential" damage -- not real damage UINT8 wipeoutslow; // Timer before you slowdown when getting wiped out UINT8 justbumped; // Prevent players from endlessly bumping into each other UINT8 tumbleBounces; @@ -613,7 +616,12 @@ struct player_t INT16 lastsidehit, lastlinehit; - //UINT8 timeshit; // That's TIMES HIT, not TIME SHIT, you doofus! -- in memoriam + // These track how many things tried to damage you, not + // whether you actually took damage. + UINT8 timeshit; // times hit this tic + UINT8 timeshitprev; // times hit before + // That's TIMES HIT, not TIME SHIT, you doofus! -- in memoriam + // No longer in memoriam =P -jart INT32 onconveyor; // You are on a conveyor belt if nonzero diff --git a/src/deh_soc.c b/src/deh_soc.c index 9f9224ae2..1d7b94214 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -131,11 +131,44 @@ static float searchfvalue(const char *s) #endif // These are for clearing all of various things +void clear_emblems(void) +{ + INT32 i; + + for (i = 0; i < MAXEMBLEMS; ++i) + { + Z_Free(emblemlocations[i].level); + emblemlocations[i].level = NULL; + + Z_Free(emblemlocations[i].stringVar); + emblemlocations[i].stringVar = NULL; + } + + memset(&emblemlocations, 0, sizeof(emblemlocations)); + numemblems = 0; +} + +void clear_unlockables(void) +{ + INT32 i; + + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + Z_Free(unlockables[i].stringVar); + unlockables[i].stringVar = NULL; + + Z_Free(unlockables[i].icon); + unlockables[i].icon = NULL; + } + + memset(&unlockables, 0, sizeof(unlockables)); +} + void clear_conditionsets(void) { UINT8 i; for (i = 0; i < MAXCONDITIONSETS; ++i) - M_ClearConditionSet(i+1); + M_ClearConditionSet(i); } void clear_levels(void) @@ -1148,13 +1181,6 @@ void readlevelheader(MYFILE *f, char * name) mapheaderinfo[num]->encorepal = (UINT16)i; else if (fastcmp(word, "NUMLAPS")) mapheaderinfo[num]->numlaps = (UINT8)i; - else if (fastcmp(word, "UNLOCKABLE")) - { - if (i >= 0 && i <= MAXUNLOCKABLES) // 0 for no unlock required, anything else requires something - mapheaderinfo[num]->unlockrequired = (SINT8)i - 1; - else - deh_warning("Level header %d: invalid unlockable number %d", num, i); - } else if (fastcmp(word, "SKYBOXSCALE")) mapheaderinfo[num]->skybox_scalex = mapheaderinfo[num]->skybox_scaley = mapheaderinfo[num]->skybox_scalez = (INT16)i; else if (fastcmp(word, "SKYBOXSCALEX")) @@ -2098,14 +2124,6 @@ void reademblemdata(MYFILE *f, INT32 num) word2 = tmp += 2; value = atoi(word2); // used for numerical settings - // Up here to allow lowercase in hints - if (fastcmp(word, "HINT")) - { - while ((tmp = strchr(word2, '\\'))) - *tmp = '\n'; - deh_strlcpy(emblemlocations[num-1].hint, word2, sizeof (emblemlocations[num-1].hint), va("Emblem %d: hint", num)); - continue; - } strupr(word2); if (fastcmp(word, "TYPE")) @@ -2124,6 +2142,7 @@ void reademblemdata(MYFILE *f, INT32 num) else if (fastcmp(word, "MAPNAME")) { emblemlocations[num-1].level = Z_StrDup(word2); + emblemlocations[num-1].levelCache = NEXTMAP_INVALID; } else if (fastcmp(word, "SPRITE")) { @@ -2140,7 +2159,14 @@ void reademblemdata(MYFILE *f, INT32 num) else if (fastcmp(word, "COLOR")) emblemlocations[num-1].color = get_number(word2); else if (fastcmp(word, "VAR")) + { + Z_Free(emblemlocations[num-1].stringVar); + emblemlocations[num-1].stringVar = Z_StrDup(word2); + emblemlocations[num-1].var = get_number(word2); + } + else if (fastcmp(word, "FLAGS")) + emblemlocations[num-1].flags = get_number(word2); else deh_warning("Emblem %d: unknown word '%s'", num, word); } @@ -2165,88 +2191,6 @@ void reademblemdata(MYFILE *f, INT32 num) Z_Free(s); } -void readextraemblemdata(MYFILE *f, INT32 num) -{ - char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL); - char *word = s; - char *word2; - char *tmp; - INT32 value; - - memset(&extraemblems[num-1], 0, sizeof(extraemblem_t)); - - do - { - if (myfgets(s, MAXLINELEN, f)) - { - if (s[0] == '\n') - break; - - // First remove trailing newline, if there is one - tmp = strchr(s, '\n'); - if (tmp) - *tmp = '\0'; - - tmp = strchr(s, '#'); - if (tmp) - *tmp = '\0'; - if (s == tmp) - continue; // Skip comment lines, but don't break. - - // Get the part before the " = " - tmp = strchr(s, '='); - if (tmp) - *(tmp-1) = '\0'; - else - break; - strupr(word); - - // Now get the part after - word2 = tmp += 2; - - value = atoi(word2); // used for numerical settings - - if (fastcmp(word, "NAME")) - deh_strlcpy(extraemblems[num-1].name, word2, - sizeof (extraemblems[num-1].name), va("Extra emblem %d: name", num)); - else if (fastcmp(word, "OBJECTIVE")) - deh_strlcpy(extraemblems[num-1].description, word2, - sizeof (extraemblems[num-1].description), va("Extra emblem %d: objective", num)); - else if (fastcmp(word, "CONDITIONSET")) - extraemblems[num-1].conditionset = (UINT8)value; - else if (fastcmp(word, "SHOWCONDITIONSET")) - extraemblems[num-1].showconditionset = (UINT8)value; - else - { - strupr(word2); - if (fastcmp(word, "SPRITE")) - { - if (word2[0] >= 'A' && word2[0] <= 'Z') - value = word2[0]; - else - value += 'A'-1; - - if (value < 'A' || value > 'Z') - deh_warning("Emblem %d: sprite must be from A - Z (1 - 26)", num); - else - extraemblems[num-1].sprite = (UINT8)value; - } - else if (fastcmp(word, "COLOR")) - extraemblems[num-1].color = get_number(word2); - else - deh_warning("Extra emblem %d: unknown word '%s'", num, word); - } - } - } while (!myfeof(f)); - - if (!extraemblems[num-1].sprite) - extraemblems[num-1].sprite = 'C'; - if (!extraemblems[num-1].color) - extraemblems[num-1].color = SKINCOLOR_RED; - - Z_Free(s); -} - void readunlockable(MYFILE *f, INT32 num) { char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL); @@ -2256,7 +2200,6 @@ void readunlockable(MYFILE *f, INT32 num) INT32 i; memset(&unlockables[num], 0, sizeof(unlockable_t)); - unlockables[num].objective[0] = '/'; do { @@ -2291,62 +2234,65 @@ void readunlockable(MYFILE *f, INT32 num) if (fastcmp(word, "NAME")) deh_strlcpy(unlockables[num].name, word2, - sizeof (unlockables[num].name), va("Unlockable %d: name", num)); - else if (fastcmp(word, "OBJECTIVE")) - deh_strlcpy(unlockables[num].objective, word2, - sizeof (unlockables[num].objective), va("Unlockable %d: objective", num)); + sizeof (unlockables[num].name), va("Unlockable %d: name", num+1)); else { strupr(word2); if (fastcmp(word, "CONDITIONSET")) unlockables[num].conditionset = (UINT8)i; - else if (fastcmp(word, "SHOWCONDITIONSET")) - unlockables[num].showconditionset = (UINT8)i; - else if (fastcmp(word, "NOCECHO")) - unlockables[num].nocecho = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y'); - else if (fastcmp(word, "NOCHECKLIST")) - unlockables[num].nochecklist = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y'); + else if (fastcmp(word, "MAJORUNLOCK")) + unlockables[num].majorunlock = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y'); else if (fastcmp(word, "TYPE")) { if (fastcmp(word2, "NONE")) unlockables[num].type = SECRET_NONE; - else if (fastcmp(word2, "HEADER")) - unlockables[num].type = SECRET_HEADER; + else if (fastcmp(word2, "EXTRAMEDAL")) + unlockables[num].type = SECRET_EXTRAMEDAL; + else if (fastcmp(word2, "CUP")) + unlockables[num].type = SECRET_CUP; + else if (fastcmp(word2, "MAP")) + unlockables[num].type = SECRET_MAP; else if (fastcmp(word2, "SKIN")) unlockables[num].type = SECRET_SKIN; - else if (fastcmp(word2, "WARP")) - unlockables[num].type = SECRET_WARP; - else if (fastcmp(word2, "LEVELSELECT")) - unlockables[num].type = SECRET_LEVELSELECT; + else if (fastcmp(word2, "FOLLOWER")) + unlockables[num].type = SECRET_FOLLOWER; + else if (fastcmp(word2, "HARDSPEED")) + unlockables[num].type = SECRET_HARDSPEED; + 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, "SOUNDTEST")) unlockables[num].type = SECRET_SOUNDTEST; - else if (fastcmp(word2, "CREDITS")) - unlockables[num].type = SECRET_CREDITS; + else if (fastcmp(word2, "ALTTITLE")) + unlockables[num].type = SECRET_ALTTITLE; else if (fastcmp(word2, "ITEMFINDER")) unlockables[num].type = SECRET_ITEMFINDER; - else if (fastcmp(word2, "EMBLEMHINTS")) - unlockables[num].type = SECRET_EMBLEMHINTS; - else if (fastcmp(word2, "ENCORE")) - unlockables[num].type = SECRET_ENCORE; - else if (fastcmp(word2, "HARDSPEED")) - unlockables[num].type = SECRET_HARDSPEED; - else if (fastcmp(word2, "HELLATTACK")) - unlockables[num].type = SECRET_HELLATTACK; - else if (fastcmp(word2, "PANDORA")) - unlockables[num].type = SECRET_PANDORA; else unlockables[num].type = (INT16)i; + unlockables[num].stringVarCache = -1; } else if (fastcmp(word, "VAR")) { - // TODO: different field for level name string + Z_Free(unlockables[num].stringVar); + unlockables[num].stringVar = Z_StrDup(word2); + unlockables[num].stringVarCache = -1; unlockables[num].variable = (INT16)i; } + else if (fastcmp(word, "ICON")) + { + Z_Free(unlockables[num].icon); + unlockables[num].icon = Z_StrDup(word2); + } + else if (fastcmp(word, "COLOR")) + { + unlockables[num].color = get_number(word2); + } else deh_warning("Unlockable %d: unknown word '%s'", num+1, word); } @@ -2383,7 +2329,7 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) if (!params[0]) { - deh_warning("condition line is empty for condition ID %d", id); + deh_warning("condition line is empty for condition ID %d", id+1); return; } @@ -2401,9 +2347,15 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) re = atoi(params[1]); x1 = atoi(params[2]); + if (re < PWRLVRECORD_MIN || re > PWRLVRECORD_MAX) + { + deh_warning("Power level requirement %d out of range (%d - %d) for condition ID %d", re, PWRLVRECORD_MIN, PWRLVRECORD_MAX, id+1); + return; + } + if (x1 < 0 || x1 >= PWRLV_NUMTYPES) { - deh_warning("Power level type %d out of range (0 - %d) for condition ID %d", x1, PWRLV_NUMTYPES-1, id); + deh_warning("Power level type %d out of range (0 - %d) for condition ID %d", x1, PWRLV_NUMTYPES-1, id+1); return; } } @@ -2427,7 +2379,7 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) if (re >= nummapheaders) { - deh_warning("Invalid level %s for condition ID %d", params[1], id); + deh_warning("Invalid level %s for condition ID %d", params[1], id+1); return; } } @@ -2440,7 +2392,7 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) if (x1 >= nummapheaders) { - deh_warning("Invalid level %s for condition ID %d", params[1], id); + deh_warning("Invalid level %s for condition ID %d", params[1], id+1); return; } } @@ -2453,14 +2405,14 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) // 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); + deh_warning("Trigger ID %d out of range (0 - 31) for condition ID %d", re, id+1); return; } } - else if (fastcmp(params[0], "TOTALEMBLEMS")) + else if (fastcmp(params[0], "TOTALMEDALS")) { PARAMCHECK(1); - ty = UC_TOTALEMBLEMS; + ty = UC_TOTALMEDALS; re = atoi(params[1]); } else if (fastcmp(params[0], "EMBLEM")) @@ -2471,19 +2423,19 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) if (re <= 0 || re > MAXEMBLEMS) { - deh_warning("Emblem %d out of range (1 - %d) for condition ID %d", re, MAXEMBLEMS, id); + deh_warning("Emblem %d out of range (1 - %d) for condition ID %d", re, MAXEMBLEMS, id+1); return; } } - else if (fastcmp(params[0], "EXTRAEMBLEM")) + else if (fastcmp(params[0], "UNLOCKABLE")) { PARAMCHECK(1); - ty = UC_EXTRAEMBLEM; + ty = UC_UNLOCKABLE; re = atoi(params[1]); - if (re <= 0 || re > MAXEXTRAEMBLEMS) + if (re <= 0 || re > MAXUNLOCKABLES) { - deh_warning("Extra emblem %d out of range (1 - %d) for condition ID %d", re, MAXEXTRAEMBLEMS, id); + deh_warning("Unlockable %d out of range (1 - %d) for condition ID %d", re, MAXUNLOCKABLES, id+1); return; } } @@ -2495,13 +2447,13 @@ static void readcondition(UINT8 set, UINT32 id, char *word2) if (re <= 0 || re > MAXCONDITIONSETS) { - deh_warning("Condition set %d out of range (1 - %d) for condition ID %d", re, MAXCONDITIONSETS, id); + deh_warning("Condition set %d out of range (1 - %d) for condition ID %d", re, MAXCONDITIONSETS, id+1); return; } } else { - deh_warning("Invalid condition name %s for condition ID %d", params[0], id); + deh_warning("Invalid condition name %s for condition ID %d", params[0], id+1); return; } @@ -2554,13 +2506,13 @@ void readconditionset(MYFILE *f, UINT8 setnum) id = (UINT8)atoi(word + 9); if (id == 0) { - deh_warning("Condition set %d: unknown word '%s'", setnum, word); + deh_warning("Condition set %d: unknown word '%s'", setnum+1, word); continue; } else if (previd > id) { // out of order conditions can cause problems, so enforce proper order - deh_warning("Condition set %d: conditions are out of order, ignoring this line", setnum); + deh_warning("Condition set %d: conditions are out of order, ignoring this line", setnum+1); continue; } previd = id; @@ -2575,13 +2527,14 @@ void readconditionset(MYFILE *f, UINT8 setnum) Z_Free(s); } -void readmaincfg(MYFILE *f) +void readmaincfg(MYFILE *f, boolean mainfile) { char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL); char *word = s; char *word2; char *tmp; INT32 value; + boolean doClearLevels = false; do { @@ -2615,7 +2568,54 @@ void readmaincfg(MYFILE *f) value = atoi(word2); // used for numerical settings - if (fastcmp(word, "EXECCFG")) + if (fastcmp(word, "GAMEDATA")) + { + size_t filenamelen; + + // Check the data filename so that mods + // can't write arbitrary files. + if (!GoodDataFileName(word2)) + I_Error("Maincfg: bad data file name '%s'\n", word2); + + G_SaveGameData(); + strlcpy(gamedatafilename, word2, sizeof (gamedatafilename)); + strlwr(gamedatafilename); + savemoddata = true; + majormods = false; + + // Also save a time attack folder + filenamelen = strlen(gamedatafilename)-4; // Strip off the extension + filenamelen = min(filenamelen, sizeof (timeattackfolder)); + strncpy(timeattackfolder, gamedatafilename, filenamelen); + timeattackfolder[min(filenamelen, sizeof (timeattackfolder) - 1)] = '\0'; + + strcpy(savegamename, timeattackfolder); + strlcat(savegamename, "%u.ssg", sizeof(savegamename)); + // can't use sprintf since there is %u in savegamename + strcatbf(savegamename, srb2home, PATHSEP); + + strcpy(liveeventbackup, va("live%s.bkp", timeattackfolder)); + strcatbf(liveeventbackup, srb2home, PATHSEP); + + refreshdirmenu |= REFRESHDIR_GAMEDATA; + gamedataadded = true; + titlechanged = true; + + clear_unlockables(); + clear_conditionsets(); + clear_emblems(); + //clear_levels(); + doClearLevels = true; + } + else if (!mainfile && !gamedataadded) + { + deh_warning("You must define a custom gamedata to use \"%s\"", word); + } + else if (fastcmp(word, "CLEARLEVELS")) + { + doClearLevels = (UINT8)(value == 0 || word2[0] == 'F' || word2[0] == 'N'); + } + else if (fastcmp(word, "EXECCFG")) { if (strchr(word2, '.')) COM_BufAddText(va("exec %s\n", word2)); @@ -2800,40 +2800,6 @@ void readmaincfg(MYFILE *f) { maxXtraLife = (UINT8)get_number(word2); } - - else if (fastcmp(word, "GAMEDATA")) - { - size_t filenamelen; - - // Check the data filename so that mods - // can't write arbitrary files. - if (!GoodDataFileName(word2)) - I_Error("Maincfg: bad data file name '%s'\n", word2); - - G_SaveGameData(); - strlcpy(gamedatafilename, word2, sizeof (gamedatafilename)); - strlwr(gamedatafilename); - savemoddata = true; - majormods = false; - - // Also save a time attack folder - filenamelen = strlen(gamedatafilename)-4; // Strip off the extension - filenamelen = min(filenamelen, sizeof (timeattackfolder)); - strncpy(timeattackfolder, gamedatafilename, filenamelen); - timeattackfolder[min(filenamelen, sizeof (timeattackfolder) - 1)] = '\0'; - - strcpy(savegamename, timeattackfolder); - strlcat(savegamename, "%u.ssg", sizeof(savegamename)); - // can't use sprintf since there is %u in savegamename - strcatbf(savegamename, srb2home, PATHSEP); - - strcpy(liveeventbackup, va("live%s.bkp", timeattackfolder)); - strcatbf(liveeventbackup, srb2home, PATHSEP); - - refreshdirmenu |= REFRESHDIR_GAMEDATA; - gamedataadded = true; - titlechanged = true; - } else if (fastcmp(word, "RESETDATA")) { P_ResetData(value); @@ -2858,6 +2824,11 @@ void readmaincfg(MYFILE *f) } } while (!myfeof(f)); + if (doClearLevels) + { + clear_levels(); + } + Z_Free(s); } @@ -3154,13 +3125,6 @@ void readcupheader(MYFILE *f, cupheader_t *cup) else deh_warning("%s Cup: invalid emerald number %d", cup->name, i); } - else if (fastcmp(word, "UNLOCKABLE")) - { - if (i >= 0 && i <= MAXUNLOCKABLES) // 0 for no unlock required, anything else requires something - cup->unlockrequired = (SINT8)i - 1; - else - deh_warning("%s Cup: invalid unlockable number %d", cup->name, i); - } else deh_warning("%s Cup: unknown word '%s'", cup->name, word); } diff --git a/src/deh_soc.h b/src/deh_soc.h index 0d9beccab..6aed7e7f2 100644 --- a/src/deh_soc.h +++ b/src/deh_soc.h @@ -57,10 +57,9 @@ sfxenum_t get_sfx(const char *word); skincolornum_t get_skincolor(const char *word); void readwipes(MYFILE *f); -void readmaincfg(MYFILE *f); +void readmaincfg(MYFILE *f, boolean mainfile); void readconditionset(MYFILE *f, UINT8 setnum); void readunlockable(MYFILE *f, INT32 num); -void readextraemblemdata(MYFILE *f, INT32 num); void reademblemdata(MYFILE *f, INT32 num); void readsound(MYFILE *f, INT32 num); void readframe(MYFILE *f, INT32 num); @@ -77,6 +76,8 @@ void readlight(MYFILE *f, INT32 num); void readskincolor(MYFILE *f, INT32 num); void readthing(MYFILE *f, INT32 num); void readfreeslots(MYFILE *f); +void clear_emblems(void); +void clear_unlockables(void); void clear_levels(void); void clear_conditionsets(void); diff --git a/src/deh_tables.c b/src/deh_tables.c index d856a33c3..8e7bee9c1 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -5384,6 +5384,8 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_SINK_SHIELD", "MT_SINKTRAIL", + "MT_GACHABOM", + "MT_DUELBOMB", // Duel mode bombs "MT_BATTLEBUMPER", // Battle Mode bumper @@ -6356,7 +6358,8 @@ struct int_const_s const INT_CONST[] = { {"SF_X2AWAYSOUND",SF_X2AWAYSOUND}, // Global emblem var flags - // none in kart yet + {"GE_NOTMEDAL", GE_NOTMEDAL}, + {"GE_TIMED", GE_TIMED}, // Map emblem var flags {"ME_ENCORE",ME_ENCORE}, @@ -6794,6 +6797,7 @@ struct int_const_s const INT_CONST[] = { {"KRITEM_TRIPLEORBINAUT",KRITEM_TRIPLEORBINAUT}, {"KRITEM_QUADORBINAUT",KRITEM_QUADORBINAUT}, {"KRITEM_DUALJAWZ",KRITEM_DUALJAWZ}, + {"KRITEM_TRIPLEGACHABOM",KRITEM_TRIPLEGACHABOM}, {"NUMKARTRESULTS",NUMKARTRESULTS}, // kartshields_t diff --git a/src/dehacked.c b/src/dehacked.c index 99ee7b0e8..7a916c950 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -217,7 +217,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) else if (fastcmp(word, "MAINCFG")) { G_SetGameModified(multiplayer, true); - readmaincfg(f); + readmaincfg(f, mainfile); continue; } else if (fastcmp(word, "WIPES")) @@ -278,32 +278,6 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) } continue; } - else if (fastcmp(word, "EXTRAEMBLEM")) - { - if (!mainfile && !gamedataadded) - { - deh_warning("You must define a custom gamedata to use \"%s\"", word); - ignorelines(f); - } - else - { - if (!word2) - i = numextraemblems + 1; - - if (i > 0 && i <= MAXEXTRAEMBLEMS) - { - if (numextraemblems < i) - numextraemblems = i; - readextraemblemdata(f, i); - } - else - { - deh_warning("Extra emblem number %d out of range (1 - %d)", i, MAXEXTRAEMBLEMS); - ignorelines(f); - } - } - continue; - } if (word2) { if (fastcmp(word, "THING") || fastcmp(word, "MOBJ") || fastcmp(word, "OBJECT")) @@ -479,7 +453,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) ignorelines(f); } else if (i > 0 && i <= MAXCONDITIONSETS) - readconditionset(f, (UINT8)i); + readconditionset(f, (UINT8)(i-1)); else { deh_warning("Condition set number %d out of range (1 - %d)", i, MAXCONDITIONSETS); @@ -512,7 +486,6 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) { cup = Z_Calloc(sizeof (cupheader_t), PU_STATIC, NULL); cup->id = numkartcupheaders; - cup->unlockrequired = -1; deh_strlcpy(cup->name, word2, sizeof(cup->name), va("Cup header %s: name", word2)); if (prev != NULL) @@ -569,41 +542,6 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) { deh_warning("Patch is only compatible with base SRB2."); } - // Clear all data in certain locations (mostly for unlocks) - // Unless you REALLY want to piss people off, - // define a custom gamedata /before/ doing this!! - // (then again, modifiedgame will prevent game data saving anyway) - else if (fastcmp(word, "CLEAR")) - { - boolean clearall = (fastcmp(word2, "ALL")); - - if (!mainfile && !gamedataadded) - { - deh_warning("You must define a custom gamedata to use \"%s\"", word); - continue; - } - - if (clearall || fastcmp(word2, "UNLOCKABLES")) - memset(&unlockables, 0, sizeof(unlockables)); - - if (clearall || fastcmp(word2, "EMBLEMS")) - { - memset(&emblemlocations, 0, sizeof(emblemlocations)); - numemblems = 0; - } - - if (clearall || fastcmp(word2, "EXTRAEMBLEMS")) - { - memset(&extraemblems, 0, sizeof(extraemblems)); - numextraemblems = 0; - } - - if (clearall || fastcmp(word2, "CONDITIONSETS")) - clear_conditionsets(); - - if (clearall || fastcmp(word2, "LEVELS")) - clear_levels(); - } else deh_warning("Unknown word: %s", word); } diff --git a/src/discord.c b/src/discord.c index 25ba4605b..28381d250 100644 --- a/src/discord.c +++ b/src/discord.c @@ -540,7 +540,9 @@ void DRPC_UpdatePresence(void) else { // Map name on tool tip - snprintf(mapname, 48, "Map: %s", G_BuildMapTitle(gamemap)); + char *title = G_BuildMapTitle(gamemap); + snprintf(mapname, 48, "Map: %s", title); + Z_Free(title); discordPresence.largeImageText = mapname; } diff --git a/src/doomdef.h b/src/doomdef.h index 669b90c71..11ba96305 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -201,7 +201,8 @@ extern char logfilename[1024]; #define MAXGAMEPADS (MAXSPLITSCREENPLAYERS * 2) // Number of gamepads we'll be allowing #define MAXSKINS UINT8_MAX -#define SKINNAMESIZE 16 // Moved from r_skins.h as including that particular header causes issues later down the line. +#define SKINNAMESIZE 16 +#define MAXAVAILABILITY ((MAXSKINS + 7)/8) #define COLORRAMPSIZE 16 #define MAXCOLORNAME 32 diff --git a/src/doomstat.h b/src/doomstat.h index ade6a4bb2..045924213 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -186,8 +186,6 @@ extern INT32 postimgparam[MAXSPLITSCREENPLAYERS]; extern INT32 viewwindowx, viewwindowy; extern INT32 viewwidth, scaledviewwidth; -extern boolean gamedataloaded; - // Player taking events, and displaying. extern INT32 consoleplayer; extern INT32 displayplayers[MAXSPLITSCREENPLAYERS]; @@ -357,7 +355,6 @@ 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) - SINT8 unlockrequired; ///< An unlockable is required to select this cup. -1 for no unlocking required. cupheader_t *next; ///< Next cup in linked list }; @@ -393,7 +390,6 @@ struct mapheader_t // Selection metadata char keywords[33]; ///< Keywords separated by space to search for. 32 characters. - SINT8 unlockrequired; ///< Is an unlockable required to play this level? -1 if no. UINT8 levelselect; ///< Is this map available in the level select? If so, which map list is it available in? UINT16 menuflags; ///< LF2_flags: options that affect record attack menus @@ -540,9 +536,6 @@ struct tolinfo_t extern tolinfo_t TYPEOFLEVEL[NUMTOLNAMES]; extern UINT32 lastcustomtol; -extern tic_t totalplaytime; -extern UINT32 matchesplayed; - extern UINT8 stagefailed; // Emeralds stored as bits to throw savegame hackers off. @@ -580,10 +573,6 @@ extern INT32 luabanks[NUM_LUABANKS]; extern INT32 nummaprings; //keep track of spawned rings/coins -extern UINT32 token; ///< Number of tokens collected in a level -extern UINT32 tokenlist; ///< List of tokens collected -extern boolean gottoken; ///< Did you get a token? Used for end of act -extern INT32 tokenbits; ///< Used for setting token bits extern UINT32 bluescore; ///< Blue Team Scores extern UINT32 redscore; ///< Red Team Scores @@ -684,8 +673,6 @@ extern INT16 votelevels[4][2]; extern SINT8 votes[MAXPLAYERS]; extern SINT8 pickedvote; -extern UINT32 timesBeaten; // # of times the game has been beaten. - // =========================== // Internal parameters, fixed. // =========================== diff --git a/src/f_finale.c b/src/f_finale.c index ca5b44fb3..e20deae1f 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -866,7 +866,7 @@ boolean F_CreditResponder(event_t *event) return false; } - /*if (!(timesBeaten) && !(netgame || multiplayer) && !cht_debug) + /*if (!(gamedata->timesBeaten) && !(netgame || multiplayer) && !cht_debug) return false;*/ if (key != KEY_ESCAPE && key != KEY_ENTER && key != KEY_BACKSPACE) @@ -1024,31 +1024,6 @@ void F_GameEvaluationDrawer(void) V_DrawCreditString((BASEVIDWIDTH - V_CreditStringWidth(endingtext))<<(FRACBITS-1), (BASEVIDHEIGHT-100)<<(FRACBITS-1), 0, endingtext); -#if 0 // the following looks like hot garbage the more unlockables we add, and we now have a lot of unlockables - if (finalecount >= 5*TICRATE) - { - V_DrawString(8, 16, V_YELLOWMAP, "Unlocked:"); - - if (!usedCheats) - { - INT32 startcoord = 32; - - for (i = 0; i < MAXUNLOCKABLES; i++) - { - if (unlockables[i].conditionset && unlockables[i].conditionset < MAXCONDITIONSETS - && unlockables[i].type && !unlockables[i].nocecho) - { - if (unlockables[i].unlocked) - V_DrawString(8, startcoord, 0, unlockables[i].name); - startcoord += 8; - } - } - } - else - V_DrawString(8, 96, V_YELLOWMAP, "Cheated games\ncan't unlock\nextras!"); - } -#endif - if (marathonmode) { const char *rtatext, *cuttext; @@ -1101,11 +1076,9 @@ void F_GameEvaluationTicker(void) { if (!usedCheats) { - ++timesBeaten; - - if (M_UpdateUnlockablesAndExtraEmblems()) - S_StartSound(NULL, sfx_s3k68); + ++gamedata->timesBeaten; + M_UpdateUnlockablesAndExtraEmblems(true); G_SaveGameData(); } else @@ -1611,7 +1584,7 @@ void F_EndingDrawer(void) //colset(linkmap, 164, 165, 169); -- the ideal purple colour to represent a clicked in-game link, but not worth it just for a soundtest-controlled secret V_DrawCenteredString(BASEVIDWIDTH/2, 8, V_ALLOWLOWERCASE|(trans<'|(trans<= STOPPINGPOINT-TICRATE) ? V_PURPLEMAP : V_BLUEMAP)|(trans<"); + V_DrawString(40, ((finalecount == STOPPINGPOINT-(20+TICRATE)) ? 1 : 0)+BASEVIDHEIGHT-16, ((gamedata->timesBeaten || finalecount >= STOPPINGPOINT-TICRATE) ? V_PURPLEMAP : V_BLUEMAP)|(trans<"); } if (finalecount > STOPPINGPOINT-(20+(2*TICRATE))) @@ -1804,7 +1777,14 @@ static void F_CacheTitleScreen(void) break; // idk do we still want this? case TTMODE_RINGRACERS: - kts_bumper = W_CachePatchName("KTSBUMPR1", PU_PATCH_LOWPRIORITY); + if (!M_SecretUnlocked(SECRET_ALTTITLE, true)) + { + CV_StealthSetValue(&cv_alttitle, 0); + } + + kts_bumper = W_CachePatchName( + (cv_alttitle.value ? "KTSJUMPR1" : "KTSBUMPR1"), + PU_PATCH_LOWPRIORITY); kts_eggman = W_CachePatchName("KTSEGG01", PU_PATCH_LOWPRIORITY); kts_tails = W_CachePatchName("KTSTAL01", PU_PATCH_LOWPRIORITY); kts_tails_tails = W_CachePatchName("KTSTAL02", PU_PATCH_LOWPRIORITY); diff --git a/src/g_demo.c b/src/g_demo.c index 8566a4c22..e47d8ba28 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -212,47 +212,6 @@ void G_LoadMetal(UINT8 **buffer) metal_p = metalbuffer + READUINT32(*buffer); } -// Finds a skin with the closest stats if the expected skin doesn't exist. -static INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight, UINT32 flags) -{ - INT32 i, closest_skin = 0; - UINT8 closest_stats, stat_diff; - boolean doflagcheck = true; - UINT32 flagcheck = flags; - -flaglessretry: - closest_stats = UINT8_MAX; - - for (i = 0; i < numskins; i++) - { - stat_diff = abs(skins[i].kartspeed - kartspeed) + abs(skins[i].kartweight - kartweight); - if (doflagcheck && (skins[i].flags & flagcheck) != flagcheck) - { - continue; - } - if (stat_diff < closest_stats) - { - closest_stats = stat_diff; - closest_skin = i; - } - } - - if (stat_diff && (doflagcheck || closest_stats == UINT8_MAX)) - { - // Just grab *any* SF_IRONMAN if we don't get it on the first pass. - if ((flagcheck & SF_IRONMAN) && (flagcheck != SF_IRONMAN)) - { - flagcheck = SF_IRONMAN; - } - - doflagcheck = false; - - goto flaglessretry; - } - - return closest_skin; -} - void G_ReadDemoExtraData(void) { INT32 p, extradata, i; @@ -276,10 +235,8 @@ void G_ReadDemoExtraData(void) { extradata = READUINT8(demo_p); - if (extradata & DXD_PLAYSTATE) + if (extradata & DXD_JOINDATA) { - i = READUINT8(demo_p); - if (!playeringame[p]) { CL_ClearPlayer(p); @@ -288,14 +245,22 @@ void G_ReadDemoExtraData(void) players[p].spectator = true; } - if ((players[p].bot = !!(i & DXD_PST_ISBOT))) + for (i = 0; i < MAXAVAILABILITY; i++) + { + players[p].availabilities[i] = READUINT8(demo_p); + } + + players[p].bot = !!(READUINT8(demo_p)); + if (players[p].bot) { players[p].botvars.difficulty = READUINT8(demo_p); players[p].botvars.diffincrease = READUINT8(demo_p); // needed to avoid having to duplicate logic players[p].botvars.rival = (boolean)READUINT8(demo_p); - - i &= ~DXD_PST_ISBOT; } + } + if (extradata & DXD_PLAYSTATE) + { + i = READUINT8(demo_p); switch (i) { case DXD_PST_PLAYING: @@ -334,14 +299,6 @@ void G_ReadDemoExtraData(void) K_CheckBumpers(); P_CheckRacers(); } - if (extradata & DXD_RESPAWN) - { - if (players[p].mo) - { - // Is this how this should work..? - K_DoIngameRespawn(&players[p]); - } - } if (extradata & DXD_SKIN) { UINT8 skinid; @@ -397,6 +354,14 @@ void G_ReadDemoExtraData(void) } } } + if (extradata & DXD_RESPAWN) + { + if (players[p].mo) + { + // Is this how this should work..? + K_DoIngameRespawn(&players[p]); + } + } if (extradata & DXD_WEAPONPREF) { WeaponPref_Parse(&demo_p, p); @@ -454,6 +419,21 @@ void G_WriteDemoExtraData(void) WRITEUINT8(demo_p, i); WRITEUINT8(demo_p, demo_extradata[i]); + if (demo_extradata[i] & DXD_JOINDATA) + { + for (j = 0; j < MAXAVAILABILITY; j++) + { + WRITEUINT8(demo_p, players[i].availabilities[i]); + } + + WRITEUINT8(demo_p, (UINT8)players[i].bot); + if (players[i].bot) + { + WRITEUINT8(demo_p, players[i].botvars.difficulty); + WRITEUINT8(demo_p, players[i].botvars.diffincrease); // needed to avoid having to duplicate logic + WRITEUINT8(demo_p, (UINT8)players[i].botvars.rival); + } + } if (demo_extradata[i] & DXD_PLAYSTATE) { UINT8 pst = DXD_PST_PLAYING; @@ -472,19 +452,7 @@ void G_WriteDemoExtraData(void) pst = DXD_PST_SPECTATING; } - if (players[i].bot) - { - pst |= DXD_PST_ISBOT; - } - WRITEUINT8(demo_p, pst); - - if (pst & DXD_PST_ISBOT) - { - WRITEUINT8(demo_p, players[i].botvars.difficulty); - WRITEUINT8(demo_p, players[i].botvars.diffincrease); // needed to avoid having to duplicate logic - WRITEUINT8(demo_p, (UINT8)players[i].botvars.rival); - } } //if (demo_extradata[i] & DXD_RESPAWN) has no extra data if (demo_extradata[i] & DXD_SKIN) @@ -1218,8 +1186,14 @@ void G_GhostTicker(void) if (ziptic == 0) // Only support player 0 info for now { ziptic = READUINT8(g->p); + if (ziptic & DXD_JOINDATA) + { + g->p += MAXAVAILABILITY; + if (READUINT8(g->p) != 0) + I_Error("Ghost is not a record attack ghost (bot JOINDATA)"); + } if (ziptic & DXD_PLAYSTATE && READUINT8(g->p) != DXD_PST_PLAYING) - I_Error("Ghost is not a record attack ghost PLAYSTATE"); //@TODO lmao don't blow up like this + I_Error("Ghost is not a record attack ghost (has PLAYSTATE)"); if (ziptic & DXD_SKIN) g->p++; // We _could_ read this info, but it shouldn't change anything in record attack... if (ziptic & DXD_COLOR) @@ -2248,6 +2222,7 @@ static void G_SaveDemoSkins(UINT8 **pp) { char skin[16]; UINT8 i; + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true); WRITEUINT8((*pp), numskins); for (i = 0; i < numskins; i++) @@ -2262,12 +2237,17 @@ static void G_SaveDemoSkins(UINT8 **pp) WRITEUINT8((*pp), skins[i].kartweight); WRITEUINT32((*pp), skins[i].flags); } + + for (i = 0; i < MAXAVAILABILITY; i++) + { + WRITEUINT8((*pp), availabilitiesbuffer[i]); + } } static democharlist_t *G_LoadDemoSkins(UINT8 **pp, UINT8 *worknumskins, boolean getclosest) { char skin[17]; - UINT8 i; + UINT8 i, byte, shif; democharlist_t *skinlist = NULL; (*worknumskins) = READUINT8((*pp)); @@ -2300,7 +2280,7 @@ static democharlist_t *G_LoadDemoSkins(UINT8 **pp, UINT8 *worknumskins, boolean } else { - result = GetSkinNumClosestToStats(skinlist[i].kartspeed, skinlist[i].kartweight, skinlist[i].flags); + result = GetSkinNumClosestToStats(skinlist[i].kartspeed, skinlist[i].kartweight, skinlist[i].flags, true); } } @@ -2310,6 +2290,24 @@ static democharlist_t *G_LoadDemoSkins(UINT8 **pp, UINT8 *worknumskins, boolean } } + for (byte = 0; byte < MAXAVAILABILITY; byte++) + { + UINT8 availabilitiesbuffer = READUINT8((*pp)); + + for (shif = 0; shif < 8; shif++) + { + i = (byte*8) + shif; + + if (i >= (*worknumskins)) + break; + + if (availabilitiesbuffer & (1 << shif)) + { + skinlist[i].unlockrequired = true; + } + } + } + return skinlist; } @@ -2326,6 +2324,8 @@ static void G_SkipDemoSkins(UINT8 **pp) (*pp)++; // kartweight (*pp) += 4; // flags } + + (*pp) += MAXAVAILABILITY; } void G_BeginRecording(void) @@ -2358,7 +2358,11 @@ void G_BeginRecording(void) // Full replay title demo_p += 64; - snprintf(demo.titlename, 64, "%s - %s", G_BuildMapTitle(gamemap), modeattacking ? "Record Attack" : connectedservername); + { + char *title = G_BuildMapTitle(gamemap); + snprintf(demo.titlename, 64, "%s - %s", title, modeattacking ? "Record Attack" : connectedservername); + Z_Free(title); + } // demo checksum demo_p += 16; @@ -2443,6 +2447,11 @@ void G_BeginRecording(void) M_Memcpy(demo_p,name,16); demo_p += 16; + for (j = 0; j < MAXAVAILABILITY; j++) + { + WRITEUINT8(demo_p, player->availabilities[j]); + } + // Skin (now index into demo.skinlist) WRITEUINT8(demo_p, player->skin); WRITEUINT8(demo_p, player->lastfakeskin); @@ -2928,6 +2937,7 @@ void G_DoPlayDemo(char *defdemoname) UINT8 i, p, numslots = 0; lumpnum_t l; char color[MAXCOLORNAME+1],follower[17],mapname[MAXMAPLUMPNAME],*n,*pdemoname; + UINT8 availabilities[MAXPLAYERS][MAXAVAILABILITY]; UINT8 version,subversion; UINT32 randseed[PRNUMCLASS]; char msg[1024]; @@ -3293,6 +3303,11 @@ void G_DoPlayDemo(char *defdemoname) M_Memcpy(player_names[p],demo_p,16); demo_p += 16; + for (i = 0; i < MAXAVAILABILITY; i++) + { + availabilities[p][i] = READUINT8(demo_p); + } + // Skin i = READUINT8(demo_p); @@ -3376,6 +3391,8 @@ void G_DoPlayDemo(char *defdemoname) for (i = 0; i < numslots; i++) { + UINT8 j; + p = slots[i]; if (players[p].mo) { @@ -3392,6 +3409,11 @@ void G_DoPlayDemo(char *defdemoname) players[p].kartweight = ghostext[p].kartweight = demo.skinlist[demo.currentskinid[p]].kartweight; players[p].charflags = ghostext[p].charflags = demo.skinlist[demo.currentskinid[p]].flags; players[p].lastfakeskin = lastfakeskin[p]; + + for (j = 0; j < MAXAVAILABILITY; j++) + { + players[p].availabilities[j] = availabilities[p][j]; + } } demo.deferstart = true; diff --git a/src/g_demo.h b/src/g_demo.h index 78e544342..db5158b9a 100644 --- a/src/g_demo.h +++ b/src/g_demo.h @@ -33,6 +33,7 @@ struct democharlist_t { UINT8 kartspeed; UINT8 kartweight; UINT32 flags; + boolean unlockrequired; }; // Publicly-accessible demo vars @@ -119,20 +120,19 @@ typedef enum extern UINT8 demo_extradata[MAXPLAYERS]; extern UINT8 demo_writerng; -#define DXD_PLAYSTATE 0x01 // state changed between playing, spectating, or not in-game -#define DXD_RESPAWN 0x02 // "respawn" command in console +#define DXD_JOINDATA 0x01 // join-specific data +#define DXD_PLAYSTATE 0x02 // state changed between playing, spectating, or not in-game #define DXD_SKIN 0x04 // skin changed #define DXD_NAME 0x08 // name changed #define DXD_COLOR 0x10 // color changed #define DXD_FOLLOWER 0x20 // follower was changed -#define DXD_WEAPONPREF 0x40 // netsynced playsim settings were changed +#define DXD_RESPAWN 0x40 // "respawn" command in console +#define DXD_WEAPONPREF 0x80 // netsynced playsim settings were changed #define DXD_PST_PLAYING 0x01 #define DXD_PST_SPECTATING 0x02 #define DXD_PST_LEFT 0x03 -#define DXD_PST_ISBOT 0x80 // extra flag - // Record/playback tics void G_ReadDemoExtraData(void); void G_WriteDemoExtraData(void); diff --git a/src/g_game.c b/src/g_game.c index 819a6369e..fb49b598c 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -189,7 +189,8 @@ struct quake quake; // Map Header Information mapheader_t** mapheaderinfo = {NULL}; -INT32 nummapheaders, mapallocsize = 0; +INT32 nummapheaders = 0; +INT32 mapallocsize = 0; // Kart cup definitions cupheader_t *kartcupheaders = NULL; @@ -203,14 +204,6 @@ UINT8 stagefailed; // Used for GEMS BONUS? Also to see if you beat the stage. UINT16 emeralds; INT32 luabanks[NUM_LUABANKS]; -UINT32 token; // Number of tokens collected in a level -UINT32 tokenlist; // List of tokens collected -boolean gottoken; // Did you get a token? Used for end of act -INT32 tokenbits; // Used for setting token bits - -tic_t totalplaytime; -UINT32 matchesplayed; // SRB2Kart -boolean gamedataloaded = false; // Temporary holding place for nights data for the current map //nightsdata_t ntemprecords; @@ -340,9 +333,6 @@ static void G_ResetRandMapBuffer(void) //intentionally not resetting randmaps.counttogametype here } -// Grading -UINT32 timesBeaten; - typedef struct joystickvector2_s { INT32 xaxis; @@ -600,10 +590,7 @@ static void G_UpdateRecordReplays(void) if ((earnedEmblems = M_CheckLevelEmblems())) CONS_Printf(M_GetText("\x82" "Earned %hu medal%s for Record Attack records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : ""); - if (M_UpdateUnlockablesAndExtraEmblems()) - S_StartSound(NULL, sfx_ncitem); - - // SRB2Kart - save here so you NEVER lose your earned times/medals. + M_UpdateUnlockablesAndExtraEmblems(true); G_SaveGameData(); } @@ -2194,9 +2181,8 @@ static inline void G_PlayerFinishLevel(INT32 player) { if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) { - matchesplayed++; - if (M_UpdateUnlockablesAndExtraEmblems()) - S_StartSound(NULL, sfx_ncitem); + gamedata->matchesplayed++; + M_UpdateUnlockablesAndExtraEmblems(true); G_SaveGameData(); } @@ -2238,7 +2224,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) UINT8 latestlap; UINT16 skincolor; INT32 skin; - UINT32 availabilities; + UINT8 availabilities[MAXAVAILABILITY]; UINT8 fakeskin; UINT8 lastfakeskin; @@ -2306,7 +2292,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) followercolor = players[player].followercolor; followerskin = players[player].followerskin; - availabilities = players[player].availabilities; + memcpy(availabilities, players[player].availabilities, sizeof(availabilities)); followitem = players[player].followitem; @@ -2429,7 +2415,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->charflags = charflags; p->lastfakeskin = lastfakeskin; - p->availabilities = availabilities; + memcpy(players[player].availabilities, availabilities, sizeof(availabilities)); p->followitem = followitem; p->starpostnum = starpostnum; @@ -2918,7 +2904,7 @@ void G_AddPlayer(INT32 playernum) { player_t *p = &players[playernum]; p->playerstate = PST_REBORN; - demo_extradata[playernum] |= DXD_PLAYSTATE|DXD_COLOR|DXD_NAME|DXD_SKIN|DXD_FOLLOWER; // Set everything + demo_extradata[playernum] |= DXD_JOINDATA|DXD_PLAYSTATE|DXD_COLOR|DXD_NAME|DXD_SKIN|DXD_FOLLOWER; // Set everything } void G_ExitLevel(void) @@ -3315,7 +3301,7 @@ INT16 G_SometimesGetDifferentGametype(UINT8 prefgametype) // Most of the gametype references in this condition are intentionally not prefgametype. // This is so a server CAN continue playing a gametype if they like the taste of it. // The encore check needs prefgametype so can't use G_RaceGametype... - boolean encorepossible = ((M_SecretUnlocked(SECRET_ENCORE) || encorescramble == 1) + boolean encorepossible = ((M_SecretUnlocked(SECRET_ENCORE, false) || encorescramble == 1) && ((gametyperules|gametypedefaultrules[prefgametype]) & GTR_CIRCUIT)); UINT8 encoremodifier = 0; @@ -3418,23 +3404,27 @@ INT16 G_GetFirstMapOfGametype(UINT8 pgametype) { UINT8 i = 0; INT16 mapnum = NEXTMAP_INVALID; - UINT32 tol = G_TOLFlag(pgametype); + levelsearch_t templevelsearch; - levellist.cupmode = (!(gametypedefaultrules[pgametype] & GTR_NOCUPSELECT)); - levellist.timeattack = false; + templevelsearch.cup = NULL; + templevelsearch.typeoflevel = G_TOLFlag(pgametype); + templevelsearch.cupmode = (!(gametypedefaultrules[pgametype] & GTR_NOCUPSELECT)); + templevelsearch.timeattack = false; + templevelsearch.checklocked = true; - if (levellist.cupmode) + if (templevelsearch.cupmode) { - cupheader_t *cup = kartcupheaders; - while (cup && mapnum >= nummapheaders) + templevelsearch.cup = kartcupheaders; + while (templevelsearch.cup && mapnum >= nummapheaders) { - mapnum = M_GetFirstLevelInList(&i, tol, cup); + mapnum = M_GetFirstLevelInList(&i, &templevelsearch); i = 0; + templevelsearch.cup = templevelsearch.cup->next; } } else { - mapnum = M_GetFirstLevelInList(&i, tol, NULL); + mapnum = M_GetFirstLevelInList(&i, &templevelsearch); } return mapnum; @@ -3687,6 +3677,9 @@ 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); + G_SaveGameData(); } static boolean CanSaveLevel(INT32 mapnum) @@ -3869,7 +3862,7 @@ static void G_GetNextMap(void) while (cup) { // Not unlocked? Grab the next result afterwards - if (!marathonmode && cup->unlockrequired != -1 && !unlockables[cup->unlockrequired].unlocked) + if (!marathonmode && M_CupLocked(cup)) { cup = cup->next; gettingresult = 1; @@ -4223,10 +4216,6 @@ static void G_DoContinued(void) // Reset score pl->score = 0; - // Allow tokens to come back - tokenlist = 0; - token = 0; - if (!(netgame || multiplayer || demo.playback || demo.recording || metalrecording || modeattacking) && !usedCheats && cursaveslot > 0) G_SaveGameOver((UINT32)cursaveslot, true); @@ -4310,7 +4299,8 @@ void G_LoadGameSettings(void) Color_cons_t[MAXSKINCOLORS].strvalue = Followercolor_cons_t[MAXSKINCOLORS+2].strvalue = NULL; } -#define GD_VERSIONCHECK 0xBA5ED444 // Change every major version, as usual +#define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual +#define GD_VERSIONMINOR 0 // Change every format update // G_LoadGameData // Loads the main data file, which stores information such as emblems found, etc. @@ -4319,21 +4309,22 @@ void G_LoadGameData(void) size_t length; UINT32 i, j; UINT32 versionID; + UINT8 versionMinor; UINT8 rtemp; //For records UINT32 numgamedatamapheaders; // Stop saving, until we successfully load it again. - gamedataloaded = false; + gamedata->loaded = false; // Clear things so previously read gamedata doesn't transfer // to new gamedata - G_ClearRecords(); // main and nights records + G_ClearRecords(); // records M_ClearSecrets(); // emblems, unlocks, maps visited, etc - totalplaytime = 0; // total play time (separate from all) - matchesplayed = 0; // SRB2Kart: matches played & finished + gamedata->totalplaytime = 0; // total play time (separate from all) + gamedata->matchesplayed = 0; // SRB2Kart: matches played & finished if (M_CheckParm("-nodata")) { @@ -4344,7 +4335,7 @@ void G_LoadGameData(void) if (M_CheckParm("-resetdata")) { // Don't load, but do save. (essentially, reset) - gamedataloaded = true; + gamedata->loaded = true; return; } @@ -4352,7 +4343,7 @@ void G_LoadGameData(void) if (!length) { // No gamedata. We can save a new one. - gamedataloaded = true; + gamedata->loaded = true; return; } @@ -4371,8 +4362,16 @@ void G_LoadGameData(void) I_Error("Game data is not for Ring Racers v2.0.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder); } - totalplaytime = READUINT32(save_p); - matchesplayed = READUINT32(save_p); + versionMinor = READUINT8(save_p); + if (versionMinor > GD_VERSIONMINOR) + { + Z_Free(savebuffer); + save_p = NULL; + I_Error("Game data is from the future! (expected %d, got %d)", GD_VERSIONMINOR, versionMinor); + } + + gamedata->totalplaytime = READUINT32(save_p); + gamedata->matchesplayed = READUINT32(save_p); { // Quick & dirty hash for what mod this save file is for. @@ -4391,32 +4390,49 @@ void G_LoadGameData(void) { rtemp = READUINT8(save_p); for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j) - emblemlocations[j+i].collected = ((rtemp >> j) & 1); - i += j; - } - for (i = 0; i < MAXEXTRAEMBLEMS;) - { - rtemp = READUINT8(save_p); - for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j) - extraemblems[j+i].collected = ((rtemp >> j) & 1); + gamedata->collected[j+i] = ((rtemp >> j) & 1); i += j; } for (i = 0; i < MAXUNLOCKABLES;) { rtemp = READUINT8(save_p); for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) - unlockables[j+i].unlocked = ((rtemp >> j) & 1); + gamedata->unlocked[j+i] = ((rtemp >> j) & 1); + i += j; + } + for (i = 0; i < MAXUNLOCKABLES;) + { + rtemp = READUINT8(save_p); + for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) + gamedata->unlockpending[j+i] = ((rtemp >> j) & 1); i += j; } for (i = 0; i < MAXCONDITIONSETS;) { rtemp = READUINT8(save_p); for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j) - conditionSets[j+i].achieved = ((rtemp >> j) & 1); + gamedata->achieved[j+i] = ((rtemp >> j) & 1); i += j; } - timesBeaten = READUINT32(save_p); + gamedata->challengegridwidth = READUINT16(save_p); + Z_Free(gamedata->challengegrid); + if (gamedata->challengegridwidth) + { + gamedata->challengegrid = Z_Malloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), + PU_STATIC, NULL); + for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) + { + gamedata->challengegrid[i] = READUINT8(save_p); + } + } + else + { + gamedata->challengegrid = NULL; + } + + gamedata->timesBeaten = READUINT32(save_p); // Main records numgamedatamapheaders = READUINT32(save_p); @@ -4468,10 +4484,10 @@ void G_LoadGameData(void) // 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! - gamedataloaded = true; + gamedata->loaded = true; // Silent update unlockables in case they're out of sync with conditions - M_SilentUpdateUnlockablesAndEmblems(); + M_UpdateUnlockablesAndExtraEmblems(false); return; @@ -4497,10 +4513,22 @@ void G_SaveGameData(void) INT32 i, j; UINT8 btemp; - if (!gamedataloaded) + if (!gamedata->loaded) return; // If never loaded (-nodata), don't save - length = (4+4+4+1+(MAXEMBLEMS)+MAXEXTRAEMBLEMS+MAXUNLOCKABLES+MAXCONDITIONSETS+4+4); + if (usedCheats) + { +#ifdef DEVELOP + CONS_Alert(CONS_WARNING, M_GetText("Cheats used - Gamedata will not be saved.\n")); +#endif + return; + } + + length = (4+1+4+4+1+(MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+4+4+2); + if (gamedata->challengegrid) + { + length += gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; + } length += nummapheaders * (MAXMAPLUMPNAME+1+4+4); save_p = savebuffer = (UINT8 *)malloc(length); @@ -4510,18 +4538,12 @@ void G_SaveGameData(void) return; } - if (usedCheats) - { - free(savebuffer); - save_p = savebuffer = NULL; - return; - } - // Version test WRITEUINT32(save_p, GD_VERSIONCHECK); // 4 - WRITEUINT32(save_p, totalplaytime); // 4 - WRITEUINT32(save_p, matchesplayed); // 4 + WRITEUINT8(save_p, GD_VERSIONMINOR); // 1 + WRITEUINT32(save_p, gamedata->totalplaytime); // 4 + WRITEUINT32(save_p, gamedata->matchesplayed); // 4 WRITEUINT32(save_p, quickncasehash(timeattackfolder, 64)); // To save space, use one bit per collected/achieved/unlocked flag @@ -4529,36 +4551,52 @@ void G_SaveGameData(void) { btemp = 0; for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j) - btemp |= (emblemlocations[j+i].collected << j); - WRITEUINT8(save_p, btemp); - i += j; - } - for (i = 0; i < MAXEXTRAEMBLEMS;) // MAXEXTRAEMBLEMS * 1; - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j) - btemp |= (extraemblems[j+i].collected << j); - WRITEUINT8(save_p, btemp); - i += j; - } - for (i = 0; i < MAXUNLOCKABLES;) // MAXUNLOCKABLES * 1; - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) - btemp |= (unlockables[j+i].unlocked << j); - WRITEUINT8(save_p, btemp); - i += j; - } - for (i = 0; i < MAXCONDITIONSETS;) // MAXCONDITIONSETS * 1; - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j) - btemp |= (conditionSets[j+i].achieved << j); + btemp |= (gamedata->collected[j+i] << j); WRITEUINT8(save_p, btemp); i += j; } - WRITEUINT32(save_p, timesBeaten); // 4 + // MAXUNLOCKABLES * 2; + for (i = 0; i < MAXUNLOCKABLES;) + { + btemp = 0; + for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) + btemp |= (gamedata->unlocked[j+i] << j); + WRITEUINT8(save_p, btemp); + i += j; + } + for (i = 0; i < MAXUNLOCKABLES;) + { + btemp = 0; + for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) + btemp |= (gamedata->unlockpending[j+i] << j); + WRITEUINT8(save_p, btemp); + i += j; + } + + for (i = 0; i < MAXCONDITIONSETS;) // MAXCONDITIONSETS * 1; + { + btemp = 0; + for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j) + btemp |= (gamedata->achieved[j+i] << j); + WRITEUINT8(save_p, btemp); + i += j; + } + + if (gamedata->challengegrid) // 2 + gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT + { + WRITEUINT16(save_p, gamedata->challengegridwidth); + for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) + { + WRITEUINT8(save_p, gamedata->challengegrid[i]); + } + } + else // 2 + { + WRITEUINT16(save_p, 0); + } + + WRITEUINT32(save_p, gamedata->timesBeaten); // 4 // Main records WRITEUINT32(save_p, nummapheaders); // 4 diff --git a/src/hu_stuff.c b/src/hu_stuff.c index 0f861f642..bbccb232a 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -98,7 +98,7 @@ static char hu_tick; //------------------------------------------- patch_t *missingpat; -patch_t *blanklvl; +patch_t *blanklvl, *nolvl; // song credits static patch_t *songcreditbg; @@ -186,6 +186,7 @@ void HU_LoadGraphics(void) Font_Load(); HU_UpdatePatch(&blanklvl, "BLANKLVL"); + HU_UpdatePatch(&nolvl, "M_NOLVL"); HU_UpdatePatch(&songcreditbg, "K_SONGCR"); diff --git a/src/info.c b/src/info.c index 4566222f3..8e3757190 100644 --- a/src/info.c +++ b/src/info.c @@ -7999,7 +7999,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = 1000, // spawnhealth S_NULL, // seestate sfx_None, // seesound - 8, // reactiontime + 0, // reactiontime sfx_None, // attacksound S_NULL, // painstate 0, // painchance @@ -24202,6 +24202,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, + { // MT_GACHABOM + -1, // doomednum + S_ORBINAUT1, // spawnstate + 7, // spawnhealth + S_NULL, // seestate + sfx_tossed, // seesound + 8, // reactiontime + sfx_s3k49, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_ORBINAUT_DEAD,// deathstate + S_NULL, // xdeathstate + sfx_s3k5d, // deathsound + 64*FRACUNIT, // speed + 24*FRACUNIT, // radius + 32*FRACUNIT, // height + 0, // display offset + 100, // mass + 1, // damage + sfx_s3k96, // activesound + MF_SHOOTABLE|MF_DONTENCOREMAP|MF_APPLYTERRAIN|MF_SLOPE, // flags + S_NULL // raisestate + }, + { // MT_DUELBOMB 2050, // doomednum S_SPB1, // spawnstate diff --git a/src/info.h b/src/info.h index cf2b5d6bd..5056d4f69 100644 --- a/src/info.h +++ b/src/info.h @@ -6442,6 +6442,8 @@ typedef enum mobj_type MT_SINK_SHIELD, MT_SINKTRAIL, + MT_GACHABOM, + MT_DUELBOMB, // Duel mode bombs MT_BATTLEBUMPER, // Battle Mode bumpers diff --git a/src/k_botitem.c b/src/k_botitem.c index fe0837c84..a591bc38c 100644 --- a/src/k_botitem.c +++ b/src/k_botitem.c @@ -1558,6 +1558,11 @@ void K_BotItemUsage(player_t *player, ticcmd_t *cmd, INT16 turnamt) case KITEM_FLAMESHIELD: K_BotItemFlame(player, cmd); break; + /* + case KITEM_GACHABOM: + K_BotItemGachabom(player, cmd); + break; + */ } } } diff --git a/src/k_botsearch.c b/src/k_botsearch.c index 06934024d..f619035e9 100644 --- a/src/k_botsearch.c +++ b/src/k_botsearch.c @@ -415,6 +415,7 @@ static BlockItReturn_t K_FindObjectsForNudging(mobj_t *thing) case MT_SPB: case MT_BUBBLESHIELDTRAP: case MT_DUELBOMB: + case MT_GACHABOM: K_AddDodgeObject(thing, side, 20); break; case MT_SHRINK_GUN: diff --git a/src/k_collide.c b/src/k_collide.c index db7be93f0..06d6900e6 100644 --- a/src/k_collide.c +++ b/src/k_collide.c @@ -87,7 +87,7 @@ boolean K_BananaBallhogCollide(mobj_t *t1, mobj_t *t2) else if (t2->type == MT_BANANA || t2->type == MT_BANANA_SHIELD || t2->type == MT_ORBINAUT || t2->type == MT_ORBINAUT_SHIELD || t2->type == MT_JAWZ || t2->type == MT_JAWZ_SHIELD - || t2->type == MT_BALLHOG) + || t2->type == MT_BALLHOG || t2->type == MT_GACHABOM) { // Other Item Damage angle_t bounceangle = K_GetCollideAngle(t1, t2); @@ -204,7 +204,7 @@ boolean K_EggItemCollide(mobj_t *t1, mobj_t *t2) static mobj_t *grenade; static fixed_t explodedist; static boolean explodespin; -static tic_t minehitlag; +static INT32 minehitlag; static inline boolean PIT_SSMineChecks(mobj_t *thing) { @@ -272,6 +272,9 @@ void K_DoMineSearch(mobj_t *actor, fixed_t size) static inline BlockItReturn_t PIT_SSMineExplode(mobj_t *thing) { + const INT32 oldhitlag = thing->hitlag; + INT32 lagadded; + if (grenade == NULL || P_MobjWasRemoved(grenade)) return BMIT_ABORT; // There's the possibility these can chain react onto themselves after they've already died if there are enough all in one spot @@ -283,9 +286,13 @@ static inline BlockItReturn_t PIT_SSMineExplode(mobj_t *thing) if (PIT_SSMineChecks(thing) == true) return BMIT_CONTINUE; - if (P_DamageMobj(thing, grenade, grenade->target, 1, (explodespin ? DMG_NORMAL : DMG_EXPLODE))) + P_DamageMobj(thing, grenade, grenade->target, 1, (explodespin ? DMG_NORMAL : DMG_EXPLODE)); + + lagadded = (thing->hitlag - oldhitlag); + + if (lagadded > 0) { - minehitlag = thing->hitlag; + minehitlag = lagadded; } return BMIT_CONTINUE; @@ -353,7 +360,8 @@ boolean K_MineCollide(mobj_t *t1, mobj_t *t2) } } else if (t2->type == MT_ORBINAUT || t2->type == MT_JAWZ - || t2->type == MT_ORBINAUT_SHIELD || t2->type == MT_JAWZ_SHIELD) + || t2->type == MT_ORBINAUT_SHIELD || t2->type == MT_JAWZ_SHIELD + || t2->type == MT_GACHABOM) { // Bomb death angle_t bounceangle = K_GetCollideAngle(t1, t2); @@ -391,6 +399,8 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2) if (t2->player) { + const INT32 oldhitlag = t2->hitlag; + if (t2->player->flashing) return true; @@ -402,6 +412,7 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2) { // Melt item S_StartSound(t2, sfx_s3k43); + K_SetHitLagForObjects(t2, t1, 3, false); } else { @@ -409,13 +420,13 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2) P_DamageMobj(t2, t1, t1->target, 1, DMG_TUMBLE); } - t1->reactiontime = t2->hitlag; + t1->reactiontime = (t2->hitlag - oldhitlag); P_KillMobj(t1, t2, t2, DMG_NORMAL); } else if (t2->type == MT_BANANA || t2->type == MT_BANANA_SHIELD || t2->type == MT_ORBINAUT || t2->type == MT_ORBINAUT_SHIELD || t2->type == MT_JAWZ || t2->type == MT_JAWZ_SHIELD - || t2->type == MT_BALLHOG) + || t2->type == MT_BALLHOG || t2->type == MT_GACHABOM) { // Other Item Damage angle_t bounceangle = K_GetCollideAngle(t1, t2); diff --git a/src/k_follower.c b/src/k_follower.c index 636620ec8..0a8869077 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -11,6 +11,7 @@ #include "r_skins.h" #include "p_local.h" #include "p_mobj.h" +#include "m_cond.h" INT32 numfollowers = 0; follower_t followers[MAXSKINS]; @@ -38,6 +39,50 @@ INT32 K_FollowerAvailable(const char *name) return -1; } +/*-------------------------------------------------- + INT32 K_FollowerUsable(INT32 followernum) + + See header file for description. +--------------------------------------------------*/ +boolean K_FollowerUsable(INT32 skinnum) +{ + // Unlike R_SkinUsable, not netsynced. + // Solely used to prevent an invalid value being sent over the wire. + UINT8 i; + INT32 fid; + + if (skinnum == -1 || demo.playback) + { + // Simplifies things elsewhere, since there's already plenty of checks for less-than-0... + return true; + } + + // Determine if this follower is supposed to be unlockable or not + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (unlockables[i].type != SECRET_FOLLOWER) + continue; + + fid = M_UnlockableFollowerNum(&unlockables[i]); + + if (fid != skinnum) + continue; + + // i is now the unlockable index, we can use this later + break; + } + + if (i == MAXUNLOCKABLES) + { + // Didn't trip anything, so we can use this follower. + return true; + } + + // Use the unlockables table directly + // DEFINITELY not M_CheckNetUnlockByID + return (boolean)(gamedata->unlocked[i]); +} + /*-------------------------------------------------- boolean K_SetFollowerByName(INT32 playernum, const char *skinname) diff --git a/src/k_follower.h b/src/k_follower.h index 13c67bb11..a5f5d54a8 100644 --- a/src/k_follower.h +++ b/src/k_follower.h @@ -115,6 +115,22 @@ extern followercategory_t followercategories[MAXFOLLOWERCATEGORIES]; INT32 K_FollowerAvailable(const char *name); +/*-------------------------------------------------- + boolean K_FollowerUsable(INT32 followernum); + + Check if a follower is usable or not. + + Input Arguments:- + skinnum - The follower's skin ID + + Return:- + true if it was a valid follower, + otherwise false. +--------------------------------------------------*/ + +boolean K_FollowerUsable(INT32 skinnum); + + /*-------------------------------------------------- boolean K_SetFollowerByName(INT32 playernum, const char *skinname) diff --git a/src/k_grandprix.c b/src/k_grandprix.c index 6149434c7..221e9d10c 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -231,9 +231,9 @@ void K_InitGrandPrixBots(void) // Rearrange usable bot skins list to prevent gaps for randomised selection for (i = 0; i < usableskins; i++) { - if (!(grabskins[i] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/)) + if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true))) continue; - while (usableskins > i && (grabskins[usableskins] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/)) + while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins], true))) { usableskins--; } @@ -557,9 +557,9 @@ void K_RetireBots(void) // Rearrange usable bot skins list to prevent gaps for randomised selection for (i = 0; i < usableskins; i++) { - if (!(grabskins[i] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/)) + if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true))) continue; - while (usableskins > i && (grabskins[usableskins] == MAXSKINS /*|| K_SkinLocked(grabskins[i])*/)) + while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins], true))) usableskins--; grabskins[i] = grabskins[usableskins]; grabskins[usableskins] = MAXSKINS; diff --git a/src/k_hud.c b/src/k_hud.c index 8d9167a4e..8da9ca089 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -70,7 +70,7 @@ static patch_t *kp_racefinish[6]; static patch_t *kp_positionnum[10][2][2]; // number, overlay or underlay, splitscreen static patch_t *kp_facenum[MAXPLAYERS+1]; -static patch_t *kp_facehighlight[8]; +patch_t *kp_facehighlight[8]; static patch_t *kp_nocontestminimap; static patch_t *kp_spbminimap; @@ -173,6 +173,11 @@ static patch_t *kp_bossret[4]; static patch_t *kp_trickcool[2]; +static patch_t *kp_capsuletarget_arrow[2][2]; +static patch_t *kp_capsuletarget_icon[2]; +static patch_t *kp_capsuletarget_far[2]; +static patch_t *kp_capsuletarget_near[8]; + void K_LoadKartHUDGraphics(void) { INT32 i, j, k; @@ -644,6 +649,39 @@ void K_LoadKartHUDGraphics(void) buffer[7] = '0'+((i+1)%10); HU_UpdatePatch(&kp_bossret[i], "%s", buffer); } + + sprintf(buffer, "HCAPARxx"); + for (i = 0; i < 2; i++) + { + buffer[6] = 'A'+i; + + for (j = 0; j < 2; j++) + { + buffer[7] = '0'+j; + HU_UpdatePatch(&kp_capsuletarget_arrow[i][j], "%s", buffer); + } + } + + sprintf(buffer, "HUDCAPCx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_icon[i], "%s", buffer); + } + + sprintf(buffer, "HUDCAPBx"); + for (i = 0; i < 2; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_far[i], "%s", buffer); + } + + sprintf(buffer, "HUDCAPAx"); + for (i = 0; i < 8; i++) + { + buffer[7] = '0'+i; + HU_UpdatePatch(&kp_capsuletarget_near[i], "%s", buffer); + } } // For the item toggle menu @@ -699,6 +737,9 @@ const char *K_GetItemPatch(UINT8 item, boolean tiny) return (tiny ? "K_ISDTRG" : "K_ITDTRG"); case KITEM_GARDENTOP: return (tiny ? "K_ISGTOP" : "K_ITGTOP"); + case KITEM_GACHABOM: // temp + case KRITEM_TRIPLEGACHABOM: // temp + return (tiny ? "K_ISSINK" : "K_ITSINK"); case KRITEM_TRIPLEORBINAUT: return (tiny ? "K_ISORBN" : "K_ITORB3"); case KRITEM_QUADORBINAUT: @@ -735,6 +776,7 @@ static patch_t *K_GetCachedItemPatch(INT32 item, UINT8 offset) kp_kitchensink, kp_droptarget, kp_gardentop, + kp_kitchensink, // temp }; if (item == KITEM_SAD || (item > KITEM_NONE && item < NUMKARTITEMS)) @@ -778,7 +820,7 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, boolean revers fixed_t fovDiff, fov, fovTangent, fg; fixed_t h; - INT32 da; + INT32 da, dp; UINT8 cameraNum = R_GetViewNumber(); @@ -839,6 +881,7 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, boolean revers // Determine viewpoint factors. h = R_PointToDist2(point->x, point->y, viewx, viewy); da = AngleDeltaSigned(viewpointAngle, R_PointToAngle2(point->x, point->y, viewx, viewy)); + dp = AngleDeltaSigned(viewpointAiming, R_PointToAngle2(0, 0, h, viewz)); if (reverse) { @@ -849,6 +892,10 @@ void K_ObjectTracking(trackingResult_t *result, vector3_t *point, boolean revers result->x = FixedMul(NEWTAN(da), fg); result->y = FixedMul((NEWTAN(viewpointAiming) - FixedDiv((viewz - point->z), 1 + FixedMul(NEWCOS(da), h))), fg); + result->angle = da; + result->pitch = dp; + result->fov = fg; + // Rotate for screen roll... if (viewpointRoll) { @@ -1014,6 +1061,40 @@ static void K_initKartHUD(void) } } +void K_DrawMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, UINT16 map, UINT8 *colormap) +{ + patch_t *PictureOfLevel = NULL; + + if (map >= nummapheaders || !mapheaderinfo[map]) + { + PictureOfLevel = nolvl; + } + else if (!mapheaderinfo[map]->thumbnailPic) + { + PictureOfLevel = blanklvl; + } + else + { + PictureOfLevel = mapheaderinfo[map]->thumbnailPic; + } + + K_DrawLikeMapThumbnail(x, y, width, flags, PictureOfLevel, colormap); +} + +void K_DrawLikeMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, patch_t *patch, UINT8 *colormap) +{ + if (flags & V_FLIP) + x += width; + + V_DrawFixedPatch( + x, y, + FixedDiv(width, (320 << FRACBITS)), + flags, + patch, + colormap + ); +} + // see also MT_PLAYERARROW mobjthinker in p_mobj.c static void K_drawKartItem(void) { @@ -1465,7 +1546,7 @@ void K_drawKartTimestamp(tic_t drawtime, INT32 TX, INT32 TY, INT16 emblemmap, UI static boolean canplaysound = true; tic_t timetoreach = emblem->var; - if (emblem->collected) + if (gamedata->collected[(emblem-emblemlocations)]) { emblempic[curemb] = W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_CACHE); emblemcol[curemb] = R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_CACHE); @@ -3041,6 +3122,217 @@ static void K_DrawWeakSpot(weakspotdraw_t *ws) V_DrawFixedPatch(ws->x, ws->y, FRACUNIT, 0, kp_bossret[j+1], colormap); } +typedef struct capsuletracking_s +{ + mobj_t *mobj; + vector3_t point; + fixed_t camDist; +} capsuletracking_t; + +static void K_DrawCapsuleTracking(capsuletracking_t *caps) +{ + trackingResult_t result = {0}; + INT32 timer = 0; + + K_ObjectTracking(&result, &caps->point, false); + + if (result.onScreen == false) + { + // Off-screen, draw alongside the borders of the screen. + // Probably the most complicated thing. + + INT32 scrVal = 240; + vector2_t screenSize = {0}; + + INT32 borderSize = 7; + vector2_t borderWin = {0}; + vector2_t borderDir = {0}; + fixed_t borderLen = FRACUNIT; + + vector2_t arrowDir = {0}; + + vector2_t arrowPos = {0}; + patch_t *arrowPatch = NULL; + INT32 arrowFlags = 0; + + vector2_t capsulePos = {0}; + patch_t *capsulePatch = NULL; + + timer = (leveltime / 3); + + screenSize.x = vid.width / vid.dupx; + screenSize.y = vid.height / vid.dupy; + + if (r_splitscreen >= 2) + { + // Half-wide screens + screenSize.x >>= 1; + borderSize >>= 1; + } + + if (r_splitscreen >= 1) + { + // Half-tall screens + screenSize.y >>= 1; + } + + scrVal = max(screenSize.x, screenSize.y) - 80; + + borderWin.x = screenSize.x - borderSize; + borderWin.y = screenSize.y - borderSize; + + arrowDir.x = 0; + arrowDir.y = P_MobjFlip(caps->mobj) * FRACUNIT; + + // Simply pointing towards the result doesn't work, so inaccurate hack... + borderDir.x = FixedMul( + FixedMul( + FINESINE((-result.angle >> ANGLETOFINESHIFT) & FINEMASK), + FINECOSINE((-result.pitch >> ANGLETOFINESHIFT) & FINEMASK) + ), + result.fov + ); + + borderDir.y = FixedMul( + FINESINE((-result.pitch >> ANGLETOFINESHIFT) & FINEMASK), + result.fov + ); + + borderLen = R_PointToDist2(0, 0, borderDir.x, borderDir.y); + + if (borderLen > 0) + { + borderDir.x = FixedDiv(borderDir.x, borderLen); + borderDir.y = FixedDiv(borderDir.y, borderLen); + } + else + { + // Eh just put it at the bottom. + borderDir.x = 0; + borderDir.y = FRACUNIT; + } + + capsulePatch = kp_capsuletarget_icon[timer & 1]; + + if (abs(borderDir.x) > abs(borderDir.y)) + { + // Horizontal arrow + arrowPatch = kp_capsuletarget_arrow[1][timer & 1]; + arrowDir.y = 0; + + if (borderDir.x < 0) + { + // LEFT + arrowDir.x = -FRACUNIT; + } + else + { + // RIGHT + arrowDir.x = FRACUNIT; + } + } + else + { + // Vertical arrow + arrowPatch = kp_capsuletarget_arrow[0][timer & 1]; + arrowDir.x = 0; + + if (borderDir.y < 0) + { + // UP + arrowDir.y = -FRACUNIT; + } + else + { + // DOWN + arrowDir.y = FRACUNIT; + } + } + + arrowPos.x = (screenSize.x >> 1) + FixedMul(scrVal, borderDir.x); + arrowPos.y = (screenSize.y >> 1) + FixedMul(scrVal, borderDir.y); + + arrowPos.x = min(max(arrowPos.x, borderSize), borderWin.x) * FRACUNIT; + arrowPos.y = min(max(arrowPos.y, borderSize), borderWin.y) * FRACUNIT; + + capsulePos.x = arrowPos.x - (arrowDir.x * 12); + capsulePos.y = arrowPos.y - (arrowDir.y * 12); + + arrowPos.x -= (arrowPatch->width << FRACBITS) >> 1; + arrowPos.y -= (arrowPatch->height << FRACBITS) >> 1; + + capsulePos.x -= (capsulePatch->width << FRACBITS) >> 1; + capsulePos.y -= (capsulePatch->height << FRACBITS) >> 1; + + if (arrowDir.x < 0) + { + arrowPos.x += arrowPatch->width << FRACBITS; + arrowFlags |= V_FLIP; + } + + if (arrowDir.y < 0) + { + arrowPos.y += arrowPatch->height << FRACBITS; + arrowFlags |= V_VFLIP; + } + + V_DrawFixedPatch( + capsulePos.x, capsulePos.y, + FRACUNIT, + V_SPLITSCREEN, + capsulePatch, NULL + ); + + V_DrawFixedPatch( + arrowPos.x, arrowPos.y, + FRACUNIT, + V_SPLITSCREEN | arrowFlags, + arrowPatch, NULL + ); + } + else + { + // Draw simple overlay. + const fixed_t farDistance = 1280*mapobjectscale; + boolean useNear = (caps->camDist < farDistance); + + patch_t *capsulePatch = NULL; + vector2_t capsulePos = {0}; + + boolean visible = P_CheckSight(stplyr->mo, caps->mobj); + + if (visible == false && (leveltime & 1)) + { + // Flicker when not visible. + return; + } + + capsulePos.x = result.x; + capsulePos.y = result.y; + + if (useNear == true) + { + timer = (leveltime / 2); + capsulePatch = kp_capsuletarget_near[timer % 8]; + } + else + { + timer = (leveltime / 3); + capsulePatch = kp_capsuletarget_far[timer & 1]; + } + + capsulePos.x -= (capsulePatch->width << FRACBITS) >> 1; + capsulePos.y -= (capsulePatch->height << FRACBITS) >> 1; + + V_DrawFixedPatch( + capsulePos.x, capsulePos.y, + FRACUNIT, + V_SPLITSCREEN, + capsulePatch, NULL + ); + } +} + static void K_drawKartNameTags(void) { const fixed_t maxdistance = 8192*mapobjectscale; @@ -3049,7 +3341,7 @@ static void K_drawKartNameTags(void) UINT8 tobesorted[MAXPLAYERS]; fixed_t sortdist[MAXPLAYERS]; UINT8 sortlen = 0; - UINT8 i, j; + size_t i, j; if (stplyr == NULL || stplyr->mo == NULL || P_MobjWasRemoved(stplyr->mo)) { @@ -3140,6 +3432,85 @@ static void K_drawKartNameTags(void) } } + if (battlecapsules == true) + { +#define MAX_CAPSULE_HUD 32 + capsuletracking_t capsuleList[MAX_CAPSULE_HUD]; + size_t capsuleListLen = 0; + + mobj_t *mobj = NULL; + mobj_t *next = NULL; + + for (mobj = kitemcap; mobj; mobj = next) + { + capsuletracking_t *caps = NULL; + + next = mobj->itnext; + + if (mobj->health <= 0) + { + continue; + } + + if (mobj->type != MT_BATTLECAPSULE) + { + continue; + } + + caps = &capsuleList[capsuleListLen]; + + caps->mobj = mobj; + caps->point.x = R_InterpolateFixed(mobj->old_x, mobj->x); + caps->point.y = R_InterpolateFixed(mobj->old_y, mobj->y); + caps->point.z = R_InterpolateFixed(mobj->old_z, mobj->z); + caps->point.z += (mobj->height >> 1); + caps->camDist = R_PointToDist2(c.x, c.y, caps->point.x, caps->point.y); + + capsuleListLen++; + + if (capsuleListLen >= MAX_CAPSULE_HUD) + { + break; + } + } + + if (capsuleListLen > 0) + { + // Sort by distance from camera. + if (capsuleListLen > 1) + { + for (i = 0; i < capsuleListLen-1; i++) + { + size_t swap = i; + + for (j = i + 1; j < capsuleListLen; j++) + { + capsuletracking_t *cj = &capsuleList[j]; + capsuletracking_t *cSwap = &capsuleList[swap]; + + if (cj->camDist > cSwap->camDist) + { + swap = j; + } + } + + if (swap != i) + { + capsuletracking_t temp = capsuleList[swap]; + capsuleList[swap] = capsuleList[i]; + capsuleList[i] = temp; + } + } + } + + for (i = 0; i < capsuleListLen; i++) + { + K_DrawCapsuleTracking(&capsuleList[i]); + } + } +#undef MAX_CAPSULE_HUD + } + for (i = 0; i < MAXPLAYERS; i++) { player_t *ntplayer = &players[i]; @@ -3254,7 +3625,7 @@ static void K_drawKartNameTags(void) { if (!(demo.playback == true && demo.freecam == true)) { - for (j = 0; j <= r_splitscreen; j++) + for (j = 0; j <= (unsigned)r_splitscreen; j++) { if (ntplayer == &players[displayplayers[j]]) { @@ -3262,7 +3633,7 @@ static void K_drawKartNameTags(void) } } - if (j <= r_splitscreen && j != cnum) + if (j <= (unsigned)r_splitscreen && j != cnum) { localindicator = j; } diff --git a/src/k_hud.h b/src/k_hud.h index 41515a432..09420503f 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -26,6 +26,8 @@ struct trackingResult_t fixed_t x, y; fixed_t scale; boolean onScreen; + INT32 angle, pitch; + fixed_t fov; }; void K_ObjectTracking(trackingResult_t *result, vector3_t *point, boolean reverse); @@ -36,5 +38,9 @@ void K_drawKartHUD(void); void K_drawKartFreePlay(void); void K_drawKartTimestamp(tic_t drawtime, INT32 TX, INT32 TY, INT16 emblemmap, UINT8 mode); void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, INT32 whiteplayer, INT32 hilicol); +void K_DrawMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, UINT16 map, UINT8 *colormap); +void K_DrawLikeMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, patch_t *patch, UINT8 *colormap); + +extern patch_t *kp_facehighlight[8]; #endif diff --git a/src/k_kart.c b/src/k_kart.c index cc7b1453f..f1c0b8be2 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -436,6 +436,9 @@ SINT8 K_ItemResultToType(SINT8 getitem) case KRITEM_DUALJAWZ: return KITEM_JAWZ; + case KRITEM_TRIPLEGACHABOM: + return KITEM_GACHABOM; + default: I_Error("Bad item cooldown redirect for result %d\n", getitem); break; @@ -456,6 +459,7 @@ UINT8 K_ItemResultToAmount(SINT8 getitem) case KRITEM_TRIPLESNEAKER: case KRITEM_TRIPLEBANANA: case KRITEM_TRIPLEORBINAUT: + case KRITEM_TRIPLEGACHABOM: return 3; case KRITEM_QUADORBINAUT: @@ -629,6 +633,7 @@ fixed_t K_GetMobjWeight(mobj_t *mobj, mobj_t *against) break; case MT_ORBINAUT: case MT_ORBINAUT_SHIELD: + case MT_GACHABOM: case MT_DUELBOMB: if (against->player) weight = K_PlayerWeight(against, NULL); @@ -4428,7 +4433,7 @@ static mobj_t *K_SpawnKartMissile(mobj_t *source, mobjtype_t type, angle_t an, I finalscale = source->scale; } - if (dir == -1 && (type == MT_ORBINAUT || type == MT_BALLHOG)) + if (dir == -1 && (type == MT_ORBINAUT || type == MT_GACHABOM || type == MT_BALLHOG)) { // Backwards nerfs finalspeed /= 8; @@ -4485,6 +4490,7 @@ static mobj_t *K_SpawnKartMissile(mobj_t *source, mobjtype_t type, angle_t an, I switch (type) { case MT_ORBINAUT: + case MT_GACHABOM: Obj_OrbinautThrown(th, finalspeed, dir); break; case MT_JAWZ: @@ -5332,6 +5338,12 @@ mobj_t *K_ThrowKartItem(player_t *player, boolean missile, mobjtype_t mapthing, dir = defaultDir; } + if (mapthing == MT_GACHABOM && dir > 0) + { + // This item is both a missile and not! + missile = false; + } + if (missile) // Shootables { if (dir < 0 && mapthing != MT_SPB && mapthing != MT_GARDENTOP) @@ -5399,6 +5411,16 @@ mobj_t *K_ThrowKartItem(player_t *player, boolean missile, mobjtype_t mapthing, mo->rollangle = FixedAngle(P_RandomRange(PR_DECORATION, -180, 180) << FRACBITS); } + if (mapthing == MT_GACHABOM) + { + // Set dropped flag + mo->flags2 |= MF2_AMBUSH; + mo->movecount = 2; + P_SetMobjState(mo, mo->info->deathstate); + mo->tics = -1; + mo->color = player->skincolor; + } + // this is the small graphic effect that plops in you when you throw an item: throwmo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height/2, MT_FIREDITEM); P_SetTarget(&throwmo->target, player->mo); @@ -8103,6 +8125,32 @@ void K_KartPlayerAfterThink(player_t *player) { K_LookForRings(player->mo); } + + if (player->invulnhitlag > 0) + { + // Hitlag from what would normally be damage but the + // player was invulnerable. + // + // If we're constantly getting hit the same number of + // times, we're probably standing on a damage floor. + // + // Checking if we're hit more than before ensures + // that: + // + // 1) repeating damage doesn't count + // 2) new damage sources still count + + if (player->timeshit <= player->timeshitprev) + { + if (!P_MobjWasRemoved(player->mo)) + { + player->mo->hitlag -= player->invulnhitlag; + player->mo->eflags &= ~(MFE_DAMAGEHITLAG); + } + } + + player->invulnhitlag = 0; + } } /*-------------------------------------------------- @@ -10623,6 +10671,15 @@ void K_MoveKartPlayer(player_t *player, boolean onground) K_UpdateHnextList(player, true); } break; + case KITEM_GACHABOM: + if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO) + { + K_ThrowKartItem(player, true, MT_GACHABOM, 0, 0, 0); + K_PlayAttackTaunt(player->mo); + player->itemamount--; + K_UpdateHnextList(player, false); + } + break; case KITEM_SAD: if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO && !player->sadtimer) diff --git a/src/k_menu.h b/src/k_menu.h index 7623a19e7..eb57069b7 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -398,12 +398,16 @@ extern menu_t EXTRAS_ReplayStartDef; extern menuitem_t PAUSE_Main[]; extern menu_t PAUSE_MainDef; +// EXTRAS +extern menuitem_t MISC_Manual[]; +extern menu_t MISC_ManualDef; + extern menuitem_t MISC_Addons[]; extern menu_t MISC_AddonsDef; -// MANUAL -extern menuitem_t MISC_Manual[]; -extern menu_t MISC_ManualDef; +extern menuitem_t MISC_ChallengesStatsDummyMenu[]; +extern menu_t MISC_ChallengesDef; +extern menu_t MISC_StatisticsDef; // We'll need this since we're gonna have to dynamically enable and disable options depending on which state we're in. typedef enum @@ -593,6 +597,9 @@ extern struct setup_chargrid_s { UINT8 numskins; } setup_chargrid[9][9]; +extern UINT8 setup_followercategories[MAXFOLLOWERCATEGORIES][2]; +extern UINT8 setup_numfollowercategories; + typedef enum { CSSTEP_NONE = 0, @@ -650,9 +657,9 @@ extern UINT8 setup_maxpage; #define CSEXPLOSIONS 48 extern struct setup_explosions_s { - UINT8 x, y; + INT16 x, y; UINT8 tics; - UINT8 color; + UINT16 color; } setup_explosions[CSEXPLOSIONS]; typedef enum @@ -688,23 +695,28 @@ extern struct cupgrid_s { boolean netgame; // Start the game in an actual server } cupgrid; +typedef struct levelsearch_s { + UINT32 typeoflevel; + cupheader_t *cup; + boolean timeattack; + boolean cupmode; + boolean checklocked; +} levelsearch_t; + extern struct levellist_s { SINT8 cursor; UINT16 y; UINT16 dest; - cupheader_t *selectedcup; INT16 choosemap; UINT8 newgametype; - UINT32 typeoflevel; - boolean cupmode; - boolean timeattack; // Setup time attack menu after picking + levelsearch_t levelsearch; boolean netgame; // Start the game in an actual server } levellist; -boolean M_CanShowLevelInList(INT16 mapnum, UINT32 tol, cupheader_t *cup); -UINT16 M_CountLevelsToShowInList(UINT32 tol, cupheader_t *cup); -UINT16 M_GetFirstLevelInList(UINT8 *i, UINT32 tol, cupheader_t *cup); -UINT16 M_GetNextLevelInList(UINT16 map, UINT8 *i, UINT32 tol, cupheader_t *cup); +boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch); +UINT16 M_CountLevelsToShowInList(levelsearch_t *levelsearch); +UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch); +UINT16 M_GetNextLevelInList(UINT16 mapnum, UINT8 *i, levelsearch_t *levelsearch); void M_LevelSelectInit(INT32 choice); void M_CupSelectHandler(INT32 choice); @@ -1081,6 +1093,59 @@ void M_DrawReplayStartMenu(void); #define LOCATIONSTRING2 "Visit \x88SRB2.ORG/MODS\x80 to get & make addons!" void M_DrawAddons(void); +// Challenges menu: +#define UNLOCKTIME 5 +#define MAXUNLOCKTIME TICRATE +#define RIGHTUNLOCKSCROLL 3 +#define LEFTUNLOCKSCROLL (RIGHTUNLOCKSCROLL-1) + +#define CC_TOTAL 0 +#define CC_UNLOCKED 1 +#define CC_TALLY 2 +#define CC_ANIM 3 +#define CC_MAX 4 + +// Keep track of some pause menu data for visual goodness. +extern struct challengesmenu_s { + + tic_t ticker; // How long the menu's been open for + INT16 offset; // To make the icons move smoothly when we transition! + + UINT8 currentunlock; + char *unlockcondition; + + tic_t unlockanim; + + SINT8 row, hilix, focusx; + UINT8 col, hiliy; + + UINT8 *extradata; + + boolean pending; + boolean requestnew; + + UINT8 unlockcount[CC_MAX]; + + UINT8 fade; +} challengesmenu; + +menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu); +void M_Challenges(INT32 choice); +void M_DrawChallenges(void); +void M_ChallengesTick(void); +boolean M_ChallengesInputs(INT32 ch); + +extern struct statisticsmenu_s { + INT32 location; + INT32 nummaps; + INT32 maxscroll; + UINT16 *maplist; +} statisticsmenu; + +void M_Statistics(INT32 choice); +void M_DrawStatistics(void); +boolean M_StatisticsInputs(INT32 ch); + // These defines make it a little easier to make menus #define DEFAULTMENUSTYLE(source, prev, x, y)\ {\ diff --git a/src/k_menudef.c b/src/k_menudef.c index f080ff4e5..99903d788 100644 --- a/src/k_menudef.c +++ b/src/k_menudef.c @@ -1496,14 +1496,14 @@ menuitem_t EXTRAS_Main[] = {IT_STRING | IT_CALL, "Addons", "Add files to customize your experience.", 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, {NULL}, 0, 0}, - - {IT_STRING | IT_TRANSTEXT, "Extras Checklist", "View the requirement for some of the secret content you can unlock!", - NULL, {NULL}, 0, 0}, + NULL, {.routine = M_Statistics}, 0, 0}, }; // the extras menu essentially reuses the options menu stuff @@ -1744,3 +1744,39 @@ menu_t MISC_AddonsDef = { NULL, NULL }; + +// Challenges. +menuitem_t MISC_ChallengesStatsDummyMenu[] = +{ + {IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0}, +}; + +menu_t MISC_ChallengesDef = { + sizeof (MISC_ChallengesStatsDummyMenu)/sizeof (menuitem_t), + &MainDef, + 0, + MISC_ChallengesStatsDummyMenu, + BASEVIDWIDTH/2, 32, + 0, 0, + 98, 0, + M_DrawChallenges, + M_ChallengesTick, + NULL, + NULL, + M_ChallengesInputs, +}; + +menu_t MISC_StatisticsDef = { + sizeof (MISC_ChallengesStatsDummyMenu)/sizeof (menuitem_t), + &MainDef, + 0, + MISC_ChallengesStatsDummyMenu, + 280, 185, + 0, 0, + 98, 0, + M_DrawStatistics, + NULL, + NULL, + NULL, + M_StatisticsInputs, +}; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 729f99464..9071907bc 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -600,10 +600,10 @@ static void M_DrawMenuTooltips(void) // Used for the secrets menu, to hide yet-to-be-unlocked stuff. static const char *M_CreateSecretMenuOption(const char *str) { - static char qbuf[32]; + static char qbuf[64]; int i; - for (i = 0; i < 31; ++i) + for (i = 0; i < 63; ++i) { if (!str[i]) { @@ -616,7 +616,7 @@ static const char *M_CreateSecretMenuOption(const char *str) qbuf[i] = ' '; } - qbuf[31] = '\0'; + qbuf[63] = '\0'; return qbuf; } @@ -981,10 +981,10 @@ static void M_DrawCharSelectCircle(setup_player_t *p, INT16 x, INT16 y) numoptions = nummenucolors; break; case CSSTEP_FOLLOWERCATEGORY: - numoptions = numfollowercategories+1; + numoptions = setup_numfollowercategories+1; break; case CSSTEP_FOLLOWER: - numoptions = followercategories[p->followercategory].numincategory; + numoptions = setup_followercategories[p->followercategory][0]; break; case CSSTEP_FOLLOWERCOLORS: numoptions = nummenucolors+2; @@ -1084,7 +1084,7 @@ static void M_DrawCharSelectCircle(setup_player_t *p, INT16 x, INT16 y) } else { - fc = &followercategories[n - 1]; + fc = &followercategories[setup_followercategories[n - 1][1]]; patch = W_CachePatchName(fc->icon, PU_CACHE); } @@ -1111,7 +1111,7 @@ static void M_DrawCharSelectCircle(setup_player_t *p, INT16 x, INT16 y) n = numfollowers-1; if (n == startfollowern) break; - if (followers[n].category == p->followercategory) + if (followers[n].category == setup_followercategories[p->followercategory][1]) r--; } l = r = n; @@ -1126,7 +1126,7 @@ static void M_DrawCharSelectCircle(setup_player_t *p, INT16 x, INT16 y) if (l == startfollowern) break; } - while (followers[l].category != p->followercategory); + while (followers[l].category != setup_followercategories[p->followercategory][1]); n = l; } else @@ -1139,7 +1139,7 @@ static void M_DrawCharSelectCircle(setup_player_t *p, INT16 x, INT16 y) if (r == startfollowern) break; } - while (followers[r].category != p->followercategory); + while (followers[r].category != setup_followercategories[p->followercategory][1]); n = r; } @@ -1266,7 +1266,7 @@ static boolean M_DrawCharacterSprite(INT16 x, INT16 y, INT16 skin, boolean charf // Returns false is the follower shouldn't be rendered. // 'num' can be used to directly specify the follower number, but doing this will not animate it. // if a setup_player_t is specified instead, its data will be used to animate the follower sprite. -static boolean M_DrawFollowerSprite(INT16 x, INT16 y, INT32 num, boolean charflip, INT32 addflags, UINT16 color, setup_player_t *p) +static boolean M_DrawFollowerSprite(INT16 x, INT16 y, INT32 num, boolean charflip, INT32 addflags, UINT8 *colormap, setup_player_t *p) { spritedef_t *sprdef; @@ -1276,7 +1276,6 @@ static boolean M_DrawFollowerSprite(INT16 x, INT16 y, INT32 num, boolean charfli state_t *usestate; UINT32 useframe; follower_t fl; - UINT8 *colormap = NULL; UINT8 rotation = (charflip ? 1 : 7); if (p != NULL) @@ -1320,13 +1319,13 @@ static boolean M_DrawFollowerSprite(INT16 x, INT16 y, INT32 num, boolean charfli if (p != NULL) { - sine = FixedMul(fl.bobamp, FINESINE(((FixedMul(4 * M_TAU_FIXED, fl.bobspeed) * p->follower_timer)>>ANGLETOFINESHIFT) & FINEMASK)); - color = K_GetEffectiveFollowerColor( + UINT16 color = K_GetEffectiveFollowerColor( (p->mdepth < CSSTEP_FOLLOWERCOLORS) ? fl.defaultcolor : p->followercolor, p->color); + sine = FixedMul(fl.bobamp, FINESINE(((FixedMul(4 * M_TAU_FIXED, fl.bobspeed) * p->follower_timer)>>ANGLETOFINESHIFT) & FINEMASK)); + colormap = R_GetTranslationColormap(TC_DEFAULT, color, GTC_MENUCACHE); } - colormap = R_GetTranslationColormap(TC_DEFAULT, color, GTC_MENUCACHE); V_DrawFixedPatch((x*FRACUNIT), ((y-12)*FRACUNIT) + sine, fl.scale, addflags, patch, colormap); return true; @@ -1481,13 +1480,13 @@ static void M_DrawCharSelectPreview(UINT8 num) } } -static void M_DrawCharSelectExplosions(void) +static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey) { UINT8 i; + INT16 quadx = 0, quady = 0; for (i = 0; i < CSEXPLOSIONS; i++) { - INT16 quadx, quady; UINT8 *colormap; UINT8 frame; @@ -1496,14 +1495,17 @@ static void M_DrawCharSelectExplosions(void) frame = 6 - setup_explosions[i].tics; - quadx = 4 * (setup_explosions[i].x / 3); - quady = 4 * (setup_explosions[i].y / 3); + if (charsel) + { + quadx = 4 * (setup_explosions[i].x / 3); + quady = 4 * (setup_explosions[i].y / 3); + } colormap = R_GetTranslationColormap(TC_DEFAULT, setup_explosions[i].color, GTC_MENUCACHE); V_DrawMappedPatch( - 82 + (setup_explosions[i].x*16) + quadx - 6, - 22 + (setup_explosions[i].y*16) + quady - 6, + basex + (setup_explosions[i].x*16) + quadx - 6, + basey + (setup_explosions[i].y*16) + quady - 6, 0, W_CachePatchName(va("CHCNFRM%d", frame), PU_CACHE), colormap ); @@ -1537,6 +1539,9 @@ static void M_DrawCharSelectCursor(UINT8 num) INT16 x, y; INT16 quadx, quady; + if (p->mdepth < CSSTEP_ASKCHANGES) + return; + quadx = 4 * (p->gridx / 3); quady = 4 * (p->gridy / 3); @@ -1580,6 +1585,7 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p) patch_t *card = W_CachePatchName("PR_CARD", PU_CACHE); patch_t *cardbot = W_CachePatchName("PR_CARDB", PU_CACHE); patch_t *pwrlv = W_CachePatchName("PR_PWR", PU_CACHE); + UINT16 truecol = SKINCOLOR_BLACK; UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_BLACK, GTC_CACHE); INT32 skinnum = -1; INT32 powerlevel = -1; @@ -1588,7 +1594,8 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p) if (p != NULL && p->version) { - colormap = R_GetTranslationColormap(TC_DEFAULT, PR_GetProfileColor(p), GTC_CACHE); + truecol = PR_GetProfileColor(p); + colormap = R_GetTranslationColormap(TC_DEFAULT, truecol, GTC_CACHE); strcpy(pname, p->profilename); skinnum = R_SkinAvailable(p->skinname); powerlevel = p->powerlevels[0]; // Only display race power level. @@ -1597,7 +1604,10 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p) // check setup_player for colormap for the card. // we'll need to check again for drawing afterwards unfortunately. if (sp->mdepth >= CSSTEP_CHARS) + { + truecol = PR_GetProfileColor(p); colormap = R_GetTranslationColormap(skinnum, sp->color, GTC_MENUCACHE); + } // Card V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, greyedout ? V_TRANSLUCENT : 0, card, colormap); @@ -1631,25 +1641,34 @@ static void M_DrawProfileCard(INT32 x, INT32 y, boolean greyedout, profile_t *p) { UINT16 col = K_GetEffectiveFollowerColor(sp->followercolor, sp->color);; patch_t *ico = W_CachePatchName(followers[sp->followern].icon, PU_CACHE); - UINT8 *fcolormap; - - fcolormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + UINT8 *fcolormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); V_DrawMappedPatch(x+14+18, y+66, 0, ico, fcolormap); } } } else if (skinnum > -1) // otherwise, read from profile. { - UINT16 col = K_GetEffectiveFollowerColor(p->followercolor, p->color);; + UINT8 *ccolormap, *fcolormap; + UINT16 col = K_GetEffectiveFollowerColor(p->followercolor, p->color); UINT8 fln = K_FollowerAvailable(p->follower); - if (M_DrawCharacterSprite(x-22, y+119, skinnum, false, false, 0, colormap)) - V_DrawMappedPatch(x+14, y+66, 0, faceprefix[skinnum][FACE_RANK], colormap); + if (R_SkinUsable(g_localplayers[0], skinnum, false)) + ccolormap = colormap; + else + ccolormap = R_GetTranslationColormap(TC_BLINK, truecol, GTC_MENUCACHE); - if (M_DrawFollowerSprite(x-22 - 16, y+119, fln, false, 0, col, NULL)) + fcolormap = R_GetTranslationColormap( + (K_FollowerUsable(fln) ? TC_DEFAULT : TC_BLINK), + col, GTC_MENUCACHE); + + if (M_DrawCharacterSprite(x-22, y+119, skinnum, false, false, 0, ccolormap)) + { + V_DrawMappedPatch(x+14, y+66, 0, faceprefix[skinnum][FACE_RANK], ccolormap); + } + + if (M_DrawFollowerSprite(x-22 - 16, y+119, fln, false, 0, fcolormap, NULL)) { patch_t *ico = W_CachePatchName(followers[fln].icon, PU_CACHE); - UINT8 *fcolormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); V_DrawMappedPatch(x+14+18, y+66, 0, ico, fcolormap); } } @@ -1715,8 +1734,11 @@ void M_DrawCharacterSelect(void) { for (k = 0; k < setup_numplayers; k++) { - if (setup_player[k].gridx == i && setup_player[k].gridy == j) - break; // k == setup_numplayers means no one has it selected + if (setup_player[k].mdepth < CSSTEP_ASKCHANGES) + continue; + if (setup_player[k].gridx != i || setup_player[k].gridy != j) + continue; + break; // k == setup_numplayers means no one has it selected } skin = setup_chargrid[i][j].skinlist[setup_page]; @@ -1742,7 +1764,7 @@ void M_DrawCharacterSelect(void) } // Explosions when you've made your final selection - M_DrawCharSelectExplosions(); + M_DrawCharSelectExplosions(true, 82, 22); for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { @@ -1900,24 +1922,24 @@ void M_DrawRaceDifficulty(void) // LEVEL SELECT -static void M_DrawCupPreview(INT16 y, cupheader_t *cup) +static void M_DrawCupPreview(INT16 y, levelsearch_t *levelsearch) { UINT8 i = 0; - INT16 maxlevels = M_CountLevelsToShowInList(levellist.typeoflevel, cup); + INT16 maxlevels = M_CountLevelsToShowInList(levelsearch); INT16 x = -(cupgrid.previewanim % 82); INT16 add; - INT16 map, start = M_GetFirstLevelInList(&i, levellist.typeoflevel, cup); + INT16 map, start = M_GetFirstLevelInList(&i, levelsearch); UINT8 starti = i; V_DrawFill(0, y, BASEVIDWIDTH, 54, 31); - if (cup && (cup->unlockrequired == -1 || unlockables[cup->unlockrequired].unlocked)) + if (levelsearch->cup && !M_CupLocked(levelsearch->cup)) { add = (cupgrid.previewanim / 82) % maxlevels; map = start; while (add > 0) { - map = M_GetNextLevelInList(map, &i, levellist.typeoflevel, cup); + map = M_GetNextLevelInList(map, &i, levelsearch); if (map >= nummapheaders) { @@ -1928,26 +1950,22 @@ static void M_DrawCupPreview(INT16 y, cupheader_t *cup) } while (x < BASEVIDWIDTH) { - patch_t *PictureOfLevel = NULL; - if (map >= nummapheaders) { map = start; i = starti; } - if (map < nummapheaders && mapheaderinfo[map]) - { - PictureOfLevel = mapheaderinfo[map]->thumbnailPic; - } + K_DrawMapThumbnail( + (x+1)<unlockrequired == -1 || unlockables[cup->unlockrequired].unlocked); + boolean unlocked = !M_CupLocked(cup); UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE); patch_t *icon = W_CachePatchName(cup->icon, PU_CACHE); const char *str = (unlocked ? va("%s Cup", cup->name) : "???"); @@ -1991,7 +2009,8 @@ static void M_DrawCupTitle(INT16 y, cupheader_t *cup) void M_DrawCupSelect(void) { UINT8 i, j; - cupheader_t *cup = cupgrid.builtgrid[CUPMENU_CURSORID]; + levelsearch_t templevelsearch = levellist.levelsearch; // full copy + templevelsearch.cup = cupgrid.builtgrid[CUPMENU_CURSORID]; for (i = 0; i < CUPMENU_COLUMNS; i++) { @@ -2021,7 +2040,7 @@ void M_DrawCupSelect(void) V_DrawScaledPatch(x, y, 0, patch); - if (iconcup->unlockrequired != -1 && !unlockables[iconcup->unlockrequired].unlocked) + if (M_CupLocked(iconcup)) { patch_t *st = W_CachePatchName(va("ICONST0%d", (cupgrid.previewanim % 4) + 1), PU_CACHE); V_DrawScaledPatch(x + 8, y + icony, 0, st); @@ -2039,8 +2058,8 @@ void M_DrawCupSelect(void) 0, W_CachePatchName("CUPCURS", PU_CACHE) ); - M_DrawCupPreview(146 + (24*menutransition.tics), cup); - M_DrawCupTitle(120 - (24*menutransition.tics), cup); + M_DrawCupPreview(146 + (24*menutransition.tics), &templevelsearch); + M_DrawCupTitle(120 - (24*menutransition.tics), templevelsearch.cup); } static void M_DrawHighLowLevelTitle(INT16 x, INT16 y, INT16 map) @@ -2149,26 +2168,22 @@ static void M_DrawHighLowLevelTitle(INT16 x, INT16 y, INT16 map) static void M_DrawLevelSelectBlock(INT16 x, INT16 y, INT16 map, boolean redblink, boolean greyscale) { - patch_t *PictureOfLevel = NULL; UINT8 *colormap = NULL; if (greyscale) colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE); - if (mapheaderinfo[map]) - { - PictureOfLevel = mapheaderinfo[map]->thumbnailPic; - } - - if (!PictureOfLevel) - PictureOfLevel = blanklvl; - if (redblink) V_DrawScaledPatch(3+x, y, 0, W_CachePatchName("LVLSEL2", PU_CACHE)); else V_DrawScaledPatch(3+x, y, 0, W_CachePatchName("LVLSEL", PU_CACHE)); - V_DrawSmallMappedPatch(9+x, y+6, 0, PictureOfLevel, colormap); + K_DrawMapThumbnail( + (9+x)<type) { @@ -3920,40 +3935,29 @@ static void M_DrawReplayHutReplayInfo(menudemo_t *demoref) // Draw level stuff x = 15; y = 15; - // A 160x100 image of the level as entry MAPxxP - if (demoref->map < nummapheaders && mapheaderinfo[demoref->map]) - { - patch = mapheaderinfo[demoref->map]->thumbnailPic; - if (!patch) - { - patch = blanklvl; - } - } - else - { - patch = W_CachePatchName("M_NOLVL", PU_CACHE); - } + K_DrawMapThumbnail( + x<kartspeed & DF_ENCORE) ? V_FLIP : 0), + demoref->map, + NULL); - if (!(demoref->kartspeed & DF_ENCORE)) - V_DrawSmallScaledPatch(x, y, V_SNAPTOTOP, patch); - else + if (demoref->kartspeed & DF_ENCORE) { - w = SHORT(patch->width); - h = SHORT(patch->height); - V_DrawSmallScaledPatch(x+(w>>1), y, V_SNAPTOTOP|V_FLIP, patch); - - { - static angle_t rubyfloattime = 0; - const fixed_t rubyheight = FINESINE(rubyfloattime>>ANGLETOFINESHIFT); - V_DrawFixedPatch((x+(w>>2))<>2))<>ANGLETOFINESHIFT); + V_DrawFixedPatch((x+40)<map < nummapheaders && mapheaderinfo[demoref->map]) - V_DrawString(x, y, V_SNAPTOTOP, G_BuildMapTitle(demoref->map+1)); + { + char *title = G_BuildMapTitle(demoref->map+1); + V_DrawString(x, y, V_SNAPTOTOP, title); + Z_Free(title); + } else V_DrawString(x, y, V_SNAPTOTOP|V_ALLOWLOWERCASE|V_TRANSLUCENT, "Level is not loaded."); @@ -4462,3 +4466,660 @@ void M_DrawAddons(void) } #undef addonsseperation + +// Challenges Menu + +#define challengesbordercolor 27 + +static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili) +{ + unlockable_t *ref = NULL; + patch_t *pat = missingpat; + UINT8 *colormap = NULL; + fixed_t siz; + UINT8 id, num, work; + + id = (i * CHALLENGEGRIDHEIGHT) + j; + num = gamedata->challengegrid[id]; + + // Empty spots in the grid are always unconnected. + if (num >= MAXUNLOCKABLES) + { + V_DrawFill(x, y, 16, 16, challengesbordercolor); + goto drawborder; + } + + // Okay, this is what we want to draw. + ref = &unlockables[num]; + + // ...unless we simply aren't unlocked yet. + if ((gamedata->unlocked[num] == false) + || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)) + { + work = (ref->majorunlock) ? 2 : 1; + V_DrawFill(x, y, 16*work, 16*work, + ((challengesmenu.extradata[id] == CHE_HINT) ? 132 : 11)); + goto drawborder; + } + + if (ref->icon != NULL && ref->icon[0]) + { + pat = W_CachePatchName(ref->icon, PU_CACHE); + if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, ref->color, GTC_MENUCACHE); + } + } + else switch (ref->type) + { + case SECRET_SKIN: + { + INT32 skin = M_UnlockableSkinNum(ref); + if (skin != -1) + { + colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE); + pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK]; + } + break; + } + case SECRET_FOLLOWER: + { + INT32 skin = M_UnlockableFollowerNum(ref); + if (skin != -1) + { + UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); + colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + pat = W_CachePatchName(followers[skin].icon, PU_CACHE); + } + break; + } + default: + { + pat = W_CachePatchName(va("UN_RR00%c", ref->majorunlock ? 'B' : 'A'), PU_CACHE); + if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) + { + //CONS_Printf(" color for %d is %s\n", num, skincolors[unlockables[num].color].name); + colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE); + } + break; + } + } + + siz = (SHORT(pat->width) << FRACBITS); + siz = FixedDiv(((ref->majorunlock) ? 32 : 16) << FRACBITS, siz); + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + siz, + 0, pat, + colormap + ); + +drawborder: + if (!hili) + { + if (ref != NULL) + { + work = 16 * (ref->majorunlock ? 2 : 1); + // Horizontal + V_DrawFill(x, y , work, 1, challengesbordercolor); + V_DrawFill(x, y + work-1, work, 1, challengesbordercolor); + // Vertical + V_DrawFill(x , y+1, 1, work-2, challengesbordercolor); + V_DrawFill(x + work-1, y+1, 1, work-2, challengesbordercolor); + } + return; + } + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + ((ref != NULL && ref->majorunlock) ? FRACUNIT*2 : FRACUNIT), + 0, kp_facehighlight[(challengesmenu.ticker / 4) % 8], + NULL + ); +} + +static void M_DrawChallengePreview(INT32 x, INT32 y) +{ + unlockable_t *ref = NULL; + UINT8 *colormap = NULL; + UINT16 specialmap = NEXTMAP_INVALID; + + if (challengesmenu.currentunlock >= MAXUNLOCKABLES) + { + V_DrawFill(0, 146, BASEVIDWIDTH, 54, challengesbordercolor); + return; + } + + // Okay, this is what we want to draw. + ref = &unlockables[challengesmenu.currentunlock]; + + if (!gamedata->unlocked[challengesmenu.currentunlock]) + { + // todo draw some sort of question mark? + V_DrawFill(0, 146, BASEVIDWIDTH, 54, challengesbordercolor); + return; + } + + if (ref->type != SECRET_CUP) + V_DrawFill(0, 146, BASEVIDWIDTH, 54, challengesbordercolor); + + switch (ref->type) + { + case SECRET_SKIN: + { + INT32 skin = M_UnlockableSkinNum(ref); + // Draw our character! + if (skin != -1) + { + colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE); + M_DrawCharacterSprite(x, y, skin, false, false, 0, colormap); + } + break; + } + case SECRET_FOLLOWER: + { + INT32 skin = R_SkinAvailable(cv_skin[0].string); + INT32 fskin = M_UnlockableFollowerNum(ref); + + // Draw proximity reference for character + if (skin == -1) + skin = 0; + colormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_MENUCACHE); + M_DrawCharacterSprite(x, y, skin, false, false, 0, colormap); + + // Draw follower next to them + if (fskin != -1) + { + 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); + } + break; + } + case SECRET_CUP: + { + levelsearch_t templevelsearch; + + templevelsearch.cup = M_UnlockableCup(ref); + templevelsearch.typeoflevel = G_TOLFlag(GT_RACE)|G_TOLFlag(GT_BATTLE); + templevelsearch.cupmode = true; + templevelsearch.timeattack = false; + templevelsearch.checklocked = false; + + M_DrawCupPreview(146, &templevelsearch); + + break; + } + case SECRET_MAP: + { + UINT16 mapnum = M_UnlockableMapNum(ref); + K_DrawMapThumbnail( + (x-30)< nummapheaders) + { + encoremapcache = G_RandMap(G_TOLFlag(GT_RACE), -1, 2, 0, false, NULL); + } + specialmap = encoremapcache; + break; + } + case SECRET_TIMEATTACK: + { + static UINT16 tamapcache = NEXTMAP_INVALID; + if (tamapcache > nummapheaders) + { + tamapcache = G_RandMap(G_TOLFlag(GT_RACE), -1, 2, 0, false, NULL); + } + specialmap = tamapcache; + break; + } + case SECRET_BREAKTHECAPSULES: + { + static UINT16 btcmapcache = NEXTMAP_INVALID; + if (btcmapcache > nummapheaders) + { + btcmapcache = G_RandMap(G_TOLFlag(GT_BATTLE), -1, 2, 0, false, NULL); + } + specialmap = btcmapcache; + break; + } + case SECRET_HARDSPEED: + { + static UINT16 hardmapcache = NEXTMAP_INVALID; + if (hardmapcache > nummapheaders) + { + hardmapcache = G_RandMap(G_TOLFlag(GT_RACE), -1, 2, 0, false, NULL); + } + specialmap = hardmapcache; + 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)"); + } + default: + { + break; + } + } + + if (specialmap == NEXTMAP_INVALID || !ref) + return; + + x -= 50; + y = 146+2; + + K_DrawMapThumbnail( + (x)<type == SECRET_ENCORE) ? V_FLIP : 0, + specialmap, + NULL); + + if (ref->type == SECRET_ENCORE) + { + static angle_t rubyfloattime = 0; + const fixed_t rubyheight = FINESINE(rubyfloattime>>ANGLETOFINESHIFT); + V_DrawFixedPatch((x+40)<type == SECRET_HARDSPEED) + { + V_DrawFixedPatch((x+40-25)<x, explodex, selectx; + INT32 y = currentMenu->y; + INT16 i, j; + const char *str; + INT16 offset; + + { + patch_t *bg = W_CachePatchName("M_XTRABG", PU_CACHE); + V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL); + } + + if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL) + { + V_DrawCenteredString(x, y, V_REDMAP, "No challenges available!?"); + goto challengedesc; + } + + x -= 16; + + x += challengesmenu.offset; + + if (challengegridloops) + { + if (!challengesmenu.col && challengesmenu.hilix) + x -= gamedata->challengegridwidth*16; + i = challengesmenu.col + challengesmenu.focusx; + explodex = x - (i*16); + + while (x < BASEVIDWIDTH-16) + { + i = (i + 1) % gamedata->challengegridwidth; + x += 16; + } + } + else + { + if (gamedata->challengegridwidth & 1) + x += 8; + + i = gamedata->challengegridwidth-1; + explodex = x - (i*16)/2; + x += (i*16)/2; + + V_DrawFill(0, currentMenu->y, explodex, (CHALLENGEGRIDHEIGHT*16), challengesbordercolor); + V_DrawFill((x+16), currentMenu->y, BASEVIDWIDTH - (x+16), (CHALLENGEGRIDHEIGHT*16), challengesbordercolor); + } + + selectx = explodex + (challengesmenu.hilix*16); + + V_DrawFill(0, (currentMenu->y)-1 , BASEVIDWIDTH, 1, challengesbordercolor); + V_DrawFill(0, (currentMenu->y) + (CHALLENGEGRIDHEIGHT*16), BASEVIDWIDTH, 1, challengesbordercolor); + while (i >= 0 && x >= -32) + { + y = currentMenu->y-16; + for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) + { + y += 16; + + if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j] & CHE_DONTDRAW) + { + continue; + } + + if (x == selectx && j == challengesmenu.hiliy) + { + continue; + } + + M_DrawChallengeTile(i, j, x, y, false); + } + + x -= 16; + i--; + if (challengegridloops && i < 0) + { + i = (i + gamedata->challengegridwidth) + % gamedata->challengegridwidth; + } + } + + if (challengesmenu.fade) + V_DrawFadeScreen(31, challengesmenu.fade); + + M_DrawChallengeTile( + challengesmenu.hilix, + challengesmenu.hiliy, + selectx, + currentMenu->y + (challengesmenu.hiliy*16), + true); + M_DrawCharSelectExplosions(false, explodex, currentMenu->y); + +challengedesc: + + // Tally + { + str = va("%d/%d", + challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY], + challengesmenu.unlockcount[CC_TOTAL] + ); + V_DrawRightAlignedKartString(BASEVIDWIDTH-7, 9-challengesmenu.unlockcount[CC_ANIM], 0, str); + } + + // Name bar + { + y = 120; + V_DrawScaledPatch(0, y, 0, W_CachePatchName("MENUHINT", PU_CACHE)); + + if (challengesmenu.currentunlock < MAXUNLOCKABLES) + { + str = unlockables[challengesmenu.currentunlock].name; + if (!gamedata->unlocked[challengesmenu.currentunlock]) + { + str = "???"; //M_CreateSecretMenuOption(str); + } + } + else + { + str = "---"; + } + + offset = V_LSTitleLowStringWidth(str, 0) / 2; + V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str); + } + + // Derived from M_DrawCharSelectPreview + x = 40; + y = BASEVIDHEIGHT-16; + + // Unlock preview + M_DrawChallengePreview(x, y); + + // Conditions for unlock + i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy; + + if (challengesmenu.unlockcondition != NULL + && ((gamedata->unlocked[challengesmenu.currentunlock] == true) + || ((challengesmenu.extradata != NULL) + && (challengesmenu.extradata[i] & CHE_HINT)) + ) + ) + { + V_DrawCenteredString(BASEVIDWIDTH/2, 120 + 32, V_ALLOWLOWERCASE, challengesmenu.unlockcondition); + } +} + +// Statistics menu + +#define STATSSTEP 10 + +static void M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y) +{ + UINT8 lasttype = UINT8_MAX, curtype; + emblem_t *emblem = M_GetLevelEmblems(mapnum); + + while (emblem) + { + switch (emblem->type) + { + case ET_TIME: + curtype = 1; + break; + case ET_GLOBAL: + { + if (emblem->flags & GE_NOTMEDAL) + { + emblem = M_GetLevelEmblems(-1); + continue; + } + curtype = 2; + break; + } + default: + curtype = 0; + break; + } + + // Shift over if emblem is of a different discipline + if (lasttype != UINT8_MAX && lasttype != curtype) + x -= 4; + lasttype = curtype; + + if (gamedata->collected[emblem-emblemlocations]) + V_DrawSmallMappedPatch(x, y, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_MENUCACHE)); + else + V_DrawSmallScaledPatch(x, y, 0, W_CachePatchName("NEEDIT", PU_CACHE)); + + emblem = M_GetLevelEmblems(-1); + x -= 8; + } +} + +static void M_DrawStatsMaps(void) +{ + INT32 y = 80, i = -1; + INT16 mnum; + boolean dotopname = true, dobottomarrow = (statisticsmenu.location < statisticsmenu.maxscroll); + INT32 location = statisticsmenu.location; + + if (location) + V_DrawCharacter(10, y-(skullAnimCounter/5), + '\x1A' | highlightflags, false); // up arrow + + while (statisticsmenu.maplist[++i] != NEXTMAP_INVALID) + { + if (location) + { + --location; + continue; + } + else if (dotopname) + { + V_DrawThinString(20, y, V_6WIDTHSPACE|highlightflags, "LEVEL NAME"); + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, y, V_6WIDTHSPACE|highlightflags, "MEDALS"); + y += STATSSTEP; + dotopname = false; + } + + mnum = statisticsmenu.maplist[i]+1; + M_DrawMapMedals(mnum, 291, y); + + { + char *title = G_BuildMapTitle(mnum); + V_DrawThinString(20, y, V_6WIDTHSPACE, title); + Z_Free(title); + } + + y += STATSSTEP; + + 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) + --location; + + // Extra Emblem headers + for (i = 0; i < 2; ++i) + { + if (i == 1) + { + V_DrawThinString(20, y, V_6WIDTHSPACE|highlightflags, "EXTRA MEDALS"); + if (location) + { + y += STATSSTEP; + location++; + } + } + if (location) + { + --location; + continue; + } + + y += STATSSTEP; + + if (y >= BASEVIDHEIGHT-STATSSTEP) + goto bottomarrow; + } + + // Extra Emblems + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (unlockables[i].type != SECRET_EXTRAMEDAL) + { + continue; + } + + if (location) + { + --location; + continue; + } + + if (i >= 0) + { + if (gamedata->unlocked[i]) + { + UINT16 color = min(unlockables[i].color, numskincolors-1); + if (!color) + color = SKINCOLOR_GOLD; + V_DrawSmallMappedPatch(291, y+1, V_6WIDTHSPACE, W_CachePatchName("GOTITA", PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, color, GTC_MENUCACHE)); + } + else + { + V_DrawSmallScaledPatch(291, y+1, V_6WIDTHSPACE, W_CachePatchName("NEEDIT", PU_CACHE)); + } + + V_DrawThinString(20, y, V_6WIDTHSPACE, va("%s", unlockables[i].name)); + } + + y += STATSSTEP; + + if (y >= BASEVIDHEIGHT-STATSSTEP) + goto bottomarrow; + } +bottomarrow: + if (dobottomarrow) + V_DrawCharacter(10, y-STATSSTEP + (skullAnimCounter/5), + '\x1B' | highlightflags, false); // down arrow +} + +void M_DrawStatistics(void) +{ + char beststr[40]; + + tic_t besttime = 0; + + INT32 i; + INT32 mapsunfinished = 0; + + { + patch_t *bg = W_CachePatchName("M_XTRABG", PU_CACHE); + V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL); + } + + 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)); + + if (!statisticsmenu.maplist) + { + V_DrawCenteredThinString(BASEVIDWIDTH/2, 62, V_6WIDTHSPACE|V_ALLOWLOWERCASE, "No maps!?"); + return; + } + + for (i = 0; i < nummapheaders; i++) + { + if (!mapheaderinfo[i] || (mapheaderinfo[i]->menuflags & (LF2_NOTIMEATTACK|LF2_HIDEINSTATS|LF2_HIDEINMENU))) + continue; + + if (!mapheaderinfo[i]->mainrecord || mapheaderinfo[i]->mainrecord->time <= 0) + { + mapsunfinished++; + continue; + } + + besttime += mapheaderinfo[i]->mainrecord->time; + } + + V_DrawThinString(20, 60, V_6WIDTHSPACE|V_ALLOWLOWERCASE, "Combined time records:"); + + sprintf(beststr, "%i:%02i:%02i.%02i", G_TicsToHours(besttime), G_TicsToMinutes(besttime, false), G_TicsToSeconds(besttime), G_TicsToCentiseconds(besttime)); + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 60, V_6WIDTHSPACE|V_ALLOWLOWERCASE|(mapsunfinished ? V_REDMAP : 0), beststr); + + if (mapsunfinished) + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 70, V_6WIDTHSPACE|V_ALLOWLOWERCASE|V_REDMAP, va("(%d unfinished)", mapsunfinished)); + else + V_DrawRightAlignedThinString(BASEVIDWIDTH-20, 70, V_6WIDTHSPACE|V_ALLOWLOWERCASE, "(complete)"); + + V_DrawThinString(32, 70, V_6WIDTHSPACE, va("x %d/%d", M_CountMedals(false, false), M_CountMedals(true, false))); + V_DrawSmallMappedPatch(20, 70, 0, W_CachePatchName("GOTITA", PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GOLD, GTC_MENUCACHE)); + + M_DrawStatsMaps(); +} + +#undef STATSSTEP diff --git a/src/k_menufunc.c b/src/k_menufunc.c index b15fdf8ad..71a447a9e 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -343,8 +343,8 @@ static void M_EraseDataResponse(INT32 ch) if (optionsmenu.erasecontext == 2) { // SRB2Kart: This actually needs to be done FIRST, so that you don't immediately regain playtime/matches secrets - totalplaytime = 0; - matchesplayed = 0; + gamedata->totalplaytime = 0; + gamedata->matchesplayed = 0; } if (optionsmenu.erasecontext != 1) G_ClearRecords(); @@ -981,7 +981,7 @@ void M_StartControlPanel(void) } else { - currentMenu = &MainDef; + currentMenu = M_InterruptMenuWithChallenges(&MainDef); } } else @@ -1776,15 +1776,14 @@ 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, i, strlines; + size_t max = 0, start = 0, strlines = 0, i; static char *message = NULL; Z_Free(message); message = Z_StrDup(string); DEBFILE(message); // Rudementary word wrapping. - // Simple and effective. Does not handle nonuniform letter sizes, colors, etc. but who cares. - strlines = 0; + // Simple and effective. Does not handle nonuniform letter sizes, etc. but who cares. for (i = 0; message[i]; i++) { if (message[i] == ' ') @@ -1799,6 +1798,8 @@ void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtyp max = 0; continue; } + else if (message[i] & 0x80) + continue; else max += 8; @@ -2053,7 +2054,10 @@ void M_QuitSRB2(INT32 choice) struct setup_chargrid_s setup_chargrid[9][9]; setup_player_t setup_player[MAXSPLITSCREENPLAYERS]; -struct setup_explosions_s setup_explosions[48]; +struct setup_explosions_s setup_explosions[CSEXPLOSIONS]; + +UINT8 setup_followercategories[MAXFOLLOWERCATEGORIES][2]; +UINT8 setup_numfollowercategories; UINT8 setup_numplayers = 0; // This variable is very important, it was extended to determine how many players exist in ALL menus. tic_t setup_animcounter = 0; @@ -2065,64 +2069,61 @@ UINT8 setup_maxpage = 0; // For charsel page to identify alts easier... static void M_SetupProfileGridPos(setup_player_t *p) { profile_t *pr = PR_GetProfile(p->profilen); - INT32 i; + INT32 i = R_SkinAvailable(pr->skinname); + INT32 alt = 0; // Hey it's my character's name! // While we're here, read follower values. p->followern = K_FollowerAvailable(pr->follower); - if (p->followern < 0 || p->followern >= numfollowers || followers[p->followern].category >= numfollowercategories) - p->followercategory = -1; + if (p->followern < 0 || p->followern >= numfollowers || followers[p->followern].category >= numfollowercategories || !K_FollowerUsable(p->followern)) + p->followercategory = p->followern = -1; else p->followercategory = followers[p->followern].category; p->followercolor = pr->followercolor; - // Now position the grid for skin - for (i = 0; i < numskins; i++) + if (!R_SkinUsable(g_localplayers[0], i, false)) { - if (!(strcmp(pr->skinname, skins[i].name))) - { - INT32 alt = 0; // Hey it's my character's name! - p->gridx = skins[i].kartspeed-1; - p->gridy = skins[i].kartweight-1; - - // Now this put our cursor on the good alt - while (setup_chargrid[p->gridx][p->gridy].skinlist[alt] != i) - alt++; - - p->clonenum = alt; - p->color = PR_GetProfileColor(pr); - return; // we're done here - } + i = GetSkinNumClosestToStats(skins[i].kartspeed, skins[i].kartweight, skins[i].flags, false); } + + // Now position the grid for skin + p->gridx = skins[i].kartspeed-1; + p->gridy = skins[i].kartweight-1; + + // Now this put our cursor on the good alt + while (alt < setup_chargrid[p->gridx][p->gridy].numskins && setup_chargrid[p->gridx][p->gridy].skinlist[alt] != i) + alt++; + + p->clonenum = alt; + p->color = PR_GetProfileColor(pr); } static void M_SetupMidGameGridPos(setup_player_t *p, UINT8 num) { - INT32 i; + INT32 i = R_SkinAvailable(cv_skin[num].zstring); + INT32 alt = 0; // Hey it's my character's name! // While we're here, read follower values. p->followern = cv_follower[num].value; p->followercolor = cv_followercolor[num].value; + if (p->followern < 0 || p->followern >= numfollowers || followers[p->followern].category >= numfollowercategories || !K_FollowerUsable(p->followern)) + p->followercategory = p->followern = -1; + else + p->followercategory = followers[p->followern].category; + // Now position the grid for skin - for (i = 0; i < numskins; i++) - { - if (!(strcmp(cv_skin[num].zstring, skins[i].name))) - { - INT32 alt = 0; // Hey it's my character's name! - p->gridx = skins[i].kartspeed-1; - p->gridy = skins[i].kartweight-1; + p->gridx = skins[i].kartspeed-1; + p->gridy = skins[i].kartweight-1; - // Now this put our cursor on the good alt - while (setup_chargrid[p->gridx][p->gridy].skinlist[alt] != i) - alt++; + // Now this put our cursor on the good alt + while (alt < setup_chargrid[p->gridx][p->gridy].numskins && setup_chargrid[p->gridx][p->gridy].skinlist[alt] != i) + alt++; - p->clonenum = alt; - p->color = cv_playercolor[num].value; - return; // we're done here - } - } + p->clonenum = alt; + p->color = cv_playercolor[num].value; + return; // we're done here } @@ -2175,6 +2176,9 @@ void M_CharacterSelectInit(void) UINT8 x = skins[i].kartspeed-1; UINT8 y = skins[i].kartweight-1; + if (!R_SkinUsable(g_localplayers[0], i, false)) + continue; + if (setup_chargrid[x][y].numskins >= MAXCLONES) CONS_Alert(CONS_ERROR, "Max character alts reached for %d,%d\n", x+1, y+1); else @@ -2216,6 +2220,32 @@ void M_CharacterSelectInit(void) } } + setup_numfollowercategories = 0; + for (i = 0; i < numfollowercategories; i++) + { + if (followercategories[i].numincategory == 0) + continue; + + setup_followercategories[setup_numfollowercategories][0] = 0; + + for (j = 0; j < numfollowers; j++) + { + if (followers[j].category != i) + continue; + + if (!K_FollowerUsable(j)) + continue; + + setup_followercategories[setup_numfollowercategories][0]++; + setup_followercategories[setup_numfollowercategories][1] = i; + } + + if (!setup_followercategories[setup_numfollowercategories][0]) + continue; + + setup_numfollowercategories++; + } + setup_page = 0; } @@ -2226,10 +2256,12 @@ void M_CharacterSelect(INT32 choice) M_SetupNextMenu(&PLAY_CharSelectDef, false); } -static void M_SetupReadyExplosions(setup_player_t *p) +static void M_SetupReadyExplosions(boolean charsel, UINT16 basex, UINT16 basey, UINT16 color) { UINT8 i, j; UINT8 e = 0; + UINT16 maxx = (charsel ? 9 : gamedata->challengegridwidth); + UINT16 maxy = (charsel ? 9 : CHALLENGEGRIDHEIGHT); while (setup_explosions[e].tics) { @@ -2245,7 +2277,7 @@ static void M_SetupReadyExplosions(setup_player_t *p) for (j = 0; j < 4; j++) { - SINT8 x = p->gridx, y = p->gridy; + INT16 x = basex, y = basey; switch (j) { @@ -2255,11 +2287,17 @@ static void M_SetupReadyExplosions(setup_player_t *p) case 3: y -= offset; break; } - if ((x < 0 || x > 8) || (y < 0 || y > 8)) + if ((y < 0 || y >= maxy)) continue; + if (charsel || !challengegridloops) + { + if (x < 0 || x >= maxx) + continue; + } + setup_explosions[e].tics = t; - setup_explosions[e].color = p->color; + setup_explosions[e].color = color; setup_explosions[e].x = x; setup_explosions[e].y = y; @@ -2547,7 +2585,7 @@ static void M_HandleCharAskChange(setup_player_t *p, UINT8 num) p->delay = TICRATE; S_StartSound(NULL, sfx_s3k4e); - M_SetupReadyExplosions(p); + M_SetupReadyExplosions(true, p->gridx, p->gridy, p->color); } else { @@ -2822,7 +2860,7 @@ static void M_HandleFollowerCategoryRotate(setup_player_t *p, UINT8 num) if (menucmd[num].dpad_lr > 0) { p->followercategory++; - if (p->followercategory >= numfollowercategories) + if (p->followercategory >= setup_numfollowercategories) p->followercategory = -1; p->rotate = CSROTATETICS; @@ -2833,7 +2871,7 @@ static void M_HandleFollowerCategoryRotate(setup_player_t *p, UINT8 num) { p->followercategory--; if (p->followercategory < -1) - p->followercategory = numfollowercategories-1; + p->followercategory = setup_numfollowercategories-1; p->rotate = -CSROTATETICS; p->delay = CSROTATETICS; @@ -2847,7 +2885,7 @@ static void M_HandleFollowerCategoryRotate(setup_player_t *p, UINT8 num) p->followern = -1; p->mdepth = CSSTEP_READY; p->delay = TICRATE; - M_SetupReadyExplosions(p); + M_SetupReadyExplosions(true, p->gridx, p->gridy, p->color); S_StartSound(NULL, sfx_s3k4e); } else @@ -2855,7 +2893,9 @@ static void M_HandleFollowerCategoryRotate(setup_player_t *p, UINT8 num) if (p->followern < 0 || followers[p->followern].category != p->followercategory) { p->followern = 0; - while (p->followern < numfollowers && followers[p->followern].category != p->followercategory) + while (p->followern < numfollowers + && (followers[p->followern].category != setup_followercategories[p->followercategory][1] + || !K_FollowerUsable(p->followern))) p->followern++; } @@ -2910,7 +2950,7 @@ static void M_HandleFollowerRotate(setup_player_t *p, UINT8 num) if (p->followern == startfollowern) break; } - while (followers[p->followern].category != p->followercategory); + while (followers[p->followern].category != setup_followercategories[p->followercategory][1] || !K_FollowerUsable(p->followern)); M_GetFollowerState(p); @@ -2928,7 +2968,7 @@ static void M_HandleFollowerRotate(setup_player_t *p, UINT8 num) if (p->followern == startfollowern) break; } - while (followers[p->followern].category != p->followercategory); + while (followers[p->followern].category != setup_followercategories[p->followercategory][1] || !K_FollowerUsable(p->followern)); M_GetFollowerState(p); @@ -2948,7 +2988,7 @@ static void M_HandleFollowerRotate(setup_player_t *p, UINT8 num) { p->mdepth = CSSTEP_READY; p->delay = TICRATE; - M_SetupReadyExplosions(p); + M_SetupReadyExplosions(true, p->gridx, p->gridy, p->color); S_StartSound(NULL, sfx_s3k4e); } @@ -2997,7 +3037,7 @@ static void M_HandleFollowerColorRotate(setup_player_t *p, UINT8 num) { p->mdepth = CSSTEP_READY; p->delay = TICRATE; - M_SetupReadyExplosions(p); + M_SetupReadyExplosions(true, p->gridx, p->gridy, p->color); S_StartSound(NULL, sfx_s3k4e); M_SetMenuDelay(num); } @@ -3255,17 +3295,25 @@ void M_SetupGametypeMenu(INT32 choice) PLAY_GamemodesDef.prevMenu = currentMenu; - if (cv_splitplayers.value <= 1) + // Battle and Capsules disabled + PLAY_GamemodesMenu[1].status = IT_DISABLED; + PLAY_GamemodesMenu[2].status = IT_DISABLED; + + if (cv_splitplayers.value > 1) { - // Remove Battle, add Capsules - PLAY_GamemodesMenu[1].status = IT_DISABLED; + // Re-add Battle + PLAY_GamemodesMenu[1].status = IT_STRING | IT_CALL; + } + else if (M_SecretUnlocked(SECRET_BREAKTHECAPSULES, true)) + { + // Re-add Capsules PLAY_GamemodesMenu[2].status = IT_STRING | IT_CALL; } else { - // Add Battle, remove Capsules - PLAY_GamemodesMenu[1].status = IT_STRING | IT_CALL; - PLAY_GamemodesMenu[2].status = IT_DISABLED; + // Only one non-Back entry, let's skip straight to Race. + M_SetupRaceMenu(-1); + return; } M_SetupNextMenu(&PLAY_GamemodesDef, false); @@ -3277,15 +3325,15 @@ void M_SetupRaceMenu(INT32 choice) PLAY_RaceGamemodesDef.prevMenu = currentMenu; + // Time Attack disabled + PLAY_RaceGamemodesMenu[2].status = IT_DISABLED; + // Time Attack is 1P only - if (cv_splitplayers.value <= 1) + if (cv_splitplayers.value <= 1 + && M_SecretUnlocked(SECRET_TIMEATTACK, true)) { PLAY_RaceGamemodesMenu[2].status = IT_STRING | IT_CALL; } - else - { - PLAY_RaceGamemodesMenu[2].status = IT_DISABLED; - } M_SetupNextMenu(&PLAY_RaceGamemodesDef, false); } @@ -3323,7 +3371,7 @@ void M_SetupDifficultySelect(INT32 choice) PLAY_RaceDifficultyDef.lastOn = drace_cupselect; // Select cup select by default. } - if (M_SecretUnlocked(SECRET_ENCORE)) + if (M_SecretUnlocked(SECRET_ENCORE, false)) { PLAY_RaceDifficulty[drace_encore].status = IT_STRING2|IT_CVAR; // Encore on/off } @@ -3338,8 +3386,11 @@ void M_SetupDifficultySelect(INT32 choice) // // Determines whether to show a given map in the various level-select lists. // -boolean M_CanShowLevelInList(INT16 mapnum, UINT32 tol, cupheader_t *cup) +boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch) { + if (!levelsearch) + return false; + if (mapnum >= nummapheaders) return false; @@ -3355,11 +3406,11 @@ boolean M_CanShowLevelInList(INT16 mapnum, UINT32 tol, cupheader_t *cup) if (mapheaderinfo[mapnum]->lumpnum == LUMPERROR) return false; - if (M_MapLocked(mapnum+1)) + if (levelsearch->checklocked && M_MapLocked(mapnum+1)) return false; // not unlocked // Check for TOL - if (!(mapheaderinfo[mapnum]->typeoflevel & tol)) + if (!(mapheaderinfo[mapnum]->typeoflevel & levelsearch->typeoflevel)) return false; // Should the map be hidden? @@ -3367,26 +3418,31 @@ boolean M_CanShowLevelInList(INT16 mapnum, UINT32 tol, cupheader_t *cup) return false; // I don't know why, but some may have exceptions. - if (levellist.timeattack && (mapheaderinfo[mapnum]->menuflags & LF2_NOTIMEATTACK)) + if (levelsearch->timeattack && (mapheaderinfo[mapnum]->menuflags & LF2_NOTIMEATTACK)) return false; // Don't permit cup when no cup requested (also no dupes in time attack) - if (levellist.cupmode && (levellist.timeattack || !cup) && mapheaderinfo[mapnum]->cup != cup) + if (levelsearch->cupmode + && (levelsearch->timeattack || !levelsearch->cup) + && mapheaderinfo[mapnum]->cup != levelsearch->cup) return false; // Survived our checks. return true; } -UINT16 M_CountLevelsToShowInList(UINT32 tol, cupheader_t *cup) +UINT16 M_CountLevelsToShowInList(levelsearch_t *levelsearch) { INT16 i, count = 0; - if (cup) + if (!levelsearch) + return false; + + if (levelsearch->cup) { for (i = 0; i < CUPCACHE_MAX; i++) { - if (!M_CanShowLevelInList(cup->cachedlevels[i], tol, cup)) + if (!M_CanShowLevelInList(levelsearch->cup->cachedlevels[i], levelsearch)) continue; count++; } @@ -3395,60 +3451,66 @@ UINT16 M_CountLevelsToShowInList(UINT32 tol, cupheader_t *cup) } for (i = 0; i < nummapheaders; i++) - if (M_CanShowLevelInList(i, tol, NULL)) + if (M_CanShowLevelInList(i, levelsearch)) count++; return count; } -UINT16 M_GetFirstLevelInList(UINT8 *i, UINT32 tol, cupheader_t *cup) +UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch) { INT16 mapnum = NEXTMAP_INVALID; - if (cup) + if (!levelsearch) + return false; + + if (levelsearch->cup) { *i = 0; mapnum = NEXTMAP_INVALID; for (; *i < CUPCACHE_MAX; (*i)++) { - if (!M_CanShowLevelInList(cup->cachedlevels[*i], tol, cup)) + if (!M_CanShowLevelInList(levelsearch->cup->cachedlevels[*i], levelsearch)) continue; - mapnum = cup->cachedlevels[*i]; + mapnum = levelsearch->cup->cachedlevels[*i]; break; } } else { for (mapnum = 0; mapnum < nummapheaders; mapnum++) - if (M_CanShowLevelInList(mapnum, tol, NULL)) + if (M_CanShowLevelInList(mapnum, levelsearch)) break; } return mapnum; } -UINT16 M_GetNextLevelInList(UINT16 map, UINT8 *i, UINT32 tol, cupheader_t *cup) +UINT16 M_GetNextLevelInList(UINT16 mapnum, UINT8 *i, levelsearch_t *levelsearch) { - if (cup) + if (!levelsearch) + return false; + + if (levelsearch->cup) { - map = NEXTMAP_INVALID; + mapnum = NEXTMAP_INVALID; (*i)++; for (; *i < CUPCACHE_MAX; (*i)++) { - if (!M_CanShowLevelInList(cup->cachedlevels[*i], tol, cup)) + if (!M_CanShowLevelInList(levelsearch->cup->cachedlevels[*i], levelsearch)) continue; - map = cup->cachedlevels[*i]; + mapnum = levelsearch->cup->cachedlevels[*i]; break; } } else { - map++; - while (!M_CanShowLevelInList(map, tol, NULL) && map < nummapheaders) - map++; + mapnum++; + while (!M_CanShowLevelInList(mapnum, levelsearch) && mapnum < nummapheaders) + mapnum++; } - return map; + return mapnum; } struct cupgrid_s cupgrid; @@ -3456,7 +3518,7 @@ struct levellist_s levellist; static void M_LevelSelectScrollDest(void) { - UINT16 m = M_CountLevelsToShowInList(levellist.typeoflevel, levellist.selectedcup)-1; + UINT16 m = M_CountLevelsToShowInList(&levellist.levelsearch)-1; levellist.dest = (6*levellist.cursor); @@ -3474,9 +3536,9 @@ static void M_LevelListFromGametype(INT16 gt) if (first || gt != levellist.newgametype) { levellist.newgametype = gt; - levellist.typeoflevel = G_TOLFlag(gt); - levellist.cupmode = (!(gametypedefaultrules[gt] & GTR_NOCUPSELECT)); - levellist.selectedcup = NULL; + levellist.levelsearch.typeoflevel = G_TOLFlag(gt); + levellist.levelsearch.cupmode = (!(gametypedefaultrules[gt] & GTR_NOCUPSELECT)); + levellist.levelsearch.cup = NULL; first = false; } @@ -3485,14 +3547,17 @@ static void M_LevelListFromGametype(INT16 gt) // Obviously go to Cup Select in gametypes that have cups. // Use a really long level select in gametypes that don't use cups. - if (levellist.cupmode) + if (levellist.levelsearch.cupmode) { - cupheader_t *cup = kartcupheaders; + levelsearch_t templevelsearch = levellist.levelsearch; // full copy size_t currentid = 0, highestunlockedid = 0; const size_t unitlen = sizeof(cupheader_t*) * (CUPMENU_COLUMNS * CUPMENU_ROWS); + templevelsearch.cup = kartcupheaders; + templevelsearch.checklocked = false; + // Make sure there's valid cups before going to this menu. - if (cup == NULL) + if (templevelsearch.cup == NULL) I_Error("Can you really call this a racing game, I didn't recieve any Cups on my pillow or anything"); if (!cupgrid.builtgrid) @@ -3510,12 +3575,12 @@ static void M_LevelListFromGametype(INT16 gt) } memset(cupgrid.builtgrid, 0, cupgrid.cappages * unitlen); - while (cup) + while (templevelsearch.cup) { - if (!M_CountLevelsToShowInList(levellist.typeoflevel, cup)) + if (!M_CountLevelsToShowInList(&templevelsearch)) { // No valid maps, skip. - cup = cup->next; + templevelsearch.cup = templevelsearch.cup->next; continue; } @@ -3536,21 +3601,21 @@ static void M_LevelListFromGametype(INT16 gt) cupgrid.cappages *= 2; } - cupgrid.builtgrid[currentid] = cup; + cupgrid.builtgrid[currentid] = templevelsearch.cup; - if (cup->unlockrequired == -1 || unlockables[cup->unlockrequired].unlocked) + if (!M_CupLocked(templevelsearch.cup)) { highestunlockedid = currentid; - if (Playing() && mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->cup == cup) + if (Playing() && mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->cup == templevelsearch.cup) { - cupgrid.x = cup->id % CUPMENU_COLUMNS; - cupgrid.y = (cup->id / CUPMENU_COLUMNS) % CUPMENU_ROWS; - cupgrid.pageno = cup->id / (CUPMENU_COLUMNS * CUPMENU_ROWS); + cupgrid.x = currentid % CUPMENU_COLUMNS; + cupgrid.y = (currentid / CUPMENU_COLUMNS) % CUPMENU_ROWS; + cupgrid.pageno = currentid / (CUPMENU_COLUMNS * CUPMENU_ROWS); } } currentid++; - cup = cup->next; + templevelsearch.cup = templevelsearch.cup->next; } cupgrid.numpages = (highestunlockedid / (CUPMENU_COLUMNS * CUPMENU_ROWS)) + 1; @@ -3566,10 +3631,10 @@ static void M_LevelListFromGametype(INT16 gt) } // Reset position properly if you go back & forth between gametypes - if (levellist.selectedcup) + if (levellist.levelsearch.cup) { levellist.cursor = 0; - levellist.selectedcup = NULL; + levellist.levelsearch.cup = NULL; } M_LevelSelectScrollDest(); @@ -3588,22 +3653,24 @@ void M_LevelSelectInit(INT32 choice) { (void)choice; - levellist.netgame = false; // Make sure this is reset as we'll only be using this function for offline games! - cupgrid.netgame = false; // Ditto + // Make sure this is reset as we'll only be using this function for offline games! + cupgrid.netgame = false; + levellist.netgame = false; + levellist.levelsearch.checklocked = true; switch (currentMenu->menuitems[itemOn].mvar1) { case 0: cupgrid.grandprix = false; - levellist.timeattack = false; + levellist.levelsearch.timeattack = false; break; case 1: cupgrid.grandprix = false; - levellist.timeattack = true; + levellist.levelsearch.timeattack = true; break; case 2: cupgrid.grandprix = true; - levellist.timeattack = false; + levellist.levelsearch.timeattack = false; break; default: CONS_Alert(CONS_WARNING, "Bad level select init\n"); @@ -3671,7 +3738,7 @@ void M_CupSelectHandler(INT32 choice) M_SetMenuDelay(pid); if ((!newcup) - || (newcup && newcup->unlockrequired != -1 && !unlockables[newcup->unlockrequired].unlocked) + || (M_CupLocked(newcup)) || (newcup->cachedlevels[0] == NEXTMAP_INVALID)) { S_StartSound(NULL, sfx_s3kb2); @@ -3739,10 +3806,10 @@ void M_CupSelectHandler(INT32 choice) else { // Keep cursor position if you select the same cup again, reset if it's a different cup - if (levellist.selectedcup != newcup) + if (levellist.levelsearch.cup != newcup) { levellist.cursor = 0; - levellist.selectedcup = newcup; + levellist.levelsearch.cup = newcup; } M_LevelSelectScrollDest(); @@ -3770,7 +3837,7 @@ void M_CupSelectTick(void) void M_LevelSelectHandler(INT32 choice) { - INT16 maxlevels = M_CountLevelsToShowInList(levellist.typeoflevel, levellist.selectedcup); + INT16 maxlevels = M_CountLevelsToShowInList(&levellist.levelsearch); const UINT8 pid = 0; (void)choice; @@ -3802,14 +3869,14 @@ void M_LevelSelectHandler(INT32 choice) if (M_MenuConfirmPressed(pid) /*|| M_MenuButtonPressed(pid, MBT_START)*/) { UINT8 i = 0; - INT16 map = M_GetFirstLevelInList(&i, levellist.typeoflevel, levellist.selectedcup); + INT16 map = M_GetFirstLevelInList(&i, &levellist.levelsearch); INT16 add = levellist.cursor; M_SetMenuDelay(pid); while (add > 0) { - map = M_GetNextLevelInList(map, &i, levellist.typeoflevel, levellist.selectedcup); + map = M_GetNextLevelInList(map, &i, &levellist.levelsearch); if (map >= nummapheaders) { @@ -3827,7 +3894,7 @@ void M_LevelSelectHandler(INT32 choice) levellist.choosemap = map; - if (levellist.timeattack) + if (levellist.levelsearch.timeattack) { M_SetupNextMenu(&PLAY_TimeAttackDef, false); S_StartSound(NULL, sfx_s3k63); @@ -4058,10 +4125,13 @@ void M_MPSetupNetgameMapSelect(INT32 choice) INT16 gt = GT_RACE; (void)choice; - levellist.netgame = true; // Yep, we'll be starting a netgame. - cupgrid.netgame = true; // Ditto - levellist.timeattack = false; // Make sure we reset those - cupgrid.grandprix = false; // Ditto + // Yep, we'll be starting a netgame. + levellist.netgame = true; + cupgrid.netgame = true; + // Make sure we reset those + levellist.levelsearch.timeattack = false; + levellist.levelsearch.checklocked = true; + cupgrid.grandprix = false; // In case we ever want to add new gamemodes there somehow, have at it! switch (cv_dummygametype.value) @@ -4673,7 +4743,7 @@ void M_InitOptions(INT32 choice) OPTIONS_MainDef.menuitems[mopt_gameplay].status = IT_STRING | IT_SUBMENU; OPTIONS_MainDef.menuitems[mopt_server].status = IT_STRING | IT_SUBMENU; OPTIONS_GameplayDef.menuitems[gopt_encore].status = - (M_SecretUnlocked(SECRET_ENCORE) ? (IT_STRING | IT_CVAR) : IT_DISABLED); + (M_SecretUnlocked(SECRET_ENCORE, false) ? (IT_STRING | IT_CVAR) : IT_DISABLED); } OPTIONS_DataDef.menuitems[dopt_erase].status = (gamestate == GS_MENU @@ -4906,7 +4976,7 @@ static void M_FirstPickProfile(INT32 c) optionsmenu.profile = NULL; // Make sure to get rid of that, too. PR_ApplyProfile(optionsmenu.profilen, 0); - M_SetupNextMenu(&MainDef, false); + M_SetupNextMenu(M_InterruptMenuWithChallenges(&MainDef), false); // Tell the game this is the last profile we picked. CV_StealthSetValue(&cv_ttlprofilen, optionsmenu.profilen); @@ -6902,3 +6972,645 @@ void M_Manual(INT32 choice) MISC_ManualDef.prevMenu = (choice == INT32_MAX ? NULL : currentMenu); M_SetupNextMenu(&MISC_ManualDef, true); } + +// Challenges menu + +struct challengesmenu_s challengesmenu; + +menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) +{ + UINT8 i; + + M_UpdateUnlockablesAndExtraEmblems(false); + + if ((challengesmenu.pending = challengesmenu.requestnew = (M_GetNextAchievedUnlock() < MAXUNLOCKABLES))) + { + MISC_ChallengesDef.prevMenu = desiredmenu; + } + + if (challengesmenu.pending || desiredmenu == NULL) + { + challengesmenu.currentunlock = MAXUNLOCKABLES; + challengesmenu.unlockcondition = NULL; + + M_PopulateChallengeGrid(); + if (gamedata->challengegrid) + challengesmenu.extradata = M_ChallengeGridExtraData(); + + memset(setup_explosions, 0, sizeof(setup_explosions)); + memset(&challengesmenu.unlockcount, 0, sizeof(challengesmenu.unlockcount)); + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (!unlockables[i].conditionset) + { + continue; + } + + challengesmenu.unlockcount[CC_TOTAL]++; + + if (!gamedata->unlocked[i]) + { + continue; + } + + challengesmenu.unlockcount[CC_UNLOCKED]++; + } + + return &MISC_ChallengesDef; + } + + return desiredmenu; +} + +static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh) +{ + UINT8 i; + SINT8 work; + + if (unlockid >= MAXUNLOCKABLES) + return; + + challengesmenu.currentunlock = unlockid; + challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); + challengesmenu.unlockanim = 0; + + if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL) + return; + + for (i = 0; i < (CHALLENGEGRIDHEIGHT * gamedata->challengegridwidth); i++) + { + if (gamedata->challengegrid[i] != unlockid) + { + // Not what we're looking for. + continue; + } + + if (challengesmenu.extradata[i] & CHE_CONNECTEDLEFT) + { + // no need to check for CHE_CONNECTEDUP in linear iteration + continue; + } + + // Helper calculation for non-fresh scrolling. + work = (challengesmenu.col + challengesmenu.focusx); + + challengesmenu.col = challengesmenu.hilix = i/CHALLENGEGRIDHEIGHT; + challengesmenu.row = challengesmenu.hiliy = i%CHALLENGEGRIDHEIGHT; + + if (fresh) + { + // We're just entering the menu. Immediately jump to the desired position... + challengesmenu.focusx = 0; + // ...and since the menu is even-width, randomly select whether it's left or right of center. + if (!unlockables[unlockid].majorunlock + && M_RandomChance(FRACUNIT/2)) + challengesmenu.focusx--; + } + else + { + // We're jumping between multiple unlocks in sequence. Get the difference (looped from -range/2 < work <= range/2). + work -= challengesmenu.col; + if (work <= -gamedata->challengegridwidth/2) + work += gamedata->challengegridwidth; + else if (work >= gamedata->challengegridwidth/2) + work -= gamedata->challengegridwidth; + + if (work > 0) + { + // We only need to scroll as far as the rightward edge. + if (unlockables[unlockid].majorunlock) + { + work--; + challengesmenu.col++; + if (challengesmenu.col >= gamedata->challengegridwidth) + challengesmenu.col = 0; + } + + // Offset right, scroll left? + if (work > LEFTUNLOCKSCROLL) + { + work -= LEFTUNLOCKSCROLL; + challengesmenu.focusx = LEFTUNLOCKSCROLL; + } + else + { + challengesmenu.focusx = work; + work = 0; + } + } + else if (work < 0) + { + // Offset left, scroll right? + if (work < -RIGHTUNLOCKSCROLL) + { + challengesmenu.focusx = -RIGHTUNLOCKSCROLL; + work += RIGHTUNLOCKSCROLL; + } + else + { + challengesmenu.focusx = work; + work = 0; + } + } + else + { + // We're right where we want to be. + challengesmenu.focusx = 0; + } + + // And put the pixel-based scrolling in play, too. + challengesmenu.offset = -work*16; + } + + break; + } +} + +void M_Challenges(INT32 choice) +{ + UINT8 i; + (void)choice; + + M_InterruptMenuWithChallenges(NULL); + MISC_ChallengesDef.prevMenu = currentMenu; + + 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_SetupNextMenu(&MISC_ChallengesDef, false); +} + +void M_ChallengesTick(void) +{ + UINT8 i, newunlock = MAXUNLOCKABLES; + boolean fresh = (challengesmenu.currentunlock >= MAXUNLOCKABLES); + + // Ticking + challengesmenu.ticker++; + challengesmenu.offset /= 2; + for (i = 0; i < CSEXPLOSIONS; i++) + { + if (setup_explosions[i].tics > 0) + setup_explosions[i].tics--; + } + if (challengesmenu.unlockcount[CC_ANIM] > 0) + challengesmenu.unlockcount[CC_ANIM]--; + M_CupSelectTick(); + + if (challengesmenu.pending) + { + // Pending mode. + + 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, fresh); + } + else + { + // All done! Let's save the unlocks we've busted open. + challengesmenu.pending = false; + G_SaveGameData(); + } + } + else if (challengesmenu.fade < 5) + { + // Fade increase. + challengesmenu.fade++; + } + else + { + // Unlock sequence. + + if (++challengesmenu.unlockanim >= MAXUNLOCKTIME) + { + 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); + + // Update shown description just in case..? + challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); + + challengesmenu.unlockcount[CC_TALLY]++; + challengesmenu.unlockcount[CC_ANIM]++; + + Z_Free(challengesmenu.extradata); + if ((challengesmenu.extradata = M_ChallengeGridExtraData())) + { + unlockable_t *ref = &unlockables[challengesmenu.currentunlock]; + UINT16 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 + { + + // Tick down the tally. (currently not visible) + /*if ((challengesmenu.ticker & 1) + && challengesmenu.unlockcount[CC_TALLY] > 0) + { + challengesmenu.unlockcount[CC_TALLY]--; + challengesmenu.unlockcount[CC_UNLOCKED]++; + }*/ + + if (challengesmenu.fade > 0) + { + // Fade decrease. + challengesmenu.fade--; + } + } +} + +boolean M_ChallengesInputs(INT32 ch) +{ + const UINT8 pid = 0; + UINT8 i; + const boolean start = M_MenuButtonPressed(pid, MBT_START); + const boolean move = (menucmd[pid].dpad_ud != 0 || menucmd[pid].dpad_lr != 0); + (void) ch; + + if (challengesmenu.fade) + { + ; + } +#ifdef DEVELOP + else if (M_MenuExtraPressed(pid) && challengesmenu.extradata) // debugging + { + if (challengesmenu.currentunlock < MAXUNLOCKABLES) + { + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = NULL; + gamedata->challengegridwidth = 0; + M_PopulateChallengeGrid(); + Z_Free(challengesmenu.extradata); + challengesmenu.extradata = M_ChallengeGridExtraData(); + + M_ChallengesAutoFocus(challengesmenu.currentunlock, true); + + challengesmenu.pending = true; + } + return true; + } +#endif + else + { + if (M_MenuBackPressed(pid) || start) + { + M_GoBack(0); + M_SetMenuDelay(pid); + + Z_Free(challengesmenu.extradata); + challengesmenu.extradata = NULL; + + challengesmenu.unlockcondition = NULL; + + return true; + } + + if (challengesmenu.extradata != NULL && move) + { + // Determine movement around the grid + // For right/down movement, we can pre-determine the number of steps based on extradata. + // For left/up movement, we can't - we have to be ready to iterate twice, and break early if we don't run into a large tile. + + if (menucmd[pid].dpad_ud > 0) + { + i = 2; + while (i > 0) + { + if (challengesmenu.row < CHALLENGEGRIDHEIGHT-1) + { + challengesmenu.row++; + } + else + { + challengesmenu.row = 0; + } + if (!(challengesmenu.extradata[ + (challengesmenu.col * CHALLENGEGRIDHEIGHT) + + challengesmenu.row] + & CHE_CONNECTEDUP)) + { + break; + } + i--; + } + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + else if (menucmd[pid].dpad_ud < 0) + { + i = (challengesmenu.extradata[ + (challengesmenu.col * CHALLENGEGRIDHEIGHT) + + challengesmenu.row] + & CHE_CONNECTEDUP) ? 2 : 1; + while (i > 0) + { + if (challengesmenu.row > 0) + { + challengesmenu.row--; + } + else + { + challengesmenu.row = CHALLENGEGRIDHEIGHT-1; + } + i--; + } + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + + if (menucmd[pid].dpad_lr > 0) + { + i = 2; + while (i > 0) + { + // Slide the focus counter to movement, if we can. + if (challengesmenu.focusx > -RIGHTUNLOCKSCROLL) + { + challengesmenu.focusx--; + } + + // Step the actual column right. + if (challengesmenu.col < gamedata->challengegridwidth-1) + { + challengesmenu.col++; + } + else + { + challengesmenu.col = 0; + } + + if (!(challengesmenu.extradata[ + (challengesmenu.col * CHALLENGEGRIDHEIGHT) + + challengesmenu.row] + & CHE_CONNECTEDLEFT)) + { + break; + } + + i--; + } + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + else if (menucmd[pid].dpad_lr < 0) + { + i = (challengesmenu.extradata[ + (challengesmenu.col * CHALLENGEGRIDHEIGHT) + + challengesmenu.row] + & CHE_CONNECTEDLEFT) ? 2 : 1; + while (i > 0) + { + // Slide the focus counter to movement, if we can. + if (challengesmenu.focusx < LEFTUNLOCKSCROLL) + { + challengesmenu.focusx++; + } + + // Step the actual column left. + if (challengesmenu.col > 0) + { + challengesmenu.col--; + } + else + { + challengesmenu.col = gamedata->challengegridwidth-1; + } + + i--; + } + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + + // After movement has been determined, figure out the current selection. + i = (challengesmenu.col * CHALLENGEGRIDHEIGHT) + challengesmenu.row; + challengesmenu.currentunlock = (gamedata->challengegrid[i]); + challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); + + challengesmenu.hilix = challengesmenu.col; + challengesmenu.hiliy = challengesmenu.row; + + if (challengesmenu.currentunlock < MAXUNLOCKABLES + && unlockables[challengesmenu.currentunlock].majorunlock) + { + // Adjust highlight coordinates up/to the left for large tiles. + + if (challengesmenu.hiliy > 0 && (challengesmenu.extradata[i] & CHE_CONNECTEDUP)) + { + challengesmenu.hiliy--; + } + + if ((challengesmenu.extradata[i] & CHE_CONNECTEDLEFT)) + { + if (challengesmenu.hilix > 0) + { + challengesmenu.hilix--; + } + else + { + challengesmenu.hilix = gamedata->challengegridwidth-1; + } + } + } + + return true; + } + + if (M_MenuConfirmPressed(pid) + && challengesmenu.currentunlock < MAXUNLOCKABLES + && gamedata->unlocked[challengesmenu.currentunlock]) + { + switch (unlockables[challengesmenu.currentunlock].type) + { + case SECRET_ALTTITLE: + CV_AddValue(&cv_alttitle, 1); + S_StartSound(NULL, sfx_s3kc3s); + M_SetMenuDelay(pid); + break; + default: + break; + } + + return true; + } + } + + return true; +} + +// Statistics menu + +struct statisticsmenu_s statisticsmenu; + +void M_Statistics(INT32 choice) +{ + UINT16 i = 0; + + (void)choice; + + statisticsmenu.maplist = Z_Malloc(sizeof(UINT16) * nummapheaders, PU_STATIC, NULL); + statisticsmenu.nummaps = 0; + + for (i = 0; i < nummapheaders; i++) + { + if (!mapheaderinfo[i]) + continue; + + if (mapheaderinfo[i]->menuflags & (LF2_NOTIMEATTACK|LF2_HIDEINSTATS|LF2_HIDEINMENU)) + continue; + + if (M_MapLocked(i+1)) + continue; + + statisticsmenu.maplist[statisticsmenu.nummaps++] = i; + } + statisticsmenu.maplist[statisticsmenu.nummaps] = NEXTMAP_INVALID; + statisticsmenu.maxscroll = (statisticsmenu.nummaps + M_CountMedals(true, true) + 2) - 10; + statisticsmenu.location = 0; + + if (statisticsmenu.maxscroll < 0) + { + statisticsmenu.maxscroll = 0; + } + + MISC_StatisticsDef.prevMenu = currentMenu; + M_SetupNextMenu(&MISC_StatisticsDef, false); +} + +boolean M_StatisticsInputs(INT32 ch) +{ + const UINT8 pid = 0; + + (void)ch; + + if (M_MenuBackPressed(pid)) + { + M_GoBack(0); + M_SetMenuDelay(pid); + + Z_Free(statisticsmenu.maplist); + statisticsmenu.maplist = NULL; + + return true; + } + + if (M_MenuExtraPressed(pid)) + { + if (statisticsmenu.location > 0) + { + statisticsmenu.location = 0; + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + } + else if (menucmd[pid].dpad_ud > 0) + { + if (statisticsmenu.location < statisticsmenu.maxscroll) + { + statisticsmenu.location++; + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + } + else if (menucmd[pid].dpad_ud < 0) + { + if (statisticsmenu.location > 0) + { + statisticsmenu.location--; + S_StartSound(NULL, sfx_s3k5b); + M_SetMenuDelay(pid); + } + } + + return true; +} diff --git a/src/k_pwrlv.c b/src/k_pwrlv.c index 32a4b8ecd..6ad088cae 100644 --- a/src/k_pwrlv.c +++ b/src/k_pwrlv.c @@ -418,11 +418,7 @@ void K_CashInPowerLevels(void) { pr->powerlevels[powerType] = clientpowerlevels[i][powerType]; - if (M_UpdateUnlockablesAndExtraEmblems()) - { - S_StartSound(NULL, sfx_ncitem); - } - + M_UpdateUnlockablesAndExtraEmblems(true); G_SaveGameData(); } } @@ -642,11 +638,7 @@ void K_PlayerForfeit(UINT8 playerNum, boolean pointLoss) { pr->powerlevels[powerType] = yourPower + inc; - if (M_UpdateUnlockablesAndExtraEmblems()) - { - S_StartSound(NULL, sfx_ncitem); - } - + M_UpdateUnlockablesAndExtraEmblems(true); G_SaveGameData(); } } diff --git a/src/k_roulette.c b/src/k_roulette.c index c2a34698d..b747da9d7 100644 --- a/src/k_roulette.c +++ b/src/k_roulette.c @@ -98,12 +98,14 @@ static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][8] = { 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink { 3, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target { 0, 0, 0, 3, 5, 0, 0, 0 }, // Garden Top + { 0, 0, 0, 0, 0, 0, 0, 0 }, // Gachabom { 0, 0, 2, 2, 2, 0, 0, 0 }, // Sneaker x2 { 0, 0, 0, 0, 4, 4, 4, 0 }, // Sneaker x3 { 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3 { 0, 0, 1, 0, 0, 0, 0, 0 }, // Orbinaut x3 { 0, 0, 0, 2, 0, 0, 0, 0 }, // Orbinaut x4 - { 0, 0, 1, 2, 1, 0, 0, 0 } // Jawz x2 + { 0, 0, 1, 2, 1, 0, 0, 0 }, // Jawz x2 + { 0, 0, 0, 0, 0, 0, 0, 0 } // Gachabom x3 }; static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS-1][2] = @@ -130,12 +132,14 @@ static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS-1][2] = { 0, 0 }, // Kitchen Sink { 2, 0 }, // Drop Target { 4, 0 }, // Garden Top + { 0, 0 }, // Gachabom { 0, 0 }, // Sneaker x2 { 0, 1 }, // Sneaker x3 { 0, 0 }, // Banana x3 { 2, 0 }, // Orbinaut x3 { 1, 1 }, // Orbinaut x4 - { 5, 1 } // Jawz x2 + { 5, 1 }, // Jawz x2 + { 0, 0 } // Gachabom x3 }; static UINT8 K_KartItemOddsSpecial[NUMKARTRESULTS-1][4] = @@ -179,13 +183,14 @@ static kartitems_t K_KartItemReelTimeAttack[] = static kartitems_t K_KartItemReelBreakTheCapsules[] = { - KRITEM_TRIPLEORBINAUT, - KITEM_BANANA, + KITEM_GACHABOM, + KRITEM_TRIPLEGACHABOM, KITEM_NONE }; static kartitems_t K_KartItemReelBoss[] = { + // FIXME: gachabom...? KITEM_ORBINAUT, KITEM_BANANA, KITEM_NONE diff --git a/src/lua_baselib.c b/src/lua_baselib.c index 8c88c5155..87e720bea 100644 --- a/src/lua_baselib.c +++ b/src/lua_baselib.c @@ -2351,7 +2351,7 @@ static int lib_rSetPlayerSkin(lua_State *L) return luaL_error(L, "skin %s (argument 2) is not loaded", skinname); } - if (!R_SkinUsable(j, i)) + if (!R_SkinUsable(j, i, false)) return luaL_error(L, "skin %d (argument 2) not usable - check with R_SkinUsable(player_t, skin) first.", i); SetPlayerSkinByNum(j, i); return 0; @@ -2381,7 +2381,7 @@ static int lib_rSkinUsable(lua_State *L) return luaL_error(L, "skin %s (argument 2) is not loaded", skinname); } - lua_pushboolean(L, R_SkinUsable(j, i)); + lua_pushboolean(L, R_SkinUsable(j, i, false)); return 1; } diff --git a/src/lua_maplib.c b/src/lua_maplib.c index 0557456b2..37ccc955b 100644 --- a/src/lua_maplib.c +++ b/src/lua_maplib.c @@ -2449,8 +2449,6 @@ static int mapheaderinfo_get(lua_State *L) lua_pushinteger(L, header->palette); else if (fastcmp(field,"numlaps")) lua_pushinteger(L, header->numlaps); - else if (fastcmp(field,"unlockrequired")) - lua_pushinteger(L, header->unlockrequired); else if (fastcmp(field,"levelselect")) lua_pushinteger(L, header->levelselect); else if (fastcmp(field,"levelflags")) diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c index b87ad7234..6d4b6b6e4 100644 --- a/src/lua_mobjlib.c +++ b/src/lua_mobjlib.c @@ -669,7 +669,7 @@ static int mobj_set(lua_State *L) for (i = 0; i < numskins; i++) if (fastcmp(skins[i].name, skin)) { - if (!mo->player || R_SkinUsable(mo->player-players, i)) + if (!mo->player || R_SkinUsable(mo->player-players, i, false)) mo->skin = &skins[i]; return 0; } diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c index b69449ba2..7195c90f0 100644 --- a/src/lua_playerlib.c +++ b/src/lua_playerlib.c @@ -232,6 +232,8 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->spinouttimer); else if (fastcmp(field,"instashield")) lua_pushinteger(L, plr->instashield); + else if (fastcmp(field,"invulnhitlag")) + lua_pushinteger(L, plr->invulnhitlag); else if (fastcmp(field,"wipeoutslow")) lua_pushinteger(L, plr->wipeoutslow); else if (fastcmp(field,"justbumped")) @@ -408,8 +410,6 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->skincolor); else if (fastcmp(field,"skin")) lua_pushinteger(L, plr->skin); - else if (fastcmp(field,"availabilities")) - lua_pushinteger(L, plr->availabilities); else if (fastcmp(field,"fakeskin")) lua_pushinteger(L, plr->fakeskin); else if (fastcmp(field,"lastfakeskin")) @@ -474,6 +474,10 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->lastsidehit); else if (fastcmp(field,"lastlinehit")) lua_pushinteger(L, plr->lastlinehit); + else if (fastcmp(field,"timeshit")) + lua_pushinteger(L, plr->timeshit); + else if (fastcmp(field,"timeshitprev")) + lua_pushinteger(L, plr->timeshitprev); else if (fastcmp(field,"onconveyor")) lua_pushinteger(L, plr->onconveyor); else if (fastcmp(field,"awayviewmobj")) @@ -577,8 +581,6 @@ static int player_set(lua_State *L) } else if (fastcmp(field,"skin")) return NOSET; - else if (fastcmp(field,"availabilities")) - return NOSET; else if (fastcmp(field,"fakeskin")) return NOSET; else if (fastcmp(field,"lastfakeskin")) @@ -608,6 +610,8 @@ static int player_set(lua_State *L) plr->spinouttimer = luaL_checkinteger(L, 3); else if (fastcmp(field,"instashield")) plr->instashield = luaL_checkinteger(L, 3); + else if (fastcmp(field,"invulnhitlag")) + plr->invulnhitlag = luaL_checkinteger(L, 3); else if (fastcmp(field,"wipeoutslow")) plr->wipeoutslow = luaL_checkinteger(L, 3); else if (fastcmp(field,"justbumped")) @@ -834,6 +838,10 @@ static int player_set(lua_State *L) plr->lastsidehit = (INT16)luaL_checkinteger(L, 3); else if (fastcmp(field,"lastlinehit")) plr->lastlinehit = (INT16)luaL_checkinteger(L, 3); + else if (fastcmp(field,"timeshit")) + plr->timeshit = (UINT8)luaL_checkinteger(L, 3); + else if (fastcmp(field,"timeshitprev")) + plr->timeshitprev = (UINT8)luaL_checkinteger(L, 3); else if (fastcmp(field,"onconveyor")) plr->onconveyor = (INT32)luaL_checkinteger(L, 3); else if (fastcmp(field,"awayviewmobj")) diff --git a/src/lua_script.c b/src/lua_script.c index 4fcd7dd00..4f9eab969 100644 --- a/src/lua_script.c +++ b/src/lua_script.c @@ -403,8 +403,6 @@ int LUA_WriteGlobals(lua_State *L, const char *word) skincolor_bluering = (UINT16)luaL_checkinteger(L, 2); else if (fastcmp(word, "emeralds")) emeralds = (UINT16)luaL_checkinteger(L, 2); - else if (fastcmp(word, "token")) - token = (UINT32)luaL_checkinteger(L, 2); else if (fastcmp(word, "gravity")) gravity = (fixed_t)luaL_checkinteger(L, 2); else if (fastcmp(word, "stoppedclock")) diff --git a/src/m_cheat.c b/src/m_cheat.c index be4f231e1..edd23041c 100644 --- a/src/m_cheat.c +++ b/src/m_cheat.c @@ -76,16 +76,16 @@ static UINT8 cheatf_warp(void) { if (!unlockables[i].conditionset) continue; - if (!unlockables[i].unlocked) + if (!gamedata->unlocked[i]) { - unlockables[i].unlocked = true; + gamedata->unlocked[i] = true; success = true; } } if (success) { - G_SaveGameData(); //G_SetUsedCheats(); + G_SetUsedCheats(); S_StartSound(0, sfx_kc42); } @@ -111,7 +111,7 @@ static UINT8 cheatf_devmode(void) // Just unlock all the things and turn on -debug and console devmode. G_SetUsedCheats(); for (i = 0; i < MAXUNLOCKABLES; i++) - unlockables[i].unlocked = true; + gamedata->unlocked[i] = true; devparm = true; cht_debug |= 0x8000; diff --git a/src/m_cond.c b/src/m_cond.c index 1568d0845..052f23dc0 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -11,6 +11,7 @@ /// \brief Unlockable condition system for SRB2 version 2.1 #include "m_cond.h" +#include "m_random.h" // M_RandomKey #include "doomstat.h" #include "z_zone.h" @@ -19,11 +20,16 @@ #include "g_game.h" // record info #include "r_skins.h" // numskins +#include "k_follower.h" #include "r_draw.h" // R_GetColorByName +#include "s_sound.h" // S_StartSound #include "k_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; @@ -34,15 +40,383 @@ conditionset_t conditionSets[MAXCONDITIONSETS]; // Emblem locations emblem_t emblemlocations[MAXEMBLEMS]; -// Extra emblems -extraemblem_t extraemblems[MAXEXTRAEMBLEMS]; - // Unlockables unlockable_t unlockables[MAXUNLOCKABLES]; -// Number of emblems and extra emblems +// Number of emblems INT32 numemblems = 0; -INT32 numextraemblems = 0; + +// Create a new gamedata_t, for start-up +void M_NewGameDataStruct(void) +{ + gamedata = Z_Calloc(sizeof (gamedata_t), PU_STATIC, NULL); + M_ClearSecrets(); + G_ClearRecords(); +} + +void M_PopulateChallengeGrid(void) +{ + UINT16 i, j; + UINT16 numunlocks = 0, nummajorunlocks = 0, numempty = 0; + UINT8 selection[2][MAXUNLOCKABLES + (CHALLENGEGRIDHEIGHT-1)]; + UINT16 majorcompact = 2; + + if (gamedata->challengegrid != NULL) + { + // todo tweak your grid if unlocks are changed + return; + } + + // Go through unlockables + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + if (!unlockables[i].conditionset) + { + continue; + } + + if (unlockables[i].majorunlock) + { + selection[1][nummajorunlocks++] = i; + //CONS_Printf(" found %d (LARGE)\n", selection[1][nummajorunlocks-1]); + continue; + } + + selection[0][numunlocks++] = i; + //CONS_Printf(" found %d\n", selection[0][numunlocks-1]); + } + + gamedata->challengegridwidth = 0; + + if (numunlocks + nummajorunlocks == 0) + { + return; + } + + if (nummajorunlocks) + { + // Getting the number of 2-highs you can fit into two adjacent columns. + UINT8 majorpad = (CHALLENGEGRIDHEIGHT/2); + majorpad = (nummajorunlocks+1)/majorpad; + + gamedata->challengegridwidth = majorpad*2; + +#if (CHALLENGEGRIDHEIGHT % 2) + // One empty per column. + numempty = gamedata->challengegridwidth; +#endif + } + + if (numunlocks > numempty) + { + // Getting the number of extra columns to store normal unlocks + gamedata->challengegridwidth += ((numunlocks - numempty) + (CHALLENGEGRIDHEIGHT-1))/CHALLENGEGRIDHEIGHT; + majorcompact = 1; + } + else if (challengegridloops) + { + // Another case where offset large tiles are permitted. + majorcompact = 1; + } + + gamedata->challengegrid = Z_Malloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), + PU_STATIC, NULL); + + if (!gamedata->challengegrid) + { + I_Error("M_PopulateChallengeGrid: was not able to allocate grid"); + } + + memset(gamedata->challengegrid, + MAXUNLOCKABLES, + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8))); + + // Attempt to place all large tiles first. + if (nummajorunlocks) + { + // You lose one from CHALLENGEGRIDHEIGHT because it is impossible to place a 2-high tile on the bottom row. + // You lose one from the width if it doesn't loop. + // You divide by two if the grid is so compacted that large tiles can't be in offset columns. + UINT16 numspots = (gamedata->challengegridwidth - (challengegridloops ? 0 : majorcompact)) + * ((CHALLENGEGRIDHEIGHT-1) / majorcompact); + // 0 is row, 1 is column + INT16 quickcheck[numspots][2]; + + // Prepare the easy-grab spots. + for (i = 0; i < numspots; i++) + { + quickcheck[i][0] = i%(CHALLENGEGRIDHEIGHT-1); + quickcheck[i][1] = majorcompact * i/(CHALLENGEGRIDHEIGHT-1); + } + + // Place in random valid locations. + while (nummajorunlocks > 0 && numspots > 0) + { + INT16 row, col; + j = M_RandomKey(numspots); + row = quickcheck[j][0]; + col = quickcheck[j][1]; + + // We always take from selection[1][] in order, but the PLACEMENT is still random. + nummajorunlocks--; + + //CONS_Printf("--- %d (LARGE) placed at (%d, %d)\n", selection[1][nummajorunlocks], row, col); + + i = row + (col * CHALLENGEGRIDHEIGHT); + gamedata->challengegrid[i] = gamedata->challengegrid[i+1] = selection[1][nummajorunlocks]; + if (col == gamedata->challengegridwidth-1) + { + i = row; + } + else + { + i += CHALLENGEGRIDHEIGHT; + } + gamedata->challengegrid[i] = gamedata->challengegrid[i+1] = selection[1][nummajorunlocks]; + + if (nummajorunlocks == 0) + { + break; + } + + for (i = 0; i < numspots; i++) + { +quickcheckagain: + if (abs((quickcheck[i][0]) - (row)) <= 1 // Row distance + && (abs((quickcheck[i][1]) - (col)) <= 1 // Column distance + || (quickcheck[i][1] == 0 && col == gamedata->challengegridwidth-1) // Wraparounds l->r + || (quickcheck[i][1] == gamedata->challengegridwidth-1 && col == 0))) // Wraparounds r->l + { + numspots--; // Remove from possible indicies + if (i == numspots) + break; + // Shuffle remaining so we can keep on using M_RandomKey + quickcheck[i][0] = quickcheck[numspots][0]; + quickcheck[i][1] = quickcheck[numspots][1]; + // Woah there - we've gotta check the one that just got put in our place. + goto quickcheckagain; + } + continue; + } + } + + if (nummajorunlocks > 0) + { + I_Error("M_PopulateChallengeGrid: was not able to populate %d large tiles (width %d)", nummajorunlocks, gamedata->challengegridwidth); + } + } + + numempty = 0; + // Space out empty entries to pepper into unlock list + for (i = 0; i < gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; i++) + { + if (gamedata->challengegrid[i] < MAXUNLOCKABLES) + { + continue; + } + + numempty++; + } + + if (numunlocks > numempty) + { + I_Error("M_PopulateChallengeGrid: %d small unlocks vs %d empty spaces (%d gap)", numunlocks, numempty, (numunlocks-numempty)); + } + + //CONS_Printf(" %d unlocks vs %d empty spaces\n", numunlocks, numempty); + + while (numunlocks < numempty) + { + //CONS_Printf(" adding empty)\n"); + selection[0][numunlocks++] = MAXUNLOCKABLES; + } + + // Fill the remaining spots with random ordinary unlocks (and empties). + for (i = 0; i < gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; i++) + { + if (gamedata->challengegrid[i] < MAXUNLOCKABLES) + { + continue; + } + + j = M_RandomKey(numunlocks); // Get an entry + gamedata->challengegrid[i] = selection[0][j]; // Set that entry + //CONS_Printf(" %d placed at (%d, %d)\n", selection[0][j], i/CHALLENGEGRIDHEIGHT, i%CHALLENGEGRIDHEIGHT); + numunlocks--; // Remove from possible indicies + selection[0][j] = selection[0][numunlocks]; // Shuffle remaining so we can keep on using M_RandomKey + + if (numunlocks == 0) + { + break; + } + } +} + +UINT8 *M_ChallengeGridExtraData(void) +{ + UINT8 i, j, num, id, tempid, work; + UINT8 *extradata; + boolean idchange; + + if (!gamedata->challengegrid) + { + return NULL; + } + + extradata = Z_Malloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), + PU_STATIC, NULL); + + if (!extradata) + { + I_Error("M_ChallengeGridExtraData: was not able to allocate extradata"); + } + + //CONS_Printf(" --- \n"); + + for (i = 0; i < gamedata->challengegridwidth; i++) + { + for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) + { + id = (i * CHALLENGEGRIDHEIGHT) + j; + num = gamedata->challengegrid[id]; + idchange = false; + + extradata[id] = CHE_NONE; + + // Empty spots in the grid are always unconnected. + if (num >= MAXUNLOCKABLES) + { + continue; + } + + // Check the spot above. + if (j > 0) + { + tempid = (i * CHALLENGEGRIDHEIGHT) + (j - 1); + work = gamedata->challengegrid[tempid]; + if (work == num) + { + extradata[id] = CHE_CONNECTEDUP; + + // Get the id to write extra hint data to. + // This check is safe because extradata's order of population + if (extradata[tempid] & CHE_CONNECTEDLEFT) + { + extradata[id] |= CHE_CONNECTEDLEFT; + //CONS_Printf(" %d - %d above %d is invalid, check to left\n", num, tempid, id); + if (i > 0) + { + tempid -= CHALLENGEGRIDHEIGHT; + } + else + { + tempid = ((gamedata->challengegridwidth - 1) * CHALLENGEGRIDHEIGHT) + j - 1; + } + } + /*else + CONS_Printf(" %d - %d above %d is valid\n", num, tempid, id);*/ + + id = tempid; + idchange = true; + + if (extradata[id] == CHE_HINT) + { + continue; + } + } + else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + { + extradata[id] = CHE_HINT; + } + } + + // Check the spot to the left. + { + if (i > 0) + { + tempid = ((i - 1) * CHALLENGEGRIDHEIGHT) + j; + } + else + { + tempid = ((gamedata->challengegridwidth - 1) * CHALLENGEGRIDHEIGHT) + j; + } + work = gamedata->challengegrid[tempid]; + + if (work == num) + { + if (!idchange && (i > 0 || challengegridloops)) + { + //CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id); + // If we haven't already updated our id, it's the one to our left. + if (extradata[id] == CHE_HINT) + { + extradata[tempid] = CHE_HINT; + } + extradata[id] = CHE_CONNECTEDLEFT; + id = tempid; + } + /*else + CONS_Printf(" %d - %d to left of %d is invalid\n", work, tempid, id);*/ + } + else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + { + extradata[id] = CHE_HINT; + continue; + } + } + + // Since we're not modifying id past this point, the conditions become much simpler. + if (extradata[id] == CHE_HINT) + { + continue; + } + + // Check the spot below. + if (j < CHALLENGEGRIDHEIGHT-1) + { + tempid = (i * CHALLENGEGRIDHEIGHT) + (j + 1); + work = gamedata->challengegrid[tempid]; + + if (work == num) + { + ; + } + else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + { + extradata[id] = CHE_HINT; + continue; + } + } + + // Check the spot to the right. + { + if (i < (gamedata->challengegridwidth - 1)) + { + tempid = ((i + 1) * CHALLENGEGRIDHEIGHT) + j; + } + else + { + tempid = j; + } + work = gamedata->challengegrid[tempid]; + + if (work == num) + { + ; + } + else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + { + extradata[id] = CHE_HINT; + continue; + } + } + } + } + + return extradata; +} void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2) { @@ -51,12 +425,12 @@ void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1 I_Assert(set && set <= MAXCONDITIONSETS); - wnum = conditionSets[set - 1].numconditions; - num = ++conditionSets[set - 1].numconditions; + wnum = conditionSets[set].numconditions; + num = ++conditionSets[set].numconditions; - conditionSets[set - 1].condition = Z_Realloc(conditionSets[set - 1].condition, sizeof(condition_t)*num, PU_STATIC, 0); + conditionSets[set].condition = Z_Realloc(conditionSets[set].condition, sizeof(condition_t)*num, PU_STATIC, 0); - cond = conditionSets[set - 1].condition; + cond = conditionSets[set].condition; cond[wnum].id = id; cond[wnum].type = c; @@ -67,13 +441,13 @@ void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1 void M_ClearConditionSet(UINT8 set) { - if (conditionSets[set - 1].numconditions) + if (conditionSets[set].numconditions) { - Z_Free(conditionSets[set - 1].condition); - conditionSets[set - 1].condition = NULL; - conditionSets[set - 1].numconditions = 0; + Z_Free(conditionSets[set].condition); + conditionSets[set].condition = NULL; + conditionSets[set].numconditions = 0; } - conditionSets[set - 1].achieved = false; + gamedata->achieved[set] = false; } // Clear ALL secrets. @@ -87,31 +461,35 @@ void M_ClearSecrets(void) } for (i = 0; i < MAXEMBLEMS; ++i) - emblemlocations[i].collected = false; - for (i = 0; i < MAXEXTRAEMBLEMS; ++i) - extraemblems[i].collected = false; + gamedata->collected[i] = false; for (i = 0; i < MAXUNLOCKABLES; ++i) - unlockables[i].unlocked = false; + gamedata->unlocked[i] = gamedata->unlockpending[i] = netUnlocked[i] = false; for (i = 0; i < MAXCONDITIONSETS; ++i) - conditionSets[i].achieved = false; + gamedata->achieved[i] = false; - timesBeaten = 0; + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = NULL; + gamedata->challengegridwidth = 0; + + gamedata->timesBeaten = 0; // Re-unlock any always unlocked things - M_SilentUpdateUnlockablesAndEmblems(); + M_UpdateUnlockablesAndExtraEmblems(false); } // ---------------------- // Condition set checking // ---------------------- + +// See also M_GetConditionString UINT8 M_CheckCondition(condition_t *cn) { switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x - return (totalplaytime >= (unsigned)cn->requirement); + return (gamedata->totalplaytime >= (unsigned)cn->requirement); case UC_MATCHESPLAYED: // Requires any level completed >= x times - return (matchesplayed >= (unsigned)cn->requirement); + return (gamedata->matchesplayed >= (unsigned)cn->requirement); case UC_POWERLEVEL: // Requires power level >= x on a certain gametype { UINT8 i; @@ -128,7 +506,7 @@ UINT8 M_CheckCondition(condition_t *cn) return false; } case UC_GAMECLEAR: // Requires game beaten >= x times - return (timesBeaten >= (unsigned)cn->requirement); + return (gamedata->timesBeaten >= (unsigned)cn->requirement); case UC_OVERALLTIME: // Requires overall time <= x return (M_GotLowEnoughTime(cn->requirement)); case UC_MAPVISITED: // Requires map x to be visited @@ -149,12 +527,12 @@ UINT8 M_CheckCondition(condition_t *cn) return (G_GetBestTime(cn->extrainfo1) <= (unsigned)cn->requirement); case UC_TRIGGER: // requires map trigger set return !!(unlocktriggers & (1 << cn->requirement)); - case UC_TOTALEMBLEMS: // Requires number of emblems >= x - return (M_GotEnoughEmblems(cn->requirement)); + case UC_TOTALMEDALS: // Requires number of emblems >= x + return (M_GotEnoughMedals(cn->requirement)); case UC_EMBLEM: // Requires emblem x to be obtained - return emblemlocations[cn->requirement-1].collected; - case UC_EXTRAEMBLEM: // Requires extra emblem x to be obtained - return extraemblems[cn->requirement-1].collected; + return gamedata->collected[cn->requirement-1]; + case UC_UNLOCKABLE: // Requires unlockable x to be obtained + return gamedata->unlocked[cn->requirement-1]; case UC_CONDITIONSET: // requires condition set x to already be achieved return M_Achieved(cn->requirement-1); } @@ -188,7 +566,283 @@ static UINT8 M_CheckConditionSet(conditionset_t *c) return achievedSoFar; } -void M_CheckUnlockConditions(void) +static char *M_BuildConditionTitle(UINT16 map) +{ + char *title, *ref; + + if (M_MapLocked(map+1)) + return Z_StrDup("???"); + + title = ref = G_BuildMapTitle(map+1); + + if (!title) + I_Error("M_BuildConditionTitle: out of memory"); + + while (*ref != '\0') + { + *ref = toupper(*ref); + ref++; + } + + return title; +} + +// See also M_CheckCondition +static const char *M_GetConditionString(condition_t *cn) +{ + INT32 i; + char *title = NULL; + const char *work = NULL; + +#define BUILDCONDITIONTITLE(i) (M_BuildConditionTitle(i)) + + switch (cn->type) + { + case UC_PLAYTIME: // Requires total playing time >= x + 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_POWERLEVEL: // Requires power level >= x on a certain gametype + 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); + else + return va("Beat the game"); + case UC_OVERALLTIME: // Requires overall time <= x + return va("Get overall time of %i:%02i:%02i", + G_TicsToHours(cn->requirement), + G_TicsToMinutes(cn->requirement, false), + G_TicsToSeconds(cn->requirement)); + case UC_MAPVISITED: // Requires map x to be visited + case UC_MAPBEATEN: // Requires map x to be beaten + case UC_MAPENCORE: // Requires map x to be beaten in encore + { + 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" : "Beat", + 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, + G_TicsToMinutes(cn->requirement, true), + G_TicsToSeconds(cn->requirement), + G_TicsToCentiseconds(cn->requirement)); + + Z_Free(title); + return work; + } + case UC_TOTALMEDALS: // Requires number of emblems >= x + return va("Get %d medals", cn->requirement); + case UC_EMBLEM: // Requires emblem x to be obtained + { + INT32 checkLevel; + + i = cn->requirement-1; + checkLevel = M_EmblemMapNum(&emblemlocations[i]); + + if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) + return va("INVALID MEDAL MAP \"%d:%d\"", cn->requirement, checkLevel); + + title = BUILDCONDITIONTITLE(checkLevel); + switch (emblemlocations[i].type) + { + case ET_MAP: + work = va("Beat %s", title); + break; + case ET_TIME: + if (emblemlocations[i].color <= 0 || emblemlocations[i].color >= numskincolors) + { + Z_Free(title); + return va("INVALID MEDAL COLOR \"%d:%d\"", cn->requirement, checkLevel); + } + work = va("Get the %s Medal for %s", skincolors[emblemlocations[i].color].name, title); + break; + case ET_GLOBAL: + { + const char *astr, *colorstr, *medalstr; + + if (emblemlocations[i].flags & GE_NOTMEDAL) + { + astr = "a "; + colorstr = ""; + medalstr = "secret"; + } + else if (emblemlocations[i].color <= 0 || emblemlocations[i].color >= numskincolors) + { + Z_Free(title); + return va("INVALID MEDAL COLOR \"%d:%d:%d\"", cn->requirement, emblemlocations[i].tag, checkLevel); + } + else + { + astr = "the "; + colorstr = skincolors[emblemlocations[i].color].name; + medalstr = " Medal"; + } + + if (emblemlocations[i].flags & GE_TIMED) + { + work = va("Find %s%s%s in %s before %i:%02i.%02i", + astr, colorstr, medalstr, title, + 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); + } + break; + } + default: + work = va("Find a secret in %s", title); + break; + } + + Z_Free(title); + return work; + } + case UC_UNLOCKABLE: // Requires unlockable x to be obtained + return va("Get \"%s\"", + gamedata->unlocked[cn->requirement-1] + ? unlockables[cn->requirement-1].name + : "???"); + default: + break; + } + // UC_MAPTRIGGER and UC_CONDITIONSET are explicitly very hard to support proper descriptions for + return va("UNSUPPORTED CONDITION \"%d\"", cn->type); + +#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; + + message[0] = '\0'; + + if (unlockid >= MAXUNLOCKABLES) + { + return NULL; + } + + if (!unlockables[unlockid].conditionset) + { + return NULL; + } + + c = &conditionSets[unlockables[unlockid].conditionset-1]; + + for (i = 0; i < c->numconditions; ++i) + { + cn = &c->condition[i]; + + if (i > 0) + { + worklen = 3; + if (lastID == cn->id) + { + strncat(message, "\n& ", len); + } + else + { + strncat(message, "\nOR ", len); + worklen++; + } + len -= worklen; + } + lastID = cn->id; + +#ifdef ACHIEVEDBRITE + achieved = M_CheckCondition(cn); + + if (achieved) + { + strncat(message, "\0x82", len); + len--; + } +#endif + + work = M_GetConditionString(cn); + worklen = strlen(work); + + strncat(message, work, len); + len -= worklen; + +#ifdef ACHIEVEDBRITE + if (achieved) + { + strncat(message, "\0x80", len); + len--; + } +#endif + } + + // Rudementary word wrapping. + // Simple and effective. Does not handle nonuniform letter sizes, etc. but who cares. + for (i = 0; message[i]; i++) + { + if (message[i] == ' ') + { + start = i; + max += 4; + } + else if (message[i] == '\n') + { + strlines = i; + start = 0; + max = 0; + continue; + } + else if (message[i] & 0x80) + continue; + else + max += 8; + + // Start trying to wrap if presumed length exceeds the space we have on-screen. + if (max >= DESCRIPTIONWIDTH && start > 0) + { + message[start] = '\n'; + max -= (start-strlines)*8; + strlines = start; + start = 0; + } + } + + return message; +} + +static void M_CheckUnlockConditions(void) { INT32 i; conditionset_t *c; @@ -196,105 +850,89 @@ void M_CheckUnlockConditions(void) for (i = 0; i < MAXCONDITIONSETS; ++i) { c = &conditionSets[i]; - if (!c->numconditions || c->achieved) + if (!c->numconditions || gamedata->achieved[i]) continue; - c->achieved = (M_CheckConditionSet(c)); + gamedata->achieved[i] = (M_CheckConditionSet(c)); } } -UINT8 M_UpdateUnlockablesAndExtraEmblems(void) +boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) { INT32 i; - char cechoText[992] = ""; - UINT8 cechoLines = 0; + UINT8 response = 0; - M_CheckUnlockConditions(); - - // Go through extra emblems - for (i = 0; i < numextraemblems; ++i) + if (!loud) { - if (extraemblems[i].collected || !extraemblems[i].conditionset) - continue; - if ((extraemblems[i].collected = M_Achieved(extraemblems[i].conditionset - 1)) != false) - { - strcat(cechoText, va(M_GetText("Got \"%s\" medal!\\"), extraemblems[i].name)); - ++cechoLines; - } + // Just in case they aren't to sync + // Done first so that emblems are ready before check + M_CheckLevelEmblems(); + M_CompletionEmblems(); } - // Fun part: if any of those unlocked we need to go through the - // unlock conditions AGAIN just in case an emblem reward was reached - if (cechoLines) - M_CheckUnlockConditions(); + M_CheckUnlockConditions(); // Go through unlockables for (i = 0; i < MAXUNLOCKABLES; ++i) { - if (unlockables[i].unlocked || !unlockables[i].conditionset) - continue; - if ((unlockables[i].unlocked = M_Achieved(unlockables[i].conditionset - 1)) != false) + if (gamedata->unlocked[i] || !unlockables[i].conditionset) { - if (unlockables[i].nocecho) - continue; - strcat(cechoText, va(M_GetText("\"%s\" unlocked!\\"), unlockables[i].name)); - ++cechoLines; + continue; } + + if (gamedata->unlocked[i] == true + || gamedata->unlockpending[i] == true) + { + continue; + } + + if (M_Achieved(unlockables[i].conditionset - 1) == false) + { + continue; + } + + gamedata->unlockpending[i] = true; + response++; } // Announce - if (cechoLines) + if (response) { - char slashed[1024] = ""; - for (i = 0; (i < 19) && (i < 24 - cechoLines); ++i) - slashed[i] = '\\'; - slashed[i] = 0; - - strcat(slashed, cechoText); - - HU_SetCEchoFlags(V_YELLOWMAP); - HU_SetCEchoDuration(6); - HU_DoCEcho(slashed); + if (loud) + { + S_StartSound(NULL, sfx_ncitem); + } return true; } return false; } -// Used when loading gamedata to make sure all unlocks are synched with conditions -void M_SilentUpdateUnlockablesAndEmblems(void) +UINT8 M_GetNextAchievedUnlock(void) { - INT32 i; - boolean checkAgain = false; - - // Just in case they aren't to sync - M_CheckUnlockConditions(); - M_CheckLevelEmblems(); - - // Go through extra emblems - for (i = 0; i < numextraemblems; ++i) - { - if (extraemblems[i].collected || !extraemblems[i].conditionset) - continue; - if ((extraemblems[i].collected = M_Achieved(extraemblems[i].conditionset - 1)) != false) - checkAgain = true; - } - - // check again if extra emblems unlocked, blah blah, etc - if (checkAgain) - M_CheckUnlockConditions(); + UINT8 i; // Go through unlockables for (i = 0; i < MAXUNLOCKABLES; ++i) { - if (unlockables[i].unlocked || !unlockables[i].conditionset) + if (gamedata->unlocked[i] || !unlockables[i].conditionset) + { continue; - unlockables[i].unlocked = M_Achieved(unlockables[i].conditionset - 1); + } + + if (gamedata->unlocked[i] == true) + { + continue; + } + + if (gamedata->unlockpending[i] == false) + { + continue; + } + + return i; } - for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) - { - players[g_localplayers[i]].availabilities = R_GetSkinAvailabilities(); - } + return MAXUNLOCKABLES; } // Emblem unlocking shit @@ -311,10 +949,10 @@ UINT8 M_CheckLevelEmblems(void) { INT32 checkLevel; - if (emblemlocations[i].type < ET_TIME || emblemlocations[i].collected) + if (emblemlocations[i].type < ET_TIME || gamedata->collected[i]) continue; - checkLevel = G_MapNumber(emblemlocations[i].level); + checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) continue; @@ -331,7 +969,7 @@ UINT8 M_CheckLevelEmblems(void) continue; } - emblemlocations[i].collected = res; + gamedata->collected[i] = res; if (res) ++somethingUnlocked; } @@ -351,16 +989,16 @@ UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separa { INT32 checkLevel; - if (emblemlocations[i].type < ET_TIME || emblemlocations[i].collected) + if (emblemlocations[i].type < ET_TIME || gamedata->collected[i]) continue; - checkLevel = G_MapNumber(emblemlocations[i].level); + checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) continue; levelnum = checkLevel; - embtype = emblemlocations[i].var; + embtype = emblemlocations[i].flags; flags = MV_BEATEN; if (embtype & ME_ENCORE) @@ -368,96 +1006,145 @@ UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separa res = ((mapheaderinfo[levelnum]->mapvisited & flags) == flags); - emblemlocations[i].collected = res; + gamedata->collected[i] = res; if (res) ++somethingUnlocked; } + return somethingUnlocked; } // ------------------- // Quick unlock checks // ------------------- -UINT8 M_AnySecretUnlocked(void) -{ - INT32 i; -#ifdef DEVELOP - if (1) - return true; -#endif - - for (i = 0; i < MAXUNLOCKABLES; ++i) +boolean M_CheckNetUnlockByID(UINT8 unlockid) +{ + if (unlockid >= MAXUNLOCKABLES + || !unlockables[unlockid].conditionset) { - if (!unlockables[i].nocecho && unlockables[i].unlocked) - return true; + return true; // default permit } - return false; + + if (netgame) + { + return netUnlocked[unlockid]; + } + + return gamedata->unlocked[unlockid]; } -UINT8 M_SecretUnlocked(INT32 type) +boolean M_SecretUnlocked(INT32 type, boolean local) { INT32 i; - if (dedicated) - return true; - #if 0 (void)type; (void)i; return false; // for quick testing #else -#ifdef DEVELOP -#define CHADYES true -#else -#define CHADYES false -#endif - for (i = 0; i < MAXUNLOCKABLES; ++i) { - if (unlockables[i].type == type && unlockables[i].unlocked != CHADYES) - return !CHADYES; + if (unlockables[i].type != type) + continue; + if ((local && gamedata->unlocked[i]) + || (!local && M_CheckNetUnlockByID(i))) + continue; + return false; } - return CHADYES; -#undef CHADYES + return true; + #endif //if 0 } -UINT8 M_MapLocked(INT32 mapnum) +boolean M_CupLocked(cupheader_t *cup) { -#ifdef DEVELOP - (void)mapnum; - return false; -#else + UINT8 i; + // Don't lock maps in dedicated servers. // That just makes hosts' lives hell. if (dedicated) return false; - - if (!mapheaderinfo[mapnum-1] || mapheaderinfo[mapnum-1]->unlockrequired < 0) + + // No skipping over any part of your marathon. + if (marathonmode) return false; - if (!unlockables[mapheaderinfo[mapnum-1]->unlockrequired].unlocked) - return true; + if (!cup) + return false; + + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + if (unlockables[i].type != SECRET_CUP) + continue; + if (M_UnlockableCup(&unlockables[i]) != cup) + continue; + return !M_CheckNetUnlockByID(i); + } return false; -#endif } -INT32 M_CountEmblems(void) +boolean M_MapLocked(INT32 mapnum) +{ + UINT8 i; + + // Don't lock maps in dedicated servers. + // That just makes hosts' lives hell. + if (dedicated) + return false; + + // No skipping over any part of your marathon. + if (marathonmode) + return false; + + if (!mapnum || mapnum > nummapheaders) + return false; + + if (!mapheaderinfo[mapnum-1]) + return false; + + if (mapheaderinfo[mapnum-1]->cup) + { + return M_CupLocked(mapheaderinfo[mapnum-1]->cup); + } + + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + if (unlockables[i].type != SECRET_MAP) + continue; + if (M_UnlockableMapNum(&unlockables[i]) != mapnum-1) + continue; + return !M_CheckNetUnlockByID(i); + } + + return false; +} + +INT32 M_CountMedals(boolean all, boolean extraonly) { INT32 found = 0, i; - for (i = 0; i < numemblems; ++i) + if (!extraonly) { - if (emblemlocations[i].collected) + for (i = 0; i < numemblems; ++i) + { + if ((emblemlocations[i].type == ET_GLOBAL) + && (emblemlocations[i].flags & GE_NOTMEDAL)) + continue; + if (!all && !gamedata->collected[i]) + continue; found++; + } } - for (i = 0; i < numextraemblems; ++i) + for (i = 0; i < MAXUNLOCKABLES; ++i) { - if (extraemblems[i].collected) - found++; + if (unlockables[i].type != SECRET_EXTRAMEDAL) + continue; + if (!all && !gamedata->unlocked[i]) + continue; + found++; } return found; } @@ -466,20 +1153,28 @@ INT32 M_CountEmblems(void) // Quick functions for calculating things // -------------------------------------- -// Theoretically faster than using M_CountEmblems() -// Stops when it reaches the target number of emblems. -UINT8 M_GotEnoughEmblems(INT32 number) +// Theoretically faster than using M_CountMedals() +// Stops when it reaches the target number of medals. +UINT8 M_GotEnoughMedals(INT32 number) { - INT32 i, gottenemblems = 0; + INT32 i, gottenmedals = 0; for (i = 0; i < numemblems; ++i) { - if (emblemlocations[i].collected) - if (++gottenemblems >= number) return true; + if (!gamedata->collected[i]) + continue; + if (++gottenmedals < number) + continue; + return true; } - for (i = 0; i < numextraemblems; ++i) + for (i = 0; i < MAXUNLOCKABLES; ++i) { - if (extraemblems[i].collected) - if (++gottenemblems >= number) return true; + if (unlockables[i].type != SECRET_EXTRAMEDAL) + continue; + if (!gamedata->unlocked[i]) + continue; + if (++gottenmedals < number) + continue; + return true; } return false; } @@ -502,10 +1197,184 @@ UINT8 M_GotLowEnoughTime(INT32 tictime) return true; } +// Gets the skin number for a SECRET_SKIN unlockable. +INT32 M_UnlockableSkinNum(unlockable_t *unlock) +{ + if (unlock->type != SECRET_SKIN) + { + // This isn't a skin unlockable... + return -1; + } + + if (unlock->stringVar && unlock->stringVar[0]) + { + INT32 skinnum; + + if (unlock->stringVarCache != -1) + { + return unlock->stringVarCache; + } + + // Get the skin from the string. + skinnum = R_SkinAvailable(unlock->stringVar); + if (skinnum != -1) + { + unlock->stringVarCache = skinnum; + return skinnum; + } + } + + if (unlock->variable >= 0 && unlock->variable < numskins) + { + // Use the number directly. + return unlock->variable; + } + + // Invalid skin unlockable. + return -1; +} + +// Gets the skin number for a SECRET_FOLLOWER unlockable. +INT32 M_UnlockableFollowerNum(unlockable_t *unlock) +{ + if (unlock->type != SECRET_FOLLOWER) + { + // This isn't a follower unlockable... + return -1; + } + + if (unlock->stringVar && unlock->stringVar[0]) + { + INT32 skinnum; + size_t i; + char testname[SKINNAMESIZE+1]; + + if (unlock->stringVarCache != -1) + { + return unlock->stringVarCache; + } + + // match deh_soc readfollower() + for (i = 0; unlock->stringVar[i]; i++) + { + testname[i] = unlock->stringVar[i]; + if (unlock->stringVar[i] == '_') + testname[i] = ' '; + } + testname[i] = '\0'; + + // Get the skin from the string. + skinnum = K_FollowerAvailable(testname); + if (skinnum != -1) + { + unlock->stringVarCache = skinnum; + return skinnum; + } + } + + if (unlock->variable >= 0 && unlock->variable < numfollowers) + { + // Use the number directly. + return unlock->variable; + } + + // Invalid follower unlockable. + return -1; +} + +cupheader_t *M_UnlockableCup(unlockable_t *unlock) +{ + cupheader_t *cup = kartcupheaders; + INT16 val = unlock->variable-1; + + if (unlock->type != SECRET_CUP) + { + // This isn't a cup unlockable... + return NULL; + } + + if (unlock->stringVar && unlock->stringVar[0]) + { + if (unlock->stringVarCache == -1) + { + // Get the cup from the string. + while (cup) + { + if (!strcmp(cup->name, unlock->stringVar)) + break; + cup = cup->next; + } + + if (cup) + { + unlock->stringVarCache = cup->id; + } + return cup; + } + + val = unlock->stringVarCache; + } + else if (val == -1) + { + return NULL; + } + + // Use the number directly. + while (cup) + { + if (cup->id == val) + break; + cup = cup->next; + } + + return cup; +} + +UINT16 M_UnlockableMapNum(unlockable_t *unlock) +{ + if (unlock->type != SECRET_MAP) + { + // This isn't a map unlockable... + return NEXTMAP_INVALID; + } + + if (unlock->stringVar && unlock->stringVar[0]) + { + if (unlock->stringVarCache == -1) + { + INT32 result = G_MapNumber(unlock->stringVar); + + if (result >= nummapheaders) + return result; + + unlock->stringVarCache = result; + } + + return unlock->stringVarCache; + } + + return NEXTMAP_INVALID; +} + // ---------------- // Misc Emblem shit // ---------------- +UINT16 M_EmblemMapNum(emblem_t *emblem) +{ + if (emblem->levelCache == NEXTMAP_INVALID) + { + UINT16 result = G_MapNumber(emblem->level); + + if (result >= nummapheaders) + return result; + + emblem->levelCache = result; + } + + return emblem->levelCache; +} + // Returns pointer to an emblem if an emblem exists for that level. // Pass -1 mapnum to continue from last emblem. // NULL if not found. @@ -517,27 +1386,29 @@ emblem_t *M_GetLevelEmblems(INT32 mapnum) if (mapnum > 0) { - map = mapnum; + map = mapnum-1; i = numemblems; } while (--i >= 0) { - INT32 checkLevel = G_MapNumber(emblemlocations[i].level); + INT32 checkLevel = M_EmblemMapNum(&emblemlocations[i]); if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) continue; - if (checkLevel == map) - return &emblemlocations[i]; + if (checkLevel != map) + continue; + + return &emblemlocations[i]; } return NULL; } skincolornum_t M_GetEmblemColor(emblem_t *em) { - if (!em || em->color >= numskincolors) - return SKINCOLOR_NONE; + if (!em || !em->color || em->color >= numskincolors) + return SKINCOLOR_GOLD; return em->color; } @@ -559,29 +1430,3 @@ const char *M_GetEmblemPatch(emblem_t *em, boolean big) return pnamebuf; } - -skincolornum_t M_GetExtraEmblemColor(extraemblem_t *em) -{ - if (!em || em->color >= numskincolors) - return SKINCOLOR_NONE; - return em->color; -} - -const char *M_GetExtraEmblemPatch(extraemblem_t *em, boolean big) -{ - static char pnamebuf[7]; - - if (!big) - strcpy(pnamebuf, "GOTITn"); - else - strcpy(pnamebuf, "EMBMn0"); - - I_Assert(em->sprite >= 'A' && em->sprite <= 'Z'); - - if (!big) - pnamebuf[5] = em->sprite; - else - pnamebuf[4] = em->sprite; - - return pnamebuf; -} diff --git a/src/m_cond.h b/src/m_cond.h index c340fbef1..5c50146f0 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -30,9 +30,9 @@ typedef enum UC_MAPENCORE, // MAPENCORE [map number] UC_MAPTIME, // MAPTIME [map number] [time to beat, tics] UC_TRIGGER, // TRIGGER [trigger number] - UC_TOTALEMBLEMS, // TOTALEMBLEMS [number of emblems] + UC_TOTALMEDALS, // TOTALMEDALS [number of emblems] UC_EMBLEM, // EMBLEM [emblem number] - UC_EXTRAEMBLEM, // EXTRAEMBLEM [extra emblem number] + UC_UNLOCKABLE, // UNLOCKABLE [unlockable number] UC_CONDITIONSET, // CONDITIONSET [condition set number] } conditiontype_t; @@ -52,97 +52,147 @@ struct conditionset_t { UINT32 numconditions; /// <- number of conditions. condition_t *condition; /// <- All conditionals to be checked. - UINT8 achieved; /// <- Whether this conditional has been achieved already or not. - /// (Conditional checking is skipped if true -- it's assumed you can't relock an unlockable) }; // Emblem information -#define ET_GLOBAL 0 // Emblem with a position in space -#define ET_MAP 1 // Beat the map -#define ET_TIME 2 // Get the time +#define ET_GLOBAL 0 // Emblem with a position in space +#define ET_MAP 1 // Beat the map +#define ET_TIME 2 // Get the time //#define ET_DEVTIME 3 // Time, but the value is tied to a Time Trial demo, not pre-defined // Global emblem flags -// (N/A to Kart yet) -//#define GE_OH 1 +#define GE_NOTMEDAL 1 // Doesn't count towards number of medals +#define GE_TIMED 2 // Disappears after var time // Map emblem flags -#define ME_ENCORE 1 +#define ME_ENCORE 1 // Achieve in Encore struct emblem_t { - UINT8 type; ///< Emblem type - INT16 tag; ///< Tag of emblem mapthing - char * level; ///< Level on which this emblem can be found. - UINT8 sprite; ///< emblem sprite to use, 0 - 25 - UINT16 color; ///< skincolor to use - INT32 var; ///< If needed, specifies information on the target amount to achieve (or target skin) - char hint[110]; ///< Hint for emblem hints menu - UINT8 collected; ///< Do you have this emblem? -}; -struct extraemblem_t -{ - char name[20]; ///< Name of the goal (used in the "emblem awarded" cecho) - char description[40]; ///< Description of goal (used in statistics) - UINT8 conditionset; ///< Condition set that awards this emblem. - UINT8 showconditionset; ///< Condition set that shows this emblem. - UINT8 sprite; ///< emblem sprite to use, 0 - 25 - UINT16 color; ///< skincolor to use - UINT8 collected; ///< Do you have this emblem? + UINT8 type; ///< Emblem type + INT16 tag; ///< Tag of emblem mapthing + char *level; ///< Level on which this emblem can be found. + INT16 levelCache; ///< Stored G_MapNumber()+1 result + UINT8 sprite; ///< emblem sprite to use, 0 - 25 + UINT16 color; ///< skincolor to use + INT32 flags; ///< GE or ME constants + INT32 var; ///< If needed, specifies extra information + char *stringVar; ///< String version }; // Unlockable information struct unlockable_t { char name[64]; - char objective[64]; + char *icon; + UINT16 color; UINT8 conditionset; - UINT8 showconditionset; INT16 type; INT16 variable; - UINT8 nocecho; - UINT8 nochecklist; - UINT8 unlocked; + char *stringVar; + INT16 stringVarCache; + UINT8 majorunlock; }; -#define SECRET_NONE 0 // Does nil. Use with levels locked by UnlockRequired -#define SECRET_HEADER 1 // Does nothing on its own, just serves as a header for the menu +typedef enum +{ + SECRET_NONE = 0, // Does nil, useful as a default only -#define SECRET_SKIN 2 // Allow this character to be selected -#define SECRET_WARP 3 // Selectable warp -#define SECRET_LEVELSELECT 4 // Selectable level select + // One step above bragging rights + SECRET_EXTRAMEDAL, // Extra medal for your counter -#define SECRET_TIMEATTACK 5 // Enables Time Attack on the main menu -#define SECRET_BREAKTHECAPSULES 6 // Enables Break the Capsules on the main menu -#define SECRET_SOUNDTEST 7 // Sound Test -#define SECRET_CREDITS 8 // Enables Credits + // Level restrictions + SECRET_CUP, // Permit access to entire cup (overrides SECRET_MAP) + SECRET_MAP, // Permit access to single map -#define SECRET_ITEMFINDER 9 // Enables Item Finder/Emblem Radar -#define SECRET_EMBLEMHINTS 10 // Enables Emblem Hints + // Player restrictions + SECRET_SKIN, // Permit this character + SECRET_FOLLOWER, // Permit this follower -#define SECRET_ENCORE 11 // Enables Encore mode cvar -#define SECRET_HARDSPEED 12 // Enables Hard gamespeed -#define SECRET_HELLATTACK 13 // Map Hell in record attack + // Difficulty restrictions + SECRET_HARDSPEED, // Permit Hard gamespeed + SECRET_ENCORE, // Permit Encore option + SECRET_LEGACYBOXRUMMAGE, // Permit the Legacy Box for record attack, etc -#define SECRET_PANDORA 14 // Enables Pandora's Box + // Menu restrictions + SECRET_TIMEATTACK, // Permit Time attack + SECRET_BREAKTHECAPSULES, // Permit SP Capsules + SECRET_SOUNDTEST, // Permit Sound Test + SECRET_ALTTITLE, // Permit alternate titlescreen + + // Assist restrictions + SECRET_ITEMFINDER, // Permit locating in-level secrets +} secrettype_t; // If you have more secrets than these variables allow in your game, // you seriously need to get a life. -#define MAXCONDITIONSETS 128 +#define MAXCONDITIONSETS UINT8_MAX #define MAXEMBLEMS 512 -#define MAXEXTRAEMBLEMS 16 -#define MAXUNLOCKABLES 32 +#define MAXUNLOCKABLES MAXCONDITIONSETS + +#define CHALLENGEGRIDHEIGHT 5 +#ifdef DEVELOP +#define CHALLENGEGRIDLOOPWIDTH 3 +#else +#define CHALLENGEGRIDLOOPWIDTH (BASEVIDWIDTH/16) +#endif +#define challengegridloops (gamedata->challengegridwidth >= CHALLENGEGRIDLOOPWIDTH) + +// GAMEDATA STRUCTURE +// Everything that would get saved in gamedata.dat +struct gamedata_t +{ + // WHENEVER OR NOT WE'RE READY TO SAVE + boolean loaded; + + // CONDITION SETS ACHIEVED + boolean achieved[MAXCONDITIONSETS]; + + // EMBLEMS COLLECTED + boolean collected[MAXEMBLEMS]; + + // UNLOCKABLES UNLOCKED + boolean unlocked[MAXUNLOCKABLES]; + boolean unlockpending[MAXUNLOCKABLES]; + + // CHALLENGE GRID + UINT16 challengegridwidth; + UINT8 *challengegrid; + + // # OF TIMES THE GAME HAS BEEN BEATEN + UINT32 timesBeaten; + + // PLAY TIME + UINT32 totalplaytime; + UINT32 matchesplayed; +}; + +extern gamedata_t *gamedata; + +// Netsynced functional alternative to gamedata->unlocked +extern boolean netUnlocked[MAXUNLOCKABLES]; extern conditionset_t conditionSets[MAXCONDITIONSETS]; extern emblem_t emblemlocations[MAXEMBLEMS]; -extern extraemblem_t extraemblems[MAXEXTRAEMBLEMS]; extern unlockable_t unlockables[MAXUNLOCKABLES]; extern INT32 numemblems; -extern INT32 numextraemblems; extern UINT32 unlocktriggers; +void M_NewGameDataStruct(void); + +// Challenges menu stuff +void M_PopulateChallengeGrid(void); +UINT8 *M_ChallengeGridExtraData(void); +#define CHE_NONE 0 +#define CHE_HINT 1 +#define CHE_CONNECTEDLEFT (1<<1) +#define CHE_CONNECTEDUP (1<<2) +#define CHE_DONTDRAW (CHE_CONNECTEDLEFT|CHE_CONNECTEDUP) +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); @@ -151,30 +201,36 @@ void M_ClearConditionSet(UINT8 set); void M_ClearSecrets(void); // Updating conditions and unlockables -void M_CheckUnlockConditions(void); UINT8 M_CheckCondition(condition_t *cn); -UINT8 M_UpdateUnlockablesAndExtraEmblems(void); -void M_SilentUpdateUnlockablesAndEmblems(void); +boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud); +UINT8 M_GetNextAchievedUnlock(void); UINT8 M_CheckLevelEmblems(void); UINT8 M_CompletionEmblems(void); // Checking unlockable status -UINT8 M_AnySecretUnlocked(void); -UINT8 M_SecretUnlocked(INT32 type); -UINT8 M_MapLocked(INT32 mapnum); -INT32 M_CountEmblems(void); +boolean M_CheckNetUnlockByID(UINT8 unlockid); +boolean M_SecretUnlocked(INT32 type, boolean local); +boolean M_CupLocked(cupheader_t *cup); +boolean M_MapLocked(INT32 mapnum); +INT32 M_CountMedals(boolean all, boolean extraonly); // Emblem shit emblem_t *M_GetLevelEmblems(INT32 mapnum); skincolornum_t M_GetEmblemColor(emblem_t *em); const char *M_GetEmblemPatch(emblem_t *em, boolean big); -skincolornum_t M_GetExtraEmblemColor(extraemblem_t *em); -const char *M_GetExtraEmblemPatch(extraemblem_t *em, boolean big); // If you're looking to compare stats for unlocks or what not, use these // They stop checking upon reaching the target number so they // should be (theoretically?) slightly faster. -UINT8 M_GotEnoughEmblems(INT32 number); +UINT8 M_GotEnoughMedals(INT32 number); UINT8 M_GotLowEnoughTime(INT32 tictime); -#define M_Achieved(a) ((a) >= MAXCONDITIONSETS || conditionSets[a].achieved) +INT32 M_UnlockableSkinNum(unlockable_t *unlock); +INT32 M_UnlockableFollowerNum(unlockable_t *unlock); +cupheader_t *M_UnlockableCup(unlockable_t *unlock); +UINT16 M_UnlockableMapNum(unlockable_t *unlock); + +INT32 M_EmblemSkinNum(emblem_t *emblem); +UINT16 M_EmblemMapNum(emblem_t *emblem); + +#define M_Achieved(a) ((a) >= MAXCONDITIONSETS || gamedata->achieved[a]) diff --git a/src/m_random.c b/src/m_random.c index 6d8beeaa3..755f7ca65 100644 --- a/src/m_random.c +++ b/src/m_random.c @@ -14,7 +14,7 @@ #include "doomdef.h" #include "doomtype.h" -#include "doomstat.h" // totalplaytime +#include "m_cond.h" // gamedata->totalplaytime #include "m_random.h" #include "m_fixed.h" @@ -372,5 +372,5 @@ void P_ClearRandom(UINT32 seed) */ UINT32 M_RandomizedSeed(void) { - return ((totalplaytime & 0xFFFF) << 16) | M_RandomFixed(); + return ((gamedata->totalplaytime & 0xFFFF) << 16) | M_RandomFixed(); } diff --git a/src/objects/orbinaut.c b/src/objects/orbinaut.c index b4daefb9b..0e8bf7d24 100644 --- a/src/objects/orbinaut.c +++ b/src/objects/orbinaut.c @@ -184,7 +184,7 @@ boolean Obj_OrbinautJawzCollide(mobj_t *t1, mobj_t *t2) if (t2->player) { if ((t2->player->flashing > 0 && t2->hitlag == 0) - && !(t1->type == MT_ORBINAUT || t1->type == MT_JAWZ)) + && !(t1->type == MT_ORBINAUT || t1->type == MT_JAWZ || t1->type == MT_GACHABOM)) return true; if (t2->player->hyudorotimer) @@ -209,7 +209,7 @@ boolean Obj_OrbinautJawzCollide(mobj_t *t1, mobj_t *t2) else if (t2->type == MT_ORBINAUT || t2->type == MT_JAWZ || t2->type == MT_ORBINAUT_SHIELD || t2->type == MT_JAWZ_SHIELD || t2->type == MT_BANANA || t2->type == MT_BANANA_SHIELD - || t2->type == MT_BALLHOG) + || t2->type == MT_BALLHOG || t2->type == MT_GACHABOM) { // Other Item Damage angle_t bounceangle = K_GetCollideAngle(t1, t2); diff --git a/src/p_inter.c b/src/p_inter.c index ca6612308..ff6ba99b0 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -157,6 +157,40 @@ boolean P_CanPickupItem(player_t *player, UINT8 weapon) return true; } +boolean P_CanPickupEmblem(player_t *player, INT32 emblemID) +{ + if (emblemID < 0 || emblemID >= MAXEMBLEMS) + { + // Invalid emblem ID, can't pickup. + return false; + } + + if (demo.playback) + { + // Never collect emblems in replays. + return false; + } + + if (player->bot) + { + // Your nefarious opponent puppy can't grab these for you. + return false; + } + + return true; +} + +boolean P_EmblemWasCollected(INT32 emblemID) +{ + if (emblemID < 0 || emblemID >= numemblems) + { + // Invalid emblem ID, can't pickup. + return true; + } + + return gamedata->collected[emblemID]; +} + /** Takes action based on a ::MF_SPECIAL thing touched by a player. * Actually, this just checks a few things (heights, toucher->player, no * objectplace, no dead or disappearing things) @@ -508,12 +542,24 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) // Secret emblem thingy case MT_EMBLEM: { - if (demo.playback || special->health > MAXEMBLEMS) + boolean gotcollected = false; + + if (!P_CanPickupEmblem(player, special->health - 1)) return; - emblemlocations[special->health-1].collected = true; - M_UpdateUnlockablesAndExtraEmblems(); - G_SaveGameData(); + if (P_IsLocalPlayer(player) && !gamedata->collected[special->health-1]) + { + gamedata->collected[special->health-1] = gotcollected = true; + M_UpdateUnlockablesAndExtraEmblems(true); + G_SaveGameData(); + } + + if (netgame) + { + // Don't delete the object in netgames, just fade it. + return; + } + break; } @@ -962,7 +1008,8 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget || target->type == MT_BANANA || target->type == MT_BANANA_SHIELD || target->type == MT_DROPTARGET || target->type == MT_DROPTARGET_SHIELD || target->type == MT_EGGMANITEM || target->type == MT_EGGMANITEM_SHIELD - || target->type == MT_BALLHOG || target->type == MT_SPB)) // kart dead items + || target->type == MT_BALLHOG || target->type == MT_SPB + || target->type == MT_GACHABOM)) // kart dead items target->flags |= MF_NOGRAVITY; // Don't drop Tails 03-08-2000 else target->flags &= ~MF_NOGRAVITY; // lose it if you for whatever reason have it, I'm looking at you shields @@ -1975,7 +2022,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da if (!(target->flags & MF_SHOOTABLE)) return false; // shouldn't happen... - if (!(damagetype & DMG_DEATHMASK) && target->hitlag > 0 && inflictor == NULL) + if (!(damagetype & DMG_DEATHMASK) && (target->eflags & MFE_PAUSED)) return false; } @@ -1998,6 +2045,18 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da if (player) // Player is the target { + { + const INT32 oldtimeshit = player->timeshit; + + player->timeshit++; + + // overflow prevention + if (player->timeshit < oldtimeshit) + { + player->timeshit = oldtimeshit; + } + } + if (player->pflags & PF_GODMODE) return false; @@ -2031,6 +2090,9 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da // If not, then spawn the instashield effect instead. if (!force) { + boolean invincible = true; + sfxenum_t sfx = sfx_None; + if (gametyperules & GTR_BUMPERS) { if (player->bumpers <= 0 && player->karmadelay) @@ -2050,8 +2112,35 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da } } - if (player->invincibilitytimer > 0 || K_IsBigger(target, inflictor) == true || player->hyudorotimer > 0) + if (player->invincibilitytimer > 0) { + sfx= sfx_invind; + } + else if (K_IsBigger(target, inflictor) == true) + { + sfx = sfx_grownd; + } + else if (player->hyudorotimer > 0) + ; + else + { + invincible = false; + } + + if (invincible) + { + const INT32 oldhitlag = target->hitlag; + + laglength = max(laglength / 2, 1); + K_SetHitLagForObjects(target, inflictor, laglength, false); + + player->invulnhitlag += (target->hitlag - oldhitlag); + + if (player->timeshit > player->timeshitprev) + { + S_StartSound(target, sfx); + } + // Full invulnerability K_DoInstashield(player); return false; diff --git a/src/p_local.h b/src/p_local.h index 321ba682b..95677b447 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -546,6 +546,8 @@ void P_CheckPointLimit(void); boolean P_CheckRacers(void); boolean P_CanPickupItem(player_t *player, UINT8 weapon); +boolean P_CanPickupEmblem(player_t *player, INT32 emblemID); +boolean P_EmblemWasCollected(INT32 emblemID); // // P_SPEC diff --git a/src/p_map.c b/src/p_map.c index 04978c483..ef4819118 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -277,6 +277,12 @@ P_DoSpringEx angle_t finalAngle, UINT16 starcolor) { + if (object->eflags & MFE_SPRUNG) + { + // Object was already sprung this tic + return; + } + if (horizspeed < 0) { horizspeed = -(horizspeed); @@ -402,7 +408,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object) return false; } - spring->flags &= ~(MF_SOLID|MF_SPECIAL); // De-solidify + spring->flags |= MF_NOCLIPTHING; // De-solidify if (spring->eflags & MFE_VERTICALFLIP) vertispeed *= -1; @@ -445,7 +451,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object) spring->angle, starcolor); // Re-solidify - spring->flags |= (spring->info->flags & (MF_SPRING|MF_SPECIAL)); + spring->flags = (spring->flags & ~(MF_NOCLIPTHING)) | (spring->info->flags & (MF_NOCLIPTHING)); if (object->player) { @@ -921,7 +927,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) // Bubble Shield reflect if (((thing->type == MT_BUBBLESHIELD && thing->target->player && thing->target->player->bubbleblowup) || (thing->player && thing->player->bubbleblowup)) - && (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ + && (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ || tm.thing->type == MT_GACHABOM || tm.thing->type == MT_BANANA || tm.thing->type == MT_EGGMANITEM || tm.thing->type == MT_BALLHOG || tm.thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || tm.thing->type == MT_SINK || tm.thing->type == MT_GARDENTOP @@ -937,7 +943,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) } else if (((tm.thing->type == MT_BUBBLESHIELD && tm.thing->target->player && tm.thing->target->player->bubbleblowup) || (tm.thing->player && tm.thing->player->bubbleblowup)) - && (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ + && (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_GACHABOM || thing->type == MT_BANANA || thing->type == MT_EGGMANITEM || thing->type == MT_BALLHOG || thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || thing->type == MT_SINK || thing->type == MT_GARDENTOP @@ -958,7 +964,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) // Droptarget reflect if ((thing->type == MT_DROPTARGET || thing->type == MT_DROPTARGET_SHIELD) - && (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ + && (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ || tm.thing->type == MT_GACHABOM || tm.thing->type == MT_BANANA || tm.thing->type == MT_EGGMANITEM || tm.thing->type == MT_BALLHOG || tm.thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || tm.thing->type == MT_SINK || tm.thing->type == MT_GARDENTOP @@ -973,7 +979,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) return K_DropTargetCollide(thing, tm.thing) ? BMIT_CONTINUE : BMIT_ABORT; } else if ((tm.thing->type == MT_DROPTARGET || tm.thing->type == MT_DROPTARGET_SHIELD) - && (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ + && (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_GACHABOM || thing->type == MT_BANANA || thing->type == MT_EGGMANITEM || thing->type == MT_BALLHOG || thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || thing->type == MT_SINK || thing->type == MT_GARDENTOP @@ -993,7 +999,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) || thing->type == MT_DROPTARGET_SHIELD || tm.thing->type == MT_DROPTARGET_SHIELD) return BMIT_CONTINUE; - if (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ + if (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ || tm.thing->type == MT_GACHABOM || tm.thing->type == MT_ORBINAUT_SHIELD || tm.thing->type == MT_JAWZ_SHIELD || tm.thing->type == MT_GARDENTOP) { @@ -1005,7 +1011,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) return Obj_OrbinautJawzCollide(tm.thing, thing) ? BMIT_CONTINUE : BMIT_ABORT; } - else if (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ + else if (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_GACHABOM || thing->type == MT_ORBINAUT_SHIELD || thing->type == MT_JAWZ_SHIELD || thing->type == MT_GARDENTOP) { diff --git a/src/p_mobj.c b/src/p_mobj.c index c028dcbd9..26b38d631 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -1196,6 +1196,13 @@ fixed_t P_GetMobjGravity(mobj_t *mo) case MT_BATTLEBUMPER: gravityadd /= 2; break; + case MT_GACHABOM: + if (!(mo->flags2 & MF2_AMBUSH)) + { + // Use normal gravity, unless if it was tossed. + break; + } + /*FALLTHRU*/ case MT_BANANA: case MT_EGGMANITEM: case MT_SSMINE: @@ -1743,7 +1750,7 @@ void P_XYMovement(mobj_t *mo) //{ SRB2kart - Orbinaut, Ballhog // Bump sparks - if (mo->type == MT_ORBINAUT || mo->type == MT_BALLHOG) + if (mo->type == MT_ORBINAUT || mo->type == MT_BALLHOG || mo->type == MT_GACHABOM) { mobj_t *fx; fx = P_SpawnMobj(mo->x, mo->y, mo->z, MT_BUMP); @@ -1757,6 +1764,7 @@ void P_XYMovement(mobj_t *mo) switch (mo->type) { case MT_ORBINAUT: // Orbinaut speed decreasing + case MT_GACHABOM: case MT_GARDENTOP: if (mo->health > 1) { @@ -5175,6 +5183,7 @@ boolean P_IsKartFieldItem(INT32 type) case MT_SINK: case MT_DROPTARGET: case MT_DUELBOMB: + case MT_GACHABOM: return true; default: @@ -6669,6 +6678,7 @@ static boolean P_MobjDeadThink(mobj_t *mobj) case MT_LANDMINE: //case MT_DROPTARGET: case MT_SPB: + case MT_GACHABOM: if (P_IsObjectOnGround(mobj)) { P_RemoveMobj(mobj); @@ -6959,9 +6969,38 @@ static boolean P_MobjRegularThink(mobj_t *mobj) } break; case MT_EMBLEM: - if (mobj->flags2 & MF2_NIGHTSPULL) - P_NightsItemChase(mobj); + { + INT32 trans = 0; + + mobj->frame &= ~FF_TRANSMASK; + mobj->renderflags &= ~RF_TRANSMASK; + + if (P_EmblemWasCollected(mobj->health - 1) || !P_CanPickupEmblem(&players[consoleplayer], mobj->health - 1)) + { + trans = tr_trans50; + mobj->renderflags |= (tr_trans50 << RF_TRANSSHIFT); + } + + if (mobj->reactiontime > 0 + && leveltime > starttime) + { + INT32 diff = mobj->reactiontime - (signed)(leveltime - starttime); + if (diff < 10) + { + if (diff <= 0) + { + P_RemoveMobj(mobj); + return false; + } + + trans = max(trans, 10-diff); + } + } + + mobj->renderflags |= (trans << RF_TRANSSHIFT); + break; + } case MT_FLOATINGITEM: { mobj->pitch = mobj->roll = 0; @@ -7069,6 +7108,39 @@ static boolean P_MobjRegularThink(mobj_t *mobj) } } break; + case MT_GACHABOM: + { + if (mobj->flags2 & MF2_AMBUSH) + { + mobj->friction = ORIG_FRICTION/4; + + if (mobj->momx || mobj->momy) + { + mobj_t *ghost = P_SpawnGhostMobj(mobj); + + if (mobj->target && !P_MobjWasRemoved(mobj->target) && mobj->target->player) + { + ghost->color = mobj->target->player->skincolor; + ghost->colorized = true; + } + } + + if (P_IsObjectOnGround(mobj)) + { + if (mobj->movecount > 1) + { + S_StartSound(mobj, mobj->info->activesound); + mobj->momx = mobj->momy = 0; + mobj->movecount = 1; + } + } + + if (mobj->threshold > 0) + mobj->threshold--; + break; + } + } + /* FALLTHRU */ case MT_ORBINAUT: { Obj_OrbinautThink(mobj); @@ -9624,6 +9696,7 @@ void P_MobjThinker(mobj_t *mobj) // Don't run any thinker code while in hitlag if (mobj->hitlag > 0) { + mobj->eflags |= MFE_PAUSED; mobj->hitlag--; if (mobj->type == MT_DROPTARGET && mobj->reactiontime > 0 && mobj->hitlag == 2) @@ -9643,7 +9716,7 @@ void P_MobjThinker(mobj_t *mobj) return; } - mobj->eflags &= ~(MFE_PUSHED|MFE_SPRUNG|MFE_JUSTBOUNCEDWALL|MFE_DAMAGEHITLAG|MFE_SLOPELAUNCHED); + mobj->eflags &= ~(MFE_PUSHED|MFE_SPRUNG|MFE_JUSTBOUNCEDWALL|MFE_DAMAGEHITLAG|MFE_SLOPELAUNCHED|MFE_PAUSED); // sal: what the hell? is there any reason this isn't done, like, literally ANYWHERE else? P_SetTarget(&tm.floorthing, NULL); @@ -9824,6 +9897,7 @@ void P_MobjThinker(mobj_t *mobj) || mobj->type == MT_CANNONBALLDECOR || mobj->type == MT_FALLINGROCK || mobj->type == MT_ORBINAUT + || mobj->type == MT_GACHABOM || mobj->type == MT_JAWZ || (mobj->type == MT_DROPTARGET && mobj->reactiontime)) { @@ -10103,6 +10177,7 @@ static void P_DefaultMobjShadowScale(mobj_t *thing) case MT_ROCKETSNEAKER: case MT_SPB: case MT_DUELBOMB: + case MT_GACHABOM: thing->shadowscale = 3*FRACUNIT/2; break; case MT_BANANA_SHIELD: @@ -11424,7 +11499,7 @@ void P_RespawnSpecials(void) } else if (pcount > 1) { - time = (120 - ((pcount-2) * 20)) * TICRATE; + time = (120 * TICRATE) / (pcount - 1); // If the map is longer or shorter than 3 laps, then adjust ring respawn to account for this. // 5 lap courses would have more retreaded ground, while 2 lap courses would have less. @@ -12043,7 +12118,7 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj) if (!emblem) { - CONS_Debug(DBG_GAMELOGIC, "No map emblem for map %d with tag %d found!\n", gamemap, Tag_FGet(&mthing->tags)); + CONS_Alert(CONS_WARNING, "P_SetupEmblem: No map emblem for map %d with tag %d found!\n", gamemap, Tag_FGet(&mthing->tags)); return false; } @@ -12056,24 +12131,13 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj) emcolor = M_GetEmblemColor(&emblemlocations[j]); // workaround for compiler complaint about bad function casting mobj->color = (UINT16)emcolor; - if (emblemlocations[j].collected) - { - P_UnsetThingPosition(mobj); - mobj->flags |= MF_NOCLIP; - mobj->flags &= ~MF_SPECIAL; - mobj->flags |= MF_NOBLOCKMAP; - mobj->frame |= (tr_trans50 << FF_TRANSSHIFT); - P_SetThingPosition(mobj); - } - else - { - mobj->frame &= ~FF_TRANSMASK; + mobj->frame &= ~FF_TRANSMASK; - if (emblemlocations[j].type == ET_GLOBAL) - { - mobj->reactiontime = emblemlocations[j].var; - } + if (emblemlocations[j].flags & GE_TIMED) + { + mobj->reactiontime = emblemlocations[j].var; } + return true; } @@ -12517,7 +12581,10 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean case MT_EMBLEM: { if (!P_SetupEmblem(mthing, mobj)) + { + P_RemoveMobj(mobj); return false; + } break; } case MT_SKYBOX: diff --git a/src/p_mobj.h b/src/p_mobj.h index 8811c0149..8e2686900 100644 --- a/src/p_mobj.h +++ b/src/p_mobj.h @@ -255,7 +255,8 @@ typedef enum MFE_DAMAGEHITLAG = 1<<13, // Slope physics sent you airborne MFE_SLOPELAUNCHED = 1<<14, - // free: to and including 1<<15 + // Thinker is paused due to hitlag + MFE_PAUSED = 1<<15, } mobjeflag_t; // @@ -275,14 +276,17 @@ struct mobj_t // List: thinker links. thinker_t thinker; - mobjtype_t type; - const mobjinfo_t *info; // &mobjinfo[mobj->type] - // Info for drawing: position. fixed_t x, y, z; + // --- Please make sure you keep the fields up to this + // --- point in sync with degenmobj_t. + fixed_t old_x, old_y, old_z; // position interpolation fixed_t old_x2, old_y2, old_z2; + mobjtype_t type; + const mobjinfo_t *info; // &mobjinfo[mobj->type] + // More list: links in sector (if needed) mobj_t *snext; mobj_t **sprev; // killough 8/11/98: change to ptr-to-ptr @@ -428,14 +432,14 @@ struct precipmobj_t // List: thinker links. thinker_t thinker; - mobjtype_t type; - const mobjinfo_t *info; // &mobjinfo[mobj->type] - // Info for drawing: position. fixed_t x, y, z; fixed_t old_x, old_y, old_z; // position interpolation fixed_t old_x2, old_y2, old_z2; + mobjtype_t type; + const mobjinfo_t *info; // &mobjinfo[mobj->type] + // More list: links in sector (if needed) precipmobj_t *snext; precipmobj_t **sprev; // killough 8/11/98: change to ptr-to-ptr diff --git a/src/p_saveg.c b/src/p_saveg.c index 3ce18aa5f..118706d4a 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -35,6 +35,7 @@ #include "p_polyobj.h" #include "lua_script.h" #include "p_slopes.h" +#include "m_cond.h" // netUnlocked // SRB2Kart #include "k_battle.h" @@ -150,7 +151,12 @@ static void P_NetArchivePlayers(void) WRITEUINT8(save_p, players[i].skincolor); WRITEINT32(save_p, players[i].skin); - WRITEUINT32(save_p, players[i].availabilities); + + for (j = 0; j < MAXAVAILABILITY; j++) + { + WRITEUINT8(save_p, players[i].availabilities[j]); + } + WRITEUINT8(save_p, players[i].fakeskin); WRITEUINT8(save_p, players[i].lastfakeskin); WRITEUINT32(save_p, players[i].score); @@ -184,6 +190,9 @@ static void P_NetArchivePlayers(void) WRITEINT32(save_p, players[i].onconveyor); + WRITEUINT8(save_p, players[i].timeshit); + WRITEUINT8(save_p, players[i].timeshitprev); + WRITEUINT32(save_p, players[i].jointime); WRITEUINT8(save_p, players[i].splitscreenindex); @@ -260,6 +269,7 @@ static void P_NetArchivePlayers(void) WRITEUINT16(save_p, players[i].spinouttimer); WRITEUINT8(save_p, players[i].spinouttype); WRITEUINT8(save_p, players[i].instashield); + WRITEINT32(save_p, players[i].invulnhitlag); WRITEUINT8(save_p, players[i].wipeoutslow); WRITEUINT8(save_p, players[i].justbumped); WRITEUINT8(save_p, players[i].tumbleBounces); @@ -514,7 +524,12 @@ static void P_NetUnArchivePlayers(void) players[i].skincolor = READUINT8(save_p); players[i].skin = READINT32(save_p); - players[i].availabilities = READUINT32(save_p); + + for (j = 0; j < MAXAVAILABILITY; j++) + { + players[i].availabilities[j] = READUINT8(save_p); + } + players[i].fakeskin = READUINT8(save_p); players[i].lastfakeskin = READUINT8(save_p); players[i].score = READUINT32(save_p); @@ -546,6 +561,9 @@ static void P_NetUnArchivePlayers(void) players[i].lastsidehit = READINT16(save_p); players[i].lastlinehit = READINT16(save_p); + players[i].timeshit = READUINT8(save_p); + players[i].timeshitprev = READUINT8(save_p); + players[i].onconveyor = READINT32(save_p); players[i].jointime = READUINT32(save_p); @@ -604,6 +622,7 @@ static void P_NetUnArchivePlayers(void) players[i].spinouttimer = READUINT16(save_p); players[i].spinouttype = READUINT8(save_p); players[i].instashield = READUINT8(save_p); + players[i].invulnhitlag = READINT32(save_p); players[i].wipeoutslow = READUINT8(save_p); players[i].justbumped = READUINT8(save_p); players[i].tumbleBounces = READUINT8(save_p); @@ -4684,9 +4703,6 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride) //lastmapsaved = gamemap; lastmaploaded = gamemap; - tokenlist = 0; - token = 0; - savedata.emeralds = READUINT16(save_p)-357; READSTRINGN(save_p, testname, sizeof(testname)); @@ -4705,7 +4721,7 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride) static void P_NetArchiveMisc(boolean resending) { - size_t i; + size_t i, j; WRITEUINT32(save_p, ARCHIVEBLOCK_MISC); @@ -4726,7 +4742,14 @@ static void P_NetArchiveMisc(boolean resending) WRITEUINT32(save_p, pig); } - WRITEUINT32(save_p, tokenlist); + for (i = 0; i < MAXUNLOCKABLES;) + { + UINT8 btemp = 0; + for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) + btemp |= (netUnlocked[j+i] << j); + WRITEUINT8(save_p, btemp); + i += j; + } WRITEUINT8(save_p, encoremode); @@ -4755,7 +4778,6 @@ static void P_NetArchiveMisc(boolean resending) WRITEUINT8(save_p, globools); } - WRITEUINT32(save_p, token); WRITEUINT32(save_p, bluescore); WRITEUINT32(save_p, redscore); @@ -4851,8 +4873,9 @@ static void P_NetArchiveMisc(boolean resending) static inline boolean P_NetUnArchiveMisc(boolean reloading) { - size_t i; + size_t i, j; size_t numTasks; + UINT8 *old_save_p; if (READUINT32(save_p) != ARCHIVEBLOCK_MISC) I_Error("Bad $$$.sav at archive block Misc"); @@ -4885,16 +4908,27 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading) } } - tokenlist = READUINT32(save_p); + for (i = 0; i < MAXUNLOCKABLES;) + { + UINT8 rtemp = READUINT8(save_p); + for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) + netUnlocked[j+i] = ((rtemp >> j) & 1); + i += j; + } encoremode = (boolean)READUINT8(save_p); + // FIXME: save_p should not be global!!! + old_save_p = save_p; + if (!P_LoadLevel(true, reloading)) { CONS_Alert(CONS_ERROR, M_GetText("Can't load the level!\n")); return false; } + save_p = old_save_p; + // get the time leveltime = READUINT32(save_p); lastmap = READINT16(save_p); @@ -4918,7 +4952,6 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading) stoppedclock = !!(globools & (1<<1)); } - token = READUINT32(save_p); bluescore = READUINT32(save_p); redscore = READUINT32(save_p); diff --git a/src/p_setup.c b/src/p_setup.c index 38872851a..61c2a9ec3 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -400,7 +400,6 @@ static void P_ClearSingleMapHeaderInfo(INT16 num) mapheaderinfo[num]->palette = UINT16_MAX; mapheaderinfo[num]->encorepal = UINT16_MAX; mapheaderinfo[num]->numlaps = NUMLAPS_DEFAULT; - mapheaderinfo[num]->unlockrequired = -1; mapheaderinfo[num]->levelselect = 0; mapheaderinfo[num]->levelflags = 0; mapheaderinfo[num]->menuflags = 0; @@ -6807,7 +6806,6 @@ static void P_InitLevelSettings(void) modulothing = 0; // special stage tokens, emeralds, and ring total - tokenbits = 0; runemeraldmanager = false; emeraldspawndelay = 60*TICRATE; @@ -7564,10 +7562,13 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) nextmapoverride = 0; skipstats = 0; - if (!(netgame || multiplayer || demo.playback) && !majormods) + if (!demo.playback) + { mapheaderinfo[gamemap-1]->mapvisited |= MV_VISITED; - else if (!demo.playback) - mapheaderinfo[gamemap-1]->mapvisited |= MV_MP; // you want to record that you've been there this session, but not permanently + + M_UpdateUnlockablesAndExtraEmblems(true); + G_SaveGameData(); + } G_AddMapToBuffer(gamemap-1); @@ -8037,8 +8038,8 @@ UINT16 P_PartialAddWadFile(const char *wadfilename) // // look for skins // - R_AddSkins(wadnum); // faB: wadfile index in wadfiles[] - R_PatchSkins(wadnum); // toast: PATCH PATCH + R_AddSkins(wadnum, false); // faB: wadfile index in wadfiles[] + R_PatchSkins(wadnum, false); // toast: PATCH PATCH // // edit music defs diff --git a/src/p_spec.c b/src/p_spec.c index 56eb3d941..4988ad881 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -1495,14 +1495,14 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller { // An unlockable itself must be unlocked! INT32 unlockid = triggerline->args[1]; - if ((modifiedgame && !savemoddata) || (netgame || multiplayer)) + if (modifiedgame && !savemoddata) return false; - else if (unlockid < 0 || unlockid >= MAXUNLOCKABLES) // limited by unlockable count + else if (unlockid <= 0 || unlockid > MAXUNLOCKABLES) // limited by unlockable count { CONS_Debug(DBG_GAMELOGIC, "Unlockable check (sidedef %hu): bad unlockable ID %d\n", triggerline->sidenum[0], unlockid); return false; } - else if (!(unlockables[unlockid-1].unlocked)) + else if (!(M_CheckNetUnlockByID(unlockid))) return false; } break; @@ -2752,7 +2752,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec) break; case 441: // Trigger unlockable - if ((!modifiedgame || savemoddata) && !(netgame || multiplayer)) + if (!(demo.playback || netgame || multiplayer)) { INT32 trigid = line->args[0]; @@ -2763,9 +2763,8 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec) unlocktriggers |= 1 << trigid; // Unlocked something? - if (M_UpdateUnlockablesAndExtraEmblems()) + if (M_UpdateUnlockablesAndExtraEmblems(true)) { - S_StartSound(NULL, sfx_s3k68); G_SaveGameData(); // only save if unlocked something } } diff --git a/src/p_tick.c b/src/p_tick.c index 7cf1748b5..8312a18a8 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -20,6 +20,7 @@ #include "st_stuff.h" #include "p_polyobj.h" #include "m_random.h" +#include "m_cond.h" // gamedata->playtime #include "lua_script.h" #include "lua_hook.h" #include "m_perfstats.h" @@ -628,7 +629,7 @@ void P_Ticker(boolean run) // Keep track of how long they've been playing! if (!demo.playback) // Don't increment if a demo is playing. - totalplaytime++; + gamedata->totalplaytime++; // formality so kitemcap gets updated properly each frame. P_RunKartItems(); diff --git a/src/p_user.c b/src/p_user.c index 57f4b2af9..ac96ea345 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -4341,6 +4341,12 @@ void P_PlayerAfterThink(player_t *player) // so a lag value of 1 is exactly attached to the player. K_HandleFollower(player); + if (P_MobjWasRemoved(player->mo) || (player->mo->eflags & MFE_PAUSED) == 0) + { + player->timeshitprev = player->timeshit; + player->timeshit = 0; + } + if (K_PlayerUsesBotMovement(player)) { diff --git a/src/r_defs.h b/src/r_defs.h index 59faac3c0..9216603ed 100644 --- a/src/r_defs.h +++ b/src/r_defs.h @@ -821,7 +821,7 @@ struct patch_t }; extern patch_t *missingpat; -extern patch_t *blanklvl; +extern patch_t *blanklvl, *nolvl; #if defined(_MSC_VER) #pragma pack(1) diff --git a/src/r_skins.c b/src/r_skins.c index 9779fdcfc..d38e7a41b 100644 --- a/src/r_skins.c +++ b/src/r_skins.c @@ -32,6 +32,7 @@ #if 0 #include "k_kart.h" // K_KartResetPlayerColor #endif +#include "k_grandprix.h" // K_CanChangeRules #ifdef HWRENDER #include "hardware/hw_md2.h" #endif @@ -150,36 +151,52 @@ void R_InitSkins(void) for (i = 0; i < numwadfiles; i++) { - R_AddSkins((UINT16)i); - R_PatchSkins((UINT16)i); + R_AddSkins((UINT16)i, true); + R_PatchSkins((UINT16)i, true); R_LoadSpriteInfoLumps(i, wadfiles[i]->numlumps); } ST_ReloadSkinFaceGraphics(); } -UINT32 R_GetSkinAvailabilities(void) +UINT8 *R_GetSkinAvailabilities(boolean demolock) { - UINT8 i; - UINT32 response = 0; + UINT8 i, shif, byte; + INT32 skinid; + static UINT8 responsebuffer[MAXAVAILABILITY]; + + memset(&responsebuffer, 0, sizeof(responsebuffer)); for (i = 0; i < MAXUNLOCKABLES; i++) { - if (unlockables[i].type == SECRET_SKIN && unlockables[i].unlocked) - { - UINT8 s = min(unlockables[i].variable, MAXSKINS); - response |= (1 << s); - } + 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; + + shif = (skinid % 8); + byte = (skinid / 8); + + responsebuffer[byte] |= (1 << shif); } - return response; + return responsebuffer; } // returns true if available in circumstances, otherwise nope // warning don't use with an invalid skinnum other than -1 which always returns true -boolean R_SkinUsable(INT32 playernum, INT32 skinnum) +boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins) { boolean needsunlocked = false; + boolean useplayerstruct = (Playing() && playernum != -1); UINT8 i; + INT32 skinid; if (skinnum == -1) { @@ -187,55 +204,55 @@ boolean R_SkinUsable(INT32 playernum, INT32 skinnum) return true; } - if (modeattacking) - { - // If you have someone else's run, you should be able to take a look - return true; - } - - if (netgame && (cv_forceskin.value == skinnum)) + if (K_CanChangeRules(true) && (cv_forceskin.value == skinnum)) { // Being forced to play as this character by the server return true; } - if (metalrecording) - { - // Recording a Metal Sonic race - const INT32 metalskin = R_SkinAvailable("metalsonic"); - return (skinnum == metalskin); - } - // Determine if this character is supposed to be unlockable or not - for (i = 0; i < MAXUNLOCKABLES; i++) + if (useplayerstruct && demo.playback) { - if (unlockables[i].type == SECRET_SKIN && unlockables[i].variable == skinnum) + if (!demoskins) + skinnum = demo.skinlist[skinnum].mapping; + needsunlocked = demo.skinlist[skinnum].unlockrequired; + } + else + { + for (i = 0; i < MAXUNLOCKABLES; i++) { + if (unlockables[i].type != SECRET_SKIN) + continue; + + skinid = M_UnlockableSkinNum(&unlockables[i]); + + if (skinid != skinnum) + continue; + // i is now the unlockable index, we can use this later needsunlocked = true; break; } } - if (needsunlocked == true) - { - // You can use this character IF you have it unlocked. - if ((netgame || multiplayer) && playernum != -1) - { - // Use the netgame synchronized unlocks. - return (boolean)(!(players[playernum].availabilities & (1 << skinnum))); - } - else - { - // Use the unlockables table directly - return (boolean)(unlockables[i].unlocked); - } - } - else + if (needsunlocked == false) { // Didn't trip anything, so we can use this character. return true; } + + // Ok, you can use this character IF you have it unlocked. + if (useplayerstruct) + { + // Use the netgame synchronized unlocks. + UINT8 shif = (skinnum % 8); + UINT8 byte = (skinnum / 8); + return !!(players[playernum].availabilities[byte] & (1 << shif)); + } + + // 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]); } // returns true if the skin name is found (loaded from pwad) @@ -253,15 +270,79 @@ INT32 R_SkinAvailable(const char *name) return -1; } +// Auxillary function that actually sets the skin +static void SetSkin(player_t *player, INT32 skinnum) +{ + skin_t *skin = &skins[skinnum]; + + player->skin = skinnum; + + player->followitem = skin->followitem; + + player->kartspeed = skin->kartspeed; + player->kartweight = skin->kartweight; + player->charflags = skin->flags; + +#if 0 + if (!CV_CheatsEnabled() && !(netgame || multiplayer || demo.playback)) + { + for (i = 0; i <= r_splitscreen; i++) + { + if (playernum == g_localplayers[i]) + { + CV_StealthSetValue(&cv_playercolor[i], skin->prefcolor); + } + } + + player->skincolor = skin->prefcolor; + K_KartResetPlayerColor(player); + } +#endif + + if (player->followmobj) + { + P_RemoveMobj(player->followmobj); + P_SetTarget(&player->followmobj, NULL); + } + + if (player->mo) + { + player->mo->skin = skin; + P_SetScale(player->mo, player->mo->scale); + P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames + } + + // for replays: We have changed our skin mid-game; let the game know so it can do the same in the replay! + demo_extradata[(player-players)] |= DXD_SKIN; +} + +// Gets the player to the first usuable skin in the game. +// (If your mod locked them all, then you kinda stupid) +static INT32 GetPlayerDefaultSkin(INT32 playernum) +{ + INT32 i; + + for (i = 0; i < numskins; i++) + { + if (R_SkinUsable(playernum, i, false)) + { + return i; + } + } + + I_Error("All characters are locked!"); + return 0; +} + // network code calls this when a 'skin change' is received void SetPlayerSkin(INT32 playernum, const char *skinname) { INT32 i = R_SkinAvailable(skinname); player_t *player = &players[playernum]; - if ((i != -1) && R_SkinUsable(playernum, i)) + if ((i != -1) && R_SkinUsable(playernum, i, false)) { - SetPlayerSkinByNum(playernum, i); + SetSkin(player, i); return; } @@ -270,7 +351,7 @@ void SetPlayerSkin(INT32 playernum, const char *skinname) else if(server || IsPlayerAdmin(consoleplayer)) CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) skin '%s' not found\n"), playernum, player_names[playernum], skinname); - SetPlayerSkinByNum(playernum, 0); + SetSkin(player, GetPlayerDefaultSkin(playernum)); } // Same as SetPlayerSkin, but uses the skin #. @@ -278,53 +359,10 @@ void SetPlayerSkin(INT32 playernum, const char *skinname) void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum) { player_t *player = &players[playernum]; - skin_t *skin = &skins[skinnum]; - //UINT16 newcolor = 0; - //UINT8 i; - if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum)) // Make sure it exists! + if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum, false)) // Make sure it exists! { - player->skin = skinnum; - - player->charflags = (UINT32)skin->flags; - - player->followitem = skin->followitem; - - player->kartspeed = skin->kartspeed; - player->kartweight = skin->kartweight; - -#if 0 - if (!CV_CheatsEnabled() && !(netgame || multiplayer || demo.playback)) - { - for (i = 0; i <= r_splitscreen; i++) - { - if (playernum == g_localplayers[i]) - { - CV_StealthSetValue(&cv_playercolor[i], skin->prefcolor); - } - } - - player->skincolor = newcolor = skin->prefcolor; - K_KartResetPlayerColor(player); - } -#endif - - if (player->followmobj) - { - P_RemoveMobj(player->followmobj); - P_SetTarget(&player->followmobj, NULL); - } - - if (player->mo) - { - player->mo->skin = skin; - P_SetScale(player->mo, player->mo->scale); - P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames - } - - // for replays: We have changed our skin mid-game; let the game know so it can do the same in the replay! - demo_extradata[playernum] |= DXD_SKIN; - + SetSkin(player, skinnum); return; } @@ -333,7 +371,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum) else if(server || IsPlayerAdmin(consoleplayer)) CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum); - SetPlayerSkinByNum(playernum, 0); // not found, put in the default skin + SetSkin(player, GetPlayerDefaultSkin(playernum)); // not found put the eggman skin } // Set mo skin but not player_t skin, for ironman @@ -383,11 +421,14 @@ void SetRandomFakePlayerSkin(player_t* player, boolean fast) } else if (skins[i].flags & SF_IRONMAN) continue; - /*if (K_SkinLocked(i)) - continue;*/ + if (!R_SkinUsable(player-players, i, true)) + continue; grabskins[usableskins++] = i; } + if (!usableskins) + I_Error("SetRandomFakePlayerSkin: No valid skins to pick from!?"); + i = grabskins[P_RandomKey(PR_RANDOMSKIN, usableskins)]; SetFakePlayerSkin(player, i); @@ -466,6 +507,51 @@ void ClearFakePlayerSkin(player_t* player) player->fakeskin = MAXSKINS; } +// Finds a skin with the closest stats if the expected skin doesn't exist. +INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight, UINT32 flags, boolean unlock) +{ + INT32 i, closest_skin = 0; + UINT8 closest_stats, stat_diff; + boolean doflagcheck = true; + UINT32 flagcheck = flags; + +flaglessretry: + closest_stats = stat_diff = UINT8_MAX; + + for (i = 0; i < numskins; i++) + { + if (!unlock && !R_SkinUsable(-1, i, false)) + { + continue; + } + stat_diff = abs(skins[i].kartspeed - kartspeed) + abs(skins[i].kartweight - kartweight); + if (doflagcheck && (skins[i].flags & flagcheck) != flagcheck) + { + continue; + } + if (stat_diff < closest_stats) + { + closest_stats = stat_diff; + closest_skin = i; + } + } + + if (stat_diff && (doflagcheck || closest_stats == UINT8_MAX)) + { + // Just grab *any* SF_IRONMAN if we don't get it on the first pass. + if ((flagcheck & SF_IRONMAN) && (flagcheck != SF_IRONMAN)) + { + flagcheck = SF_IRONMAN; + } + + doflagcheck = false; + + goto flaglessretry; + } + + return closest_skin; +} + // // Add skins from a pwad, each skin preceded by 'S_SKIN' marker // @@ -659,7 +745,7 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value) // // Find skin sprites, sounds & optional status bar face, & add them // -void R_AddSkins(UINT16 wadnum) +void R_AddSkins(UINT16 wadnum, boolean mainfile) { UINT16 lump, lastlump = 0; char *buf; @@ -807,7 +893,8 @@ next_token: R_FlushTranslationColormapCache(); - CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name); + if (mainfile == false) + CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name); #ifdef SKINVALUES skin_cons_t[numskins].value = numskins; @@ -831,7 +918,7 @@ next_token: // // Patch skin sprites // -void R_PatchSkins(UINT16 wadnum) +void R_PatchSkins(UINT16 wadnum, boolean mainfile) { UINT16 lump, lastlump = 0; char *buf; @@ -973,7 +1060,8 @@ next_token: R_FlushTranslationColormapCache(); - CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name); + if (mainfile == false) + CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name); } return; } diff --git a/src/r_skins.h b/src/r_skins.h index f9dd944c0..551cf56b9 100644 --- a/src/r_skins.h +++ b/src/r_skins.h @@ -84,11 +84,14 @@ void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002 void SetFakePlayerSkin(player_t* player, INT32 skinnum); void SetRandomFakePlayerSkin(player_t* player, boolean fast); void ClearFakePlayerSkin(player_t* player); -boolean R_SkinUsable(INT32 playernum, INT32 skinnum); -UINT32 R_GetSkinAvailabilities(void); +boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins); +INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight, UINT32 flags, boolean unlock); + +UINT8 *R_GetSkinAvailabilities(boolean demolock); INT32 R_SkinAvailable(const char *name); -void R_PatchSkins(UINT16 wadnum); -void R_AddSkins(UINT16 wadnum); + +void R_PatchSkins(UINT16 wadnum, boolean mainfile); +void R_AddSkins(UINT16 wadnum, boolean mainfile); UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player); diff --git a/src/sounds.c b/src/sounds.c index 016cf5029..b18670667 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1148,6 +1148,10 @@ sfxinfo_t S_sfx[NUMSFX] = {"pass15", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""}, {"pass16", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""}, + // SRB2Kart - Blocked damage + {"grownd", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SF_X8AWAYSOUND + {"invind", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SF_X8AWAYSOUND + // SRB2Kart - Engine sounds // Engine class A {"krta00", false, 48, 65, -1, NULL, 0, -1, -1, LUMPERROR, ""}, diff --git a/src/sounds.h b/src/sounds.h index 01dfbfc2b..51aecb94a 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -1212,6 +1212,10 @@ typedef enum sfx_pass15, sfx_pass16, + // Blocked damage SFX + sfx_grownd, + sfx_invind, + // Next up: UNIQUE ENGINE SOUNDS! Hoooooo boy... // Engine class A - Low Speed, Low Weight sfx_krta00, diff --git a/src/typedef.h b/src/typedef.h index 0be1884bc..34c652f5e 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -197,8 +197,8 @@ TYPEDEF (aatree_t); TYPEDEF (condition_t); TYPEDEF (conditionset_t); TYPEDEF (emblem_t); -TYPEDEF (extraemblem_t); TYPEDEF (unlockable_t); +TYPEDEF (gamedata_t); // m_dllist.h TYPEDEF (mdllistitem_t); diff --git a/src/y_inter.c b/src/y_inter.c index 10e1b732f..8cb21fe04 100644 --- a/src/y_inter.c +++ b/src/y_inter.c @@ -42,6 +42,7 @@ #include "m_random.h" // M_RandomKey #include "g_input.h" // G_PlayerInputDown +#include "k_hud.h" // K_DrawMapThumbnail #include "k_battle.h" #include "k_boss.h" #include "k_pwrlv.h" @@ -1006,34 +1007,11 @@ void Y_VoteDrawer(void) y = (200-height)/2; for (i = 0; i < 4; i++) { - const char *str; - patch_t *pic; UINT8 j, color; - if (i == 3) - { - str = "RANDOM"; - pic = randomlvl; - } - else - { - str = levelinfo[i].str; - - pic = NULL; - - if (mapheaderinfo[votelevels[i][0]]) - { - pic = mapheaderinfo[votelevels[i][0]]->thumbnailPic; - } - - if (!pic) - { - pic = blanklvl; - } - } - if (selected[i]) { + const char *str; UINT8 sizeadd = selected[i]; for (j = 0; j <= splitscreen; j++) // another loop for drawing the selection backgrounds in the right order, grumble grumble.. @@ -1093,11 +1071,30 @@ void Y_VoteDrawer(void) sizeadd--; } - if (!levelinfo[i].encore) - V_DrawSmallScaledPatch(BASEVIDWIDTH-100, y, V_SNAPTORIGHT, pic); + if (i == 3) + { + str = "RANDOM"; + K_DrawLikeMapThumbnail( + (BASEVIDWIDTH-100)<= 3 && (i != pickedvote || voteendtic == -1)) - { - pic = randomlvl; - } - else - { - pic = NULL; - - if (mapheaderinfo[votelevels[votes[i]][0]]) - { - pic = mapheaderinfo[votelevels[votes[i]][0]]->thumbnailPic; - } - - if (!pic) - { - pic = blanklvl; - } - } - if (!timer && i == voteclient.ranim) { V_DrawScaledPatch(x-18, y+9, V_SNAPTOLEFT, cursor); @@ -1176,11 +1169,28 @@ void Y_VoteDrawer(void) V_DrawFill(x-1, y-1, 42, 27, levelinfo[votes[i]].gtc|V_SNAPTOLEFT); } - if (!levelinfo[votes[i]].encore) - V_DrawTinyScaledPatch(x, y, V_SNAPTOLEFT, pic); + if (votes[i] >= 3 && (i != pickedvote || voteendtic == -1)) + { + K_DrawLikeMapThumbnail( + (x)<