]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into TimePath/modules
authorTimePath <andrew.hardaker1995@gmail.com>
Sun, 21 Aug 2016 04:44:45 +0000 (14:44 +1000)
committerTimePath <andrew.hardaker1995@gmail.com>
Sun, 21 Aug 2016 04:45:23 +0000 (14:45 +1000)
# Conflicts:
# qcsrc/common/gamemodes/gamemode/onslaught/onslaught.qc
# qcsrc/common/mutators/mutator/campcheck/campcheck.qc
# qcsrc/server/mutators/mutator/gamemode_domination.qh

22 files changed:
1  2 
qcsrc/client/hud/hud.qc
qcsrc/client/hud/hud.qh
qcsrc/client/hud/panel/powerups.qc
qcsrc/client/main.qh
qcsrc/client/view.qc
qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc
qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qh
qcsrc/common/monsters/sv_monsters.qc
qcsrc/common/mutators/mutator/campcheck/sv_campcheck.qc
qcsrc/common/mutators/mutator/itemstime/itemstime.qc
qcsrc/common/mutators/mutator/spawn_near_teammate/sv_spawn_near_teammate.qc
qcsrc/common/stats.qh
qcsrc/server/defs.qh
qcsrc/server/g_world.qc
qcsrc/server/miscfunctions.qc
qcsrc/server/mutators/events.qh
qcsrc/server/mutators/mutator/gamemode_domination.qh
qcsrc/server/player.qc
qcsrc/server/player.qh
qcsrc/server/teamplay.qc
qcsrc/server/weapons/tracing.qc
qcsrc/server/weapons/weaponsystem.qc

diff --combined qcsrc/client/hud/hud.qc
index 8b23dd6fb95a6c8559877d99a1ed428c26e45249,c87e733e1d4e633dc2bd28e815d8c885c89ab22e..91dc98fdcb2c32f7efa917bafd3b8912c1574644
@@@ -1,13 -1,11 +1,13 @@@
  #include "hud.qh"
  
 +#include "panel/scoreboard.qh"
 +
  #include "hud_config.qh"
  #include "../mapvoting.qh"
  #include "../teamradar.qh"
  #include <common/t_items.qh>
  #include <common/deathtypes/all.qh>
 -#include <common/items/all.qc>
 +#include <common/items/_mod.qh>
  #include <common/mapinfo.qh>
  #include <common/vehicles/all.qh>
  #include <common/mutators/mutator/waypoints/all.qh>
@@@ -336,6 -334,8 +336,6 @@@ void DrawNumIcon(vector myPos, vector m
        DrawNumIcon_expanding(myPos, mySize, x, icon, vertical, icon_right_align, color, theAlpha, 0);
  }
  
 -#include "all.inc"
 -
  /*
  ==================
  Main HUD system
@@@ -357,8 -357,6 +357,8 @@@ void HUD_Vehicle(
        }
  }
  
 +bool HUD_Minigame_Showpanels();
 +
  bool HUD_Panel_CheckFlags(int showflags)
  {
      TC(int, showflags);
@@@ -415,7 -413,6 +415,7 @@@ bool Hud_Shake_Update(
        return true;
  }
  
 +entity CSQCModel_server2csqc(int i);
  void calc_followmodel_ofs(entity view);
  void Hud_Dynamic_Frame()
  {
@@@ -496,7 -493,7 +496,7 @@@ void HUD_Main(
  {
        int i;
        // global hud alpha fade (scoreboard-related panels behave differently and override it temporarly)
-       if(menu_enabled == 1)
+       if(hud_configure_menu_open == 1)
                hud_fade_alpha = 1;
        else if(!autocvar__hud_configure)
                hud_fade_alpha = (1 - scoreboard_fade_alpha) * (1 - autocvar__menu_alpha);
diff --combined qcsrc/client/hud/hud.qh
index 0dc89a8b4dffbb0b87ddeb4fcf12982409b506a7,b269312d8871ceb2212ea6fd8beba1f15d28779d..7ee177524bea40d90eb5c547183ddba56a7859a4
@@@ -1,6 -1,6 +1,6 @@@
  #pragma once
  
 -#include <common/weapons/all.qh>
 +#include <common/weapons/_all.qh>
  
  bool HUD_Radar_Clickable();
  void HUD_Radar_Mouse();
@@@ -98,18 -98,11 +98,11 @@@ float weaponprevtime
  
  float teamnagger;
  
- float hud_configure_checkcollisions;
- float hud_configure_prev;
- vector hud_configure_gridSize;
- vector hud_configure_realGridSize;
  int hudShiftState;
  const int S_SHIFT = 1;
  const int S_CTRL = 2;
  const int S_ALT = 4;
  
- float menu_enabled; // 1 showing the entire HUD, 2 showing only the clicked panel
  float hud_fade_alpha;
  
  string hud_skin_path;
@@@ -316,7 -309,7 +309,7 @@@ REGISTER_HUD_PANEL(SCOREBOARD,      Sco
        if (autocvar__hud_configure) {                                                                                  \
                if (!panel_enabled)                                                                                         \
                        panel_bg_alpha = 0.25;                                                                                  \
-               else if (menu_enabled == 2 && panel == highlightedPanel)                                                    \
+               else if (hud_configure_menu_open == 2 && panel == highlightedPanel)                                                    \
                        panel_bg_alpha = (1 - autocvar__menu_alpha) * max(cvar("hud_configure_bg_minalpha"), panel_bg_alpha) + autocvar__menu_alpha * panel_bg_alpha;\
                else                                                                                                        \
                        panel_bg_alpha = max(cvar("hud_configure_bg_minalpha"), panel_bg_alpha);                                \
  // return smoothly faded pos and size of given panel when a dialog is active
  // don't center too wide panels, it doesn't work with different resolutions
  #define HUD_Panel_UpdatePosSize_ForMenu() MACRO_BEGIN { \
-       vector menu_enable_size = panel_size; \
+       vector new_size = panel_size; \
        float max_panel_width = 0.52 * vid_conwidth; \
        if(panel_size.x > max_panel_width) \
        { \
-               menu_enable_size.x = max_panel_width; \
-               menu_enable_size.y = panel_size.y * (menu_enable_size.x / panel_size.x); \
+               new_size.x = max_panel_width; \
+               new_size.y = panel_size.y * (new_size.x / panel_size.x); \
        } \
-       vector menu_enable_pos = eX * (panel_bg_border + 0.5 * max_panel_width) + eY * 0.5 * vid_conheight - 0.5 * menu_enable_size; \
-       panel_pos = (1 - autocvar__menu_alpha) * panel_pos + (autocvar__menu_alpha) * menu_enable_pos; \
-       panel_size = (1 - autocvar__menu_alpha) * panel_size + (autocvar__menu_alpha) * menu_enable_size; \
+       vector new_pos = eX * (panel_bg_border + 0.5 * max_panel_width) + eY * 0.5 * vid_conheight - 0.5 * new_size; \
+       panel_pos = (1 - autocvar__menu_alpha) * panel_pos + (autocvar__menu_alpha) * new_pos; \
+       panel_size = (1 - autocvar__menu_alpha) * panel_size + (autocvar__menu_alpha) * new_size; \
  } MACRO_END
  
  // Scale the pos and size vectors to absolute coordinates
                HUD_Panel_GetPadding();                                                                                     \
                panel.current_panel_bg_alpha = panel_bg_alpha;                                                              \
                panel.current_panel_fg_alpha = panel_fg_alpha;                                                              \
-               if (menu_enabled == 2 && panel == highlightedPanel) {                                                       \
+               if (hud_configure_menu_open == 2 && panel == highlightedPanel) {                                                       \
                        HUD_Panel_UpdatePosSize_ForMenu();                                                                      \
                } else {                                                                                                    \
                        panel_bg_alpha *= hud_fade_alpha;                                                                       \
        panel_pos = stov(cvar_string(strcat("hud_panel_", panel.panel_name, "_pos")));                                  \
        panel_size = stov(cvar_string(strcat("hud_panel_", panel.panel_name, "_size")));                                \
        HUD_Panel_ScalePosSize();                                                                                       \
-       if (menu_enabled == 2 && panel == highlightedPanel) {                                                           \
+       if (hud_configure_menu_open == 2 && panel == highlightedPanel) {                                                           \
                HUD_Panel_UpdatePosSize_ForMenu();                                                                          \
        }                                                                                                               \
        panel_bg_border_str = cvar_string(strcat("hud_panel_", panel.panel_name, "_bg_border"));                        \
index 7f8703f5b685449e8341292fa1af5e3ede97515b,7528c2ba2b1a6f89353b6e4cf83116f5ee4a6473..4779ab747999365cf458f889304a06893484cdef
@@@ -1,6 -1,6 +1,6 @@@
  #include "powerups.qh"
  
 -#include <common/items/all.qc>
 +#include <common/items/_mod.qh>
  
  // Powerups (#2)
  
@@@ -68,9 -68,10 +68,10 @@@ void HUD_Powerups(
        // Initialize items
        if(!autocvar__hud_configure)
        {
-               if(!autocvar_hud_panel_powerups) return;
-               if(spectatee_status == -1) return;
-               if(STAT(HEALTH) <= 0) return;
+               if((!autocvar_hud_panel_powerups) || (spectatee_status == -1))
+                       return;
+               if(STAT(HEALTH) <= 0 && autocvar_hud_panel_powerups_hide_ondeath)
+                       return;
                if(!(allItems & (ITEM_Strength.m_itemid | ITEM_Shield.m_itemid | IT_SUPERWEAPON)) && !allBuffs) return;
  
                strengthTime = bound(0, STAT(STRENGTH_FINISHED) - time, 99);
diff --combined qcsrc/client/main.qh
index 49167845d414d56a6dfa68d77b58330246baa8ea,e736d9d4463732353e3f4608dce038424a9bcd5f..9da7547299dd013e27e8e93731fc7599b70948f3
@@@ -1,7 -1,7 +1,7 @@@
  #pragma once
  
  #include <common/constants.qh>
 -#include <common/weapons/all.qh>
 +#include <common/weapons/_all.qh>
  
  // --------------------------------------------------------------------------
  // MENU Functionality
@@@ -126,7 -126,7 +126,7 @@@ const int HOOK_END =      2
  
  float g_trueaim_minrange;
  
float hud;
int hud;
  float view_quality;
  
  int num_spectators;
diff --combined qcsrc/client/view.qc
index 336707d3cf96fc7c0bf78bc7f6ebd263156fe818,db6df4dadfc972de58f3c8f2c4effbd1f563c883..c629ab30c32bdd33987f3f087f465ee88f15a56c
@@@ -1,7 -1,7 +1,7 @@@
  #include "view.qh"
  
  #include "announcer.qh"
 -#include "hud/all.qh"
 +#include "hud/_mod.qh"
  #include "mapvoting.qh"
  #include "shownames.qh"
  #include "hud/panel/scoreboard.qh"
  #include <common/constants.qh>
  #include <common/debug.qh>
  #include <common/mapinfo.qh>
 -#include <common/gamemodes/all.qh>
 +#include <common/gamemodes/_mod.qh>
  #include <common/physics/player.qh>
  #include <common/stats.qh>
  #include <common/triggers/target/music.qh>
  #include <common/teams.qh>
  
  #include <common/vehicles/all.qh>
 -#include <common/weapons/all.qh>
 +#include <common/weapons/_all.qh>
  #include <common/viewloc.qh>
  #include <common/minigames/cl_minigames.qh>
  #include <common/minigames/cl_minigames_hud.qh>
@@@ -937,7 -937,7 +937,7 @@@ void HUD_Crosshair(entity this
        float f, i, j;
        vector v;
        if(!scoreboard_active && !camera_active && intermission != 2 &&
-               spectatee_status != -1 && !csqcplayer.viewloc &&
+               spectatee_status != -1 && !csqcplayer.viewloc && !MUTATOR_CALLHOOK(DrawCrosshair) &&
                !HUD_MinigameMenu_IsOpened() )
        {
                if (!autocvar_crosshair_enabled) // main toggle for crosshair rendering
@@@ -1337,6 -1337,11 +1337,11 @@@ void HUD_Draw(entity this
                DrawCircleClippedPic(eX * 0.5 * vid_conwidth + eY * 0.6 * vid_conheight, 0.1 * vid_conheight, "gfx/crosshair_ring.tga", STAT(NADE_TIMER), '0.25 0.90 1' + ('1 0 0' * STAT(NADE_TIMER)) - ('0 1 1' * STAT(NADE_TIMER)), autocvar_hud_colorflash_alpha, DRAWFLAG_ADDITIVE);
                drawstring_aspect(eY * 0.64 * vid_conheight, ((autocvar_cl_nade_timer == 2) ? _("Nade timer") : ""), eX * vid_conwidth + eY * 0.025 * vid_conheight, '1 1 1', 1, DRAWFLAG_NORMAL);
        }
+       else if(STAT(CAPTURE_PROGRESS))
+       {
+               DrawCircleClippedPic(eX * 0.5 * vid_conwidth + eY * 0.6 * vid_conheight, 0.1 * vid_conheight, "gfx/crosshair_ring.tga", STAT(CAPTURE_PROGRESS), '0.25 0.90 1', autocvar_hud_colorflash_alpha, DRAWFLAG_ADDITIVE);
+               drawstring_aspect(eY * 0.64 * vid_conheight, _("Capture progress"), eX * vid_conwidth + eY * 0.025 * vid_conheight, '1 1 1', 1, DRAWFLAG_NORMAL);
+       }
        else if(STAT(REVIVE_PROGRESS))
        {
                DrawCircleClippedPic(eX * 0.5 * vid_conwidth + eY * 0.6 * vid_conheight, 0.1 * vid_conheight, "gfx/crosshair_ring.tga", STAT(REVIVE_PROGRESS), '0.25 0.90 1', autocvar_hud_colorflash_alpha, DRAWFLAG_ADDITIVE);
@@@ -1368,6 -1373,7 +1373,7 @@@ float oldr_useportalculling
  float oldr_useinfinitefarclip;
  
  void cl_notice_run();
  float prev_myteam;
  int lasthud;
  float vh_notice_time;
@@@ -1825,8 -1831,7 +1831,7 @@@ void CSQC_UpdateView(entity this, floa
        else if(csqcplayer.viewloc) { setproperty(VF_FOV, GetViewLocationFOV(110)); } // enforce 110 fov, so things dont look odd
        else { setproperty(VF_FOV, GetCurrentFov(fov)); }
  
-       // Camera for demo playback
-       if(camera_active)
+       if(camera_active) // Camera for demo playback
        {
                if(autocvar_camera_enable)
                        CSQC_Demo_Camera();
index 9551e9d85ee34b035820bbc5d32c513cce6af9b0,0000000000000000000000000000000000000000..f57625607a4114333779798d067adbcc23bba5a6
mode 100644,000000..100644
--- /dev/null
@@@ -1,2128 -1,0 +1,2128 @@@
-               it.ons_roundlost = true;
 +#include "sv_controlpoint.qh"
 +#include "sv_generator.qh"
 +
 +bool g_onslaught;
 +
 +float autocvar_g_onslaught_teleport_wait;
 +bool autocvar_g_onslaught_spawn_at_controlpoints;
 +bool autocvar_g_onslaught_spawn_at_generator;
 +float autocvar_g_onslaught_cp_proxydecap;
 +float autocvar_g_onslaught_cp_proxydecap_distance = 512;
 +float autocvar_g_onslaught_cp_proxydecap_dps = 100;
 +float autocvar_g_onslaught_spawn_at_controlpoints_chance = 0.5;
 +float autocvar_g_onslaught_spawn_at_controlpoints_random;
 +float autocvar_g_onslaught_spawn_at_generator_chance;
 +float autocvar_g_onslaught_spawn_at_generator_random;
 +float autocvar_g_onslaught_cp_buildhealth;
 +float autocvar_g_onslaught_cp_buildtime;
 +float autocvar_g_onslaught_cp_health;
 +float autocvar_g_onslaught_cp_regen;
 +float autocvar_g_onslaught_gen_health;
 +float autocvar_g_onslaught_shield_force = 100;
 +float autocvar_g_onslaught_allow_vehicle_touch;
 +float autocvar_g_onslaught_round_timelimit;
 +float autocvar_g_onslaught_warmup;
 +float autocvar_g_onslaught_teleport_radius;
 +float autocvar_g_onslaught_spawn_choose;
 +float autocvar_g_onslaught_click_radius;
 +
 +void FixSize(entity e);
 +
 +// =======================
 +// CaptureShield Functions
 +// =======================
 +
 +bool ons_CaptureShield_Customize(entity this, entity client)
 +{
 +      entity e = WaypointSprite_getviewentity(client);
 +
 +      if(!this.enemy.isshielded && (ons_ControlPoint_Attackable(this.enemy, e.team) > 0 || this.enemy.classname != "onslaught_controlpoint")) { return false; }
 +      if(SAME_TEAM(this, e)) { return false; }
 +
 +      return true;
 +}
 +
 +void ons_CaptureShield_Touch(entity this, entity toucher)
 +{
 +      if(!this.enemy.isshielded && (ons_ControlPoint_Attackable(this.enemy, toucher.team) > 0 || this.enemy.classname != "onslaught_controlpoint")) { return; }
 +      if(!IS_PLAYER(toucher)) { return; }
 +      if(SAME_TEAM(toucher, this)) { return; }
 +
 +      vector mymid = (this.absmin + this.absmax) * 0.5;
 +      vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
 +
 +      Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(theirmid - mymid) * ons_captureshield_force);
 +
 +      if(IS_REAL_CLIENT(toucher))
 +      {
 +              play2(toucher, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
 +
 +              if(this.enemy.classname == "onslaught_generator")
 +                      Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_GENERATOR_SHIELDED);
 +              else
 +                      Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_CONTROLPOINT_SHIELDED);
 +      }
 +}
 +
 +void ons_CaptureShield_Reset(entity this)
 +{
 +      this.colormap = this.enemy.colormap;
 +      this.team = this.enemy.team;
 +}
 +
 +void ons_CaptureShield_Spawn(entity generator, bool is_generator)
 +{
 +      entity shield = new(ons_captureshield);
 +
 +      shield.enemy = generator;
 +      shield.team = generator.team;
 +      shield.colormap = generator.colormap;
 +      shield.reset = ons_CaptureShield_Reset;
 +      settouch(shield, ons_CaptureShield_Touch);
 +      setcefc(shield, ons_CaptureShield_Customize);
 +      shield.effects = EF_ADDITIVE;
 +      set_movetype(shield, MOVETYPE_NOCLIP);
 +      shield.solid = SOLID_TRIGGER;
 +      shield.avelocity = '7 0 11';
 +      shield.scale = 1;
 +      shield.model = ((is_generator) ? "models/onslaught/generator_shield.md3" : "models/onslaught/controlpoint_shield.md3");
 +
 +      precache_model(shield.model);
 +      setorigin(shield, generator.origin);
 +      _setmodel(shield, shield.model);
 +      setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
 +}
 +
 +
 +// ==========
 +// Junk Pile
 +// ==========
 +
 +void setmodel_fixsize(entity e, Model m)
 +{
 +      setmodel(e, m);
 +      FixSize(e);
 +}
 +
 +void onslaught_updatelinks()
 +{
 +      entity l;
 +      // first check if the game has ended
 +      LOG_DEBUG("--- updatelinks ---");
 +      // mark generators as being shielded and networked
 +      for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
 +      {
 +              if (l.iscaptured)
 +                      LOG_DEBUG(etos(l), " (generator) belongs to team ", ftos(l.team));
 +              else
 +                      LOG_DEBUG(etos(l), " (generator) is destroyed");
 +              l.islinked = l.iscaptured;
 +              l.isshielded = l.iscaptured;
 +              l.sprite.SendFlags |= 16;
 +      }
 +      // mark points as shielded and not networked
 +      for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
 +      {
 +              l.islinked = false;
 +              l.isshielded = true;
 +              int i;
 +              for(i = 0; i < 17; ++i) { l.isgenneighbor[i] = false; l.iscpneighbor[i] = false; }
 +              LOG_DEBUG(etos(l), " (point) belongs to team ", ftos(l.team));
 +              l.sprite.SendFlags |= 16;
 +      }
 +      // flow power outward from the generators through the network
 +      bool stop = false;
 +      while (!stop)
 +      {
 +              stop = true;
 +              for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
 +              {
 +                      // if both points are captured by the same team, and only one of
 +                      // them is powered, mark the other one as powered as well
 +                      if (l.enemy.iscaptured && l.goalentity.iscaptured)
 +                              if (l.enemy.islinked != l.goalentity.islinked)
 +                                      if(SAME_TEAM(l.enemy, l.goalentity))
 +                                      {
 +                                              if (!l.goalentity.islinked)
 +                                              {
 +                                                      stop = false;
 +                                                      l.goalentity.islinked = true;
 +                                                      LOG_DEBUG(etos(l), " (link) is marking ", etos(l.goalentity), " (point) because its team matches ", etos(l.enemy), " (point)");
 +                                              }
 +                                              else if (!l.enemy.islinked)
 +                                              {
 +                                                      stop = false;
 +                                                      l.enemy.islinked = true;
 +                                                      LOG_DEBUG(etos(l), " (link) is marking ", etos(l.enemy), " (point) because its team matches ", etos(l.goalentity), " (point)");
 +                                              }
 +                                      }
 +              }
 +      }
 +      // now that we know which points are powered we can mark their neighbors
 +      // as unshielded if team differs
 +      for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
 +      {
 +              if (l.goalentity.islinked)
 +              {
 +                      if(DIFF_TEAM(l.goalentity, l.enemy))
 +                      {
 +                              LOG_DEBUG(etos(l), " (link) is unshielding ", etos(l.enemy), " (point) because its team does not match ", etos(l.goalentity), " (point)");
 +                              l.enemy.isshielded = false;
 +                      }
 +                      if(l.goalentity.classname == "onslaught_generator")
 +                              l.enemy.isgenneighbor[l.goalentity.team] = true;
 +                      else
 +                              l.enemy.iscpneighbor[l.goalentity.team] = true;
 +              }
 +              if (l.enemy.islinked)
 +              {
 +                      if(DIFF_TEAM(l.goalentity, l.enemy))
 +                      {
 +                              LOG_DEBUG(etos(l), " (link) is unshielding ", etos(l.goalentity), " (point) because its team does not match ", etos(l.enemy), " (point)");
 +                              l.goalentity.isshielded = false;
 +                      }
 +                      if(l.enemy.classname == "onslaught_generator")
 +                              l.goalentity.isgenneighbor[l.enemy.team] = true;
 +                      else
 +                              l.goalentity.iscpneighbor[l.enemy.team] = true;
 +              }
 +      }
 +      // now update the generators
 +      for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
 +      {
 +              if (l.isshielded)
 +              {
 +                      LOG_DEBUG(etos(l), " (generator) is shielded");
 +                      l.takedamage = DAMAGE_NO;
 +                      l.bot_attack = false;
 +              }
 +              else
 +              {
 +                      LOG_DEBUG(etos(l), " (generator) is not shielded");
 +                      l.takedamage = DAMAGE_AIM;
 +                      l.bot_attack = true;
 +              }
 +
 +              ons_Generator_UpdateSprite(l);
 +      }
 +      // now update the takedamage and alpha variables on control point icons
 +      for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
 +      {
 +              if (l.isshielded)
 +              {
 +                      LOG_DEBUG(etos(l), " (point) is shielded");
 +                      if (l.goalentity)
 +                      {
 +                              l.goalentity.takedamage = DAMAGE_NO;
 +                              l.goalentity.bot_attack = false;
 +                      }
 +              }
 +              else
 +              {
 +                      LOG_DEBUG(etos(l), " (point) is not shielded");
 +                      if (l.goalentity)
 +                      {
 +                              l.goalentity.takedamage = DAMAGE_AIM;
 +                              l.goalentity.bot_attack = true;
 +                      }
 +              }
 +              ons_ControlPoint_UpdateSprite(l);
 +      }
 +      FOREACH_ENTITY_CLASS("ons_captureshield", true,
 +      {
 +              it.team = it.enemy.team;
 +              it.colormap = it.enemy.colormap;
 +      });
 +}
 +
 +
 +// ===================
 +// Main Link Functions
 +// ===================
 +
 +bool ons_Link_Send(entity this, entity to, int sendflags)
 +{
 +      WriteHeader(MSG_ENTITY, ENT_CLIENT_RADARLINK);
 +      WriteByte(MSG_ENTITY, sendflags);
 +      if(sendflags & 1)
 +      {
 +              WriteCoord(MSG_ENTITY, this.goalentity.origin_x);
 +              WriteCoord(MSG_ENTITY, this.goalentity.origin_y);
 +              WriteCoord(MSG_ENTITY, this.goalentity.origin_z);
 +      }
 +      if(sendflags & 2)
 +      {
 +              WriteCoord(MSG_ENTITY, this.enemy.origin_x);
 +              WriteCoord(MSG_ENTITY, this.enemy.origin_y);
 +              WriteCoord(MSG_ENTITY, this.enemy.origin_z);
 +      }
 +      if(sendflags & 4)
 +      {
 +              WriteByte(MSG_ENTITY, this.clientcolors); // which is goalentity's color + enemy's color * 16
 +      }
 +      return true;
 +}
 +
 +void ons_Link_CheckUpdate(entity this)
 +{
 +      // TODO check if the two sides have moved (currently they won't move anyway)
 +      float cc = 0, cc1 = 0, cc2 = 0;
 +
 +      if(this.goalentity.islinked || this.goalentity.iscaptured) { cc1 = (this.goalentity.team - 1) * 0x01; }
 +      if(this.enemy.islinked || this.enemy.iscaptured) { cc2 = (this.enemy.team - 1) * 0x10; }
 +
 +      cc = cc1 + cc2;
 +
 +      if(cc != this.clientcolors)
 +      {
 +              this.clientcolors = cc;
 +              this.SendFlags |= 4;
 +      }
 +
 +      this.nextthink = time;
 +}
 +
 +void ons_DelayedLinkSetup(entity this)
 +{
 +      this.goalentity = find(NULL, targetname, this.target);
 +      this.enemy = find(NULL, targetname, this.target2);
 +      if(!this.goalentity) { objerror(this, "can not find target\n"); }
 +      if(!this.enemy) { objerror(this, "can not find target2\n"); }
 +
 +      LOG_DEBUG(etos(this.goalentity), " linked with ", etos(this.enemy));
 +      this.SendFlags |= 3;
 +      setthink(this, ons_Link_CheckUpdate);
 +      this.nextthink = time;
 +}
 +
 +
 +// =============================
 +// Main Control Point Functions
 +// =============================
 +
 +int ons_ControlPoint_CanBeLinked(entity cp, int teamnumber)
 +{
 +      if(cp.isgenneighbor[teamnumber]) { return 2; }
 +      if(cp.iscpneighbor[teamnumber]) { return 1; }
 +
 +      return 0;
 +}
 +
 +int ons_ControlPoint_Attackable(entity cp, int teamnumber)
 +      // -2: SAME TEAM, attackable by enemy!
 +      // -1: SAME TEAM!
 +      // 0: off limits
 +      // 1: attack it
 +      // 2: touch it
 +      // 3: attack it (HIGH PRIO)
 +      // 4: touch it (HIGH PRIO)
 +{
 +      int a;
 +
 +      if(cp.isshielded)
 +      {
 +              return 0;
 +      }
 +      else if(cp.goalentity)
 +      {
 +              // if there's already an icon built, nothing happens
 +              if(cp.team == teamnumber)
 +              {
 +                      a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
 +                      if(a) // attackable by enemy?
 +                              return -2; // EMERGENCY!
 +                      return -1;
 +              }
 +              // we know it can be linked, so no need to check
 +              // but...
 +              a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
 +              if(a == 2) // near our generator?
 +                      return 3; // EMERGENCY!
 +              return 1;
 +      }
 +      else
 +      {
 +              // free point
 +              if(ons_ControlPoint_CanBeLinked(cp, teamnumber))
 +              {
 +                      a = ons_ControlPoint_CanBeLinked(cp, teamnumber); // why was this here NUM_TEAM_1 + NUM_TEAM_2 - t
 +                      if(a == 2)
 +                              return 4; // GET THIS ONE NOW!
 +                      else
 +                              return 2; // TOUCH ME
 +              }
 +      }
 +      return 0;
 +}
 +
 +void ons_ControlPoint_Icon_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
 +{
 +      if(damage <= 0) { return; }
 +
 +      if (this.owner.isshielded)
 +      {
 +              // this is protected by a shield, so ignore the damage
 +              if (time > this.pain_finished)
 +                      if (IS_PLAYER(attacker))
 +                      {
 +                              play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
 +                              this.pain_finished = time + 1;
 +                              attacker.typehitsound += 1; // play both sounds (shield is way too quiet)
 +                      }
 +
 +              return;
 +      }
 +
 +      if(IS_PLAYER(attacker))
 +      if(time - ons_notification_time[this.team] > 10)
 +      {
 +              play2team(this.team, SND(ONS_CONTROLPOINT_UNDERATTACK));
 +              ons_notification_time[this.team] = time;
 +      }
 +
 +      this.health = this.health - damage;
 +      if(this.owner.iscaptured)
 +              WaypointSprite_UpdateHealth(this.owner.sprite, this.health);
 +      else
 +              WaypointSprite_UpdateBuildFinished(this.owner.sprite, time + (this.max_health - this.health) / (this.count / ONS_CP_THINKRATE));
 +      this.pain_finished = time + 1;
 +      // particles on every hit
 +      pointparticles(EFFECT_SPARKS, hitloc, force*-1, 1);
 +      //sound on every hit
 +      if (random() < 0.5)
 +              sound(this, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE+0.3, ATTEN_NORM);
 +      else
 +              sound(this, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE+0.3, ATTEN_NORM);
 +
 +      if (this.health < 0)
 +      {
 +              sound(this, CH_TRIGGER, SND_GRENADE_IMPACT, VOL_BASE, ATTEN_NORM);
 +              pointparticles(EFFECT_ROCKET_EXPLODE, this.origin, '0 0 0', 1);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_CPDESTROYED), this.owner.message, attacker.netname);
 +
 +              PlayerScore_Add(attacker, SP_ONS_TAKES, 1);
 +              PlayerScore_Add(attacker, SP_SCORE, 10);
 +
 +              this.owner.goalentity = NULL;
 +              this.owner.islinked = false;
 +              this.owner.iscaptured = false;
 +              this.owner.team = 0;
 +              this.owner.colormap = 1024;
 +
 +              WaypointSprite_UpdateMaxHealth(this.owner.sprite, 0);
 +
 +              onslaught_updatelinks();
 +
 +              // Use targets now (somebody make sure this is in the right place..)
 +              SUB_UseTargets(this.owner, this, NULL);
 +
 +              this.owner.waslinked = this.owner.islinked;
 +              if(this.owner.model != "models/onslaught/controlpoint_pad.md3")
 +                      setmodel_fixsize(this.owner, MDL_ONS_CP_PAD1);
 +              //setsize(this, '-32 -32 0', '32 32 8');
 +
 +              delete(this);
 +      }
 +
 +      this.SendFlags |= CPSF_STATUS;
 +}
 +
 +void ons_ControlPoint_Icon_Think(entity this)
 +{
 +      this.nextthink = time + ONS_CP_THINKRATE;
 +
 +      if(autocvar_g_onslaught_cp_proxydecap)
 +      {
 +              int _enemy_count = 0;
 +              int _friendly_count = 0;
 +
 +              FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
 +                      if(vdist(it.origin - this.origin, <, autocvar_g_onslaught_cp_proxydecap_distance))
 +                      {
 +                              if(SAME_TEAM(it, this))
 +                                      ++_friendly_count;
 +                              else
 +                                      ++_enemy_count;
 +                      }
 +              });
 +
 +              _friendly_count = _friendly_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
 +              _enemy_count = _enemy_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
 +
 +              this.health = bound(0, this.health + (_friendly_count - _enemy_count), this.max_health);
 +              this.SendFlags |= CPSF_STATUS;
 +              if(this.health <= 0)
 +              {
 +                      ons_ControlPoint_Icon_Damage(this, this, this, 1, 0, this.origin, '0 0 0');
 +                      return;
 +              }
 +      }
 +
 +      if (time > this.pain_finished + 5)
 +      {
 +              if(this.health < this.max_health)
 +              {
 +                      this.health = this.health + this.count;
 +                      if (this.health >= this.max_health)
 +                              this.health = this.max_health;
 +                      WaypointSprite_UpdateHealth(this.owner.sprite, this.health);
 +              }
 +      }
 +
 +      if(this.owner.islinked != this.owner.waslinked)
 +      {
 +              // unteam the spawnpoint if needed
 +              int t = this.owner.team;
 +              if(!this.owner.islinked)
 +                      this.owner.team = 0;
 +
 +              SUB_UseTargets(this.owner, this, NULL);
 +
 +              this.owner.team = t;
 +
 +              this.owner.waslinked = this.owner.islinked;
 +      }
 +
 +      // damaged fx
 +      if(random() < 0.6 - this.health / this.max_health)
 +      {
 +              Send_Effect(EFFECT_ELECTRIC_SPARKS, this.origin + randompos('-10 -10 -20', '10 10 20'), '0 0 0', 1);
 +
 +              if(random() > 0.8)
 +                      sound(this, CH_PAIN, SND_ONS_SPARK1, VOL_BASE, ATTEN_NORM);
 +              else if (random() > 0.5)
 +                      sound(this, CH_PAIN, SND_ONS_SPARK2, VOL_BASE, ATTEN_NORM);
 +      }
 +}
 +
 +void ons_ControlPoint_Icon_BuildThink(entity this)
 +{
 +      int a;
 +
 +      this.nextthink = time + ONS_CP_THINKRATE;
 +
 +      // only do this if there is power
 +      a = ons_ControlPoint_CanBeLinked(this.owner, this.owner.team);
 +      if(!a)
 +              return;
 +
 +      this.health = this.health + this.count;
 +
 +      this.SendFlags |= CPSF_STATUS;
 +
 +      if (this.health >= this.max_health)
 +      {
 +              this.health = this.max_health;
 +              this.count = autocvar_g_onslaught_cp_regen * ONS_CP_THINKRATE; // slow repair rate from now on
 +              setthink(this, ons_ControlPoint_Icon_Think);
 +              sound(this, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILT, VOL_BASE, ATTEN_NORM);
 +              this.owner.iscaptured = true;
 +              this.solid = SOLID_BBOX;
 +
 +              Send_Effect(EFFECT_CAP(this.owner.team), this.owner.origin, '0 0 0', 1);
 +
 +              WaypointSprite_UpdateMaxHealth(this.owner.sprite, this.max_health);
 +              WaypointSprite_UpdateHealth(this.owner.sprite, this.health);
 +
 +              if(IS_PLAYER(this.owner.ons_toucher))
 +              {
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ONSLAUGHT_CAPTURE, this.owner.ons_toucher.netname, this.owner.message);
 +                      Send_Notification(NOTIF_ALL_EXCEPT, this.owner.ons_toucher, MSG_CENTER, APP_TEAM_NUM(this.owner.ons_toucher.team, CENTER_ONS_CAPTURE), this.owner.message);
 +                      Send_Notification(NOTIF_ONE, this.owner.ons_toucher, MSG_CENTER, CENTER_ONS_CAPTURE, this.owner.message);
 +                      PlayerScore_Add(this.owner.ons_toucher, SP_ONS_CAPS, 1);
 +                      PlayerTeamScore_AddScore(this.owner.ons_toucher, 10);
 +              }
 +
 +              this.owner.ons_toucher = NULL;
 +
 +              onslaught_updatelinks();
 +
 +              // Use targets now (somebody make sure this is in the right place..)
 +              SUB_UseTargets(this.owner, this, NULL);
 +
 +              this.SendFlags |= CPSF_SETUP;
 +      }
 +      if(this.owner.model != MDL_ONS_CP_PAD2.model_str())
 +              setmodel_fixsize(this.owner, MDL_ONS_CP_PAD2);
 +
 +      if(random() < 0.9 - this.health / this.max_health)
 +              Send_Effect(EFFECT_RAGE, this.origin + 10 * randomvec(), '0 0 -1', 1);
 +}
 +
 +void onslaught_controlpoint_icon_link(entity e, void(entity this) spawnproc);
 +
 +void ons_ControlPoint_Icon_Spawn(entity cp, entity player)
 +{
 +      entity e = new(onslaught_controlpoint_icon);
 +
 +      setsize(e, CPICON_MIN, CPICON_MAX);
 +      setorigin(e, cp.origin + CPICON_OFFSET);
 +
 +      e.owner = cp;
 +      e.max_health = autocvar_g_onslaught_cp_health;
 +      e.health = autocvar_g_onslaught_cp_buildhealth;
 +      e.solid = SOLID_NOT;
 +      e.takedamage = DAMAGE_AIM;
 +      e.bot_attack = true;
 +      e.event_damage = ons_ControlPoint_Icon_Damage;
 +      e.team = player.team;
 +      e.colormap = 1024 + (e.team - 1) * 17;
 +      e.count = (e.max_health - e.health) * ONS_CP_THINKRATE / autocvar_g_onslaught_cp_buildtime; // how long it takes to build
 +
 +      sound(e, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILD, VOL_BASE, ATTEN_NORM);
 +
 +      cp.goalentity = e;
 +      cp.team = e.team;
 +      cp.colormap = e.colormap;
 +
 +      Send_Effect(EFFECT_FLAG_TOUCH(player.team), e.origin, '0 0 0', 1);
 +
 +      WaypointSprite_UpdateBuildFinished(cp.sprite, time + (e.max_health - e.health) / (e.count / ONS_CP_THINKRATE));
 +      WaypointSprite_UpdateRule(cp.sprite,cp.team,SPRITERULE_TEAMPLAY);
 +      cp.sprite.SendFlags |= 16;
 +
 +      onslaught_controlpoint_icon_link(e, ons_ControlPoint_Icon_BuildThink);
 +}
 +
 +entity ons_ControlPoint_Waypoint(entity e)
 +{
 +      if(e.team)
 +      {
 +              int a = ons_ControlPoint_Attackable(e, e.team);
 +
 +              if(a == -2) { return WP_OnsCPDefend; } // defend now
 +              if(a == -1 || a == 1 || a == 2) { return WP_OnsCP; } // touch
 +              if(a == 3 || a == 4) { return WP_OnsCPAttack; } // attack
 +      }
 +      else
 +              return WP_OnsCP;
 +
 +      return WP_Null;
 +}
 +
 +void ons_ControlPoint_UpdateSprite(entity e)
 +{
 +      entity s1 = ons_ControlPoint_Waypoint(e);
 +      WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
 +
 +      bool sh;
 +      sh = !(ons_ControlPoint_CanBeLinked(e, NUM_TEAM_1) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_2) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_3) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_4));
 +
 +      if(e.lastteam != e.team + 2 || e.lastshielded != sh || e.iscaptured != e.lastcaptured)
 +      {
 +              if(e.iscaptured) // don't mess up build bars!
 +              {
 +                      if(sh)
 +                      {
 +                              WaypointSprite_UpdateMaxHealth(e.sprite, 0);
 +                      }
 +                      else
 +                      {
 +                              WaypointSprite_UpdateMaxHealth(e.sprite, e.goalentity.max_health);
 +                              WaypointSprite_UpdateHealth(e.sprite, e.goalentity.health);
 +                      }
 +              }
 +              if(e.lastshielded)
 +              {
 +                      if(e.team)
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, 0.5 * colormapPaletteColor(e.team - 1, false));
 +                      else
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.5 0.5 0.5');
 +              }
 +              else
 +              {
 +                      if(e.team)
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, colormapPaletteColor(e.team - 1, false));
 +                      else
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.75 0.75 0.75');
 +              }
 +              WaypointSprite_Ping(e.sprite);
 +
 +              e.lastteam = e.team + 2;
 +              e.lastshielded = sh;
 +              e.lastcaptured = e.iscaptured;
 +      }
 +}
 +
 +void ons_ControlPoint_Touch(entity this, entity toucher)
 +{
 +      int attackable;
 +
 +      if(IS_VEHICLE(toucher) && toucher.owner)
 +      if(autocvar_g_onslaught_allow_vehicle_touch)
 +              toucher = toucher.owner;
 +      else
 +              return;
 +
 +      if(!IS_PLAYER(toucher)) { return; }
 +      if(STAT(FROZEN, toucher)) { return; }
 +      if(IS_DEAD(toucher)) { return; }
 +
 +      if ( SAME_TEAM(this,toucher) )
 +      if ( this.iscaptured )
 +      {
 +              if(time <= toucher.teleport_antispam)
 +                      Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT_ANTISPAM, rint(toucher.teleport_antispam - time));
 +              else
 +                      Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT);
 +      }
 +
 +      attackable = ons_ControlPoint_Attackable(this, toucher.team);
 +      if(attackable != 2 && attackable != 4)
 +              return;
 +      // we've verified that this player has a legitimate claim to this point,
 +      // so start building the captured point icon (which only captures this
 +      // point if it successfully builds without being destroyed first)
 +      ons_ControlPoint_Icon_Spawn(this, toucher);
 +
 +      this.ons_toucher = toucher;
 +
 +      onslaught_updatelinks();
 +}
 +
 +void ons_ControlPoint_Think(entity this)
 +{
 +      this.nextthink = time + ONS_CP_THINKRATE;
 +      CSQCMODEL_AUTOUPDATE(this);
 +}
 +
 +void ons_ControlPoint_Reset(entity this)
 +{
 +      if(this.goalentity)
 +              delete(this.goalentity);
 +
 +      this.goalentity = NULL;
 +      this.team = 0;
 +      this.colormap = 1024;
 +      this.iscaptured = false;
 +      this.islinked = false;
 +      this.isshielded = true;
 +      setthink(this, ons_ControlPoint_Think);
 +      this.ons_toucher = NULL;
 +      this.nextthink = time + ONS_CP_THINKRATE;
 +      setmodel_fixsize(this, MDL_ONS_CP_PAD1);
 +
 +      WaypointSprite_UpdateMaxHealth(this.sprite, 0);
 +      WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY);
 +
 +      onslaught_updatelinks();
 +
 +      SUB_UseTargets(this, this, NULL); // to reset the structures, playerspawns etc.
 +
 +      CSQCMODEL_AUTOUPDATE(this);
 +}
 +
 +void ons_DelayedControlPoint_Setup(entity this)
 +{
 +      onslaught_updatelinks();
 +
 +      // captureshield setup
 +      ons_CaptureShield_Spawn(this, false);
 +
 +      CSQCMODEL_AUTOINIT(this);
 +}
 +
 +void ons_ControlPoint_Setup(entity cp)
 +{
 +      // main setup
 +      cp.ons_worldcpnext = ons_worldcplist; // link control point into ons_worldcplist
 +      ons_worldcplist = cp;
 +
 +      cp.netname = "Control point";
 +      cp.team = 0;
 +      cp.solid = SOLID_BBOX;
 +      set_movetype(cp, MOVETYPE_NONE);
 +      settouch(cp, ons_ControlPoint_Touch);
 +      setthink(cp, ons_ControlPoint_Think);
 +      cp.nextthink = time + ONS_CP_THINKRATE;
 +      cp.reset = ons_ControlPoint_Reset;
 +      cp.colormap = 1024;
 +      cp.iscaptured = false;
 +      cp.islinked = false;
 +      cp.isshielded = true;
 +
 +      if(cp.message == "") { cp.message = "a"; }
 +
 +      // appearence
 +      setmodel_fixsize(cp, MDL_ONS_CP_PAD1);
 +
 +      // control point placement
 +      if((cp.spawnflags & 1) || cp.noalign) // don't drop to floor, just stay at fixed location
 +      {
 +              cp.noalign = true;
 +              set_movetype(cp, MOVETYPE_NONE);
 +      }
 +      else // drop to floor, automatically find a platform and set that as spawn origin
 +      {
 +              setorigin(cp, cp.origin + '0 0 20');
 +              cp.noalign = false;
 +              droptofloor(cp);
 +              set_movetype(cp, MOVETYPE_TOSS);
 +      }
 +
 +      // waypointsprites
 +      WaypointSprite_SpawnFixed(WP_Null, cp.origin + CPGEN_WAYPOINT_OFFSET, cp, sprite, RADARICON_NONE);
 +      WaypointSprite_UpdateRule(cp.sprite, cp.team, SPRITERULE_TEAMPLAY);
 +
 +      InitializeEntity(cp, ons_DelayedControlPoint_Setup, INITPRIO_SETLOCATION);
 +}
 +
 +
 +// =========================
 +// Main Generator Functions
 +// =========================
 +
 +entity ons_Generator_Waypoint(entity e)
 +{
 +      if (e.isshielded)
 +              return WP_OnsGenShielded;
 +      return WP_OnsGen;
 +}
 +
 +void ons_Generator_UpdateSprite(entity e)
 +{
 +      entity s1 = ons_Generator_Waypoint(e);
 +      WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
 +
 +      if(e.lastteam != e.team + 2 || e.lastshielded != e.isshielded)
 +      {
 +              e.lastteam = e.team + 2;
 +              e.lastshielded = e.isshielded;
 +              if(e.lastshielded)
 +              {
 +                      if(e.team)
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, 0.5 * colormapPaletteColor(e.team - 1, false));
 +                      else
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.5 0.5 0.5');
 +              }
 +              else
 +              {
 +                      if(e.team)
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, colormapPaletteColor(e.team - 1, false));
 +                      else
 +                              WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.75 0.75 0.75');
 +              }
 +              WaypointSprite_Ping(e.sprite);
 +      }
 +}
 +
 +void ons_GeneratorDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
 +{
 +      if(damage <= 0) { return; }
 +      if(warmup_stage || gameover) { return; }
 +      if(!round_handler_IsRoundStarted()) { return; }
 +
 +      if (attacker != this)
 +      {
 +              if (this.isshielded)
 +              {
 +                      // this is protected by a shield, so ignore the damage
 +                      if (time > this.pain_finished)
 +                              if (IS_PLAYER(attacker))
 +                              {
 +                                      play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
 +                                      attacker.typehitsound += 1;
 +                                      this.pain_finished = time + 1;
 +                              }
 +                      return;
 +              }
 +              if (time > this.pain_finished)
 +              {
 +                      this.pain_finished = time + 10;
 +                      FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && SAME_TEAM(it, this), Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_GENERATOR_UNDERATTACK));
 +                      play2team(this.team, SND(ONS_GENERATOR_UNDERATTACK));
 +              }
 +      }
 +      this.health = this.health - damage;
 +      WaypointSprite_UpdateHealth(this.sprite, this.health);
 +      // choose an animation frame based on health
 +      this.frame = 10 * bound(0, (1 - this.health / this.max_health), 1);
 +      // see if the generator is still functional, or dying
 +      if (this.health > 0)
 +      {
 +              this.lasthealth = this.health;
 +      }
 +      else
 +      {
 +              if (attacker == this)
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_GENDESTROYED_OVERTIME));
 +              else
 +              {
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_GENDESTROYED));
 +                      PlayerScore_Add(attacker, SP_SCORE, 100);
 +              }
 +              this.iscaptured = false;
 +              this.islinked = false;
 +              this.isshielded = false;
 +              this.takedamage = DAMAGE_NO; // can't be hurt anymore
 +              this.event_damage = func_null; // won't do anything if hurt
 +              this.count = 0; // reset counter
 +              setthink(this, func_null);
 +              this.nextthink = 0;
 +              //this.think(); // do the first explosion now
 +
 +              WaypointSprite_UpdateMaxHealth(this.sprite, 0);
 +              WaypointSprite_Ping(this.sprite);
 +              //WaypointSprite_Kill(this.sprite); // can't do this yet, code too poor
 +
 +              onslaught_updatelinks();
 +      }
 +
 +      // Throw some flaming gibs on damage, more damage = more chance for gib
 +      if(random() < damage/220)
 +      {
 +              sound(this, CH_TRIGGER, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
 +      }
 +      else
 +      {
 +              // particles on every hit
 +              Send_Effect(EFFECT_SPARKS, hitloc, force * -1, 1);
 +
 +              //sound on every hit
 +              if (random() < 0.5)
 +                      sound(this, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE, ATTEN_NORM);
 +              else
 +                      sound(this, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE, ATTEN_NORM);
 +      }
 +
 +      this.SendFlags |= GSF_STATUS;
 +}
 +
 +void ons_GeneratorThink(entity this)
 +{
 +      this.nextthink = time + GEN_THINKRATE;
 +      if (!gameover)
 +      {
 +              if(!this.isshielded && this.wait < time)
 +              {
 +                      this.wait = time + 5;
 +                      FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
 +                              if(SAME_TEAM(it, this))
 +                              {
 +                                      Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_ONS_NOTSHIELDED_TEAM);
 +                                      soundto(MSG_ONE, it, CHAN_AUTO, SND(KH_ALARM), VOL_BASE, ATTEN_NONE);    // FIXME: unique sound?
 +                              }
 +                              else
 +                                      Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_ONS_NOTSHIELDED));
 +                      });
 +              }
 +      }
 +}
 +
 +void ons_GeneratorReset(entity this)
 +{
 +      this.team = this.team_saved;
 +      this.lasthealth = this.max_health = this.health = autocvar_g_onslaught_gen_health;
 +      this.takedamage = DAMAGE_AIM;
 +      this.bot_attack = true;
 +      this.iscaptured = true;
 +      this.islinked = true;
 +      this.isshielded = true;
 +      this.event_damage = ons_GeneratorDamage;
 +      setthink(this, ons_GeneratorThink);
 +      this.nextthink = time + GEN_THINKRATE;
 +
 +      Net_LinkEntity(this, false, 0, generator_send);
 +
 +      this.SendFlags = GSF_SETUP; // just incase
 +      this.SendFlags |= GSF_STATUS;
 +
 +      WaypointSprite_UpdateMaxHealth(this.sprite, this.max_health);
 +      WaypointSprite_UpdateHealth(this.sprite, this.health);
 +      WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY);
 +
 +      onslaught_updatelinks();
 +}
 +
 +void ons_DelayedGeneratorSetup(entity this)
 +{
 +      // bot waypoints
 +      waypoint_spawnforitem_force(this, this.origin);
 +      this.nearestwaypointtimeout = 0; // activate waypointing again
 +      this.bot_basewaypoint = this.nearestwaypoint;
 +
 +      // captureshield setup
 +      ons_CaptureShield_Spawn(this, true);
 +
 +      onslaught_updatelinks();
 +
 +      Net_LinkEntity(this, false, 0, generator_send);
 +}
 +
 +
 +void onslaught_generator_touch(entity this, entity toucher)
 +{
 +      if ( IS_PLAYER(toucher) )
 +      if ( SAME_TEAM(this,toucher) )
 +      if ( this.iscaptured )
 +      {
 +              Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT);
 +      }
 +}
 +
 +void ons_GeneratorSetup(entity gen) // called when spawning a generator entity on the map as a spawnfunc
 +{
 +      // declarations
 +      int teamnumber = gen.team;
 +
 +      // main setup
 +      gen.ons_worldgeneratornext = ons_worldgeneratorlist; // link generator into ons_worldgeneratorlist
 +      ons_worldgeneratorlist = gen;
 +
 +      gen.netname = sprintf("%s generator", Team_ColoredFullName(teamnumber));
 +      gen.classname = "onslaught_generator";
 +      gen.solid = SOLID_BBOX;
 +      gen.team_saved = teamnumber;
 +      set_movetype(gen, MOVETYPE_NONE);
 +      gen.lasthealth = gen.max_health = gen.health = autocvar_g_onslaught_gen_health;
 +      gen.takedamage = DAMAGE_AIM;
 +      gen.bot_attack = true;
 +      gen.event_damage = ons_GeneratorDamage;
 +      gen.reset = ons_GeneratorReset;
 +      setthink(gen, ons_GeneratorThink);
 +      gen.nextthink = time + GEN_THINKRATE;
 +      gen.iscaptured = true;
 +      gen.islinked = true;
 +      gen.isshielded = true;
 +      settouch(gen, onslaught_generator_touch);
 +
 +      // appearence
 +      // model handled by CSQC
 +      setsize(gen, GENERATOR_MIN, GENERATOR_MAX);
 +      setorigin(gen, (gen.origin + CPGEN_SPAWN_OFFSET));
 +      gen.colormap = 1024 + (teamnumber - 1) * 17;
 +
 +      // generator placement
 +      droptofloor(gen);
 +
 +      // waypointsprites
 +      WaypointSprite_SpawnFixed(WP_Null, gen.origin + CPGEN_WAYPOINT_OFFSET, gen, sprite, RADARICON_NONE);
 +      WaypointSprite_UpdateRule(gen.sprite, gen.team, SPRITERULE_TEAMPLAY);
 +      WaypointSprite_UpdateMaxHealth(gen.sprite, gen.max_health);
 +      WaypointSprite_UpdateHealth(gen.sprite, gen.health);
 +
 +      InitializeEntity(gen, ons_DelayedGeneratorSetup, INITPRIO_SETLOCATION);
 +}
 +
 +
 +// ===============
 +//  Round Handler
 +// ===============
 +
 +int total_generators;
 +void Onslaught_count_generators()
 +{
 +      entity e;
 +      total_generators = redowned = blueowned = yellowowned = pinkowned = 0;
 +      for(e = ons_worldgeneratorlist; e; e = e.ons_worldgeneratornext)
 +      {
 +              ++total_generators;
 +              redowned += (e.team == NUM_TEAM_1 && e.health > 0);
 +              blueowned += (e.team == NUM_TEAM_2 && e.health > 0);
 +              yellowowned += (e.team == NUM_TEAM_3 && e.health > 0);
 +              pinkowned += (e.team == NUM_TEAM_4 && e.health > 0);
 +      }
 +}
 +
 +int Onslaught_GetWinnerTeam()
 +{
 +      int winner_team = 0;
 +      if(redowned > 0)
 +              winner_team = NUM_TEAM_1;
 +      if(blueowned > 0)
 +      {
 +              if(winner_team) return 0;
 +              winner_team = NUM_TEAM_2;
 +      }
 +      if(yellowowned > 0)
 +      {
 +              if(winner_team) return 0;
 +              winner_team = NUM_TEAM_3;
 +      }
 +      if(pinkowned > 0)
 +      {
 +              if(winner_team) return 0;
 +              winner_team = NUM_TEAM_4;
 +      }
 +      if(winner_team)
 +              return winner_team;
 +      return -1; // no generators left?
 +}
 +
 +void nades_Clear(entity e);
 +
 +#define ONS_OWNED_GENERATORS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
 +#define ONS_OWNED_GENERATORS_OK() (ONS_OWNED_GENERATORS() > 1)
 +bool Onslaught_CheckWinner()
 +{
 +      if ((autocvar_timelimit && time > game_starttime + autocvar_timelimit * 60) || (round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0))
 +      {
 +              ons_stalemate = true;
 +
 +              if (!wpforenemy_announced)
 +              {
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT);
 +                      sound(NULL, CH_INFO, SND_ONS_GENERATOR_DECAY, VOL_BASE, ATTEN_NONE);
 +
 +                      wpforenemy_announced = true;
 +              }
 +
 +              entity tmp_entity; // temporary entity
 +              float d;
 +              for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) if(time >= tmp_entity.ons_overtime_damagedelay)
 +              {
 +                      // tmp_entity.max_health / 300 gives 5 minutes of overtime.
 +                      // control points reduce the overtime duration.
 +                      d = 1;
 +                      entity e;
 +                      for(e = ons_worldcplist; e; e = e.ons_worldcpnext)
 +                      {
 +                              if(DIFF_TEAM(e, tmp_entity))
 +                              if(e.islinked)
 +                                      d = d + 1;
 +                      }
 +
 +                      if(autocvar_g_campaign && autocvar__campaign_testrun)
 +                              d = d * tmp_entity.max_health;
 +                      else
 +                              d = d * tmp_entity.max_health / max(30, 60 * autocvar_timelimit_suddendeath);
 +
 +                      Damage(tmp_entity, tmp_entity, tmp_entity, d, DEATH_HURTTRIGGER.m_id, tmp_entity.origin, '0 0 0');
 +
 +                      tmp_entity.sprite.SendFlags |= 16;
 +
 +                      tmp_entity.ons_overtime_damagedelay = time + 1;
 +              }
 +      }
 +      else { wpforenemy_announced = false; ons_stalemate = false; }
 +
 +      Onslaught_count_generators();
 +
 +      if(ONS_OWNED_GENERATORS_OK())
 +              return 0;
 +
 +      int winner_team = Onslaught_GetWinnerTeam();
 +
 +      if(winner_team > 0)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN));
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_WIN));
 +              TeamScore_AddToTeam(winner_team, ST_ONS_CAPS, +1);
 +      }
 +      else if(winner_team == -1)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
 +      }
 +
 +      ons_stalemate = false;
 +
 +      play2all(SND(CTF_CAPTURE(winner_team)));
 +
 +      round_handler_Init(7, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
 +
 +      FOREACH_CLIENT(IS_PLAYER(it), {
-               it.ons_roundlost = false;
++              STAT(ROUNDLOST, it) = true;
 +              it.player_blocked = true;
 +
 +              nades_Clear(it);
 +      });
 +
 +      return 1;
 +}
 +
 +bool Onslaught_CheckPlayers()
 +{
 +      return 1;
 +}
 +
 +void Onslaught_RoundStart()
 +{
 +      entity tmp_entity;
 +      FOREACH_CLIENT(IS_PLAYER(it), it.player_blocked = false);
 +
 +      for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
 +              tmp_entity.sprite.SendFlags |= 16;
 +
 +      for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
 +              tmp_entity.sprite.SendFlags |= 16;
 +}
 +
 +
 +// ================
 +// Bot player logic
 +// ================
 +
 +// NOTE: LEGACY CODE, needs to be re-written!
 +
 +void havocbot_goalrating_ons_offenseitems(entity this, float ratingscale, vector org, float sradius)
 +{
 +      bool needarmor = false, needweapons = false;
 +
 +      // Needs armor/health?
 +      if(this.health<100)
 +              needarmor = true;
 +
 +      // Needs weapons?
 +      int c = 0;
 +      FOREACH(Weapons, it != WEP_Null, {
 +              if(this.weapons & (it.m_wepset))
 +              if(++c >= 4)
 +                      break;
 +      });
 +
 +      if(c<4)
 +              needweapons = true;
 +
 +      if(!needweapons && !needarmor)
 +              return;
 +
 +      LOG_DEBUG(this.netname, " needs weapons ", ftos(needweapons));
 +      LOG_DEBUG(this.netname, " needs armor ", ftos(needarmor));
 +
 +      // See what is around
 +      FOREACH_ENTITY_FLOAT(bot_pickup, true,
 +      {
 +              // gather health and armor only
 +              if (it.solid)
 +              if ( ((it.health || it.armorvalue) && needarmor) || (it.weapons && needweapons ) )
 +              if (vdist(it.origin - org, <, sradius))
 +              {
 +                      int t = it.bot_pickupevalfunc(this, it);
 +                      if (t > 0)
 +                              navigation_routerating(this, it, t * ratingscale, 500);
 +              }
 +      });
 +}
 +
 +void havocbot_role_ons_setrole(entity this, int role)
 +{
 +      LOG_DEBUG(this.netname," switched to ");
 +      switch(role)
 +      {
 +              case HAVOCBOT_ONS_ROLE_DEFENSE:
 +                      LOG_DEBUG("defense");
 +                      this.havocbot_role = havocbot_role_ons_defense;
 +                      this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_DEFENSE;
 +                      this.havocbot_role_timeout = 0;
 +                      break;
 +              case HAVOCBOT_ONS_ROLE_ASSISTANT:
 +                      LOG_DEBUG("assistant");
 +                      this.havocbot_role = havocbot_role_ons_assistant;
 +                      this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_ASSISTANT;
 +                      this.havocbot_role_timeout = 0;
 +                      break;
 +              case HAVOCBOT_ONS_ROLE_OFFENSE:
 +                      LOG_DEBUG("offense");
 +                      this.havocbot_role = havocbot_role_ons_offense;
 +                      this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_OFFENSE;
 +                      this.havocbot_role_timeout = 0;
 +                      break;
 +      }
 +      LOG_DEBUG("");
 +}
 +
 +void havocbot_goalrating_ons_controlpoints_attack(entity this, float ratingscale)
 +{
 +      entity cp, cp1, cp2, best, wp;
 +      float radius, bestvalue;
 +      int c;
 +      bool found;
 +
 +      // Filter control points
 +      for(cp2 = ons_worldcplist; cp2; cp2 = cp2.ons_worldcpnext)
 +      {
 +              cp2.wpcost = c = 0;
 +              cp2.wpconsidered = false;
 +
 +              if(cp2.isshielded)
 +                      continue;
 +
 +              // Ignore owned controlpoints
 +              if(!(cp2.isgenneighbor[this.team] || cp2.iscpneighbor[this.team]))
 +                      continue;
 +
 +              // Count team mates interested in this control point
 +              // (easier and cleaner than keeping counters per cp and teams)
 +              FOREACH_CLIENT(IS_PLAYER(it), {
 +                      if(SAME_TEAM(it, this))
 +                      if(it.havocbot_role_flags & HAVOCBOT_ONS_ROLE_OFFENSE)
 +                      if(it.havocbot_ons_target == cp2)
 +                              ++c;
 +              });
 +
 +              // NOTE: probably decrease the cost of attackable control points
 +              cp2.wpcost = c;
 +              cp2.wpconsidered = true;
 +      }
 +
 +      // We'll consider only the best case
 +      bestvalue = 99999999999;
 +      cp = NULL;
 +      for(cp1 = ons_worldcplist; cp1; cp1 = cp1.ons_worldcpnext)
 +      {
 +              if (!cp1.wpconsidered)
 +                      continue;
 +
 +              if(cp1.wpcost<bestvalue)
 +              {
 +                      bestvalue = cp1.wpcost;
 +                      cp = cp1;
 +                      this.havocbot_ons_target = cp1;
 +              }
 +      }
 +
 +      if (!cp)
 +              return;
 +
 +      LOG_DEBUG(this.netname, " chose cp ranked ", ftos(bestvalue));
 +
 +      if(cp.goalentity)
 +      {
 +              // Should be attacked
 +              // Rate waypoints near it
 +              found = false;
 +              best = NULL;
 +              bestvalue = 99999999999;
 +              for(radius=0; radius<1000 && !found; radius+=500)
 +              {
 +                      for(wp=findradius(cp.origin,radius); wp; wp=wp.chain)
 +                      {
 +                              if(!(wp.wpflags & WAYPOINTFLAG_GENERATED))
 +                              if(wp.classname=="waypoint")
 +                              if(checkpvs(wp.origin,cp))
 +                              {
 +                                      found = true;
 +                                      if(wp.cnt<bestvalue)
 +                                      {
 +                                              best = wp;
 +                                              bestvalue = wp.cnt;
 +                                      }
 +                              }
 +                      }
 +              }
 +
 +              if(best)
 +              {
 +                      navigation_routerating(this, best, ratingscale, 10000);
 +                      best.cnt += 1;
 +
 +                      this.havocbot_attack_time = 0;
 +                      if(checkpvs(this.view_ofs,cp))
 +                      if(checkpvs(this.view_ofs,best))
 +                              this.havocbot_attack_time = time + 2;
 +              }
 +              else
 +              {
 +                      navigation_routerating(this, cp, ratingscale, 10000);
 +              }
 +              LOG_DEBUG(this.netname, " found an attackable controlpoint at ", vtos(cp.origin));
 +      }
 +      else
 +      {
 +              // Should be touched
 +              LOG_DEBUG(this.netname, " found a touchable controlpoint at ", vtos(cp.origin));
 +              found = false;
 +
 +              // Look for auto generated waypoint
 +              if (!bot_waypoints_for_items)
 +              for (wp = findradius(cp.origin,100); wp; wp = wp.chain)
 +              {
 +                      if(wp.classname=="waypoint")
 +                      {
 +                              navigation_routerating(this, wp, ratingscale, 10000);
 +                              found = true;
 +                      }
 +              }
 +
 +              // Nothing found, rate the controlpoint itself
 +              if (!found)
 +                      navigation_routerating(this, cp, ratingscale, 10000);
 +      }
 +}
 +
 +bool havocbot_goalrating_ons_generator_attack(entity this, float ratingscale)
 +{
 +      entity g, wp, bestwp;
 +      bool found;
 +      int best;
 +
 +      for(g = ons_worldgeneratorlist; g; g = g.ons_worldgeneratornext)
 +      {
 +              if(SAME_TEAM(g, this) || g.isshielded)
 +                      continue;
 +
 +              // Should be attacked
 +              // Rate waypoints near it
 +              found = false;
 +              bestwp = NULL;
 +              best = 99999999999;
 +
 +              for(wp=findradius(g.origin,400); wp; wp=wp.chain)
 +              {
 +                      if(wp.classname=="waypoint")
 +                      if(checkpvs(wp.origin,g))
 +                      {
 +                              found = true;
 +                              if(wp.cnt<best)
 +                              {
 +                                      bestwp = wp;
 +                                      best = wp.cnt;
 +                              }
 +                      }
 +              }
 +
 +              if(bestwp)
 +              {
 +                      LOG_DEBUG("waypoints found around generator");
 +                      navigation_routerating(this, bestwp, ratingscale, 10000);
 +                      bestwp.cnt += 1;
 +
 +                      this.havocbot_attack_time = 0;
 +                      if(checkpvs(this.view_ofs,g))
 +                      if(checkpvs(this.view_ofs,bestwp))
 +                              this.havocbot_attack_time = time + 5;
 +
 +                      return true;
 +              }
 +              else
 +              {
 +                      LOG_DEBUG("generator found without waypoints around");
 +                      // if there aren't waypoints near the generator go straight to it
 +                      navigation_routerating(this, g, ratingscale, 10000);
 +                      this.havocbot_attack_time = 0;
 +                      return true;
 +              }
 +      }
 +      return false;
 +}
 +
 +void havocbot_role_ons_offense(entity this)
 +{
 +      if(IS_DEAD(this))
 +      {
 +              this.havocbot_attack_time = 0;
 +              havocbot_ons_reset_role(this);
 +              return;
 +      }
 +
 +      // Set the role timeout if necessary
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + 120;
 +
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              havocbot_ons_reset_role(this);
 +              return;
 +      }
 +
 +      if(this.havocbot_attack_time>time)
 +              return;
 +
 +      if (this.bot_strategytime < time)
 +      {
 +              navigation_goalrating_start(this);
 +              havocbot_goalrating_enemyplayers(this, 20000, this.origin, 650);
 +              if(!havocbot_goalrating_ons_generator_attack(this, 20000))
 +                      havocbot_goalrating_ons_controlpoints_attack(this, 20000);
 +              havocbot_goalrating_ons_offenseitems(this, 10000, this.origin, 10000);
 +              navigation_goalrating_end(this);
 +
 +              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
 +      }
 +}
 +
 +void havocbot_role_ons_assistant(entity this)
 +{
 +      havocbot_ons_reset_role(this);
 +}
 +
 +void havocbot_role_ons_defense(entity this)
 +{
 +      havocbot_ons_reset_role(this);
 +}
 +
 +void havocbot_ons_reset_role(entity this)
 +{
 +      if(IS_DEAD(this))
 +              return;
 +
 +      this.havocbot_ons_target = NULL;
 +
 +      // TODO: Defend control points or generator if necessary
 +
 +      havocbot_role_ons_setrole(this, HAVOCBOT_ONS_ROLE_OFFENSE);
 +}
 +
 +
 +/*
 + * Find control point or generator owned by the same team self which is nearest to pos
 + * if max_dist is positive, only control points within this range will be considered
 + */
 +entity ons_Nearest_ControlPoint(entity this, vector pos, float max_dist)
 +{
 +      entity closest_target = NULL;
 +      FOREACH_ENTITY_CLASS("onslaught_controlpoint", true,
 +      {
 +              if(SAME_TEAM(it, this))
 +              if(it.iscaptured)
 +              if(max_dist <= 0 || vdist(it.origin - pos, <=, max_dist))
 +              if(vlen2(it.origin - pos) <= vlen2(closest_target.origin - pos) || closest_target == NULL)
 +                      closest_target = it;
 +      });
 +      FOREACH_ENTITY_CLASS("onslaught_generator", true,
 +      {
 +              if(SAME_TEAM(it, this))
 +              if(max_dist <= 0 || vdist(it.origin - pos, <, max_dist))
 +              if(vlen2(it.origin - pos) <= vlen2(closest_target.origin - pos) || closest_target == NULL)
 +                      closest_target = it;
 +      });
 +
 +      return closest_target;
 +}
 +
 +/*
 + * Find control point or generator owned by the same team self which is nearest to pos
 + * if max_dist is positive, only control points within this range will be considered
 + * This function only check distances on the XY plane, disregarding Z
 + */
 +entity ons_Nearest_ControlPoint_2D(entity this, vector pos, float max_dist)
 +{
 +      entity closest_target = NULL;
 +      vector delta;
 +      float smallest_distance = 0, distance;
 +
 +      FOREACH_ENTITY_CLASS("onslaught_controlpoint", true,
 +      {
 +              delta = it.origin - pos;
 +              delta_z = 0;
 +              distance = vlen(delta);
 +
 +              if(SAME_TEAM(it, this))
 +              if(it.iscaptured)
 +              if(max_dist <= 0 || distance <= max_dist)
 +              if(closest_target == NULL || distance <= smallest_distance )
 +              {
 +                      closest_target = it;
 +                      smallest_distance = distance;
 +              }
 +      });
 +      FOREACH_ENTITY_CLASS("onslaught_generator", true,
 +      {
 +              delta = it.origin - pos;
 +              delta_z = 0;
 +              distance = vlen(delta);
 +
 +              if(SAME_TEAM(it, this))
 +              if(max_dist <= 0 || distance <= max_dist)
 +              if(closest_target == NULL || distance <= smallest_distance )
 +              {
 +                      closest_target = it;
 +                      smallest_distance = distance;
 +              }
 +      });
 +
 +      return closest_target;
 +}
 +/**
 + * find the number of control points and generators in the same team as this
 + */
 +int ons_Count_SelfControlPoints(entity this)
 +{
 +      int n = 0;
 +      FOREACH_ENTITY_CLASS("onslaught_controlpoint", true,
 +      {
 +              if(SAME_TEAM(it, this))
 +              if(it.iscaptured)
 +                      n++;
 +      });
 +      FOREACH_ENTITY_CLASS("onslaught_generator", true,
 +      {
 +              if(SAME_TEAM(it, this))
 +                      n++;
 +      });
 +      return n;
 +}
 +
 +/**
 + * Teleport player to a random position near tele_target
 + * if tele_effects is true, teleport sound+particles are created
 + * return false on failure
 + */
 +bool ons_Teleport(entity player, entity tele_target, float range, bool tele_effects)
 +{
 +      if ( !tele_target )
 +              return false;
 +
 +      int i;
 +      vector loc;
 +      float theta;
 +      // narrow the range for each iteration to increase chances that a spawnpoint
 +      // can be found even if there's little room around the control point
 +      float iteration_scale = 1;
 +      for(i = 0; i < 16; ++i)
 +      {
 +              iteration_scale -= i / 16;
 +              theta = random() * 2 * M_PI;
 +              loc_y = sin(theta);
 +              loc_x = cos(theta);
 +              loc_z = 0;
 +              loc *= random() * range * iteration_scale;
 +
 +              loc += tele_target.origin + '0 0 128' * iteration_scale;
 +
 +              tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player);
 +              if(trace_fraction == 1.0 && !trace_startsolid)
 +              {
 +                      traceline(tele_target.origin, loc, MOVE_NOMONSTERS, tele_target); // double check to make sure we're not spawning outside the NULL
 +                      if(trace_fraction == 1.0 && !trace_startsolid)
 +                      {
 +                              if ( tele_effects )
 +                              {
 +                                      Send_Effect(EFFECT_TELEPORT, player.origin, '0 0 0', 1);
 +                                      sound (player, CH_TRIGGER, SND_TELEPORT, VOL_BASE, ATTEN_NORM);
 +                              }
 +                              setorigin(player, loc);
 +                              player.angles = '0 1 0' * ( theta * RAD2DEG + 180 );
 +                              makevectors(player.angles);
 +                              player.fixangle = true;
 +                              player.teleport_antispam = time + autocvar_g_onslaught_teleport_wait;
 +
 +                              if ( tele_effects )
 +                                      Send_Effect(EFFECT_TELEPORT, player.origin + v_forward * 32, '0 0 0', 1);
 +                              return true;
 +                      }
 +              }
 +      }
 +
 +      return false;
 +}
 +
 +// ==============
 +// Hook Functions
 +// ==============
 +
 +MUTATOR_HOOKFUNCTION(ons, reset_map_global)
 +{
 +      FOREACH_CLIENT(IS_PLAYER(it), {
-       client.ons_roundlost = spectatee.ons_roundlost; // make spectators see it too
++              STAT(ROUNDLOST, it) = false;
 +              it.ons_deathloc = '0 0 0';
 +              PutClientInServer(it);
 +      });
 +      return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, ClientDisconnect)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      player.ons_deathloc = '0 0 0';
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, MakePlayerObserver)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      player.ons_deathloc = '0 0 0';
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, PlayerSpawn)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(!round_handler_IsRoundStarted())
 +      {
 +              player.player_blocked = true;
 +              return false;
 +      }
 +
 +      entity l;
 +      for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
 +      {
 +              l.sprite.SendFlags |= 16;
 +      }
 +      for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
 +      {
 +              l.sprite.SendFlags |= 16;
 +      }
 +
 +      if(ons_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT); }
 +
 +      if ( autocvar_g_onslaught_spawn_choose )
 +      if ( player.ons_spawn_by )
 +      if ( ons_Teleport(player,player.ons_spawn_by,autocvar_g_onslaught_teleport_radius,false) )
 +      {
 +              player.ons_spawn_by = NULL;
 +              return false;
 +      }
 +
 +      if(autocvar_g_onslaught_spawn_at_controlpoints)
 +      if(random() <= autocvar_g_onslaught_spawn_at_controlpoints_chance)
 +      {
 +              float random_target = autocvar_g_onslaught_spawn_at_controlpoints_random;
 +              entity tmp_entity, closest_target = NULL;
 +              vector spawn_loc = player.ons_deathloc;
 +
 +              // new joining player or round reset, don't bother checking
 +              if(spawn_loc == '0 0 0') { return false; }
 +
 +              if(random_target) { RandomSelection_Init(); }
 +
 +              for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
 +              {
 +                      if(SAME_TEAM(tmp_entity, player))
 +                      if(random_target)
 +                              RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
 +                      else if(vlen2(tmp_entity.origin - spawn_loc) <= vlen2(closest_target.origin - spawn_loc) || closest_target == NULL)
 +                              closest_target = tmp_entity;
 +              }
 +
 +              if(random_target) { closest_target = RandomSelection_chosen_ent; }
 +
 +              if(closest_target)
 +              {
 +                      float i;
 +                      vector loc;
 +                      float iteration_scale = 1;
 +                      for(i = 0; i < 10; ++i)
 +                      {
 +                              iteration_scale -= i / 10;
 +                              loc = closest_target.origin + '0 0 96' * iteration_scale;
 +                              loc += ('0 1 0' * random()) * 128 * iteration_scale;
 +                              tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player);
 +                              if(trace_fraction == 1.0 && !trace_startsolid)
 +                              {
 +                                      traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the NULL
 +                                      if(trace_fraction == 1.0 && !trace_startsolid)
 +                                      {
 +                                              setorigin(player, loc);
 +                                              player.angles = normalize(loc - closest_target.origin) * RAD2DEG;
 +                                              return false;
 +                                      }
 +                              }
 +                      }
 +              }
 +      }
 +
 +      if(autocvar_g_onslaught_spawn_at_generator)
 +      if(random() <= autocvar_g_onslaught_spawn_at_generator_chance)
 +      {
 +              float random_target = autocvar_g_onslaught_spawn_at_generator_random;
 +              entity tmp_entity, closest_target = NULL;
 +              vector spawn_loc = player.ons_deathloc;
 +
 +              // new joining player or round reset, don't bother checking
 +              if(spawn_loc == '0 0 0') { return false; }
 +
 +              if(random_target) { RandomSelection_Init(); }
 +
 +              for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
 +              {
 +                      if(random_target)
 +                              RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
 +                      else
 +                      {
 +                              if(SAME_TEAM(tmp_entity, player))
 +                              if(vlen2(tmp_entity.origin - spawn_loc) <= vlen2(closest_target.origin - spawn_loc) || closest_target == NULL)
 +                                      closest_target = tmp_entity;
 +                      }
 +              }
 +
 +              if(random_target) { closest_target = RandomSelection_chosen_ent; }
 +
 +              if(closest_target)
 +              {
 +                      float i;
 +                      vector loc;
 +                      float iteration_scale = 1;
 +                      for(i = 0; i < 10; ++i)
 +                      {
 +                              iteration_scale -= i / 10;
 +                              loc = closest_target.origin + '0 0 128' * iteration_scale;
 +                              loc += ('0 1 0' * random()) * 256 * iteration_scale;
 +                              tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player);
 +                              if(trace_fraction == 1.0 && !trace_startsolid)
 +                              {
 +                                      traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the NULL
 +                                      if(trace_fraction == 1.0 && !trace_startsolid)
 +                                      {
 +                                              setorigin(player, loc);
 +                                              player.angles = normalize(loc - closest_target.origin) * RAD2DEG;
 +                                              return false;
 +                                      }
 +                              }
 +                      }
 +              }
 +      }
 +
 +      return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, PlayerDies)
 +{
 +      entity frag_target = M_ARGV(2, entity);
 +
 +      frag_target.ons_deathloc = frag_target.origin;
 +      entity l;
 +      for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
 +      {
 +              l.sprite.SendFlags |= 16;
 +      }
 +      for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
 +      {
 +              l.sprite.SendFlags |= 16;
 +      }
 +
 +      if ( autocvar_g_onslaught_spawn_choose )
 +      if ( ons_Count_SelfControlPoints(frag_target) > 1 )
 +              stuffcmd(frag_target, "qc_cmd_cl hud clickradar\n");
 +
 +      return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, MonsterMove)
 +{
 +      entity mon = M_ARGV(0, entity);
 +
 +      entity e = find(NULL, targetname, mon.target);
 +      if (e != NULL)
 +              mon.team = e.team;
 +}
 +
 +void ons_MonsterSpawn_Delayed(entity this)
 +{
 +      entity own = this.owner;
 +
 +      if(!own) { delete(this); return; }
 +
 +      if(own.targetname)
 +      {
 +              entity e = find(NULL, target, own.targetname);
 +              if(e != NULL)
 +              {
 +                      own.team = e.team;
 +
 +                      own.use(own, e, NULL);
 +              }
 +      }
 +
 +      delete(this);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, MonsterSpawn)
 +{
 +      entity mon = M_ARGV(0, entity);
 +
 +      entity e = spawn();
 +      e.owner = mon;
 +      InitializeEntity(e, ons_MonsterSpawn_Delayed, INITPRIO_FINDTARGET);
 +}
 +
 +void ons_TurretSpawn_Delayed(entity this)
 +{
 +      entity own = this.owner;
 +
 +      if(!own) { delete(this); return; }
 +
 +      if(own.targetname)
 +      {
 +              entity e = find(NULL, target, own.targetname);
 +              if(e != NULL)
 +              {
 +                      own.team = e.team;
 +                      own.active = ACTIVE_NOT;
 +
 +                      own.use(own, e, NULL);
 +              }
 +      }
 +
 +      delete(this);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, TurretSpawn)
 +{
 +      entity turret = M_ARGV(0, entity);
 +
 +      entity e = spawn();
 +      e.owner = turret;
 +      InitializeEntity(e, ons_TurretSpawn_Delayed, INITPRIO_FINDTARGET);
 +
 +      return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, HavocBot_ChooseRole)
 +{
 +      entity bot = M_ARGV(0, entity);
 +
 +      havocbot_ons_reset_role(bot);
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, GetTeamCount)
 +{
 +      // onslaught is special
 +      for(entity tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
 +      {
 +              switch(tmp_entity.team)
 +              {
 +                      case NUM_TEAM_1: c1 = 0; break;
 +                      case NUM_TEAM_2: c2 = 0; break;
 +                      case NUM_TEAM_3: c3 = 0; break;
 +                      case NUM_TEAM_4: c4 = 0; break;
 +              }
 +      }
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, SpectateCopy)
 +{
 +      entity spectatee = M_ARGV(0, entity);
 +      entity client = M_ARGV(1, entity);
 +
++      STAT(ROUNDLOST, client) = STAT(ROUNDLOST, spectatee); // make spectators see it too
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, SV_ParseClientCommand)
 +{
 +      if(MUTATOR_RETURNVALUE) // command was already handled?
 +              return false;
 +
 +      entity player = M_ARGV(0, entity);
 +      string cmd_name = M_ARGV(1, string);
 +      int cmd_argc = M_ARGV(2, int);
 +
 +      if ( cmd_name == "ons_spawn" )
 +      {
 +              vector pos = player.origin;
 +              if(cmd_argc > 1)
 +                      pos_x = stof(argv(1));
 +              if(cmd_argc > 2)
 +                      pos_y = stof(argv(2));
 +              if(cmd_argc > 3)
 +                      pos_z = stof(argv(3));
 +
 +              if ( IS_PLAYER(player) )
 +              {
 +                      if ( !STAT(FROZEN, player) )
 +                      {
 +                              entity source_point = ons_Nearest_ControlPoint(player, player.origin, autocvar_g_onslaught_teleport_radius);
 +
 +                              if ( !source_point && player.health > 0 )
 +                              {
 +                                      sprint(player, "\nYou need to be next to a control point\n");
 +                                      return true;
 +                              }
 +
 +
 +                              entity closest_target = ons_Nearest_ControlPoint_2D(player, pos, autocvar_g_onslaught_click_radius);
 +
 +                              if ( closest_target == NULL )
 +                              {
 +                                      sprint(player, "\nNo control point found\n");
 +                                      return true;
 +                              }
 +
 +                              if ( player.health <= 0 )
 +                              {
 +                                      player.ons_spawn_by = closest_target;
 +                                      player.respawn_flags = player.respawn_flags | RESPAWN_FORCE;
 +                              }
 +                              else
 +                              {
 +                                      if ( source_point == closest_target )
 +                                      {
 +                                              sprint(player, "\nTeleporting to the same point\n");
 +                                              return true;
 +                                      }
 +
 +                                      if ( !ons_Teleport(player,closest_target,autocvar_g_onslaught_teleport_radius,true) )
 +                                              sprint(player, "\nUnable to teleport there\n");
 +                              }
 +
 +                              return true;
 +                      }
 +
 +                      sprint(player, "\nNo teleportation for you\n");
 +              }
 +
 +              return true;
 +      }
 +      return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, PlayerUseKey)
 +{
 +      if(MUTATOR_RETURNVALUE || gameover) { return false; }
 +
 +      entity player = M_ARGV(0, entity);
 +
 +      if((time > player.teleport_antispam) && (!IS_DEAD(player)) && !player.vehicle)
 +      {
 +              entity source_point = ons_Nearest_ControlPoint(player, player.origin, autocvar_g_onslaught_teleport_radius);
 +              if ( source_point )
 +              {
 +                      stuffcmd(player, "qc_cmd_cl hud clickradar\n");
 +                      return true;
 +              }
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, PlayHitsound)
 +{
 +      entity frag_victim = M_ARGV(0, entity);
 +
 +      return (frag_victim.classname == "onslaught_generator" && !frag_victim.isshielded)
 +              || (frag_victim.classname == "onslaught_controlpoint_icon" && !frag_victim.owner.isshielded);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, SendWaypoint)
 +{
 +    entity wp = M_ARGV(0, entity);
 +    entity to = M_ARGV(1, entity);
 +    int sf = M_ARGV(2, int);
 +    int wp_flag = M_ARGV(3, int);
 +
 +      if(sf & 16)
 +      {
 +              if(wp.owner.classname == "onslaught_controlpoint")
 +              {
 +                      entity wp_owner = wp.owner;
 +                      entity e = WaypointSprite_getviewentity(to);
 +                      if(SAME_TEAM(e, wp_owner) && wp_owner.goalentity.health >= wp_owner.goalentity.max_health) { wp_flag |= 2; }
 +                      if(!ons_ControlPoint_Attackable(wp_owner, e.team)) { wp_flag |= 2; }
 +              }
 +              if(wp.owner.classname == "onslaught_generator")
 +              {
 +                      entity wp_owner = wp.owner;
 +                      if(wp_owner.isshielded && wp_owner.health >= wp_owner.max_health) { wp_flag |= 2; }
 +                      if(wp_owner.health <= 0) { wp_flag |= 2; }
 +              }
 +      }
 +
 +      M_ARGV(3, int) = wp_flag;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, TurretValidateTarget)
 +{
 +      entity turret_target = M_ARGV(1, entity);
 +
 +      if(substring(turret_target.classname, 0, 10) == "onslaught_") // don't attack onslaught targets, that's the player's job!
 +      {
 +              M_ARGV(3, float) = -3;
 +              return true;
 +      }
 +
 +      return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ons, TurretThink)
 +{
 +    entity turret = M_ARGV(0, entity);
 +
 +      // ONS uses somewhat backwards linking.
 +      if(turret.target)
 +      {
 +              entity e = find(NULL, targetname, turret.target);
 +              if (e != NULL)
 +                      turret.team = e.team;
 +      }
 +
 +      if(turret.team != turret.tur_head.team)
 +              turret_respawn(turret);
 +}
 +
 +
 +// ==========
 +// Spawnfuncs
 +// ==========
 +
 +/*QUAKED spawnfunc_onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16)
 +  Link between control points.
 +
 +  This entity targets two different spawnfunc_onslaught_controlpoint or spawnfunc_onslaught_generator entities, and suppresses shielding on both if they are owned by different teams.
 +
 +keys:
 +"target" - first control point.
 +"target2" - second control point.
 + */
 +spawnfunc(onslaught_link)
 +{
 +      if(!g_onslaught) { delete(this); return; }
 +
 +      if (this.target == "" || this.target2 == "")
 +              objerror(this, "target and target2 must be set\n");
 +
 +      this.ons_worldlinknext = ons_worldlinklist; // link into ons_worldlinklist
 +      ons_worldlinklist = this;
 +
 +      InitializeEntity(this, ons_DelayedLinkSetup, INITPRIO_FINDTARGET);
 +      Net_LinkEntity(this, false, 0, ons_Link_Send);
 +}
 +
 +/*QUAKED spawnfunc_onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128)
 +  Control point. Be sure to give this enough clearance so that the shootable part has room to exist
 +
 +  This should link to an spawnfunc_onslaught_controlpoint entity or spawnfunc_onslaught_generator entity.
 +
 +keys:
 +"targetname" - name that spawnfunc_onslaught_link entities will use to target this.
 +"target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities.
 +"message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc)
 + */
 +
 +spawnfunc(onslaught_controlpoint)
 +{
 +      if(!g_onslaught) { delete(this); return; }
 +
 +      ons_ControlPoint_Setup(this);
 +}
 +
 +/*QUAKED spawnfunc_onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64)
 +  Base generator.
 +
 +  spawnfunc_onslaught_link entities can target this.
 +
 +keys:
 +"team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET.
 +"targetname" - name that spawnfunc_onslaught_link entities will use to target this.
 + */
 +spawnfunc(onslaught_generator)
 +{
 +      if(!g_onslaught) { delete(this); return; }
 +      if(!this.team) { objerror(this, "team must be set"); }
 +
 +      ons_GeneratorSetup(this);
 +}
 +
 +// scoreboard setup
 +void ons_ScoreRules()
 +{
 +      CheckAllowedTeams(NULL);
 +      int teams = 0;
 +      if(c1 >= 0) teams |= BIT(0);
 +      if(c2 >= 0) teams |= BIT(1);
 +      if(c3 >= 0) teams |= BIT(2);
 +      if(c4 >= 0) teams |= BIT(3);
 +      ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
 +      ScoreInfo_SetLabel_TeamScore  (ST_ONS_CAPS,     "destroyed", SFL_SORT_PRIO_PRIMARY);
 +      ScoreInfo_SetLabel_PlayerScore(SP_ONS_CAPS,    "caps",      SFL_SORT_PRIO_SECONDARY);
 +      ScoreInfo_SetLabel_PlayerScore(SP_ONS_TAKES,    "takes",     0);
 +      ScoreRules_basics_end();
 +}
 +
 +void ons_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up
 +{
 +      ons_ScoreRules();
 +
 +      round_handler_Spawn(Onslaught_CheckPlayers, Onslaught_CheckWinner, Onslaught_RoundStart);
 +      round_handler_Init(5, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
 +}
 +
 +void ons_Initialize()
 +{
 +      g_onslaught = true;
 +      ons_captureshield_force = autocvar_g_onslaught_shield_force;
 +
 +      InitializeEntity(NULL, ons_DelayedInit, INITPRIO_GAMETYPE);
 +}
index f70ba3c55c678e2c0470c07ce620d3356251266d,0000000000000000000000000000000000000000..70a5c8b6ba999078cb459c8933e2c11f7ab660e8
mode 100644,000000..100644
--- /dev/null
@@@ -1,119 -1,0 +1,117 @@@
- .bool ons_roundlost = _STAT(ROUNDLOST);
 +#pragma once
 +
 +float autocvar_g_onslaught_point_limit;
 +void ons_Initialize();
 +
 +REGISTER_MUTATOR(ons, false)
 +{
 +      MUTATOR_ONADD
 +      {
 +              if (time > 1) // game loads at time 1
 +                      error("This is a game type and it cannot be added at runtime.");
 +              ons_Initialize();
 +
 +              ActivateTeamplay();
 +              SetLimits(autocvar_g_onslaught_point_limit, autocvar_leadlimit_override, autocvar_timelimit_override, -1);
 +              have_team_spawns = -1; // request team spawns
 +      }
 +
 +      MUTATOR_ONROLLBACK_OR_REMOVE
 +      {
 +              // we actually cannot roll back ons_Initialize here
 +              // BUT: we don't need to! If this gets called, adding always
 +              // succeeds.
 +      }
 +
 +      MUTATOR_ONREMOVE
 +      {
 +              LOG_INFO("This is a game type and it cannot be removed at runtime.");
 +              return -1;
 +      }
 +
 +      return false;
 +}
 +
 +.entity ons_toucher; // player who touched the control point
 +
 +// control point / generator constants
 +const float ONS_CP_THINKRATE = 0.2;
 +const float GEN_THINKRATE = 1;
 +#define CPGEN_SPAWN_OFFSET ('0 0 1' * (PL_MAX_CONST.z - 13))
 +const vector CPGEN_WAYPOINT_OFFSET = ('0 0 128');
 +const vector CPICON_OFFSET = ('0 0 96');
 +
 +// list of generators on the map
 +entity ons_worldgeneratorlist;
 +.entity ons_worldgeneratornext;
 +.entity ons_stalegeneratornext;
 +
 +// list of control points on the map
 +entity ons_worldcplist;
 +.entity ons_worldcpnext;
 +.entity ons_stalecpnext;
 +
 +// list of links on the map
 +entity ons_worldlinklist;
 +.entity ons_worldlinknext;
 +.entity ons_stalelinknext;
 +
 +// definitions
 +.entity sprite;
 +.string target2;
 +.int iscaptured;
 +.int islinked;
 +.int isshielded;
 +.float lasthealth;
 +.int lastteam;
 +.int lastshielded;
 +.int lastcaptured;
 +
 +.bool waslinked;
 +
 +bool ons_stalemate;
 +
 +.float teleport_antispam;
 +
 +// waypoint sprites
 +.entity bot_basewaypoint; // generator waypointsprite
 +
 +.bool isgenneighbor[17];
 +.bool iscpneighbor[17];
 +float ons_notification_time[17];
 +
 +.float ons_overtime_damagedelay;
 +
 +.vector ons_deathloc;
 +
 +.entity ons_spawn_by;
 +
 +// declarations for functions used outside gamemode_onslaught.qc
 +void ons_Generator_UpdateSprite(entity e);
 +void ons_ControlPoint_UpdateSprite(entity e);
 +bool ons_ControlPoint_Attackable(entity cp, int teamnumber);
 +
 +// CaptureShield: Prevent capturing or destroying control point/generator if it is not available yet
 +float ons_captureshield_force; // push force of the shield
 +
 +// bot player logic
 +const int HAVOCBOT_ONS_ROLE_NONE              = 0;
 +const int HAVOCBOT_ONS_ROLE_DEFENSE   = 2;
 +const int HAVOCBOT_ONS_ROLE_ASSISTANT         = 4;
 +const int HAVOCBOT_ONS_ROLE_OFFENSE   = 8;
 +
 +.entity havocbot_ons_target;
 +
 +.int havocbot_role_flags;
 +.float havocbot_attack_time;
 +
 +void havocbot_role_ons_defense(entity this);
 +void havocbot_role_ons_offense(entity this);
 +void havocbot_role_ons_assistant(entity this);
 +
 +void havocbot_ons_reset_role(entity this);
 +void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius);
 +void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius);
 +
 +// score rule declarations
 +const int ST_ONS_CAPS = 1;
index 9a45e856a74570c704ca9d6497c662b296ca1ee3,c72c7461ed5e039f39b949c0c5a6543803e3b9f8..a4f7e6acaace738772fd366d81f499b57a4cdf7c
@@@ -1,27 -1,29 +1,27 @@@
 -#if defined(CSQC)
 -#elif defined(MENUQC)
 -#elif defined(SVQC)
 -    #include <lib/warpzone/common.qh>
 -    #include "../constants.qh"
 -    #include "../teams.qh"
 -    #include "../util.qh"
 -    #include "all.qh"
 -    #include "sv_monsters.qh"
 -      #include "../physics/movelib.qh"
 -    #include "../weapons/all.qh"
 -    #include <server/autocvars.qh>
 -    #include <server/defs.qh>
 -    #include "../deathtypes/all.qh"
 -    #include <server/mutators/all.qh>
 -      #include <server/steerlib.qh>
 -      #include "../turrets/sv_turrets.qh"
 -      #include "../turrets/util.qh"
 -    #include "../vehicles/all.qh"
 -    #include <server/campaign.qh>
 -    #include <server/command/common.qh>
 -    #include <server/command/cmd.qh>
 -      #include "../triggers/triggers.qh"
 -    #include <lib/csqcmodel/sv_model.qh>
 -    #include <server/round_handler.qh>
 -#endif
 +#include "sv_monsters.qh"
 +
 +#include <server/g_subs.qh>
 +#include <lib/warpzone/common.qh>
 +#include "../constants.qh"
 +#include "../teams.qh"
 +#include "../util.qh"
 +#include "all.qh"
 +#include "../physics/movelib.qh"
 +#include "../weapons/_mod.qh"
 +#include <server/autocvars.qh>
 +#include <server/defs.qh>
 +#include "../deathtypes/all.qh"
 +#include <server/mutators/_mod.qh>
 +#include <server/steerlib.qh>
 +#include "../turrets/sv_turrets.qh"
 +#include "../turrets/util.qh"
 +#include "../vehicles/all.qh"
 +#include <server/campaign.qh>
 +#include <server/command/_mod.qh>
 +#include "../triggers/triggers.qh"
 +#include <lib/csqcmodel/sv_model.qh>
 +#include <server/round_handler.qh>
 +#include <server/weapons/_mod.qh>
  
  void monsters_setstatus(entity this)
  {
@@@ -92,6 -94,7 +92,7 @@@ bool Monster_ValidTarget(entity this, e
        || (SAME_TEAM(targ, this))
        || (STAT(FROZEN, targ))
        || (targ.alpha != 0 && targ.alpha < 0.5)
+       || (MUTATOR_CALLHOOK(MonsterValidTarget, this, targ))
        )
        {
                // if any of the above checks fail, target is not valid
@@@ -506,8 -509,6 +507,8 @@@ bool Monster_Respawn_Check(entity this
  
  void Monster_Respawn(entity this) { Monster_Spawn(this, this.monsterid); }
  
 +.vector       pos1, pos2;
 +
  void Monster_Dead_Fade(entity this)
  {
        if(Monster_Respawn_Check(this))
@@@ -543,7 -544,6 +544,7 @@@ void Monster_Use(entity this, entity ac
        if(Monster_ValidTarget(this, actor)) { this.enemy = actor; }
  }
  
 +.float pass_distance;
  vector Monster_Move_Target(entity this, entity targ)
  {
        // enemy is always preferred target
@@@ -687,7 -687,6 +688,7 @@@ void Monster_CalculateVelocity(entity t
  }
  
  .entity draggedby;
 +.entity target2;
  
  void Monster_Move(entity this, float runspeed, float walkspeed, float stpspeed)
  {
index be7d3a2e2bbf3372029df5ea66db773525d87dc5,0000000000000000000000000000000000000000..c4d2e030267114b46cba723d8322a614a0d8de26
mode 100644,000000..100644
--- /dev/null
@@@ -1,91 -1,0 +1,92 @@@
 +#include "sv_campcheck.qh"
 +
 +float autocvar_g_campcheck_damage;
 +float autocvar_g_campcheck_distance;
 +float autocvar_g_campcheck_interval;
 +
 +REGISTER_MUTATOR(campcheck, cvar("g_campcheck"));
 +
 +.float campcheck_nextcheck;
 +.float campcheck_traveled_distance;
 +
 +MUTATOR_HOOKFUNCTION(campcheck, PlayerDies)
 +{
 +      entity frag_target = M_ARGV(2, entity);
 +
 +      Kill_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CPID_CAMPCHECK);
 +}
 +
 +MUTATOR_HOOKFUNCTION(campcheck, PlayerDamage_Calculate)
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +
 +      if(IS_PLAYER(frag_target))
 +      if(IS_PLAYER(frag_attacker))
 +      if(frag_attacker != frag_target)
 +      {
 +              frag_target.campcheck_traveled_distance = autocvar_g_campcheck_distance;
 +              frag_attacker.campcheck_traveled_distance = autocvar_g_campcheck_distance;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(campcheck, PlayerPreThink)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(!gameover)
 +      if(!warmup_stage) // don't consider it camping during warmup?
 +      if(time >= game_starttime)
 +      if(IS_PLAYER(player))
 +      if(IS_REAL_CLIENT(player)) // bots may camp, but that's no reason to constantly kill them
 +      if(!IS_DEAD(player))
++      if(!forbidWeaponUse(player))
 +      if(!STAT(FROZEN, player))
 +      if(!PHYS_INPUT_BUTTON_CHAT(player))
 +      if(autocvar_g_campcheck_interval)
 +      {
 +              vector dist;
 +
 +              // calculate player movement (in 2 dimensions only, so jumping on one spot doesn't count as movement)
 +              dist = player.prevorigin - player.origin;
 +              dist.z = 0;
 +              player.campcheck_traveled_distance += fabs(vlen(dist));
 +
 +              if((autocvar_g_campaign && !campaign_bots_may_start) || (time < game_starttime) || (round_handler_IsActive() && !round_handler_IsRoundStarted()))
 +              {
 +                      player.campcheck_nextcheck = time + autocvar_g_campcheck_interval * 2;
 +                      player.campcheck_traveled_distance = 0;
 +              }
 +
 +              if(time > player.campcheck_nextcheck)
 +              {
 +                      if(player.campcheck_traveled_distance < autocvar_g_campcheck_distance)
 +                      {
 +                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CAMPCHECK);
 +                              if(player.vehicle)
 +                                      Damage(player.vehicle, NULL, NULL, autocvar_g_campcheck_damage * 2, DEATH_CAMP.m_id, player.vehicle.origin, '0 0 0');
 +                              else
 +                                      Damage(player, NULL, NULL, bound(0, autocvar_g_campcheck_damage, player.health + player.armorvalue * autocvar_g_balance_armor_blockpercent + 5), DEATH_CAMP.m_id, player.origin, '0 0 0');
 +                      }
 +                      player.campcheck_nextcheck = time + autocvar_g_campcheck_interval;
 +                      player.campcheck_traveled_distance = 0;
 +              }
 +
 +              return;
 +      }
 +
 +      player.campcheck_nextcheck = time + autocvar_g_campcheck_interval; // one of the above checks failed, so keep the timer up to date
 +}
 +
 +MUTATOR_HOOKFUNCTION(campcheck, PlayerSpawn)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      player.campcheck_nextcheck = time + autocvar_g_campcheck_interval * 2;
 +      player.campcheck_traveled_distance = 0;
 +}
 +
 +MUTATOR_HOOKFUNCTION(campcheck, BuildMutatorsString)
 +{
 +      M_ARGV(0, string) = strcat(M_ARGV(0, string), ":CampCheck");
 +}
index 0c348b62410d48310e5c9f8d139b33c72c0fdde7,0000000000000000000000000000000000000000..3fe9de6d7d5f849ea9356d04b6930a59caf76769
mode 100644,000000..100644
--- /dev/null
@@@ -1,434 -1,0 +1,434 @@@
-         if (menu_enabled != 2)
 +#include "itemstime.qh"
 +
 +REGISTER_MUTATOR(itemstime, true);
 +
 +REGISTER_NET_TEMP(itemstime)
 +
 +#ifdef SVQC
 +void IT_Write(entity e, int i, float f) {
 +    if (!IS_REAL_CLIENT(e)) return;
 +    msg_entity = e;
 +    WriteHeader(MSG_ONE, itemstime);
 +    WriteByte(MSG_ONE, i);
 +    WriteFloat(MSG_ONE, f);
 +}
 +#endif
 +
 +#ifdef CSQC
 +// reserve one more spot for superweapons time
 +float ItemsTime_time[Items_MAX + 1];
 +float ItemsTime_availableTime[Items_MAX + 1];
 +NET_HANDLE(itemstime, bool isNew)
 +{
 +    int i = ReadByte();
 +    float f = ReadFloat();
 +    return = true;
 +    ItemsTime_time[i] = f;
 +}
 +#endif
 +
 +#ifdef CSQC
 +void Item_ItemsTime_Init()
 +{
 +    FOREACH(Items, true, LAMBDA(
 +        ItemsTime_time[it.m_id] = -1;
 +    ));
 +    ItemsTime_time[Items_MAX] = -1;
 +}
 +
 +STATIC_INIT(ItemsTime_Init) {
 +    Item_ItemsTime_Init();
 +}
 +
 +int autocvar_hud_panel_itemstime = 2;
 +float autocvar_hud_panel_itemstime_dynamicsize = 1;
 +float autocvar_hud_panel_itemstime_ratio = 2;
 +int autocvar_hud_panel_itemstime_iconalign;
 +bool autocvar_hud_panel_itemstime_progressbar = 0;
 +float autocvar_hud_panel_itemstime_progressbar_maxtime = 30;
 +string autocvar_hud_panel_itemstime_progressbar_name = "progressbar";
 +float autocvar_hud_panel_itemstime_progressbar_reduced;
 +bool autocvar_hud_panel_itemstime_hidespawned = 1;
 +bool autocvar_hud_panel_itemstime_hidelarge = false;
 +int autocvar_hud_panel_itemstime_text = 1;
 +#define hud_panel_itemstime_hidelarge autocvar_hud_panel_itemstime_hidelarge
 +#else
 +#define hud_panel_itemstime_hidelarge false
 +#endif
 +
 +bool Item_ItemsTime_SpectatorOnly(GameItem it)
 +{
 +    return (false
 +    || it == ITEM_ArmorMega     || (it == ITEM_ArmorLarge && !hud_panel_itemstime_hidelarge)
 +    || it == ITEM_HealthMega    || (it == ITEM_HealthLarge && !hud_panel_itemstime_hidelarge)
 +    );
 +}
 +
 +bool Item_ItemsTime_Allow(GameItem it)
 +{
 +    return (false
 +    || it.instanceOfPowerup
 +    || Item_ItemsTime_SpectatorOnly(it)
 +    );
 +}
 +
 +#ifdef SVQC
 +
 +// reserve one more spot for superweapons time
 +float it_times[Items_MAX + 1];
 +
 +void Item_ItemsTime_Init()
 +{
 +    FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +        it_times[it.m_id] = -1;
 +    ));
 +    it_times[Items_MAX] = -1;
 +}
 +
 +STATIC_INIT(ItemsTime_Init) {
 +    // items time
 +    Item_ItemsTime_Init();
 +}
 +
 +void Item_ItemsTime_ResetTimes()
 +{
 +    FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +        it_times[it.m_id] = (it_times[it.m_id] == -1) ? -1 : 0;
 +    ));
 +    it_times[Items_MAX] = (it_times[Items_MAX] == -1) ? -1 : 0;
 +}
 +
 +void Item_ItemsTime_ResetTimesForPlayer(entity e)
 +{
 +    FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +        IT_Write(e, it.m_id, (it_times[it.m_id] == -1) ? -1 : 0);
 +    ));
 +    IT_Write(e, Items_MAX, (it_times[Items_MAX] == -1) ? -1 : 0);
 +}
 +
 +void Item_ItemsTime_SetTimesForPlayer(entity e)
 +{
 +    FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +        IT_Write(e, it.m_id, it_times[it.m_id]);
 +    ));
 +    IT_Write(e, Items_MAX, it_times[Items_MAX]);
 +}
 +
 +void Item_ItemsTime_SetTime(entity e, float t)
 +{
 +    if (!autocvar_sv_itemstime)
 +        return;
 +
 +    GameItem item = e.itemdef;
 +    if (item.instanceOfGameItem)
 +    {
 +              if (!item.instanceOfWeaponPickup)
 +                      it_times[item.m_id] = t;
 +              else if (e.weapons & WEPSET_SUPERWEAPONS)
 +                      it_times[Items_MAX] = t;
 +    }
 +}
 +
 +void Item_ItemsTime_SetTimesForAllPlayers()
 +{
 +    FOREACH_CLIENT(IS_REAL_CLIENT(it) && (warmup_stage || !IS_PLAYER(it) || autocvar_sv_itemstime == 2), LAMBDA(Item_ItemsTime_SetTimesForPlayer(it)));
 +}
 +
 +float Item_ItemsTime_UpdateTime(entity e, float t)
 +{
 +    bool isavailable = (t == 0);
 +    FOREACH_ENTITY_FLOAT(pure_data, false,
 +    {
 +        if(!(it.itemdef == e.itemdef || ((e.weapons & WEPSET_SUPERWEAPONS) && (it.weapons & WEPSET_SUPERWEAPONS) && clienttype(it) == CLIENTTYPE_NOTACLIENT)))
 +            continue;
 +        if (e == it) continue;
 +        if (it.scheduledrespawntime <= time)
 +            isavailable = true;
 +        else if (t == 0 || it.scheduledrespawntime < t)
 +            t = it.scheduledrespawntime;
 +    });
 +    if (isavailable)
 +        t = -t; // let know the client there's another available item
 +    return t;
 +}
 +
 +MUTATOR_HOOKFUNCTION(itemstime, reset_map_global)
 +{
 +    Item_ItemsTime_ResetTimes();
 +    // ALL the times need to be reset before .reset()ing each item
 +    // since Item_Reset schedules respawn of superweapons and powerups
 +    FOREACH_ENTITY_FLOAT(pure_data, false,
 +    {
 +        if(IS_CLIENT(it))
 +            continue;
 +        if (it.reset) Item_ItemsTime_SetTime(it, 0);
 +    });
 +    Item_ItemsTime_SetTimesForAllPlayers();
 +}
 +
 +MUTATOR_HOOKFUNCTION(itemstime, MakePlayerObserver)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    Item_ItemsTime_SetTimesForPlayer(player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(itemstime, ClientConnect, CBC_ORDER_LAST)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +      if(IS_PLAYER(player))
 +      {
 +              // client became player on connection skipping putObserverInServer step
 +              if (IS_REAL_CLIENT(player))
 +              if (warmup_stage || autocvar_sv_itemstime == 2)
 +                      Item_ItemsTime_SetTimesForPlayer(player);
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(itemstime, PlayerSpawn)
 +{
 +    if (warmup_stage || autocvar_sv_itemstime == 2) return;
 +    entity player = M_ARGV(0, entity);
 +
 +    Item_ItemsTime_ResetTimesForPlayer(player);
 +}
 +
 +#endif
 +
 +#ifdef CSQC
 +
 +void DrawItemsTimeItem(vector myPos, vector mySize, float ar, string item_icon, float item_time, bool item_available, float item_availableTime)
 +{
 +    float t = 0;
 +    vector color = '0 0 0';
 +    float picalpha;
 +
 +    if (autocvar_hud_panel_itemstime_hidespawned == 2)
 +        picalpha = 1;
 +    else if (item_available)
 +    {
 +        float BLINK_FACTOR = 0.15;
 +        float BLINK_BASE = 0.85;
 +        float BLINK_FREQ = 5;
 +        picalpha = BLINK_BASE + BLINK_FACTOR * cos(time * BLINK_FREQ);
 +    }
 +    else
 +        picalpha = 0.5;
 +    t = floor(item_time - time + 0.999);
 +    if (t < 5)
 +        color = '0.7 0 0';
 +    else if (t < 10)
 +        color = '0.7 0.7 0';
 +    else
 +        color = '1 1 1';
 +
 +    vector picpos, numpos;
 +    if (autocvar_hud_panel_itemstime_iconalign)
 +    {
 +        numpos = myPos;
 +        picpos = myPos + eX * (ar - 1) * mySize_y;
 +    }
 +    else
 +    {
 +        numpos = myPos + eX * mySize_y;
 +        picpos = myPos;
 +    }
 +
 +    if (t > 0 && autocvar_hud_panel_itemstime_progressbar)
 +    {
 +        vector p_pos, p_size;
 +        if (autocvar_hud_panel_itemstime_progressbar_reduced)
 +        {
 +            p_pos = numpos;
 +            p_size = eX * ((ar - 1)/ar) * mySize_x + eY * mySize_y;
 +        }
 +        else
 +        {
 +            p_pos = myPos;
 +            p_size = mySize;
 +        }
 +        HUD_Panel_DrawProgressBar(p_pos, p_size, autocvar_hud_panel_itemstime_progressbar_name, t/autocvar_hud_panel_itemstime_progressbar_maxtime, 0, autocvar_hud_panel_itemstime_iconalign, color, autocvar_hud_progressbar_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +    }
 +
 +    if(autocvar_hud_panel_itemstime_text)
 +    {
 +        if(t > 0)
 +            drawstring_aspect(numpos, ftos(t), eX * ((ar - 1)/ar) * mySize_x + eY * mySize_y, color, panel_fg_alpha, DRAWFLAG_NORMAL);
 +        else if(precache_pic("gfx/hud/default/checkmark")) // COMPAT: check if this image exists, as 0.8.1 clients lack it
 +            drawpic_aspect_skin(numpos, "checkmark", eX * (ar - 1) * mySize_y + eY * mySize_y, '1 1 1', panel_fg_alpha * picalpha, DRAWFLAG_NORMAL);
 +        else // legacy code, if the image is missing just center the icon
 +            picpos.x = myPos.x + mySize.x / 2 - mySize.y / 2;
 +    }
 +    if (item_availableTime)
 +        drawpic_aspect_skin_expanding(picpos, item_icon, '1 1 0' * mySize_y, '1 1 1', panel_fg_alpha * picalpha, DRAWFLAG_NORMAL, item_availableTime);
 +    drawpic_aspect_skin(picpos, item_icon, '1 1 0' * mySize_y, '1 1 1', panel_fg_alpha * picalpha, DRAWFLAG_NORMAL);
 +}
 +
 +void HUD_ItemsTime()
 +{
 +    if (!autocvar__hud_configure)
 +    {
 +        if (!(
 +            (autocvar_hud_panel_itemstime == 1 && spectatee_status != 0)
 +        ||    (autocvar_hud_panel_itemstime == 2 && (spectatee_status != 0 || warmup_stage || STAT(ITEMSTIME) == 2))
 +            )) { return; }
 +    }
 +    else
 +    {
 +        ItemsTime_time[ITEM_ArmorMega.m_id] = time + 0;
 +        ItemsTime_time[ITEM_HealthMega.m_id] = time + 8;
 +        ItemsTime_time[ITEM_Strength.m_id] = time + 0;
 +        ItemsTime_time[ITEM_Shield.m_id] = time + 4;
 +    }
 +
 +    int count = 0;
 +    if (autocvar_hud_panel_itemstime_hidespawned == 1)
 +    {
 +        FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +            count += (ItemsTime_time[it.m_id] > time || -ItemsTime_time[it.m_id] > time);
 +        ));
 +        count += (ItemsTime_time[Items_MAX] > time || -ItemsTime_time[Items_MAX] > time);
 +    }
 +    else if (autocvar_hud_panel_itemstime_hidespawned == 2)
 +    {
 +        FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +            count += (ItemsTime_time[it.m_id] > time);
 +        ));
 +        count += (ItemsTime_time[Items_MAX] > time);
 +    }
 +    else
 +    {
 +        FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA(
 +            count += (ItemsTime_time[it.m_id] != -1);
 +        ));
 +        count += (ItemsTime_time[Items_MAX] != -1);
 +    }
 +    if (count == 0)
 +        return;
 +
 +    HUD_Panel_UpdateCvars();
 +
 +    vector pos, mySize;
 +    pos = panel_pos;
 +    mySize = panel_size;
 +
 +    if (panel_bg_padding)
 +    {
 +        pos += '1 1 0' * panel_bg_padding;
 +        mySize -= '2 2 0' * panel_bg_padding;
 +    }
 +
 +    float rows, columns;
 +    float ar = max(2, autocvar_hud_panel_itemstime_ratio) + 1;
 +    rows = HUD_GetRowCount(count, mySize, ar);
 +    columns = ceil(count/rows);
 +
 +    vector itemstime_size = eX * mySize.x*(1/columns) + eY * mySize.y*(1/rows);
 +
 +    vector offset = '0 0 0';
 +    float newSize;
 +    if (autocvar_hud_panel_itemstime_dynamicsize)
 +    {
 +        if (autocvar__hud_configure)
++        if (hud_configure_menu_open != 2)
 +            HUD_Panel_DrawBg(1); // also draw the bg of the entire panel
 +
 +        // reduce panel to avoid spacing items
 +        if (itemstime_size.x / itemstime_size.y < ar)
 +        {
 +            newSize = rows * itemstime_size.x / ar;
 +            pos.y += (mySize.y - newSize) / 2;
 +            mySize.y = newSize;
 +            itemstime_size.y = mySize.y / rows;
 +        }
 +        else
 +        {
 +            newSize = columns * itemstime_size.y * ar;
 +            pos.x += (mySize.x - newSize) / 2;
 +            mySize.x = newSize;
 +            itemstime_size.x = mySize.x / columns;
 +        }
 +        panel_pos = pos - '1 1 0' * panel_bg_padding;
 +        panel_size = mySize + '2 2 0' * panel_bg_padding;
 +    }
 +    else
 +    {
 +        if (itemstime_size.x/itemstime_size.y > ar)
 +        {
 +            newSize = ar * itemstime_size.y;
 +            offset.x = itemstime_size.x - newSize;
 +            pos.x += offset.x/2;
 +            itemstime_size.x = newSize;
 +        }
 +        else
 +        {
 +            newSize = 1/ar * itemstime_size.x;
 +            offset.y = itemstime_size.y - newSize;
 +            pos.y += offset.y/2;
 +            itemstime_size.y = newSize;
 +        }
 +    }
 +
 +    HUD_Scale_Enable();
 +    HUD_Panel_DrawBg(1);
 +
 +    float row = 0, column = 0;
 +    bool item_available;
 +    int id = 0;
 +    string icon = "";
 +    FOREACH(Items, Item_ItemsTime_Allow(it) && ItemsTime_time[it.m_id] != -1, LAMBDA(
 +      id = it.m_id;
 +      icon = it.m_icon;
 +
 +LABEL(iteration)
 +        float item_time = ItemsTime_time[id];
 +        if (item_time < -1)
 +        {
 +            item_available = true;
 +            item_time = -item_time;
 +        }
 +        else
 +            item_available = (item_time <= time);
 +
 +        if (ItemsTime_time[id] >= 0)
 +        {
 +            if (time <= ItemsTime_time[id])
 +                ItemsTime_availableTime[id] = 0;
 +            else if (ItemsTime_availableTime[id] == 0)
 +                ItemsTime_availableTime[id] = time;
 +        }
 +        else if (ItemsTime_availableTime[id] == 0)
 +            ItemsTime_availableTime[id] = time;
 +
 +        float f = (time - ItemsTime_availableTime[id]) * 2;
 +        f = (f > 1) ? 0 : bound(0, f, 1);
 +
 +        if (autocvar_hud_panel_itemstime_hidespawned == 1)
 +            if (!(ItemsTime_time[id] > time || -ItemsTime_time[id] > time))
 +                continue;
 +
 +        if (autocvar_hud_panel_itemstime_hidespawned == 2)
 +            if (!(ItemsTime_time[id] > time))
 +                continue;
 +
 +        DrawItemsTimeItem(pos + eX * column * (itemstime_size.x + offset.x) + eY * row * (itemstime_size.y + offset.y), itemstime_size, ar, icon, item_time, item_available, f);
 +        ++row;
 +        if (row >= rows)
 +        {
 +            row = 0;
 +            column = column + 1;
 +        }
 +        if(id == Items_MAX) // can happen only in the last fake iteration
 +              break;
 +    ));
 +    // add another fake iteration for superweapons time
 +    if(id < Items_MAX && ItemsTime_time[Items_MAX] != -1)
 +    {
 +              id = Items_MAX;
 +              icon = "superweapons";
 +              goto iteration;
 +    }
 +}
 +
 +#endif
index 4c57d018950d4445e061924bfa4c000b3a962ddf,0000000000000000000000000000000000000000..fb14f27db8063cb22d96b1dd9baad197b2f545d2
mode 100644,000000..100644
--- /dev/null
@@@ -1,187 -1,0 +1,188 @@@
 +#include "sv_spawn_near_teammate.qh"
 +
 +float autocvar_g_spawn_near_teammate_distance;
 +int autocvar_g_spawn_near_teammate_ignore_spawnpoint;
 +float autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay;
 +float autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death;
 +int autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health;
 +bool autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath;
 +
 +REGISTER_MUTATOR(spawn_near_teammate, cvar("g_spawn_near_teammate"));
 +
 +.entity msnt_lookat;
 +
 +.float msnt_timer;
 +.vector msnt_deathloc;
 +
 +.float cvar_cl_spawn_near_teammate;
 +
 +MUTATOR_HOOKFUNCTION(spawn_near_teammate, Spawn_Score)
 +{
 +      entity player = M_ARGV(0, entity);
 +      entity spawn_spot = M_ARGV(1, entity);
 +      vector spawn_score = M_ARGV(2, vector);
 +
 +      if(autocvar_g_spawn_near_teammate_ignore_spawnpoint == 1 || (autocvar_g_spawn_near_teammate_ignore_spawnpoint == 2 && player.cvar_cl_spawn_near_teammate))
 +              return;
 +
 +      spawn_spot.msnt_lookat = NULL;
 +
 +      if(!teamplay)
 +              return;
 +
 +      RandomSelection_Init();
 +      FOREACH_CLIENT(IS_PLAYER(it) && it != player && SAME_TEAM(it, player) && !IS_DEAD(it), LAMBDA(
 +              if(vdist(spawn_spot.origin - it.origin, >, autocvar_g_spawn_near_teammate_distance))
 +                      continue;
 +              if(vdist(spawn_spot.origin - it.origin, <, 48))
 +                      continue;
 +              if(!checkpvs(spawn_spot.origin, it))
 +                      continue;
 +              RandomSelection_Add(it, 0, string_null, 1, 1);
 +      ));
 +
 +      if(RandomSelection_chosen_ent)
 +      {
 +              spawn_spot.msnt_lookat = RandomSelection_chosen_ent;
 +              spawn_score.x += SPAWN_PRIO_NEAR_TEAMMATE_FOUND;
 +      }
 +      else if(player.team == spawn_spot.team)
 +              spawn_score.x += SPAWN_PRIO_NEAR_TEAMMATE_SAMETEAM; // prefer same team, if we can't find a spawn near teammate
 +
 +      M_ARGV(2, vector) = spawn_score;
 +}
 +
 +MUTATOR_HOOKFUNCTION(spawn_near_teammate, PlayerSpawn)
 +{
 +      if(!teamplay) { return; }
 +      entity player = M_ARGV(0, entity);
 +      entity spawn_spot = M_ARGV(1, entity);
 +
 +      int num_red = 0, num_blue = 0, num_yellow = 0, num_pink = 0;
 +      FOREACH_CLIENT(IS_PLAYER(it),
 +      {
 +              switch(it.team)
 +              {
 +                      case NUM_TEAM_1: ++num_red; break;
 +                      case NUM_TEAM_2: ++num_blue; break;
 +                      case NUM_TEAM_3: ++num_yellow; break;
 +                      case NUM_TEAM_4: ++num_pink; break;
 +              }
 +      });
 +
 +      if(num_red == 1 || num_blue == 1 || num_yellow == 1 || num_pink == 1)
 +              return; // at least 1 team has only 1 player, let's not give the bigger team too much of an advantage!
 +
 +      // Note: when entering this, fixangle is already set.
 +      if(autocvar_g_spawn_near_teammate_ignore_spawnpoint == 1 || (autocvar_g_spawn_near_teammate_ignore_spawnpoint == 2 && player.cvar_cl_spawn_near_teammate))
 +      {
 +              if(autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death)
 +                      player.msnt_timer = time + autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death;
 +
 +              entity best_mate = NULL;
 +              vector best_spot = '0 0 0';
 +              float pc = 0, best_dist = 0, dist = 0;
 +              FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
 +                      if((autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health >= 0 && it.health >= autocvar_g_balance_health_regenstable) || autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health == 0)
 +                      if(!IS_DEAD(it))
 +                      if(it.msnt_timer < time)
 +                      if(SAME_TEAM(player, it))
 +                      if(time > it.spawnshieldtime) // spawn shielding
++                      if(!forbidWeaponUse(it))
 +                      if(STAT(FROZEN, it) == 0)
 +                      if(it != player)
 +                      {
 +                              tracebox(it.origin, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - '0 0 100', MOVE_NOMONSTERS, it);
 +                              if(trace_fraction != 1.0)
 +                              if(!(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY))
 +                              {
 +                                      pc = pointcontents(trace_endpos + '0 0 1');
 +                                      if(pc == CONTENT_EMPTY)
 +                                      {
 +                                              if(vdist(it.velocity, >, 5))
 +                                                      fixedmakevectors(vectoangles(it.velocity));
 +                                              else
 +                                                      fixedmakevectors(it.angles);
 +
 +                                              for(pc = 0; pc < 4; ++pc) // test 4 diffrent spots close to mate
 +                                              {
 +                                                      switch(pc)
 +                                                      {
 +                                                              case 0:
 +                                                                      tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin + v_right * 128, MOVE_NOMONSTERS, it);
 +                                                                      break;
 +                                                              case 1:
 +                                                                      tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - v_right * 128 , MOVE_NOMONSTERS, it);
 +                                                                      break;
 +                                                              case 2:
 +                                                                      tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin + v_right * 128 - v_forward * 64, MOVE_NOMONSTERS, it);
 +                                                                      break;
 +                                                              case 3:
 +                                                                      tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - v_right * 128 - v_forward * 64, MOVE_NOMONSTERS, it);
 +                                                                      break;
 +                                                              //case 4:
 +                                                                      //tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - v_forward * 128, MOVE_NOMONSTERS, it);
 +                                                                      //break;
 +                                                      }
 +
 +                                                      if(trace_fraction == 1.0)
 +                                                      {
 +                                                              traceline(trace_endpos + '0 0 4', trace_endpos - '0 0 100', MOVE_NOMONSTERS, it);
 +                                                              if(trace_fraction != 1.0)
 +                                                              {
 +                                                                      if(autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath)
 +                                                                      {
 +                                                                              dist = vlen(trace_endpos - player.msnt_deathloc);
 +                                                                              if(dist < best_dist || best_dist == 0)
 +                                                                              {
 +                                                                                      best_dist = dist;
 +                                                                                      best_spot = trace_endpos;
 +                                                                                      best_mate = it;
 +                                                                              }
 +                                                                      }
 +                                                                      else
 +                                                                      {
 +                                                                              setorigin(player, trace_endpos);
 +                                                                              player.angles = it.angles;
 +                                                                              player.angles_z = 0; // never spawn tilted even if the spot says to
 +                                                                              it.msnt_timer = time + autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay;
 +                                                                              return;
 +                                                                      }
 +                                                              }
 +                                                      }
 +                                              }
 +                                      }
 +                              }
 +                      }
 +              ));
 +
 +              if(autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath)
 +              if(best_dist)
 +              {
 +                      setorigin(player, best_spot);
 +                      player.angles = best_mate.angles;
 +                      player.angles_z = 0; // never spawn tilted even if the spot says to
 +                      best_mate.msnt_timer = time + autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay;
 +              }
 +      }
 +      else if(spawn_spot.msnt_lookat)
 +      {
 +              player.angles = vectoangles(spawn_spot.msnt_lookat.origin - player.origin);
 +              player.angles_x = -player.angles.x;
 +              player.angles_z = 0; // never spawn tilted even if the spot says to
 +              /*
 +              sprint(player, "You should be looking at ", spawn_spot.msnt_lookat.netname, "^7.\n");
 +              sprint(player, "distance: ", vtos(spawn_spot.msnt_lookat.origin - player.origin), "\n");
 +              sprint(player, "angles: ", vtos(player.angles), "\n");
 +              */
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(spawn_near_teammate, PlayerDies)
 +{
 +      entity frag_target = M_ARGV(0, entity);
 +
 +      frag_target.msnt_deathloc = frag_target.origin;
 +}
 +
 +REPLICATE(cvar_cl_spawn_near_teammate, bool, "cl_spawn_near_teammate");
diff --combined qcsrc/common/stats.qh
index 61a4bfeaed25cd8aa48f0f3be33ccc37fa99219a,c8822926f2bd7892df35470dd543836e635ec83d..d6aa068ea67eb775da3cba979c9a62398e9cbcf2
@@@ -1,7 -1,7 +1,7 @@@
  #pragma once
  
  #ifdef SVQC
 -#include <server/cl_client.qh>
 +#include <server/client.qh>
  #endif
  
  // Full list of all stat constants, included in a single location for easy reference
@@@ -121,6 -121,7 +121,7 @@@ 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)
  REGISTER_STAT(ENTRAP_ORB_ALPHA, float)
  REGISTER_STAT(ITEMSTIME, int, autocvar_sv_itemstime)
diff --combined qcsrc/server/defs.qh
index f233bb7d8f41c5612ce300dc99862c202912b9ae,d2b4b148efffff48763f65ffe292132d05887ce6..a6bd1552d2250d7c0a2ee1895173586b4f9ab81f
@@@ -1,7 -1,7 +1,7 @@@
  #pragma once
  
  float warmup_limit;
 -#include <common/weapons/all.qh>
 +#include <common/weapons/_all.qh>
  #include <common/stats.qh>
  
  #define INDEPENDENT_ATTACK_FINISHED 1
@@@ -12,6 -12,20 +12,6 @@@ float g_footsteps, g_grappling_hook, g_
  float g_warmup_allguns;
  float g_warmup_allow_timeout;
  float warmup_stage;
 -PROPERTY(float, g_pickup_respawntime_weapon)
 -PROPERTY(float, g_pickup_respawntime_superweapon)
 -PROPERTY(float, g_pickup_respawntime_ammo)
 -PROPERTY(float, g_pickup_respawntime_short)
 -PROPERTY(float, g_pickup_respawntime_medium)
 -PROPERTY(float, g_pickup_respawntime_long)
 -PROPERTY(float, g_pickup_respawntime_powerup)
 -PROPERTY(float, g_pickup_respawntimejitter_weapon)
 -PROPERTY(float, g_pickup_respawntimejitter_superweapon)
 -PROPERTY(float, g_pickup_respawntimejitter_ammo)
 -PROPERTY(float, g_pickup_respawntimejitter_short)
 -PROPERTY(float, g_pickup_respawntimejitter_medium)
 -PROPERTY(float, g_pickup_respawntimejitter_long)
 -PROPERTY(float, g_pickup_respawntimejitter_powerup)
  float g_jetpack;
  
  float sv_clones;
@@@ -416,7 -430,6 +416,6 @@@ const int MIF_GUIDED_CONFUSABLE = MIF_G
  #define MISSILE_IS_GUIDED(m) ((m.missile_flags & MIF_GUIDED_ALL) ? true : false)
  #define MISSILE_IS_TRACKING(m) ((m.missile_flags & MIF_GUIDED_TRACKING) ? true : false)
  
  ////
  
  .entity player_stats;
diff --combined qcsrc/server/g_world.qc
index 657ac07cb683243d78b0d034b8822f73d4122eee,30cf5517f7ca5f2c3dc095695ace5d4cce3e632f..5b63aa4e0c99c3e52389d6c6bf9ae1826895aa8f
@@@ -5,7 -5,7 +5,7 @@@
  #include "bot/api.qh"
  #include "campaign.qh"
  #include "cheats.qh"
 -#include "cl_client.qh"
 +#include "client.qh"
  #include "command/common.qh"
  #include "command/getreplies.qh"
  #include "command/sv_cmd.qh"
@@@ -13,7 -13,7 +13,7 @@@
  #include "g_hook.qh"
  #include "ipban.qh"
  #include "mapvoting.qh"
 -#include "mutators/all.qh"
 +#include "mutators/_mod.qh"
  #include "race.qh"
  #include "scores.qh"
  #include "teamplay.qh"
@@@ -32,8 -32,8 +32,8 @@@
  #include "../common/triggers/trigger/secret.qh"
  #include "../common/triggers/target/music.qh"
  #include "../common/util.qh"
 -#include "../common/items/all.qh"
 -#include "../common/weapons/all.qh"
 +#include "../common/items/_mod.qh"
 +#include <common/weapons/_all.qh>
  #include "../common/state.qh"
  
  const float LATENCY_THINKRATE = 10;
@@@ -1980,6 -1980,34 +1980,34 @@@ string GotoMap(string m
                return "Map switch will happen after scoreboard.";
  }
  
+ bool autocvar_sv_gameplayfix_multiplethinksperframe;
+ void RunThink(entity this)
+ {
+       // don't let things stay in the past.
+       // it is possible to start that way by a trigger with a local time.
+       if(this.nextthink <= 0 || this.nextthink > time + frametime)
+               return;
+       float oldtime = time; // do we need to save this?
+       for (int iterations = 0; iterations < 128 && !wasfreed(this); iterations++)
+       {
+               time = max(oldtime, this.nextthink);
+               this.nextthink = 0;
+               if(getthink(this))
+                       getthink(this)(this);
+               // mods often set nextthink to time to cause a think every frame,
+               // we don't want to loop in that case, so exit if the new nextthink is
+               // <= the time the qc was told, also exit if it is past the end of the
+               // frame
+               if(this.nextthink <= time || this.nextthink > oldtime + frametime || !autocvar_sv_gameplayfix_multiplethinksperframe)
+                       break;
+       }
+       time = oldtime;
+ }
  bool autocvar_sv_freezenonclients;
  bool autocvar_sv_gameplayfix_delayprojectiles;
  void Physics_Frame()
  
                if(it.move_qcphysics)
                        Movetype_Physics_NoMatchTicrate(it, PHYS_INPUT_TIMELENGTH, false);
+               if(it.movetype >= MOVETYPE_USER_FIRST && it.movetype <= MOVETYPE_USER_LAST) // these cases have no think handling
+               {
+                       // handle thinking here
+                       if (getthink(it) && it.nextthink > 0 && it.nextthink <= time + frametime)
+                               RunThink(it);
+               }
        });
  
        if(autocvar_sv_gameplayfix_delayprojectiles >= 0)
index 530e43f4a6a0392e2bd6d10b66a929829d5f9136,fa61ab1ab6275caa5d57181765c08af47d433406..58cd949297acefe1af5e3e48d65f8f098f6c58b4
@@@ -4,12 -4,12 +4,12 @@@
  #include "constants.qh"
  #include "g_hook.qh"
  #include "ipban.qh"
 -#include "mutators/all.qh"
 +#include "mutators/_mod.qh"
  #include "../common/t_items.qh"
  #include "weapons/accuracy.qh"
  #include "weapons/csqcprojectile.qh"
  #include "weapons/selection.qh"
 -#include "../common/command/generic.qh"
 +#include "../common/command/_mod.qh"
  #include "../common/constants.qh"
  #include "../common/deathtypes/all.qh"
  #include "../common/mapinfo.qh"
  #include "../common/triggers/subs.qh"
  #include "../common/util.qh"
  #include "../common/turrets/sv_turrets.qh"
 -#include "../common/weapons/all.qh"
 +#include <common/weapons/_all.qh>
  #include "../common/vehicles/sv_vehicles.qh"
  #include "../common/vehicles/vehicle.qh"
 -#include "../common/items/all.qc"
 +#include "../common/items/_mod.qh"
  #include "../common/state.qh"
  #include "../common/effects/qc/globalsound.qh"
  #include "../lib/csqcmodel/sv_model.qh"
@@@ -97,7 -97,7 +97,7 @@@ void GameLogEcho(string s
      }
      if (autocvar_sv_eventlog_console)
      {
-         LOG_INFO(s, "\n");
+         dedicated_print(strcat(s, "\n"));
      }
  }
  
index 89021e3237a12c22dd4b7429384d99d84485ef39,1c0985723000b0a5580f54a090df46f0b2c17e85..ae0fd211c275d0797ae2eb66cfda14416af36e55
@@@ -271,6 -271,15 +271,15 @@@ MUTATOR_HOOKABLE(MonsterMove, EV_Monste
  /** called when a monster looks for another target */
  MUTATOR_HOOKABLE(MonsterFindTarget, EV_NO_ARGS);
  
+ /**
+  * called when validating a monster's target
+  */
+ #define EV_MonsterValidTarget(i, o) \
+     /** monster */       i(entity, MUTATOR_ARGV_0_entity) \
+     /** target */        i(entity, MUTATOR_ARGV_1_entity) \
+     /**/
+ MUTATOR_HOOKABLE(MonsterValidTarget, EV_MonsterValidTarget);
  /** called to change a random monster to a miniboss */
  #define EV_MonsterCheckBossFlag(i, o) \
      /** monster */ i(entity, MUTATOR_ARGV_0_entity) \
@@@ -350,7 -359,7 +359,7 @@@ MUTATOR_HOOKABLE(W_DecreaseAmmo, EV_W_D
      /**/
  MUTATOR_HOOKABLE(W_Reload, EV_W_Reload);
  
 -/** called at the end of player_powerups() in cl_client.qc, used for manipulating the values which are set by powerup items. */
 +/** called at the end of player_powerups() in client.qc, used for manipulating the values which are set by powerup items. */
  #define EV_PlayerPowerups(i, o) \
      /** player */    i(entity, MUTATOR_ARGV_0_entity) \
      /** old items */ i(int, MUTATOR_ARGV_1_int) \
@@@ -499,7 -508,7 +508,7 @@@ MUTATOR_HOOKABLE(BotShouldAttack, EV_Bo
  MUTATOR_HOOKABLE(PortalTeleport, EV_PortalTeleport);
  
  /**
 - * called whenever a player uses impulse 33 (help me) in cl_impulse.qc
 + * called whenever a player uses impulse 33 (help me) in impulse.qc
   * normally help me ping uses .waypointsprite_attachedforcarrier,
   * but if your mutator uses something different then you can handle it
   * in a special manner using this hook
@@@ -837,3 -846,19 +846,19 @@@ MUTATOR_HOOKABLE(Player_ChangeTeam, EV_
        /** data */     i(string, MUTATOR_ARGV_2_string) \
      /**/
  MUTATOR_HOOKABLE(URI_GetCallback, EV_URI_GetCallback);
+ /**
+  * return true to prevent weapon use for a player
+  */
+  #define EV_ForbidWeaponUse(i, o) \
+     /** player */ i(entity, MUTATOR_ARGV_0_entity) \
+     /**/
+ MUTATOR_HOOKABLE(ForbidWeaponUse, EV_ForbidWeaponUse);
+ /** called when a player spawns as player, after shared setup, before his weapon is chosen (so items may be changed in here) */
+ #define EV_CopyBody(i, o) \
+     /** player */               i(entity, MUTATOR_ARGV_0_entity) \
+     /** newly created clone */  i(entity, MUTATOR_ARGV_1_entity) \
+     /** keepvelocity? */        i(bool, MUTATOR_ARGV_2_bool) \
+     /**/
+ MUTATOR_HOOKABLE(CopyBody, EV_CopyBody);
index eb2379ca2291d10ec7d76de1e5eaed9c4fac3f28,9a0a1262f671d75b977df33c8d0dd6bb6104a055..e9eb2c2d9da3e4ca46189662785cc13c62f08f2e
@@@ -2,60 -2,4 +2,62 @@@
  
  #include "../gamemode.qh"
  
 +#define autocvar_g_domination_point_limit cvar("g_domination_point_limit")
 +bool autocvar_g_domination_roundbased;
 +int autocvar_g_domination_roundbased_point_limit;
 +int autocvar_g_domination_point_leadlimit;
 +
 +void dom_Initialize();
 +
 +REGISTER_MUTATOR(dom, false)
 +{
 +      MUTATOR_ONADD
 +      {
 +              if (time > 1) // game loads at time 1
 +                      error("This is a game type and it cannot be added at runtime.");
 +              dom_Initialize();
 +
 +              int fraglimit_override = autocvar_g_domination_point_limit;
 +              if (autocvar_g_domination_roundbased && autocvar_g_domination_roundbased_point_limit)
 +                      fraglimit_override = autocvar_g_domination_roundbased_point_limit;
 +
 +              ActivateTeamplay();
 +              SetLimits(fraglimit_override, autocvar_g_domination_point_leadlimit, autocvar_timelimit_override, -1);
 +              have_team_spawns = -1; // request team spawns
 +      }
 +
 +      MUTATOR_ONREMOVE
 +      {
 +              LOG_INFO("This is a game type and it cannot be removed at runtime.");
 +              return -1;
 +      }
 +
 +      return 0;
 +}
 +
 +// score rule declarations
 +const float ST_DOM_TICKS = 1;
 +const float ST_DOM_CAPS = 1;
 +
 +// pps: points per second
 +.float dom_total_pps = _STAT(DOM_TOTAL_PPS);
 +.float dom_pps_red = _STAT(DOM_PPS_RED);
 +.float dom_pps_blue = _STAT(DOM_PPS_BLUE);
 +.float dom_pps_yellow = _STAT(DOM_PPS_YELLOW);
 +.float dom_pps_pink = _STAT(DOM_PPS_PINK);
 +float total_pps;
 +float pps_red;
 +float pps_blue;
 +float pps_yellow;
 +float pps_pink;
 +
 +// capture declarations
 +.float enemy_playerid;
 +.entity sprite;
 +.float captime;
 +
 +// misc globals
 +float domination_roundbased;
 +float domination_teams;
++
+ void AnimateDomPoint(entity this);
diff --combined qcsrc/server/player.qc
index 5316a85b587ef90bf72a26cd1de13936eec09252,0000000000000000000000000000000000000000..f2ceecf9ee4596d1cc9125da2dc7513f69f13c25
mode 100644,000000..100644
--- /dev/null
@@@ -1,962 -1,0 +1,964 @@@
 +#include "player.qh"
 +
 +#include "bot/api.qh"
 +#include "cheats.qh"
 +#include "g_damage.qh"
 +#include "g_subs.qh"
 +#include "miscfunctions.qh"
 +#include "portals.qh"
 +#include "teamplay.qh"
 +#include "weapons/throwing.qh"
 +#include "command/common.qh"
 +#include "../common/state.qh"
 +#include "../common/anim.qh"
 +#include "../common/animdecide.qh"
 +#include "../common/csqcmodel_settings.qh"
 +#include "../common/deathtypes/all.qh"
 +#include "../common/triggers/subs.qh"
 +#include "../common/playerstats.qh"
 +#include "../lib/csqcmodel/sv_model.qh"
 +
 +#include "../common/minigames/sv_minigames.qh"
 +
 +#include "../common/physics/player.qh"
 +#include "../common/effects/qc/all.qh"
 +#include "../common/mutators/mutator/waypoints/waypointsprites.qh"
 +#include "../common/triggers/include.qh"
 +
 +#include "weapons/weaponstats.qh"
 +
 +#include "../common/animdecide.qh"
 +
 +void Drop_Special_Items(entity player)
 +{
 +      // called when the player has become stuck or frozen
 +      // so objective items aren't stuck with the player
 +
 +      MUTATOR_CALLHOOK(DropSpecialItems, player);
 +}
 +
 +void CopyBody_Think(entity this)
 +{
 +      if(this.CopyBody_nextthink && time > this.CopyBody_nextthink)
 +      {
 +              this.CopyBody_think(this);
 +              if(wasfreed(this))
 +                      return;
 +              this.CopyBody_nextthink = this.nextthink;
 +              this.CopyBody_think = getthink(this);
 +              setthink(this, CopyBody_Think);
 +      }
 +      CSQCMODEL_AUTOUPDATE(this);
 +      this.nextthink = time;
 +}
 +void CopyBody(entity this, float keepvelocity)
 +{
 +      if (this.effects & EF_NODRAW)
 +              return;
 +      entity clone = new(body);
 +      clone.enemy = this;
 +      clone.lip = this.lip;
 +      clone.colormap = this.colormap;
 +      clone.iscreature = this.iscreature;
 +      clone.teleportable = this.teleportable;
 +      clone.damagedbycontents = this.damagedbycontents;
 +      clone.angles = this.angles;
 +      clone.v_angle = this.v_angle;
 +      clone.avelocity = this.avelocity;
 +      clone.damageforcescale = this.damageforcescale;
 +      clone.effects = this.effects;
 +      clone.glowmod = this.glowmod;
 +      clone.event_damage = this.event_damage;
 +      clone.anim_state = this.anim_state;
 +      clone.anim_time = this.anim_time;
 +      clone.anim_lower_action = this.anim_lower_action;
 +      clone.anim_lower_time = this.anim_lower_time;
 +      clone.anim_upper_action = this.anim_upper_action;
 +      clone.anim_upper_time = this.anim_upper_time;
 +      clone.anim_implicit_state = this.anim_implicit_state;
 +      clone.anim_implicit_time = this.anim_implicit_time;
 +      clone.anim_lower_implicit_action = this.anim_lower_implicit_action;
 +      clone.anim_lower_implicit_time = this.anim_lower_implicit_time;
 +      clone.anim_upper_implicit_action = this.anim_upper_implicit_action;
 +      clone.anim_upper_implicit_time = this.anim_upper_implicit_time;
 +      clone.dphitcontentsmask = this.dphitcontentsmask;
 +      clone.death_time = this.death_time;
 +      clone.pain_finished = this.pain_finished;
 +      clone.health = this.health;
 +      clone.armorvalue = this.armorvalue;
 +      clone.armortype = this.armortype;
 +      clone.model = this.model;
 +      clone.modelindex = this.modelindex;
 +      clone.skin = this.skin;
 +      clone.species = this.species;
 +      clone.move_qcphysics = false; // don't run gamecode logic on clones, too many
 +      set_movetype(clone, this.move_movetype);
 +      clone.solid = this.solid;
 +      clone.ballistics_density = this.ballistics_density;
 +      clone.takedamage = this.takedamage;
 +      setcefc(clone, getcefc(this));
 +      clone.uncustomizeentityforclient = this.uncustomizeentityforclient;
 +      clone.uncustomizeentityforclient_set = this.uncustomizeentityforclient_set;
 +      if (keepvelocity == 1)
 +              clone.velocity = this.velocity;
 +      clone.oldvelocity = clone.velocity;
 +      clone.alpha = this.alpha;
 +      clone.fade_time = this.fade_time;
 +      clone.fade_rate = this.fade_rate;
 +      //clone.weapon = this.weapon;
 +      setorigin(clone, this.origin);
 +      setsize(clone, this.mins, this.maxs);
 +      clone.prevorigin = this.origin;
 +      clone.reset = SUB_Remove;
 +      clone._ps = this._ps;
 +
 +      Drag_MoveDrag(this, clone);
 +
 +      if(clone.colormap <= maxclients && clone.colormap > 0)
 +              clone.colormap = 1024 + this.clientcolors;
 +
 +      CSQCMODEL_AUTOINIT(clone);
 +      clone.CopyBody_nextthink = this.nextthink;
 +      clone.CopyBody_think = getthink(this);
 +      clone.nextthink = time;
 +      setthink(clone, CopyBody_Think);
 +      // "bake" the current animation frame for clones (they don't get clientside animation)
 +      animdecide_load_if_needed(clone);
 +      animdecide_setframes(clone, false, frame, frame1time, frame2, frame2time);
++
++      MUTATOR_CALLHOOK(CopyBody, this, clone, keepvelocity);
 +}
 +
 +void player_setupanimsformodel(entity this)
 +{
 +      // load animation info
 +      animdecide_load_if_needed(this);
 +      animdecide_setstate(this, 0, false);
 +}
 +
 +void player_anim(entity this)
 +{
 +      int deadbits = (this.anim_state & (ANIMSTATE_DEAD1 | ANIMSTATE_DEAD2));
 +      if(IS_DEAD(this)) {
 +              if (!deadbits) {
 +                      // Decide on which death animation to use.
 +                      if(random() < 0.5)
 +                              deadbits = ANIMSTATE_DEAD1;
 +                      else
 +                              deadbits = ANIMSTATE_DEAD2;
 +              }
 +      } else {
 +              // Clear a previous death animation.
 +              deadbits = 0;
 +      }
 +      int animbits = deadbits;
 +      if(STAT(FROZEN, this))
 +              animbits |= ANIMSTATE_FROZEN;
 +      if(this.move_movetype == MOVETYPE_FOLLOW)
 +              animbits |= ANIMSTATE_FOLLOW;
 +      if(this.crouch)
 +              animbits |= ANIMSTATE_DUCK;
 +      animdecide_setstate(this, animbits, false);
 +      animdecide_setimplicitstate(this, IS_ONGROUND(this));
 +}
 +
 +void PlayerCorpseDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
 +{
 +      float take, save;
 +      vector v;
 +      Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker);
 +
 +      // damage resistance (ignore most of the damage from a bullet or similar)
 +      damage = max(damage - 5, 1);
 +
 +      v = healtharmor_applydamage(this.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
 +      take = v.x;
 +      save = v.y;
 +
 +      if(sound_allowed(MSG_BROADCAST, attacker))
 +      {
 +              if (save > 10)
 +                      sound (this, CH_SHOTS, SND_ARMORIMPACT, VOL_BASE, ATTEN_NORM);
 +              else if (take > 30)
 +                      sound (this, CH_SHOTS, SND_BODYIMPACT2, VOL_BASE, ATTEN_NORM);
 +              else if (take > 10)
 +                      sound (this, CH_SHOTS, SND_BODYIMPACT1, VOL_BASE, ATTEN_NORM);
 +      }
 +
 +      if (take > 50)
 +              Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, this, attacker);
 +      if (take > 100)
 +              Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker);
 +
 +      this.armorvalue = this.armorvalue - save;
 +      this.health = this.health - take;
 +      // pause regeneration for 5 seconds
 +      this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen);
 +
 +      this.dmg_save = this.dmg_save + save;//max(save - 10, 0);
 +      this.dmg_take = this.dmg_take + take;//max(take - 10, 0);
 +      this.dmg_inflictor = inflictor;
 +
 +      if (this.health <= -autocvar_sv_gibhealth && this.alpha >= 0)
 +      {
 +              // don't use any animations as a gib
 +              this.frame = 0;
 +              // view just above the floor
 +              this.view_ofs = '0 0 4';
 +
 +              Violence_GibSplash(this, 1, 1, attacker);
 +              this.alpha = -1;
 +              this.solid = SOLID_NOT; // restore later
 +              this.takedamage = DAMAGE_NO; // restore later
 +              this.damagedbycontents = false;
 +      }
 +}
 +
 +void calculate_player_respawn_time(entity this)
 +{
 +      if(g_ca)
 +              return;
 +
 +      float gametype_setting_tmp;
 +      float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
 +      float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
 +      float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
 +      float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
 +      float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
 +      float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
 +
 +      float pcount = 1;  // Include myself whether or not team is already set right and I'm a "player".
 +      if (teamplay)
 +      {
 +              FOREACH_CLIENT(IS_PLAYER(it) && it != this, LAMBDA(
 +                      if(it.team == this.team)
 +                              ++pcount;
 +              ));
 +              if (sdelay_small_count == 0)
 +                      sdelay_small_count = 1;
 +              if (sdelay_large_count == 0)
 +                      sdelay_large_count = 1;
 +      }
 +      else
 +      {
 +              FOREACH_CLIENT(IS_PLAYER(it) && it != this, LAMBDA(
 +                      ++pcount;
 +              ));
 +              if (sdelay_small_count == 0)
 +              {
 +                      if (g_cts)
 +                      {
 +                              // Players play independently. No point in requiring enemies.
 +                              sdelay_small_count = 1;
 +                      }
 +                      else
 +                      {
 +                              // Players play AGAINST each other. Enemies required.
 +                              sdelay_small_count = 2;
 +                      }
 +              }
 +              if (sdelay_large_count == 0)
 +              {
 +                      if (g_cts)
 +                      {
 +                              // Players play independently. No point in requiring enemies.
 +                              sdelay_large_count = 1;
 +                      }
 +                      else
 +                      {
 +                              // Players play AGAINST each other. Enemies required.
 +                              sdelay_large_count = 2;
 +                      }
 +              }
 +      }
 +
 +      float sdelay;
 +
 +      if (pcount <= sdelay_small_count)
 +              sdelay = sdelay_small;
 +      else if (pcount >= sdelay_large_count)
 +              sdelay = sdelay_large;
 +      else  // NOTE: this case implies sdelay_large_count > sdelay_small_count.
 +              sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
 +
 +      if(waves)
 +              this.respawn_time = ceil((time + sdelay) / waves) * waves;
 +      else
 +              this.respawn_time = time + sdelay;
 +
 +      if(sdelay < sdelay_max)
 +              this.respawn_time_max = time + sdelay_max;
 +      else
 +              this.respawn_time_max = this.respawn_time;
 +
 +      if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
 +              this.respawn_countdown = 10; // first number to count down from is 10
 +      else
 +              this.respawn_countdown = -1; // do not count down
 +
 +      if(autocvar_g_forced_respawn)
 +              this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
 +}
 +
 +void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
 +{
 +      float take, save, dh, da;
 +      vector v;
 +      float valid_damage_for_weaponstats;
 +      float excess;
 +
 +      dh = max(this.health, 0);
 +      da = max(this.armorvalue, 0);
 +
 +      if(!DEATH_ISSPECIAL(deathtype))
 +      {
 +              damage *= sqrt(bound(1.0, this.cvar_cl_handicap, 100.0));
 +              if(this != attacker)
 +                      damage /= sqrt(bound(1.0, attacker.cvar_cl_handicap, 100.0));
 +      }
 +
 +      if(DEATH_ISWEAPON(deathtype, WEP_TUBA))
 +      {
 +              // tuba causes blood to come out of the ears
 +              vector ear1, ear2;
 +              vector d;
 +              float f;
 +              ear1 = this.origin;
 +              ear1_z += 0.125 * this.view_ofs.z + 0.875 * this.maxs.z; // 7/8
 +              ear2 = ear1;
 +              makevectors(this.angles);
 +              ear1 += v_right * -10;
 +              ear2 += v_right * +10;
 +              d = inflictor.origin - this.origin;
 +              if (d)
 +                      f = (d * v_right) / vlen(d); // this is cos of angle of d and v_right!
 +              else
 +                      f = 0;  // Assum ecenter.
 +              force = v_right * vlen(force);
 +              Violence_GibSplash_At(ear1, force * -1, 2, bound(0, damage, 25) / 2 * (0.5 - 0.5 * f), this, attacker);
 +              Violence_GibSplash_At(ear2, force,      2, bound(0, damage, 25) / 2 * (0.5 + 0.5 * f), this, attacker);
 +              if(f > 0)
 +              {
 +                      hitloc = ear1;
 +                      force = force * -1;
 +              }
 +              else
 +              {
 +                      hitloc = ear2;
 +                      // force is already good
 +              }
 +      }
 +      else
 +              Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker);
 +
 +
 +      v = healtharmor_applydamage(this.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
 +      take = v.x;
 +      save = v.y;
 +
 +      if(attacker == this)
 +      {
 +              // don't reset pushltime for this damage as it may be an attempt to
 +              // escape a lava pit or similar
 +              //this.pushltime = 0;
 +              this.istypefrag = 0;
 +      }
 +      else if(IS_PLAYER(attacker))
 +      {
 +              this.pusher = attacker;
 +              this.pushltime = time + autocvar_g_maxpushtime;
 +              this.istypefrag = PHYS_INPUT_BUTTON_CHAT(this);
 +      }
 +      else if(time < this.pushltime)
 +      {
 +              attacker = this.pusher;
 +              this.pushltime = max(this.pushltime, time + 0.6);
 +      }
 +      else
 +      {
 +              this.pushltime = 0;
 +              this.istypefrag = 0;
 +      }
 +
 +      if (time < this.spawnshieldtime && autocvar_g_spawnshield_blockdamage < 1)
 +      {
 +              vector v = healtharmor_applydamage(this.armorvalue, max(0, autocvar_g_spawnshield_blockdamage), deathtype, damage);
 +              take = v.x;
 +              save = v.y;
 +      }
 +
 +      MUTATOR_CALLHOOK(PlayerDamage_SplitHealthArmor, inflictor, attacker, this, force, take, save, deathtype, damage);
 +      take = bound(0, M_ARGV(4, float), this.health);
 +      save = bound(0, M_ARGV(5, float), this.armorvalue);
 +      excess = max(0, damage - take - save);
 +
 +      if(sound_allowed(MSG_BROADCAST, attacker))
 +      {
 +              if (save > 10)
 +                      sound (this, CH_SHOTS, SND_ARMORIMPACT, VOL_BASE, ATTEN_NORM);
 +              else if (take > 30)
 +                      sound (this, CH_SHOTS, SND_BODYIMPACT2, VOL_BASE, ATTEN_NORM);
 +              else if (take > 10)
 +                      sound (this, CH_SHOTS, SND_BODYIMPACT1, VOL_BASE, ATTEN_NORM); // FIXME possibly remove them?
 +      }
 +
 +      if (take > 50)
 +              Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, this, attacker);
 +      if (take > 100)
 +              Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker);
 +
 +      if (time >= this.spawnshieldtime || autocvar_g_spawnshield_blockdamage < 1)
 +      {
 +              if (!(this.flags & FL_GODMODE))
 +              {
 +                      this.armorvalue = this.armorvalue - save;
 +                      this.health = this.health - take;
 +                      // pause regeneration for 5 seconds
 +                      if(take)
 +                              this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen);
 +
 +                      if (time > this.pain_finished)          //Don't switch pain sequences like crazy
 +                      {
 +                              this.pain_finished = time + 0.5;        //Supajoe
 +
 +                              if(autocvar_sv_gentle < 1) {
 +                                      if(this.classname != "body") // pain anim is BORKED on our ZYMs, FIXME remove this once we have good models
 +                                      {
 +                                              if (!this.animstate_override)
 +                                              {
 +                                                      if (random() > 0.5)
 +                                                              animdecide_setaction(this, ANIMACTION_PAIN1, true);
 +                                                      else
 +                                                              animdecide_setaction(this, ANIMACTION_PAIN2, true);
 +                                              }
 +                                      }
 +
 +                                      if(sound_allowed(MSG_BROADCAST, attacker))
 +                                      if((this.health < 2 * WEP_CVAR_PRI(blaster, damage) * autocvar_g_balance_selfdamagepercent + 1) || !(DEATH_WEAPONOF(deathtype).spawnflags & WEP_FLAG_CANCLIMB) || attacker != this) // WEAPONTODO: create separate limit for pain notification with laser
 +                                      if(this.health > 1)
 +                                      // exclude pain sounds for laserjumps as long as you aren't REALLY low on health and would die of the next two
 +                                      {
 +                                              if(deathtype == DEATH_FALL.m_id)
 +                                                      PlayerSound(this, playersound_fall, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +                                              else if(this.health > 75)
 +                                                      PlayerSound(this, playersound_pain100, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +                                              else if(this.health > 50)
 +                                                      PlayerSound(this, playersound_pain75, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +                                              else if(this.health > 25)
 +                                                      PlayerSound(this, playersound_pain50, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +                                              else
 +                                                      PlayerSound(this, playersound_pain25, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +                                      }
 +                              }
 +                      }
 +
 +                      // throw off bot aim temporarily
 +                      float shake;
 +                      if(IS_BOT_CLIENT(this) && this.health >= 1)
 +                      {
 +                              shake = damage * 5 / (bound(0,skill,100) + 1);
 +                              this.v_angle_x = this.v_angle.x + (random() * 2 - 1) * shake;
 +                              this.v_angle_y = this.v_angle.y + (random() * 2 - 1) * shake;
 +                              this.v_angle_x = bound(-90, this.v_angle.x, 90);
 +                      }
 +              }
 +              else
 +                      this.max_armorvalue += (save + take);
 +      }
 +      this.dmg_save = this.dmg_save + save;//max(save - 10, 0);
 +      this.dmg_take = this.dmg_take + take;//max(take - 10, 0);
 +      this.dmg_inflictor = inflictor;
 +
 +      if (this != attacker) {
 +              float realdmg = damage - excess;
 +              if (IS_PLAYER(attacker)) {
 +                      PlayerScore_Add(attacker, SP_DMG, realdmg);
 +              }
 +              if (IS_PLAYER(this)) {
 +                      PlayerScore_Add(this, SP_DMGTAKEN, realdmg);
 +              }
 +      }
 +
 +      bool abot = (IS_BOT_CLIENT(attacker));
 +      bool vbot = (IS_BOT_CLIENT(this));
 +
 +      valid_damage_for_weaponstats = 0;
 +      Weapon awep = WEP_Null;
 +
 +      if(vbot || IS_REAL_CLIENT(this))
 +      if(abot || IS_REAL_CLIENT(attacker))
 +      if(attacker && this != attacker)
 +      if(DIFF_TEAM(this, attacker))
 +      {
 +              if(DEATH_ISSPECIAL(deathtype))
 +                      awep = PS(attacker).m_weapon;
 +              else
 +                      awep = DEATH_WEAPONOF(deathtype);
 +              valid_damage_for_weaponstats = 1;
 +      }
 +
 +      dh = dh - max(this.health, 0);
 +      da = da - max(this.armorvalue, 0);
 +      if(valid_damage_for_weaponstats)
 +      {
 +              WeaponStats_LogDamage(awep.m_id, abot, PS(this).m_weapon.m_id, vbot, dh + da);
 +      }
 +      if (dh + da)
 +      {
 +              MUTATOR_CALLHOOK(PlayerDamaged, attacker, this, dh, da, hitloc, deathtype);
 +      }
 +
 +      if (this.health < 1)
 +      {
 +              float defer_ClientKill_Now_TeamChange;
 +              defer_ClientKill_Now_TeamChange = false;
 +
 +              if(this.alivetime)
 +              {
 +                      PS_GR_P_ADDVAL(this, PLAYERSTATS_ALIVETIME, time - this.alivetime);
 +                      this.alivetime = 0;
 +              }
 +
 +              if(valid_damage_for_weaponstats)
 +                      WeaponStats_LogKill(awep.m_id, abot, PS(this).m_weapon.m_id, vbot);
 +
 +              if(autocvar_sv_gentle < 1)
 +              if(sound_allowed(MSG_BROADCAST, attacker))
 +              {
 +                      if(deathtype == DEATH_DROWN.m_id)
 +                              PlayerSound(this, playersound_drown, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +                      else
 +                              PlayerSound(this, playersound_death, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
 +              }
 +
 +              // get rid of kill indicator
 +              if(this.killindicator)
 +              {
 +                      delete(this.killindicator);
 +                      this.killindicator = NULL;
 +                      if(this.killindicator_teamchange)
 +                              defer_ClientKill_Now_TeamChange = true;
 +
 +                      if(this.classname == "body")
 +                      if(deathtype == DEATH_KILL.m_id)
 +                      {
 +                              // for the lemmings fans, a small harmless explosion
 +                              Send_Effect(EFFECT_ROCKET_EXPLODE, this.origin, '0 0 0', 1);
 +                      }
 +              }
 +
 +              // print an obituary message
 +              if(this.classname != "body")
 +                      Obituary (attacker, inflictor, this, deathtype);
 +
 +        // increment frag counter for used weapon type
 +        Weapon w = DEATH_WEAPONOF(deathtype);
 +        if(w != WEP_Null)
 +      if(accuracy_isgooddamage(attacker, this))
 +        attacker.accuracy.(accuracy_frags[w.m_id-1]) += 1;
 +
 +              MUTATOR_CALLHOOK(PlayerDies, inflictor, attacker, this, deathtype, damage);
 +              excess = M_ARGV(4, float);
 +
 +              Weapon wep = PS(this).m_weapon;
 +              /*for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +              {
 +                      .entity weaponentity = weaponentities[slot];
 +                      wep.wr_playerdeath(wep, this, weaponentity);
 +              }*/
 +              .entity weaponentity = weaponentities[0]; // TODO: unhardcode
 +              wep.wr_playerdeath(wep, this, weaponentity);
 +
 +              RemoveGrapplingHook(this);
 +
 +              Portal_ClearAllLater(this);
 +
 +              this.fixangle = true;
 +
 +              if(defer_ClientKill_Now_TeamChange)
 +                      ClientKill_Now_TeamChange(this); // can turn player into spectator
 +
 +              // player could have been miraculously resuscitated ;)
 +              // e.g. players in freezetag get frozen, they don't really die
 +              if(this.health >= 1 || !(IS_PLAYER(this) || this.classname == "body"))
 +                      return;
 +
 +              // when we get here, player actually dies
 +
 +              Unfreeze(this); // remove any icy remains
 +              this.health = 0; // Unfreeze resets health, so we need to set it back
 +
 +              // clear waypoints
 +              WaypointSprite_PlayerDead(this);
 +              // throw a weapon
 +              SpawnThrownWeapon(this, this.origin + (this.mins + this.maxs) * 0.5, PS(this).m_switchweapon.m_id);
 +
 +              // become fully visible
 +              this.alpha = default_player_alpha;
 +              // make the corpse upright (not tilted)
 +              this.angles_x = 0;
 +              this.angles_z = 0;
 +              // don't spin
 +              this.avelocity = '0 0 0';
 +              // view from the floor
 +              this.view_ofs = '0 0 -8';
 +              // toss the corpse
 +              set_movetype(this, MOVETYPE_TOSS);
 +              // shootable corpse
 +              this.solid = SOLID_CORPSE;
 +              this.ballistics_density = autocvar_g_ballistics_density_corpse;
 +              // don't stick to the floor
 +              UNSET_ONGROUND(this);
 +              // dying animation
 +              this.deadflag = DEAD_DYING;
 +
 +              // when to allow respawn
 +              calculate_player_respawn_time(this);
 +
 +              this.death_time = time;
 +              if (random() < 0.5)
 +                      animdecide_setstate(this, this.anim_state | ANIMSTATE_DEAD1, true);
 +              else
 +                      animdecide_setstate(this, this.anim_state | ANIMSTATE_DEAD2, true);
 +              if (this.maxs.z > 5)
 +              {
 +                      this.maxs_z = 5;
 +                      setsize(this, this.mins, this.maxs);
 +              }
 +              // set damage function to corpse damage
 +              this.event_damage = PlayerCorpseDamage;
 +              // call the corpse damage function just in case it wants to gib
 +              this.event_damage(this, inflictor, attacker, excess, deathtype, hitloc, force);
 +
 +              // set up to fade out later
 +              SUB_SetFade (this, time + 6 + random (), 1);
 +              // reset body think wrapper broken by SUB_SetFade
 +              if(this.classname == "body" && getthink(this) != CopyBody_Think) {
 +                      this.CopyBody_think = getthink(this);
 +                      this.CopyBody_nextthink = this.nextthink;
 +                      setthink(this, CopyBody_Think);
 +                      this.nextthink = time;
 +              }
 +
 +              if(autocvar_sv_gentle > 0 || autocvar_ekg || this.classname == "body") {
 +                      // remove corpse
 +                      // clones don't run any animation code any more, so we must gib them when they die :(
 +                      PlayerCorpseDamage(this, inflictor, attacker, autocvar_sv_gibhealth+1.0, deathtype, hitloc, force);
 +              }
 +
 +              // reset fields the weapons may use just in case
 +              FOREACH(Weapons, it != WEP_Null, LAMBDA(
 +                      it.wr_resetplayer(it, this);
 +                      for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +                      {
 +                              ATTACK_FINISHED_FOR(this, it.m_id, slot) = 0;
 +                      }
 +              ));
 +      }
 +}
 +
 +void MoveToTeam(entity client, int team_colour, int type)
 +{
 +      int lockteams_backup = lockteams;  // backup any team lock
 +      lockteams = 0;  // disable locked teams
 +      TeamchangeFrags(client);  // move the players frags
 +      SetPlayerColors(client, team_colour - 1);  // set the players colour
 +      Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, client.origin, '0 0 0');  // kill the player
 +      lockteams = lockteams_backup;  // restore the team lock
 +      LogTeamchange(client.playerid, client.team, type);
 +}
 +
 +/** print(), but only print if the server is not local */
 +void dedicated_print(string input)
 +{
 +      if (server_is_dedicated) print(input);
 +}
 +
 +/**
 + * message "": do not say, just test flood control
 + * return value:
 + *   1 = accept
 + *   0 = reject
 + *  -1 = fake accept
 + */
 +int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
 +{
 +      if (!teamsay && !privatesay) if (substring(msgin, 0, 1) == " ")
 +        msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
 +
 +      msgin = formatmessage(source, msgin);
 +
 +    string colorstr;
 +      if (!IS_PLAYER(source))
 +              colorstr = "^0"; // black for spectators
 +      else if(teamplay)
 +              colorstr = Team_ColorCode(source.team);
 +      else
 +      {
 +              colorstr = "";
 +              teamsay = false;
 +      }
 +
 +      if(intermission_running)
 +              teamsay = false;
 +
 +    if (!source) {
 +              colorstr = "";
 +              teamsay = false;
 +    }
 +
 +      if(msgin != "")
 +              msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
 +
 +      /*
 +       * using bprint solves this... me stupid
 +      // how can we prevent the message from appearing in a listen server?
 +      // for now, just give "say" back and only handle say_team
 +      if(!teamsay)
 +      {
 +              clientcommand(source, strcat("say ", msgin));
 +              return;
 +      }
 +      */
 +
 +    string namestr = "";
 +    if (source)
 +        namestr = autocvar_g_chat_teamcolors ? playername(source) : source.netname;
 +
 +    string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
 +
 +    string msgstr, cmsgstr;
 +    string privatemsgprefix = string_null;
 +    int privatemsgprefixlen = 0;
 +      if (msgin == "") {
 +        msgstr = cmsgstr = "";
 +      } else {
 +              if(privatesay)
 +              {
 +                      msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7");
 +                      privatemsgprefixlen = strlen(msgstr);
 +                      msgstr = strcat(msgstr, msgin);
 +                      cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin);
 +                      if(autocvar_g_chat_teamcolors)
 +                              privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay), ": ^7");
 +                      else
 +                              privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", privatesay.netname, ": ^7");
 +              }
 +              else if(teamsay)
 +              {
 +                      if(strstrofs(msgin, "/me", 0) >= 0)
 +                      {
 +                              //msgin = strreplace("/me", "", msgin);
 +                              //msgin = substring(msgin, 3, strlen(msgin));
 +                              msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin);
 +                              msgstr = strcat("\{1}\{13}^4* ", "^7", msgin);
 +                      }
 +                      else
 +                              msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
 +                      cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
 +              }
 +              else
 +              {
 +                      if(strstrofs(msgin, "/me", 0) >= 0)
 +                      {
 +                              //msgin = strreplace("/me", "", msgin);
 +                              //msgin = substring(msgin, 3, strlen(msgin));
 +                              msgin = strreplace("/me", strcat(colorprefix, namestr), msgin);
 +                              msgstr = strcat("\{1}^4* ", "^7", msgin);
 +                      }
 +                      else {
 +                msgstr = "\{1}";
 +                msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
 +                msgstr = strcat(msgstr, msgin);
 +            }
 +                      cmsgstr = "";
 +              }
 +              msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
 +      }
 +
 +      string fullmsgstr = msgstr;
 +      string fullcmsgstr = cmsgstr;
 +
 +      // FLOOD CONTROL
 +      int flood = 0;
 +      var .float flood_field = floodcontrol_chat;
 +      if(floodcontrol)
 +      {
 +              float flood_spl;
 +              float flood_burst;
 +              float flood_lmax;
 +              float lines;
 +              if(privatesay)
 +              {
 +                      flood_spl = autocvar_g_chat_flood_spl_tell;
 +                      flood_burst = autocvar_g_chat_flood_burst_tell;
 +                      flood_lmax = autocvar_g_chat_flood_lmax_tell;
 +                      flood_field = floodcontrol_chattell;
 +              }
 +              else if(teamsay)
 +              {
 +                      flood_spl = autocvar_g_chat_flood_spl_team;
 +                      flood_burst = autocvar_g_chat_flood_burst_team;
 +                      flood_lmax = autocvar_g_chat_flood_lmax_team;
 +                      flood_field = floodcontrol_chatteam;
 +              }
 +              else
 +              {
 +                      flood_spl = autocvar_g_chat_flood_spl;
 +                      flood_burst = autocvar_g_chat_flood_burst;
 +                      flood_lmax = autocvar_g_chat_flood_lmax;
 +                      flood_field = floodcontrol_chat;
 +              }
 +              flood_burst = max(0, flood_burst - 1);
 +              // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
 +
 +              // do flood control for the default line size
 +              if(msgstr != "")
 +              {
 +                      getWrappedLine_remaining = msgstr;
 +                      msgstr = "";
 +                      lines = 0;
 +                      while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
 +                      {
 +                              msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
 +                              ++lines;
 +                      }
 +                      msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
 +
 +                      if(getWrappedLine_remaining != "")
 +                      {
 +                              msgstr = strcat(msgstr, "\n");
 +                              flood = 2;
 +                      }
 +
 +                      if (time >= source.(flood_field))
 +                      {
 +                              source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
 +                      }
 +                      else
 +                      {
 +                              flood = 1;
 +                              msgstr = fullmsgstr;
 +                      }
 +              }
 +              else
 +              {
 +                      if (time >= source.(flood_field))
 +                              source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
 +                      else
 +                              flood = 1;
 +              }
 +
 +              if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
 +                      source.(flood_field) = flood = 0;
 +      }
 +
 +    string sourcemsgstr, sourcecmsgstr;
 +      if(flood == 2) // cannot happen for empty msgstr
 +      {
 +              if(autocvar_g_chat_flood_notify_flooder)
 +              {
 +                      sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
 +                      sourcecmsgstr = "";
 +              }
 +              else
 +              {
 +                      sourcemsgstr = fullmsgstr;
 +                      sourcecmsgstr = fullcmsgstr;
 +              }
 +              cmsgstr = "";
 +      }
 +      else
 +      {
 +              sourcemsgstr = msgstr;
 +              sourcecmsgstr = cmsgstr;
 +      }
 +
 +      if (!privatesay && source && !IS_PLAYER(source))
 +      {
 +              if (!intermission_running)
 +                      if(teamsay || (autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !(warmup_stage || gameover)))
 +                              teamsay = -1; // spectators
 +      }
 +
 +      if(flood)
 +              LOG_INFO("NOTE: ", playername(source), "^7 is flooding.\n");
 +
 +      // build sourcemsgstr by cutting off a prefix and replacing it by the other one
 +      if(privatesay)
 +              sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
 +
 +    int ret;
 +      if(source.muted)
 +      {
 +              // always fake the message
 +              ret = -1;
 +      }
 +      else if(flood == 1)
 +      {
 +              if (autocvar_g_chat_flood_notify_flooder)
 +              {
 +                      sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
 +                      ret = 0;
 +              }
 +              else
 +                      ret = -1;
 +      }
 +      else
 +      {
 +              ret = 1;
 +      }
 +
 +      if(sourcemsgstr != "" && ret != 0)
 +      {
 +              if(ret < 0) // faked message, because the player is muted
 +              {
 +                      sprint(source, sourcemsgstr);
 +                      if(sourcecmsgstr != "" && !privatesay)
 +                              centerprint(source, sourcecmsgstr);
 +              }
 +              else if(privatesay) // private message, between 2 people only
 +              {
 +                      sprint(source, sourcemsgstr);
 +                      sprint(privatesay, msgstr);
 +                      if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
 +                      if(cmsgstr != "")
 +                              centerprint(privatesay, cmsgstr);
 +              }
 +              else if ( teamsay && source.active_minigame )
 +              {
 +                      sprint(source, sourcemsgstr);
 +                      dedicated_print(msgstr); // send to server console too
 +                      FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && it.active_minigame == source.active_minigame, sprint(it, msgstr));
 +              }
 +              else if(teamsay > 0) // team message, only sent to team mates
 +              {
 +                      sprint(source, sourcemsgstr);
 +                      dedicated_print(msgstr); // send to server console too
 +                      if(sourcecmsgstr != "")
 +                              centerprint(source, sourcecmsgstr);
 +                      FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && it != source && it.team == source.team, {
 +                              sprint(it, msgstr);
 +                              if(cmsgstr != "")
 +                                      centerprint(it, cmsgstr);
 +                      });
 +              }
 +              else if(teamsay < 0) // spectator message, only sent to spectators
 +              {
 +                      sprint(source, sourcemsgstr);
 +                      dedicated_print(msgstr); // send to server console too
 +                      FOREACH_CLIENT(!IS_PLAYER(it) && IS_REAL_CLIENT(it) && it != source, sprint(it, msgstr));
 +              }
 +              else
 +              {
 +            if (source) {
 +                sprint(source, sourcemsgstr);
 +                dedicated_print(msgstr); // send to server console too
 +                MX_Say(strcat(playername(source), "^7: ", msgin));
 +            }
 +            FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source, sprint(it, msgstr));
 +        }
 +      }
 +
 +      return ret;
 +}
diff --combined qcsrc/server/player.qh
index b5f8ca07c8d4a5dc81db14099d86f597405b2827,0000000000000000000000000000000000000000..3e2b860162203e798733f441d3e35dcd07405bd7
mode 100644,000000..100644
--- /dev/null
@@@ -1,39 -1,0 +1,41 @@@
 +#pragma once
 +
 +.entity pusher;
 +.float pushltime;
 +.float istypefrag;
 +
 +.float CopyBody_nextthink;
 +.void(entity this) CopyBody_think;
 +void CopyBody_Think(entity this);
 +void CopyBody(entity this, float keepvelocity);
 +
++void dedicated_print(string input);
++
 +void player_setupanimsformodel(entity this);
 +
 +void player_anim(entity this);
 +
 +void PlayerCorpseDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force);
 +
 +// g_<gametype>_str:
 +// If 0, default is used.
 +// If <0, 0 is used.
 +// Otherwise, g_str (default value) is used.
 +// For consistency, negative values there are mapped to zero too.
 +#define GAMETYPE_DEFAULTED_SETTING(str) \
 +      ((gametype_setting_tmp = cvar(strcat("g_", GetGametype(), "_" #str))), \
 +      (gametype_setting_tmp < 0) ? 0 \
 +      : (gametype_setting_tmp == 0 || autocvar_g_respawn_delay_forced) ? max(0, autocvar_g_##str) \
 +      : gametype_setting_tmp)
 +
 +void calculate_player_respawn_time(entity this);
 +
 +void ClientKill_Now_TeamChange(entity this);
 +
 +void MoveToTeam(entity client, float team_colour, float type);
 +
 +void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force);
 +
 +/** to be used by `prvm_edictset server playernumber muted 1` */
 +.float muted;
 +int Say(entity source, float teamsay, entity privatesay, string msgin, float floodcontrol);
diff --combined qcsrc/server/teamplay.qc
index a193423783563ead129785941da806efdfedff3c,61a9d2fadd52f4292bff1cf0719378939f726b39..f429628b01a05d1d82541292fbeeb14aa2989cf5
@@@ -1,6 -1,6 +1,6 @@@
  #include "teamplay.qh"
  
 -#include "cl_client.qh"
 +#include "client.qh"
  #include "race.qh"
  #include "scores.qh"
  #include "scores_rules.qh"
@@@ -9,10 -9,10 +9,10 @@@
  
  #include "command/vote.qh"
  
 -#include "mutators/all.qh"
 +#include "mutators/_mod.qh"
  
  #include "../common/deathtypes/all.qh"
 -#include "../common/gamemodes/all.qh"
 +#include "../common/gamemodes/_mod.qh"
  #include "../common/teams.qh"
  
  void TeamchangeFrags(entity e)
@@@ -473,8 -473,10 +473,10 @@@ float FindSmallestTeam(entity pl, floa
        {
                if(autocvar_g_campaign && pl && IS_REAL_CLIENT(pl))
                        return 1; // special case for campaign and player joining
-               else
-                       error(sprintf("Too few teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
+               else if(totalteams == 1) // single team
+                       LOG_TRACEF("Only 1 team available for %s, you may need to fix your map", MapInfo_Type_ToString(MapInfo_CurrentGametype()));
+               else // no teams, major no no
+                       error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
        }
  
        // count how many players are in each team
@@@ -540,7 -542,7 +542,7 @@@ int JoinBestTeam(entity this, bool only
                                SetPlayerColors(this, selectedteam - 1);
  
                                // when JoinBestTeam is called by client.qc/ClientKill_Now_TeamChange the players team is -1 and thus skipped
 -                              // when JoinBestTeam is called by cl_client.qc/ClientConnect the player_id is 0 the log attempt is rejected
 +                              // when JoinBestTeam is called by client.qc/ClientConnect the player_id is 0 the log attempt is rejected
                                LogTeamchange(this.playerid, this.team, 99);
                        }
                        return selectedteam;
index 68714da33867d5f8eb8ba433b997ed5058118b0d,f773180fea091f1621a03b1886aba93a3ed9ee69..176b69e6b6c9e5f720f2dfc09fb9af13367c1ff8
@@@ -12,7 -12,7 +12,7 @@@
  #include <common/constants.qh>
  #include <common/util.qh>
  
 -#include <common/weapons/all.qh>
 +#include <common/weapons/_all.qh>
  #include <common/state.qh>
  
  #include <lib/warpzone/common.qh>
@@@ -22,7 -22,7 +22,7 @@@
  // make sure you call makevectors first (FIXME?)
  void W_SetupShot_Dir_ProjectileSize_Range(entity ent, .entity weaponentity, vector s_forward, vector mi, vector ma, float antilag, float recoil, Sound snd, float chan, float maxdamage, float range)
  {
-     TC(Sound, snd);
+       TC(Sound, snd);
        float nudge = 1; // added to traceline target and subtracted from result  TOOD(divVerent): do we still need this? Doesn't the engine do this now for us?
        float oldsolid;
        vector vecs, dv;
@@@ -475,7 -475,10 +475,10 @@@ void fireBullet(entity this, vector sta
                        break;
  
                float dist_taken = max(autocvar_g_ballistics_mindistance, vlen(trace_endpos - start));
-               solid_penetration_left *= (dist_taken / maxdist);
+               // fraction_used_of_what_is_left = dist_taken / maxdist
+               // solid_penetration_left = solid_penetration_left - solid_penetration_left * fraction_used_of_what_is_left
+               solid_penetration_left *= 1 - dist_taken / maxdist;
+               solid_penetration_left = max(solid_penetration_left, 0);
  
                // Only show effect when going through a player (invisible otherwise)
                if (hit && (hit.solid != SOLID_BSP))
index 1e42121529f8940c4102dfb31f7e8b8b37b3f63e,2efea3ff5ee3da4c36bde9914ebe0b98615304e6..13f32ee14d2a76e9cae1ec69dea3d01b3fa4c1e9
@@@ -3,7 -3,7 +3,7 @@@
  #include "selection.qh"
  
  #include "../command/common.qh"
 -#include "../mutators/all.qh"
 +#include "../mutators/_mod.qh"
  #include "../round_handler.qh"
  #include <common/t_items.qh>
  #include <common/animdecide.qh>
@@@ -11,7 -11,7 +11,7 @@@
  #include <common/monsters/all.qh>
  #include <common/notifications/all.qh>
  #include <common/util.qh>
 -#include <common/weapons/all.qh>
 +#include <common/weapons/_all.qh>
  #include <common/state.qh>
  #include <lib/csqcmodel/sv_model.qh>
  
@@@ -412,6 -412,7 +412,7 @@@ bool forbidWeaponUse(entity player
        if (gameover) return true;
        if (STAT(FROZEN, player)) return true;
        if (player.weapon_blocked) return true;
+       if (MUTATOR_CALLHOOK(ForbidWeaponUse, player)) return true;
        return false;
  }