]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into z411/bai-server
authorz411 <z411@omaera.org>
Sat, 23 Apr 2022 21:04:45 +0000 (17:04 -0400)
committerz411 <z411@omaera.org>
Sat, 23 Apr 2022 21:04:45 +0000 (17:04 -0400)
1  2 
qcsrc/client/announcer.qc
qcsrc/client/hud/panel/centerprint.qc
qcsrc/client/hud/panel/centerprint.qh
qcsrc/client/main.qc
qcsrc/server/client.qc
qcsrc/server/command/vote.qc
qcsrc/server/teamplay.qc
qcsrc/server/world.qc

index 5c7a7d58985c16b62695667cb0815a20a40be6fc,e86702afa086a8314ee12ba92540f605929a6b7d..7ff4602b2ab2c110376f35179b84fd3269b68d84
@@@ -42,8 -42,7 +42,7 @@@ void Announcer_Duel(
        strcpy(prev_pl2_name, pl2_name);
  
        // There are new duelers, update title
-       float offset = stringwidth(pl2_name, true, hud_fontsize) - stringwidth(pl1_name, true, hud_fontsize) - 1;
-       centerprint_SetTitle(sprintf("^BG%s^BG  %s  %s", pl1_name, _("vs"), pl2_name), offset / 2);
+       centerprint_SetDuelTitle(pl1_name, pl2_name, _("vs"));
  }
  
  void Announcer_ClearTitle()
@@@ -58,9 -57,7 +57,9 @@@ void Announcer_Countdown(entity this
  {
        float starttime = STAT(GAMESTARTTIME);
        float roundstarttime = STAT(ROUNDSTARTTIME);
 -      if(roundstarttime == -1)
 +      bool game_timeout = (STAT(TIMEOUT_LAST) > 0);
 +      
 +      if(roundstarttime == -1 || game_timeout)
        {
                Local_Notification(MSG_CENTER, CENTER_COUNTDOWN_ROUNDSTOP);
                delete(this);
@@@ -73,8 -70,6 +72,8 @@@
        float countdown = (inround ? roundstarttime - time : starttime - time);
        float countdown_rounded = floor(0.5 + countdown);
  
 +      if(time >= starttime) centerprint_ClearTitle();
 +      
        if(countdown <= 0) // countdown has finished, starttime is now
        {
                Local_Notification(MSG_CENTER, CENTER_COUNTDOWN_BEGIN);
@@@ -154,13 -149,10 +153,13 @@@ void Announcer_Gamestart(
                                if (gametype.m_1v1)
                                        Announcer_Duel();
                                else
-                                       centerprint_SetTitle(strcat("^BG", MapInfo_Type_ToText(gametype)), 0); // Show game type as title
+                                       centerprint_SetTitle(strcat("^BG", MapInfo_Type_ToText(gametype))); // Show game type as title
  
                                if(time + 5.0 < startTime) // if connecting to server while restart was active don't always play prepareforbattle
 -                                      Local_Notification(MSG_ANNCE, ANNCE_PREPARE);
 +                                      if(teamplay)
 +                                              Local_Notification(MSG_ANNCE, ANNCE_PREPARE_TEAM);
 +                                      else
 +                                              Local_Notification(MSG_ANNCE, ANNCE_PREPARE);
                        }
  
                        announcer_countdown.nextthink = startTime - floor(startTime - time + 0.5); //synchronize nextthink to startTime
index 391cb5364f9804d5f23ccb4008d819fbcfc8ab9b,1436f2ef6a9d48e8675976e190ee76c542847329..7688d3b57c0bb93dba97c45719ddda712cd4b8b0
@@@ -45,12 -45,9 +45,13 @@@ float centerprint_expire_time[CENTERPRI
  int centerprint_countdown_num[CENTERPRINT_MAX_MSGS];
  bool centerprint_showing;
  
 +float centerprint_medal_expire_time;
 +string centerprint_medal_icon;
 +float centerprint_medal_times;
 +
  string centerprint_title;
- float centerprint_title_offset;
+ string centerprint_title_left;
+ string centerprint_title_right;
  
  void centerprint_Add(int new_id, string strMessage, float duration, int countdown_num)
  {
@@@ -154,33 -151,24 +155,39 @@@ void centerprint_KillAll(
        }
  }
  
- void centerprint_ClearTitle()
 +void centerprint_Medal(string icon, float times)
 +{
 +      if(!autocvar_hud_panel_centerprint_medals) return;
 +
 +      //LOG_INFOF("centerprint_Medal: icon: %s times: %d", icon, times);
 +      //centerprint_medal_expire_time = time + autocvar_hud_panel_centerprint_time;
 +      centerprint_medal_expire_time = time + MSG_MEDAL_TIME;
 +      centerprint_medal_times = times;
 +      if(centerprint_medal_icon)
 +              strunzone(centerprint_medal_icon);
 +      centerprint_medal_icon = strzone(strcat("gfx/medal/", icon));
 +
 +      centerprint_showing = true;
 +}
 +
+ void centerprint_SetDuelTitle(string left, string right, string div)
  {
-       strfree(centerprint_title);
-       centerprint_title_offset = 0;
+       strcpy(centerprint_title_left, left);
+       strcpy(centerprint_title_right, right);
+       centerprint_SetTitle(sprintf("^BG%s^BG  %s  %s", left, div, right));
  }
  
- void centerprint_SetTitle(string title, float offset)
+ void centerprint_SetTitle(string title)
  {
-       if(title != centerprint_title) {
+       if(title != centerprint_title)
                strcpy(centerprint_title, CCR(title));
-               centerprint_title_offset = offset;
-       }
+ }
+ void centerprint_ClearTitle()
+ {
+       strfree(centerprint_title);
+       strfree(centerprint_title_left);
+       strfree(centerprint_title_right);
  }
  
  float hud_configure_cp_generation_time;
@@@ -206,7 -194,7 +213,7 @@@ void HUD_CenterPrint(
                {
                        if(highlightedPanel == HUD_PANEL(CENTERPRINT))
                        {
-                               centerprint_SetTitle(sprintf(_("Title at %s"), seconds_tostring(hud_configure_cp_generation_time)), 0);
+                               centerprint_SetTitle(sprintf(_("Title at %s"), seconds_tostring(hud_configure_cp_generation_time)));
  
                                float r;
                                r = random();
        bool all_messages_expired = true;
  
        pos = panel_pos;
 +
        if (autocvar_hud_panel_centerprint_flip)
                pos.y += panel_size.y;
        align = bound(0, autocvar_hud_panel_centerprint_align, 1);
 +      
 +      // z411 draw medals first
 +      if (autocvar_hud_panel_centerprint_medals && time < centerprint_medal_expire_time) {
 +              float height = vid_conheight/50 * 4;
 +              pos.y -= height;
 +
 +              if(time < centerprint_medal_expire_time - MSG_MEDAL_FADE_TIME)
 +                      a = 1;
 +              else
 +                      a = (centerprint_medal_expire_time - time) / MSG_MEDAL_FADE_TIME;
 +              
 +              vector tmp_in = pos;
 +                      
 +              vector mysize = draw_getimagesize(centerprint_medal_icon);
 +              vector newsize = vec2(height*(mysize.x/mysize.y), height);
 +              vector fontsize = '1 1 0' * (newsize.y/2);
 +              
 +              tmp_in.x += (panel_size.x - newsize.x) / 2; // center medal icon
 +              
 +              if(centerprint_medal_times < autocvar_hud_panel_centerprint_medals_max) {
 +                      tmp_in.x -= ((newsize.x * 1.1) * (centerprint_medal_times - 1) / 2);
 +                      for(int t = 0; t < centerprint_medal_times; t++) {
 +                              drawpic(tmp_in, centerprint_medal_icon, newsize, '1 1 1', a, DRAWFLAG_NORMAL);
 +                              tmp_in.x += newsize.x * 1.1;
 +                      }
 +              } else {
 +                      drawpic(tmp_in, centerprint_medal_icon, newsize, '1 1 1', a, DRAWFLAG_NORMAL);
 +                      tmp_in.x += newsize.x + fontsize.x * 0.25; // draw times next to it
 +                      tmp_in.y += (newsize.y - fontsize.y) / 2;
 +                      drawstring(tmp_in, ftos(centerprint_medal_times), fontsize, '1 1 1', a, DRAWFLAG_NORMAL);
 +              }
 +
 +              pos.y += height;
 +              
 +              all_messages_expired = false;
 +      }
  
        // Show title if available
        if(centerprint_title) {
  
                if (autocvar_hud_panel_centerprint_flip)
                        pos.y -= fontsize.y;
-               if (centerprint_title_offset && align == 0.5)
-                       pos.x += centerprint_title_offset * CENTERPRINT_BASE_SIZE * autocvar_hud_panel_centerprint_fontscale_title;
+               if (centerprint_title_left != "" && align == 0.5) // Center line at the main word (for duels)
+                       pos.x += (stringwidth(centerprint_title_right, true, fontsize) - stringwidth(centerprint_title_left, true, fontsize)) / 2;
  
                drawcolorcodedstring(pos, centerprint_title, fontsize, 1, DRAWFLAG_NORMAL);
  
  
                if (time < centerprint_start_time[j]) continue;
  
 -              float fade_in_time = autocvar_hud_panel_centerprint_fade_in;
 +              float fade_in_time = 0;
                float fade_out_time = autocvar_hud_panel_centerprint_fade_out;
  
                if (centerprint_countdown_num[j] && centerprint_start_time[j]) {
                a *= panel_fg_alpha;
  
                // finally set the size based on the alpha
 -              sz = autocvar_hud_panel_centerprint_fade_minfontsize + a * (1 - autocvar_hud_panel_centerprint_fade_minfontsize);
 +              //sz = autocvar_hud_panel_centerprint_fade_minfontsize + a * (1 - autocvar_hud_panel_centerprint_fade_minfontsize);
 +              sz = 1; // remove zoom;
                drawfontscale = hud_scale * sz;
  
                if (centerprint_countdown_num[j])
index 7a75f45cee4a2f33f721ebd01b2ac09751260dfc,02a046a6a140015eae8167254f1ff670e2354854..a3e419ba8a4d0a29d287bdf6fb593ce5ab148f81
@@@ -3,14 -3,14 +3,14 @@@
  
  bool autocvar_hud_panel_centerprint;
  float autocvar_hud_panel_centerprint_align;
 -float autocvar_hud_panel_centerprint_fade_in = 0.15;
 +//float autocvar_hud_panel_centerprint_fade_in = 0;
  float autocvar_hud_panel_centerprint_fade_out = 0.15;
  float autocvar_hud_panel_centerprint_fade_subsequent = 1;
  float autocvar_hud_panel_centerprint_fade_subsequent_passone = 3;
  float autocvar_hud_panel_centerprint_fade_subsequent_passone_minalpha = 0.5;
  float autocvar_hud_panel_centerprint_fade_subsequent_passtwo = 10;
  float autocvar_hud_panel_centerprint_fade_subsequent_passtwo_minalpha = 0.5;
 -float autocvar_hud_panel_centerprint_fade_minfontsize = 1;
 +//float autocvar_hud_panel_centerprint_fade_minfontsize = 1;
  bool autocvar_hud_panel_centerprint_flip;
  float autocvar_hud_panel_centerprint_fontscale = 1;
  float autocvar_hud_panel_centerprint_fontscale_bold = 1.4;
@@@ -18,14 -18,11 +18,15 @@@ float autocvar_hud_panel_centerprint_fo
  bool autocvar_hud_panel_centerprint_dynamichud = true;
  float autocvar_hud_panel_centerprint_time;
  
 +bool autocvar_hud_panel_centerprint_medals = true;
 +int autocvar_hud_panel_centerprint_medals_max = 5;
 +
  void centerprint_Add(int new_id, string strMessage, float duration, int countdown_num);
  void centerprint_AddStandard(string strMessage);
  void centerprint_Kill(int id);
  void centerprint_KillAll();
  
+ void centerprint_SetDuelTitle(string left, string right, string div);
+ void centerprint_SetTitle(string title);
  void centerprint_ClearTitle();
- void centerprint_SetTitle(string title, float offset);
 +void centerprint_Medal(string icon, int times);
diff --combined qcsrc/client/main.qc
index 27560696b35e97f5e290b2284d49914f0a9c292f,8bd5d78638c1fc607fd379d7d1b20d82d0b52ca9..9def65d8bbd2bd8b65d05c6ed7da630afc131a74
@@@ -58,8 -58,6 +58,6 @@@ void CSQC_Init(
                maxclients = i;
        }
  
-       ReplicateVars(REPLICATEVARS_SEND_ALL);
        // needs to be done so early because of the constants they create
        static_init();
        static_init_late();
@@@ -98,9 -96,6 +96,9 @@@
        registercvar("cl_weapon_switch_fallback_to_impulse", "1");
  
        registercvar("cl_allow_uidranking", "1");
 +      
 +      // z411
 +      registercvar("cl_chat_sounds", "1");
  
        if(autocvar_cl_lockview)
                cvar_set("cl_lockview", "0");
@@@ -442,6 -437,9 +440,9 @@@ void PostInit(
  
        TrueAim_Init();
  
+       // this can't be called in CSQC_Init as it'd send cvars too early
+       ReplicateVars_Start();
        postinit = true;
  }
  
@@@ -764,8 -762,6 +765,8 @@@ NET_HANDLE(ENT_CLIENT_RANDOMSEED, bool 
  NET_HANDLE(ENT_CLIENT_ACCURACY, bool isnew)
  {
        make_pure(this);
 +      float entnum = ReadByte();
 +      
        int sf = ReadInt24_t();
        if (sf == 0) {
                for (int w = 0; w <= WEP_LAST - WEP_FIRST; ++w)
        int f = 1;
        for (int w = 0; w <= WEP_LAST - WEP_FIRST; ++w) {
                if (sf & f) {
 -                      int b = ReadByte();
 -                      if (b == 0)
 -                              weapon_accuracy[w] = -1;
 -                      else if (b == 255)
 -                              weapon_accuracy[w] = 1.0; // no better error handling yet, sorry
 -                      else
 -                              weapon_accuracy[w] = (b - 1.0) / 100.0;
 +                      if(entnum > 0) {
 +                              playerslots[entnum-1].accuracy_frags[w] = ReadByte();
 +                              playerslots[entnum-1].accuracy_hit[w] = ReadShort();
 +                              playerslots[entnum-1].accuracy_cnt_hit[w] = ReadShort();
 +                              playerslots[entnum-1].accuracy_cnt_fired[w] = ReadShort();
 +                              
 +                              //LOG_INFOF("Duel stats ?/%d", playerslots[entnum-1].accuracy_cnt_fired[w]);
 +                      } else {
 +                              int b = ReadByte();
 +                              if (b == 0)
 +                                      weapon_accuracy[w] = -1;
 +                              else if (b == 255)
 +                                      weapon_accuracy[w] = 1.0; // no better error handling yet, sorry
 +                              else
 +                                      weapon_accuracy[w] = (b - 1.0) / 100.0;
 +                      }
                }
                f = (f == 0x800000) ? 1 : f * 2;
        }
@@@ -1096,11 -1083,6 +1097,11 @@@ NET_HANDLE(ENT_CLIENT_INIT, bool isnew
        serverflags = ReadByte();
  
        g_trueaim_minrange = ReadCoord();
 +      
 +      strcpy(hostname_full, ReadString());
 +      strcpy(motd_permanent, ReadString());
 +      
 +      sv_timer_countdown = ReadByte();
  
        return = true;
  
        if (!postinit) PostInit();
  }
  
 +NET_HANDLE(TE_CSQC_TEAMNAMES, bool isNew)
 +{
 +      teamname_red = strzone(ReadString());
 +      teamname_blue = strzone(ReadString());
 +      teamname_yellow = strzone(ReadString());
 +      teamname_pink = strzone(ReadString());
 +
 +      return = true;
 +}
 +
 +NET_HANDLE(TE_CSQC_CHATSOUND, bool isNew)
 +{
 +      string snd = ReadString();
 +      snd = strcat("chat/", snd, ".ogg");
 +      
 +      precache_sound(snd);
 +      _sound(NULL, CH_INFO, snd, VOL_BASE, ATTN_NONE);
 +
 +      return = true;
 +}
 +
  float GetSpeedUnitFactor(int speed_unit)
  {
        switch(speed_unit)
diff --combined qcsrc/server/client.qc
index 4bd078e02b4eef6bd4effd33e434deba54417a9a,cab642e67fd9b0f8e65a3eafedb54800e8cc3927..a7ac519af1628d1daeb60a4cd7e6f299436a9158
@@@ -98,16 -98,6 +98,16 @@@ void send_CSQC_teamnagger() 
        WriteHeader(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
  }
  
 +void send_TeamNames(int channel, entity to) {
 +      msg_entity = to;
 +      
 +      WriteHeader(channel, TE_CSQC_TEAMNAMES);
 +      WriteString(channel, autocvar_g_teamnames_red);
 +      WriteString(channel, autocvar_g_teamnames_blue);
 +      WriteString(channel, autocvar_g_teamnames_yellow);
 +      WriteString(channel, autocvar_g_teamnames_pink);
 +}
 +
  int CountSpectators(entity player, entity to)
  {
        if(!player) { return 0; } // not sure how, but best to be safe
@@@ -147,7 -137,8 +147,8 @@@ bool ClientData_Send(entity this, entit
        if (CS(e).race_completed)       sf |= BIT(0); // forced scoreboard
        if (CS(to).spectatee_status)    sf |= BIT(1); // spectator ent number follows
        if (CS(e).zoomstate)            sf |= BIT(2); // zoomed
-       if (autocvar_sv_showspectators) sf |= BIT(4); // show spectators
+       if (autocvar_sv_showspectators == 1 || (autocvar_sv_showspectators && IS_SPEC(to)))
+                                       sf |= BIT(4); // show spectators
  
        WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
        WriteByte(MSG_ENTITY, sf);
@@@ -410,7 -401,7 +411,7 @@@ void PutObserverInServer(entity this, b
        }
        else
        {
-               SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR);
+               SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR); // clears scores too in game modes without teams
                this.frags = FRAGS_SPECTATOR;
        }
  
  
        if (CS(this).just_joined)
                CS(this).just_joined = false;
 +      
 +      // for RJZ
 +      if (autocvar_rjz_count_shards)
 +              send_TotalShards(this);
  }
  
  int player_getspecies(entity this)
@@@ -546,36 -533,6 +547,36 @@@ void FixPlayermodel(entity player
                                setcolor(player, stof(autocvar_sv_defaultplayercolors));
  }
  
 +void ResetPlayerResources(entity this)
 +{
 +      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 {
 +              SetResource(this, RES_SHELLS, start_ammo_shells);
 +              SetResource(this, RES_BULLETS, start_ammo_nails);
 +              SetResource(this, RES_ROCKETS, start_ammo_rockets);
 +              SetResource(this, RES_CELLS, start_ammo_cells);
 +              SetResource(this, RES_PLASMA, start_ammo_plasma);
 +              SetResource(this, RES_FUEL, start_ammo_fuel);
 +              SetResource(this, RES_HEALTH, start_health);
 +              SetResource(this, RES_ARMOR, start_armorvalue);
 +              STAT(WEAPONS, this) = start_weapons;
 +              if (MUTATOR_CALLHOOK(ForbidRandomStartWeapons, this) == false)
 +              {
 +                      GiveRandomWeapons(this, random_start_weapons_count,
 +                              autocvar_g_random_start_weapons, random_start_ammo);
 +              }
 +      }
 +}
 +
  void PutPlayerInServer(entity this)
  {
        if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
        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 {
 -              SetResource(this, RES_SHELLS, start_ammo_shells);
 -              SetResource(this, RES_BULLETS, start_ammo_nails);
 -              SetResource(this, RES_ROCKETS, start_ammo_rockets);
 -              SetResource(this, RES_CELLS, start_ammo_cells);
 -              SetResource(this, RES_PLASMA, start_ammo_plasma);
 -              SetResource(this, RES_FUEL, start_ammo_fuel);
 -              SetResource(this, RES_HEALTH, start_health);
 -              SetResource(this, RES_ARMOR, start_armorvalue);
 -              STAT(WEAPONS, this) = start_weapons;
 -              if (MUTATOR_CALLHOOK(ForbidRandomStartWeapons, this) == false)
 -              {
 -                      GiveRandomWeapons(this, random_start_weapons_count,
 -                              autocvar_g_random_start_weapons, random_start_ammo);
 -              }
 -      }
 +      ResetPlayerResources(this);
 +      
        SetSpectatee_status(this, 0);
  
        PS(this).dual_weapons = '0 0 0';
        Unfreeze(this, false);
  
        MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
        {
                string s = spot.target;
                if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack
@@@ -854,9 -834,7 +854,9 @@@ void PutClientInServer(entity this
        } else if (IS_PLAYER(this)) {
                PutPlayerInServer(this);
        }
 -
 +      // send team names
 +      if(teamplay && IS_REAL_CLIENT(this))
 +              send_TeamNames(MSG_ONE, this);
        bot_relinkplayerlist();
  }
  
@@@ -896,13 -874,6 +896,13 @@@ void ClientInit_misc(entity this
        WriteByte(channel, this.cnt * 255.0); // g_balance_damagepush_speedfactor
        WriteByte(channel, serverflags);
        WriteCoord(channel, autocvar_g_trueaim_minrange);
 +      
 +      // z411 send full hostname
 +      WriteString(channel, (autocvar_hostname_full != "" ? autocvar_hostname_full : autocvar_hostname));
 +      WriteString(channel, autocvar_sv_motd_permanent);
 +      
 +      // z411 send client countdown type
 +      WriteByte(channel, autocvar_sv_timer_countdown);
  }
  
  void ClientInit_CheckUpdate(entity this)
@@@ -1134,12 -1105,8 +1134,12 @@@ void ClientConnect(entity this
        else
                CS(this).allowed_timeouts = autocvar_sv_timeout_number;
  
 -      if (autocvar_sv_eventlog)
 +      if (autocvar_sv_eventlog) {
                GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this.netname, this.team, false)));
 +              
 +              /* z411 for RJZ */
 +              if(autocvar_rjz_ranks) GameLogEcho(strcat(":idfp:", ftos(etof(this)), ":", this.crypto_idfp));
 +      }
  
        CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
  
@@@ -1986,6 -1953,9 +1986,9 @@@ bool ShowTeamSelection(entity this
  }
  void Join(entity this)
  {
+       if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
+               ReadyRestart(true);
        TRANSMUTE(Player, this);
  
        if(!this.team_selected)
        else
                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
        this.team_selected = false;
 +      
 +      // z411
 +      // send constant ready notification
 +      if(warmup_stage)
 +              Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MISSING_READY);
  }
  
  int GetPlayerLimit()
@@@ -2150,13 -2115,12 +2153,13 @@@ bool PlayerThink(entity this
                return false;
        }
  
 -      if (timeout_status == TIMEOUT_ACTIVE) {
 -              // don't allow the player to turn around while game is paused
 +      if (game_timeout) {
 +        // don't allow the player to turn around while game is paused
                // FIXME turn this into CSQC stuff
                this.v_angle = this.lastV_angle;
                this.angles = this.lastV_angle;
                this.fixangle = true;
 +              return false;
        }
  
        if (frametime) player_powerups(this);
@@@ -2576,7 -2540,6 +2579,6 @@@ void PlayerPreThink (entity this
                        || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
                                && (!teamplay || autocvar_g_balance_teams)))
                {
-                       campaign_bots_may_start = true;
                        if(joinAllowed(this))
                                Join(this);
                        return;
index 2a03697479f8d0fb1726c7a5a5671c41078b52fa,01758efe5e4630e357962b9e9b65b330cb740480..acf4e4eb63d408d865c70f828ff0142e6085f233
@@@ -11,6 -11,7 +11,7 @@@
  #include <common/stats.qh>
  #include <common/util.qh>
  #include <common/weapons/_all.qh>
+ #include <server/campaign.qh>
  #include <server/client.qh>
  #include <server/command/banning.qh>
  #include <server/command/common.qh>
@@@ -219,10 -220,6 +220,10 @@@ void VoteCount(float first_count
  
        // add up all the votes from each connected client
        FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_CLIENT(it), {
 +              // z411
 +              if(vote_target_type == VOTE_TARGET_TEAM && it.team != vote_caller.team) continue;
 +              if(vote_target_type == VOTE_TARGET_SINGLE && it != vote_target) continue;
 +              
                ++vote_player_count;
                if (IS_PLAYER(it))   ++vote_real_player_count;
                switch (it.vote_selection)
@@@ -356,12 -353,6 +357,12 @@@ void reset_map(bool dorespawn, bool is_
                if (round_handler_IsActive())
                        round_handler_Reset(game_starttime);
        }
 +      
 +      // for RJZ
 +      if (autocvar_rjz_count_shards) {
 +              total_shards = 0;
 +              send_TotalShardsAll();
 +      }
  
        if (shuffleteams_on_reset_map)
        {
@@@ -449,6 -440,8 +450,8 @@@ void ReadyRestart_force(bool is_fake_ro
  
        if(warmup_stage)
                game_starttime = time; // Warmup: No countdown in warmup
+       else if (autocvar_g_campaign)
+               game_starttime = time + 3;
        else
                game_starttime = time + RESTART_COUNTDOWN; // Go into match mode
  
                localcmd("\nsv_hook_warmupend\n");
  
        // reset the .ready status of all players (also spectators)
 -      FOREACH_CLIENT(IS_REAL_CLIENT(it), { it.ready = false; });
 +      FOREACH_CLIENT(IS_REAL_CLIENT(it), {
 +              it.ready = false;
 +              Kill_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CPID_MISSING_READY);
 +      });
        readycount = 0;
        Nagger_ReadyCounted();  // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client
  
                FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), { CS(it).allowed_timeouts = autocvar_sv_timeout_number; });
        }
  
 +      round_handler_Activate(!warmup_stage);
        if (!sv_ready_restart_after_countdown || warmup_stage)
                reset_map(true, is_fake_round_start);
  
@@@ -500,7 -489,7 +503,7 @@@ void ReadyRestart(bool forceWarmupEnd
        if (MUTATOR_CALLHOOK(ReadyRestart_Deny) || intermission_running || race_completing) localcmd("restart\n");
        else localcmd("\nsv_hook_readyrestart\n");
  
-       if(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
@@@ -706,8 -695,6 +709,8 @@@ int VoteCommand_parse(entity caller, st
                case MUT_VOTEPARSE_UNACCEPTABLE: { return 0; }
        }
  
 +      vote_target_type = VOTE_TARGET_ALL;
 +      
        switch (first_command) // now go through and parse the proper commands to adjust as needed.
        {
                case "kick":
                                if (first_command == "kickban")
                                        command_arguments = strcat(ftos(autocvar_g_ban_default_bantime), " ", ftos(autocvar_g_ban_default_masksize), " ~");
  
 -                              vote_parsed_command = strcat(first_command, " # ", ftos(etof(victim)), " ", command_arguments);
 +                              if (first_command == "kick") // z411 : Use our kick implementation - kind of hacky...
 +                                      vote_parsed_command = strcat("defer 2 \"sv_cmd kickkick # ", ftos(etof(victim)), " ", command_arguments, "\"");
 +                              else
 +                                      vote_parsed_command = strcat("defer 2 \"", first_command, " # ", ftos(etof(victim)), " ", command_arguments, "\"");
 +                              
                                vote_parsed_display = sprintf("^1%s #%d ^7%s^1 %s", first_command, etof(victim), victim.netname, reason);
                        }
                        else
                {
                        vote_command = ValidateMap(argv(startpos + 1), caller);
                        if (!vote_command)  return -1;
 -                      vote_parsed_command = strcat("gotomap ", vote_command);
 -                      vote_parsed_display = strzone(strcat("^1", vote_parsed_command));
 +                      vote_parsed_command = strcat("defer 2 \"gotomap ", vote_command, "\"");
 +                      vote_parsed_display = strzone(strcat("^1gotomap ", vote_command));
  
                        break;
                }
 +              
 +              // z411 team calls
 +              case "teamname":
 +              {
 +                      if (teamplay && Team_IsValidTeam(caller.team)) {
 +                              vote_target_type = VOTE_TARGET_TEAM;
 +                      
 +                              string tmname = strtolower(Static_Team_ColorName(caller.team));
 +                              string newname = argv(startpos + 1);
 +                              
 +                              vote_parsed_command = strcat(first_command, " ", tmname, " \"", newname, "\"");
 +                              vote_parsed_display = strzone(strcat("^3(Team) ^1", first_command, " ^2", newname));
 +                      } else { print_to(caller, "vcall: Not in a team\n"); return 0; }
 +                      
 +                      break;
 +              }
  
                // TODO: replicate the old behaviour of being able to vote for maps from different modes on multimode servers (possibly support it in gotomap too)
                // maybe fallback instead of aborting if map name is invalid?
                        break;
                }
  
 -              case "restart":
 +              case "gg":
 +              case "shuffleteams":
 +              case "endmatch":
                {
                        // add a delay so that vote result can be seen and announcer can be heard
                        // if the vote is accepted
 -                      vote_parsed_command = strcat("defer 1 ", vote_command);
 +                      vote_parsed_command = strcat("defer 2 ", vote_command);
 +                      vote_parsed_display = strzone(strcat("^1", vote_command));
 +                      
 +                      break;
 +              }
 +
 +              case "reset":
 +              case "restart": // re-direct all match restarting to resetmatch
 +                      vote_command = "resetmatch"; // fall-through
 +              case "resetmatch":
 +              {
 +                      vote_parsed_command = strcat("defer 2 ", vote_command);
                        vote_parsed_display = strzone(strcat("^1", vote_command));
  
                        break;
                                return -1;
                        }
  
 -                      vote_parsed_command = vote_command;
 +                      vote_parsed_command = strcat("defer 2 ", vote_command);
                        vote_parsed_display = strzone(strcat("^1", vote_command));
                        break;
                }
diff --combined qcsrc/server/teamplay.qc
index 88ec63c706635b79d2aa6328f977f0d5ef4e3c46,80892e652891804e7fa6f83ff2f4cd623dcdd840..2a81de1efbda26458544e44a34a16399192ae327
@@@ -31,7 -31,6 +31,7 @@@ const int TEAM_NOT_ALLOWED = -1
  .int m_team_balance_state; ///< Holds the state of the team balance entity.
  .entity m_team_balance_team[NUM_TEAMS]; ///< ???
  
 +.string m_team_name; // z411 team name
  .float m_team_score; ///< The score of the team.
  .int m_num_players; ///< Number of players (both humans and bots) in a team.
  .int m_num_bots; ///< Number of bots in a team.
@@@ -73,16 -72,6 +73,16 @@@ entity Team_GetTeam(int team_num
        return g_team_entities[Team_TeamToIndex(team_num) - 1];
  }
  
 +string Team_GetTeamName(entity team_ent)
 +{
 +      return team_ent.m_team_name;
 +}
 +
 +void Team_SetTeamName(entity team_ent, string name)
 +{
 +      team_ent.m_team_name = name;
 +}
 +
  float Team_GetTeamScore(entity team_ent)
  {
        return team_ent.m_team_score;
@@@ -103,16 -92,6 +103,16 @@@ void Team_SetNumberOfAlivePlayers(entit
        team_ent.m_num_players_alive = number;
  }
  
 +int Team_GetNumberOfPlayers(entity team_ent)
 +{
 +      return team_ent.m_num_players;
 +}
 +
 +void Team_SetNumberOfPlayers(entity team_ent, int number)
 +{
 +      team_ent.m_num_players = number;
 +}
 +
  int Team_GetWinnerAliveTeam()
  {
        int winner = 0;
@@@ -267,7 -246,7 +267,7 @@@ bool SetPlayerTeam(entity player, int t
        if (team_index != old_team_index)
        {
                KillPlayerForTeamChange(player);
-               PlayerScore_Clear(player);
+               PlayerScore_Clear(player); // works only in game modes without teams
                CS(player).parm_idlesince = time;
  
                if (!IS_BOT_CLIENT(player))
diff --combined qcsrc/server/world.qc
index 35d2c9029987b167c8f1aac77bd2bb17ffdd1ae9,fca7c1741497e7a092245ad376744a1b3e4b630a..101fc633a84c01ddaf1f4fb99e5765677fba912e
@@@ -101,27 -101,6 +101,27 @@@ void PingPLReport_Spawn(
  
  const float SPAWNFLAG_NO_WAYPOINTS_FOR_ITEMS = 1;
  
 +void send_TotalShards(entity to) {
 +      // for RJZ
 +      // Send total number of picked up shards
 +      if(!autocvar_rjz_count_shards) return;
 +      if(!IS_REAL_CLIENT(to)) return;
 +      
 +      msg_entity = to;
 +      WriteHeader(MSG_ONE, TE_CSQC_TOTALSHARDS);
 +      WriteInt24_t(MSG_ONE, total_shards);
 +}
 +
 +void send_TotalShardsAll() {
 +      // for RJZ
 +      // Send total number of picked up shards
 +      if(!autocvar_rjz_count_shards) return;
 +      
 +      FOREACH_CLIENT(IS_REAL_CLIENT(it) && (IS_SPEC(it) || IS_OBSERVER(it)), {
 +              send_TotalShards(it);
 +      });
 +}
 +
  void SetDefaultAlpha()
  {
        if (!MUTATOR_CALLHOOK(SetDefaultAlpha))
@@@ -308,7 -287,6 +308,7 @@@ void cvar_changes_init(
                BADCVAR("g_keyhunt");
                BADCVAR("g_keyhunt_teams");
                BADCVAR("g_lms");
 +              BADCVAR("g_mayhem");
                BADCVAR("g_nexball");
                BADCVAR("g_onslaught");
                BADCVAR("g_race");
                BADCVAR("g_tdm");
                BADCVAR("g_tdm_on_dm_maps");
                BADCVAR("g_tdm_teams");
 +              BADCVAR("g_mmm");
 +              BADCVAR("g_mmm_not_dm_maps");
 +              BADCVAR("g_tmayhem");
 +              BADCVAR("g_tmayhem_teams");
                BADCVAR("g_vip");
                BADCVAR("leadlimit");
                BADCVAR("nextmap");
                BADCVAR("g_mapinfo_ignore_warnings");
                BADCVAR("g_maplist_ignore_sizes");
                BADCVAR("g_maplist_sizes_count_bots");
 +              //LegendGuard adds BADCVAR(g_*) for MMM 20-02-2021
  
                // long
                BADCVAR("hostname");
 +              BADCVAR("hostname_full");
                BADCVAR("g_maplist");
                BADCVAR("g_maplist_mostrecent");
                BADCVAR("sv_motd");
 +              BADCVAR("sv_motd_permanent");
 +              
 +              BADPREFIX("g_teamnames_");
  
                v = cvar_string(k);
                d = cvar_defstring(k);
                BADCVAR("g_forced_respawn");
                BADCVAR("g_freezetag_point_leadlimit");
                BADCVAR("g_freezetag_point_limit");
 +              BADCVAR("g_freezetag_revive_respawn");
 +              BADCVAR("g_freezetag_round_stop");
 +              BADCVAR("g_freezetag_round_respawn");
                BADCVAR("g_glowtrails");
                BADCVAR("g_hats");
                BADCVAR("g_casings");
                BADCVAR("g_spawn_alloweffects");
                BADCVAR("g_tdm_point_leadlimit");
                BADCVAR("g_tdm_point_limit");
 +              BADCVAR("g_mayhem_fraglimit");
 +              BADCVAR("g_tmayhem_fraglimit");
 +              BADCVAR("g_mayhem_visual_score_limit");
 +              BADCVAR("g_tmayhem_visual_score_limit");
 +              BADCVAR("g_tmayhem_score_leadlimit");
                BADCVAR("leadlimit_and_fraglimit");
                BADCVAR("leadlimit_override");
 +              BADCVAR("g_mayhem_scoringmethod");
 +              BADCVAR("g_mayhem_scoringmethod_damage_weight");
 +              BADCVAR("g_mayhem_scoringmethod_frag_weight");
 +              BADCVAR("g_tmayhem_scoringmethod");
 +              BADCVAR("g_tmayhem_scoringmethod_damage_weight");
 +              BADCVAR("g_tmayhem_scoringmethod_frag_weight");
                BADCVAR("pausable");
                BADCVAR("sv_announcer");
                BADCVAR("sv_checkforpacketsduringsleep");
                BADPREFIX("sv_timeout_");
                BADPREFIX("sv_vote_");
                BADPREFIX("timelimit_");
 +              BADPREFIX("sv_chat_");
 +              BADPREFIX("sv_jingle_");
  
                // allowed changes to server admins (please sync this to server.cfg)
                // vi commands:
                BADCVAR("g_keyhunt_point_limit");
                BADCVAR("g_keyhunt_teams_override");
                BADCVAR("g_lms_lives_override");
 +              BADCVAR("g_mayhem_powerups");
                BADCVAR("g_maplist");
                BADCVAR("g_maxplayers");
                BADCVAR("g_mirrordamage");
                BADCVAR("g_start_delay");
                BADCVAR("g_superspectate");
                BADCVAR("g_tdm_teams_override");
 +              BADCVAR("g_tmayhem_teams_override");
 +              BADCVAR("g_tmayhem_powerups");
                BADCVAR("g_warmup");
                BADCVAR("g_weapon_stay"); BADPRESUFFIX("g_", "_weapon_stay");
                BADCVAR("hostname");
                BADCVAR("sv_motd");
                BADCVAR("sv_public");
                BADCVAR("sv_showfps");
+               BADCVAR("sv_showspectators");
                BADCVAR("sv_status_privacy");
                BADCVAR("sv_taunt");
                BADCVAR("sv_vote_call");
                BADCVAR("g_ca_weaponarena");
                BADCVAR("g_freezetag_weaponarena");
                BADCVAR("g_lms_weaponarena");
 +              BADCVAR("g_mayhem_weaponarena");
 +              BADCVAR("g_tmayhem_weaponarena");
                BADCVAR("g_ctf_stalemate_time");
  
  #undef BADPRESUFFIX
@@@ -872,11 -821,11 +873,11 @@@ spawnfunc(worldspawn
  
        if(autocvar_g_campaign)
                CampaignPreInit();
+       else
+               PlayerStats_GameReport_Init(); // we need this to be initiated before InitGameplayMode
  
        Map_MarkAsRecent(mapname);
  
-       PlayerStats_GameReport_Init(); // we need this to be initiated before InitGameplayMode
        InitGameplayMode();
        static_init_late();
        static_init_precache();
  
        WinningConditionHelper(this); // set worldstatus
  
 +      round_handler_Activate(!warmup_stage);
 +      
 +      // for RJZ
 +      if (autocvar_rjz_count_shards && warmup_stage) {
 +              total_shards = -2;
 +              send_TotalShardsAll();
 +      }
 +      
        world_initialized = 1;
        __spawnfunc_spawn_all();
  }
@@@ -1353,8 -1294,6 +1354,8 @@@ void NextLevel(
        */
  
        //pos = FindIntermission ();
 +      
 +      sound(NULL, CH_INFO, SND_ENDMATCH, VOL_BASE, ATTN_NONE);
  
        VoteReset();
  
        WeaponStats_Shutdown();
  
        Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_Null); // kill all centerprints now
 +      
 +      // send winner notification
 +      if(teamplay) {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_ANNCE, APP_TEAM_NUM(WinningConditionHelper_winnerteam, ANNCE_TEAM_WINS));
 +      }
  
        if(autocvar_sv_eventlog)
                GameLogEcho(":gameover");
        });
  
        target_music_kill();
 +      
 +      // z411
 +      if(autocvar_sv_jingle_end) {
 +              int jingles_len = 0;
 +              string jingles[32];
 +              jingles[0] = "";
 +              
 +              FOREACH_WORD(autocvar_sv_jingle_end_list, it,
 +              {
 +                      jingles[jingles_len] = it;
 +                      jingles_len++;
 +              });
 +              
 +              if(jingles_len) {
 +                      int song_to_play = rint(random() * (jingles_len - 1));
 +              
 +                      FOREACH_CLIENT(IS_REAL_CLIENT(it),
 +                      {
 +                              stuffcmd(it, "cd stop\n");
 +                              _sound(it, CH_INFO, strcat("jingle/", jingles[song_to_play], ".ogg"), VOL_BASE * autocvar_sv_jingle_end_volume, ATTEN_NORM);
 +                      });
 +              }
 +      }
  
        if(autocvar_g_campaign)
                CampaignPreIntermission();
@@@ -1463,12 -1374,6 +1464,12 @@@ void InitiateOvertime() // ONLY call th
        //add one more overtime by simply extending the timelimit
        cvar_set("timelimit", ftos(autocvar_timelimit + autocvar_timelimit_overtime));
        Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_OVERTIME_TIME, autocvar_timelimit_overtime * 60);
 +      
 +      sound(NULL, CH_INFO, SND_OVERTIME, VOL_BASE, ATTN_NONE);
 +      if(checkrules_overtimesadded == 1) {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_ANNCE, ANNCE_OVERTIME);
 +              overtime_starttime = time;
 +      }
  }
  
  float GetWinningCode(float fraglimitreached, float equality)
@@@ -1520,49 -1425,7 +1521,49 @@@ void ClearWinners(
        FOREACH_CLIENT(IS_PLAYER(it) || INGAME(it), { it.winning = 0; });
  }
  
 -int fragsleft_last;
 +void AnnounceNewLeader()
 +{
 +      if(teamplay) {
 +              if (WinningConditionHelper_equality)
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_ANNCE, ANNCE_TEAM_LEADS_TIED);
 +              else
 +                      FOREACH_CLIENT(IS_PLAYER(it), {
 +                              if(it.team == WinningConditionHelper_winnerteam)
 +                                      Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_TEAM_LEADS_TEAM);
 +                              else
 +                                      Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_TEAM_LEADS_ENEMY);
 +                      });
 +                      Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(WinningConditionHelper_winnerteam, ANNCE_TEAM_LEADS));
 +      } else {
 +              if (WinningConditionHelper_equality)
 +              {
 +                      Send_Notification(NOTIF_ONE, WinningConditionHelper_equality_one, MSG_ANNCE, ANNCE_LEAD_TIED);
 +                      Send_Notification(NOTIF_ONE, WinningConditionHelper_equality_two, MSG_ANNCE, ANNCE_LEAD_TIED);
 +              }
 +              else
 +              {
 +                      Send_Notification(NOTIF_ONE, WinningConditionHelper_winner, MSG_ANNCE, ANNCE_LEAD_GAINED);
 +                      Send_Notification(NOTIF_ONE, WinningConditionHelper_second, MSG_ANNCE, ANNCE_LEAD_LOST);
 +              }
 +      }
 +}
 +
 +void AnnounceScores(float tm)
 +{
 +      WinningConditionHelper(NULL);
 +      if (Score_NewLeader()) {
 +              AnnounceNewLeader();
 +      } else {
 +              FOREACH_CLIENT(IS_PLAYER(it), {
 +                      if(it.team == tm)
 +                              Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_TEAM_SCORES_TEAM);
 +                      else
 +                              Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_TEAM_SCORES_ENEMY);
 +              });
 +              Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(tm, ANNCE_TEAM_SCORES));
 +      }
 +}
 +
  float WinningCondition_Scores(float limit, float leadlimit)
  {
        // TODO make everything use THIS winning condition (except LMS)
  
        if(MUTATOR_CALLHOOK(Scores_CountFragsRemaining))
        {
 -              float fragsleft;
                if (checkrules_suddendeathend && time >= checkrules_suddendeathend)
                {
                        fragsleft = 1;
                        fragsleft_last = fragsleft;
                }
        }
 -
 +      
 +      // z411 - lead announcer
 +      if(MUTATOR_CALLHOOK(Scores_AnnounceLeads)) {
 +              if (Score_NewLeader())
 +                      AnnounceNewLeader();
 +      }
 +      
        bool fraglimit_reached = (limit && WinningConditionHelper_topscore >= limit);
        bool leadlimit_reached = (leadlimit && WinningConditionHelper_topscore - WinningConditionHelper_secondscore >= leadlimit);
  
@@@ -1748,9 -1606,6 +1749,9 @@@ void CheckRules_World(
                        // again, but this shouldn't hurt
                return;
        }
 +      
 +      // z411 don't check rules if we're in a timeout
 +      if (game_timeout) return;
  
        float timelimit = autocvar_timelimit * 60;
        float fraglimit = autocvar_fraglimit;
                                Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_RACE_FINISHLAP);
                        else
                                Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_OVERTIME_FRAG);
 +                      checkrules_overtimesadded = 1;
 +                      sound(NULL, CH_INFO, SND_OVERTIME, VOL_BASE, ATTN_NONE);
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_ANNCE, ANNCE_OVERTIME);
                }
        }
        else
@@@ -2223,8 -2075,6 +2224,8 @@@ void readlevelcvars(
  {
        if(cvar("sv_allow_fullbright"))
                serverflags |= SERVERFLAG_ALLOW_FULLBRIGHT;
 +      if(cvar("sv_forbid_pickuptimer"))
 +              serverflags |= SERVERFLAG_FORBID_PICKUPTIMER;
  
        sv_ready_restart_after_countdown = cvar("sv_ready_restart_after_countdown");
  
  
      MUTATOR_CALLHOOK(ReadLevelCvars);
  
-       if (!warmup_stage)
+       if (!warmup_stage && !autocvar_g_campaign)
                game_starttime = time + cvar("g_start_delay");
  
        FOREACH(Weapons, it != WEP_Null, { it.wr_init(it); });
@@@ -2418,7 -2268,7 +2419,7 @@@ void RunThink(entity this, float dt
  bool autocvar_sv_freezenonclients;
  void Physics_Frame()
  {
 -      if(autocvar_sv_freezenonclients)
 +      if(autocvar_sv_freezenonclients || game_timeout)
                return;
  
        IL_EACH(g_moveables, true,