From 9bdea0f4a035b3212de1f334f8243009ed5a816b Mon Sep 17 00:00:00 2001 From: drjaska Date: Thu, 12 May 2022 17:59:22 +0300 Subject: [PATCH] major bugfix patch spectators which joined warmup but returned to spec no longer are forced into the tournament at the start round ends when everyone has reached the finish line if it happens before timelimits bots no longer forcejoin and can be kept in spectator spectators are no longer tried to be eliminated savestate entities are deleted for disconnecting players added chat prints for warmup (re)start warmup now restarts if there are not enough players when it tries to end checkpoints hit by corpses no longer save savestates savestate reloading no longer reloads the same exact time which player had when saving if a savestate is loaded 5 times without saving any the next load will fail and player is reset players who have finished can no longer interact with checkpoints and more fixes I forgot --- .../common/gamemodes/gamemode/ctscup/TODO.txt | 12 +- .../gamemodes/gamemode/ctscup/sv_ctscup.qc | 531 ++++++++++++------ qcsrc/server/savestate.qc | 39 +- qcsrc/server/savestate.qh | 2 + 4 files changed, 363 insertions(+), 221 deletions(-) diff --git a/qcsrc/common/gamemodes/gamemode/ctscup/TODO.txt b/qcsrc/common/gamemodes/gamemode/ctscup/TODO.txt index 898d1db1a..530826d24 100644 --- a/qcsrc/common/gamemodes/gamemode/ctscup/TODO.txt +++ b/qcsrc/common/gamemodes/gamemode/ctscup/TODO.txt @@ -1,15 +1,7 @@ CTS Cup TODO list: -Round handler: -- End the match when there are no more participants playing -- Move players to spectator after each tournament round -- Clear times upon round reset - Savestates: -- Softlock prevention -- FIXME?: Splat damage is dealt AFTER health restoring of savestate loading if the player were to smash into something and die from another source during I think the same frame -- I hope that loading a savestate from Damage_Calculate hook when player is about to die and setting incoming -damage to 0 can still save them and dying (for example by suicide bind) isn't a hardcoded death :) +- FIXME?: Splat damage is dealt AFTER health restoring of savestate loading if the player were to first save a savestate with low speed, get to high speeds and then load the savestate HUD: - Display current round's players @@ -19,7 +11,7 @@ HUD: - Medals for placements? Info messages: -- tournament in progress +- can not join, tournament in progress - you survived the round and will continue to the next one - you lost the round and got eliminated from the tournament - upon elimination show placement diff --git a/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc b/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc index 774fbf87f..dde31189c 100644 --- a/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc +++ b/qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc @@ -130,22 +130,6 @@ MUTATOR_HOOKFUNCTION(ctscup, AbortSpeedrun) race_PreparePlayer(player); // nice try } -MUTATOR_HOOKFUNCTION(ctscup, PutClientInServer) -{ - entity player = M_ARGV(0, entity); - - if(IS_PLAYER(player)) - if(!game_stopped) - { - if(CS(player).killcount == FRAGS_SPECTATOR /* initial spawn */ || g_race_qualifying) // spawn - race_PreparePlayer(player); - else // respawn - race_RetractPlayer(player); - - race_AbandonRaceCheck(player); - } -} - MUTATOR_HOOKFUNCTION(ctscup, PlayerDamaged) { int frag_deathtype = M_ARGV(5, int); @@ -269,19 +253,55 @@ 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 +// are we currently in a round? +// checked by turning this to false on round end and true when first player +// touches a checkpoint (a start trigger) +bool roundStarted; float roundFirstFinisherTime; // time when the first finisher crossed the finish line +int roundFinisherCount; // how many players have reached the finish line 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; // is this player an active player? if not then they must be an eliminated player or a spectator +// is this player an active player? +// if not then they must be an eliminated player or a spectator +.bool tournamentParticipant; + +// when a player or a bot connects, initialize their race values +// if it's a bot which isn't a tournament participant and the tournament has started +// override their forced join and keep them in spectator +MUTATOR_HOOKFUNCTION(ctscup, PutClientInServer) +{ + entity player = M_ARGV(0, entity); + + if(IS_PLAYER(player)) + { + if(!game_stopped) + { + if(CS(player).killcount == FRAGS_SPECTATOR /* initial spawn */ ) + race_PreparePlayer(player); + else // respawn + // only reset player race state during warmup when respawning + if (!tournamentStarted) + race_RetractPlayer(player); + + race_AbandonRaceCheck(player); + } + + // override bot force join in PutClientInServer() + if (IS_BOT_CLIENT(player) && tournamentStarted && !player.tournamentParticipant) + TRANSMUTE(Observer, player); + } +} MUTATOR_HOOKFUNCTION(ctscup, PlayerSpawn) { entity player = M_ARGV(0, entity); + // if tournament has not started register the player for the tournament + // if they're a tournament player respawning make sure they still are in if (!tournamentStarted || player.tournamentParticipant) { player.frags = FRAGS_PLAYER; @@ -298,24 +318,23 @@ MUTATOR_HOOKFUNCTION(ctscup, PlayerSpawn) SaveSaveState(player); } - // 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"); - // 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 (tournamentStarted && player.tournamentParticipant) { - 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 + // if somehow dying during a round and respawning, try toload last savestate + if (roundStarted && player.savestate.loadCounter < 5) + LoadSaveState(player); else { - LoadSaveState(player); + // did loading fail due to exceeding load counter + if (player.savestate.loadCounter >= 5) + // frontend notification + + // anyways as we couldn't load a savestate, save one here + race_PreparePlayer(player); + SaveSaveState(player); } } } @@ -325,12 +344,17 @@ MUTATOR_HOOKFUNCTION(ctscup, MakePlayerObserver) { entity player = M_ARGV(0, entity); - if(tournamentStarted && player.frags != FRAGS_PLAYER_OUT_OF_GAME) + // if the player wasn't an eliminated player + // give them frags which correspond to being a spec + if(player.frags != FRAGS_PLAYER_OUT_OF_GAME) player.frags = FRAGS_SPECTATOR; + // they're no longer a tournament participant + // if they were they've now forfeit + player.tournamentParticipant = false; + // race state reset race_PreparePlayer(player); - player.race_checkpoint = -1; // delete any savestate entities the player is associated with DeleteSaveState(player); @@ -339,18 +363,32 @@ MUTATOR_HOOKFUNCTION(ctscup, MakePlayerObserver) // 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; + return false; } MUTATOR_HOOKFUNCTION(ctscup, Race_FinalCheckpoint) { - //entity player = M_ARGV(0, entity); + entity player = M_ARGV(0, entity); // 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); - if (roundFirstFinisherTime == 0 && tournamentStarted) roundFirstFinisherTime = time; + if (tournamentStarted) + { + roundFinisherCount++; + + // if there are no round finish times, log this one as the first one + if (roundFirstFinisherTime == 0) + roundFirstFinisherTime = time; + } + + // softlock player's race checkpoint progress + // until next round, prevents 2nd laps and + // registering the better time out of those 2 + // while waiting for slow players + player.race_checkpoint = -3; } MUTATOR_HOOKFUNCTION(ctscup, Damage_Calculate) @@ -406,6 +444,33 @@ int CTSCUP_AliveParticipants() return roundPlayers; } +// this is called when fake warmup ends +void CTSCUP_TournamentStart() +{ + if(tournamentStarted)return; // double calls shouldn't ever happen but handle those just in case + + tournamentStarted = true; + + // tournament started, make timelimit infinite to make sure tournament can go its full length + // tournaments can not go on forever as at least 1 player has to get eliminated each round, + // no one can join mid-game and there is a max round length + // admins can override this timelimit as it's only set once at the start if they so please + cvar_set("timelimit", "0"); + + PrintToChatAll(" \n"); + PrintToChatAll("^1Tournament started! ^2GLHF! \n"); + PrintToChatAll(" \n"); + + // register every tournament participant here + FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, + { + it.tournamentParticipant = true; + }); + + return; + +} + // return true if we have any active players // increase required amount in the future when this is not as WIP bool CTSCUP_CanRoundStart() @@ -415,9 +480,15 @@ bool CTSCUP_CanRoundStart() // 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)) + { + PrintToChatAll(" \n"); + PrintToChatAll("^3W A R M U P started! \n"); + PrintToChatAll(" \n"); return true; + } return false; } @@ -430,6 +501,7 @@ void CTSCUP_RoundStart() // due to someone finishing and g_ctscup_finishwait time ending the current round before // max roundtimelimit would end it) roundFirstFinisherTime = 0; + roundFinisherCount = 0; CTSCUP_AliveParticipants(); //count players, not including spectators nextRoundPlayers = floor(roundPlayers * 0.9); // up to 90% of those players are allowed into the next round @@ -437,204 +509,265 @@ void CTSCUP_RoundStart() bool CTSCUP_CheckRoundEnd() { - 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)) + if (roundFirstFinisherTime) // check if someone has finished in active tournament + // if g_ctscup_finishwait has passed since someone finished, end the current round + if (time>=(roundFirstFinisherTime + autocvar_g_ctscup_finishwait)) { game_stopped = true; round_handler_Init(5, 1, autocvar_g_ctscup_maxroundlength); return true; } + // 1 player or less left playing in tournament + CTSCUP_AliveParticipants(); + // max timelimit for current round - if(time > round_handler_GetEndTime()) + if (time > round_handler_GetEndTime()) + { + // start the tournament if enough players are present as warmup ended + if (!tournamentStarted && (roundPlayers >= autocvar_g_ctscup_minplayers)) + { + roundCounter = -1; + game_stopped = true; + round_handler_Init(5, 1, autocvar_g_ctscup_maxroundlength); + return true; + } + + // not enough players present, restart warmup + if (!tournamentStarted && (roundPlayers < autocvar_g_ctscup_minplayers)) + { + PrintToChatAll(" \n"); + PrintToChatAll("^3Not enough players! W A R M U P restarted! \n"); + PrintToChatAll(" \n"); + game_stopped = true; + round_handler_Init(0, 0.5, autocvar_g_ctscup_warmup); + return true; + } + + // to reach here the tournament has started and current round time has ended + // just restart another round + else + { + game_stopped = true; + round_handler_Init(5, 1, autocvar_g_ctscup_maxroundlength); + return true; + } + } + + // when all players playing the round have a clear time + if (tournamentStarted && (roundPlayers == roundFinisherCount)) { 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 -// implementation #1 -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, + // if tournament has yet to start + if (!tournamentStarted) { - // when finding a player find their right spot - for (int k = 0 ; k < roundPlayers ; k++) + // if there are enough players to start a tournament + if (roundPlayers >= autocvar_g_ctscup_minplayers) { - // 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 + // if all players have a clear time start the tournament + bool everyoneHasAClearTime = true; - // the player we found did not finish, place them to the end of the array + FOREACH_CLIENT(IS_PLAYER(it) && !IS_BOT_CLIENT(it) && it.frags == FRAGS_PLAYER && it.tournamentParticipant == true, + { if (PlayerScore_Get(it, SP_RACE_FASTEST) == 0) { - sortRoundParticipants[roundPlayers - unsortedPlayers] = it; - unsortedPlayers--; - break; + // found someone who is playing warmup and is without a clear time + everyoneHasAClearTime = false; } + }); - // 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; - } + if (everyoneHasAClearTime) + { + PrintToChatAll(" \n"); + PrintToChatAll("^3Everyone has a clear time! Ending warmup and starting the tournament! \n"); + //PrintToChatAll(" \n"); + CTSCUP_TournamentStart(); + game_stopped = true; + round_handler_Init(5, 1, autocvar_g_ctscup_maxroundlength); + return true; } } - }); - - // move players to spectator until we only have the allowed amount of players left - int amountOfPlayersToEliminate = (roundPlayers - nextRoundPlayers); - - for (int i = amountOfPlayersToEliminate ; i > 0 ; i--) - { - // - 1 is index offset, if we have 2 players the 2nd player is in index 1 - //int indexOfEliminatedPlayer = ((roundPlayers - i) - 1) - sortRoundParticipants[indexOfEliminatedPlayer].tournamentParticipant = false; - sortRoundParticipants[indexOfEliminatedPlayer].frags = FRAGS_PLAYER_OUT_OF_GAME; - TRANSMUTE(Observer, sortRoundParticipants[indexOfEliminatedPlayer]); } - return; + // condition recap: + // round continues if: + // early finish timer triggered by first finisher hasn't expired, + // 2 or more players, + // round's timelimit is not over, + // and not everyone playing has a clear time + return false; } -*/ // When a tournament round ends find all the slowest players and // eliminate until we have the desired amount of players left -// implementation #2 -// this implementation does not store a global array which has constant size and is useless 99.9999% of the server ticks -// it would be more optimal to use a linked list which is round or looping, if starting at the fastest go -// backwards one step to find slowest player and then the 2nd slowest etc. until all eliminations are done +// implementation #3 +// improvements from #2: +// spectators with scorekeeper entities are no longer kept in the linked list +// non-finishers are eliminated at random instead of connection order .entity chain; void CTSCUP_EliminatePlayers() { - // as we have a linked list and 3 or more eliminations(most likely meaning >20 players if 10% of them are eliminated) - // it would be the most efficient to go backwards in the linked list to find the wanted players - // fastest -> slowest -> 2nd slowest -> 3rd slowest etc. - // but current implementation only supports going forwards so currently following is done - // fastest -> 2nd fastest -> 3rd fastest -> ... -> 1st player to get eliminated -> 2nd player to get eliminated etc. - // until all eliminations are done. Difference between forwards and backwards is neglible for modern CPUs - - // following loops will eliminate players like so with 9 players and 3 eliminations: - // 1st(fastest) 2nd 3rd 4th 5th 6th 7th 8th 9th(slowest) - // 1st(fastest) 2nd 3rd 4th 5th 6th elim 8th 9th(slowest) - // 1st(fastest) 2nd 3rd 4th 5th 6th elim elim 9th(slowest) - // 1st(fastest) 2nd 3rd 4th 5th 6th elim elim elim - + // sort scorekeeper entities into a forwards non-round linked list entity fastestPlayer = PlayerScore_Sort(scoreboard_pos, 0, true, false); entity index = fastestPlayer; + // abort, oh no how are we here + // 2 last players on the server disconnected at the same time? + // unknown if actually possible + if (fastestPlayer == NULL)return; + CTSCUP_AliveParticipants(); // count players, not including spectators int amountOfPlayersToEliminate = (roundPlayers - nextRoundPlayers); - // find and move players to spectator until we only have the allowed amount of players left - for (int i = 0 ; i < amountOfPlayersToEliminate ; i++) + // rebuild the linked list as two new linked lists which do not have any spectators in them + // one list for those who finished and one for those who did not + + entity lastFinishingPlayer = NULL, lastNonFinishingPlayer = NULL; // pointer storage entities + entity backwardsLinkedListHelperPointer = NULL; + entity finishers = NULL, nonFinishers = NULL; // linked lists' first entities + int finishersCheckerValue = 0; // this should always match roundFinisherCount + int nonFinishersCheckerValue = 0; // this should always match (roundPlayers - roundFinisherCount) + + // while old linked list has entries in it + while (index) { - if ( i == 0) + // is our current list entry a tournament participant? + if (index.tournamentParticipant) { - // find the fastest player about to get eliminated - for (int j = 0 ; j < (roundPlayers - amountOfPlayersToEliminate) ; j++) + // do they have a finish time? + if (PlayerScore_Get(index, SP_RACE_FASTEST) == 0) { + // we found a non-finisher + nonFinishersCheckerValue++; + + // forwards linked list of non-finishers in connection + // order excluding reused disconnected player slots + if (nonFinishers) + { + // we found a +1 non-finisher + lastNonFinishingPlayer.chain = index; + + // update our "last in the list" to be the latest entry + lastNonFinishingPlayer = lastNonFinishingPlayer.chain; + } + else + { + // we found our first non-finisher + lastNonFinishingPlayer = index; + + // initialize our linked list + nonFinishers = lastNonFinishingPlayer; + } + + // move the index over to the next list entry index = index.chain; } + else + { + // we found a finisher + finishersCheckerValue++; + + // backwards linked list + // 1st -> 5th -> 4th -> 3rd -> 2nd -> 1st + if (finishers) + { + // we found a +1 finisher + backwardsLinkedListHelperPointer = index; + + // move the index over to the next list entry + index = index.chain; + + // reverse chain order + backwardsLinkedListHelperPointer.chain = lastFinishingPlayer; + + // update our "laTEst in the list" to be the latest entry + lastFinishingPlayer = backwardsLinkedListHelperPointer; + } + else + { + // we found our first finisher + lastFinishingPlayer = index; + + // initialize our linked list + finishers = lastFinishingPlayer; + + // move the index over to the next list entry + index = index.chain; + } + } } else { - // only bump +1 towards the tail as we've found the fastest player about to get eliminated previously - // +1 bumps index to one player slower + // found a spectator or etc., skip over them index = index.chain; } - - index.tournamentParticipant = false; - index.frags = FRAGS_PLAYER_OUT_OF_GAME; - //PutObserverInServer(index, true, true); - TRANSMUTE(Observer, index); } -} -// this is called when fake warmup ends -void CTSCUP_TournamentStart() -{ - if(tournamentStarted)return; // double calls shouldn't ever happen but handle those just in case + // make the linked lists round + if (finishers) finishers.chain = lastFinishingPlayer; + + if (nonFinishers) lastNonFinishingPlayer.chain = nonFinishers; + + if (finishersCheckerValue != roundFinisherCount && tournamentStarted) + print("^1 ", sprintf("%f", finishersCheckerValue), " finishersCheckerValue != ", sprintf("%f", roundFinisherCount), " roundFinisherCount! \n"); + if (nonFinishersCheckerValue != (roundPlayers - roundFinisherCount) && tournamentStarted) + print("^1 ", sprintf("%f", nonFinishersCheckerValue), " nonFinishersCheckerValue != ", sprintf("%f", (roundPlayers - roundFinisherCount)), " (roundPlayers - roundFinisherCount)! \n"); + + // find and move players to spectator until we only have the allowed amount of players left - // = 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)) + if (amountOfPlayersToEliminate > 0 && (finishers || nonFinishers)) { - roundCounter = 0; + // eliminate the non-finishers at random while they last + index = nonFinishers; + while (nonFinishers && amountOfPlayersToEliminate > 0) + { + // if we have more than 1 player who didn't finish + if (nonFinishersCheckerValue > 1) + { + // randomize who out of them gets eliminated + for (int i = 0; i < ((random() * roundPlayers)); i++) + { + index = index.chain; + } + } - tournamentStarted = true; + // elimination + index.chain.tournamentParticipant = false; + index.chain.frags = FRAGS_PLAYER_OUT_OF_GAME; + TRANSMUTE(Observer, index.chain); - PrintToChatAll("^1Tournament started! ^2GLHF! \n"); + // repair the linked list so it can be looped again in case we + // would ever need to eliminate 2 or more random players + index.chain = index.chain.chain; + amountOfPlayersToEliminate--; + } - // register every tournament participant here - FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, + // if we've ran out of non-finishers move to slowest finishing player + // do not bother to check if they have the same time at decimal level + // as it's far too unlikely to happen to matter + + index = finishers.chain; // start with the slowest player + // finishers is fastest -> .chain slowest -> .chain.chain 2nd slowest etc. + while (finishers && amountOfPlayersToEliminate > 0) { - it.tournamentParticipant = true; - }); + // elimination + index.tournamentParticipant = false; + index.frags = FRAGS_PLAYER_OUT_OF_GAME; + TRANSMUTE(Observer, index); - return; - } + index = index.chain; + amountOfPlayersToEliminate--; + } - // implement feature: skip warmup if all players ready up? + if (amountOfPlayersToEliminate) + print("^1TRIED TO ELIMINATE ", + sprintf("%f", amountOfPlayersToEliminate), + " MORE PLAYERS THAN IN LINKED LISTS \n"); + } } // upon map reset @@ -643,22 +776,36 @@ void CTSCUP_TournamentStart() // always clear scores and reset and restart players MUTATOR_HOOKFUNCTION(ctscup, reset_map_players) { - if (tournamentStarted) + // reset rounds after warmup + if (tournamentStarted && roundCounter >= 0) { roundCounter++; CTSCUP_EliminatePlayers(); + + Score_ClearAll(); } - else - CTSCUP_TournamentStart(); - Score_ClearAll(); + // mark round as not started as it's being reset + roundStarted = false; + // reset warmup ending round + if (roundCounter == -1) + { + CTSCUP_TournamentStart(); + roundCounter++; + } + + // respawn players FOREACH_CLIENT(true, { if (it.tournamentParticipant) { TRANSMUTE(Player, it); } + else + { + TRANSMUTE(Observer, it); + } PutClientInServer(it); }); return true; @@ -679,12 +826,31 @@ MUTATOR_HOOKFUNCTION(ctscup, ForbidSpawn) return blockSpawning; } +// someone disconnected +MUTATOR_HOOKFUNCTION(ctscup, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + // if the player disconnecting was a player who had finished + // redact one from the amount players waiting at the finish line + if (player.tournamentParticipant && PlayerScore_Get(player, SP_RACE_FASTEST)) + roundFinisherCount--; + + // delete possible savestate entities they may have had + DeleteSaveState(player); +} + // when a player touches any checkpoint update their savestate MUTATOR_HOOKFUNCTION(ctscup, Race_Checkpoint) { entity player = M_ARGV(0, entity); - SaveSaveState(player); + // mark round as started + roundStarted = true; + + // do not register savestates with corpses that hit checkpoints + if (tournamentStarted && (GetResource(player, RES_HEALTH) >= 1)) + SaveSaveState(player); } // when a player changes their own team to spectator @@ -692,7 +858,12 @@ MUTATOR_HOOKFUNCTION(ctscup, ClientCommand_Spectate) { entity player = M_ARGV(0, entity); - if(tournamentStarted && (player.frags == FRAGS_PLAYER_OUT_OF_GAME || player.frags == FRAGS_PLAYER)) + // if the player moving to spectator was a player who had finished + // redact one from the amount players waiting at the finish line + if (player.tournamentParticipant && PlayerScore_Get(player, SP_RACE_FASTEST)) + roundFinisherCount--; + + if (tournamentStarted && (player.frags == FRAGS_PLAYER_OUT_OF_GAME || player.frags == FRAGS_PLAYER)) { player.tournamentParticipant = false; player.frags = FRAGS_PLAYER_OUT_OF_GAME; @@ -720,5 +891,5 @@ void ctscup_Initialize() //arguments: can round start, can round end (prematurely), called when round starts round_handler_Spawn(CTSCUP_CanRoundStart, CTSCUP_CheckRoundEnd, CTSCUP_RoundStart); //arguments: time until this round starts, pre-round preparation time, round timelimit - round_handler_Init(5, 1, autocvar_g_ctscup_warmup); + round_handler_Init(5, 0.5, autocvar_g_ctscup_warmup); } diff --git a/qcsrc/server/savestate.qc b/qcsrc/server/savestate.qc index 1561bcd03..5b6057d90 100644 --- a/qcsrc/server/savestate.qc +++ b/qcsrc/server/savestate.qc @@ -4,23 +4,8 @@ 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; - } + // reset loads since last save counter + player.savestate.loadCounter = 0; // fields stored by personal checkpoint, copy them to this implementation player.savestate.origin = player.origin; @@ -50,21 +35,10 @@ 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; - } + // store a counterfor loads since last save + player.savestate.loadCounter++; + // restore player state player.origin = player.savestate.origin; player.v_angle = player.savestate.v_angle; player.angles = player.savestate.angles; @@ -87,8 +61,11 @@ bool LoadSaveState(entity player) player.pauserotfuel_finished = time + player.savestate.pauserotfuel_finished - player.savestate.teleport_time; player.pauseregen_finished = time + player.savestate.pauseregen_finished - player.savestate.teleport_time; + // successfully loaded savestate return true; } + + // did not succeed in loading a savestate return false; } diff --git a/qcsrc/server/savestate.qh b/qcsrc/server/savestate.qh index 720ce4eb2..3b693aac4 100644 --- a/qcsrc/server/savestate.qh +++ b/qcsrc/server/savestate.qh @@ -8,6 +8,8 @@ void SaveSaveState(entity player); bool LoadSaveState(entity player); void DeleteSaveState(entity player); +.int loadCounter; + .vector origin; .vector v_angle; .vector angles; -- 2.39.2