Merge branch 'duel-shuffle' into 'master'

Shuffle Loser

See merge request kart-krew-dev/ring-racers-internal!2278
This commit is contained in:
Oni VelocitOni 2025-04-09 03:34:12 +00:00
commit 82a542fe33
4 changed files with 185 additions and 42 deletions

View file

@ -137,7 +137,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
k_bot.cpp k_bot.cpp
k_botitem.cpp k_botitem.cpp
k_botsearch.cpp k_botsearch.cpp
k_grandprix.c k_grandprix.cpp
k_boss.c k_boss.c
k_hud.cpp k_hud.cpp
k_hud_track.cpp k_hud_track.cpp

View file

@ -739,6 +739,8 @@ void LiveStudioAudience_OnChange(void);
consvar_t cv_maxplayers = NetVar("maxplayers", "8").min_max(1, MAXPLAYERS); consvar_t cv_maxplayers = NetVar("maxplayers", "8").min_max(1, MAXPLAYERS);
consvar_t cv_shuffleloser = NetVar("shuffleloser", "On").on_off();
// Scoring type options // Scoring type options
consvar_t cv_overtime = UnsavedNetVar("overtime", "Yes").yes_no(); consvar_t cv_overtime = UnsavedNetVar("overtime", "Yes").yes_no();

View file

@ -64,7 +64,7 @@ extern consvar_t cv_mute;
extern consvar_t cv_voice_servermute; extern consvar_t cv_voice_servermute;
extern consvar_t cv_pause; extern consvar_t cv_pause;
extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers; extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers, cv_shuffleloser;
extern consvar_t cv_spectatorreentry, cv_duelspectatorreentry, cv_antigrief; extern consvar_t cv_spectatorreentry, cv_duelspectatorreentry, cv_antigrief;
// SRB2kart items // SRB2kart items

View file

@ -7,10 +7,14 @@
// terms of the GNU General Public License, version 2. // terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details. // See the 'LICENSE' file for more details.
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
/// \file k_grandprix.c /// \file k_grandprix.cpp
/// \brief Grand Prix mode game logic & bot behaviors /// \brief Grand Prix mode game logic & bot behaviors
#include "k_grandprix.h" #include "k_grandprix.h"
#include <algorithm>
#include <vector>
#include "k_specialstage.h" #include "k_specialstage.h"
#include "doomdef.h" #include "doomdef.h"
#include "d_player.h" #include "d_player.h"
@ -108,7 +112,7 @@ UINT8 K_GetGPPlayerCount(UINT8 humans)
// 2P -> 8 total // 2P -> 8 total
// 3P -> 12 total // 3P -> 12 total
// 4P -> 16 total // 4P -> 16 total
return max(min(humans * 4, MAXPLAYERS), 8); return std::clamp<UINT8>(humans * 4, 8, MAXPLAYERS);
} }
/*-------------------------------------------------- /*--------------------------------------------------
@ -160,22 +164,22 @@ void K_InitGrandPrixBots(void)
else else
{ {
// init difficulty levels list // init difficulty levels list
difficultylevels[0] = max(1, startingdifficulty); difficultylevels[ 0] = std::max<UINT8>(1, startingdifficulty);
difficultylevels[1] = max(1, startingdifficulty-1); difficultylevels[ 1] = std::max<UINT8>(1, startingdifficulty-1);
difficultylevels[2] = max(1, startingdifficulty-2); difficultylevels[ 2] = std::max<UINT8>(1, startingdifficulty-2);
difficultylevels[3] = max(1, startingdifficulty-3); difficultylevels[ 3] = std::max<UINT8>(1, startingdifficulty-3);
difficultylevels[4] = max(1, startingdifficulty-3); difficultylevels[ 4] = std::max<UINT8>(1, startingdifficulty-3);
difficultylevels[5] = max(1, startingdifficulty-4); difficultylevels[ 5] = std::max<UINT8>(1, startingdifficulty-4);
difficultylevels[6] = max(1, startingdifficulty-4); difficultylevels[ 6] = std::max<UINT8>(1, startingdifficulty-4);
difficultylevels[7] = max(1, startingdifficulty-4); difficultylevels[ 7] = std::max<UINT8>(1, startingdifficulty-4);
difficultylevels[8] = max(1, startingdifficulty-5); difficultylevels[ 8] = std::max<UINT8>(1, startingdifficulty-5);
difficultylevels[9] = max(1, startingdifficulty-5); difficultylevels[ 9] = std::max<UINT8>(1, startingdifficulty-5);
difficultylevels[10] = max(1, startingdifficulty-5); difficultylevels[10] = std::max<UINT8>(1, startingdifficulty-5);
difficultylevels[11] = max(1, startingdifficulty-6); difficultylevels[11] = std::max<UINT8>(1, startingdifficulty-6);
difficultylevels[12] = max(1, startingdifficulty-6); difficultylevels[12] = std::max<UINT8>(1, startingdifficulty-6);
difficultylevels[13] = max(1, startingdifficulty-7); difficultylevels[13] = std::max<UINT8>(1, startingdifficulty-7);
difficultylevels[14] = max(1, startingdifficulty-7); difficultylevels[14] = std::max<UINT8>(1, startingdifficulty-7);
difficultylevels[15] = max(1, startingdifficulty-8); difficultylevels[15] = std::max<UINT8>(1, startingdifficulty-8);
} }
for (i = 0; i < MAXPLAYERS; i++) for (i = 0; i < MAXPLAYERS; i++)
@ -661,6 +665,42 @@ void K_IncreaseBotDifficulty(player_t *bot)
bot->botvars.diffincrease = increase; bot->botvars.diffincrease = increase;
} }
static boolean CompareJoiners(player_t *a, player_t *b)
{
if (a->spectatorReentry != b->spectatorReentry)
{
// Push low re-entry cooldown to the back.
return (a->spectatorReentry > b->spectatorReentry);
}
if (a->spectatewait != b->spectatewait)
{
// Push high waiting time to the back.
return (a->spectatewait < b->spectatewait);
}
// They are equals, so just randomize
return (P_Random(PR_BOTS) & 1);
}
static boolean CompareReplacements(player_t *a, player_t *b)
{
if ((a->pflags & PF_NOCONTEST) != (b->pflags & PF_NOCONTEST))
{
// Push NO CONTEST to the back.
return ((a->pflags & PF_NOCONTEST) == 0);
}
if (a->position != b->position)
{
// Push bad position to the back.
return (a->position < b->position);
}
// They are equals, so just randomize
return (P_Random(PR_BOTS) & 1);
}
/*-------------------------------------------------- /*--------------------------------------------------
void K_RetireBots(void) void K_RetireBots(void)
@ -764,45 +804,146 @@ void K_RetireBots(void)
} }
} }
// Duel Shuffle:
// In games with limited player count, shuffle the NO CONTEST
// player with a spectator that wants to play. Intended for 1v1
// servers, but really this can help any server with a lower
// player count than connection count.
std::vector<player_t *> joining;
std::vector<player_t *> humans;
std::vector<player_t *> bots;
for (i = 0; i < MAXPLAYERS; i++) for (i = 0; i < MAXPLAYERS; i++)
{ {
player_t *bot = NULL; if (playeringame[i] == false)
if (!playeringame[i] || !players[i].bot || players[i].spectator)
{ {
continue; continue;
} }
bot = &players[i]; player_t *player = &players[i];
if (bot->pflags & PF_NOCONTEST) if (player->spectator == true)
{ {
UINT8 skinnum = defaultbotskin; if (player->bot == false && (player->pflags & PF_WANTSTOJOIN) == PF_WANTSTOJOIN)
if (usableskins > 0)
{ {
UINT8 index = P_RandomKey(PR_BOTS, usableskins); joining.push_back(player);
skinnum = grabskins[index]; }
grabskins[index] = grabskins[--usableskins]; }
else
{
if (player->bot == true)
{
bots.push_back(player);
}
else
{
humans.push_back(player);
}
}
}
const size_t max_lobby_size = static_cast<size_t>(cv_maxplayers.value);
size_t num_joining = joining.size();
size_t num_playing = humans.size() + bots.size();
std::stable_sort(joining.begin(), joining.end(), CompareJoiners);
std::stable_sort(humans.begin(), humans.end(), CompareReplacements);
std::stable_sort(bots.begin(), bots.end(), CompareReplacements);
boolean did_replacement = false;
if (G_GametypeHasSpectators() == true && grandprixinfo.gp == false && cv_shuffleloser.value != 0)
{
// While joiners and players still exist, insert joiners.
//UINT8 replacements = max_lobby_size / 2; // Only replace bottom half
UINT8 replacements = 1; // Only replace a single player.
while (replacements > 0)
{
if (joining.size() == 0 || (humans.size() + bots.size()) == 0)
{
// No one to replace or to join.
break;
} }
memcpy(&bot->availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8)); if (num_playing + num_joining <= max_lobby_size)
{
// We can fit everyone, so we don't need to do manual replacement.
break;
}
bot->botvars.difficulty = newDifficulty; player_t *joiner = joining.back();
bot->botvars.diffincrease = 0; joining.pop_back();
K_SetNameForBot(i, skins[skinnum].realname); player_t *replace = nullptr;
if (bots.size() > 0)
{
replace = bots.back();
bot->prefskin = skinnum; // A bot is taking up this slot? Remove it entirely.
bot->prefcolor = skins[skinnum].prefcolor; CL_RemovePlayer(replace - players, KR_LEAVE);
bot->preffollower = -1;
bot->preffollowercolor = SKINCOLOR_NONE;
G_UpdatePlayerPreferences(bot);
bot->score = 0; bots.pop_back();
bot->pflags &= ~PF_NOCONTEST; }
else
{
replace = humans.back();
// A human is taking up this slot? Spectate them.
replace->spectator = true;
replace->pflags |= PF_WANTSTOJOIN; // We were are spectator against our will, we want to play ASAP.
humans.pop_back();
}
// Add our waiting-to-play spectator to the game.
P_SpectatorJoinGame(joiner);
did_replacement = true;
replacements--;
num_joining--;
} }
} }
if (did_replacement == true)
{
// No need to run the bot swapping code,
// we already replaced the loser.
return;
}
// Replace last place bot.
if (bots.size() > 0)
{
player_t *bot = bots.back();
UINT8 skinnum = defaultbotskin;
if (usableskins > 0)
{
UINT8 index = P_RandomKey(PR_BOTS, usableskins);
skinnum = grabskins[index];
grabskins[index] = grabskins[--usableskins];
}
memcpy(&bot->availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8));
bot->botvars.difficulty = newDifficulty;
bot->botvars.diffincrease = 0;
K_SetNameForBot(bot - players, skins[skinnum].realname);
bot->prefskin = skinnum;
bot->prefcolor = skins[skinnum].prefcolor;
bot->preffollower = -1;
bot->preffollowercolor = SKINCOLOR_NONE;
G_UpdatePlayerPreferences(bot);
bot->score = 0;
bot->pflags &= ~PF_NOCONTEST;
}
} }
/*-------------------------------------------------- /*--------------------------------------------------