// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by Vivian "toastergrl" Grannell. // Copyright (C) 2024 by James Robert Roman. // Copyright (C) 2024 by Kart Krew. // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- #include #include #include #include #include #include #include #include #include #include "modp_b64/modp_b64.h" #include "cxxutil.hpp" #include "command.h" #include "d_main.h" #include "doomdef.h" #include "doomstat.h" #include "doomtype.h" #include "g_game.h" #include "k_menu.h" #include "m_cheat.h" #include "m_cond.h" #include "m_pw.h" #include "m_pw_hash.h" #include "s_sound.h" #include "sounds.h" #include "z_zone.h" namespace { constexpr const UINT8 kRRSalt[17] = "0L4rlK}{9ay6'VJS"; std::array decode_hash(std::string encoded) { std::array decoded; if (modp::b64_decode(encoded).size() != decoded.size()) throw std::invalid_argument("hash is incorrectly sized"); std::copy(encoded.begin(), encoded.end(), decoded.begin()); return decoded; } struct Pw { Pw(void (*cb)(), const char *encoded_hash) : cb_(cb), hash_(decode_hash(encoded_hash)) {} void (*cb_)(); const std::array hash_; }; std::vector passwords; // m_cond.c template void iter_conditions(F&& f) { UINT32 i, j; conditionset_t *c; condition_t *cn; for (i = 0; i < MAXCONDITIONSETS; ++i) { c = &conditionSets[i]; if (!c->numconditions || gamedata->achieved[i]) continue; for (j = 0; j < c->numconditions; ++j) { cn = &c->condition[j]; if (cn->type != UC_PASSWORD) continue; if (cn->stringvar == NULL) continue; f(cn); } } } // // responders // void f_tournament() { UINT16 i; boolean success = false; /*if (modifiedgame) return 0;*/ // Unlock EVERYTHING. for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; gamedata->unlocked[i] = true; success = true; } // Unlock all hidden levels. #define GD_MV_SET (MV_VISITED|MV_BEATEN) for (i = 0; i < nummapheaders; i++) { if ((mapheaderinfo[i]->records.mapvisited & GD_MV_SET) == GD_MV_SET) continue; if (mapheaderinfo[i]->typeoflevel & TOL_VERSUS) continue; if (!strcmp(mapheaderinfo[i]->lumpname, "RR_HIDDENPALACE")) continue; mapheaderinfo[i]->records.mapvisited |= GD_MV_SET; success = true; } #undef GD_MV_SET // Goofy, but this call needs to be before M_ClearMenus because that path // calls G_LoadLevel, which will trigger a gamedata save. Garbage factory if (success) { gamedata->gonerlevel = GDGONER_DONE; gamedata->sealedswapalerted = true; G_SetUsedCheats(); } M_ClearMenus(true); const char *text; if (success) { S_StartSound(0, sfx_kc42); text = M_GetText( "Unlocked\x83 almost\x80 everything.\n" "Saving is disabled - the game will\n" "return to normal on next launch.\n" ); usedTourney = true; } else { S_StartSound(0, sfx_s3k7b); if (usedCheats) { text = M_GetText( "This is the correct password,\n" "but there's nothing to unlock\n" "right now -- nothing has changed.\n" ); } else { text = M_GetText( "This is the correct password, but\n" "there's nothing to unlock right\n" "now, so saving is still allowed!\n" ); usedTourney = true; } } M_StartMessage("Tournament Mode", text, NULL, MM_NOTHING, NULL, NULL); } void f_bighead() { CV_SetValue(&cv_bighead, !cv_bighead.value); if (cv_bighead.value) { S_StartSound(NULL, sfx_gshbb); } else { S_StartSound(NULL, sfx_kc46); } } void f_shittysigns() { CV_SetValue(&cv_shittysigns, !cv_shittysigns.value); if (cv_shittysigns.value) { S_StartSound(NULL, sfx_mixup); } else { S_StartSound(NULL, sfx_nghurt); } } void f_tastelesstaunts() { CV_SetValue(&cv_tastelesstaunts, !cv_tastelesstaunts.value); if (cv_tastelesstaunts.value) { S_StartSound(NULL, sfx_d4getm); } else { S_StartSound(NULL, sfx_kc46); } } void f_4thgear() { CV_SetValue(&cv_4thgear, !cv_4thgear.value); if (cv_4thgear.value) { M_StartMessage("Restraint device compromised!", "Local play sped up to ""\x85""4th Gear!""\x80""\nLet's see what you're made of!\n\n""\x86""No effect in netplay and attack modes.", NULL, MM_NOTHING, NULL, NULL); S_StartSound(NULL, sfx_gshc4); } else { S_StartSound(NULL, sfx_kc46); } } void f_colors() { UINT16 i; boolean success = false; for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type != SECRET_COLOR) continue; gamedata->unlocked[i] = true; success = true; } if (success) { S_StartSound(0, sfx_kc42); M_StartMessage("Time for a new look!", "Unlocked all colors. Try not to show off!", NULL, MM_NOTHING, NULL, NULL); G_SaveGameData(); } else { M_StartMessage("Time for a new look!", "You've already unlocked all colors.", NULL, MM_NOTHING, NULL, NULL); } } void f_followers() { UINT16 i; boolean success = false; for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type != SECRET_FOLLOWER) continue; gamedata->unlocked[i] = true; success = true; } if (success) { S_StartSound(0, sfx_kc42); M_StartMessage("Creatures captured!", "Unlocked all followers. Who's your favorite?", NULL, MM_NOTHING, NULL, NULL); G_SaveGameData(); } else { M_StartMessage("Creatures captured!", "You've already unlocked all followers.", NULL, MM_NOTHING, NULL, NULL); } } void f_maps() { UINT16 i; boolean success = false; for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type != SECRET_MAP && unlockables[i].type != SECRET_CUP) continue; gamedata->unlocked[i] = true; success = true; } #define GD_MV_SET (MV_VISITED|MV_BEATEN) for (i = 0; i < nummapheaders; i++) { if ((mapheaderinfo[i]->records.mapvisited & GD_MV_SET) == GD_MV_SET) continue; if (mapheaderinfo[i]->typeoflevel & TOL_VERSUS) continue; if (!strcmp(mapheaderinfo[i]->lumpname, "RR_HIDDENPALACE")) continue; mapheaderinfo[i]->records.mapvisited |= GD_MV_SET; success = true; } #undef GD_MV_SET if (success) { S_StartSound(0, sfx_kc42); M_StartMessage("// FIXME don't crash in certification test", "Unlocked most maps. Go see the world!", NULL, MM_NOTHING, NULL, NULL); G_SaveGameData(); } else { M_StartMessage("// FIXME don't crash in certification test", "There are no maps to unlock.", NULL, MM_NOTHING, NULL, NULL); } } void f_characters() { UINT16 i; boolean success = false; for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type != SECRET_SKIN) continue; gamedata->unlocked[i] = true; success = true; } if (success) { S_StartSound(0, sfx_kc42); M_StartMessage("...Is that how you spell it?", "Unlocked most characters. All together now!", NULL, MM_NOTHING, NULL, NULL); G_SaveGameData(); } else { M_StartMessage("...Is that how you spell it?", "There are no characters to unlock!", NULL, MM_NOTHING, NULL, NULL); } } void f_altmusic() { UINT16 i; boolean success = false; for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type != SECRET_ALTMUSIC && unlockables[i].type != SECRET_SOUNDTEST) continue; gamedata->unlocked[i] = true; success = true; } if (success) { S_StartSound(0, sfx_kc42); M_StartMessage("Wanna listen to some tunes?", "Unlocked all alternate music -- and Stereo Mode!", NULL, MM_NOTHING, NULL, NULL); G_SaveGameData(); } else { M_StartMessage("Wanna listen to some tunes?", "You've already unlocked all music!", NULL, MM_NOTHING, NULL, NULL); } } void f_timeattack() { UINT16 i; boolean success = false; boolean already_have_encore = M_SecretUnlocked(SECRET_ENCORE, true); for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type == SECRET_TIMEATTACK || unlockables[i].type == SECRET_PRISONBREAK || unlockables[i].type == SECRET_SPECIALATTACK || (unlockables[i].type == SECRET_SPBATTACK && already_have_encore)) { gamedata->unlocked[i] = true; success = true; } } if (success) { S_StartSound(0, sfx_kc42); if (already_have_encore) { M_StartMessage("Time Trial ON, OK!", "Unlocked all Time Attack modes -- including SPB Attack!", NULL, MM_NOTHING, NULL, NULL); } else { M_StartMessage("Time Trial ON, OK!", "Unlocked all Time Attack modes!", NULL, MM_NOTHING, NULL, NULL); } G_SaveGameData(); } else { M_StartMessage("Time Trial ON, OK!", "You already have all Time Attack modes.", NULL, MM_NOTHING, NULL, NULL); } } void f_encore() { UINT16 i; boolean success = false; boolean already_have_timeattacks = ( M_SecretUnlocked(SECRET_TIMEATTACK, true) && M_SecretUnlocked(SECRET_PRISONBREAK, true) && M_SecretUnlocked(SECRET_SPECIALATTACK, true) ); for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type == SECRET_ENCORE || (unlockables[i].type == SECRET_SPBATTACK && already_have_timeattacks)) { gamedata->unlocked[i] = true; success = true; } } if (success) { S_StartSound(0, sfx_kc42); if (already_have_timeattacks) { M_StartMessage("And turn it all around!", "Unlocked Encore Mode -- and SPB Attack!", NULL, MM_NOTHING, NULL, NULL); } else { M_StartMessage("And turn it all around!", "Unlocked Encore Mode!", NULL, MM_NOTHING, NULL, NULL); } G_SaveGameData(); } else { M_StartMessage("And turn it all around!", "You already have Encore Mode.", NULL, MM_NOTHING, NULL, NULL); } } void f_difficulty() { UINT16 i; boolean success = false; for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; if (unlockables[i].conditionset == CH_FURYBIKE) continue; if (gamedata->unlocked[i]) continue; if (unlockables[i].type == SECRET_HARDSPEED || unlockables[i].type == SECRET_MASTERMODE) { gamedata->unlocked[i] = true; success = true; } } if (success) { S_StartSound(0, sfx_kc42); M_StartMessage("TWO SPEEDS,", "All Gear speeds unlocked!", NULL, MM_NOTHING, NULL, NULL); G_SaveGameData(); } else { M_StartMessage("TWO SPEEDS,", "You already have all Gear speeds.", NULL, MM_NOTHING, NULL, NULL); } } void f_devmode() { INT32 i; if (modifiedgame) return; // Just unlock all the things and turn on -debug and console devmode. for (i = 0; i < MAXUNLOCKABLES; i++) { if (!unlockables[i].conditionset) continue; gamedata->unlocked[i] = true; } // Unlock all hidden levels. for (i = 0; i < nummapheaders; i++) { mapheaderinfo[i]->records.mapvisited = MV_MAX; } gamedata->gonerlevel = GDGONER_DONE; gamedata->sealedswapalerted = true; M_ClearMenus(true); // This is a developer feature, you know how to delete ringdata // G_SetUsedCheats(); S_StartSound(0, sfx_kc42); devparm = true; cht_debug |= 0x8000; G_SaveGameData(); } void f_proceed() { gamedata->gonerlevel = GDGONER_DONE; gamedata->finishedtutorialchallenge = true; M_UpdateUnlockablesAndExtraEmblems(true, true); M_ClearMenus(true); S_StartSound(0, sfx_kc42); G_SaveGameData(); } }; // namespace try_password_e M_TryPassword(const char *password, boolean conditions) { using var = std::variant; // Normalize input casing std::string key = password; strlwr(key.data()); UINT8 key_hash[M_PW_HASH_SIZE]; M_HashPassword(key_hash, key.c_str(), kRRSalt); auto worker = [key_hash](const UINT8* hash, var result) { if (memcmp(key_hash, hash, M_PW_HASH_SIZE)) result = std::monostate {}; // fail state return result; }; var result; auto add_job = [&](auto&&... args) { if (var n = worker(args...); !std::holds_alternative(n)) result = n; }; for (Pw& pw : passwords) add_job(pw.hash_.data(), &pw); // Only consider challenges passwords as needed. if (conditions && !usedTourney) iter_conditions([&](condition_t* cn) { add_job((const UINT8*)cn->stringvar, cn); }); try_password_e return_code = M_PW_INVALID; if (!std::holds_alternative(result)) { // Evaluate the password's function. auto visitor = srb2::Overload { [&](condition_t* cn) { // Remove the password for this session. Z_Free(cn->stringvar); cn->stringvar = NULL; return_code = M_PW_CHALLENGES; }, [&](Pw* pw) { pw->cb_(); return_code = M_PW_EXTRAS; }, [](std::monostate) {}, }; std::visit(visitor, result); } return return_code; } boolean M_TryExactPassword(const char *password, const char *encodedhash) { // Normalize input casing std::string key = password; strlwr(key.data()); UINT8 key_hash[M_PW_HASH_SIZE]; M_HashPassword(key_hash, key.c_str(), kRRSalt); auto hash = decode_hash(encodedhash); return (memcmp(key_hash, hash.data(), M_PW_HASH_SIZE) == 0); } #ifdef DEVELOP void Command_Crypt_f(void) { if (COM_Argc() == 1) { CONS_Printf( "crypt : generate a password hash\n" "crypt -i : generate multiple hashes by reading from file\n" ); return; } auto gen = [](char *input) { UINT8 bin[M_PW_BUF_SIZE]; strlwr(input); M_HashPassword(bin, input, kRRSalt); CONS_Printf("%s %s\n", input, modp::b64_encode((const char*)bin, M_PW_BUF_SIZE).c_str()); }; if (!stricmp(COM_Argv(1), "-i")) { if (COM_Argc() != 3) { CONS_Printf("crypt: missing file argument\n"); return; } std::ifstream file{va(pandf, srb2home, COM_Argv(2))}; if (!file.is_open()) { CONS_Printf("crypt: file error\n"); return; } for (std::string line; std::getline(file, line);) { // remove comments std::size_t p = line.find("#"); if (p == line.npos) p = line.size(); // remove trailing whitespace while (p > 0 && std::isspace(line[p - 1])) p--; line.erase(p); // ignore empty or completely filtered lines if (!line.empty()) gen(line.data()); } return; } gen(COM_Args()); } #endif void M_PasswordInit(void) { passwords.emplace_back(f_tournament, "dSZpCST31Tu3rPJ4z18iR9Tcv+9Xi8/f7nQGplj2mvruy2A4CJJqZm1gzi6CQKl68pRXiNGUX0n4BI2LjaBcoA=="); // Tee hee. passwords.emplace_back([] { M_WrongWarp(0); }, "WAJJ66pw2rSopXOuw4c4iKzIz3goKtivrv7b/THqYP8ev+E/sRn2LMXHqv8s+uzwMcVNoDxNn+AgG26xi+wgzg=="); passwords.emplace_back([] { M_GonerGDQ(true); }, "B287p2gJUgmUikAABl1ndG/3r0zqdIMvsMDzBrypwo78BR58S9Whu+Doma00oV+DySTalWYi1VyTs/5GWzgFEg=="); passwords.emplace_back([] { M_GonerGDQ(false); }, "1yO8FCDe0PhtgrQt0IQ4TPPfggSOnf4NiRaT86gnj4/PxMbyi4vXl4F4zpm/Xhf2oSStuhr+n7Qv2tcqv6lzaA=="); passwords.emplace_back(f_bighead, "V+YkwthNUePKS7zs5uB90VwN6Jeqgl+1r663U5zSGOEIxAO6BoWipzZoxa5H//LM+5Ag9GIGRnEcLbU21hjGfQ=="); passwords.emplace_back(f_4thgear, "zRMhR+s27VTYE0jgFf2l+PX51N3qJPvZ3oWuM/71oUaKY5zyQ2y7WIrIb464MFWn4IsK2P5rShsR9MotC/9ojQ=="); passwords.emplace_back(f_shittysigns, "yd02TPSLRgBydXlkZaEJABqegGjfJfn1aIMODfc2CC5ymJ4ydG7FblW20CH6vbo1IB1X9eBKJShuunPBClnWOQ=="); passwords.emplace_back(f_tastelesstaunts, "4QfCuCG0/7z5U5A1hxqlqx83uQTGmQ1aaWPBQ8pqQvw9KRGvxxiDq9UF0N24fDlu0+XYksgkPHJg4A5h5aEQiw=="); passwords.emplace_back(f_devmode, "ybYqLUlREa9TJqV0uftxqGL8jPR1U+uEgrff/jast0kCfeIdzY15VxjveSZho8GOjfRuC3Zt4aJQTvhJcPAhkw=="); passwords.emplace_back(f_proceed, "GZPKJsa++Tt134yS3eBKdP+8vdAHB1thwK2ys6VDfFxcIRtABtM9j4qt8WULFrI+KxCSYMZ6K0mwt5BVzcvvuw=="); passwords.emplace_back(f_colors, "aSk8dw6FzJtTEmovh8fVEtUBpu6lj3QlRT/B5lwiEhAw8dAhRBQLdvtYlPaQcZISWI4wneAfAo6w5d6uf5r++g=="); passwords.emplace_back(f_followers, "zYCIZw2qcnUbtF0P2ybLNHajdl8zrje0hzGex7yuMFe7fj4mvx4AegoMmvir28YvAbfAqkz/ekQRzr+RhrycHw=="); passwords.emplace_back(f_maps, "u/Svaf+DCnCpJ8xmP3AVP4CK6X6X4O3fY73cmIq88ZJEygwz+n+L66q4Vhlv13vWgld1PEyRszFErzflQt9WZw=="); passwords.emplace_back(f_characters, "MohmPqpaGSd3MEHLfQKUFl/Yg8pHE+12X1LHEP59Gs/5w1u8mPtGUXNv1GYTF+c8gQqT5hXpZ3FeZ/EfCxo34g=="); passwords.emplace_back(f_altmusic, "dZgxKNagOtB9F7wXqUUPzsuq4tfQlfK8ZqEeFXdI3Hd+k5tYfRm3ToLgbqawaNmwuLVrJ8PB+QnH4gT3ojnTMw=="); passwords.emplace_back(f_timeattack, "mFu5OB9d6jnc2kth7HE66wJ42F/GHDzSvuciK1Qw++6iGnpBccxcKjpoxgOvD3eIoqR606ruBINuXi23proXHQ=="); passwords.emplace_back(f_encore, "i5u5sIsMs5eITy+LzAXvKm6D9OzOVKhUqSy1mTTV/oUxJX6RPsk8OcyLbNaey9Vc6wXOhz+2+mTXILkIRzvXqA=="); passwords.emplace_back(f_difficulty, "MKjOtEFLkgXf21uiECdBTU6XtbkuFWaGh7i8znKo7JrXXEDrCBJmGwINvPg0T3TLn0zlscLvmC5nve7I+NTrnA=="); }