]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'bones_was_here/warmup' into 'master'
authorterencehill <piuntn@gmail.com>
Sun, 5 Feb 2023 12:34:05 +0000 (12:34 +0000)
committerterencehill <piuntn@gmail.com>
Sun, 5 Feb 2023 12:34:05 +0000 (12:34 +0000)
Implement g_warmup > 1 and related fixes and refactoring

See merge request xonotic/xonotic-data.pk3dir!1082

18 files changed:
.gitlab-ci.yml
qcsrc/client/announcer.qc
qcsrc/client/hud/panel/infomessages.qc
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/client/hud/panel/timer.qc
qcsrc/client/main.qc
qcsrc/client/main.qh
qcsrc/server/bot/default/bot.qc
qcsrc/server/chat.qc
qcsrc/server/client.qc
qcsrc/server/client.qh
qcsrc/server/command/cmd.qc
qcsrc/server/command/cmd.qh
qcsrc/server/command/vote.qc
qcsrc/server/command/vote.qh
qcsrc/server/world.qc
qcsrc/server/world.qh
xonotic-server.cfg

index 31378d8d25d85d6432a5b73209a2b11fc88127c1..148a8bd47658f7a71e7b1f932436c290690e6ad8 100644 (file)
@@ -69,7 +69,7 @@ test_sv_game:
     - wget -nv -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints\r
     - wget -nv -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache\r
 \r
-    - EXPECT=8fb740a3cb3754ca7b9295ddc5c911b5\r
+    - EXPECT=55338fabce73c671336171e6cb055f74\r
     - HASH=$(${ENGINE} +timestamps 1 +exec serverbench.cfg\r
       | tee /dev/stderr\r
       | sed -e 's,^\[[^]]*\] ,,'\r
index 70834c509b3c32dcf3d4393f279d0e3386c811c3..077a1c6d012ecb97f68f95fc15f00cad0d06ff9a 100644 (file)
@@ -214,7 +214,7 @@ void Announcer_Time()
        {
                float warmup_timelimit = STAT(WARMUP_TIMELIMIT);
                if(warmup_timelimit > 0)
-                       timeleft = max(0, warmup_timelimit - time);
+                       timeleft = max(0, warmup_timelimit + starttime - time);
                else
                        timeleft = 0;
        }
index e054bd94f05c3c091b02fbc435d22ea652051629..e85a7f30c3e82a022a543d640ae0621f90bf1a0b 100644 (file)
@@ -140,39 +140,44 @@ void HUD_InfoMessages()
                        InfoMessage(s);
                }
 
-               if(warmup_stage)
-               {
-                       s = _("^2Currently in ^1warmup^2 stage!");
-                       InfoMessage(s);
-               }
-
                string blinkcolor;
                if(time % 1 >= 0.5)
                        blinkcolor = "^1";
                else
                        blinkcolor = "^3";
 
-               if(warmup_stage && STAT(WARMUP_TIMELIMIT) <= 0 && srv_minplayers)
-               {
-                       Scoreboard_UpdatePlayerTeams(); // ensure numplayers is current
-                       if(srv_minplayers - numplayers == 1)
-                               s = _("^31^2 more player is needed for the match to start.");
-                       else
-                               s = sprintf(_("^3%d^2 more players are needed for the match to start."), srv_minplayers - numplayers);
-                       InfoMessage(s);
-               }
-               else if(ready_waiting && !spectatee_status)
-               {
-                       if(ready_waiting_for_me)
-                               s = sprintf(_("%sPress ^3%s%s to end warmup"), blinkcolor, getcommandkey(_("ready"), "ready"), blinkcolor);
-                       else
-                               s = _("^2Waiting for others to ready up to end warmup...");
-                       InfoMessage(s);
-               }
-               else if(warmup_stage && !spectatee_status)
+               if(warmup_stage)
                {
-                       s = sprintf(_("^2Press ^3%s^2 to end warmup"), getcommandkey(_("ready"), "ready"));
-                       InfoMessage(s);
+                       InfoMessage(_("^2Currently in ^1warmup^2 stage!"));
+
+                       int players_needed = 0;
+                       if(STAT(WARMUP_TIMELIMIT) <= 0 && srv_minplayers)
+                       {
+                               Scoreboard_UpdatePlayerTeams(); // ensure numplayers is current
+                               players_needed = srv_minplayers - numplayers;
+                       }
+
+                       if(players_needed > 0)
+                       {
+                               if(players_needed == 1)
+                                       s = _("^31^2 more player is needed for the match to start.");
+                               else
+                                       s = sprintf(_("^3%d^2 more players are needed for the match to start."), players_needed);
+                               InfoMessage(s);
+                       }
+                       else if(!spectatee_status)
+                       {
+                               if(ready_waiting)
+                               {
+                                       if(ready_waiting_for_me)
+                                               s = sprintf(_("%sPress ^3%s%s to end warmup"), blinkcolor, getcommandkey(_("ready"), "ready"), blinkcolor);
+                                       else
+                                               s = _("^2Waiting for others to ready up to end warmup...");
+                               }
+                               else
+                                       s = sprintf(_("^2Press ^3%s^2 to end warmup"), getcommandkey(_("ready"), "ready"));
+                               InfoMessage(s);
+                       }
                }
 
                if(teamplay && !spectatee_status && teamnagger)
index 9256d162bc83721c86c06e94f075a5fb4d81391f..c47d9611594a9b0daa6bd5a9a0a6e256412fef27 100644 (file)
@@ -2301,7 +2301,10 @@ void Scoreboard_Draw()
                }
                drawcolorcodedstring(pos + '1 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_detail_fontsize)), str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align right
                // map name and player count
-               str = sprintf(_("^5%d^7/^5%d ^7players"), numplayers, srv_maxplayers ? srv_maxplayers : maxclients);
+               if (campaign)
+                       str = "";
+               else
+                       str = sprintf(_("^5%d^7/^5%d ^7players"), numplayers, srv_maxplayers ? srv_maxplayers : maxclients);
                str = strcat("^7", _("Map:"), " ^2", mi_shortname, "    ", str); // reusing "Map:" translatable string
                drawcolorcodedstring(pos, str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align left
        }
index aa0c73b5bf90c4d08ea4e37d0d49cdb7f1ab460e..269d12b5edc800f9f2d50df6e7e70faad317bbb2 100644 (file)
@@ -125,7 +125,13 @@ void HUD_Timer()
                if (STAT(WARMUP_TIMELIMIT) > 0)
                        subtext = _("Warmup");
                else
-                       subtext = srv_minplayers ? _("Warmup: too few players") : _("Warmup: no time limit");
+               {
+                       Scoreboard_UpdatePlayerTeams(); // ensure numplayers is current
+                       if (srv_minplayers - numplayers > 0)
+                               subtext = _("Warmup: too few players");
+                       else
+                               subtext = _("Warmup: no time limit");
+               }
        }
        else if(STAT(TIMEOUT_STATUS) == 2)
                subtext = _("Timeout");
index 4e05e681db3d48d8c37276dfe4339e4e895fcd9e..689618fd116712f79ca9bc09191949d7a190e479 100644 (file)
@@ -709,7 +709,6 @@ NET_HANDLE(ENT_CLIENT_CLIENTDATA, bool isnew)
 NET_HANDLE(ENT_CLIENT_NAGGER, bool isnew)
 {
        make_pure(this);
-       int i, j, b, f;
 
        int nags = ReadByte(); // NAGS NAGS NAGS NAGS NAGS NAGS NADZ NAGS NAGS NAGS
 
@@ -736,20 +735,11 @@ NET_HANDLE(ENT_CLIENT_NAGGER, bool isnew)
                strcpy(vote_called_vote, ReadString());
        }
 
-       if(nags & 1)
-       {
-               for(j = 0; j < maxclients; ++j)
-                       if(playerslots[j])
-                               playerslots[j].ready = true;
-               for(i = 1; i <= maxclients; i += 8)
-               {
-                       f = ReadByte();
-                       for(j = i-1, b = BIT(0); b < BIT(8); b <<= 1, ++j)
-                               if (!(f & b))
-                                       if(playerslots[j])
-                                               playerslots[j].ready = false;
-               }
-       }
+       if(nags & BIT(0))
+               for(int i = 0; i < maxclients;)
+                       for(int f = ReadByte(), b = 0; b < 8 && i < maxclients; ++b, ++i)
+                               if(playerslots[i])
+                                       playerslots[i].ready = f & BIT(b);
 
        return = true;
 
@@ -1404,7 +1394,7 @@ string GetVersionMessage(string hostversion, bool version_mismatch, bool version
 
 bool net_handle_ServerWelcome()
 {
-       bool campaign = ReadByte();
+       campaign = ReadByte();
        if (campaign)
        {
                string campaign_title = ReadString();
index 1f9e6a9a03e5aa1f78ef13dd78a8de13b79853ca..288a303ccad4008ab87f4902131381736ee5cb52 100644 (file)
@@ -105,6 +105,7 @@ float current_viewzoom;
 float zoomin_effect;
 bool warmup_stage;
 
+bool campaign;
 string hostname;
 string welcome_msg;
 int srv_minplayers;
index b81507890f302da772d225f28f44c12a73ac85ae..5bfa79aa55edbb3ace3986e6fe60c8721d259329 100644 (file)
@@ -30,6 +30,7 @@
 #include <server/weapons/accuracy.qh>
 #include <server/weapons/selection.qh>
 #include <server/world.qh>
+#include <server/command/vote.qh>
 
 STATIC_INIT(bot) { bot_calculate_stepheightvec(); }
 
@@ -118,7 +119,6 @@ void bot_think(entity this)
                        W_NextWeapon(this, 0, weaponentity);
                // block the bot during the countdown to game start
                CS(this).movement = '0 0 0';
-               this.bot_nextthink = game_starttime;
                return;
        }
 
@@ -146,6 +146,12 @@ void bot_think(entity this)
        else if(this.aistatus & AI_STATUS_STUCK)
                navigation_unstuck(this);
 
+       if (warmup_stage && !this.ready)
+       {
+               this.ready = true;
+               ReadyCount(); // this must be delayed until the bot has spawned
+       }
+
        // now call the current bot AI (havocbot for example)
        this.bot_ai(this);
 }
index a73de2a1f6c58f2ed97e4303ae5179a7354b07db..41af1de57c1bae1b23c9bbf42111a3f4fcf29a51 100644 (file)
@@ -208,7 +208,7 @@ int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodc
        {
                if(autocvar_g_chat_flood_notify_flooder)
                {
-                       sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
+                       sourcemsgstr = strcat(msgstr, "\n^3CHAT FLOOD CONTROL: ^7message too long, trimmed\n");
                        sourcecmsgstr = "";
                }
                else
@@ -247,7 +247,7 @@ int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodc
        {
                if (autocvar_g_chat_flood_notify_flooder)
                {
-                       sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
+                       sprint(source, strcat("^3CHAT FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
                        ret = 0;
                }
                else
index f27a4e2a7b0185e657e79cae19326a87c35d34c5..5b7509e484d5fecec7e6f49a852de6ea218e39e9 100644 (file)
@@ -536,6 +536,19 @@ void FixPlayermodel(entity player)
                                setcolor(player, stof(autocvar_sv_defaultplayercolors));
 }
 
+void GiveWarmupResources(entity this)
+{
+       SetResource(this, RES_SHELLS, warmup_start_ammo_shells);
+       SetResource(this, RES_BULLETS, warmup_start_ammo_nails);
+       SetResource(this, RES_ROCKETS, warmup_start_ammo_rockets);
+       SetResource(this, RES_CELLS, warmup_start_ammo_cells);
+       SetResource(this, RES_PLASMA, warmup_start_ammo_plasma);
+       SetResource(this, RES_FUEL, warmup_start_ammo_fuel);
+       SetResource(this, RES_HEALTH, warmup_start_health);
+       SetResource(this, RES_ARMOR, warmup_start_armorvalue);
+       STAT(WEAPONS, this) = WARMUP_START_WEAPONS;
+}
+
 void PutPlayerInServer(entity this)
 {
        if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
@@ -578,17 +591,10 @@ void PutPlayerInServer(entity this)
        this.takedamage = DAMAGE_AIM;
        this.effects = EF_TELEPORT_BIT | EF_RESTARTANIM_BIT;
 
-       if (warmup_stage) {
-               SetResource(this, RES_SHELLS, warmup_start_ammo_shells);
-               SetResource(this, RES_BULLETS, warmup_start_ammo_nails);
-               SetResource(this, RES_ROCKETS, warmup_start_ammo_rockets);
-               SetResource(this, RES_CELLS, warmup_start_ammo_cells);
-               SetResource(this, RES_PLASMA, warmup_start_ammo_plasma);
-               SetResource(this, RES_FUEL, warmup_start_ammo_fuel);
-               SetResource(this, RES_HEALTH, warmup_start_health);
-               SetResource(this, RES_ARMOR, warmup_start_armorvalue);
-               STAT(WEAPONS, this) = WARMUP_START_WEAPONS;
-       } else {
+       if (warmup_stage)
+               GiveWarmupResources(this);
+       else
+       {
                SetResource(this, RES_SHELLS, start_ammo_shells);
                SetResource(this, RES_BULLETS, start_ammo_nails);
                SetResource(this, RES_ROCKETS, start_ammo_rockets);
@@ -812,7 +818,7 @@ void PutPlayerInServer(entity this)
 
        antilag_clear(this, CS(this));
 
-       if (warmup_stage == -1)
+       if (warmup_stage < 0 || warmup_stage > 1)
                ReadyCount();
 }
 
@@ -1042,7 +1048,7 @@ void SendWelcomeMessage(entity this, int msg_type)
        WriteString(msg_type, autocvar_g_xonoticversion);
        WriteByte(msg_type, CS(this).version_mismatch);
        WriteByte(msg_type, (CS(this).version < autocvar_gameversion));
-       WriteByte(msg_type, map_minplayers);
+       WriteByte(msg_type, autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers);
        WriteByte(msg_type, GetPlayerLimit());
 
        MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
index 56cce52a996dd338b551cb08441425f32ce957a0..33b9d4511967c5c0834fdc9f0190c78b78d10df3 100644 (file)
@@ -185,7 +185,6 @@ CLASS(Client, Object)
     ATTRIB(Client, specialcommand_pos, int, this.specialcommand_pos);
     ATTRIB(Client, hitplotfh, int, this.hitplotfh);
     ATTRIB(Client, clientdata, entity, this.clientdata);
-    ATTRIB(Client, cmd_floodcount, int, this.cmd_floodcount);
     ATTRIB(Client, cmd_floodtime, float, this.cmd_floodtime);
     ATTRIB(Client, wasplayer, bool, this.wasplayer);
     ATTRIB(Client, weaponorder_byimpulse, string, this.weaponorder_byimpulse);
@@ -393,6 +392,8 @@ void SetSpectatee_status(entity this, int spectatee_num);
 
 void FixPlayermodel(entity player);
 
+void GiveWarmupResources(entity this);
+
 void ClientInit_misc(entity this);
 
 int GetPlayerLimit();
index e2d71597f44ae1f20369a8b6c7e2e2c59f38c90e..947edd891e96e4b171396a5bc054019bcbd76668 100644 (file)
 //  Last updated: December 28th, 2011
 // =========================================================
 
-bool SV_ParseClientCommand_floodcheck(entity this)
-{
-       entity store = IS_CLIENT(this) ? CS(this) : this; // unfortunately, we need to store these on the client initially
-
-       if (!timeout_status)  // not while paused
-       {
-               if (time <= (store.cmd_floodtime + autocvar_sv_clientcommand_antispam_time))
-               {
-                       store.cmd_floodcount += 1;
-                       if (store.cmd_floodcount > autocvar_sv_clientcommand_antispam_count)   return false;  // too much spam, halt
-               }
-               else
-               {
-                       store.cmd_floodtime = time;
-                       store.cmd_floodcount = 1;
-               }
-       }
-       return true;  // continue, as we're not flooding yet
-}
-
 
 // =======================
 //  Command Sub-Functions
@@ -373,28 +353,20 @@ void ClientCommand_ready(entity caller, int request)
        {
                case CMD_REQUEST_COMMAND:
                {
-                       if (IS_CLIENT(caller) && caller.last_ready < time - 3)
+                       if (warmup_stage || g_race_qualifying == 2)
+                       if (IS_PLAYER(caller) || INGAME_JOINED(caller))
                        {
-                               if (warmup_stage || g_race_qualifying == 2)
+                               if (caller.ready) // toggle
                                {
-                                       if (time < game_starttime) // game is already restarting
-                                               return;
-                                       if (caller.ready)            // toggle
-                                       {
-                                               caller.ready = false;
-                                               if (IS_PLAYER(caller) || INGAME_JOINED(caller))
-                                                       bprint(playername(caller.netname, caller.team, false), "^2 is ^1NOT^2 ready\n");
-                                       }
-                                       else
-                                       {
-                                               caller.ready = true;
-                                               if (IS_PLAYER(caller) || INGAME_JOINED(caller))
-                                                       bprint(playername(caller.netname, caller.team, false), "^2 is ready\n");
-                                       }
-
-                                       caller.last_ready = time;
-                                       ReadyCount();
+                                       caller.ready = false;
+                                       bprint(playername(caller.netname, caller.team, false), "^2 is ^1NOT^2 ready\n");
                                }
+                               else
+                               {
+                                       caller.ready = true;
+                                       bprint(playername(caller.netname, caller.team, false), "^2 is ready\n");
+                               }
+                               ReadyCount();
                        }
                        return;  // never fall through to usage
                }
@@ -894,6 +866,7 @@ void SV_ParseClientCommand(entity this, string command)
                case "prespawn": break;                            // handled by engine in host_cmd.c
                case "sentcvar": break;                            // handled by server in this file
                case "spawn": break;                               // handled by engine in host_cmd.c
+               case "say": case "say_team": case "tell": break;   // chat has its own flood control in chat.qc
                case "color": case "topcolor": case "bottomcolor": // handled by engine in host_cmd.c
                        if(!IS_CLIENT(this)) // on connection
                        {
@@ -905,9 +878,24 @@ void SV_ParseClientCommand(entity this, string command)
                        break;
                case "c2s": Net_ClientCommand(this, command); return; // handled by net.qh
 
+               // on connection, client sends all of these
+               case "name": case "rate": case "rate_burstsize": case "playermodel": case "playerskin": case "clientversion":
+                       if(!IS_CLIENT(this)) break;
+                       // else fall through to default: flood control
                default:
-                       if (SV_ParseClientCommand_floodcheck(this)) break; // "true": continue, as we're not flooding yet
-                       else return;                                   // "false": not allowed to continue, halt // print("^1ERROR: ^7ANTISPAM CAUGHT: ", command, ".\n");
+                       if (!timeout_status)  // not while paused
+                       {
+                               entity store = IS_CLIENT(this) ? CS(this) : this; // unfortunately, we need to store these on the client initially
+                               // this is basically the same as the chat flood control
+                               if (time < store.cmd_floodtime)
+                               {
+                                       sprint(this, strcat("^3CMD FLOOD CONTROL: wait ^1", ftos(store.cmd_floodtime - time), "^3 seconds, command was: ", command, "\n"));
+                                       return;  // too much spam, halt
+                               }
+                               else
+                                       store.cmd_floodtime = max(time - autocvar_sv_clientcommand_antispam_count * autocvar_sv_clientcommand_antispam_time, store.cmd_floodtime) + autocvar_sv_clientcommand_antispam_time;
+                       }
+                       break;  // continue, as we're not flooding yet
        }
 
        /* NOTE: should this be disabled? It can be spammy perhaps, but hopefully it's okay for now */
index 802afc8bde6f86e1ed3e145df61f111ea51ec715..bb97f0d0b17cf18926d1d0c05a996d4a515e116e 100644 (file)
@@ -4,7 +4,6 @@ float autocvar_sv_clientcommand_antispam_time;
 int autocvar_sv_clientcommand_antispam_count;
 
 .float cmd_floodtime;
-.float cmd_floodcount;
 
 string MapVote_Suggest(entity this, string m);
 
index a1675ba566f5a25e06569a8b9c201b2798f0c053..a207b226b67a6a3259c049d0c15a948d96345f45 100644 (file)
@@ -35,8 +35,7 @@
 //  Nagger for players to know status of voting
 bool Nagger_SendEntity(entity this, entity to, float sendflags)
 {
-       int nags, i, f, b;
-       entity e;
+       int nags = 0;
        WriteHeader(MSG_ENTITY, ENT_CLIENT_NAGGER);
 
        // bits:
@@ -49,25 +48,23 @@ bool Nagger_SendEntity(entity this, entity to, float sendflags)
        //  64 = vote counts
        // 128 = vote string
 
-       nags = 0;
-       if (readycount)
+       if (warmup_stage)
        {
-               nags |= BIT(0);
-               if (to.ready == 0) nags |= BIT(1);
+               if (readycount)
+               {
+                       nags |= BIT(0);
+                       if (!to.ready) nags |= BIT(1);
+               }
+               nags |= BIT(4);
        }
+
        if (vote_called)
        {
                nags |= BIT(2);
                if (to.vote_selection == 0) nags |= BIT(3);
+               nags |= sendflags & BIT(6);
+               nags |= sendflags & BIT(7);
        }
-       if (warmup_stage) nags |= BIT(4);
-
-       if (sendflags & BIT(6)) nags |= BIT(6);
-
-       if (sendflags & BIT(7)) nags |= BIT(7);
-
-       if (!(nags & 4))  // no vote called? send no string
-               nags &= ~(BIT(6) | BIT(7));
 
        WriteByte(MSG_ENTITY, nags);
 
@@ -81,13 +78,14 @@ bool Nagger_SendEntity(entity this, entity to, float sendflags)
 
        if (nags & BIT(7)) WriteString(MSG_ENTITY, vote_called_display);
 
-       if (nags & 1)
+       if (nags & BIT(0))
        {
-               for (i = 1; i <= maxclients; i += 8)
+               for (int i = 1; i <= maxclients;)
                {
-                       for (f = 0, e = edict_num(i), b = BIT(0); b < BIT(8); b <<= 1, e = nextent(e))
-                               if (!IS_REAL_CLIENT(e) || e.ready)
-                                       f |= b;
+                       int f = 0;
+                       for (int b = 0; b < 8 && i <= maxclients; ++b, ++i)
+                               if (edict_num(i).ready)
+                                       f |= BIT(b);
                        WriteByte(MSG_ENTITY, f);
                }
        }
@@ -160,7 +158,7 @@ void VoteAccept()
 {
        bprint("\{1}^2* ^3", OriginalCallerName(), "^2's vote for ^1", vote_called_display, "^2 was accepted\n");
 
-       if ((vote_called == VOTE_MASTER) && vote_caller) vote_caller.vote_master = 1;
+       if ((vote_called == VOTE_MASTER) && vote_caller) vote_caller.vote_master = true;
        else localcmd(strcat(vote_called_command, "\n"));
 
        if (vote_caller)   vote_caller.vote_waittime = 0;  // people like your votes, you don't need to wait to vote again
@@ -419,7 +417,8 @@ void reset_map(bool dorespawn, bool is_fake_round_start)
 // Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown is set)
 void ReadyRestart_think(entity this)
 {
-       reset_map(true, false);
+       if (!warmup_stage) // if the countdown was not aborted
+               reset_map(true, false);
        delete(this);
 }
 
@@ -428,7 +427,7 @@ void ReadyRestart_force(bool is_fake_round_start)
 {
        if (time <= game_starttime && game_stopped)
                return;
-       if (!is_fake_round_start)
+       if (!is_fake_round_start && !autocvar_g_campaign)
                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_COUNTDOWN_RESTART);
 
        VoteReset();
@@ -455,8 +454,8 @@ void ReadyRestart_force(bool is_fake_round_start)
        if(!is_fake_round_start && !warmup_stage)
                localcmd("\nsv_hook_warmupend\n");
 
-       // reset the .ready status of all players (also spectators)
-       FOREACH_CLIENT(IS_REAL_CLIENT(it), { it.ready = false; });
+       // reset the .ready status of all clients (including spectators and bots)
+       FOREACH_CLIENT(true, { it.ready = false; });
        readycount = 0;
        Nagger_ReadyCounted();  // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client
 
@@ -498,7 +497,7 @@ void ReadyRestart(bool forceWarmupEnd)
        if(forceWarmupEnd || autocvar_g_campaign)
                warmup_stage = 0; // forcefully end warmup and go to match stage
        else
-               warmup_stage = cvar("g_warmup"); // go into warmup if it's enabled, otherwise restart into match stage
+               warmup_stage = autocvar_g_warmup; // go into warmup if it's enabled, otherwise restart into match stage
 
        ReadyRestart_force(false);
 }
@@ -513,30 +512,41 @@ void ReadyCount()
        // cannot reset the game while a timeout is active or pending
        if (timeout_status) return;
 
-       float ready_needed_factor, ready_needed_count;
-       float t_players = 0;
+       int total_players = 0, human_players = 0, humans_ready = 0;
        readycount = 0;
 
-       FOREACH_CLIENT(IS_REAL_CLIENT(it) && (IS_PLAYER(it) || INGAME_JOINED(it)), {
-               ++t_players;
+       FOREACH_CLIENT(IS_PLAYER(it) || INGAME_JOINED(it), {
+               ++total_players;
                if (it.ready) ++readycount;
+               if (IS_REAL_CLIENT(it))
+               {
+                       ++human_players;
+                       if (it.ready) ++humans_ready;
+               }
        });
 
        Nagger_ReadyCounted();
 
-       if (t_players < map_minplayers) // map_minplayers will only be set if g_warmup -1 at worldspawn
+       // can't read warmup_stage here as it could have been set to 0 by ReadyRestart()
+       // and we need to use this when checking if we should abort the countdown
+       // map_minplayers can only be > 0 if g_warmup was -1 at worldspawn
+       int minplayers = autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers;
+
+       if (total_players < minplayers)
        {
                if (game_starttime > time) // someone bailed during countdown, back to warmup
                {
-                       warmup_stage = -1; // CAN change it AFTER calling Nagger_ReadyCounted() this frame
+                       warmup_stage = autocvar_g_warmup; // CAN change it AFTER calling Nagger_ReadyCounted() this frame
                        game_starttime = time;
-                       Send_Notification(NOTIF_ALL, NULL, MSG_MULTI, COUNTDOWN_STOP, map_minplayers);
+                       Send_Notification(NOTIF_ALL, NULL, MSG_MULTI, COUNTDOWN_STOP, minplayers);
+                       if (!sv_ready_restart_after_countdown) // if we ran reset_map() at start of countdown
+                               FOREACH_CLIENT(IS_PLAYER(it), { GiveWarmupResources(it); });
                }
                if (warmup_limit > 0)
                        warmup_limit = -1;
                return; // don't ReadyRestart if players are ready but too few
        }
-       else if (map_minplayers && warmup_limit <= 0)
+       else if (minplayers && warmup_limit <= 0)
        {
                // there's enough players now but we're still in infinite warmup
                warmup_limit = cvar("g_warmup_limit");
@@ -548,10 +558,8 @@ void ReadyCount()
                // warmup continues until enough players AND enough RUPs (no time limit)
        }
 
-       ready_needed_factor = bound(0.5, cvar("g_warmup_majority_factor"), 1);
-       ready_needed_count = ceil(t_players * ready_needed_factor);
-
-       if (readycount >= ready_needed_count) ReadyRestart(true);
+       if (humans_ready && humans_ready >= rint(human_players * bound(0.5, cvar("g_warmup_majority_factor"), 1)))
+               ReadyRestart(true);
 }
 
 
index 88e311beb670eb4581deec4f2ff2b12a72656af5..184e502e55e6287f783c0febbb0ed47c6f818e03 100644 (file)
@@ -40,15 +40,15 @@ const float VOTE_MASTER = 2;
 // global vote information declarations
 entity vote_caller;         // original caller of the current vote
 string vote_caller_name;    // name of the vote caller
-float vote_called;          // stores status of current vote (See VOTE_*)
+int vote_called;            // stores status of current vote (See VOTE_*)
 float vote_endtime;         // time when the vote is finished
-float vote_accept_count;    // total amount of players who accept the vote (counted by VoteCount() function)
-float vote_reject_count;    // same as above, but rejected
-float vote_abstain_count;   // same as above, but abstained
-float vote_needed_overall;  // total amount of players NEEDED for a vote to pass (based on sv_vote_majority_factor)
-.float vote_master;         // flag for if the player has vote master privelages
+int vote_accept_count;      // total amount of players who accept the vote (counted by VoteCount() function)
+int vote_reject_count;      // same as above, but rejected
+int vote_abstain_count;     // same as above, but abstained
+int vote_needed_overall;    // total amount of players NEEDED for a vote to pass (based on sv_vote_majority_factor)
+.bool vote_master;          // flag for if the player has vote master privileges
 .float vote_waittime;       // flag for how long the player must wait before they can vote again
-.float vote_selection;      // flag for which vote selection the player has made (See VOTE_SELECT_*)
+.int vote_selection;        // flag for which vote selection the player has made (See VOTE_SELECT_*)
 string vote_called_command; // command sent by client
 string vote_called_display; // visual string of command sent by client
 string vote_parsed_command; // command which is fixed after being parsed
@@ -63,8 +63,7 @@ void VoteCommand(int request, entity caller, int argc, string vote_command);
 const float RESTART_COUNTDOWN = 10;
 entity nagger;
 int readycount;                    // amount of players who are ready
-.float ready;                      // flag for if a player is ready
-.float last_ready;                 // last ready time for anti-spam
+.bool ready;                       // flag for if a player is ready
 .int team_saved;                   // team number to restore upon map reset
 .void(entity this) reset;          // if set, an entity is reset using this
 .void(entity this) reset2;         // if set, an entity is reset using this (after calling ALL the reset functions for other entities)
index 0605a8fbdc31b7a3ea4c7c9e474d49c97f2cde5a..f893c337b66fc2d2a6c09361330b7dc8b747bf10 100644 (file)
@@ -660,10 +660,9 @@ void GameplayMode_DelayedInit(entity this)
                        int u = AVAILABLE_TEAMS - d;
                        map_minplayers += (u < d && u + map_minplayers <= m) ? u : -d;
                }
-               warmup_limit = -1;
        }
        else
-               map_minplayers = 0; // don't display a minimum if it's not used
+               map_minplayers = 0; // don't display a minimum if it's not used (g_maxplayers < 0 && g_warmup >= 0)
 }
 
 void InitGameplayMode()
@@ -864,9 +863,6 @@ spawnfunc(worldspawn)
 
        GameRules_limit_fallbacks();
 
-       if(warmup_limit == 0)
-               warmup_limit = autocvar_timelimit * 60;
-
        player_count = 0;
        bot_waypoints_for_items = autocvar_g_waypoints_for_items;
        if(bot_waypoints_for_items == 1)
@@ -2105,11 +2101,21 @@ void readlevelcvars()
 
        sv_ready_restart_after_countdown = cvar("sv_ready_restart_after_countdown");
 
-       warmup_stage = cvar("g_warmup");
-       warmup_limit = cvar("g_warmup_limit");
-
        if(cvar("g_campaign"))
                warmup_stage = 0; // no warmup during campaign
+       else
+       {
+               warmup_stage = autocvar_g_warmup;
+               if (warmup_stage < 0 || warmup_stage > 1)
+                       warmup_limit = -1; // don't start until there's enough players
+               else if (warmup_stage == 1)
+               {
+                       // this code is duplicated in ReadyCount()
+                       warmup_limit = cvar("g_warmup_limit");
+                       if(warmup_limit == 0)
+                               warmup_limit = autocvar_timelimit * 60;
+               }
+       }
 
        g_pickup_respawntime_weapon = cvar("g_pickup_respawntime_weapon");
        g_pickup_respawntime_superweapon = cvar("g_pickup_respawntime_superweapon");
index 06c5f5a2be3a3c0701990d5a5f7eb3baddc70a32..ff799e64cc529d7d406aebd5d929d12cb25b4766 100644 (file)
@@ -6,6 +6,7 @@ bool autocvar__sv_init;
 bool autocvar__endmatch;
 bool autocvar_g_use_ammunition;
 bool autocvar_g_jetpack;
+int autocvar_g_warmup;
 bool autocvar_g_warmup_allguns;
 bool autocvar_g_warmup_allow_timeout;
 #define autocvar_g_weaponarena cvar_string("g_weaponarena")
index 7d7b94733bf4eced7a520896910db10b73ee2180..82cccae0f3b324fdcf0ce8ee999962c54772f483 100644 (file)
@@ -30,11 +30,11 @@ set g_maxplayers 0 "maximum number of players allowed to play at the same time,
 set g_maxplayers_spectator_blocktime 5 "if the players voted for the \"nospectators\" command, this setting defines the number of seconds a observer/spectator has time to join the game before they get kicked"
 
 // tournament mod
-set g_warmup 0 "split the game into a warmup- and match-stage, -1 means stay in warmup until enough (set by map, lower bound of 2 or 2 per team) players join, then g_warmup_limit and readiness apply"
+set g_warmup 0 "splits the game into warmup and match stages, 1 means the match starts when g_warmup_majority_factor of players are ready OR g_warmup_limit is hit, >1 also requires at least g_warmup players (including bots) to join, -1 means that minimum player requrement is set by the map (lower bound of 2 or 2 per team)"
 set g_warmup_limit 180 "limit warmup-stage to this time (in seconds); if set to -1 the warmup-stage is not affected by any timelimit, if set to 0 the usual timelimit also affects warmup-stage"
 set g_warmup_allow_timeout 0 "allow calling timeouts in the warmup-stage (if sv_timeout is set to 1)"
 set g_warmup_allguns 1 "provide more weapons on start while in warmup: 0 = normal start weapons, 1 = all guns available on the map, 2 = all normal weapons"
-set g_warmup_majority_factor 0.8 "fraction of joined players sufficient to end warmup before g_warmup_limit by readying up"
+set g_warmup_majority_factor 0.8 "fraction of joined players (not including bots) sufficient to end warmup before g_warmup_limit by readying up"
 
 alias sv_hook_warmupend