// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by Sally "TehRealSalt" Cochenour // 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. //----------------------------------------------------------------------------- // \brief Power Level system #include "k_pwrlv.h" #include "d_netcmd.h" #include "g_game.h" #include "s_sound.h" #include "m_random.h" #include "m_cond.h" // M_UpdateUnlockablesAndExtraEmblems #include "p_tick.h" // leveltime #include "k_grandprix.h" #include "k_profiles.h" #include "k_serverstats.h" // Client-sided calculations done for Power Levels. // This is done so that clients will never be able to hack someone else's score over the server. UINT16 clientpowerlevels[MAXPLAYERS][PWRLV_NUMTYPES]; // Total calculated power add during the match, // totalled at the end of the round. INT16 clientPowerAdd[MAXPLAYERS]; // Players who spectated mid-race UINT8 spectateGriefed = 0; // Game setting scrambles based on server Power Level SINT8 speedscramble = -1; SINT8 encorescramble = -1; SINT8 K_UsingPowerLevels(void) { if (!cv_kartusepwrlv.value) { // Explicitly forbidden. return PWRLV_DISABLED; } if (!(netgame || (demo.playback && demo.netgame))) { // Servers only. return PWRLV_DISABLED; } if (roundqueue.size > 0 && roundqueue.position > 0) { // When explicit progression is in place, we're going by different rules. return PWRLV_DISABLED; } if (gametype == GT_RACE) { // Race PWR. return PWRLV_RACE; } if (gametype == GT_BATTLE) { // Battle PWR. return PWRLV_BATTLE; } // We do not support PWR for custom gametypes at this moment in time. return PWRLV_DISABLED; } void K_ClearClientPowerLevels(void) { memset(clientpowerlevels, 0, sizeof clientpowerlevels); memset(clientPowerAdd, 0, sizeof clientPowerAdd); } // Adapted from this: http://wiki.tockdom.com/wiki/Player_Rating INT16 K_CalculatePowerLevelInc(INT16 diff) { INT16 control[10] = {0,0,0,1,8,50,125,125,125,125}; fixed_t increment = 0; fixed_t x; UINT8 j; #define MAXDIFF (PWRLVRECORD_MAX - 1) if (diff > MAXDIFF) diff = MAXDIFF; if (diff < -MAXDIFF) diff = -MAXDIFF; #undef MAXDIFF x = ((diff-2)<= (2<= (1<> FRACBITS); } INT16 K_PowerLevelPlacementScore(player_t *player) { if ((player->pflags & PF_NOCONTEST) || (player->spectator)) { return 0; } if (gametyperules & GTR_CIRCUIT) { return MAXPLAYERS - player->position; } else { return player->roundscore; } } INT16 K_CalculatePowerLevelAvg(void) { INT32 avg = 0; UINT8 div = 0; SINT8 t = PWRLV_DISABLED; UINT8 i; if (!netgame || !cv_kartusepwrlv.value) { CONS_Debug(DBG_PWRLV, "Not in a netgame, or not using power levels -- no average.\n"); return 0; // No average. } if ((gametyperules & GTR_CIRCUIT)) t = PWRLV_RACE; else if ((gametyperules & GTR_BUMPERS)) t = PWRLV_BATTLE; if (t == PWRLV_DISABLED) { CONS_Debug(DBG_PWRLV, "Could not set a power level type -- no average.\n"); return 0; // Hmm?! } for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator || clientpowerlevels[i][t] == 0) // splitscreen player continue; avg += clientpowerlevels[i][t]; div++; } if (!div) { CONS_Debug(DBG_PWRLV, "Found no players -- no average.\n"); return 0; // No average. } avg /= div; return (INT16)avg; } void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit) { const UINT8 playerNum = player - players; const boolean exitBonus = ((lap > numlaps) || (player->pflags & PF_NOCONTEST)); SINT8 powerType = K_UsingPowerLevels(); INT16 yourScore = 0; UINT16 yourPower = 0; UINT8 i; // Compare every single player against each other for power level increases. // Every player you won against gives you more points, and vice versa. // The amount of points won per match-up depends on the difference between the loser's power and the winner's power. // See K_CalculatePowerLevelInc for more info. if (powerType == PWRLV_DISABLED) { return; } if (!playeringame[playerNum] || player->spectator) { return; } CONS_Debug(DBG_PWRLV, "\n========\n"); CONS_Debug(DBG_PWRLV, "* Power Level change for player %s (LAP %d) *\n", player_names[playerNum], lap); CONS_Debug(DBG_PWRLV, "========\n"); yourPower = clientpowerlevels[playerNum][powerType]; if (yourPower == 0) { // Guests don't record power level changes. return; } CONS_Debug(DBG_PWRLV, "%s's PWR.LV: %d\n", player_names[playerNum], yourPower); yourScore = K_PowerLevelPlacementScore(player); CONS_Debug(DBG_PWRLV, "%s's gametype score: %d\n", player_names[playerNum], yourScore); CONS_Debug(DBG_PWRLV, "========\n"); for (i = 0; i < MAXPLAYERS; i++) { UINT16 theirScore = 0; INT16 theirPower = 0; INT16 diff = 0; // Loser PWR.LV - Winner PWR.LV INT16 inc = 0; // Total pt increment boolean won = false; if (i == playerNum) // Same person { continue; } if (!playeringame[i] || players[i].spectator) { continue; } CONS_Debug(DBG_PWRLV, "%s VS %s:\n", player_names[playerNum], player_names[i]); theirPower = clientpowerlevels[i][powerType]; if (theirPower == 0) { // No power level (splitscreen guests, bots) continue; } CONS_Debug(DBG_PWRLV, "%s's PWR.LV: %d\n", player_names[i], theirPower); if (forfeit == true) { diff = yourPower - theirPower; inc -= K_CalculatePowerLevelInc(diff); CONS_Debug(DBG_PWRLV, "FORFEIT! Diff is %d, increment is %d\n", diff, inc); } else { theirScore = K_PowerLevelPlacementScore(&players[i]); CONS_Debug(DBG_PWRLV, "%s's gametype score: %d\n", player_names[i], theirScore); if (yourScore == theirScore && forfeit == false) // Tie -- neither get any points for this match up. { CONS_Debug(DBG_PWRLV, "TIE, no change.\n"); continue; } won = (yourScore > theirScore); if (won == true && forfeit == false) // This player won! { diff = theirPower - yourPower; inc += K_CalculatePowerLevelInc(diff); CONS_Debug(DBG_PWRLV, "WON! Diff is %d, increment is %d\n", diff, inc); } else // This player lost... { diff = yourPower - theirPower; inc -= K_CalculatePowerLevelInc(diff); CONS_Debug(DBG_PWRLV, "LOST... Diff is %d, increment is %d\n", diff, inc); } } if (exitBonus == false) { INT16 prevInc = inc; inc /= max(numlaps-1, 1); if (inc == 0) { if (prevInc > 0) { inc = 1; } else if (prevInc < 0) { inc = -1; } } CONS_Debug(DBG_PWRLV, "Reduced (%d / %d = %d) because it's not the end of the race\n", prevInc, numlaps, inc); } CONS_Debug(DBG_PWRLV, "========\n"); if (inc == 0) { CONS_Debug(DBG_PWRLV, "Total Result: No increment, no change.\n"); continue; } CONS_Debug(DBG_PWRLV, "Total Result:\n"); CONS_Debug(DBG_PWRLV, "Increment: %d\n", inc); CONS_Debug(DBG_PWRLV, "%s current: %d\n", player_names[playerNum], clientPowerAdd[playerNum]); clientPowerAdd[playerNum] += inc; CONS_Debug(DBG_PWRLV, "%s final: %d\n", player_names[playerNum], clientPowerAdd[playerNum]); CONS_Debug(DBG_PWRLV, "%s current: %d\n", player_names[i], clientPowerAdd[i]); clientPowerAdd[i] -= inc; CONS_Debug(DBG_PWRLV, "%s final: %d\n", player_names[i], clientPowerAdd[i]); CONS_Debug(DBG_PWRLV, "========\n"); } } void K_UpdatePowerLevelsFinalize(player_t *player, boolean onForfeit) { // Finalize power level increments for any laps not yet calculated. // For spectate / quit / NO CONTEST INT16 lapsLeft = 0; UINT8 i; lapsLeft = (numlaps - player->latestlap) + 1; if (lapsLeft <= 0) { // We've done every lap already. return; } for (i = 0; i < lapsLeft; i++) { K_UpdatePowerLevels(player, player->latestlap + (i + 1), onForfeit); } player->latestlap = numlaps+1; } INT16 K_FinalPowerIncrement(player_t *player, INT16 yourPower, INT16 baseInc) { INT16 inc = baseInc; UINT8 numPlayers = 0; UINT8 i; if (yourPower == 0) { // Guests don't record power level changes. return 0; } SINT8 powerType = K_UsingPowerLevels(); for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) { continue; } INT16 theirPower = clientpowerlevels[i][powerType]; if (theirPower == 0) { // Don't count guests or bots. continue; } numPlayers++; } if (inc <= 0) { if (player->position == 1 && numPlayers > 1) { // Won the whole match? // Get at least one point. inc = 1; } #if 0 else { // You trade points in 1v1s, // but is more lenient in bigger lobbies. inc /= max(1, numPlayers-1); if (inc == 0) { if (baseInc > 0) { inc = 1; } else if (baseInc < 0) { inc = -1; } } } #endif } if (yourPower + inc > PWRLVRECORD_MAX) { inc -= ((yourPower + inc) - PWRLVRECORD_MAX); } if (yourPower + inc < PWRLVRECORD_MIN) { inc -= ((yourPower + inc) - PWRLVRECORD_MIN); } return inc; } void K_CashInPowerLevels(void) { SINT8 powerType = K_UsingPowerLevels(); UINT8 i; CONS_Debug(DBG_PWRLV, "\n========\n"); CONS_Debug(DBG_PWRLV, "Cashing in power level changes...\n"); CONS_Debug(DBG_PWRLV, "========\n"); for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] == true && powerType != PWRLV_DISABLED) { INT16 inc = K_FinalPowerIncrement(&players[i], clientpowerlevels[i][powerType], clientPowerAdd[i]); clientpowerlevels[i][powerType] += inc; CONS_Debug(DBG_PWRLV, "%s: %d -> %d (%d)\n", player_names[i], clientpowerlevels[i][powerType] - inc, clientpowerlevels[i][powerType], inc); } clientPowerAdd[i] = 0; } SV_UpdateStats(); CONS_Debug(DBG_PWRLV, "========\n"); } void K_SetPowerLevelScrambles(SINT8 powertype) { switch (powertype) { case PWRLV_RACE: if (cv_kartspeed.value == -1 || cv_kartencore.value == -1) { UINT8 speed = KARTSPEED_NORMAL; boolean encore = false; INT16 avg = 0, min = 0; UINT8 i, t = 1; avg = K_CalculatePowerLevelAvg(); for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator || clientpowerlevels[i][t] == 0) // splitscreen player continue; if (min == 0 || clientpowerlevels[i][0] < min) min = clientpowerlevels[i][0]; } CONS_Debug(DBG_GAMELOGIC, "Min: %d, Avg: %d\n", min, avg); if (avg == 0 || min == 0) { CONS_Debug(DBG_GAMELOGIC, "No average/minimum, no scramblin'.\n"); speedscramble = encorescramble = -1; return; } if (min >= 7800) { if (avg >= 8200) t = 5; else t = 4; } else if (min >= 6800) { if (avg >= 7200) t = 4; else t = 3; } else if (min >= 5800) { if (avg >= 6200) t = 3; else t = 2; } else if (min >= 3800) { if (avg >= 4200) t = 2; else t = 1; } #if 1 else t = 1; #else else if (min >= 1800) { if (avg >= 2200) t = 1; else t = 0; } else t = 0; #endif CONS_Debug(DBG_GAMELOGIC, "Table position: %d\n", t); switch (t) { case 5: speed = KARTSPEED_HARD; encore = P_RandomChance(PR_RULESCRAMBLE, FRACUNIT>>1); break; case 4: speed = P_RandomChance(PR_RULESCRAMBLE, (7<>1); break; case 3: speed = P_RandomChance(PR_RULESCRAMBLE, (3<>2); break; case 2: speed = KARTSPEED_NORMAL; encore = P_RandomChance(PR_RULESCRAMBLE, FRACUNIT>>3); break; case 1: default: speed = KARTSPEED_NORMAL; encore = false; break; case 0: speed = P_RandomChance(PR_RULESCRAMBLE, (3<