]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'Mario/showspecs' into 'master'
authorTimePath <andrew.hardaker1995@gmail.com>
Sat, 6 Aug 2016 06:46:36 +0000 (06:46 +0000)
committerTimePath <andrew.hardaker1995@gmail.com>
Sat, 6 Aug 2016 06:46:36 +0000 (06:46 +0000)
Merge branch Mario/showspecs (M merge request)

Shows who's spectating you (disabled by default).

See merge request !340

1  2 
defaultXonotic.cfg
qcsrc/client/hud/panel/infomessages.qc
qcsrc/client/main.qc
qcsrc/client/main.qh
qcsrc/server/autocvars.qh
qcsrc/server/cl_client.qc

diff --combined defaultXonotic.cfg
index 11ba8e4ff3428a486bd95b67d71d032b77c63f4f,eca60f67d98a3076a6f66c922de78f85e3582ba2..afa60ccf6a19488d963abc4165a04d9cb91f122c
@@@ -360,9 -360,9 +360,9 @@@ set bot_ai_keyboard_threshold 0.5
  set bot_ai_aimskill_offset 0.3 "Amount of error induced to the bots aim"
  set bot_ai_aimskill_think 1 "Aiming velocity. Use values below 1 for slower aiming"
  set bot_ai_custom_weapon_priority_distances "300 850" "Define close and far distances in any order. Based on the distance to the enemy bots will choose different weapons"
 -set bot_ai_custom_weapon_priority_far   "vaporizer vortex rifle electro devastator mortar hagar hlac crylink blaster machinegun fireball seeker shotgun tuba minelayer"       "Desired weapons for far distances ordered by priority"
 -set bot_ai_custom_weapon_priority_mid   "vaporizer devastator vortex fireball seeker mortar electro machinegun crylink hlac hagar shotgun blaster rifle tuba minelayer arc shockwave" "Desired weapons for middle distances ordered by priority"
 -set bot_ai_custom_weapon_priority_close "vaporizer shotgun vortex machinegun hlac tuba seeker hagar crylink mortar electro devastator blaster fireball rifle minelayer arc shockwave" "Desired weapons for close distances ordered by priority"
 +set bot_ai_custom_weapon_priority_far   "vaporizer vortex rifle electro devastator mortar hagar hlac crylink blaster machinegun fireball seeker shotgun shockwave tuba minelayer"     "Desired weapons for far distances ordered by priority"
 +set bot_ai_custom_weapon_priority_mid   "vaporizer devastator vortex fireball seeker mortar electro machinegun arc crylink hlac hagar shotgun shockwave blaster rifle tuba minelayer" "Desired weapons for middle distances ordered by priority"
 +set bot_ai_custom_weapon_priority_close "vaporizer shotgun shockwave vortex machinegun arc hlac tuba seeker hagar crylink mortar electro devastator blaster fireball rifle minelayer" "Desired weapons for close distances ordered by priority"
  set bot_ai_weapon_combo 1     "Enable bots to do weapon combos"
  set bot_ai_weapon_combo_threshold 0.4 "Try to make a combo N seconds after the last attack"
  set bot_ai_friends_aware_pickup_radius "500"  "Bots will not pickup items if a team mate is this distance near the item"
@@@ -487,15 -487,15 +487,15 @@@ seta timelimit_suddendeath 5 "number o
  set g_tdm 0 "Team Deathmatch: the team who kills their opponents most often wins"
  set g_tdm_on_dm_maps 0 "when this is set, all DM maps automatically support TDM"
  
 -seta teamplay_mode 4 "default teamplay setting in team games. 1 = no friendly fire, self damage. 2 = friendly fire and self damage enabled. 3 = no friendly fire, but self damage enabled. 4 = obey the cvars g_mirrordamage*, g_friendlyfire* and g_teamdamage_threshold*"
 -seta g_mirrordamage 0.700000  "for teamplay 4: mirror damage factor"
 -seta g_mirrordamage_virtual 1 "for teamplay 4: do not actually apply mirror damage, just show graphics effect for it"
 -seta g_mirrordamage_onlyweapons 0 "for teamplay 4: only apply mirror damage if the attack was from a weapon"
 -seta g_friendlyfire 0.500000  "for teamplay 4: fiendly fire factor"
 -seta g_friendlyfire_virtual 1 "for teamplay 4: do not actually apply friendly fire, just show graphics effect for it"
 -seta g_friendlyfire_virtual_force 1   "for teamplay 4: apply force even though damage was made virtual only"
 -seta g_teamdamage_threshold 40        "for teamplay 4: threshold over which to apply mirror damage"
 -seta g_teamdamage_resetspeed 20       "for teamplay 4: how fast player's teamdamage count decreases"
 +seta teamplay_mode 4 "default teamplay setting in team games. 1 = no friendly fire, self damage. 2 = friendly fire and self damage enabled. 3 = no friendly fire, but self damage enabled. 4 = obey the cvars g_mirrordamage*, g_friendlyfire* and g_teamdamage*"
 +seta g_mirrordamage 0.7              "for teamplay_mode 4: mirror damage factor"
 +seta g_mirrordamage_virtual 1        "for teamplay_mode 4: do not actually apply mirror damage, just show graphics effect for it"
 +seta g_mirrordamage_onlyweapons 0    "for teamplay_mode 4: only apply mirror damage if the attack was from a weapon"
 +seta g_friendlyfire 0.5              "for teamplay_mode 4: friendly fire factor"
 +seta g_friendlyfire_virtual 1        "for teamplay_mode 4: do not actually apply friendly fire, just show graphics effect for it"
 +seta g_friendlyfire_virtual_force 1  "for teamplay_mode 4: apply force even though damage was made virtual only"
 +seta g_teamdamage_threshold 40       "for teamplay_mode 4: threshold over which to apply mirror damage"
 +seta g_teamdamage_resetspeed 20      "for teamplay_mode 4: how fast player's teamdamage count decreases"
  
  seta g_balance_teams 1        "automatically balance out players entering instead of asking them for their preferred team"
  seta g_balance_teams_prevent_imbalance        1       "prevent players from changing to larger teams"
@@@ -938,7 -938,7 +938,7 @@@ set con_completion_playermodel     "models/
  
  // helper
  // these non-saved engine cvars shall be saved
 -alias makesaved "seta $1 \"${$1 ?}\"
 +alias makesaved "seta $1 \"${$1 ?}\""
  makesaved cl_maxfps_alwayssleep
  makesaved cl_port
  makesaved gl_finish
@@@ -963,6 -963,8 +963,6 @@@ sv_gameplayfix_delayprojectiles 
  sv_gameplayfix_q2airaccelerate 1
  sv_gameplayfix_stepmultipletimes 1
  
 -cl_gameplayfix_fixedcheckwatertransition 1
 -
  // delay for "kill" to prevent abuse
  set g_balance_kill_delay 2
  set g_balance_kill_antispam 5
@@@ -1020,13 -1022,13 +1020,13 @@@ sv_allowdownloads 0 // download protoco
  
  set g_jump_grunt 0    "Do you make a grunting noise every time you jump? Is it the same grunting noise every time?"
  
 -seta cl_weaponpriority "vaporizer vortex fireball mortar machinegun hagar rifle arc electro devastator crylink minelayer shotgun hlac tuba blaster porto seeker hook" "weapon priority list"
 +seta cl_weaponpriority "vaporizer vortex fireball mortar machinegun hagar rifle arc electro devastator crylink minelayer shotgun shockwave hlac tuba blaster porto seeker hook" "weapon priority list"
  seta cl_weaponpriority_useforcycling 0 "when set, weapon cycling by the mouse wheel makes use of the weapon priority list (the special value 2 uses the weapon ID list for cycling)"
  seta cl_weaponpriority0 "devastator mortar hagar seeker fireball"                       "use weapon_priority_0_prev for prev gun from this list, weapon_priority_0_best for best gun, weapon_priority_0_next for next gun.  Default value: explosives"
  seta cl_weaponpriority1 "vaporizer vortex crylink hlac arc electro blaster shockwave"   "use weapon_priority_1_prev for prev gun from this list, weapon_priority_1_best for best gun, weapon_priority_1_next for next gun.  Default value: energy"
  seta cl_weaponpriority2 "vaporizer vortex rifle"                                        "use weapon_priority_2_prev for prev gun from this list, weapon_priority_2_best for best gun, weapon_priority_2_next for next gun.  Default value: hitscan exact"
 -seta cl_weaponpriority3 "vaporizer vortex rifle machinegun shotgun"                     "use weapon_priority_3_prev for prev gun from this list, weapon_priority_3_best for best gun, weapon_priority_3_next for next gun.  Default value: hitscan all"
 -seta cl_weaponpriority4 "mortar minelayer hlac hagar crylink seeker shotgun"            "use weapon_priority_4_prev for prev gun from this list, weapon_priority_4_best for best gun, weapon_priority_4_next for next gun.  Default value: spam weapons"
 +seta cl_weaponpriority3 "vaporizer vortex rifle machinegun shotgun shockwave"                     "use weapon_priority_3_prev for prev gun from this list, weapon_priority_3_best for best gun, weapon_priority_3_next for next gun.  Default value: hitscan all"
 +seta cl_weaponpriority4 "mortar minelayer hlac hagar crylink seeker shotgun shockwave"            "use weapon_priority_4_prev for prev gun from this list, weapon_priority_4_best for best gun, weapon_priority_4_next for next gun.  Default value: spam weapons"
  seta cl_weaponpriority5 "blaster shockwave hook porto"                                  "use weapon_priority_5_prev for prev gun from this list, weapon_priority_5_best for best gun, weapon_priority_5_next for next gun.  Default value: weapons for moving"
  seta cl_weaponpriority6 ""                                                              "use weapon_priority_6_prev for prev gun from this list, weapon_priority_6_best for best gun, weapon_priority_6_next for next gun"
  seta cl_weaponpriority7 ""                                                              "use weapon_priority_7_prev for prev gun from this list, weapon_priority_7_best for best gun, weapon_priority_7_next for next gun"
@@@ -1452,6 -1454,9 +1452,9 @@@ set cl_fullbright_items 0 "enable fullb
  set cl_weapon_stay_color "2 0.5 0.5" "Color of picked up weapons when g_weapon_stay > 0"
  set cl_weapon_stay_alpha 0.75 "Alpha of picked up weapons when g_weapon_stay > 0"
  
+ set sv_showspectators 0 "Show who's spectating who in the player info panel. Shouldn't be used on competitive servers, also disable when watching a suspected cheater"
+ seta cl_showspectators 1
  // Facility for config.cfg use ONLY.
  // Interpreted in post-config.cfg.
  seta menu_forced_saved_cvars "" "These cvars will always be saved, despite engine/Xonotic cvar saving status"
index 39c5bc64392526f637f2a3fd38c6c586b5b78bca,2df27e715aba8876e42266f6a56a6501c34d0b93..1839263a4563b7f1cd7a73bc3f2db8abad12c15e
@@@ -5,53 -5,12 +5,53 @@@
  
  // Info messages panel (#14)
  
 -#define drawInfoMessage(s) MACRO_BEGIN {                                                                                                                                                      \
 -      if(autocvar_hud_panel_infomessages_flip)                                                                                                                                                \
 -              o.x = pos.x + mySize.x - stringwidth(s, true, fontsize);                                                                                                        \
 -      drawcolorcodedstring(o, s, fontsize, a, DRAWFLAG_NORMAL);                                                                                                               \
 -      o.y += fontsize.y;                                                                                                                                                                                              \
 +float autocvar_hud_panel_infomessages_group_fadetime = 0.4;
 +float autocvar_hud_panel_infomessages_group_time = 6;
 +const int IMG_COUNT = 1; // number of InfoMessage Groups
 +float img_fade[IMG_COUNT];
 +int img_cur_msg[IMG_COUNT];
 +float img_time[IMG_COUNT];
 +
 +int img_select(int group_id)
 +{
 +      float fadetime = max(0.001, autocvar_hud_panel_infomessages_group_fadetime);
 +      if(time > img_time[group_id])
 +      {
 +              img_fade[group_id] = max(0, img_fade[group_id] - frametime / fadetime);
 +              if(!img_fade[group_id])
 +              {
 +                      ++img_cur_msg[group_id];
 +                      img_time[group_id] = floor(time) + autocvar_hud_panel_infomessages_group_time;
 +              }
 +      }
 +      else
 +              img_fade[group_id] = min(1, img_fade[group_id] + frametime / fadetime);
 +      return img_cur_msg[group_id];
 +}
 +
 +float stringwidth_colors(string s, vector theSize);
 +vector InfoMessages_drawstring(string s, vector pos, vector sz, float a, vector fontsize)
 +{
 +      getWrappedLine_remaining = s;
 +      float offset = 0;
 +      while(getWrappedLine_remaining)
 +      {
 +              s = getWrappedLine(sz.x - offset, fontsize, stringwidth_colors);
 +              if(autocvar_hud_panel_infomessages_flip)
 +                      offset = sz.x - stringwidth_colors(s, fontsize) - offset;
 +              drawcolorcodedstring(pos + eX * offset, s, fontsize, a, DRAWFLAG_NORMAL);
 +              pos.y += fontsize.y;
 +              offset = fontsize.x;
 +      }
 +      pos.y += fontsize.y * 0.25;
 +      return pos;
 +}
 +
 +#define InfoMessage(s) MACRO_BEGIN { \
 +      pos = InfoMessages_drawstring(s, pos, mySize, ((img_curr_group >= 0) ? panel_fg_alpha * img_fade[img_curr_group] : panel_fg_alpha), fontsize); \
 +      img_curr_group = -1; \
  } MACRO_END
 +
  void HUD_InfoMessages()
  {
        if(!autocvar__hud_configure)
                mySize -= '2 2 0' * panel_bg_padding;
        }
  
 -      // always force 5:1 aspect
 -      vector newSize = '0 0 0';
 -      if(mySize.x/mySize.y > 5)
 -      {
 -              newSize.x = 5 * mySize.y;
 -              newSize.y = mySize.y;
 -
 -              pos.x = pos.x + (mySize.x - newSize.x) / 2;
 -      }
 -      else
 -      {
 -              newSize.y = 1/5 * mySize.x;
 -              newSize.x = mySize.x;
 -
 -              pos.y = pos.y + (mySize.y - newSize.y) / 2;
 -      }
 -
 -      mySize = newSize;
 -      entity tm;
 -      vector o;
 -      o = pos;
 -
 -      vector fontsize;
 -      fontsize = '0.20 0.20 0' * mySize.y;
 -
 -      float a;
 -      a = panel_fg_alpha;
 -
 +      vector fontsize = '0.2 0.2 0' * mySize.y;
        string s;
 +      int img_curr_group = -1;
        if(!autocvar__hud_configure)
        {
                if(spectatee_status)
                {
 -                      a = 1;
                        if(spectatee_status == -1)
                                s = _("^1Observing");
                        else
                                s = sprintf(_("^1Spectating: ^7%s"), entcs_GetName(current_player));
 -                      drawInfoMessage(s);
 +                      InfoMessage(s);
  
 -                      if(spectatee_status == -1)
 -                              s = sprintf(_("^1Press ^3%s^1 to spectate"), getcommandkey("primary fire", "+fire"));
 -                      else
 -                              s = sprintf(_("^1Press ^3%s^1 or ^3%s^1 for next or previous player"), getcommandkey("next weapon", "weapnext"), getcommandkey("previous weapon", "weapprev"));
 -                      drawInfoMessage(s);
 -
 -                      if(spectatee_status == -1)
 -                              s = sprintf(_("^1Use ^3%s^1 or ^3%s^1 to change the speed"), getcommandkey("next weapon", "weapnext"), getcommandkey("previous weapon", "weapprev"));
 -                      else
 -                              s = sprintf(_("^1Press ^3%s^1 to observe"), getcommandkey("secondary fire", "+fire2"));
 -                      drawInfoMessage(s);
 -
 -                      s = sprintf(_("^1Press ^3%s^1 for gamemode info"), getcommandkey("server info", "+show_info"));
 -                      drawInfoMessage(s);
 +                      img_curr_group = 0;
 +                      switch(img_select(img_curr_group) % 3)
 +                      {
 +                              default:
 +                              case 0:
 +                                      if(spectatee_status == -1)
 +                                              s = sprintf(_("^1Press ^3%s^1 to spectate"), getcommandkey(_("primary fire"), "+fire"));
 +                                      else
 +                                              s = sprintf(_("^1Press ^3%s^1 or ^3%s^1 for next or previous player"), getcommandkey(_("next weapon"), "weapnext"), getcommandkey(_("previous weapon"), "weapprev"));
 +                                      break;
 +                              case 1:
 +                                      if(spectatee_status == -1)
 +                                              s = sprintf(_("^1Use ^3%s^1 or ^3%s^1 to change the speed"), getcommandkey(_("next weapon"), "weapnext"), getcommandkey(_("previous weapon"), "weapprev"));
 +                                      else
 +                                              s = sprintf(_("^1Press ^3%s^1 to observe, ^3%s^1 to change camera mode"), getcommandkey(_("secondary fire"), "+fire2"), getcommandkey(_("drop weapon"), "dropweapon"));
 +                                      break;
 +                              case 2:
 +                                      s = sprintf(_("^1Press ^3%s^1 for gamemode info"), getcommandkey(_("server info"), "+show_info"));
 +                                      break;
 +                      }
 +                      InfoMessage(s);
  
                        if(gametype == MAPINFO_TYPE_LMS)
                        {
                                else if(sk.(scores[ps_primary]) > 0)
                                        s = _("^1You have no more lives left");
                                else
 -                                      s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey("jump", "+jump"));
 +                                      s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey(_("jump"), "+jump"));
                        }
                        else
 -                              s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey("jump", "+jump"));
 -                      drawInfoMessage(s);
 +                              s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey(_("jump"), "+jump"));
 +                      InfoMessage(s);
                }
  
                if (time < STAT(GAMESTARTTIME))
                        //we need to ceil, otherwise the countdown would be off by .5 when using round()
                        float countdown = ceil(STAT(GAMESTARTTIME) - time);
                        s = sprintf(_("^1Game starts in ^3%d^1 seconds"), countdown);
 -                      drawInfoMessage(s);
 +                      InfoMessage(s);
                }
  
                if(warmup_stage)
                {
                        s = _("^2Currently in ^1warmup^2 stage!");
 -                      drawInfoMessage(s);
 +                      InfoMessage(s);
                }
  
                string blinkcolor;
                        if(ready_waiting_for_me)
                        {
                                if(warmup_stage)
 -                                      s = sprintf(_("%sPress ^3%s%s to end warmup"), blinkcolor, getcommandkey("ready", "ready"), blinkcolor);
 +                                      s = sprintf(_("%sPress ^3%s%s to end warmup"), blinkcolor, getcommandkey(_("ready"), "ready"), blinkcolor);
                                else
 -                                      s = sprintf(_("%sPress ^3%s%s once you are ready"), blinkcolor, getcommandkey("ready", "ready"), blinkcolor);
 +                                      s = sprintf(_("%sPress ^3%s%s once you are ready"), blinkcolor, getcommandkey(_("ready"), "ready"), blinkcolor);
                        }
                        else
                        {
                                else
                                        s = _("^2Waiting for others to ready up...");
                        }
 -                      drawInfoMessage(s);
 +                      InfoMessage(s);
                }
                else if(warmup_stage && !spectatee_status)
                {
 -                      s = sprintf(_("^2Press ^3%s^2 to end warmup"), getcommandkey("ready", "ready"));
 -                      drawInfoMessage(s);
 +                      s = sprintf(_("^2Press ^3%s^2 to end warmup"), getcommandkey(_("ready"), "ready"));
 +                      InfoMessage(s);
                }
  
                if(teamplay && !spectatee_status && gametype != MAPINFO_TYPE_CA && teamnagger)
                {
                        float ts_min = 0, ts_max = 0;
 -                      tm = teams.sort_next;
 +                      entity tm = teams.sort_next;
                        if (tm)
                        {
                                for (; tm.sort_next; tm = tm.sort_next)
                                {
                                        s = strcat(blinkcolor, _("Teamnumbers are unbalanced!"));
                                        tm = GetTeam(myteam, false);
 -                                      if (tm)
 -                                      if (tm.team != NUM_SPECTATOR)
 -                                      if (tm.team_size == ts_max)
 -                                              s = strcat(s, sprintf(_(" Press ^3%s%s to adjust"), getcommandkey("team menu", "menu_showteamselect"), blinkcolor));
 -                                      drawInfoMessage(s);
 +                                      if (tm && tm.team != NUM_SPECTATOR && tm.team_size == ts_max)
 +                                              s = strcat(s, sprintf(_(" Press ^3%s%s to adjust"), getcommandkey(_("team menu"), "menu_showteamselect"), blinkcolor));
 +                                      InfoMessage(s);
                                }
                        }
                }
+               if(autocvar_cl_showspectators)
+               if(num_spectators)
+               //if(spectatee_status != -1)
+               {
+                       s = ((spectatee_status) ? _("^1Spectating this player:") : _("^1Spectating you:"));
+                       //drawInfoMessage(s)
+                       int limit = min(num_spectators, MAX_SPECTATORS);
+                       for(int i = 0; i < limit; ++i)
+                       {
+                               float slot = spectatorlist[i];
+                               if(i == 0)
+                                       s = strcat(s, " ^7", entcs_GetName(slot));
+                               else
+                                       s = strcat("^7", entcs_GetName(slot));
+                               drawInfoMessage(s);
+                       }
+               }
        }
        else
        {
 -              s = _("^7Press ^3ESC ^7to show HUD options.");
 -              drawInfoMessage(s);
 -              s = _("^3Doubleclick ^7a panel for panel-specific options.");
 -              drawInfoMessage(s);
 -              s = _("^3CTRL ^7to disable collision testing, ^3SHIFT ^7and");
 -              drawInfoMessage(s);
 -              s = _("^3ALT ^7+ ^3ARROW KEYS ^7for fine adjustments.");
 -              drawInfoMessage(s);
 +              InfoMessage(_("^7Press ^3ESC ^7to show HUD options."));
 +              InfoMessage(_("^3Doubleclick ^7a panel for panel-specific options."));
 +              InfoMessage(_("^3CTRL ^7to disable collision testing, ^3SHIFT ^7and"));
 +              InfoMessage(_("^3ALT ^7+ ^3ARROW KEYS ^7for fine adjustments."));
        }
  }
diff --combined qcsrc/client/main.qc
index 1733008a5c5f6326ea03de2f5bb56de2b1b3bf14,9637c98c55542fd7b173019550e2714885e3a7ac..2905b8b2da90e8ff51c6e135e6bac8206586a846
@@@ -185,8 -185,8 +185,8 @@@ void Shutdown(
  {
        WarpZone_Shutdown();
  
 -      remove(teams);
 -      remove(players);
 +      delete(teams);
 +      delete(players);
        db_close(binddb);
        db_close(tempdb);
        if(autocvar_cl_db_saveasdump)
@@@ -516,6 -516,22 +516,22 @@@ NET_HANDLE(ENT_CLIENT_CLIENTDATA, bool 
        else
                angles_held_status = 0;
  
+       if(f & 16)
+       {
+               num_spectators = ReadByte();
+               float i, slot;
+               for(i = 0; i < MAX_SPECTATORS; ++i)
+                       spectatorlist[i] = 0; // reset list first
+               for(i = 0; i < num_spectators; ++i)
+               {
+                       slot = ReadByte();
+                       spectatorlist[i] = slot - 1;
+               }
+       }
        return = true;
  
        if(newspectatee_status != spectatee_status)
@@@ -699,9 -715,8 +715,9 @@@ NET_HANDLE(ENT_CLIENT_SPAWNPOINT, bool 
                        precache_model(this.mdl);
                        setmodel(this, this.mdl);
                        this.drawmask = MASK_NORMAL;
 -                      //this.movetype = MOVETYPE_NOCLIP;
 +                      //this.move_movetype = MOVETYPE_NOCLIP;
                        //this.draw = Spawn_Draw;
 +                      IL_PUSH(g_drawables, this);
                }*/
                if(autocvar_cl_spawn_point_particles)
                {
                        else { this.cnt = particleeffectnum(EFFECT_SPAWNPOINT_NEUTRAL); }
  
                        this.draw = Spawn_Draw;
 +                      if (is_new) IL_PUSH(g_drawables, this);
                        setpredraw(this, Spawn_PreDraw);
                        this.fade_start = autocvar_cl_spawn_point_dist_min;
                        this.fade_end = autocvar_cl_spawn_point_dist_max;
@@@ -874,7 -888,7 +890,7 @@@ void CSQC_Ent_Remove(entity this
                return;
        }
        if (this.enttype) Ent_Remove(this);
 -      remove(this);
 +      delete(this);
  }
  
  void Gamemode_Init()
@@@ -1222,13 -1236,13 +1238,13 @@@ NET_HANDLE(TE_CSQC_WEAPONCOMPLAIN, boo
        }
  }
  
 -string getcommandkey(string text, string command)
 +string _getcommandkey(string cmd_name, string command, bool forcename)
  {
        string keys;
        float n, j, k, l = 0;
  
        if (!autocvar_hud_showbinds)
 -              return text;
 +              return cmd_name;
  
        keys = db_get(binddb, command);
        if (keys == "")
  
        if (keys == "NO_KEY") {
                if (autocvar_hud_showbinds > 1)
 -                      return sprintf(_("%s (not bound)"), text);
 +                      return sprintf(_("%s (not bound)"), cmd_name);
                else
 -                      return text;
 +                      return cmd_name;
        }
 -      else if (autocvar_hud_showbinds > 1)
 -              return sprintf("%s (%s)", text, keys);
 +      else if (autocvar_hud_showbinds > 1 || forcename)
 +              return sprintf("%s (%s)", cmd_name, keys);
        else
                return keys;
  }
diff --combined qcsrc/client/main.qh
index ffb853d2f8379d93d47bac930923b1813defb23e,bc6b5afec0d4059f446ba60df7443b035d7d4163..b47836f23caca4f5ec4b3fb400d5f5e1034d5ee1
@@@ -85,20 -85,11 +85,20 @@@ entity teamslots[17];    // 17 teams (i
  .float eliminated;
  
  .void(entity) draw;
 +IntrusiveList g_drawables;
 +STATIC_INIT(g_drawables) { g_drawables = IL_NEW(); }
  .void(entity) draw2d;
 +IntrusiveList g_drawables_2d;
 +STATIC_INIT(g_drawables_2d) { g_drawables_2d = IL_NEW(); }
  .void(entity) entremove;
  float drawframetime;
  vector view_origin, view_forward, view_right, view_up;
  
 +IntrusiveList g_radarlinks;
 +STATIC_INIT(g_radarlinks) { g_radarlinks = IL_NEW(); }
 +IntrusiveList g_radaricons;
 +STATIC_INIT(g_radaricons) { g_radaricons = IL_NEW(); }
 +
  bool button_zoom;
  bool spectatorbutton_zoom;
  bool button_attack2;
@@@ -112,9 -103,7 +112,9 @@@ float warmup_stage
  
  void Fog_Force();
  
 -string getcommandkey(string text, string command);
 +string _getcommandkey(string text, string command, bool forcename);
 +#define getcommandkey(cmd_name, command) _getcommandkey(cmd_name, command, false)
 +#define getcommandkey_forcename(cmd_name, command) _getcommandkey(cmd_name, command, true)
  
  string vote_called_vote;
  float ready_waiting;
@@@ -151,6 -140,11 +151,11 @@@ float g_trueaim_minrange
  
  float hud;
  float view_quality;
+ int num_spectators;
+ const int MAX_SPECTATORS = 7;
+ int spectatorlist[MAX_SPECTATORS];
  int framecount;
  .float health;
  
index f66b77a068145eadb0460bfdd776d531aed6b454,fc50ee1aee479b918ba3b6b7e1f6ead144353669..188cc713137adff0ff81112ecc795719b4abe612
@@@ -357,7 -357,7 +357,7 @@@ float autocvar_sv_gameplayfix_q2airacce
  int autocvar_sv_gentle;
  #define autocvar_sv_gravity cvar("sv_gravity")
  string autocvar_sv_intermission_cdtrack;
 -float autocvar_sv_itemstime;
 +int autocvar_sv_itemstime;
  string autocvar_sv_jumpspeedcap_max;
  float autocvar_sv_jumpspeedcap_max_disable_on_ramps;
  string autocvar_sv_jumpspeedcap_min;
@@@ -574,3 -574,4 +574,4 @@@ float autocvar_sv_stopspeed
  float autocvar_sv_airaccelerate;
  float autocvar_sv_airstopaccelerate;
  float autocvar_sv_track_canjump;
+ bool autocvar_sv_showspectators;
index 4a86c936e9b653fb3b1096b85a2f7cb066e65df0,e3669c73191b01f5a38b8971a29d70a1d9639267..8de814507c39e53e16436826f921b30db1bea6b3
@@@ -75,6 -75,30 +75,30 @@@ void send_CSQC_teamnagger() 
        WriteHeader(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
  }
  
+ int CountSpectators(entity player, entity to)
+ {
+       if(!player) { return 0; } // not sure how, but best to be safe
+       int spec_count = 0;
+       FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
+       {
+               spec_count++;
+       });
+       return spec_count;
+ }
+ void WriteSpectators(entity player, entity to)
+ {
+       if(!player) { return; } // not sure how, but best to be safe
+       FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
+       {
+               WriteByte(MSG_ENTITY, num_for_edict(it));
+       });
+ }
  bool ClientData_Send(entity this, entity to, int sf)
  {
        assert(to == this.owner, return false);
        if (to.spectatee_status)    sf |= 2; // spectator ent number follows
        if (e.zoomstate)            sf |= 4; // zoomed
        if (e.porto_v_angle_held)   sf |= 8; // angles held
+       if (autocvar_sv_showspectators) sf |= 16; // show spectators
  
        WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
        WriteByte(MSG_ENTITY, sf);
                WriteAngle(MSG_ENTITY, e.v_angle.x);
                WriteAngle(MSG_ENTITY, e.v_angle.y);
        }
+       if(sf & 16)
+       {
+               float specs = CountSpectators(e, to);
+               WriteByte(MSG_ENTITY, specs);
+               WriteSpectators(e, to);
+       }
        return true;
  }
  
@@@ -112,7 -145,7 +145,7 @@@ void ClientData_Attach(entity this
  
  void ClientData_Detach(entity this)
  {
 -      remove(this.clientdata);
 +      delete(this.clientdata);
        this.clientdata = NULL;
  }
  
@@@ -224,6 -257,7 +257,7 @@@ void PutObserverInServer(entity this
      RemoveGrapplingHook(this);
        Portal_ClearAll(this);
        Unfreeze(this);
+       SetSpectatee(this, NULL);
  
        if (this.alivetime)
        {
        this.health = FRAGS_SPECTATOR;
        this.takedamage = DAMAGE_NO;
        this.solid = SOLID_NOT;
 -      this.movetype = MOVETYPE_FLY_WORLDONLY; // user preference is controlled by playerprethink
 +      set_movetype(this, MOVETYPE_FLY_WORLDONLY); // user preference is controlled by playerprethink
        this.flags = FL_CLIENT | FL_NOTARGET;
        this.armorvalue = 666;
        this.effects = 0;
@@@ -481,7 -515,7 +515,7 @@@ void PutClientInServer(entity this
                this.iscreature = true;
                this.teleportable = TELEPORT_NORMAL;
                this.damagedbycontents = true;
 -              this.movetype = MOVETYPE_WALK;
 +              set_movetype(this, MOVETYPE_WALK);
                this.solid = SOLID_SLIDEBOX;
                this.dphitcontentsmask = DPCONTENTS_BODY | DPCONTENTS_SOLID;
                if (autocvar_g_playerclip_collisions)
                if (autocvar_spawn_debug)
                {
                        sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
 -                      remove(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
 +                      delete(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
                }
  
                PS(this).m_switchweapon = w_getbestweapon(this);
@@@ -797,7 -831,7 +831,7 @@@ void ClientKill_Now(entity this
        }
  
        if(this.killindicator && !wasfreed(this.killindicator))
 -              remove(this.killindicator);
 +              delete(this.killindicator);
  
        this.killindicator = NULL;
  
@@@ -814,14 -848,14 +848,14 @@@ void KillIndicator_Think(entity this
        if (gameover)
        {
                this.owner.killindicator = NULL;
 -              remove(this);
 +              delete(this);
                return;
        }
  
        if (this.owner.alpha < 0 && !this.owner.vehicle)
        {
                this.owner.killindicator = NULL;
 -              remove(this);
 +              delete(this);
                return;
        }
  
@@@ -1147,7 -1181,7 +1181,7 @@@ void ClientConnect(entity this
        if (!sv_foginterval && world.fog != "")
                stuffcmd(this, strcat("\nfog ", world.fog, "\nr_fog_exp2 0\nr_drawfog 1\n"));
  
 -      if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && (c3==-1 && c4==-1)))
 +      if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AvailableTeams() == 2))
                if (!g_ca && !g_cts && !g_race) // teamnagger is currently bad for ca, race & cts
                        send_CSQC_teamnagger();
  
@@@ -1187,6 -1221,8 +1221,8 @@@ void ClientDisconnect(entity this
  
        Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_DISCONNECT, this.netname);
  
+       SetSpectatee(this, NULL);
      MUTATOR_CALLHOOK(ClientDisconnect, this);
  
        ClientState_detach(this);
  
        this.flags &= ~FL_CLIENT;
  
 -      if (this.chatbubbleentity) remove(this.chatbubbleentity);
 -      if (this.killindicator) remove(this.killindicator);
 +      if (this.chatbubbleentity) delete(this.chatbubbleentity);
 +      if (this.killindicator) delete(this.killindicator);
  
        WaypointSprite_PlayerGone(this);
  
        if (this.netname_previous) strunzone(this.netname_previous);
        if (this.clientstatus) strunzone(this.clientstatus);
        if (this.weaponorder_byimpulse) strunzone(this.weaponorder_byimpulse);
 -      if (this.personal) remove(this.personal);
 +      if (this.personal) delete(this.personal);
  
        this.playerid = 0;
        ReadyCount();
@@@ -1225,7 -1261,7 +1261,7 @@@ void ChatBubbleThink(entity this
        {
                if(this.owner) // but why can that ever be NULL?
                        this.owner.chatbubbleentity = NULL;
 -              remove(this);
 +              delete(this);
                return;
        }
  
@@@ -1289,7 -1325,7 +1325,7 @@@ void respawn(entity this
        {
                this.solid = SOLID_NOT;
                this.takedamage = DAMAGE_NO;
 -              this.movetype = MOVETYPE_FLY;
 +              set_movetype(this, MOVETYPE_FLY);
                this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
                this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
                this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
@@@ -1615,7 -1651,7 +1651,7 @@@ void SpectateCopy(entity this, entity s
        this.angles = spectatee.v_angle;
        STAT(FROZEN, this) = STAT(FROZEN, spectatee);
        this.revive_progress = spectatee.revive_progress;
 -      if(!PHYS_INPUT_BUTTON_USE(this))
 +      if(!PHYS_INPUT_BUTTON_USE(this) && STAT(CAMERA_SPECTATOR, this) != 2)
                this.fixangle = true;
        setorigin(this, spectatee.origin);
        setsize(this, spectatee.mins, spectatee.maxs);
@@@ -1672,10 -1708,12 +1708,12 @@@ bool SpectateSet(entity this
        if(!IS_PLAYER(this.enemy))
                return false;
  
+       ClientData_Touch(this.enemy);
        msg_entity = this;
        WriteByte(MSG_ONE, SVC_SETVIEW);
        WriteEntity(MSG_ONE, this.enemy);
 -      this.movetype = MOVETYPE_NONE;
 +      set_movetype(this, MOVETYPE_NONE);
        accuracy_resend(this);
  
        if(!SpectateUpdate(this))
@@@ -1694,6 -1732,9 +1732,9 @@@ void SetSpectatee(entity this, entity s
        // these are required to fix the spectator bug with arc
        if(old_spectatee && old_spectatee.arc_beam) { old_spectatee.arc_beam.SendFlags |= ARC_SF_SETTINGS; }
        if(this.enemy && this.enemy.arc_beam) { this.enemy.arc_beam.SendFlags |= ARC_SF_SETTINGS; }
+       // needed to update spectator list
+       if(old_spectatee) { ClientData_Touch(old_spectatee); }
  }
  
  bool Spectate(entity this, entity pl)
@@@ -1791,6 -1832,8 +1832,8 @@@ void LeaveSpectatorMode(entity this
                {
                        TRANSMUTE(Player, this);
  
+                       SetSpectatee(this, NULL);
                        if(autocvar_g_campaign || autocvar_g_balance_teams)
                                { JoinBestTeam(this, false, true); }
  
@@@ -1925,6 -1968,7 +1968,6 @@@ void ObserverThink(entity this
                MinigameImpulse(this, this.impulse);
                this.impulse = 0;
        }
 -      float prefered_movetype;
        if (this.flags & FL_JUMPRELEASED) {
                if (PHYS_INPUT_BUTTON_JUMP(this) && !this.version_mismatch) {
                        this.flags &= ~FL_JUMPRELEASED;
                                TRANSMUTE(Spectator, this);
                        }
                } else {
 -                      prefered_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? this.cvar_cl_clippedspectating : !this.cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
 -                      if (this.movetype != prefered_movetype)
 -                              this.movetype = prefered_movetype;
 +                      int preferred_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? this.cvar_cl_clippedspectating : !this.cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
 +                      set_movetype(this, preferred_movetype);
                }
        } else {
                if (!(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this))) {
@@@ -1957,13 -2002,6 +2000,13 @@@ void SpectatorThink(entity this
        {
                if(MinigameImpulse(this, this.impulse))
                        this.impulse = 0;
 +
 +              if (this.impulse == IMP_weapon_drop.impulse)
 +              {
 +                      STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
 +                      this.impulse = 0;
 +                      return;
 +              }
        }
        if (this.flags & FL_JUMPRELEASED) {
                if (PHYS_INPUT_BUTTON_JUMP(this) && !this.version_mismatch) {
@@@ -2286,15 -2324,12 +2329,15 @@@ void PlayerPreThink (entity this
          .entity weaponentity = weaponentities[0]; // TODO: unhardcode
                if (this.hook.state) {
                        do_crouch = false;
 +              } else if (this.waterlevel >= WATERLEVEL_SWIMMING) {
 +                      do_crouch = false;
                } else if (this.vehicle) {
                        do_crouch = false;
                } else if (STAT(FROZEN, this)) {
                        do_crouch = false;
 -        } else if ((PS(this).m_weapon == WEP_SHOTGUN || PS(this).m_weapon == WEP_SHOCKWAVE) && this.(weaponentity).wframe == WFRAME_FIRE2 && time < this.(weaponentity).weapon_nextthink) {
 -                  // WEAPONTODO: predict
 +        } else if ((PS(this).m_weapon.spawnflags & WEP_TYPE_MELEE_PRI) && this.(weaponentity).wframe == WFRAME_FIRE1 && time < this.(weaponentity).weapon_nextthink) {
 +                      do_crouch = false;
 +        } else if ((PS(this).m_weapon.spawnflags & WEP_TYPE_MELEE_SEC) && this.(weaponentity).wframe == WFRAME_FIRE2 && time < this.(weaponentity).weapon_nextthink) {
                        do_crouch = false;
          }
  
@@@ -2426,30 -2461,6 +2469,30 @@@ void DrownPlayer(entity this
        }
  }
  
 +void Player_Physics(entity this)
 +{
 +      set_movetype(this, ((this.move_qcphysics) ? MOVETYPE_NONE : this.move_movetype));
 +
 +      if(!this.move_qcphysics)
 +              return;
 +
 +      int mt = this.move_movetype;
 +
 +      if(mt == MOVETYPE_PUSH || mt == MOVETYPE_FAKEPUSH || mt == MOVETYPE_PHYSICS)
 +      {
 +              this.move_qcphysics = false;
 +              set_movetype(this, mt);
 +              return;
 +      }
 +
 +      if(!frametime && !this.pm_frametime)
 +              return;
 +
 +      Movetype_Physics_NoMatchTicrate(this, this.pm_frametime, true);
 +
 +      this.pm_frametime = 0;
 +}
 +
  /*
  =============
  PlayerPostThink
@@@ -2460,8 -2471,6 +2503,8 @@@ Called every frame for each client afte
  .float idlekick_lasttimeleft;
  void PlayerPostThink (entity this)
  {
 +      Player_Physics(this);
 +
        if (sv_maxidle > 0)
        if (frametime) // WORKAROUND: only use dropclient in server frames (frametime set). Never use it in cl_movement frames (frametime zero).
        if (IS_REAL_CLIENT(this))