// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by "Lat'". // Copyright (C) 2024 by AJ "Tyron" Martinez. // 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. //----------------------------------------------------------------------------- /// \file k_profiles.c /// \brief implements methods for profiles etc. #include #include #include #include "io/streams.hpp" #include "doomtype.h" #include "d_main.h" // pandf #include "byteptr.h" // READ/WRITE macros #include "p_saveg.h" // savebuffer_t #include "m_misc.h" //FIL_WriteFile() #include "k_profiles.h" #include "z_zone.h" #include "r_skins.h" #include "monocypher/monocypher.h" #include "stun.h" #include "k_color.h" #include "command.h" extern "C" consvar_t cv_dummyprofilefov, cv_fov[MAXSPLITSCREENPLAYERS]; CV_PossibleValue_t lastprofile_cons_t[] = {{-1, "MIN"}, {MAXPROFILES, "MAX"}, {0, NULL}}; // List of all the profiles. static profile_t *profilesList[MAXPROFILES+1]; // +1 because we're gonna add a default "GUEST' profile. static UINT8 numprofiles = 0; // # of loaded profiles INT32 PR_GetNumProfiles(void) { return numprofiles; } static void PR_GenerateProfileKeys(profile_t *newprofile) { static uint8_t seed[32]; csprng(seed, 32); crypto_eddsa_key_pair(newprofile->secret_key, newprofile->public_key, seed); } profile_t* PR_MakeProfile( const char *prname, const char *pname, const char *sname, const UINT16 col, const char *fname, const UINT16 fcol, INT32 controlarray[num_gamecontrols][MAXINPUTMAPPING], boolean guest) { profile_t *newprofile = static_cast(Z_Calloc(sizeof(profile_t), PU_STATIC, NULL)); newprofile->version = PROFILEVER; memset(newprofile->secret_key, 0, sizeof(newprofile->secret_key)); memset(newprofile->public_key, 0, sizeof(newprofile->public_key)); if (!guest) { PR_GenerateProfileKeys(newprofile); } strcpy(newprofile->profilename, prname); newprofile->profilename[sizeof newprofile->profilename - 1] = '\0'; strcpy(newprofile->skinname, sname); strcpy(newprofile->playername, pname); newprofile->color = col; strcpy(newprofile->follower, fname); newprofile->followercolor = fcol; newprofile->kickstartaccel = false; newprofile->autoroulette = false; newprofile->litesteer = false; newprofile->descriptiveinput = 1; newprofile->autoring = false; newprofile->rumble = true; newprofile->fov = atoi(cv_dummyprofilefov.defaultvalue); // Copy from gamecontrol directly as we'll be setting controls up directly in the profile. memcpy(newprofile->controls, controlarray, sizeof(newprofile->controls)); newprofile->wins = 0; newprofile->rounds = 0; return newprofile; } profile_t* PR_MakeProfileFromPlayer(const char *prname, const char *pname, const char *sname, const UINT16 col, const char *fname, UINT16 fcol, UINT8 pnum) { // Generate profile using the player's gamecontrol, as we set them directly when making profiles from menus. profile_t *newprofile = PR_MakeProfile(prname, pname, sname, col, fname, fcol, gamecontrol[pnum], false); // Player bound cvars: newprofile->kickstartaccel = cv_kickstartaccel[pnum].value; newprofile->autoroulette = cv_autoroulette[pnum].value; newprofile->litesteer = cv_litesteer[pnum].value; newprofile->descriptiveinput = cv_descriptiveinput[pnum].value; newprofile->autoring = cv_autoring[pnum].value; newprofile->rumble = cv_rumble[pnum].value; newprofile->fov = cv_fov[pnum].value / FRACUNIT; return newprofile; } boolean PR_AddProfile(profile_t *p) { if (numprofiles < MAXPROFILES+1) { profilesList[numprofiles] = p; numprofiles++; CONS_Printf("Profile '%s' added\n", p->profilename); return true; } else return false; } profile_t* PR_GetProfile(INT32 num) { if (num < numprofiles) return profilesList[num]; else return NULL; } boolean PR_DeleteProfile(INT32 num) { UINT8 i; profile_t* sacrifice; if (num <= 0 || num > numprofiles) { return false; } sacrifice = profilesList[num]; // If we're deleting inbetween profiles, move everything. if (num < numprofiles) { for (i = num; i < numprofiles-1; i++) { profilesList[i] = profilesList[i+1]; } // Make sure to move cv_lastprofile (and title/current profile) values as well! for (i = 0; i < MAXSPLITSCREENPLAYERS+2; i++) { consvar_t *cv; if (i < MAXSPLITSCREENPLAYERS) cv = &cv_lastprofile[i]; else if (i == MAXSPLITSCREENPLAYERS) cv = &cv_ttlprofilen; else cv = &cv_currprofile; if (cv->value < num) { // Not affected. continue; } if (cv->value > num) { // Shift our lastprofile number down to match the new order. CV_StealthSetValue(cv, cv->value-1); continue; } if (cv != &cv_currprofile) { // There's no hope for it. If we were on the deleted profile, default back to guest. CV_StealthSetValue(cv, PROFILE_GUEST); continue; } // Oh boy, now we're really in for it. CV_StealthSetValue(cv, -1); } } // In any case, delete the last profile as well. profilesList[numprofiles] = NULL; numprofiles--; PR_SaveProfiles(); // Finally, clear up our memory! Z_Free(sacrifice); return true; } void PR_InitNewProfile(void) { char pname[PROFILENAMELEN+1] = "PRF"; profile_t *dprofile; UINT8 usenum = numprofiles-1; UINT8 i; boolean nameok = false; pname[4] = '\0'; // When deleting profile, it's possible to do some pretty wacko stuff that would lead a new fresh profile to share the same name as another profile we have never changed the name of. // This could become an infinite loop if MAXPROFILES >= 26. while (!nameok) { pname[3] = 'A'+usenum; for (i = 0; i < numprofiles; i++) { profile_t *pr = PR_GetProfile(i); if (!strcmp(pr->profilename, pname)) { usenum++; if (pname[3] == 'Z') usenum = 0; break; } // if we got here, then it means the name is okay! if (i == numprofiles-1) nameok = true; } } dprofile = PR_MakeProfile( pname, PROFILEDEFAULTPNAME, PROFILEDEFAULTSKIN, PROFILEDEFAULTCOLOR, PROFILEDEFAULTFOLLOWER, PROFILEDEFAULTFOLLOWERCOLOR, gamecontroldefault, false ); PR_AddProfile(dprofile); } void PR_SaveProfiles(void) { namespace fs = std::filesystem; using json = nlohmann::json; using namespace srb2; namespace io = srb2::io; if (profilesList[PROFILE_GUEST] == NULL) { // Profiles have not been loaded yet, don't overwrite with garbage. return; } ProfilesJson ng{}; for (size_t i = 1; i < numprofiles; i++) { ProfileJson jsonprof; profile_t* cprof = profilesList[i]; jsonprof.version = PROFILEVER; jsonprof.profilename = std::string(cprof->profilename); std::copy(std::begin(cprof->public_key), std::end(cprof->public_key), std::begin(jsonprof.publickey)); std::copy(std::begin(cprof->secret_key), std::end(cprof->secret_key), std::begin(jsonprof.secretkey)); jsonprof.playername = std::string(cprof->playername); jsonprof.skinname = std::string(cprof->skinname); jsonprof.colorname = std::string(skincolors[cprof->color].name); jsonprof.followername = std::string(cprof->follower); if (cprof->followercolor == FOLLOWERCOLOR_MATCH) { jsonprof.followercolorname = "Match"; } else if (cprof->followercolor == FOLLOWERCOLOR_OPPOSITE) { jsonprof.followercolorname = "Opposite"; } else if (cprof->followercolor == SKINCOLOR_NONE) { jsonprof.followercolorname = "Default"; } else if (cprof->followercolor >= numskincolors) { jsonprof.followercolorname = std::string(); } else { jsonprof.followercolorname = std::string(skincolors[cprof->followercolor].name); } jsonprof.records.wins = cprof->wins; jsonprof.records.rounds = cprof->rounds; jsonprof.preferences.kickstartaccel = cprof->kickstartaccel; jsonprof.preferences.autoroulette = cprof->autoroulette; jsonprof.preferences.litesteer = cprof->litesteer; jsonprof.preferences.descriptiveinput = cprof->descriptiveinput; jsonprof.preferences.autoring = cprof->autoring; jsonprof.preferences.rumble = cprof->rumble; jsonprof.preferences.fov = cprof->fov; for (size_t j = 0; j < num_gamecontrols; j++) { for (size_t k = 0; k < MAXINPUTMAPPING; k++) { jsonprof.controls[j][k] = cprof->controls[j][k]; } } ng.profiles.emplace_back(std::move(jsonprof)); } std::vector ubjson = json::to_ubjson(ng); std::string realpath = fmt::format("{}/{}", srb2home, PROFILESFILE); std::string bakpath = fmt::format("{}.bak", realpath); if (fs::exists(realpath)) { try { fs::rename(realpath, bakpath); } catch (const fs::filesystem_error& ex) { CONS_Alert(CONS_ERROR, "Failed to record profiles backup. Not attempting to save profiles. %s\n", ex.what()); return; } } try { io::FileStream file {realpath, io::FileStreamMode::kWrite}; io::write(static_cast(0x52494E47), file, io::Endian::kBE); // "RING" io::write(static_cast(0x5052464C), file, io::Endian::kBE); // "PRFL" io::write(static_cast(0), file); // reserved1 io::write(static_cast(0), file); // reserved2 io::write(static_cast(0), file); // reserved3 io::write(static_cast(0), file); // reserved4 io::write_exact(file, tcb::as_bytes(tcb::make_span(ubjson))); file.close(); } catch (const std::exception& ex) { I_Error("Couldn't save profiles. Are you out of Disk space / playing in a protected folder? Check directory for a ringprofiles.prf.bak if the profiles file is corrupt.\n\nException: %s", ex.what()); } catch (...) { I_Error("Couldn't save profiles. Are you out of Disk space / playing in a protected folder? Check directory for a ringprofiles.prf.bak if the profiles file is corrupt."); } } void PR_LoadProfiles(void) { namespace fs = std::filesystem; using namespace srb2; namespace io = srb2::io; using json = nlohmann::json; profile_t *dprofile = PR_MakeProfile( PROFILEDEFAULTNAME, PROFILEDEFAULTPNAME, PROFILEDEFAULTSKIN, PROFILEDEFAULTCOLOR, PROFILEDEFAULTFOLLOWER, PROFILEDEFAULTFOLLOWERCOLOR, gamecontroldefault, true ); std::string datapath {fmt::format("{}/{}", srb2home, PROFILESFILE)}; io::BufferedInputStream bis; try { io::FileStream file {datapath, io::FileStreamMode::kRead}; bis = io::BufferedInputStream(std::move(file)); } catch (const io::FileStreamException& ex) { PR_AddProfile(dprofile); return; } ProfilesJson js; try { uint32_t magic1; uint32_t magic2; uint8_t reserved1; uint8_t reserved2; uint8_t reserved3; uint8_t reserved4; magic1 = io::read_uint32(bis, io::Endian::kBE); magic2 = io::read_uint32(bis, io::Endian::kBE); reserved1 = io::read_uint8(bis); reserved2 = io::read_uint8(bis); reserved3 = io::read_uint8(bis); reserved4 = io::read_uint8(bis); if (magic1 != 0x52494E47 || magic2 != 0x5052464C || reserved1 != 0 || reserved2 != 0 || reserved3 != 0 || reserved4 != 0) { throw std::domain_error("Header is incompatible"); } std::vector remainder = io::read_to_vec(bis); // safety: std::byte repr is always uint8_t 1-byte aligned tcb::span remainder_as_u8 = tcb::span((uint8_t*)remainder.data(), remainder.size()); json parsed = json::from_ubjson(remainder_as_u8); js = parsed.template get(); } catch (const std::exception& ex) { I_Error("Profiles file is corrupt.\n\nException: %s", ex.what()); return; } catch (...) { I_Error("Profiles file is corrupt."); return; } numprofiles = js.profiles.size() + 1; // 1 for guest if (numprofiles > MAXPROFILES+1) { numprofiles = MAXPROFILES+1; } for (size_t i = 1; i < numprofiles; i++) { bool converted = false; auto& jsprof = js.profiles[i - 1]; profile_t* newprof = static_cast(Z_Calloc(sizeof(profile_t), PU_STATIC, NULL)); profilesList[i] = newprof; newprof->version = jsprof.version; strlcpy(newprof->profilename, jsprof.profilename.c_str(), sizeof(newprof->profilename)); memcpy(newprof->public_key, jsprof.publickey.data(), sizeof(newprof->public_key)); memcpy(newprof->secret_key, jsprof.secretkey.data(), sizeof(newprof->secret_key)); strlcpy(newprof->playername, jsprof.playername.c_str(), sizeof(newprof->playername)); strlcpy(newprof->skinname, jsprof.skinname.c_str(), sizeof(newprof->skinname)); newprof->color = PROFILEDEFAULTCOLOR; for (size_t c = 0; c < numskincolors; c++) { if (jsprof.colorname == skincolors[c].name && K_ColorUsable(static_cast(c), false, false)) { newprof->color = c; break; } } strlcpy(newprof->follower, jsprof.followername.c_str(), sizeof(newprof->follower)); newprof->followercolor = PROFILEDEFAULTFOLLOWERCOLOR; if (jsprof.followercolorname == "Match") { newprof->followercolor = FOLLOWERCOLOR_MATCH; } else if (jsprof.followercolorname == "Opposite") { newprof->followercolor = FOLLOWERCOLOR_OPPOSITE; } else if (jsprof.followercolorname == "Default") { newprof->followercolor = SKINCOLOR_NONE; } else if (!jsprof.followercolorname.empty()) { for (size_t c = 0; c < numskincolors; c++) { if (jsprof.followercolorname == skincolors[c].name && K_ColorUsable(static_cast(c), false, false)) { newprof->followercolor = c; break; } } } newprof->wins = jsprof.records.wins; newprof->rounds = jsprof.records.rounds; newprof->kickstartaccel = jsprof.preferences.kickstartaccel; newprof->autoroulette = jsprof.preferences.autoroulette; newprof->litesteer = jsprof.preferences.litesteer; newprof->descriptiveinput = jsprof.preferences.descriptiveinput; newprof->autoring = jsprof.preferences.autoring; newprof->rumble = jsprof.preferences.rumble; newprof->fov = jsprof.preferences.fov; try { for (size_t j = 0; j < num_gamecontrols; j++) { for (size_t k = 0; k < MAXINPUTMAPPING; k++) { newprof->controls[j][k] = jsprof.controls.at(j).at(k); } } } catch (const std::out_of_range& ex) { I_Error("Profile '%s' controls are corrupt", jsprof.playername.c_str()); return; } if (jsprof.version == 1) { // Version 1 -> 2: // - litesteer is now off by default, reset old profiles newprof->litesteer = false; // - fov is now 100 by default, reset if it was left at 90 (old default) if (newprof->fov == 90) { newprof->fov = 100; } auto unbound = [](const INT32* map) { INT32 zero[MAXINPUTMAPPING] = {}; return !memcmp(map, zero, sizeof zero); }; if (unbound(newprof->controls[gc_talk])) { // - unbound talk control gets reset to default memcpy(newprof->controls[gc_talk], gamecontroldefault[gc_talk], sizeof newprof->controls[gc_talk]); } converted = true; } if (jsprof.version < 3) { // Version 2 -> 3: // - Auto Roulette is turned off again so people have to see the warning message newprof->autoroulette = false; converted = true; } if (jsprof.version < 4) { newprof->descriptiveinput = 1; converted = true; } if (converted) { CONS_Printf("Profile '%s' was converted from version %d to version %d\n", newprof->profilename, jsprof.version, PROFILEVER); } } profilesList[PROFILE_GUEST] = dprofile; } static void PR_ApplyProfile_Appearance(profile_t *p, UINT8 playernum) { CV_StealthSet(&cv_skin[playernum], p->skinname); CV_StealthSetValue(&cv_playercolor[playernum], p->color); CV_StealthSet(&cv_playername[playernum], p->playername); // Followers CV_StealthSet(&cv_follower[playernum], p->follower); CV_StealthSetValue(&cv_followercolor[playernum], p->followercolor); } static void PR_ApplyProfile_Settings(profile_t *p, UINT8 playernum) { // toggles -- be sure to also adjust M_ProfileEditApply CV_StealthSetValue(&cv_kickstartaccel[playernum], p->kickstartaccel); CV_StealthSetValue(&cv_autoroulette[playernum], p->autoroulette); CV_StealthSetValue(&cv_litesteer[playernum], p->litesteer); CV_StealthSetValue(&cv_descriptiveinput[playernum], p->descriptiveinput); CV_StealthSetValue(&cv_autoring[playernum], p->autoring); CV_StealthSetValue(&cv_rumble[playernum], p->rumble); CV_StealthSetValue(&cv_fov[playernum], p->fov); // set controls... G_ApplyControlScheme(playernum, p->controls); } static void PR_ApplyProfile_Memory(UINT8 profilenum, UINT8 playernum) { // set memory cvar CV_StealthSetValue(&cv_lastprofile[playernum], profilenum); // If we're doing this on P1, also change current profile. if (playernum == 0) { CV_StealthSetValue(&cv_currprofile, profilenum); } } void PR_ApplyProfile(UINT8 profilenum, UINT8 playernum) { profile_t *p = PR_GetProfile(profilenum); // this CAN happen!! if (dedicated || p == NULL) { if (!dedicated) CONS_Printf("Profile '%d' could not be loaded as it does not exist. Guest Profile will be loaded instead.\n", profilenum); profilenum = 0; // make sure to set this so that the cvar is set properly. p = PR_GetProfile(profilenum); } PR_ApplyProfile_Appearance(p, playernum); PR_ApplyProfile_Settings(p, playernum); PR_ApplyProfile_Memory(profilenum, playernum); } void PR_ApplyProfileLight(UINT8 profilenum, UINT8 playernum) { profile_t *p = PR_GetProfile(profilenum); // this CAN happen!! if (p == NULL) { // no need to be as loud... profilenum = 0; // make sure to set this so that the cvar is set properly. p = PR_GetProfile(profilenum); } PR_ApplyProfile_Appearance(p, playernum); } void PR_ApplyProfileToggles(UINT8 profilenum, UINT8 playernum) { profile_t *p = PR_GetProfile(profilenum); // this CAN happen!! if (p == NULL) { // no need to be as loud... profilenum = 0; // make sure to set this so that the cvar is set properly. p = PR_GetProfile(profilenum); } PR_ApplyProfile_Settings(p, playernum); } void PR_ApplyProfilePretend(UINT8 profilenum, UINT8 playernum) { profile_t *p = PR_GetProfile(profilenum); // this CAN happen!! if (dedicated || p == NULL) { if (!dedicated) CONS_Printf("Profile '%d' could not be loaded as it does not exist. Guest Profile will be loaded instead.\n", profilenum); profilenum = 0; // make sure to set this so that the cvar is set properly. p = PR_GetProfile(profilenum); } PR_ApplyProfile_Memory(profilenum, playernum); } UINT8 PR_GetProfileNum(profile_t *p) { UINT8 i; for (i = 0; i < MAXPROFILES+1; i++) { profile_t *comp = PR_GetProfile(i); if (comp == p) return i; } return 0; } SINT8 PR_ProfileUsedBy(profile_t *p) { UINT8 i; UINT8 prn = PR_GetProfileNum(p); for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { if (prn == cv_lastprofile[i].value) return i; } return -1; } profile_t *PR_GetPlayerProfile(player_t *player) { const UINT8 playerNum = (player - players); UINT8 i; if (demo.playback) { return NULL; } for (i = 0; i <= splitscreen; i++) { if (playerNum == g_localplayers[i]) { return PR_GetProfile(cv_lastprofile[i].value); } } return NULL; } profile_t *PR_GetLocalPlayerProfile(INT32 player) { if (player >= MAXSPLITSCREENPLAYERS) return NULL; return PR_GetProfile(cv_lastprofile[player].value); } boolean PR_IsLocalPlayerGuest(INT32 player) { return !(cv_lastprofile[player].value); } static char rrid_buf[256]; char *GetPrettyRRID(const unsigned char *bin, boolean brief) { size_t i; size_t len = PUBKEYLENGTH; if (brief) len = 8; if (bin == NULL || len == 0) return NULL; for (i=0; i> 4]; rrid_buf[i*2+1] = "0123456789ABCDEF"[bin[i] & 0x0F]; } rrid_buf[len*2] = '\0'; return rrid_buf; } unsigned char *FromPrettyRRID(unsigned char *bin, const char *text) { size_t i; size_t len = PUBKEYLENGTH * 2; if (strlen(text) != len) return NULL; for (i = 0; i < len; i += 2) { char byt[3] = { text[i], text[i+1], '\0' }; char *p; bin[i/2] = strtol(byt, &p, 16); if (*p) // input is not hexadecimal return NULL; } return bin; } static char allZero[PUBKEYLENGTH]; boolean PR_IsKeyGuest(uint8_t *key) { //memset(allZero, 0, PUBKEYLENGTH); -- not required, allZero is 0's return (memcmp(key, allZero, PUBKEYLENGTH) == 0); }