]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
major update
authordrjaska <drjaska83@gmail.com>
Sun, 8 May 2022 18:13:17 +0000 (21:13 +0300)
committerdrjaska <drjaska83@gmail.com>
Sun, 8 May 2022 18:13:17 +0000 (21:13 +0300)
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
qcsrc/common/gamemodes/gamemode/ctscup/sv_ctscup.qc
qcsrc/server/savestate.qc

index a2538bb9fc1773f10b1373792dab789b9d4bc1d1..32171efda49dba21de44c1f55381397cfa03ece3 100644 (file)
@@ -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"
index 80808ea56b39cd32dfc1bf3426fadbb3e6bf7612..273f25271b6ef830ca6b80a9f9aff213cab555d6 100644 (file)
@@ -1,12 +1,14 @@
 #include "sv_ctscup.qh"
 
 #include <server/client.qh>
-#include <server/race.qh>
-#include <server/world.qh>
 #include <server/gamelog.qh>
 #include <server/intermission.qh>
+#include <server/race.qh>
+#include <server/scores.qh>
+#include <server/world.qh>
 #include <server/items/spawning.qh>
 #include <server/weapons/common.qh>
+
 #include <common/mapobjects/triggers.qh>
 
 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;
        }
index ef08083bb9739fc0dc2ce83637c0a48210820feb..1561bcd03c4a0c3d34549d54cc69244bd6b6e0ee 100644 (file)
@@ -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;