]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Resolve conflicts 1: Merge commit 'c58baab5' into bones_was_here/q3compat
authorbones_was_here <bones_was_here@xa.org.au>
Sun, 11 Jul 2021 10:21:33 +0000 (20:21 +1000)
committerbones_was_here <bones_was_here@xa.org.au>
Sun, 11 Jul 2021 10:21:33 +0000 (20:21 +1000)
19 files changed:
1  2 
qcsrc/common/items/item.qh
qcsrc/common/mutators/mutator/buffs/all.inc
qcsrc/common/mutators/mutator/buffs/buffs.qh
qcsrc/common/mutators/mutator/buffs/sv_buffs.qc
qcsrc/common/mutators/mutator/buffs/sv_buffs.qh
qcsrc/common/stats.qh
qcsrc/common/weapons/all.qc
qcsrc/lib/spawnfunc.qh
qcsrc/lib/warpzone/common.qc
qcsrc/server/client.qc
qcsrc/server/compat/quake3.qc
qcsrc/server/items/items.qc
qcsrc/server/items/items.qh
qcsrc/server/main.qc
qcsrc/server/race.qc
qcsrc/server/teamplay.qc
qcsrc/server/teamplay.qh
qcsrc/server/world.qc
xonotic-server.cfg

index 10c1bbc99d2e64358f493d82f6c8a7f55ecfaede,8f651ad049bb19fd8d44591af923f9bbbc95357c..5873c5831847961616cf40ed4dbcc1093f0e28a7
@@@ -63,24 -63,29 +63,25 @@@ const int ITS_GLOW              = BIT(6
  #ifdef SVQC
  .float strength_finished; // NOTE: this field is used only by map entities, it does not directly apply the strength stat
  .float invincible_finished; // ditto
+ .float buffs_finished; // ditts
  
 -#define spawnfunc_body(item) \
 -      if (!Item_IsDefinitionAllowed(item)) \
 +#define SPAWNFUNC_BODY(item) \
 +      if (item && Item_IsDefinitionAllowed(item)) \
 +              StartItem(this, item); \
 +      else \
        { \
                startitem_failed = true; \
                delete(this); \
 -              return; \
 -      } \
 -      StartItem(this, item)
 +      }
  
  #define SPAWNFUNC_ITEM(name, item) \
        spawnfunc(name) \
        { \
 -              spawnfunc_body(item); \
 +              SPAWNFUNC_BODY(item) \
        }
  
  #define SPAWNFUNC_ITEM_COND(name, cond, item1, item2) \
 -      spawnfunc(name) \
 -      { \
 -              entity item = (cond) ? item1 : item2; \
 -              spawnfunc_body(item); \
 -      }
 +      SPAWNFUNC_ITEM(name, (cond ? item1 : item2))
  
  #else
  
index 3b3804b84eb5dab7475e7e2e1789e2ed2faa7ea0,46a96f6612940e29ab53ee86a6f713e8d91722c0..3d540eebfdf3c715472f63f792a5e3205a131482
@@@ -2,17 -2,12 +2,17 @@@ string Buff_UndeprecateName(string buff
  {
      switch(buffname)
      {
 -        case "ammoregen": return "ammo";
 -        case "haste": case "scout": return "speed";
 -        case "guard": return "resistance";
 -        case "revival": case "regen": return "medic";
 -        case "invis": return "invisible";
 -        case "jumper": return "jump";
 +        case "ammoregen": return "ammo";              // Q3TA ammoregen
 +        case "haste": return "speed";                 // Q3A haste
 +        case "doubler": return "inferno";             // Q3TA doubler
 +        case "scout": return "bash";                  // Q3TA scout
 +        case "guard": return "resistance";            // Q3TA guard
 +        case "revival": case "regen": return "medic"; // WOP revival, Q3A regen
 +        case "invis": return "invisible";             // Q3A invis
 +        case "jumper": return "jump";                 // WOP jumper
 +        case "invulnerability": return "vampire";     // Q3TA invulnerability
 +        case "kamikaze": return "vengeance";          // Q3TA kamikaze
 +        case "teleporter": return "swapper";          // Q3A personal teleporter
          default: return buffname;
      }
  }
  REGISTER_BUFF(AMMO) {
      this.m_name = _("Ammo");
      this.netname = "ammo";
+     this.m_icon = "buff_ammo";
      this.m_skin = 3;
      this.m_color = '0.76 1 0.1';
  }
  BUFF_SPAWNFUNCS(ammo, BUFF_AMMO)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(ammoregen, BUFF_AMMO)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_ammoregen, BUFF_AMMO)
  
  REGISTER_BUFF(RESISTANCE) {
      this.m_name = _("Resistance");
      this.netname = "resistance";
+     this.m_icon = "buff_resistance";
      this.m_skin = 0;
      this.m_color = '0.36 1 0.07';
  }
  BUFF_SPAWNFUNCS(resistance, BUFF_RESISTANCE)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(guard, BUFF_RESISTANCE)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_guard, BUFF_RESISTANCE)
  
  REGISTER_BUFF(SPEED) {
      this.m_name = _("Speed");
      this.netname = "speed";
+     this.m_icon = "buff_speed";
      this.m_skin = 9;
      this.m_color = '0.1 1 0.84';
  }
  BUFF_SPAWNFUNCS(speed, BUFF_SPEED)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(haste, BUFF_SPEED)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(scout, BUFF_SPEED)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_haste, BUFF_SPEED)
  
  REGISTER_BUFF(MEDIC) {
      this.m_name = _("Medic");
      this.netname = "medic";
+     this.m_icon = "buff_medic";
      this.m_skin = 1;
      this.m_color = '1 0.12 0';
  }
  BUFF_SPAWNFUNCS(medic, BUFF_MEDIC)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(regen, BUFF_MEDIC)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(revival, BUFF_MEDIC)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_regen, BUFF_MEDIC)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_revival, BUFF_MEDIC)
  
  REGISTER_BUFF(BASH) {
      this.m_name = _("Bash");
      this.netname = "bash";
+     this.m_icon = "buff_bash";
      this.m_skin = 5;
      this.m_color = '1 0.39 0';
  }
  BUFF_SPAWNFUNCS(bash, BUFF_BASH)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(doubler, BUFF_BASH)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_scout, BUFF_BASH)
  
  REGISTER_BUFF(VAMPIRE) {
      this.m_name = _("Vampire");
      this.netname = "vampire";
+     this.m_icon = "buff_vampire";
      this.m_skin = 2;
      this.m_color = '1 0 0.24';
  }
  BUFF_SPAWNFUNCS(vampire, BUFF_VAMPIRE)
 +BUFF_SPAWNFUNC_Q3COMPAT(holdable_invulnerability, BUFF_VAMPIRE)
  
  REGISTER_BUFF(DISABILITY) {
      this.m_name = _("Disability");
      this.netname = "disability";
+     this.m_icon = "buff_disability";
      this.m_skin = 7;
      this.m_color = '0.94 0.3 1';
  }
@@@ -83,51 -85,54 +90,57 @@@ BUFF_SPAWNFUNCS(disability, BUFF_DISABI
  REGISTER_BUFF(VENGEANCE) {
      this.m_name = _("Vengeance");
      this.netname = "vengeance";
+     this.m_icon = "buff_vengeance";
      this.m_skin = 15;
      this.m_color = '1 0.23 0.61';
  }
  BUFF_SPAWNFUNCS(vengeance, BUFF_VENGEANCE)
 +BUFF_SPAWNFUNC_Q3COMPAT(holdable_kamikaze, BUFF_VENGEANCE)
  
  REGISTER_BUFF(JUMP) {
      this.m_name = _("Jump");
      this.netname = "jump";
+     this.m_icon = "buff_jump";
      this.m_skin = 10;
      this.m_color = '0.24 0.78 1';
  }
  BUFF_SPAWNFUNCS(jump, BUFF_JUMP)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(jumper, BUFF_JUMP)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_jumper, BUFF_JUMP)
  
  REGISTER_BUFF(INVISIBLE) {
      this.m_name = _("Invisible");
      this.netname = "invisible";
+     this.m_icon = "buff_invisible";
      this.m_skin = 12;
      this.m_color = '0.5 0.5 1';
  }
  BUFF_SPAWNFUNCS(invisible, BUFF_INVISIBLE)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(invis, BUFF_INVISIBLE)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_invis, BUFF_INVISIBLE)
  
  REGISTER_BUFF(INFERNO) {
      this.m_name = _("Inferno");
      this.netname = "inferno";
+     this.m_icon = "buff_inferno";
      this.m_skin = 16;
      this.m_color = '1 0.62 0';
  }
  BUFF_SPAWNFUNCS(inferno, BUFF_INFERNO)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_doubler, BUFF_INFERNO)
  
  REGISTER_BUFF(SWAPPER) {
      this.m_name = _("Swapper");
      this.netname = "swapper";
+     this.m_icon = "buff_swapper";
      this.m_skin = 17;
      this.m_color = '0.63 0.36 1';
  }
  BUFF_SPAWNFUNCS(swapper, BUFF_SWAPPER)
 +BUFF_SPAWNFUNC_Q3COMPAT(holdable_teleporter, BUFF_SWAPPER)
  
  REGISTER_BUFF(MAGNET) {
      this.m_name = _("Magnet");
      this.netname = "magnet";
+     this.m_icon = "buff_magnet";
      this.m_skin = 18;
      this.m_color = '1 0.95 0.18';
  }
@@@ -136,6 -141,7 +149,7 @@@ BUFF_SPAWNFUNCS(magnet, BUFF_MAGNET
  REGISTER_BUFF(LUCK) {
      this.m_name = _("Luck");
      this.netname = "luck";
+     this.m_icon = "buff_luck";
      this.m_skin = 19;
      this.m_color = '1 0.23 0.44';
  }
@@@ -144,8 -150,9 +158,9 @@@ BUFF_SPAWNFUNCS(luck, BUFF_LUCK
  REGISTER_BUFF(FLIGHT) {
      this.m_name = _("Flight");
      this.netname = "flight";
+     this.m_icon = "buff_flight";
      this.m_skin = 11;
      this.m_color = '0.23 0.44 1';
  }
  BUFF_SPAWNFUNCS(flight, BUFF_FLIGHT)
 -BUFF_SPAWNFUNC_Q3TA_COMPAT(flight, BUFF_FLIGHT)
 +BUFF_SPAWNFUNC_Q3COMPAT(item_flight, BUFF_FLIGHT)
index f88cda3a24d229ac7902be9cae9f632ef77487ea,5b93fa79595a78fab9e741b81ce1408873d65031..14a8ea01d0f5aced3625820ae4891e9131557289
@@@ -12,24 -12,22 +12,22 @@@ REGISTER_WAYPOINT(Buff, _("Buff"), "", 
  REGISTER_RADARICON(Buff, 1);
  #endif
  
- REGISTRY(Buffs, BITS(5))
- REGISTER_REGISTRY(Buffs)
- REGISTRY_CHECK(Buffs)
  #define REGISTER_BUFF(id) \
-     REGISTER(Buffs, BUFF_##id, m_id, NEW(Buff))
+     REGISTER(StatusEffect, BUFF_##id, m_id, NEW(Buff))
  
- #include <common/items/item/pickup.qh>
- CLASS(Buff, Pickup)
+ #include <common/mutators/mutator/status_effects/_mod.qh>
+ CLASS(Buff, StatusEffects)
        /** bit index */
        ATTRIB(Buff, m_itemid, int, 0);
        ATTRIB(Buff, netname, string, "buff");
+       ATTRIB(Buff, m_icon, string, "buff");
        ATTRIB(Buff, m_color, vector, '1 1 1');
        ATTRIB(Buff, m_name, string, "Buff");
        ATTRIB(Buff, m_skin, int, 0);
+       ATTRIB(Buff, m_lifetime, float, 60);
        ATTRIB(Buff, m_sprite, string, "");
        METHOD(Buff, display, void(entity this, void(string name, string icon) returns)) {
-               returns(this.m_name, sprintf("/gfx/hud/%s/buff_%s", cvar_string("menu_skin"), this.netname));
+               returns(this.m_name, sprintf("/gfx/hud/%s/%s", cvar_string("menu_skin"), this.m_icon));
        }
  #ifdef SVQC
        METHOD(Buff, m_time, float(Buff this))
  ENDCLASS(Buff)
  
  STATIC_INIT(REGISTER_BUFFS) {
-       FOREACH(Buffs, true, {
+       FOREACH(StatusEffect, it.instanceOfBuff, {
                it.m_itemid = BIT(it.m_id - 1);
                it.m_sprite = strzone(strcat("buff-", it.netname));
        });
  }
  
  #ifdef SVQC
+       .entity buffdef;
        void buff_Init(entity ent);
        void buff_Init_Compat(entity ent, entity replacement);
        #define BUFF_SPAWNFUNC(e, b, t) spawnfunc(item_buff_##e) { \
-               STAT(BUFFS, this) = b.m_itemid; \
+               this.buffdef = b; \
 -              this.team = t; \
 +              if(teamplay) \
 +                      this.team_forced = t; \
                buff_Init(this); \
        }
        #define BUFF_SPAWNFUNCS(e, b)                       \
                        BUFF_SPAWNFUNC(e##_team2,   b,  NUM_TEAM_2) \
                        BUFF_SPAWNFUNC(e##_team3,   b,  NUM_TEAM_3) \
                        BUFF_SPAWNFUNC(e##_team4,   b,  NUM_TEAM_4)
 -      #define BUFF_SPAWNFUNC_Q3TA_COMPAT(o, r) spawnfunc(item_##o) { buff_Init_Compat(this, r); }
 +      #define BUFF_SPAWNFUNC_Q3COMPAT(o, r) spawnfunc(o) { buff_Init_Compat(this, r); }
  #else
        #define BUFF_SPAWNFUNC(e, b, t)
        #define BUFF_SPAWNFUNCS(e, b)
 -      #define BUFF_SPAWNFUNC_Q3TA_COMPAT(o, r)
 +      #define BUFF_SPAWNFUNC_Q3COMPAT(o, r)
  #endif
  
  string Buff_UndeprecateName(string buffname);
- entity buff_FirstFromFlags(int _buffs);
  
- REGISTER_BUFF(Null);
- BUFF_SPAWNFUNCS(random, BUFF_Null)
+ BUFF_SPAWNFUNCS(random, NULL)
  
- REGISTRY_DEFINE_GET(Buffs, BUFF_Null)
  #include "all.inc"
index e52ab17592646bb470146eb3bf1be7a04f11b63a,bf680f9a43cb870b8df178fdfb23cdfdfa501d27..2c32b57e421688f56311ae2de16f08325c63c810
@@@ -53,7 -53,7 +53,7 @@@ void buffs_BuffModel_Remove(entity play
  
  vector buff_GlowColor(entity buff)
  {
 -      //if(buff.team) { return Team_ColorRGB(buff.team); }
 +      //if(buff.team_forced) { return Team_ColorRGB(buff.team_forced); }
        return buff.m_color;
  }
  
@@@ -71,12 -71,13 +71,13 @@@ void buff_Effect(entity player, string 
  // buff item
  bool buff_Waypoint_visible_for_player(entity this, entity player, entity view)
  {
-       if(!this.owner.buff_active && !this.owner.buff_activetime)
+       if(!this.owner.buff_active && !this.owner.buff_activetime || !this.owner.buffdef)
                return false;
  
-       if (STAT(BUFFS, view))
+       entity heldbuff = buff_FirstFromFlags(view); // TODO: cache this information so it isn't performing a loop every frame
+       if (heldbuff) 
        {
-               return CS_CVAR(view).cvar_cl_buffs_autoreplace == false || STAT(BUFFS, view) != STAT(BUFFS, this.owner);
+               return CS_CVAR(view).cvar_cl_buffs_autoreplace == false || heldbuff != this.owner.buffdef;
        }
  
        return WaypointSprite_visible_for_player(this, player, view);
@@@ -86,8 -87,8 +87,8 @@@ void buff_Waypoint_Spawn(entity e
  {
        if(autocvar_g_buffs_waypoint_distance <= 0) return;
  
-       entity buff = buff_FirstFromFlags(STAT(BUFFS, e));
+       entity buff = e.buffdef;
 -      entity wp = WaypointSprite_Spawn(WP_Buff, 0, autocvar_g_buffs_waypoint_distance, e, '0 0 1' * e.maxs.z, NULL, e.team, e, buff_waypoint, true, RADARICON_Buff);
 +      entity wp = WaypointSprite_Spawn(WP_Buff, 0, autocvar_g_buffs_waypoint_distance, e, '0 0 1' * e.maxs.z, NULL, e.team_forced, e, buff_waypoint, true, RADARICON_Buff);
        wp.wp_extra = buff.m_id;
        WaypointSprite_UpdateTeamRadar(e.buff_waypoint, RADARICON_Buff, e.glowmod);
        e.buff_waypoint.waypointsprite_visible_for_player = buff_Waypoint_visible_for_player;
@@@ -162,9 -163,10 +163,10 @@@ void buff_Touch(entity this, entity tou
        if(!IS_PLAYER(toucher))
                return; // incase mutator changed toucher
  
 -      if((this.team && DIFF_TEAM(toucher, this))
 +      if((this.team_forced && toucher.team != this.team_forced)
        || (STAT(FROZEN, toucher))
        || (toucher.vehicle)
+       || (!this.buffdef) // TODO: error out or maybe reset type if this occurs?
        || (time < PS(toucher).buff_shield)
        )
        {
                return;
        }
  
-       if (STAT(BUFFS, toucher))
+       entity heldbuff = buff_FirstFromFlags(toucher);
+       entity thebuff = this.buffdef;
+       if (heldbuff)
        {
-               if (CS_CVAR(toucher).cvar_cl_buffs_autoreplace && STAT(BUFFS, toucher) != STAT(BUFFS, this))
+               if (CS_CVAR(toucher).cvar_cl_buffs_autoreplace && heldbuff != thebuff)
                {
                        // TODO: lost-gained notification for this case
-                       int buffid = buff_FirstFromFlags(STAT(BUFFS, toucher)).m_id;
+                       int buffid = heldbuff.m_id;
                        Send_Notification(NOTIF_ONE, toucher, MSG_INFO, INFO_ITEM_BUFF_LOST, toucher.netname, buffid);
                        if(!IS_INDEPENDENT_PLAYER(toucher))
                                Send_Notification(NOTIF_ALL_EXCEPT, toucher, MSG_INFO, INFO_ITEM_BUFF_LOST, toucher.netname, buffid);
  
-                       STAT(BUFFS, toucher) = 0;
                        //sound(toucher, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
                }
                else { return; } // do nothing
        this.owner = toucher;
        this.buff_active = false;
        this.lifetime = 0;
-       entity thebuff = buff_FirstFromFlags(STAT(BUFFS, this));
        Send_Notification(NOTIF_ONE, toucher, MSG_MULTI, ITEM_BUFF_GOT, thebuff.m_id);
        if(!IS_INDEPENDENT_PLAYER(toucher))
                Send_Notification(NOTIF_ALL_EXCEPT, toucher, MSG_INFO, INFO_ITEM_BUFF, toucher.netname, thebuff.m_id);
  
        Send_Effect(EFFECT_ITEM_PICKUP, CENTER_OR_VIEWOFS(this), '0 0 0', 1);
        sound(toucher, CH_TRIGGER, SND_SHIELD_RESPAWN, VOL_BASE, ATTN_NORM);
-       STAT(BUFFS, toucher) |= (STAT(BUFFS, this));
-       STAT(LAST_PICKUP, toucher) = time;
-       float bufftime = ((this.count) ? this.count : thebuff.m_time(thebuff));
+       float oldtime = StatusEffects_gettime(thebuff, toucher);
+       float bufftime = ((this.buffs_finished) ? this.buffs_finished : thebuff.m_time(thebuff));
+       buff_RemoveAll(toucher, STATUSEFFECT_REMOVE_NORMAL); // remove previous buffs so that a new one may be added
        if(bufftime)
-               STAT(BUFF_TIME, toucher) = min(time + bufftime, max(STAT(BUFF_TIME, toucher), time) + bufftime);
+               StatusEffects_apply(thebuff, toucher, min(time + bufftime, max(oldtime, time) + bufftime), 0);
+       else
+               StatusEffects_apply(thebuff, toucher, time + 999, 0); // HACK: zero timer means "infinite"!
+       STAT(LAST_PICKUP, toucher) = time;
  }
  
  float buff_Available(entity buff)
  {
-       if (buff == BUFF_Null)
+       if (!buff)
                return false;
        if (buff == BUFF_AMMO && ((start_items & IT_UNLIMITED_AMMO) || cvar("g_melee_only")))
                return false;
  void buff_NewType(entity ent)
  {
        RandomSelection_Init();
-       FOREACH(Buffs, buff_Available(it),
+       FOREACH(StatusEffect, it.instanceOfBuff && buff_Available(it),
        {
                // if it's already been chosen, give it a lower priority
                float myseencount = (it.buff_seencount > 0) ? it.buff_seencount : 1; // no division by zero please!
                RandomSelection_AddEnt(it, max(0.2, 1 / myseencount), 1);
        });
        entity newbuff = RandomSelection_chosen_ent;
+       if(!newbuff)
+               return;
        newbuff.buff_seencount += 1; // lower chances of seeing this buff again soon
-       STAT(BUFFS, ent) = newbuff.m_itemid;
+       ent.buffdef = newbuff;
+ }
+ void buff_RemoveAll(entity actor, int removal_type)
+ {
+       if(!actor.statuseffects)
+               return;
+       FOREACH(StatusEffect, it.instanceOfBuff,
+       {
+               it.m_remove(it, actor, removal_type);
+       });
+ }
+ entity buff_FirstFromFlags(entity actor)
+ {
+       if(!actor.statuseffects)
+               return NULL;
+       FOREACH(StatusEffect, it.instanceOfBuff && it.m_active(it, actor), { return it; });
+       return NULL;
  }
  
  void buff_Think(entity this)
        if(this.buff_waypoint && autocvar_g_buffs_waypoint_distance <= 0)
                WaypointSprite_Kill(this.buff_waypoint);
  
-       if(STAT(BUFFS, this) != this.oldbuffs)
+       if(this.buffdef != this.oldbuffs)
        {
-               entity buff = buff_FirstFromFlags(STAT(BUFFS, this));
+               entity buff = this.buffdef;
                this.color = buff.m_color;
                this.glowmod = buff_GlowColor(buff);
                this.skin = buff.m_skin;
                                WaypointSprite_UpdateBuildFinished(this.buff_waypoint, time + this.buff_activetime - frametime);
                }
  
-               this.oldbuffs = STAT(BUFFS, this);
+               this.oldbuffs = this.buffdef;
        }
  
        if(!game_stopped)
        }
  
        if(!this.buff_active && !this.buff_activetime)
-       if(!this.owner || STAT(FROZEN, this.owner) || IS_DEAD(this.owner) || !this.owner.iscreature || this.owner.vehicle || !(STAT(BUFFS, this.owner) & STAT(BUFFS, this)) || this.pickup_anyway > 0 || (this.pickup_anyway >= 0 && autocvar_g_buffs_pickup_anyway))
+       if(!this.owner || STAT(FROZEN, this.owner) || IS_DEAD(this.owner) || !this.owner.iscreature || this.owner.vehicle
+               || this.pickup_anyway > 0 || (this.pickup_anyway >= 0 && autocvar_g_buffs_pickup_anyway) || this.buffdef != buff_FirstFromFlags(this.owner))
        {
                buff_SetCooldown(this, autocvar_g_buffs_cooldown_respawn + frametime);
                this.owner = NULL;
  
        if(this.buff_active)
        {
 -              if(this.team && !this.buff_waypoint)
 +              if(this.team_forced && !this.buff_waypoint)
                        buff_Waypoint_Spawn(this);
  
                if(this.lifetime && time >= this.lifetime)
@@@ -329,7 -358,7 +358,7 @@@ void buff_Reset(entity this
  bool buff_Customize(entity this, entity client)
  {
        entity player = WaypointSprite_getviewentity(client);
-       if(!this.buff_active || (this.team_forced && player.team != this.team_forced))
 -      if((!this.buff_active || !this.buffdef) || (this.team && DIFF_TEAM(player, this)))
++      if((!this.buff_active || !this.buffdef) || (this.team_forced && player.team != this.team_forced))
        {
                this.alpha = 0.3;
                if(this.effects & EF_FULLBRIGHT) { this.effects &= ~(EF_FULLBRIGHT); }
@@@ -355,9 -384,11 +384,9 @@@ void buff_Init(entity this
  {
        if(!cvar("g_buffs")) { delete(this); return; }
  
-       entity buff = buff_FirstFromFlags(STAT(BUFFS, this));
 -      if(!teamplay && this.team) { this.team = 0; }
 -
+       entity buff = this.buffdef;
  
-       if(!STAT(BUFFS, this) || !buff_Available(buff))
+       if(!buff || !buff_Available(buff))
                buff_NewType(this);
  
        this.classname = "item_buff";
        IL_PUSH(g_items, this);
        setthink(this, buff_Think);
        settouch(this, buff_Touch);
+       setmodel(this, MDL_BUFF);
+       setsize(this, BUFF_MIN, BUFF_MAX);
        this.reset = buff_Reset;
        this.nextthink = time + 0.1;
        this.gravity = 1;
        this.pflags = PFLAGS_FULLDYNAMIC;
        this.dtor = buff_Delete;
  
+       if(!this.buffs_finished)
+               this.buffs_finished = this.count; // legacy support
        if(this.spawnflags & 1)
                this.noalign = true;
  
        if(this.noalign)
                set_movetype(this, MOVETYPE_NONE); // reset by random location
  
-       setmodel(this, MDL_BUFF);
-       setsize(this, BUFF_MIN, BUFF_MAX);
        if(cvar("g_buffs_random_location") || (this.spawnflags & 64))
                buff_Respawn(this);
  }
  
  void buff_Init_Compat(entity ent, entity replacement)
  {
 -      if (ent.spawnflags & 2)
 -              ent.team = NUM_TEAM_1;
 -      else if (ent.spawnflags & 4)
 -              ent.team = NUM_TEAM_2;
 +      if (teamplay)
 +      {
 +              if (ent.spawnflags & 2)
 +                      ent.team_forced = NUM_TEAM_1;
 +              else if (ent.spawnflags & 4)
 +                      ent.team_forced = NUM_TEAM_2;
 +      }
  
-       STAT(BUFFS, ent) = replacement.m_itemid;
+       ent.buffdef = replacement;
  
        buff_Init(ent);
  }
@@@ -448,28 -478,28 +479,28 @@@ MUTATOR_HOOKFUNCTION(buffs, Damage_Calc
  
        if(frag_deathtype == DEATH_BUFF.m_id) { return; }
  
-       if(STAT(BUFFS, frag_target) & BUFF_RESISTANCE.m_itemid)
+       if(StatusEffects_active(BUFF_RESISTANCE, frag_target))
        {
                float reduced = frag_damage * autocvar_g_buffs_resistance_blockpercent;
                frag_damage = bound(0, frag_damage - reduced, frag_damage);
        }
  
-       if(STAT(BUFFS, frag_target) & BUFF_SPEED.m_itemid)
+       if(StatusEffects_active(BUFF_SPEED, frag_target))
        if(frag_target != frag_attacker)
                frag_damage *= autocvar_g_buffs_speed_damage_take;
  
-       if(STAT(BUFFS, frag_target) & BUFF_MEDIC.m_itemid)
+       if(StatusEffects_active(BUFF_MEDIC, frag_target))
        if((GetResource(frag_target, RES_HEALTH) - frag_damage) <= 0)
        if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
        if(frag_attacker)
        if(random() <= autocvar_g_buffs_medic_survive_chance)
                frag_damage = max(5, GetResource(frag_target, RES_HEALTH) - autocvar_g_buffs_medic_survive_health);
  
-       if(STAT(BUFFS, frag_target) & BUFF_JUMP.m_itemid)
+       if(StatusEffects_active(BUFF_JUMP, frag_target))
        if(frag_deathtype == DEATH_FALL.m_id)
                frag_damage = 0;
  
-       if(STAT(BUFFS, frag_target) & BUFF_VENGEANCE.m_itemid)
+       if(StatusEffects_active(BUFF_VENGEANCE, frag_target))
        if(frag_attacker)
        if(frag_attacker != frag_target)
        if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
                dmgent.nextthink = time + 0.1;
        }
  
-       if(STAT(BUFFS, frag_target) & BUFF_BASH.m_itemid)
+       if(StatusEffects_active(BUFF_BASH, frag_target))
        if(frag_attacker != frag_target)
                frag_force = '0 0 0';
  
-       if(STAT(BUFFS, frag_attacker) & BUFF_BASH.m_itemid)
+       if(StatusEffects_active(BUFF_BASH, frag_attacker))
        if(frag_force)
        {
                if(frag_attacker == frag_target)
                        frag_force *= autocvar_g_buffs_bash_force;
        }
  
-       if(STAT(BUFFS, frag_attacker) & BUFF_DISABILITY.m_itemid)
+       if(StatusEffects_active(BUFF_DISABILITY, frag_attacker))
        if(frag_target != frag_attacker)
                frag_target.buff_disability_time = time + autocvar_g_buffs_disability_slowtime;
  
-       if(STAT(BUFFS, frag_target) & BUFF_INFERNO.m_itemid)
+       if(StatusEffects_active(BUFF_INFERNO, frag_target))
        {
                if(frag_deathtype == DEATH_FIRE.m_id)
                        frag_damage = 0;
                        frag_damage *= 0.5; // TODO: cvarize?
        }
  
-       if(STAT(BUFFS, frag_attacker) & BUFF_LUCK.m_itemid)
+       if(StatusEffects_active(BUFF_LUCK, frag_attacker))
        if(frag_attacker != frag_target)
        if(autocvar_g_buffs_luck_damagemultiplier > 0)
        if(random() <= autocvar_g_buffs_luck_chance)
                frag_damage *= autocvar_g_buffs_luck_damagemultiplier;
  
-       if(STAT(BUFFS, frag_attacker) & BUFF_INFERNO.m_itemid)
+       if(StatusEffects_active(BUFF_INFERNO, frag_attacker))
        if(frag_target != frag_attacker) {
                float btime = buff_Inferno_CalculateTime(
                        frag_damage,
@@@ -535,7 -565,7 +566,7 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerDamag
  {
        entity frag_attacker = M_ARGV(1, entity);
        entity frag_target = M_ARGV(2, entity);
-       if(!(STAT(BUFFS, frag_attacker) & BUFF_VAMPIRE.m_itemid))
+       if(!StatusEffects_active(BUFF_VAMPIRE, frag_attacker))
                return;
        float health_take = bound(0, M_ARGV(4, float), GetResource(frag_target, RES_HEALTH));
  
@@@ -554,7 -584,7 +585,7 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerSpawn
        entity player = M_ARGV(0, entity);
  
        buffs_BuffModel_Remove(player);
-       player.oldbuffs = 0;
+       player.oldbuffs = NULL;
        // reset timers here to prevent them continuing after re-spawn
        player.buff_disability_time = 0;
        player.buff_disability_effect_time = 0;
@@@ -565,7 -595,7 +596,7 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerPhysi
        entity player = M_ARGV(0, entity);
        // these automatically reset, no need to worry
  
-       if(STAT(BUFFS, player) & BUFF_SPEED.m_itemid)
+       if(StatusEffects_active(BUFF_SPEED, player))
                STAT(MOVEVARS_HIGHSPEED, player) *= autocvar_g_buffs_speed_speed;
  
        if(time < player.buff_disability_time)
@@@ -577,7 -607,7 +608,7 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerPhysi
        entity player = M_ARGV(0, entity);
        // these automatically reset, no need to worry
  
-       if(STAT(BUFFS, player) & BUFF_JUMP.m_itemid)
+       if(StatusEffects_active(BUFF_JUMP, player))
                STAT(MOVEVARS_JUMPVELOCITY, player) = autocvar_g_buffs_jump_height;
  }
  
@@@ -596,13 -626,12 +627,12 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerDies
  {
        entity frag_target = M_ARGV(2, entity);
  
-       if(STAT(BUFFS, frag_target))
+       entity heldbuff = buff_FirstFromFlags(frag_target);
+       if(heldbuff)
        {
-               int buffid = buff_FirstFromFlags(STAT(BUFFS, frag_target)).m_id;
+               int buffid = heldbuff.m_id;
                if(!IS_INDEPENDENT_PLAYER(frag_target))
                        Send_Notification(NOTIF_ALL_EXCEPT, frag_target, MSG_INFO, INFO_ITEM_BUFF_LOST, frag_target.netname, buffid);
-               STAT(BUFFS, frag_target) = 0;
-               STAT(BUFF_TIME, frag_target) = 0;
  
                buffs_BuffModel_Remove(frag_target);
        }
@@@ -614,15 -643,15 +644,15 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerUseKe
  
        entity player = M_ARGV(0, entity);
  
-       if(STAT(BUFFS, player))
+       entity heldbuff = buff_FirstFromFlags(player);
+       if(heldbuff)
        {
-               int buffid = buff_FirstFromFlags(STAT(BUFFS, player)).m_id;
+               int buffid = heldbuff.m_id;
                Send_Notification(NOTIF_ONE, player, MSG_MULTI, ITEM_BUFF_DROP, buffid);
                if(!IS_INDEPENDENT_PLAYER(player))
                        Send_Notification(NOTIF_ALL_EXCEPT, player, MSG_INFO, INFO_ITEM_BUFF_LOST, player.netname, buffid);
  
-               STAT(BUFFS, player) = 0;
-               STAT(BUFF_TIME, player) = 0;
+               buff_RemoveAll(player, STATUSEFFECT_REMOVE_NORMAL);
                PS(player).buff_shield = time + max(0, autocvar_g_buffs_pickup_delay);
                sound(player, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
                return true;
@@@ -634,7 -663,7 +664,7 @@@ MUTATOR_HOOKFUNCTION(buffs, ForbidThrow
        if(MUTATOR_RETURNVALUE || game_stopped) return;
        entity player = M_ARGV(0, entity);
  
-       if(STAT(BUFFS, player) & BUFF_SWAPPER.m_itemid)
+       if(StatusEffects_active(BUFF_SWAPPER, player))
        {
                float best_distance = autocvar_g_buffs_swapper_range;
                entity closest = NULL;
                        sound(closest, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NORM);
  
                        // TODO: add a counter to handle how many times one can teleport, and a delay to prevent spam
-                       STAT(BUFFS, player) = 0;
+                       buff_RemoveAll(player, STATUSEFFECT_REMOVE_NORMAL);
                        return true;
                }
        }
@@@ -730,7 -759,7 +760,7 @@@ MUTATOR_HOOKFUNCTION(buffs, CustomizeWa
  
        // if you have the invisibility powerup, sprites ALWAYS are restricted to your team
        // but only apply this to real players, not to spectators
-       if((wp.owner.flags & FL_CLIENT) && (STAT(BUFFS, wp.owner) & BUFF_INVISIBLE.m_itemid) && (e == player))
+       if((wp.owner.flags & FL_CLIENT) && (e == player) && StatusEffects_active(BUFF_INVISIBLE, wp.owner))
        if(DIFF_TEAM(wp.owner, e))
                return true;
  }
@@@ -763,7 -792,7 +793,7 @@@ MUTATOR_HOOKFUNCTION(buffs, WeaponRateF
  {
        entity player = M_ARGV(1, entity);
  
-       if(STAT(BUFFS, player) & BUFF_SPEED.m_itemid)
+       if(StatusEffects_active(BUFF_SPEED, player))
                M_ARGV(0, float) *= autocvar_g_buffs_speed_rate;
  
        if(time < player.buff_disability_time)
@@@ -774,7 -803,7 +804,7 @@@ MUTATOR_HOOKFUNCTION(buffs, WeaponSpeed
  {
        entity player = M_ARGV(1, entity);
  
-       if(STAT(BUFFS, player) & BUFF_SPEED.m_itemid)
+       if(StatusEffects_active(BUFF_SPEED, player))
                M_ARGV(0, float) *= autocvar_g_buffs_speed_weaponspeed;
  
        if(time < player.buff_disability_time)
@@@ -787,9 -816,9 +817,9 @@@ MUTATOR_HOOKFUNCTION(buffs, PlayerPreTh
  {
        entity player = M_ARGV(0, entity);
  
-       if(game_stopped || IS_DEAD(player) || frametime || !IS_PLAYER(player)) return;
+       if(game_stopped || IS_DEAD(player) || !IS_PLAYER(player)) return;
  
-       if(STAT(BUFFS, player) & BUFF_FLIGHT.m_itemid)
+       if(StatusEffects_active(BUFF_FLIGHT, player))
        {
                if(!PHYS_INPUT_BUTTON_CROUCH(player))
                        player.buff_flight_crouchheld = false;
        // 2: notify carrier as well
        int buff_lost = 0;
  
-       if(STAT(BUFF_TIME, player) && STAT(BUFFS, player))
-       if(time >= STAT(BUFF_TIME, player))
-       {
-               STAT(BUFF_TIME, player) = 0;
+       entity heldbuff = buff_FirstFromFlags(player);
+       float bufftime = StatusEffects_gettime(heldbuff, player);
+       if(heldbuff && bufftime && time >= bufftime)
                buff_lost = 2;
-       }
  
        if(STAT(FROZEN, player)) { buff_lost = 1; }
  
-       if(buff_lost)
+       if(buff_lost && heldbuff)
        {
-               if(STAT(BUFFS, player))
+               int buffid = heldbuff.m_id;
+               if(buff_lost == 2)
                {
-                       int buffid = buff_FirstFromFlags(STAT(BUFFS, player)).m_id;
-                       if(buff_lost == 2)
-                       {
-                               Send_Notification(NOTIF_ONE, player, MSG_MULTI, ITEM_BUFF_DROP, buffid); // TODO: special timeout message?
-                               sound(player, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
-                       }
-                       else if(!IS_INDEPENDENT_PLAYER(player))
-                               Send_Notification(NOTIF_ALL_EXCEPT, player, MSG_INFO, INFO_ITEM_BUFF_LOST, player.netname, buffid);
-                       STAT(BUFFS, player) = 0;
-                       PS(player).buff_shield = time + max(0, autocvar_g_buffs_pickup_delay); // always put in a delay, even if small
+                       Send_Notification(NOTIF_ONE, player, MSG_MULTI, ITEM_BUFF_DROP, buffid); // TODO: special timeout message?
+                       sound(player, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
                }
+               else if(!IS_INDEPENDENT_PLAYER(player))
+                       Send_Notification(NOTIF_ALL_EXCEPT, player, MSG_INFO, INFO_ITEM_BUFF_LOST, player.netname, buffid);
+               buff_RemoveAll(player, STATUSEFFECT_REMOVE_TIMEOUT); // TODO: remove only the currently active buff?
+               heldbuff = NULL;
+               PS(player).buff_shield = time + max(0, autocvar_g_buffs_pickup_delay); // always put in a delay, even if small
        }
  
-       if(STAT(BUFFS, player) & BUFF_MAGNET.m_itemid)
+       if(StatusEffects_active(BUFF_MAGNET, player))
        {
                vector pickup_size;
                IL_EACH(g_items, it.itemdef,
                {
-                       if(STAT(BUFFS, it))
+                       if(it.buffdef)
                                pickup_size = '1 1 1' * autocvar_g_buffs_magnet_range_buff;
                        else
                                pickup_size = '1 1 1' * autocvar_g_buffs_magnet_range_item;
                });
        }
  
-       if(STAT(BUFFS, player) & BUFF_AMMO.m_itemid)
+       if(StatusEffects_active(BUFF_AMMO, player))
        {
                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                {
                }
        }
  
-       if((STAT(BUFFS, player) & BUFF_INVISIBLE.m_itemid) && (player.oldbuffs & BUFF_INVISIBLE.m_itemid))
+       if(!player.vehicle && StatusEffects_active(BUFF_INVISIBLE, player) && player.oldbuffs == BUFF_INVISIBLE)
                player.alpha = ((autocvar_g_buffs_invisible_alpha) ? autocvar_g_buffs_invisible_alpha : -1); // powerups reset alpha, so we must enforce this (TODO)
  
- #define BUFF_ONADD(b) if ( (STAT(BUFFS, player) & (b).m_itemid) && !(player.oldbuffs & (b).m_itemid))
- #define BUFF_ONREM(b) if (!(STAT(BUFFS, player) & (b).m_itemid) &&  (player.oldbuffs & (b).m_itemid))
+ #define BUFF_ONADD(b) if ( (heldbuff == (b)) && (player.oldbuffs != (b)))
+ #define BUFF_ONREM(b) if ( (heldbuff != (b)) && (player.oldbuffs == (b)))
  
-       if(STAT(BUFFS, player) != player.oldbuffs)
+       if(heldbuff != player.oldbuffs)
        {
-               entity buff = buff_FirstFromFlags(STAT(BUFFS, player));
-               float bufftime = buff != BUFF_Null ? buff.m_time(buff) : 0;
-               if(STAT(BUFF_TIME, player) <= time) // if the player still has a buff countdown, don't reset it!
-                       STAT(BUFF_TIME, player) = (bufftime) ? time + bufftime : 0;
+               bufftime = heldbuff ? heldbuff.m_time(heldbuff) : 0;
+               if(StatusEffects_gettime(heldbuff, player) <= time) // if the player still has a buff countdown, don't reset it!
+               {
+                       player.statuseffects.statuseffect_time[heldbuff.m_id] = (bufftime) ? time + bufftime : 0;
+                       StatusEffects_update(player);
+               }
  
                BUFF_ONADD(BUFF_AMMO)
                {
                        player.buff_ammo_prev_infitems = (player.items & IT_UNLIMITED_AMMO);
                        player.items |= IT_UNLIMITED_AMMO;
  
-                       if(STAT(BUFFS, player) & BUFF_AMMO.m_itemid)
+                       if(StatusEffects_active(BUFF_AMMO, player))
                        {
                                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                                {
                        else
                                player.items &= ~IT_UNLIMITED_AMMO;
  
-                       if(STAT(BUFFS, player) & BUFF_AMMO.m_itemid)
+                       if(StatusEffects_active(BUFF_AMMO, player))
                        {
                                for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                                {
  
                BUFF_ONADD(BUFF_INVISIBLE)
                {
-                       if(time < STAT(STRENGTH_FINISHED, player) && MUTATOR_IS_ENABLED(mutator_instagib))
+                       if(StatusEffects_active(STATUSEFFECT_Strength, player) && MUTATOR_IS_ENABLED(mutator_instagib))
                                player.buff_invisible_prev_alpha = default_player_alpha; // we don't want to save the powerup's alpha, as player may lose the powerup while holding the buff
                        else
                                player.buff_invisible_prev_alpha = player.alpha;
-                       player.alpha = autocvar_g_buffs_invisible_alpha;
+                       if(!player.vehicle)
+                               player.alpha = autocvar_g_buffs_invisible_alpha;
                }
  
                BUFF_ONREM(BUFF_INVISIBLE)
                {
-                       if(time < STAT(STRENGTH_FINISHED, player) && MUTATOR_IS_ENABLED(mutator_instagib))
-                               player.alpha = autocvar_g_instagib_invis_alpha;
-                       else
-                               player.alpha = player.buff_invisible_prev_alpha;
+                       if(!player.vehicle)
+                       {
+                               if(StatusEffects_active(STATUSEFFECT_Strength, player) && MUTATOR_IS_ENABLED(mutator_instagib))
+                                       player.alpha = autocvar_g_instagib_invis_alpha;
+                               else
+                                       player.alpha = player.buff_invisible_prev_alpha;
+                       }
                }
  
                BUFF_ONADD(BUFF_FLIGHT)
                BUFF_ONREM(BUFF_FLIGHT)
                        player.gravity = ((player.trigger_gravity_check) ? player.trigger_gravity_check.enemy.gravity : player.buff_flight_oldgravity);
  
-               player.oldbuffs = STAT(BUFFS, player);
-               if(STAT(BUFFS, player))
+               player.oldbuffs = heldbuff;
+               if(heldbuff)
                {
                        if(!player.buff_model)
                                buffs_BuffModel_Spawn(player);
  
-                       player.buff_model.color = buff.m_color;
-                       player.buff_model.glowmod = buff_GlowColor(player.buff_model);
-                       player.buff_model.skin = buff.m_skin;
+                       player.buff_model.color = heldbuff.m_color;
+                       player.buff_model.glowmod = buff_GlowColor(heldbuff);
+                       player.buff_model.skin = heldbuff.m_skin;
  
                        player.effects |= EF_NOSHADOW;
                }
  #undef BUFF_ONREM
  }
  
- MUTATOR_HOOKFUNCTION(buffs, SpectateCopy)
- {
-       entity spectatee = M_ARGV(0, entity);
-       entity client = M_ARGV(1, entity);
-       STAT(BUFFS, client) = STAT(BUFFS, spectatee);
-       STAT(BUFF_TIME, client) = STAT(BUFF_TIME, spectatee);
- }
  MUTATOR_HOOKFUNCTION(buffs, PlayerRegen)
  {
        entity player = M_ARGV(0, entity);
  
-       if(STAT(BUFFS, player) & BUFF_MEDIC.m_itemid)
+       if(StatusEffects_active(BUFF_MEDIC, player))
        {
                M_ARGV(2, float) = autocvar_g_buffs_medic_rot; // rot_mod
                M_ARGV(4, float) = M_ARGV(1, float) = autocvar_g_buffs_medic_max; // limit_mod = max_mod
                M_ARGV(2, float) = autocvar_g_buffs_medic_regen; // regen_mod
        }
  
-       if(STAT(BUFFS, player) & BUFF_SPEED.m_itemid)
+       if(StatusEffects_active(BUFF_SPEED, player))
                M_ARGV(2, float) = autocvar_g_buffs_speed_regen; // regen_mod
  }
  
index d198898bdceba58b6dcfad23da3b0ced6a1590e4,1b095c0f2c50db47956e4e85ec6034648af1ea04..4eb536b609d00ea4d316ed84ec0fdb6fd8649f4a
@@@ -76,11 -76,11 +76,11 @@@ float autocvar_g_buffs_luck_damagemulti
  .float buff_effect_delay;
  
  // buff definitions
 -.float buff_active;
 +.bool buff_active;
  .float buff_activetime;
 -.float buff_activetime_updated;
 +.bool buff_activetime_updated;
  .entity buff_waypoint;
- .int oldbuffs; // for updating effects
+ .entity oldbuffs; // for updating effects
  .float buff_shield; // delay for players to keep them from spamming buff pickups
  .entity buff_model; // controls effects (TODO: make csqc)
  
@@@ -91,3 -91,7 +91,7 @@@ const vector BUFF_MAX = ('16 16 60')
  .float cvar_cl_buffs_autoreplace;
  
  float buff_Available(entity buff);
+ void buff_RemoveAll(entity actor, int removal_type);
+ entity buff_FirstFromFlags(entity actor);
diff --combined qcsrc/common/stats.qh
index c709da51a769d5564386fbca0d553085980a46fc,e476969fa58fa8ac6389a876c816ef16c4ff713a..0b1b30eea2e54380376a51c80af0659744853f6c
@@@ -4,7 -4,6 +4,7 @@@
  
  #ifdef SVQC
  #include <server/client.qh>
 +#include <server/compat/quake3.qh>
  #include <server/main.qh>
  #include <common/gamemodes/sv_rules.qh>
  #include <common/mapobjects/teleporters.qh>
@@@ -85,8 -84,6 +85,6 @@@ int autocvar_leadlimit
  REGISTER_STAT(WEAPONRATEFACTOR, float, W_WeaponRateFactor(this))
  REGISTER_STAT(GAME_STOPPED, int, game_stopped)
  REGISTER_STAT(GAMESTARTTIME, float, game_starttime)
- REGISTER_STAT(STRENGTH_FINISHED, float)
- REGISTER_STAT(INVINCIBLE_FINISHED, float)
  /** arc heat in [0,1] */
  REGISTER_STAT(PRESSED_KEYS, int)
  /** this stat could later contain some other bits of info, like, more server-side particle config */
@@@ -103,7 -100,6 +101,6 @@@ REGISTER_STAT(HUD, int
  REGISTER_STAT(HIT_TIME, float)
  REGISTER_STAT(DAMAGE_DEALT_TOTAL, int)
  REGISTER_STAT(TYPEHIT_TIME, float)
- REGISTER_STAT(SUPERWEAPONS_FINISHED, float)
  REGISTER_STAT(AIR_FINISHED, float)
  REGISTER_STAT(VEHICLESTAT_HEALTH, int)
  REGISTER_STAT(VEHICLESTAT_SHIELD, int)
@@@ -120,7 -116,6 +117,6 @@@ REGISTER_STAT(RESPAWN_TIME, float
  REGISTER_STAT(ROUNDSTARTTIME, float, round_starttime)
  REGISTER_STAT(MONSTERS_TOTAL, int)
  REGISTER_STAT(MONSTERS_KILLED, int)
- REGISTER_STAT(BUFFS, int)
  REGISTER_STAT(NADE_BONUS, float)
  REGISTER_STAT(NADE_BONUS_TYPE, int)
  REGISTER_STAT(NADE_BONUS_SCORE, float)
@@@ -130,7 -125,6 +126,6 @@@ REGISTER_STAT(PLASMA, int
  REGISTER_STAT(FROZEN, int)
  REGISTER_STAT(REVIVE_PROGRESS, float)
  REGISTER_STAT(ROUNDLOST, int)
- REGISTER_STAT(BUFF_TIME, float)
  REGISTER_STAT(CTF_FLAGSTATUS, int)
  REGISTER_STAT(CAPTURE_PROGRESS, float)
  REGISTER_STAT(ENTRAP_ORB, float)
@@@ -356,7 -350,10 +351,7 @@@ bool autocvar_sv_slick_applygravity
  #endif
  REGISTER_STAT(SLICK_APPLYGRAVITY, bool, autocvar_sv_slick_applygravity)
  
 -#ifdef SVQC
 -bool autocvar_sv_q3defragcompat;
 -#endif
 -REGISTER_STAT(Q3DEFRAGCOMPAT, bool, autocvar_sv_q3defragcompat)
 +REGISTER_STAT(Q3COMPAT, int, q3compat)
  
  #ifdef SVQC
  #include "physics/movetypes/movetypes.qh"
index 5d49ed0cf42503173e29e466ce3f2e2968157424,e97de364bbab9d96a011e55151de1c59c60eb5dc..7ef3cc1e8d7a5ee18ff37591f899d635d8e0ee6d
@@@ -61,11 -61,11 +61,11 @@@ WepSet _WepSet_FromWeapon(int a
                                if (a >= 24)
                                {
                                        a -= 24;
-                                       return '0 0 1' * (2 ** a);
+                                       return '0 0 1' * BIT(a);
                                }
-                       return '0 1 0' * (2 ** a);
+                       return '0 1 0' * BIT(a);
                }
-       return '1 0 0' * (2 ** a);
+       return '1 0 0' * BIT(a);
  }
  #ifdef SVQC
        void WriteWepSet(float dst, WepSet w)
@@@ -230,23 -230,6 +230,23 @@@ string GetAmmoName(int ammotype
        }
  }
  
 +entity GetAmmoItem(int ammotype)
 +{
 +      switch (ammotype)
 +      {
 +              case RES_SHELLS:  return ITEM_Shells;
 +              case RES_BULLETS: return ITEM_Bullets;
 +              case RES_ROCKETS: return ITEM_Rockets;
 +              case RES_CELLS:   return ITEM_Cells;
 +              case RES_PLASMA:  return ITEM_Plasma;
 +              case RES_FUEL:    return ITEM_JetpackFuel;
 +      }
 +      LOG_WARNF("Invalid ammo type %d ", ammotype);
 +      return NULL;
 +      // WEAPONTODO: use this generic func to reduce duplication ?
 +      // GetAmmoPicture  GetAmmoName  notif_arg_item_wepammo  ammo_pickupevalfunc ?
 +}
 +
  #ifdef CSQC
  int GetAmmoTypeFromNum(int i)
  {
diff --combined qcsrc/lib/spawnfunc.qh
index 679ddda67f427c586f401e60b0417dcf0e332ef9,db0d83ead7d2ff2ab26db36a57b03d373633f81b..7e8c025a2a3100b1f338fc2c153e038c856f2379
@@@ -7,8 -7,7 +7,8 @@@
  noref bool require_spawnfunc_prefix;
  .bool spawnfunc_checked;
  /** Not for production use, provides access to a dump of the entity's fields when it is parsed from map data */
 -//noref string __fullspawndata;
 +noref string __fullspawndata;
 +.string fullspawndata;
  
  // Optional type checking; increases compile time too much to be enabled by default
  #if 0
                FIELD_SCALAR(fld, noise2) \
                FIELD_SCALAR(fld, noise3) \
                FIELD_SCALAR(fld, noise) \
 +              FIELD_SCALAR(fld, notcpm) \
 +              FIELD_SCALAR(fld, notfree) \
 +              FIELD_SCALAR(fld, notta) \
 +              FIELD_SCALAR(fld, notteam) \
 +              FIELD_SCALAR(fld, notvq3) \
                FIELD_SCALAR(fld, phase) \
                FIELD_SCALAR(fld, platmovetype) \
                FIELD_SCALAR(fld, race_place) \
@@@ -257,6 -251,10 +257,10 @@@ void _checkWhitelisted(entity this, str
        }
  }
  
+ // this function simply avoids expanding IL_NEW during compilation
+ // for each spawning entity
+ void g_spawn_queue_spawn() { g_spawn_queue = IL_NEW(); }
  noref bool __spawnfunc_first;
  
  #define spawnfunc(id) \
                if (__spawnfunc_expecting > 1) { __spawnfunc_expecting = 0; } \
                else if (__spawnfunc_expecting) { \
                        /* engine call */ \
-                       if (!g_spawn_queue) { g_spawn_queue = IL_NEW(); } \
+                       if (!g_spawn_queue) g_spawn_queue_spawn(); \
                        __spawnfunc_expecting = 0; \
                        this = __spawnfunc_expect; \
                        __spawnfunc_expect = NULL; \
                this.classname = #id; \
                if (!this.spawnfunc_checked) { \
                        _checkWhitelisted(this, #id); \
 +                      if (__fullspawndata) { \
 +                              /* not supported in old DP */ \
 +                              /* must be read inside the real spawnfunc */ \
 +                              this.fullspawndata = __fullspawndata; \
 +                      } \
                        this.spawnfunc_checked = true; \
                        if (this) { \
                                /* not worldspawn, delay spawn */ \
index e4824d60f11685b90e137b200955f398e17d96cd,b5198c08d1bc5b7db57e1d293ec5eb7d80cbf76e..20735d99fccfcbe8c2b23efea27edffeb2129c96
@@@ -571,13 -571,24 +571,24 @@@ vector WarpZoneLib_NearestPointOnBox(ve
        return nearest;
  }
  
+ // blacklist of entities that WarpZone_FindRadius doesn't care about
  bool WarpZoneLib_BadEntity(entity e)
  {
        if (is_pure(e)) return true;
        string s = e.classname;
  
-       //if (s == "net_linked") return true; // actually some real entities are linked without classname, fail
-       if (s == "") return true;
+       switch(s)
+       {
+               case "weaponentity":
+               case "exteriorweaponentity":
+               case "sprite_waypoint":
+               case "spawnfunc":
+               case "weaponchild":
+               case "chatbubbleentity":
+               //case "net_linked": // actually some real entities are linked without classname, fail
+               case "":
+                       return true;
+       }
  
        if (startsWith(s, "target_")) return true;
  
@@@ -786,13 -797,10 +797,13 @@@ entity WarpZone_RefSys_SpawnSameRefSys(
  bool WarpZoneLib_ExactTrigger_Touch(entity this, entity toucher)
  {
        vector emin = toucher.absmin, emax = toucher.absmax;
 -      // the engine offsets absolute bounding boxes by a single quake unit
 -      // we must undo that here to allow accurate touching
 -      emin += '1 1 1';
 -      emax -= '1 1 1';
 +      if(STAT(Q3COMPAT))
 +      {
 +              // DP's tracebox enlarges absolute bounding boxes by a single quake unit
 +              // we must undo that here to allow accurate touching
 +              emin += '1 1 1';
 +              emax -= '1 1 1';
 +      }
        return !WarpZoneLib_BoxTouchesBrush(emin, emax, this, toucher);
  }
  
diff --combined qcsrc/server/client.qc
index 613f68fd07636bf2a9955c6b2a796547de4d53d2,f3e8ca46fcad3a437a62b297fb52a56a04eb65f9..2652b16a9ae715dd07da5417d67dea2772dc9c79
@@@ -23,6 -23,7 +23,7 @@@
  #include <common/mutators/mutator/instagib/sv_instagib.qh>
  #include <common/mutators/mutator/nades/nades.qh>
  #include <common/mutators/mutator/overkill/oknex.qh>
+ #include <common/mutators/mutator/status_effects/_mod.qh>
  #include <common/mutators/mutator/waypoints/all.qh>
  #include <common/net_linked.qh>
  #include <common/net_notice.qh>
@@@ -335,9 -336,6 +336,6 @@@ void PutObserverInServer(entity this
        this.scale = 0;
        this.fade_time = 0;
        this.pain_finished = 0;
-       STAT(STRENGTH_FINISHED, this) = 0;
-       STAT(INVINCIBLE_FINISHED, this) = 0;
-       STAT(SUPERWEAPONS_FINISHED, this) = 0;
        STAT(AIR_FINISHED, this) = 0;
        //this.dphitcontentsmask = 0;
        this.dphitcontentsmask = DPCONTENTS_SOLID;
        this.punchangle = '0 0 0';
        this.punchvector = '0 0 0';
        this.oldvelocity = this.velocity;
-       this.fire_endtime = -1;
        this.event_damage = func_null;
        this.event_heal = func_null;
  
                SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR);
                this.frags = FRAGS_SPECTATOR;
        }
+       bot_relinkplayerlist();
        if (CS(this).just_joined)
                CS(this).just_joined = false;
  }
@@@ -590,8 -590,6 +590,6 @@@ void PutPlayerInServer(entity this
  
        PS(this).dual_weapons = '0 0 0';
  
-       STAT(SUPERWEAPONS_FINISHED, this) = (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0;
        this.items = start_items;
  
        this.spawnshieldtime = time + autocvar_g_spawnshieldtime;
        this.respawn_flags = 0;
        this.respawn_time = 0;
        STAT(RESPAWN_TIME, this) = 0;
 -      bool q3dfcompat = autocvar_sv_q3defragcompat && autocvar_sv_q3defragcompat_changehitbox;
 -      this.scale = ((q3dfcompat) ? 0.9 : autocvar_sv_player_scale);
 +      this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) ? 0.9 : autocvar_sv_player_scale);
        this.fade_time = 0;
        this.pain_finished = 0;
        this.pushltime = 0;
        this.punchangle = '0 0 0';
        this.punchvector = '0 0 0';
  
-       STAT(STRENGTH_FINISHED, this) = 0;
-       STAT(INVINCIBLE_FINISHED, this) = 0;
-       this.fire_endtime = -1;
        STAT(REVIVE_PROGRESS, this) = 0;
        this.revival_time = 0;
  
-       // TODO: we can't set these in the PlayerSpawn hook since the target code is called before it!
-       STAT(BUFFS, this) = 0;
-       STAT(BUFF_TIME, this) = 0;
        STAT(AIR_FINISHED, this) = 0;
        this.waterlevel = WATERLEVEL_NONE;
        this.watertype = CONTENT_EMPTY;
                }
        });
  
+       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
                        spot.target = s;
        }
  
-       Unfreeze(this, false);
-       MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
        if (autocvar_spawn_debug)
        {
                sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
                .entity weaponentity = weaponentities[slot];
+               entity w_ent = this.(weaponentity);
                if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
-                       this.(weaponentity).m_switchweapon = w_getbestweapon(this, weaponentity);
+                       w_ent.m_switchweapon = w_getbestweapon(this, weaponentity);
                else
-                       this.(weaponentity).m_switchweapon = WEP_Null;
-               this.(weaponentity).m_weapon = WEP_Null;
-               this.(weaponentity).weaponname = "";
-               this.(weaponentity).m_switchingweapon = WEP_Null;
-               this.(weaponentity).cnt = -1;
+                       w_ent.m_switchweapon = WEP_Null;
+               w_ent.m_weapon = WEP_Null;
+               w_ent.weaponname = "";
+               w_ent.m_switchingweapon = WEP_Null;
+               w_ent.cnt = -1;
        }
  
        MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
@@@ -824,6 -817,8 +816,8 @@@ void PutClientInServer(entity this
        } else if (IS_PLAYER(this)) {
                PutPlayerInServer(this);
        }
+       bot_relinkplayerlist();
  }
  
  // TODO do we need all these fields, or should we stop autodetecting runtime
@@@ -1469,7 -1464,7 +1463,7 @@@ void player_powerups(entity this
        else
                this.modelflags &= ~MF_ROCKET;
  
-       this.effects &= ~(EF_RED | EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT | EF_FLAME | EF_NODEPTHTEST);
+       this.effects &= ~(EF_RED | EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT | EF_NODEPTHTEST);
  
        if (IS_DEAD(this))
                player_powerups_remove_all(this);
        // add a way to see what the items were BEFORE all of these checks for the mutator hook
        int items_prev = this.items;
  
-       Fire_ApplyDamage(this);
-       Fire_ApplyEffect(this);
        if (!MUTATOR_IS_ENABLED(mutator_instagib))
        {
                if (this.items & ITEM_Strength.m_itemid)
                {
-                       play_countdown(this, STAT(STRENGTH_FINISHED, this), SND_POWEROFF);
+                       play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Strength, this), SND_POWEROFF);
                        this.effects = this.effects | (EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT);
-                       if (time > STAT(STRENGTH_FINISHED, this))
+                       if (time > StatusEffects_gettime(STATUSEFFECT_Strength, this))
                        {
                                this.items = this.items - (this.items & ITEM_Strength.m_itemid);
                                //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERDOWN_STRENGTH, this.netname);
                }
                else
                {
-                       if (time < STAT(STRENGTH_FINISHED, this))
+                       if (time < StatusEffects_gettime(STATUSEFFECT_Strength, this))
                        {
                                this.items = this.items | ITEM_Strength.m_itemid;
                                if(!g_cts)
                }
                if (this.items & ITEM_Shield.m_itemid)
                {
-                       play_countdown(this, STAT(INVINCIBLE_FINISHED, this), SND_POWEROFF);
+                       play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Shield, this), SND_POWEROFF);
                        this.effects = this.effects | (EF_RED | EF_ADDITIVE | EF_FULLBRIGHT);
-                       if (time > STAT(INVINCIBLE_FINISHED, this))
+                       if (time > StatusEffects_gettime(STATUSEFFECT_Shield, this))
                        {
                                this.items = this.items - (this.items & ITEM_Shield.m_itemid);
                                //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERDOWN_SHIELD, this.netname);
                }
                else
                {
-                       if (time < STAT(INVINCIBLE_FINISHED, this))
+                       if (time < StatusEffects_gettime(STATUSEFFECT_Shield, this))
                        {
                                this.items = this.items | ITEM_Shield.m_itemid;
                                if(!g_cts)
                {
                        if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
                        {
-                               STAT(SUPERWEAPONS_FINISHED, this) = 0;
+                               StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL);
                                this.items = this.items - (this.items & IT_SUPERWEAPON);
                                //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname);
                                Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST);
                        }
                        else
                        {
-                               play_countdown(this, STAT(SUPERWEAPONS_FINISHED, this), SND_POWEROFF);
-                               if (time > STAT(SUPERWEAPONS_FINISHED, this))
+                               play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
+                               if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
                                {
                                        this.items = this.items - (this.items & IT_SUPERWEAPON);
                                        STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
                }
                else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
                {
-                       if (time < STAT(SUPERWEAPONS_FINISHED, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
+                       if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
                        {
                                this.items = this.items | IT_SUPERWEAPON;
                                if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
                        }
                        else
                        {
-                               STAT(SUPERWEAPONS_FINISHED, this) = 0;
+                               if(StatusEffects_active(STATUSEFFECT_Superweapons, this))
+                                       StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT);
                                STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
                        }
                }
-               else
+               else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame!
                {
-                       STAT(SUPERWEAPONS_FINISHED, this) = 0;
+                       StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR);
                }
        }
  
@@@ -1762,9 -1755,6 +1754,6 @@@ void SpectateCopy(entity this, entity s
        this.items = spectatee.items;
        STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
        STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
-       STAT(STRENGTH_FINISHED, this) = STAT(STRENGTH_FINISHED, spectatee);
-       STAT(INVINCIBLE_FINISHED, this) = STAT(INVINCIBLE_FINISHED, spectatee);
-       STAT(SUPERWEAPONS_FINISHED, this) = STAT(SUPERWEAPONS_FINISHED, spectatee);
        STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee);
        STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
        STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
@@@ -2081,23 -2071,6 +2070,6 @@@ int nJoinAllowed(entity this, entity ig
        return free_slots;
  }
  
- /**
-  * Checks whether the client is an observer or spectator, if so, he will get kicked after
-  * g_maxplayers_spectator_blocktime seconds
-  */
- void checkSpectatorBlock(entity this)
- {
-       if(IS_SPEC(this) || IS_OBSERVER(this))
-       if(!this.caplayer)
-       if(IS_REAL_CLIENT(this))
-       {
-               if( time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime) ) {
-                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
-                       dropclient(this);
-               }
-       }
- }
  void PrintWelcomeMessage(entity this)
  {
        if(CS(this).motd_actived_time == 0)
@@@ -2346,6 -2319,8 +2318,8 @@@ void ObserverOrSpectatorThink(entity th
                                TRANSMUTE(Observer, this);
                                PutClientInServer(this);
                        }
+                       else
+                               this.would_spectate = false; // unable to spectate anyone
                        if (is_spec)
                                CS(this).impulse = 0;
                } else if (is_spec) {
                        }
                }
                else {
-                       int preferred_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? CS_CVAR(this).cvar_cl_clippedspectating : !CS_CVAR(this).cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
+                       bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating;
+                       if (PHYS_INPUT_BUTTON_USE(this))
+                               wouldclip = !wouldclip;
+                       int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
                        set_movetype(this, preferred_movetype);
                }
        } else { // jump pressed
@@@ -2456,12 -2434,16 +2433,16 @@@ void PlayerPreThink (entity this
        if (frametime) {
                // physics frames: update anticheat stuff
                anticheat_prethink(this);
-       }
  
-       if (blockSpectators && frametime) {
                // WORKAROUND: only use dropclient in server frames (frametime set).
                // Never use it in cl_movement frames (frametime zero).
-               checkSpectatorBlock(this);
+               if (blockSpectators && IS_REAL_CLIENT(this)
+                       && (IS_SPEC(this) || IS_OBSERVER(this)) && !this.caplayer
+                       && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime))
+               {
+                       if (dropclient_schedule(this))
+                               Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
+               }
        }
  
        zoomstate_set = false;
@@@ -2698,13 -2680,14 +2679,14 @@@ void PlayerPostThink (entity this
  {
        Player_Physics(this);
  
-       if (autocvar_sv_maxidle > 0)
+       if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0))
        if (frametime) // WORKAROUND: only use dropclient in server frames (frametime set). Never use it in cl_movement frames (frametime zero).
        if (IS_REAL_CLIENT(this))
-       if (IS_PLAYER(this) || autocvar_sv_maxidle_spectatorsareidle)
+       if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
+       if (!intermission_running) // NextLevel() kills all centerprints after setting this true
        {
                int totalClients = 0;
-               if(autocvar_sv_maxidle_slots > 0)
+               if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0)
                {
                        FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
                        {
                        });
                }
  
-               if (autocvar_sv_maxidle_slots > 0 && (maxclients - totalClients) > autocvar_sv_maxidle_slots)
+               if (autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0 && (maxclients - totalClients) > autocvar_sv_maxidle_slots)
                { /* do nothing */ }
                else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
                {
                }
                else
                {
-                       float timeleft = ceil(autocvar_sv_maxidle - (time - CS(this).parm_idlesince));
-                       if (timeleft == min(10, autocvar_sv_maxidle - 1)) { // - 1 to support sv_maxidle <= 10
-                               if (!CS(this).idlekick_lasttimeleft)
+                       float maxidle_time = autocvar_sv_maxidle;
+                       if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+                               maxidle_time = autocvar_sv_maxidle_playertospectator;
+                       float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
+                       float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
+                       if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
+                       {
+                               if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+                                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
+                               else
                                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
                        }
                        if (timeleft <= 0) {
-                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname);
-                               dropclient(this);
+                               if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+                               {
+                                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
+                                       if (this.caplayer)
+                                               this.caplayer = 0;
+                                       PutObserverInServer(this);
+                               }
+                               else
+                               {
+                                       if (dropclient_schedule(this))
+                                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time);
+                               }
                                return;
                        }
-                       else if (timeleft <= 10) {
+                       else if (timeleft <= countdown_time) {
                                if (timeleft != CS(this).idlekick_lasttimeleft)
-                                       Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_IDLE, timeleft));
+                                       play2(this, SND(TALK2));
                                CS(this).idlekick_lasttimeleft = timeleft;
                        }
                }
index 5acd24a58aea87f4e1d660e0ff9cfa73c34e3d59,ae9dbd35760faf0e834f184d5f64d16fcac65d82..d578994968796c6baf86f8cb816c7c7504f74f16
@@@ -5,6 -5,8 +5,8 @@@
  #include <common/mapobjects/trigger/counter.qh>
  #include <common/mapobjects/triggers.qh>
  #include <common/mutators/mutator/buffs/buffs.qh>
+ #include <common/mutators/mutator/buffs/sv_buffs.qh>
+ #include <common/mutators/mutator/status_effects/_mod.qh>
  #include <common/notifications/all.qh>
  #include <common/stats.qh>
  #include <common/weapons/_all.qh>
  #include <server/resources.qh>
  #include <server/world.qh>
  
 -//***********************
 -//QUAKE 3 ENTITIES - So people can play quake3 maps with the xonotic weapons
 -//***********************
 +/***********************
 + * QUAKE 3 ENTITIES - So people can play quake3 maps with the xonotic weapons
 + ***********************
 +
 + * Map entities NOT handled in this file:
 + holdable_invulnerability     Q3TA    buffs mutator
 + holdable_kamikaze            Q3TA    buffs mutator
 + holdable_teleporter          Q3A     buffs mutator
 + item_ammoregen                       Q3TA    buffs mutator
 + item_doubler                 Q3TA    buffs mutator
 + item_guard                   Q3TA    buffs mutator
 + item_scout                   Q3TA    buffs mutator
 + item_armor_jacket            CPMA    quake2.qc
 + item_flight                  Q3A     buffs mutator
 + item_haste                   Q3A     buffs mutator
 + item_health                  Q3A     quake.qc
 + item_health_large            Q3A     items.qc
 + item_health_small            Q3A     health.qh
 + item_health_mega             Q3A     health.qh
 + item_invis                   Q3A     buffs mutator
 + item_quad                    Q3A     items.qc
 + item_regen                   Q3A     buffs mutator
 + weapon_machinegun            Q3A     machinegun.qh
 + weapon_grenadelauncher               Q3A     mortar.qh
 + weapon_rocketlauncher                Q3A     devastator.qh
 + CTF spawnfuncs handled in sv_ctf.qc
 +
 + NOTE: for best experience, you need to swap MGs with SGs in the map or it won't have a MG
 +*/
 +
 +// SG -> MG || SG
 +SPAWNFUNC_Q3_COND(weapon_shotgun, ammo_shells, (q3compat & Q3COMPAT_ARENA), WEP_MACHINEGUN, WEP_SHOTGUN)
 +
 +// MG -> SG || MG
 +// Technically we should replace weapon_machinegun with WEP_SHOTGUN if Q3COMPAT_ARENA, but it almost never occurs on Q3 maps
 +SPAWNFUNC_Q3AMMO_COND(ammo_bullets, (q3compat & Q3COMPAT_ARENA), WEP_SHOTGUN, WEP_MACHINEGUN)
  
 -// NOTE: for best experience, you need to swap MGs with SGs in the map or it won't have a MG
 +// GL -> Mortar
 +SPAWNFUNC_Q3AMMO(ammo_grenades, WEP_MORTAR)
  
 -// SG -> SG
 -SPAWNFUNC_ITEM(ammo_shells, ITEM_Shells)
 +// Team Arena Proximity Launcher -> Mortar
 +// It's more accurate to spawn Mine Layer but players prefer Mortar, and weapon_grenadelauncher is usually disabled by "notta" and weapon_prox_launcher placed at the same origin
 +SPAWNFUNC_Q3(weapon_prox_launcher, ammo_mines, WEP_MORTAR)
  
 -// MG -> MG
 -SPAWNFUNC_ITEM(ammo_bullets, ITEM_Bullets)
 +// Team Arena Chaingun -> HLAC
 +SPAWNFUNC_Q3(weapon_chaingun, ammo_belt, WEP_HLAC)
  
 -// GL -> Mortar
 -SPAWNFUNC_ITEM(ammo_grenades, ITEM_Rockets)
 +// Quake Live Heavy Machine Gun -> HLAC
 +SPAWNFUNC_Q3(weapon_hmg, ammo_hmg, WEP_HLAC)
  
 -// Mines -> Rockets
 -SPAWNFUNC_WEAPON(weapon_prox_launcher, WEP_MINE_LAYER)
 -SPAWNFUNC_ITEM(ammo_mines, ITEM_Rockets)
 +// Team Arena Nailgun -> Crylink || Quake Nailgun -> Electro
 +SPAWNFUNC_Q3_COND(weapon_nailgun, ammo_nails, cvar("sv_mapformat_is_quake3"), WEP_CRYLINK, WEP_ELECTRO)
  
 -// LG -> Lightning
 -SPAWNFUNC_WEAPON(weapon_lightning, WEP_ELECTRO)
 -SPAWNFUNC_ITEM(ammo_lightning, ITEM_Cells)
 +// LG -> Electro
 +SPAWNFUNC_Q3(weapon_lightning, ammo_lightning, WEP_ELECTRO)
  
  // Plasma -> Hagar
 -SPAWNFUNC_WEAPON(weapon_plasmagun, WEP_HAGAR)
 -SPAWNFUNC_ITEM(ammo_cells, ITEM_Rockets)
 +SPAWNFUNC_Q3(weapon_plasmagun, ammo_cells, WEP_HAGAR)
  
  // Rail -> Vortex
 -SPAWNFUNC_WEAPON(weapon_railgun, WEP_VORTEX)
 -SPAWNFUNC_ITEM(ammo_slugs, ITEM_Cells)
 +SPAWNFUNC_Q3(weapon_railgun, ammo_slugs, WEP_VORTEX)
  
 -// BFG -> Crylink
 -SPAWNFUNC_WEAPON(weapon_bfg, WEP_CRYLINK)
 -SPAWNFUNC_ITEM(ammo_bfg, ITEM_Cells)
 +// BFG -> Crylink || Fireball
 +SPAWNFUNC_Q3_COND(weapon_bfg, ammo_bfg, cvar_string("g_mod_balance") == "XDF", WEP_CRYLINK, WEP_FIREBALL)
 +      // FIXME: WEP_FIREBALL has no ammo_type field so ammo_bfg is deleted by SPAWNFUNC_BODY
  
  // grappling hook -> hook
  SPAWNFUNC_WEAPON(weapon_grapplinghook, WEP_HOOK)
  
  // RL -> RL
 -SPAWNFUNC_ITEM(ammo_rockets, ITEM_Rockets)
 +SPAWNFUNC_Q3AMMO(ammo_rockets, WEP_DEVASTATOR)
 +
 +// Gauntlet -> Tuba
 +SPAWNFUNC_ITEM(weapon_gauntlet, WEP_TUBA)
  
  // Armor
  SPAWNFUNC_ITEM(item_armor_body, ITEM_ArmorMega)
  SPAWNFUNC_ITEM(item_armor_combat, ITEM_ArmorBig)
  SPAWNFUNC_ITEM(item_armor_shard, ITEM_ArmorSmall)
 +SPAWNFUNC_ITEM(item_armor_green, ITEM_ArmorMedium) // CCTF
 +
 +// Battle Suit
  SPAWNFUNC_ITEM(item_enviro, ITEM_Shield)
  
  // medkit -> armor (we have no holdables)
@@@ -167,17 -132,17 +169,17 @@@ void target_init_use(entity this, entit
  
        if (!(this.spawnflags & 8))
        {
-               STAT(STRENGTH_FINISHED, actor) = 0;
-               STAT(INVINCIBLE_FINISHED, actor) = 0;
-               if(STAT(BUFFS, actor)) // TODO: make a dropbuffs function to handle this
+               StatusEffects_remove(STATUSEFFECT_Strength, actor, STATUSEFFECT_REMOVE_NORMAL);
+               StatusEffects_remove(STATUSEFFECT_Shield, actor, STATUSEFFECT_REMOVE_NORMAL);
+               entity heldbuff = buff_FirstFromFlags(actor);
+               if(heldbuff) // TODO: make a dropbuffs function to handle this
                {
-                       int buffid = buff_FirstFromFlags(STAT(BUFFS, actor)).m_id;
+                       int buffid = heldbuff.m_id;
                        Send_Notification(NOTIF_ONE, actor, MSG_MULTI, ITEM_BUFF_DROP, buffid);
                        sound(actor, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
                        if(!IS_INDEPENDENT_PLAYER(actor))
                                Send_Notification(NOTIF_ALL_EXCEPT, actor, MSG_INFO, INFO_ITEM_BUFF_LOST, actor.netname, buffid);
-                       STAT(BUFFS, actor) = 0;
-                       STAT(BUFF_TIME, actor) = 0;
+                       buff_RemoveAll(actor, STATUSEFFECT_REMOVE_NORMAL);
                }
        }
  
@@@ -195,42 -160,52 +197,42 @@@ spawnfunc(target_init
        InitializeEntity(this, target_init_verify, INITPRIO_FINDTARGET);
  }
  
 -// weapon give ent from defrag
 +// weapon give ent from Q3
  void target_give_init(entity this)
  {
        IL_EACH(g_items, it.targetname == this.target,
        {
 -              if (it.classname == "weapon_devastator") {
 -                      SetResourceExplicit(this, RES_ROCKETS, GetResource(this, RES_ROCKETS) + it.count * WEP_CVAR_PRI(devastator, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "devastator");
 -              }
 -              else if (it.classname == "weapon_vortex") {
 -                      SetResourceExplicit(this, RES_CELLS, GetResource(this, RES_CELLS) + it.count * WEP_CVAR_PRI(vortex, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "vortex");
 -              }
 -              else if (it.classname == "weapon_electro") {
 -                      SetResourceExplicit(this, RES_CELLS, GetResource(this, RES_CELLS) + it.count * WEP_CVAR_PRI(electro, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "electro");
 -              }
 -              else if (it.classname == "weapon_hagar") {
 -                      SetResourceExplicit(this, RES_ROCKETS, GetResource(this, RES_ROCKETS) + it.count * WEP_CVAR_PRI(hagar, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "hagar");
 -              }
 -              else if (it.classname == "weapon_crylink") {
 -                      SetResourceExplicit(this, RES_CELLS, GetResource(this, RES_CELLS) + it.count * WEP_CVAR_PRI(crylink, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "crylink");
 -              }
 -              else if (it.classname == "weapon_mortar") {
 -                      SetResourceExplicit(this, RES_ROCKETS, GetResource(this, RES_ROCKETS) + it.count * WEP_CVAR_PRI(mortar, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "mortar");
 -              }
 -              else if (it.classname == "weapon_shotgun") {
 -                      SetResourceExplicit(this, RES_SHELLS, GetResource(this, RES_SHELLS) + it.count * WEP_CVAR_PRI(shotgun, ammo)); // WEAPONTODO
 -                      this.netname = cons(this.netname, "shotgun");
 -              }
 -              else if (it.classname == "item_armor_mega")
 -                      SetResourceExplicit(this, RES_ARMOR, 100);
 -              else if (it.classname == "item_health_mega")
 -                      SetResourceExplicit(this, RES_HEALTH, 200);
 -              else if (it.classname == "item_buff") {
 +              if (it.classname == "item_buff")
 +              {
-                       entity buff = buff_FirstFromFlags(STAT(BUFFS, it));
+                       entity buff = it.buffdef;
                        this.netname = cons(this.netname, buff.netname);
-                       STAT(BUFF_TIME, this) += it.count;
 -                      this.buffs_finished = it.count;
++                      this.buffs_finished += it.count;
 +              }
 +              else
 +              {
 +                      if (it.ammo_rockets)
 +                              this.ammo_rockets += it.ammo_rockets;
 +                      else if (it.ammo_cells)
 +                              this.ammo_cells += it.ammo_cells;
 +                      else if (it.ammo_shells)
 +                              this.ammo_shells += it.ammo_shells;
 +                      else if (it.ammo_nails)
 +                              this.ammo_nails += it.ammo_nails;
 +                      else if (it.invincible_finished)
 +                              this.invincible_finished += it.invincible_finished;
 +                      else if (it.strength_finished)
 +                              this.strength_finished += it.strength_finished;
 +                      else if (it.health)
 +                              this.health += it.health;
 +                      else if (it.armorvalue)
 +                              this.armorvalue += it.armorvalue;
 +
 +                      this.netname = cons(this.netname, it.netname);
                }
  
                //remove(it); // removing ents in init functions causes havoc, workaround:
 -        setthink(it, SUB_Remove);
 -        it.nextthink = time;
 +              setthink(it, SUB_Remove);
 +              it.nextthink = time;
        });
        this.spawnflags = 2;
        this.spawnfunc_checked = true;
@@@ -274,31 -249,35 +276,31 @@@ spawnfunc(target_fragsFilter
        this.use = fragsfilter_use;
  }
  
 -//spawnfunc(item_flight)       /* handled by buffs mutator */
 -//spawnfunc(item_doubler)        /* handled by buffs mutator */
 -//spawnfunc(item_haste)        /* handled by buffs mutator */
 -//spawnfunc(item_health)       /* handled in t_quake.qc */
 -//spawnfunc(item_health_large) /* handled in items.qc */
 -//spawnfunc(item_health_small) /* handled in items.qc */
 -//spawnfunc(item_health_mega)  /* handled in items.qc */
 -//spawnfunc(item_invis)        /* handled by buffs mutator */
 -//spawnfunc(item_regen)        /* handled by buffs mutator */
 -
 -// CTF spawnfuncs handled in mutators/gamemode_ctf.qc now
 -
 -.float notteam;
 -.float notsingle;
 -.float notfree;
 -.float notq3a;
 -.float notta;
 +.bool notteam;
 +.bool notsingle;
 +.bool notfree;
 +.bool notta;
 +.bool notvq3;
 +.bool notcpm;
  .string gametype;
  bool DoesQ3ARemoveThisEntity(entity this)
  {
        // Q3 style filters (DO NOT USE, THIS IS COMPAT ONLY)
  
 -      if(this.notq3a)
 -              if(!teamplay || g_tdm || g_ctf)
 +      // DeFRaG mappers use "notcpm" or "notvq3" to disable an entity in CPM or VQ3 physics
 +      // Xonotic is usually played with a CPM-based physics so we default to CPM mode
 +      if(cvar_string("g_mod_physics") == "Q3")
 +      {
 +              if(this.notvq3)
                        return true;
 +      }
 +      else if(this.notcpm)
 +              return true;
  
 +      // Q3 mappers use "notq3a" or "notta" to disable an entity in Q3A or Q3TA
 +      // Xonotic has ~equivalent features to Team Arena
        if(this.notta)
 -              if (!(!teamplay || g_tdm || g_ctf))
 -                      return true;
 +              return true;
  
        if(this.notsingle)
                if(maxclients == 1)
        if(this.gametype)
        {
                string gametypename;
 -              // static char *gametypeNames[] = {"ffa", "tournament", "single", "team", "ctf", "oneflag", "obelisk", "harvester", "teamtournament"}
 +              // From ioq3 g_spawn.c: static char *gametypeNames[] = {"ffa", "tournament", "single", "team", "ctf", "oneflag", "obelisk", "harvester"};
                gametypename = "ffa";
                if(teamplay)
                        gametypename = "team";
                        gametypename = "tournament";
                if(maxclients == 1)
                        gametypename = "single";
 -              // we do not have the other types (obelisk, harvester, teamtournament)
 +              // we do not have the other types (obelisk, harvester)
                if(strstrofs(this.gametype, gametypename, 0) < 0)
                        return true;
        }
  
        return false;
  }
 +
 +int GetAmmoConsumptionQ3(string netname)
 +// Returns ammo consumed per shot by the primary/default fire mode
 +// Returns 0 if the netname has no ammo cvar
 +{
 +      switch (netname)
 +      {
 +              case "arc":        return autocvar_g_balance_arc_beam_ammo;
 +              case "devastator": return autocvar_g_balance_devastator_ammo;
 +              case "machinegun": return autocvar_g_balance_machinegun_sustained_ammo;
 +              case "minelayer":  return autocvar_g_balance_minelayer_ammo;
 +              case "seeker":     return autocvar_g_balance_seeker_tag_ammo;
 +              default:           return cvar(strcat("g_balance_", netname, "_primary_ammo"));
 +      }
 +}
 +
index f5b15786f97dd35fe3e30d22d558cd98c0c8ae9a,5cf659cffff84f7cf834b34c597d1439b7d98c5b..66d0d296394b39f95e029e4117ef411709c101a6
@@@ -9,6 -9,7 +9,7 @@@
  #include <common/monsters/_mod.qh>
  #include <common/mutators/mutator/buffs/buffs.qh>
  #include <common/mutators/mutator/buffs/sv_buffs.qh>
+ #include <common/mutators/mutator/status_effects/_mod.qh>
  #include <common/notifications/all.qh>
  #include <common/util.qh>
  #include <common/weapons/_all.qh>
@@@ -549,17 -550,17 +550,17 @@@ bool Item_GiveTo(entity item, entity pl
        if (item.strength_finished)
        {
                pickedup = true;
-               STAT(STRENGTH_FINISHED, player) = max(STAT(STRENGTH_FINISHED, player), time) + item.strength_finished;
+               StatusEffects_apply(STATUSEFFECT_Strength, player, max(StatusEffects_gettime(STATUSEFFECT_Strength, player), time) + item.strength_finished, 0);
        }
        if (item.invincible_finished)
        {
                pickedup = true;
-               STAT(INVINCIBLE_FINISHED, player) = max(STAT(INVINCIBLE_FINISHED, player), time) + item.invincible_finished;
+               StatusEffects_apply(STATUSEFFECT_Shield, player, max(StatusEffects_gettime(STATUSEFFECT_Shield, player), time) + item.invincible_finished, 0);
        }
        if (item.superweapons_finished)
        {
                pickedup = true;
-               STAT(SUPERWEAPONS_FINISHED, player) = max(STAT(SUPERWEAPONS_FINISHED, player), time) + item.superweapons_finished;
+               StatusEffects_apply(STATUSEFFECT_Superweapons, player, max(StatusEffects_gettime(STATUSEFFECT_Superweapons, player), time) + item.superweapons_finished, 0);
        }
  
        // always eat teamed entities
@@@ -1018,19 -1019,19 +1019,19 @@@ void _StartItem(entity this, entity def
  
                if(autocvar_spawn_debug >= 2)
                {
 -            // why not flags & fl_item?
 -                  FOREACH_ENTITY_RADIUS(this.origin, 3, it.is_item, {
 -                LOG_TRACE("XXX Found duplicated item: ", itemname, vtos(this.origin));
 -                LOG_TRACE(" vs ", it.netname, vtos(it.origin));
 -                error("Mapper sucks.");
 -            });
 +                      // why not flags & fl_item?
 +                      FOREACH_ENTITY_RADIUS(this.origin, 3, it.is_item, {
 +                              LOG_TRACE("XXX Found duplicated item: ", itemname, vtos(this.origin));
 +                              LOG_TRACE(" vs ", it.netname, vtos(it.origin));
 +                              error("Mapper sucks.");
 +                      });
                        this.is_item = true;
                }
  
                weaponsInMap |= WepSet_FromWeapon(REGISTRY_GET(Weapons, weaponid));
  
 -              if (   def.instanceOfPowerup
 -                      || def.instanceOfWeaponPickup
 +              if (        def.instanceOfPowerup
 +                      ||  def.instanceOfWeaponPickup
                        || (def.instanceOfHealth && def != ITEM_HealthSmall)
                        || (def.instanceOfArmor && def != ITEM_ArmorSmall)
                        || (itemid & (IT_KEY1 | IT_KEY2))
        this.bot_pickupevalfunc = pickupevalfunc;
        this.bot_pickupbasevalue = pickupbasevalue;
        this.mdl = this.model ? this.model : strzone(this.item_model_ent.model_str());
 -      this.netname = itemname;
 +      this.netname = (def.m_weapon) ? def.m_weapon.netname : def.netname;
        settouch(this, Item_Touch);
        setmodel(this, MDL_Null); // precision set below
        //this.effects |= EF_LOWPRECISION;
  
  void StartItem(entity this, GameItem def)
  {
 -    def = def.m_spawnfunc_hookreplace(def, this);
 -    if (def.spawnflags & ITEM_FLAG_MUTATORBLOCKED)
 -    {
 -        delete(this);
 -        return;
 -    }
 -    this.classname = def.m_canonical_spawnfunc;
 -    _StartItem(
 -      this,
 -      this.itemdef = def,
 -      def.m_respawntime(), // defaultrespawntime
 -      def.m_respawntimejitter() // defaultrespawntimejitter
 +      def = def.m_spawnfunc_hookreplace(def, this);
 +
 +      if (def.spawnflags & ITEM_FLAG_MUTATORBLOCKED)
 +      {
 +              delete(this);
 +              return;
 +      }
 +
 +      this.classname = def.m_canonical_spawnfunc;
 +
 +      _StartItem(
 +              this,
 +              this.itemdef = def,
 +              def.m_respawntime(), // defaultrespawntime
 +              def.m_respawntimejitter() // defaultrespawntimejitter
        );
  }
  
@@@ -1226,14 -1224,14 +1227,14 @@@ spawnfunc(target_items
                        else if(argv(j) == "fuel_regen")             this.items |= ITEM_JetpackRegen.m_itemid;
                        else
                        {
-                               FOREACH(Buffs, it != BUFF_Null,
+                               FOREACH(StatusEffect, it.instanceOfBuff,
                                {
                                        string s = Buff_UndeprecateName(argv(j));
                                        if(s == it.netname)
                                        {
-                                               STAT(BUFFS, this) |= (it.m_itemid);
-                                               if(!STAT(BUFF_TIME, this))
-                                                       STAT(BUFF_TIME, this) = it.m_time(it);
+                                               this.buffdef = it;
+                                               if(!this.buffs_finished)
+                                                       this.buffs_finished = it.m_time(it);
                                                break;
                                        }
                                });
                res = GetResource(this, RES_FUEL);    if(res != 0) str = sprintf("%s %s%d %s", str, valueprefix, max(0, res), "fuel");
                res = GetResource(this, RES_HEALTH);  if(res != 0) str = sprintf("%s %s%d %s", str, valueprefix, max(0, res), "health");
                res = GetResource(this, RES_ARMOR);   if(res != 0) str = sprintf("%s %s%d %s", str, valueprefix, max(0, res), "armor");
-               // HACK: buffs share a single timer, so we need to include enabled buffs AFTER disabled ones to avoid loss
-               FOREACH(Buffs, it != BUFF_Null && !(STAT(BUFFS, this) & it.m_itemid), str = sprintf("%s %s%d %s", str, valueprefix, max(0, STAT(BUFF_TIME, this)), it.netname));
-               FOREACH(Buffs, it != BUFF_Null && (STAT(BUFFS, this) & it.m_itemid), str = sprintf("%s %s%d %s", str, valueprefix, max(0, STAT(BUFF_TIME, this)), it.netname));
+               FOREACH(StatusEffect, it.instanceOfBuff, str = sprintf("%s %s%d %s", str, valueprefix, this.buffs_finished * boolean(this.buffdef == it), it.netname));
                FOREACH(Weapons, it != WEP_Null, str = sprintf("%s %s%d %s", str, itemprefix, !!(STAT(WEAPONS, this) & (it.m_wepset)), it.netname));
        }
        this.netname = strzone(str);
@@@ -1344,8 -1340,8 +1343,8 @@@ float GiveWeapon(entity e, float wpn, f
  
  bool GiveBuff(entity e, Buff thebuff, int op, int val)
  {
-       bool had_buff = (STAT(BUFFS, e) & thebuff.m_itemid);
-       float new_buff_time = ((had_buff) ? STAT(BUFF_TIME, e) : 0);
+       bool had_buff = StatusEffects_active(thebuff, e);
+       float new_buff_time = ((had_buff) ? StatusEffects_gettime(thebuff, e) : 0);
        switch (op)
        {
                case OP_SET:
        }
        if(new_buff_time <= 0)
        {
-               if(had_buff)
-                       STAT(BUFF_TIME, e) = new_buff_time;
-               STAT(BUFFS, e) &= ~thebuff.m_itemid;
+               StatusEffects_remove(thebuff, e, STATUSEFFECT_REMOVE_TIMEOUT);
        }
        else
        {
-               STAT(BUFF_TIME, e) = new_buff_time;
-               STAT(BUFFS, e) = thebuff.m_itemid; // NOTE: replaces any existing buffs on the player!
+               buff_RemoveAll(e, STATUSEFFECT_REMOVE_CLEAR); // clear old buffs on the player first!
+               StatusEffects_apply(thebuff, e, new_buff_time, 0);
        }
-       bool have_buff = (STAT(BUFFS, e) & thebuff.m_itemid);
+       bool have_buff = StatusEffects_active(thebuff, e);
        return (had_buff != have_buff);
  }
  
@@@ -1419,6 -1413,35 +1416,35 @@@ bool GiveResourceValue(entity e, int re
  
        return SetResourceExplicit(e, res_type, new_val);
  }
+ bool GiveStatusEffect(entity e, StatusEffects this, int op, float val)
+ {
+       bool had_eff = StatusEffects_active(this, e);
+       float new_eff_time = ((had_eff) ? StatusEffects_gettime(this, e) : 0);
+       switch (op)
+       {
+               case OP_SET:
+                       new_eff_time = val;
+                       break;
+               case OP_MIN:
+                       new_eff_time = max(new_eff_time, val);
+                       break;
+               case OP_MAX:
+                       new_eff_time = min(new_eff_time, val);
+                       break;
+               case OP_PLUS:
+                       new_eff_time += val;
+                       break;
+               case OP_MINUS:
+                       new_eff_time -= val;
+                       break;
+       }
+       if(new_eff_time <= 0)
+               StatusEffects_remove(this, e, STATUSEFFECT_REMOVE_TIMEOUT);
+       else
+               StatusEffects_apply(this, e, new_eff_time, 0);
+       bool have_eff = StatusEffects_active(this, e);
+       return (had_eff != have_eff);
+ }
  
  float GiveItems(entity e, float beginarg, float endarg)
  {
                }
        }
  
-       STAT(STRENGTH_FINISHED, e) = max(0, STAT(STRENGTH_FINISHED, e) - time);
-       STAT(INVINCIBLE_FINISHED, e) = max(0, STAT(INVINCIBLE_FINISHED, e) - time);
-       STAT(SUPERWEAPONS_FINISHED, e) = max(0, STAT(SUPERWEAPONS_FINISHED, e) - time);
-       STAT(BUFF_TIME, e) = max(0, STAT(BUFF_TIME, e) - time);
+       if(e.statuseffects)
+       {
+               FOREACH(StatusEffect, true,
+               {
+                       e.statuseffects.statuseffect_time[it.m_id] = max(0, e.statuseffects.statuseffect_time[it.m_id] - time);
+               });
+       }
  
        PREGIVE(e, items);
        PREGIVE_WEAPONS(e);
-       PREGIVE(e, stat_STRENGTH_FINISHED);
-       PREGIVE(e, stat_INVINCIBLE_FINISHED);
-       PREGIVE(e, stat_SUPERWEAPONS_FINISHED);
+       PREGIVE_STATUSEFFECT(e, STATUSEFFECT_Strength);
+       PREGIVE_STATUSEFFECT(e, STATUSEFFECT_Shield);
+       //PREGIVE_STATUSEFFECT(e, STATUSEFFECT_Superweapons);
        PREGIVE_RESOURCE(e, RES_BULLETS);
        PREGIVE_RESOURCE(e, RES_CELLS);
        PREGIVE_RESOURCE(e, RES_PLASMA);
                                continue;
                        case "ALL":
                                got += GiveBit(e, items, ITEM_JetpackRegen.m_itemid, op, val);
-                               got += GiveValue(e, stat_STRENGTH_FINISHED, op, val);
-                               got += GiveValue(e, stat_INVINCIBLE_FINISHED, op, val);
-                               got += GiveValue(e, stat_SUPERWEAPONS_FINISHED, op, val);
+                               got += GiveStatusEffect(e, STATUSEFFECT_Strength, op, val);
+                               got += GiveStatusEffect(e, STATUSEFFECT_Shield, op, val);
+                               got += GiveStatusEffect(e, STATUSEFFECT_Superweapons, op, val);
                                got += GiveBit(e, items, IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS, op, val);
                        case "all":
                                got += GiveBit(e, items, ITEM_Jetpack.m_itemid, op, val);
                        case "allweapons":
                                FOREACH(Weapons, it != WEP_Null && !(it.spawnflags & (WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_SPECIALATTACK)), got += GiveWeapon(e, it.m_id, op, val));
                        //case "allbuffs": // all buffs makes a player god, do not want!
-                               //FOREACH(Buffs, it != BUFF_Null, got += GiveBuff(e, it.m_itemid, op, val));
+                               //FOREACH(StatusEffect, it.instanceOfBuff, got += GiveBuff(e, it, op, val));
                        case "allammo":
                                got += GiveResourceValue(e, RES_CELLS, op, val);
                                got += GiveResourceValue(e, RES_PLASMA, op, val);
                                got += GiveBit(e, items, ITEM_JetpackRegen.m_itemid, op, val);
                                break;
                        case "strength":
-                               got += GiveValue(e, stat_STRENGTH_FINISHED, op, val);
+                               got += GiveStatusEffect(e, STATUSEFFECT_Strength, op, val);
                                break;
                        case "invincible":
-                               got += GiveValue(e, stat_INVINCIBLE_FINISHED, op, val);
+                               got += GiveStatusEffect(e, STATUSEFFECT_Shield, op, val);
                                break;
                        case "superweapons":
-                               got += GiveValue(e, stat_SUPERWEAPONS_FINISHED, op, val);
+                               got += GiveStatusEffect(e, STATUSEFFECT_Superweapons, op, val);
                                break;
                        case "cells":
                                got += GiveResourceValue(e, RES_CELLS, op, val);
                                got += GiveResourceValue(e, RES_FUEL, op, val);
                                break;
                        default:
-                               FOREACH(Buffs, it != BUFF_Null && buff_Available(it) && Buff_UndeprecateName(cmd) == it.netname,
+                               FOREACH(StatusEffect, it.instanceOfBuff && buff_Available(it) && Buff_UndeprecateName(cmd) == it.netname,
                                {
                                        got += GiveBuff(e, it, op, val);
                                        break;
                        if(STAT(WEAPONS, e) & (it.m_wepset))
                                it.wr_init(it);
        });
-       POSTGIVE_VALUE(e, stat_STRENGTH_FINISHED, 1, SND_POWERUP, SND_POWEROFF);
-       POSTGIVE_VALUE(e, stat_INVINCIBLE_FINISHED, 1, SND_Shield, SND_POWEROFF);
-       //POSTGIVE_VALUE(e, stat_SUPERWEAPONS_FINISHED, 1, SND_Null, SND_Null);
+       POSTGIVE_STATUSEFFECT(e, STATUSEFFECT_Strength, 1, SND_POWERUP, SND_POWEROFF);
+       POSTGIVE_STATUSEFFECT(e, STATUSEFFECT_Shield, 1, SND_POWERUP, SND_POWEROFF);
        POSTGIVE_RESOURCE(e, RES_BULLETS, 0, SND_ITEMPICKUP, SND_Null);
        POSTGIVE_RESOURCE(e, RES_CELLS, 0, SND_ITEMPICKUP, SND_Null);
        POSTGIVE_RESOURCE(e, RES_PLASMA, 0, SND_ITEMPICKUP, SND_Null);
        POSTGIVE_RES_ROT(e, RES_ARMOR, 1, pauserotarmor_finished, autocvar_g_balance_pause_armor_rot, pauseregen_finished, autocvar_g_balance_pause_health_regen, SND_ARMOR25, SND_Null);
        POSTGIVE_RES_ROT(e, RES_HEALTH, 1, pauserothealth_finished, autocvar_g_balance_pause_health_rot, pauseregen_finished, autocvar_g_balance_pause_health_regen, SND_MEGAHEALTH, SND_Null);
  
-       if(STAT(SUPERWEAPONS_FINISHED, e) <= 0)
+       if(!StatusEffects_active(STATUSEFFECT_Superweapons, e))
+       {
                if(!g_weaponarena && (STAT(WEAPONS, e) & WEPSET_SUPERWEAPONS))
-                       STAT(SUPERWEAPONS_FINISHED, e) = autocvar_g_balance_superweapons_time;
+                       StatusEffects_apply(STATUSEFFECT_Superweapons, e, autocvar_g_balance_superweapons_time, 0);
+       }
  
-       if(STAT(STRENGTH_FINISHED, e) <= 0)
-               STAT(STRENGTH_FINISHED, e) = 0;
-       else
-               STAT(STRENGTH_FINISHED, e) += time;
-       if(STAT(INVINCIBLE_FINISHED, e) <= 0)
-               STAT(INVINCIBLE_FINISHED, e) = 0;
-       else
-               STAT(INVINCIBLE_FINISHED, e) += time;
-       if(STAT(SUPERWEAPONS_FINISHED, e) <= 0)
-               STAT(SUPERWEAPONS_FINISHED, e) = 0;
-       else
-               STAT(SUPERWEAPONS_FINISHED, e) += time;
-       if(STAT(BUFF_TIME, e) <= 0)
-               STAT(BUFF_TIME, e) = 0;
-       else
-               STAT(BUFF_TIME, e) += time;
+       if(e.statuseffects)
+       {
+               FOREACH(StatusEffect, true,
+               {
+                       if(e.statuseffects.statuseffect_time[it.m_id] <= 0)
+                               e.statuseffects.statuseffect_time[it.m_id] = 0;
+                       else
+                               e.statuseffects.statuseffect_time[it.m_id] += time;
+               });
+                       
+               StatusEffects_update(e);
+       }
  
        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
        {
index eed40eca39fb9a7eeb32472542e886fd40a64a79,39009fe90f3e62b8c7b0251f302ff2845b583d58..aaa5f93058b1c7919e7eb0ecfbda8ebcaf6de7c9
@@@ -29,7 -29,6 +29,7 @@@ const float ITEM_RESPAWN_TICKS = 10
  
  .float max_armorvalue;
  .float pickup_anyway;
 +.int count;
  
  .float scheduledrespawntime;
  .float respawntime;
@@@ -110,9 -109,11 +110,11 @@@ spawnfunc(target_items)
  
  #define PREGIVE_WEAPONS(e) WepSet save_weapons; save_weapons = STAT(WEAPONS, e)
  #define PREGIVE(e,f) float save_##f; save_##f = (e).f
+ #define PREGIVE_STATUSEFFECT(e,f) float save_##f = StatusEffects_gettime((f), (e))
  #define PREGIVE_RESOURCE(e,f) float save_##f = GetResource((e), (f))
  #define POSTGIVE_WEAPON(e,b,snd_incr,snd_decr) GiveSound((e), !!(save_weapons & WepSet_FromWeapon(b)), !!(STAT(WEAPONS, e) & WepSet_FromWeapon(b)), 0, snd_incr, snd_decr)
  #define POSTGIVE_BIT(e,f,b,snd_incr,snd_decr) GiveSound((e), save_##f & (b), (e).f & (b), 0, snd_incr, snd_decr)
+ #define POSTGIVE_STATUSEFFECT(e,f,t,snd_incr,snd_decr) GiveSound((e), save_##f, StatusEffects_gettime((f), (e)), t, snd_incr, snd_decr)
  #define POSTGIVE_RESOURCE(e,f,t,snd_incr,snd_decr) GiveSound((e), save_##f, GetResource((e), (f)), t, snd_incr, snd_decr)
  #define POSTGIVE_RES_ROT(e,f,t,rotfield,rottime,regenfield,regentime,snd_incr,snd_decr) GiveRot((e),save_##f,GetResource((e),(f)),rotfield,rottime,regenfield,regentime);GiveSound((e),save_##f,GetResource((e),(f)),t,snd_incr,snd_decr)
  #define POSTGIVE_VALUE(e,f,t,snd_incr,snd_decr) GiveSound((e), save_##f, (e).f, t, snd_incr, snd_decr)
diff --combined qcsrc/server/main.qc
index fb7df4ade9589d6bbbe80f77007c18947e8bcea8,73f473ae979752c412265e06e3bf596daf66dee8..9ce1ec14093ea57772039ee5bafa0f368684ccae
  #include <server/weapons/csqcprojectile.qh>
  #include <server/world.qh>
  
+ void dropclient_do(entity this)
+ {
+       if (this.owner)
+               dropclient(this.owner);
+       delete(this);
+ }
+ /**
+  * Schedules dropclient for a player and returns true;
+  * if dropclient is already scheduled (for that player) it does nothing and returns false.
+  *
+  * NOTE: this function exists only to allow sending a message to the kicked player with
+  * Send_Notification, which doesn't work if called together with dropclient
+  */
+ bool dropclient_schedule(entity this)
+ {
+       bool scheduled = false;
+       FOREACH_ENTITY_CLASS("dropclient_handler", true,
+       {
+               if(it.owner == this)
+               {
+                       scheduled = true;
+                       break; // can't use return here, compiler shows a warning
+               }
+       });
+       if (scheduled)
+               return false;
+       entity e = new_pure(dropclient_handler);
+       setthink(e, dropclient_do);
+       e.owner = this;
+       e.nextthink = time + 0.1;
+       return true;
+ }
  void CreatureFrame_hotliquids(entity this)
  {
        if (this.contents_damagetime >= time)
@@@ -331,7 -365,7 +365,7 @@@ void SV_OnEntityPreSpawnFunction(entit
                return;
        }
  
 -      if (DoesQ3ARemoveThisEntity(this)) {
 +      if (q3compat && DoesQ3ARemoveThisEntity(this)) {
                delete(this);
                return;
        }
        }
  }
  
 +string GetField_fullspawndata(entity e, string f, ...)
 +/* Retrieves the value of a map entity field from fullspawndata
 + * This bypasses field value changes made by the engine,
 + * eg string-to-float and escape sequence substitution.
 + *
 + * Avoids the need to declare fields just to read them once :)
 + *
 + * Returns the last instance of the field to match DarkPlaces behaviour.
 + * Path support: converts \ to / and tests the file if a third (bool, true) arg is passed.
 + * Returns string_null if the entity does not have the field, or the file is not in the VFS.
 + *
 + * FIXME: entities with //comments are not supported.
 + */
 +{
 +      string v = string_null;
 +
 +      if (!e.fullspawndata)
 +      {
 +              LOG_WARNF("^1EDICT %s (classname %s) has no fullspawndata, engine lacks support?", ftos(num_for_edict(e)), e.classname);
 +              return v;
 +      }
 +
 +      if (strstrofs(e.fullspawndata, "//", 0) >= 0)
 +      {
 +              // tokenize and tokenize_console return early if "//" is reached,
 +              // which can leave an odd number of tokens and break key:value pairing.
 +              LOG_WARNF("^1EDICT %s fullspawndata contains unsupported //comment^7%s", ftos(num_for_edict(e)), e.fullspawndata);
 +              return v;
 +      }
 +
 +      //print(sprintf("%s(EDICT %s, FIELD %s)\n", __FUNC__, ftos(num_for_edict(e)), f));
 +      //print(strcat("FULLSPAWNDATA:", e.fullspawndata, "\n"));
 +
 +      // tokenize treats \ as an escape, but tokenize_console returns the required literal
 +      for (int t = tokenize_console(e.fullspawndata) - 3; t > 0; t -= 2)
 +      {
 +              //print(sprintf("\tTOKEN %s:%s\t%s:%s\n", ftos(t), ftos(t + 1), argv(t), argv(t + 1)));
 +              if (argv(t) == f)
 +              {
 +                      v = argv(t + 1);
 +                      break;
 +              }
 +      }
 +
 +      //print(strcat("RESULT: ", v, "\n\n"));
 +
 +      if (v && ...(0, bool) == true)
 +      {
 +              v = strreplace("\\", "/", v);
 +              if (whichpack(v) == "")
 +                      return string_null;
 +      }
 +
 +      return v;
 +}
 +
  void WarpZone_PostInitialize_Callback()
  {
        // create waypoint links for warpzones
diff --combined qcsrc/server/race.qc
index f3f0ac907575f7e93e235c4b34e5d0fc6b5420d3,c940afb94dc4af026a48549bf321fb9d14c32744..715d00d46921df81756e1d6209d4b3a91bc5930c
@@@ -30,6 -30,8 +30,8 @@@
  #include <server/weapons/common.qh>
  #include <server/world.qh>
  
+ .string stored_netname; // TODO: store this information independently of race-based gamemodes
  string uid2name(string myuid)
  {
        string s = db_get(ServerProgsDB, strcat("/uid2name/", myuid));
@@@ -64,11 -66,6 +66,6 @@@ void write_recordmarker(entity pl, floa
  
  IntrusiveList g_race_targets;
  IntrusiveList g_racecheckpoints;
- STATIC_INIT(g_race)
- {
-       g_race_targets = IL_NEW();
-       g_racecheckpoints = IL_NEW();
- }
  
  void race_InitSpectator()
  {
  
  float race_readTime(string map, float pos)
  {
-       string rr = ((g_cts) ? CTS_RECORD : ((g_ctf) ? CTF_RECORD : RACE_RECORD));
-       return stof(db_get(ServerProgsDB, strcat(map, rr, "time", ftos(pos))));
+       return stof(db_get(ServerProgsDB, strcat(map, record_type, "time", ftos(pos))));
  }
  
  string race_readUID(string map, float pos)
  {
-       string rr = ((g_cts) ? CTS_RECORD : ((g_ctf) ? CTF_RECORD : RACE_RECORD));
-       return db_get(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(pos)));
+       return db_get(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(pos)));
  }
  
  float race_readPos(string map, float t)
  
  void race_writeTime(string map, float t, string myuid)
  {
-       string rr = ((g_cts) ? CTS_RECORD : ((g_ctf) ? CTF_RECORD : RACE_RECORD));
        float newpos;
        newpos = race_readPos(map, t);
  
                // player improved his existing record, only have to iterate on ranks between new and old recs
                for (i = prevpos; i > newpos; --i)
                {
-                       db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
-                       db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
+                       db_put(ServerProgsDB, strcat(map, record_type, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
+                       db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
                }
        }
        else
                {
                        float other_time = race_readTime(map, i - 1);
                        if (other_time) {
-                               db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(other_time));
-                               db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
+                               db_put(ServerProgsDB, strcat(map, record_type, "time", ftos(i)), ftos(other_time));
+                               db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
                        }
                }
        }
  
        // store new time itself
-       db_put(ServerProgsDB, strcat(map, rr, "time", ftos(newpos)), ftos(t));
-       db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(newpos)), myuid);
+       db_put(ServerProgsDB, strcat(map, record_type, "time", ftos(newpos)), ftos(t));
+       db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(newpos)), myuid);
  }
  
  string race_readName(string map, float pos)
  {
-       string rr = ((g_cts) ? CTS_RECORD : ((g_ctf) ? CTF_RECORD : RACE_RECORD));
+       return uid2name(db_get(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(pos))));
+ }
  
-       return uid2name(db_get(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(pos))));
+ void race_checkAndWriteName(entity player)
+ {
+       if(CS_CVAR(player).cvar_cl_allow_uidtracking == 1 && CS_CVAR(player).cvar_cl_allow_uid2name == 1)
+       {
+               if (!player.stored_netname)
+                       player.stored_netname = strzone(uid2name(player.crypto_idfp));
+               if(player.stored_netname != player.netname)
+               {
+                       db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
+                       strcpy(player.stored_netname, player.netname);
+               }
+       }
  }
  
  
@@@ -252,7 -255,6 +255,6 @@@ void race_send_recordtime(float msg
        WriteInt24_t(msg, race_readTime(GetMapname(), 1));
  }
  
  void race_send_speedaward(float msg)
  {
        // send the best speed of the round
@@@ -279,7 -281,7 +281,7 @@@ void race_send_rankings_cnt(float msg
        WriteByte(msg, m);
  }
  
- void race_SendRankings(float pos, float prevpos, float del, float msg)
+ void race_SendRanking(float pos, float prevpos, float del, float msg)
  {
        WriteHeader(msg, TE_CSQC_RACE);
        WriteByte(msg, RACE_NET_SERVER_RANKINGS);
        WriteInt24_t(msg, race_readTime(GetMapname(), pos));
  }
  
+ void race_SpeedAwardFrame(entity player)
+ {
+       if (IS_OBSERVER(player))
+               return;
+       if(vdist(player.velocity - player.velocity_z * '0 0 1', >, speedaward_speed))
+       {
+               speedaward_speed = vlen(player.velocity - player.velocity_z * '0 0 1');
+               speedaward_holder = player.netname;
+               speedaward_uid = player.crypto_idfp;
+               speedaward_lastupdate = time;
+       }
+       if (speedaward_speed > speedaward_lastsent && (time - speedaward_lastupdate > 1 || intermission_running))
+       {
+               race_send_speedaward(MSG_ALL);
+               speedaward_lastsent = speedaward_speed;
+               if (speedaward_speed > speedaward_alltimebest && speedaward_uid != "")
+               {
+                       speedaward_alltimebest = speedaward_speed;
+                       speedaward_alltimebest_holder = speedaward_holder;
+                       speedaward_alltimebest_uid = speedaward_uid;
+                       db_put(ServerProgsDB, strcat(GetMapname(), record_type, "speed/speed"), ftos(speedaward_alltimebest));
+                       db_put(ServerProgsDB, strcat(GetMapname(), record_type, "speed/crypto_idfp"), speedaward_alltimebest_uid);
+                       race_send_speedaward_alltimebest(MSG_ALL);
+               }
+       }
+ }
+ void race_SendAll(entity player, bool only_rankings)
+ {
+       if(!IS_REAL_CLIENT(player))
+               return;
+       msg_entity = player;
+       if (!only_rankings)
+       {
+               race_send_recordtime(MSG_ONE);
+               race_send_speedaward(MSG_ONE);
+               speedaward_alltimebest = stof(db_get(ServerProgsDB, strcat(GetMapname(), record_type, "speed/speed")));
+               speedaward_alltimebest_holder = uid2name(db_get(ServerProgsDB, strcat(GetMapname(), record_type, "speed/crypto_idfp")));
+               race_send_speedaward_alltimebest(MSG_ONE);
+       }
+       int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
+       race_send_rankings_cnt(MSG_ONE);
+       for (int i = 1; i <= m; ++i)
+               race_SendRanking(i, 0, 0, MSG_ONE);
+ }
  void race_SendStatus(float id, entity e)
  {
        if(!IS_REAL_CLIENT(e))
@@@ -370,7 -422,7 +422,7 @@@ void race_setTime(string map, float t, 
                race_send_recordtime(MSG_ALL);
        }
  
-       race_SendRankings(newpos, player_prevpos, 0, MSG_ALL);
+       race_SendRanking(newpos, player_prevpos, 0, MSG_ALL);
        strcpy(rankings_reply, getrankings());
  
        if(newpos == player_prevpos)
  
  void race_deleteTime(string map, float pos)
  {
-       string rr = ((g_cts) ? CTS_RECORD : ((g_ctf) ? CTF_RECORD : RACE_RECORD));
        for(int i = pos; i <= RANKINGS_CNT; ++i)
        {
                string therank = ftos(i);
                if (i == RANKINGS_CNT)
                {
-                       db_remove(ServerProgsDB, strcat(map, rr, "time", therank));
-                       db_remove(ServerProgsDB, strcat(map, rr, "crypto_idfp", therank));
+                       db_remove(ServerProgsDB, strcat(map, record_type, "time", therank));
+                       db_remove(ServerProgsDB, strcat(map, record_type, "crypto_idfp", therank));
                }
                else
                {
-                       db_put(ServerProgsDB, strcat(map, rr, "time", therank), ftos(race_readTime(GetMapname(), i+1)));
-                       db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", therank), race_readUID(GetMapname(), i+1));
+                       db_put(ServerProgsDB, strcat(map, record_type, "time", therank), ftos(race_readTime(GetMapname(), i+1)));
+                       db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", therank), race_readUID(GetMapname(), i+1));
                }
        }
  
-       race_SendRankings(pos, 0, 1, MSG_ALL);
+       race_SendRanking(pos, 0, 1, MSG_ALL);
        if(pos == 1)
                race_send_recordtime(MSG_ALL);
  
@@@ -789,35 -839,9 +839,35 @@@ bool race_waypointsprite_visible_for_pl
                return false;
  }
  
 +void defrag_waypointsprites(entity targeted, entity checkpoint)
 +{
 +      for(entity t = findchain(target, targeted.targetname); t; t = t.chain)
 +      {
 +              if(t.modelindex)
 +              {
 +                      entity s = WP_RaceStart;
 +
 +                      if(checkpoint.classname == "target_checkpoint")
 +                              s = WP_RaceCheckpoint;
 +                      else if(checkpoint.classname == "target_stopTimer")
 +                              s = WP_RaceFinish;
 +
 +                      vector o = (t.absmin + t.absmax) * 0.5;
 +
 +                      WaypointSprite_SpawnFixed(s, o, t, sprite, RADARICON_NONE);
 +
 +                      t.sprite.realowner = checkpoint;
 +                      t.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
 +              }
 +
 +              if(t.targetname)
 +                      defrag_waypointsprites(t, checkpoint);
 +      }
 +}
 +
  void trigger_race_checkpoint_verify(entity this)
  {
 -    static bool have_verified;
 +      static bool have_verified;
        if (have_verified) return;
        have_verified = true;
  
                        pl_race_place = 0;
                        if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
                                error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for respawning in race) - bailing out"));
 -            }
 +                      }
  
                        if (i == 0) {
                                // qualifying only
                                pl_race_place = race_lowest_place_spawn;
                                if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
                                        error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for qualifying) - bailing out"));
 -                }
 +                              }
  
                                // race only (initial spawn)
                                g_race_qualifying = 0;
                                        pl_race_place = p;
                                        if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
                                                error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for initially spawning in race) - bailing out"));
 -                    }
 +                                      }
                                }
                        }
                }
                pl_race_place = race_lowest_place_spawn;
                if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
                        error(strcat("Checkpoint 0 misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for qualifying) - bailing out"));
 -        }
 +              }
        } else {
                pl_race_checkpoint = race_NextCheckpoint(0);
                g_race_qualifying = 1;
                                for (entity cp = NULL; (cp = find(cp, classname, "target_checkpoint"));) {
                                        if (argv(0) == cp.targetname) {
                                                cp.race_checkpoint = stof(argv(1));
 -                    }
 -                }
 +                                      }
 +                              }
                        }
                        fclose(fh);
                }
  
        g_race_qualifying = qual;
  
 -      IL_EACH(g_race_targets, it.classname == "target_checkpoint" || it.classname == "target_startTimer" || it.classname == "target_stopTimer",
 -      {
 -              if(it.targetname == "" || !it.targetname) // somehow this is a case...
 -                      continue;
 -              entity cpt = it;
 -              FOREACH_ENTITY_STRING(target, cpt.targetname,
 -              {
 -                      vector org = (it.absmin + it.absmax) * 0.5;
 -                      if(cpt.race_checkpoint == 0)
 -                              WaypointSprite_SpawnFixed(WP_RaceStart, org, it, sprite, RADARICON_NONE);
 -                      else
 -                              WaypointSprite_SpawnFixed(WP_RaceCheckpoint, org, it, sprite, RADARICON_NONE);
 -
 -                      it.sprite.realowner = cpt;
 -                      it.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
 -              });
 -      });
 -
        if (race_timed_checkpoint) {
                if (defrag_ents) {
                        IL_EACH(g_race_targets, it.classname == "target_checkpoint" || it.classname == "target_startTimer" || it.classname == "target_stopTimer",
                        {
 -                              entity cpt = it;
 -                              if(it.classname == "target_startTimer" || it.classname == "target_stopTimer") {
 -                                      if(it.targetname == "" || !it.targetname) // somehow this is a case...
 -                                              continue;
 -                                      FOREACH_ENTITY_STRING(target, cpt.targetname, {
 -                                              if(it.sprite)
 -                                                      WaypointSprite_UpdateSprites(it.sprite, ((cpt.classname == "target_startTimer") ? WP_RaceStart : WP_RaceFinish), WP_Null, WP_Null);
 -                                      });
 -                              }
 +                              defrag_waypointsprites(it, it);
 +
                                if(it.classname == "target_checkpoint") {
                                        if(it.race_checkpoint == -2)
                                                defragcpexists = -1; // something's wrong with the defrag cp file or it has not been written yet, set defragcpexists to -1 so that it will be rewritten when someone finishes
                                for (entity cp = NULL; (cp = find(cp, classname, "target_checkpoint"));) {
                                        if (cp.race_checkpoint > largest_cp_id) {
                                                largest_cp_id = cp.race_checkpoint;
 -                    }
 -                }
 +                                      }
 +                              }
                                for (entity cp = NULL; (cp = find(cp, classname, "target_stopTimer"));) {
                                        cp.race_checkpoint = largest_cp_id + 1; // finish line
 -                }
 +                              }
                                race_highest_checkpoint = largest_cp_id + 1;
                                race_timed_checkpoint = largest_cp_id + 1;
                        } else {
                                for (entity cp = NULL; (cp = find(cp, classname, "target_stopTimer"));) {
                                        cp.race_checkpoint = 255; // finish line
 -                }
 +                              }
                                race_highest_checkpoint = 255;
                                race_timed_checkpoint = 255;
                        }
                        {
                                if (it.race_checkpoint == 0) {
                                        WaypointSprite_UpdateSprites(it.sprite, WP_RaceStart, WP_Null, WP_Null);
 -                } else if (it.race_checkpoint == race_timed_checkpoint) {
 +                              } else if (it.race_checkpoint == race_timed_checkpoint) {
                                        WaypointSprite_UpdateSprites(it.sprite, WP_RaceFinish, WP_Null, WP_Null);
                                }
 -            });
 +                      });
                }
        }
  
 -      if (defrag_ents) {
 +      if (defrag_ents) { /* The following hack shall be removed when per-player trigger_multiple.wait is implemented for cts */
                for (entity trigger = NULL; (trigger = find(trigger, classname, "trigger_multiple")); ) {
                        for (entity targ = NULL; (targ = find(targ, targetname, trigger.target)); ) {
                                if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
@@@ -1037,8 -1086,14 +1087,14 @@@ spawnfunc(trigger_race_checkpoint
        this.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
        this.spawn_evalfunc = trigger_race_checkpoint_spawn_evalfunc;
  
+       if (!g_racecheckpoints)
+               g_racecheckpoints = IL_NEW();
        IL_PUSH(g_racecheckpoints, this);
  
+       // trigger_race_checkpoint_verify checks this list too
+       if (!g_race_targets)
+               g_race_targets = IL_NEW();
        InitializeEntity(this, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
  }
  
@@@ -1082,8 -1137,14 +1138,14 @@@ void target_checkpoint_setup(entity thi
  
        race_timed_checkpoint = 1;
  
+       if (!g_race_targets)
+               g_race_targets = IL_NEW();
        IL_PUSH(g_race_targets, this);
  
+       // trigger_race_checkpoint_verify checks this list too
+       if (!g_racecheckpoints)
+               g_racecheckpoints = IL_NEW();
        InitializeEntity(this, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
  }
  
diff --combined qcsrc/server/teamplay.qc
index ff6de57bc465ef5dcc1a81c5f9ebdf5c0dde156c,11509b5428a74f26d96fdc29c21bdfb7019317e5..c26a0b16550ac51062a40c742468d5f93f214076
@@@ -28,6 -28,8 +28,6 @@@ enu
  /// \brief Indicates that the player is not allowed to join a team.
  const int TEAM_NOT_ALLOWED = -1;
  
 -.float team_forced; // can be a team number to force a team, or 0 for default action, or -1 for forced spectator
 -
  .int m_team_balance_state; ///< Holds the state of the team balance entity.
  .entity m_team_balance_team[NUM_TEAMS]; ///< ???
  
@@@ -44,11 -46,13 +44,13 @@@ string autocvar_g_forced_team_pink
  
  entity g_team_entities[NUM_TEAMS]; ///< Holds global team entities.
  
STATIC_INIT(g_team_entities)
void Team_InitTeams()
  {
+       if (g_team_entities[0])
+               return;
        for (int i = 0; i < NUM_TEAMS; ++i)
        {
-               g_team_entities[i] = new_pure();
+               g_team_entities[i] = new_pure(team_entity);
        }
  }
  
@@@ -205,38 -209,39 +207,39 @@@ bool Player_SetTeamIndex(entity player
  bool SetPlayerTeam(entity player, int team_index, int type)
  {
        int old_team_index = Entity_GetTeamIndex(player);
        if (!Player_SetTeamIndex(player, team_index))
-       {
                return false;
-       }
        LogTeamChange(player.playerid, player.team, type);
        if (team_index != old_team_index)
        {
-               PlayerScore_Clear(player);
-               if (team_index != -1)
-               {
-                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(
-                               player.team, INFO_JOIN_PLAY_TEAM), player.netname);
-               }
-               else
-               {
-                       if (!CS(player).just_joined)
-                       {
-                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE,
-                                       player.netname);
-                       }
-               }
                KillPlayerForTeamChange(player);
+               PlayerScore_Clear(player);
+               CS(player).parm_idlesince = time;
                if (!IS_BOT_CLIENT(player))
-               {
                        TeamBalance_AutoBalanceBots();
-               }
+               if (team_index != -1)
+                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team, INFO_JOIN_PLAY_TEAM), player.netname);
        }
-       else if (team_index == -1)
+       if (team_index == -1)
        {
-               if (!CS(player).just_joined && player.frags != FRAGS_SPECTATOR)
+               if (autocvar_sv_maxidle_playertospectator > 0 && CS(player).idlekick_lasttimeleft)
+               {
+                       // this done here so it happens even when manually speccing during the countdown
+                       Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_IDLING);
+                       CS(player).idlekick_lasttimeleft = 0;
+               }
+               else if (!CS(player).just_joined && player.frags != FRAGS_SPECTATOR)
+               {
                        Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE, player.netname);
+               }
        }
        return true;
  }
  
diff --combined qcsrc/server/teamplay.qh
index a8c48be15ddceff941391f350df3077ce62bbb6a,5cce9758dfb63dc44e0c798033ffa5aaf51ad965..06787c6ffa06c72ec593b4429560949c99e74996
@@@ -12,10 -12,10 +12,12 @@@ string autocvar_g_forced_team_otherwise
  
  bool lockteams;
  
 +.int team_forced; // can be a team number to force a team, or 0 for default action, or -1 for forced spectator
 +
  // ========================== Global teams API ================================
  
+ void Team_InitTeams();
  /// \brief Returns the global team entity at the given index.
  /// \param[in] index Index of the team.
  /// \return Global team entity at the given index.
diff --combined qcsrc/server/world.qc
index d57a0af9eb29bf45b33c227f4ceab508d0821266,6628b4236c39ab3ed7d1592af909b50e9b4c1356..c5032d9453f0d885492832807f9aa163b0801150
@@@ -348,6 -348,7 +348,7 @@@ void cvar_changes_init(
                BADCVAR("g_chatsounds");
                BADCVAR("g_ca_point_leadlimit");
                BADCVAR("g_ca_point_limit");
+               BADCVAR("g_ca_spectate_enemies");
                BADCVAR("g_ctf_captimerecord_always");
                BADCVAR("g_ctf_flag_glowtrails");
                BADCVAR("g_ctf_dynamiclights");
                BADPREFIX("skill_");
                BADPREFIX("sv_allow_");
                BADPREFIX("sv_cullentities_");
-               BADPREFIX("sv_maxidle_");
+               BADPREFIX("sv_maxidle");
                BADPREFIX("sv_minigames_");
                BADPREFIX("sv_radio_");
                BADPREFIX("sv_timeout_");
                BADCVAR("sv_defaultplayercolors");
                BADCVAR("sv_defaultplayermodel");
                BADCVAR("sv_defaultplayerskin");
-               BADCVAR("sv_maxidle");
                BADCVAR("sv_maxrate");
                BADCVAR("sv_motd");
                BADCVAR("sv_public");
@@@ -755,6 -755,9 +755,9 @@@ spawnfunc(worldspawn
  
        cvar_changes_init(); // do this very early now so it REALLY matches the server config
  
+       // default to RACE_RECORD, can be overwritten by gamemodes
+       record_type = RACE_RECORD;
        // needs to be done so early because of the constants they create
        static_init();
  
        MapInfo_Enumerate();
        MapInfo_FilterGametype(MapInfo_CurrentGametype(), MapInfo_CurrentFeatures(), MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags(), 1);
  
 -      if(fexists(strcat("scripts/", mapname, ".arena")))
 -              cvar_settemp("sv_q3acompat_machineshotgunswap", "1");
 -
 -      if(fexists(strcat("scripts/", mapname, ".defi")))
 -              cvar_settemp("sv_q3defragcompat", "1");
 +      q3compat = BITSET(q3compat, Q3COMPAT_ARENA, fexists(strcat("scripts/", mapname, ".arena")));
 +      q3compat = BITSET(q3compat, Q3COMPAT_DEFI, fexists(strcat("scripts/", mapname, ".defi")));
  
        if(whichpack(strcat("maps/", mapname, ".cfg")) != "")
        {
diff --combined xonotic-server.cfg
index e4db3b8f62d5f87be1754e03d356a5ac8f58136c,be57248acc8b3304fcefd6bbc96c005d779ca50d..52cacc25c2388d8ec5ce745042572203c0bb91bb
@@@ -311,9 -311,9 +311,9 @@@ set sv_logscores_filename scores.log "f
  set sv_logscores_bots 0 "exclude bots by default"
  
  // spam (frag/capture) log
- set sv_eventlog 0 "the master switch for efficiency reasons"
- set sv_eventlog_console 1 "print event log entries to the console as well"
- set sv_eventlog_files 0 "save the event log to individual files instead of the main server log"
+ set sv_eventlog 0 "enable event logging"
+ set sv_eventlog_console 1 "print event log entries to the dedicated console as well"
+ set sv_eventlog_files 0 "save the event log to individual files"
  set sv_eventlog_files_timestamps 1 "include timestamps in the log file names"
  set sv_eventlog_files_counter 0 "internal counter cvar, do not modify"
  set sv_eventlog_files_nameprefix xonotic "prefix of individual log file names"
@@@ -412,10 -412,12 +412,12 @@@ sv_gameplayfix_droptofloorstartsolid 
  set sv_foginterval 1 "force enable fog in regular intervals"
  
  set sv_maxidle 0 "kick players idle for more than this amount of time in seconds"
- set sv_maxidle_spectatorsareidle 0 "when sv_maxidle is not 0, assume spectators are idle too"
+ set sv_maxidle_alsokickspectators 1 "when sv_maxidle is > 0, kick idle spectators as well as players"
  set sv_maxidle_slots 0 "when not 0, only kick idlers when this many or less player slots are available"
  set sv_maxidle_slots_countbots 1 "count bots as player slots"
  
+ set sv_maxidle_playertospectator 60 "move players idle for more than this amount of time in seconds to spectators (sv_maxidle timer starts again after sv_maxidle_playertospectator has moved a player to spectators)"
  sv_allowdownloads_inarchive 1 // for csprogs.dat
  sv_allowdownloads 0 // download protocol is evil
  
@@@ -494,7 -496,8 +496,7 @@@ sv_gameplayfix_consistentplayerprethin
  sv_gameplayfix_gravityunaffectedbyticrate 1
  sv_gameplayfix_nogravityonground 1
  
 -set sv_q3acompat_machineshotgunswap 0 "shorthand for swapping machinegun and shotgun (for Q3A map compatibility in mapinfo files)"
 -set sv_q3defragcompat 0 "toggle for some compatibility hacks (for Q3DF map compatibility)"
 +set sv_q3compat_changehitbox 0 "use Q3 player hitbox dimensions and camera height on Q3 maps (maps with an entry in a .arena or .defi file)
  
  set g_movement_highspeed 1 "multiplier scale for movement speed (applies to sv_maxspeed and sv_maxairspeed, also applies to air acceleration when g_movement_highspeed_q3_compat is set to 0)"
  set g_movement_highspeed_q3_compat 0 "apply speed modifiers to air movement in a more Q3-compatible way (only apply speed buffs and g_movement_highspeed to max air speed, not to acceleration)"