RingRacers/src/k_pwrlv.c
Callmore 5fbe9f9827 Use integer arithmetic for pwrlv avg calculation
This fixes an oversight with pwrlv average calculation that causes
the total to overflow with enough players or high enough pwrlv.
Hopefully this might fix that bug where pwrlv is shown as negative
on the server select menu.

Maintainer note: This is still imprecise but it fixes the overflow
without potentially disrupting game code.
2024-05-19 19:02:32 +00:00

665 lines
14 KiB
C

// 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)<<FRACBITS) / PWRLVRECORD_MEDIAN;
for (j = 3; j < 10; j++) // Just skipping to 3 since 0 thru 2 will always just add 0...
{
fixed_t f = abs(x - ((j-4)<<FRACBITS));
fixed_t add;
if (f >= (2<<FRACBITS))
{
continue; //add = 0;
}
else if (f >= (1<<FRACBITS))
{
fixed_t f2 = (2<<FRACBITS) - f;
add = FixedMul(FixedMul(f2, f2), f2) / 6;
}
else
{
add = ((3*FixedMul(FixedMul(f, f), f)) - (6*FixedMul(f, f)) + (4<<FRACBITS)) / 6;
}
increment += (add * control[j]);
}
return (INT16)(increment >> 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<<FRACBITS)/10) ? KARTSPEED_HARD : KARTSPEED_NORMAL;
encore = P_RandomChance(PR_RULESCRAMBLE, FRACUNIT>>1);
break;
case 3:
speed = P_RandomChance(PR_RULESCRAMBLE, (3<<FRACBITS)/10) ? KARTSPEED_HARD : KARTSPEED_NORMAL;
encore = P_RandomChance(PR_RULESCRAMBLE, FRACUNIT>>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<<FRACBITS)/10) ? KARTSPEED_EASY : KARTSPEED_NORMAL;
encore = false;
break;
}
CONS_Debug(DBG_GAMELOGIC, "Rolled speed: %d\n", speed);
CONS_Debug(DBG_GAMELOGIC, "Rolled encore: %s\n", (encore ? "true" : "false"));
if (cv_kartspeed.value == KARTSPEED_AUTO)
speedscramble = speed;
else
speedscramble = -1;
if (cv_debugencorevote.value)
encorescramble = 1;
else if (cv_kartencore.value == -1)
encorescramble = (encore ? 1 : 0);
else
encorescramble = -1;
}
break;
default:
break;
}
}
void K_PlayerForfeit(UINT8 playerNum, boolean pointLoss)
{
UINT8 p = 0;
SINT8 powerType = PWRLV_DISABLED;
UINT16 yourPower = 0;
INT16 inc = 0;
UINT8 i;
// power level & spectating is netgames only
if (!netgame)
{
return;
}
// Hey, I just got here!
if (players[playerNum].jointime <= 1)
{
return;
}
// 20 sec into a match counts as a forfeit -- automatic loss against every other player in the match.
if (gamestate != GS_LEVEL || leveltime <= starttime+(20*TICRATE))
{
return;
}
spectateGriefed++;
// This server isn't using power levels, so don't mess with them.
if (!cv_kartusepwrlv.value)
{
return;
}
for (i = 0; i < MAXPLAYERS; i++)
{
if ((playeringame[i] && !players[i].spectator)
|| (i == playerNum))
{
p++;
}
}
if (p < 2) // no players
{
return;
}
powerType = K_UsingPowerLevels();
if (powerType == PWRLV_DISABLED) // No power type?!
{
return;
}
yourPower = clientpowerlevels[playerNum][powerType];
if (yourPower == 0) // splitscreen guests don't record power level changes
{
return;
}
K_UpdatePowerLevelsFinalize(&players[playerNum], true);
inc = K_FinalPowerIncrement(&players[playerNum], yourPower, clientPowerAdd[playerNum]);
if (inc == 0)
{
// No change
return;
}
if (pointLoss)
{
clientpowerlevels[playerNum][powerType] += clientPowerAdd[playerNum];
clientPowerAdd[playerNum] = 0;
SV_UpdateStats();
}
}