From aa0fa520353102582d779bb89f4e6f612e4cd7f5 Mon Sep 17 00:00:00 2001 From: drjaska Date: Sun, 8 May 2022 21:13:17 +0300 Subject: [PATCH] major update scores are cleared between rounds players are eliminated when a round ends match ends when there are less players than 2 as then there is a winner race conditions are stored in savestates, suiciding no longer removes checkpoint progress nor timer a lot of code comments for documentation --- gamemodes-server.cfg | 1 + .../gamemodes/gamemode/ctscup/sv_ctscup.qc | 276 ++++++++++++++---- qcsrc/server/savestate.qc | 34 +++ 3 files changed, 250 insertions(+), 61 deletions(-) diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index a2538bb9f..32171efda 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -339,6 +339,7 @@ set g_cts_removeprojectiles 0 "remove projectiles when the player dies, to preve // complete the stage cup // ======================== set g_ctscup 0 "CTSCup: complete the stage, tournament edition" +set g_ctscup_minplayers 3 "how many players are required to start a tournament" set g_ctscup_warmup 10 "time players get to run around and learn the map before the tournament starts" set g_ctscup_finishwait 10 "after first finish wait this time before disqualifying players who haven't finished set g_ctscup_maxroundlength 60 "max time for each round. adjust for map length. used to protect against multiple trolls holding spectators hostage forever if spectators can not vote" diff --git a/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc b/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc index 80808ea56..273f25271 100644 --- a/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc +++ b/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc @@ -1,12 +1,14 @@ #include "sv_ctscup.qh" #include -#include -#include #include #include +#include +#include +#include #include #include + #include MUTATOR_HOOKFUNCTION(ctscup, PlayerPhysics) @@ -250,44 +252,53 @@ MUTATOR_HOOKFUNCTION(ctscup, ForbidDropCurrentWeapon) return true; } +MUTATOR_HOOKFUNCTION(ctscup, ForbidPlayerScore_Clear) +{ + return true; // in CTS, you don't lose score by observing +} + // ============================================================================ // END OF PURE CTS, START OF SHARED FUNCTIONS -//============================================================================= +// ============================================================================ -int roundCounter; -int roundPlayers; -bool tournamentStarted; -float roundFirstFinisherTime; +int roundCounter; // unused? +int roundPlayers; // amount of players currently in the game, does not include spectators +int nextRoundPlayers; // how many players should participate in the next round +bool tournamentStarted; // has the warmup ended +float roundFirstFinisherTime; // time when the first finisher crossed the finish line -float autocvar_g_ctscup_warmup; //how long is the warmup round after loading into a map -float autocvar_g_ctscup_finishwait; //time before ending the round prematurely after first finish -float autocvar_g_ctscup_maxroundlength; //round length unless it ends prematurely +int autocvar_g_ctscup_minplayers; // how many players are required to start a tournament +float autocvar_g_ctscup_warmup; // how long is the warmup round after loading into a map +float autocvar_g_ctscup_finishwait; // time before ending the round prematurely after first finish +float autocvar_g_ctscup_maxroundlength; // round length unless it ends prematurely -.bool tournamentParticipant; +.bool tournamentParticipant; // is this player an active player? if not then they must be an eliminated player or a spectator MUTATOR_HOOKFUNCTION(ctscup, PlayerSpawn) { entity player = M_ARGV(0, entity); - //entity spawn_spot = M_ARGV(1, entity); - //player.race_place = 0; - player.tournamentParticipant = true; + if (!tournamentStarted || player.tournamentParticipant) + { + player.frags = FRAGS_PLAYER; + player.tournamentParticipant = true; + } // only spectators and connecting players do not have a savestate so here: // newly connected player has spawned for the first time // or during warmup a player who was spectating has joined (back) // and as they're now a participant they're given a savestate - if (player.savestate == NULL) + if (player.tournamentParticipant && player.savestate == NULL) { player.savestate = new_pure(savestate); SaveSaveState(player); } - //debug print + // debug print //print(sprintf("%f", time), " time \n"); //print(sprintf("%f", round_handler_GetEndTime()), " round_handler_GetEndTime() \n"); //print(sprintf("%f", (roundFirstFinisherTime + autocvar_g_ctscup_finishwait)), " (roundFirstFinisherTime + autocvar_g_ctscup_finishwait) \n\n"); @@ -295,34 +306,47 @@ MUTATOR_HOOKFUNCTION(ctscup, PlayerSpawn) // upon spawning for a new round, save a savestate instead of loading an old one // for whatever reason on the frame players are reset and spawned for a new round to start // round_handler_GetEndTime() still gives the end time for last round and time should be bigger than that - if ((time > round_handler_GetEndTime()) || (roundFirstFinisherTime && (time > (roundFirstFinisherTime + autocvar_g_ctscup_finishwait)))) - { - SaveSaveState(player); - } - // if somehow dying during a round and respawning, load last savestate - else + if (tournamentStarted && player.tournamentParticipant) { - LoadSaveState(player); + if ((time > round_handler_GetEndTime()) || (roundFirstFinisherTime && (time > (roundFirstFinisherTime + autocvar_g_ctscup_finishwait)))) + { + SaveSaveState(player); + } + // if somehow dying during a round and respawning, load last savestate + else + { + LoadSaveState(player); + } } } +// a player is made a spectator MUTATOR_HOOKFUNCTION(ctscup, MakePlayerObserver) { entity player = M_ARGV(0, entity); - player.frags = FRAGS_SPECTATOR; + if(tournamentStarted && player.frags != FRAGS_PLAYER_OUT_OF_GAME) + player.frags = FRAGS_SPECTATOR; + // race state reset race_PreparePlayer(player); player.race_checkpoint = -1; + // delete any savestate entities the player is associated with DeleteSaveState(player); + + // prevents resetting FRAGS_PLAYER_OUT_OF_GAME to FRAGS_SPECTATOR and + // setting team to Spectator in PutObserverInServer, this is already handled here + // it turns out this also makes it impossible to see which players are spectating... + //return true; } MUTATOR_HOOKFUNCTION(ctscup, Race_FinalCheckpoint) { //entity player = M_ARGV(0, entity); - // useful to prevent cheating by running back to the start line and starting out with more speed + // CTS comment: useful to prevent cheating by running back to the start line and starting out with more speed + // I don't think we want to respawn players who finish in CTS Cup? commented out next 2 lines for now //if(autocvar_g_cts_finish_kill_delay) // ClientKill_Silent(player, autocvar_g_cts_finish_kill_delay); @@ -337,16 +361,18 @@ MUTATOR_HOOKFUNCTION(ctscup, Damage_Calculate) float frag_damage = M_ARGV(4, float); vector frag_force = M_ARGV(7, vector); + // do nothing if a non-player is dealt damage + if (!IS_PLAYER(frag_target))return; + + // when selfdamage is disabled and player hurts themselves or receives splat damage nullify that damage if((frag_target == frag_attacker || frag_deathtype == DEATH_FALL.m_id) && !autocvar_g_cts_selfdamage) { frag_damage = 0; - frag_force = '0 0 0'; M_ARGV(4, float) = frag_damage; - M_ARGV(6, vector) = frag_force; } + // if the player were to be about to die, try to save them and restore a loadstate if possible - else if (IS_PLAYER(frag_target)) - if (((GetResource(frag_target, RES_HEALTH) + GetResource(frag_target, RES_ARMOR)) - frag_damage) <= 0) + else if (((GetResource(frag_target, RES_HEALTH) + GetResource(frag_target, RES_ARMOR)) - frag_damage) <= 0) if (LoadSaveState(frag_target)) { // if savestate loading was successful @@ -364,36 +390,55 @@ MUTATOR_HOOKFUNCTION(ctscup, Damage_Calculate) // END OF SHARED FUNCTIONS, START OF CTS CUP //============================================================================= - - +// count current players who are not in spectator int CTSCUP_AliveParticipants() { roundPlayers = 0; - FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_SPECTATOR, + FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, { roundPlayers++; }); + + // if there are 1 or less players in an active tournament, end the tournament as it has finished + if (tournamentStarted && (roundPlayers <= 1)) + cvar_set("_endmatch", "1"); + return roundPlayers; } +// return true if we have any active players +// increase required amount in the future when this is not as WIP bool CTSCUP_CanRoundStart() { CTSCUP_AliveParticipants(); - return boolean(roundPlayers); + + // tournament started and there are 2 or more players + if (tournamentStarted && (roundPlayers >= 2)) + return true; + // tournament has yet to start but there are enough players to start the warmup timer + if (!tournamentStarted && (roundPlayers >= autocvar_g_ctscup_minplayers)) + return true; + + return false; } +// this is called when a round starts void CTSCUP_RoundStart() { + // reset this only after a round starts so it can be used to determinate current round + // state when spawning players (after a new round starts when last round ended prematurely + // due to someone finishing and g_ctscup_finishwait time ending the current round before + // max roundtimelimit would end it) roundFirstFinisherTime = 0; - CTSCUP_AliveParticipants(); + CTSCUP_AliveParticipants(); //count players, not including spectators + nextRoundPlayers = floor(roundPlayers * 0.9); // up to 90% of those players are allowed into the next round } bool CTSCUP_CheckRoundEnd() { - //tournament end conditions - - if(roundFirstFinisherTime) //check if someone has finished + if(roundFirstFinisherTime) // check if someone has finished + // if g_ctscup_finishwait has passed since someone finished end the current round if(time>=(roundFirstFinisherTime + autocvar_g_ctscup_finishwait)) { game_stopped = true; @@ -401,19 +446,118 @@ bool CTSCUP_CheckRoundEnd() return true; } + // max timelimit for current round if(time > round_handler_GetEndTime()) { game_stopped = true; round_handler_Init(5, 1, autocvar_g_ctscup_maxroundlength); return true; } + + // 1 player or less + // optimize and move this to player changing teams and disconnecting? + CTSCUP_AliveParticipants(); + return false; } +// When a tournament round ends find all the slowest players and +// eliminate until we have the desired amount of players left +void CTSCUP_EliminatePlayers() +{ + // 255 is engine limit on maxclients, 256 players + // this global array could be smaller as it's very unlikely that 255 players would ever play on a server + // but array lengths have to be constants in QC which makes it very iffy + // To optimize and prevent this many players from playing at once or not to + entity sortRoundParticipants[255]; + + CTSCUP_AliveParticipants(); // count players, not including spectators + int unsortedPlayers = roundPlayers; // how many players there still is left to sort into the array + + // go through all entities which are clients, find players and store them in a new array + // so we don't need to go through the whole entity list many, many times + FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, + { + // when finding a player find their right spot + for (int k = 0 ; k < roundPlayers ; k++) + { + // if this index is empty, store them there and go find the next player + if (sortRoundParticipants[k] == NULL) + { + sortRoundParticipants[k] = it; + unsortedPlayers--; + break; + } + else // this index is not empty + { + // those who didn't finish, place them right at the end of the array + + // the player we found did not finish, place them to the end of the array + if (PlayerScore_Get(it, SP_RACE_FASTEST) == 0) + { + sortRoundParticipants[roundPlayers - unsortedPlayers] = it; + unsortedPlayers--; + break; + } + + // if this spot has a player who didn't finish push the old entries 1 further and place our new player in this spot + if (PlayerScore_Get(sortRoundParticipants[k], SP_RACE_FASTEST) == 0) + { + for (int j = (roundPlayers - unsortedPlayers) ; j >= k ; j--) + { + // j-1 is fine because this can not be reached with 0 players sorted + // reaching this with 1 players sorted would move index 0 to index 1 which is fine + sortRoundParticipants[j] = sortRoundParticipants[j-1]; + } + sortRoundParticipants[k] = it; + unsortedPlayers--; + break; + } + + // those players who did not finish have a time of 0 + // those players who finished have a score which is LOWER the better they did + // this makes it so that comparing size only would place those who didn't finish as the fastest players + // thus previous 2 ifs are necessary + + // after this only those who have a clear time are handled + + // if new player is faster than current index push old entries 1 further and place our new player in this spot + if (PlayerScore_Get(it, SP_RACE_FASTEST) < PlayerScore_Get(sortRoundParticipants[k], SP_RACE_FASTEST)) + { + for (int j = (roundPlayers - unsortedPlayers) ; j >= k ; j--) + { + // j-1 is fine because this can not be reached with 0 players sorted + // reaching this with 1 players sorted would move index 0 to index 1 which is fine + sortRoundParticipants[j] = sortRoundParticipants[j-1]; + } + sortRoundParticipants[k] = it; + unsortedPlayers--; + break; + } + } + } + }); + + // Move players to spectator until we only have the allowed amount of players left + // int playersToEliminate = (roundPlayers - nextRoundPlayers); + for (int i = 0 ; i < (roundPlayers - nextRoundPlayers) ; i++) + { + // - 1 is index offset, if we have 2 players the 2nd player is in index 1 + //int indexOfEliminatedPlayer = ((roundPlayers - i) - 1) + sortRoundParticipants[((roundPlayers - i) - 1)].tournamentParticipant = false; + sortRoundParticipants[((roundPlayers - i) - 1)].frags = FRAGS_PLAYER_OUT_OF_GAME; + TRANSMUTE(Observer, sortRoundParticipants[((roundPlayers - i) - 1)]); + } + + return; +} + +// this is called when fake warmup ends void CTSCUP_TournamentStart() { - if(tournamentStarted)return; + if(tournamentStarted)return; // double calls shouldn't ever happen but handle those just in case + // = 0 is for initialization which is for whatever reason required by compiler or it warns float autocvar_g_start_delay = 0; if (time >= (autocvar_g_start_delay + autocvar_g_ctscup_warmup)) { @@ -421,7 +565,10 @@ void CTSCUP_TournamentStart() tournamentStarted = true; - FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_SPECTATOR, + print("Tournament started! GLHF! \n"); + + // register every tournament participant here + FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, { it.tournamentParticipant = true; }); @@ -429,51 +576,52 @@ void CTSCUP_TournamentStart() return; } - // case: if all players F4? + // implement feature: skip warmup if all players ready up? } +// upon map reset +// if tournament has started eliminate player(s) and increment round counter +// if tournament has not started start the tournament +// always clear scores and reset and restart players MUTATOR_HOOKFUNCTION(ctscup, reset_map_players) { - // roundFirstFinisherTime = 0; + if (tournamentStarted) + { + roundCounter++; - if (tournamentStarted) roundCounter++; - else CTSCUP_TournamentStart(); + CTSCUP_EliminatePlayers(); + } + else + CTSCUP_TournamentStart(); - //foreach DeleteSaveState + Score_ClearAll(); FOREACH_CLIENT(true, { if (it.tournamentParticipant) { TRANSMUTE(Player, it); } - else - { - TRANSMUTE(Observer, it); - it.frags = FRAGS_SPECTATOR; - } PutClientInServer(it); }); return true; } -//MUTATOR_HOOKFUNCTION(ctscup, reset_map_global) in CTS code +//MUTATOR_HOOKFUNCTION(ctscup, reset_map_global) in CTS code, unchanged for now -// if player trying to spawn is not a valid tournament participant -// don't allow spawning but move them to spectator +// if player trying to spawn is not a valid tournament participant don't allow spawning MUTATOR_HOOKFUNCTION(ctscup, ForbidSpawn) { entity player = M_ARGV(0, entity); - bool canSpawn = false; + + bool blockSpawning = false; + if (tournamentStarted && !player.tournamentParticipant) - { - TRANSMUTE(Observer, player); - player.frags = FRAGS_SPECTATOR; - canSpawn = true; - } + blockSpawning = true; - return canSpawn; + return blockSpawning; } +// when a player touches any checkpoint update their savestate MUTATOR_HOOKFUNCTION(ctscup, Race_Checkpoint) { entity player = M_ARGV(0, entity); @@ -481,17 +629,23 @@ MUTATOR_HOOKFUNCTION(ctscup, Race_Checkpoint) SaveSaveState(player); } +// when a player changes their own team to spectator MUTATOR_HOOKFUNCTION(ctscup, ClientCommand_Spectate) { entity player = M_ARGV(0, entity); - if(tournamentStarted) + if(tournamentStarted && (player.frags == FRAGS_PLAYER_OUT_OF_GAME || player.frags == FRAGS_PLAYER)) + { player.tournamentParticipant = false; + player.frags = FRAGS_PLAYER_OUT_OF_GAME; + } + else player.frags = FRAGS_SPECTATOR; if (INGAME(player)) { + // CA relic in comments, should probably be refactored to announce a player forfeit // they're going to spec, we can do other checks - if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player))) + //if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player))) // Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_LEAVE); return MUT_SPECCMD_FORCE; } diff --git a/qcsrc/server/savestate.qc b/qcsrc/server/savestate.qc index ef08083bb..1561bcd03 100644 --- a/qcsrc/server/savestate.qc +++ b/qcsrc/server/savestate.qc @@ -4,6 +4,25 @@ void SaveSaveState(entity player) { if (player.savestate) { + if (g_race || g_cts || g_ctscup) + { + // store fields reset by race_PreparePlayer + player.savestate.race_place = player.race_place; + player.savestate.race_started = player.race_started; + player.savestate.race_respawn_checkpoint = player.race_respawn_checkpoint; + player.savestate.race_respawn_spotref = player.race_respawn_spotref; + + // store fields reset by race_ClearTime + player.savestate.race_checkpoint = player.race_checkpoint; + player.savestate.race_laptime = player.race_laptime; + player.savestate.race_movetime = player.race_movetime; + player.savestate.race_movetime_frac = player.race_movetime_frac; + player.savestate.race_movetime_count = player.race_movetime_count; + player.savestate.race_penalty_accumulator = player.race_penalty_accumulator; + player.savestate.race_lastpenalty = player.race_lastpenalty; + } + + // fields stored by personal checkpoint, copy them to this implementation player.savestate.origin = player.origin; player.savestate.v_angle = player.v_angle; player.savestate.angles = player.angles; @@ -31,6 +50,21 @@ bool LoadSaveState(entity player) { if (player.savestate) { + if (g_race || g_cts || g_ctscup) + { + player.race_place = player.savestate.race_place; + player.race_started = player.savestate.race_started; + player.race_respawn_checkpoint = player.savestate.race_respawn_checkpoint; + player.race_respawn_spotref = player.savestate.race_respawn_spotref; + player.race_checkpoint = player.savestate.race_checkpoint; + player.race_laptime = player.savestate.race_laptime; + player.race_movetime = player.savestate.race_movetime; + player.race_movetime_frac = player.savestate.race_movetime_frac; + player.race_movetime_count = player.savestate.race_movetime_count; + player.race_penalty_accumulator = player.savestate.race_penalty_accumulator; + player.race_lastpenalty = player.savestate.race_lastpenalty; + } + player.origin = player.savestate.origin; player.v_angle = player.savestate.v_angle; player.angles = player.savestate.angles; -- 2.39.2