Merge branch 'master' into Mario/showspecs
authorMario <mario@smbclan.net>
Sun, 12 Jun 2016 10:43:18 +0000 (20:43 +1000)
committerMario <mario@smbclan.net>
Sun, 12 Jun 2016 10:43:18 +0000 (20:43 +1000)
# Conflicts:
# qcsrc/client/hud.qc
# qcsrc/client/main.qc
# qcsrc/server/cl_client.qc

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

Simple merge
Simple merge
index 0000000,a197963..fe16ef1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,191 +1,209 @@@
+ #include "infomessages.qh"
+ #include <common/ent_cs.qh>
+ #include <common/mapinfo.qh>
+ // 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;                                                                                                                                                                                              \
+ } MACRO_END
+ void HUD_InfoMessages()
+ {
+       if(!autocvar__hud_configure)
+       {
+               if(!autocvar_hud_panel_infomessages) return;
+       }
+       HUD_Panel_UpdateCvars();
+       vector pos, mySize;
+       pos = panel_pos;
+       mySize = panel_size;
+       if (autocvar_hud_panel_infomessages_dynamichud)
+               HUD_Scale_Enable();
+       else
+               HUD_Scale_Disable();
+       HUD_Panel_DrawBg(1);
+       if(panel_bg_padding)
+       {
+               pos += '1 1 0' * panel_bg_padding;
+               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;
+       string s;
+       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);
+                       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);
+                       if(gametype == MAPINFO_TYPE_LMS)
+                       {
+                               entity sk;
+                               sk = playerslots[player_localnum];
+                               if(sk.(scores[ps_primary]) >= 666)
+                                       s = _("^1Match has already begun");
+                               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"));
+                       }
+                       else
+                               s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey("jump", "+jump"));
+                       drawInfoMessage(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);
+               }
+               if(warmup_stage)
+               {
+                       s = _("^2Currently in ^1warmup^2 stage!");
+                       drawInfoMessage(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, " ^3", entcs_GetName(slot));
++                              else
++                                      s = strcat("^3", entcs_GetName(slot));
++                              drawInfoMessage(s);
++                      }
++              }
++
+               string blinkcolor;
+               if(time % 1 >= 0.5)
+                       blinkcolor = "^1";
+               else
+                       blinkcolor = "^3";
+               if(ready_waiting && !spectatee_status)
+               {
+                       if(ready_waiting_for_me)
+                       {
+                               if(warmup_stage)
+                                       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);
+                       }
+                       else
+                       {
+                               if(warmup_stage)
+                                       s = _("^2Waiting for others to ready up to end warmup...");
+                               else
+                                       s = _("^2Waiting for others to ready up...");
+                       }
+                       drawInfoMessage(s);
+               }
+               else if(warmup_stage && !spectatee_status)
+               {
+                       s = sprintf(_("^2Press ^3%s^2 to end warmup"), getcommandkey("ready", "ready"));
+                       drawInfoMessage(s);
+               }
+               if(teamplay && !spectatee_status && gametype != MAPINFO_TYPE_CA && teamnagger)
+               {
+                       float ts_min = 0, ts_max = 0;
+                       tm = teams.sort_next;
+                       if (tm)
+                       {
+                               for (; tm.sort_next; tm = tm.sort_next)
+                               {
+                                       if(!tm.team_size || tm.team == NUM_SPECTATOR)
+                                               continue;
+                                       if(!ts_min) ts_min = tm.team_size;
+                                       else ts_min = min(ts_min, tm.team_size);
+                                       if(!ts_max) ts_max = tm.team_size;
+                                       else ts_max = max(ts_max, tm.team_size);
+                               }
+                               if ((ts_max - ts_min) > 1)
+                               {
+                                       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);
+                               }
+                       }
+               }
+       }
+       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);
+       }
+ }
@@@ -515,22 -516,8 +516,24 @@@ 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)
        {
                // clear race stuff
@@@ -138,20 -136,9 +136,14 @@@ const int HOOK_END =      2
  
  .float ping, ping_packetloss, ping_movementloss;
  
- float g_balance_mortar_bouncefactor;
- float g_balance_mortar_bouncestop;
- float g_balance_electro_secondary_bouncefactor;
- float g_balance_electro_secondary_bouncestop;
  float g_trueaim_minrange;
  
- entity entcs_receiver[255]; // 255 is the engine limit on maxclients
  float hud;
  float view_quality;
 +
 +int num_spectators;
 +const int MAX_SPECTATORS = 7;
 +int spectatorlist[MAX_SPECTATORS];
 +
  int framecount;
- #endif
+ .float health;
  
  #include "../common/monsters/sv_monsters.qh"
  
- #include "../warpzonelib/server.qh"
+ #include "../lib/warpzone/server.qh"
+ STATIC_METHOD(Client, Add, void(Client this, int _team))
+ {
+     WITHSELF(this, ClientConnect());
+     TRANSMUTE(Player, this);
+     this.frame = 12; // 7
+     this.team = _team;
+     WITHSELF(this, PutClientInServer());
+ }
+ void PutObserverInServer(entity this);
+ void ClientDisconnect();
  
- float c1, c2, c3, c4;
+ STATIC_METHOD(Client, Remove, void(Client this))
+ {
+     TRANSMUTE(Observer, this);
+     WITHSELF(this, PutClientInServer());
+     WITHSELF(this, ClientDisconnect());
+ }
  
  void send_CSQC_teamnagger() {
-       WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
-       WriteByte(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
+       WriteHeader(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
  }
  
-       entity head;
-       float spec_count = 0;
-       FOR_EACH_REALCLIENT(head)
 +int CountSpectators(entity player, entity to)
 +{
 +      if(!player) { return 0; } // not sure how, but best to be safe
 +
-               if(IS_SPEC(head))
-               if(head != to)
-               if(head.enemy == player)
-                       spec_count += 1;
-       }
++      int spec_count = 0;
++
++      FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
 +      {
-       entity head;
-       FOR_EACH_REALCLIENT(head)
++              spec_count++;
++      });
 +
 +      return spec_count;
 +}
 +
 +void WriteSpectators(entity player, entity to)
 +{
 +      if(!player) { return; } // not sure how, but best to be safe
 +
-               if(IS_SPEC(head))
-               if(head != to)
-               if(head.enemy == player)
-                       WriteByte(MSG_ENTITY, num_for_edict(head));
-       }
++      FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
 +      {
- bool ClientData_Send(entity to, int sf)
++              WriteByte(MSG_ENTITY, num_for_edict(it));
++      });
 +}
 +
+ bool ClientData_Send(entity this, entity to, int sf)
  {
-       if(to != self.owner)
-       {
-               error("wtf");
-               return false;
-       }
+       assert(to == this.owner, return false);
  
-       entity e;
-       e = to;
-       if(IS_SPEC(to))
-               e = to.enemy;
+       entity e = to;
+       if (IS_SPEC(e)) e = e.enemy;
  
        sf = 0;
+       if (e.race_completed)       sf |= 1; // forced scoreboard
+       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
++      sf |= 16; // always check spectators
  
-       if(e.race_completed)
-               sf |= 1; // forced scoreboard
-       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
-       // always check spectators
-       sf |= 16; // spectator handling?
-       WriteByte(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
        WriteByte(MSG_ENTITY, sf);
  
-       if(sf & 2)
+       if (sf & 2)
+       {
                WriteByte(MSG_ENTITY, to.spectatee_status);
-       if(sf & 8)
+       }
+       if (sf & 8)
        {
                WriteAngle(MSG_ENTITY, e.v_angle.x);
                WriteAngle(MSG_ENTITY, e.v_angle.y);
@@@ -189,162 -176,166 +209,167 @@@ string CheckPlayerModel(string plyermod
  void setplayermodel(entity e, string modelname)
  {
        precache_model(modelname);
-       setmodel(e, modelname);
-       player_setupanimsformodel();
-       UpdatePlayerSounds();
+       _setmodel(e, modelname);
+       player_setupanimsformodel(e);
+       if(!autocvar_g_debug_globalsounds)
+               UpdatePlayerSounds(e);
  }
  
- /*
- =============
- PutObserverInServer
- putting a client as observer in the server
- =============
- */
- void FixPlayermodel();
- void PutObserverInServer (void)
+ void FixPlayermodel(entity player);
+ /** putting a client as observer in the server */
+ void PutObserverInServer(entity this)
  {
-       entity spot;
-       SetSpectator(self, world);
-     self.hud = HUD_NORMAL;
-       if(IS_PLAYER(self)) { pointparticles(particleeffectnum("spawn_event_neutral"), self.origin, '0 0 0', 1); }
+     bool mutator_returnvalue = MUTATOR_CALLHOOK(MakePlayerObserver, this);
+       PlayerState_detach(this);
  
-       spot = SelectSpawnPoint (true);
-       if(!spot)
-               error("No spawnpoints for observers?!?\n");
-       RemoveGrapplingHook(self); // Wazat's Grappling Hook
-       if(IS_REAL_CLIENT(self))
-       {
-               msg_entity = self;
-               WriteByte(MSG_ONE, SVC_SETVIEW);
-               WriteEntity(MSG_ONE, self);
-       }
-       self.frags = FRAGS_SPECTATOR;
-       MUTATOR_CALLHOOK(MakePlayerObserver);
+       if (IS_PLAYER(this) && this.health >= 1) {
+         // despawn effect
+               Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
+     }
  
-       Portal_ClearAll(self);
+     {
+         entity spot = SelectSpawnPoint(this, true);
+         if (!spot) LOG_FATAL("No spawnpoints for observers?!?");
+         this.angles = spot.angles;
+         this.angles_z = 0;
+         this.fixangle = true;
+         // offset it so that the spectator spawns higher off the ground, looks better this way
+         setorigin(this, spot.origin + STAT(PL_VIEW_OFS, NULL));
+         this.prevorigin = this.origin;
+         if (IS_REAL_CLIENT(this))
+         {
+             msg_entity = this;
+             WriteByte(MSG_ONE, SVC_SETVIEW);
+             WriteEntity(MSG_ONE, this);
+         }
+         // give the spectator some space between walls for MOVETYPE_FLY_WORLDONLY
+         // so that your view doesn't go into the ceiling with MOVETYPE_FLY_WORLDONLY, previously "PL_VIEW_OFS"
+         if(!autocvar_g_debug_globalsounds)
+         {
+               // needed for player sounds
+               this.model = "";
+               FixPlayermodel(this);
+         } 
+         setmodel(this, MDL_Null);
+         setsize(this, STAT(PL_CROUCH_MIN, NULL), STAT(PL_CROUCH_MAX, NULL));
+         this.view_ofs = '0 0 0';
+     }
  
-       Unfreeze(self);
+     RemoveGrapplingHook(this);
+       Portal_ClearAll(this);
+       Unfreeze(this);
++      SetSpectatee(this, world);
  
-       if(self.alivetime)
+       if (this.alivetime)
        {
-               if(!warmup_stage)
-                       PS_GR_P_ADDVAL(self, PLAYERSTATS_ALIVETIME, time - self.alivetime);
-               self.alivetime = 0;
+               if (!warmup_stage)
+                       PS_GR_P_ADDVAL(this, PLAYERSTATS_ALIVETIME, time - this.alivetime);
+               this.alivetime = 0;
        }
  
-       if(self.vehicle)
-               vehicles_exit(VHEF_RELESE);
+       if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
  
-       WaypointSprite_PlayerDead();
+       WaypointSprite_PlayerDead(this);
  
-       if (!g_ca)  // don't reset teams when moving a ca player to the spectators
-               self.team = -1;  // move this as it is needed to log the player spectating in eventlog
+       if (mutator_returnvalue) {
+           // mutator prevents resetting teams+score
+       } else {
+               this.team = -1;  // move this as it is needed to log the player spectating in eventlog
+         this.frags = FRAGS_SPECTATOR;
+         PlayerScore_Clear(this);  // clear scores when needed
+     }
  
-       if(self.killcount != -666)
+       if (this.killcount != FRAGS_SPECTATOR)
        {
-               Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_QUIT_SPECTATE, self.netname);
-               if(autocvar_g_chat_nospectators == 1 || (cvar("g_warmup") && !(warmup_stage || gameover) && autocvar_g_chat_nospectators == 2))
-                       Send_Notification(NOTIF_ONE_ONLY, self, MSG_INFO, INFO_CHAT_NOSPECTATORS);
+               Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_QUIT_SPECTATE, this.netname);
+               if(!intermission_running)
+               if(autocvar_g_chat_nospectators == 1 || (!(warmup_stage || gameover) && autocvar_g_chat_nospectators == 2))
+                       Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS);
  
-               if(self.just_joined == false) {
-                       LogTeamchange(self.playerid, -1, 4);
+               if(this.just_joined == false) {
+                       LogTeamchange(this.playerid, -1, 4);
                } else
-                       self.just_joined = false;
-       }
-       PlayerScore_Clear(self); // clear scores when needed
-       accuracy_resend(self);
-       self.spectatortime = time;
-       self.classname = "observer";
-       self.iscreature = false;
-       self.teleportable = TELEPORT_SIMPLE;
-       self.damagedbycontents = false;
-       self.health = -666;
-       self.takedamage = DAMAGE_NO;
-       self.solid = SOLID_NOT;
-       self.movetype = MOVETYPE_FLY_WORLDONLY; // user preference is controlled by playerprethink
-       self.flags = FL_CLIENT | FL_NOTARGET;
-       self.armorvalue = 666;
-       self.effects = 0;
-       self.armorvalue = autocvar_g_balance_armor_start;
-       self.pauserotarmor_finished = 0;
-       self.pauserothealth_finished = 0;
-       self.pauseregen_finished = 0;
-       self.damageforcescale = 0;
-       self.death_time = 0;
-       self.respawn_flags = 0;
-       self.respawn_time = 0;
-       self.stat_respawn_time = 0;
-       self.alpha = 0;
-       self.scale = 0;
-       self.fade_time = 0;
-       self.pain_frame = 0;
-       self.pain_finished = 0;
-       self.strength_finished = 0;
-       self.invincible_finished = 0;
-       self.superweapons_finished = 0;
-       self.pushltime = 0;
-       self.istypefrag = 0;
-       self.think = func_null;
-       self.nextthink = 0;
-       self.hook_time = 0;
-       self.deadflag = DEAD_NO;
-       self.angles = spot.angles;
-       self.angles_z = 0;
-       self.fixangle = true;
-       self.crouch = false;
-       self.revival_time = 0;
-       setorigin (self, (spot.origin + PL_VIEW_OFS)); // offset it so that the spectator spawns higher off the ground, looks better this way
-       self.prevorigin = self.origin;
-       self.items = 0;
-       self.weapons = '0 0 0';
-       self.model = "";
-       FixPlayermodel();
-       setmodel(self, "null");
-       self.drawonlytoclient = self;
-       setsize (self, PL_CROUCH_MIN, PL_CROUCH_MAX); // give the spectator some space between walls for MOVETYPE_FLY_WORLDONLY
-       self.view_ofs = '0 0 0'; // so that your view doesn't go into the ceiling with MOVETYPE_FLY_WORLDONLY, previously "PL_VIEW_OFS"
-       self.weapon = 0;
-       self.weaponname = "";
-       self.switchingweapon = 0;
-       self.weaponmodel = "";
-       self.weaponentity = world;
-       self.exteriorweaponentity = world;
-       self.killcount = -666;
-       self.velocity = '0 0 0';
-       self.avelocity = '0 0 0';
-       self.punchangle = '0 0 0';
-       self.punchvector = '0 0 0';
-       self.oldvelocity = self.velocity;
-       self.fire_endtime = -1;
-       self.event_damage = func_null;
+                       this.just_joined = false;
+       }
+       accuracy_resend(this);
+       this.spectatortime = time;
+       this.bot_attack = false;
+     this.hud = HUD_NORMAL;
+       TRANSMUTE(Observer, this);
+       this.iscreature = false;
+       this.teleportable = TELEPORT_SIMPLE;
+       this.damagedbycontents = false;
+       this.health = FRAGS_SPECTATOR;
+       this.takedamage = DAMAGE_NO;
+       this.solid = SOLID_NOT;
+       this.movetype = MOVETYPE_FLY_WORLDONLY; // user preference is controlled by playerprethink
+       this.flags = FL_CLIENT | FL_NOTARGET;
+       this.armorvalue = 666;
+       this.effects = 0;
+       this.armorvalue = autocvar_g_balance_armor_start;
+       this.pauserotarmor_finished = 0;
+       this.pauserothealth_finished = 0;
+       this.pauseregen_finished = 0;
+       this.damageforcescale = 0;
+       this.death_time = 0;
+       this.respawn_flags = 0;
+       this.respawn_time = 0;
+       this.stat_respawn_time = 0;
+       this.alpha = 0;
+       this.scale = 0;
+       this.fade_time = 0;
+       this.pain_frame = 0;
+       this.pain_finished = 0;
+       this.strength_finished = 0;
+       this.invincible_finished = 0;
+       this.superweapons_finished = 0;
+       this.pushltime = 0;
+       this.istypefrag = 0;
+       setthink(this, func_null);
+       this.nextthink = 0;
+       this.hook_time = 0;
+       this.deadflag = DEAD_NO;
+       this.crouch = false;
+       this.revival_time = 0;
+       this.items = 0;
+       this.weapons = '0 0 0';
+       this.drawonlytoclient = this;
+       this.weaponname = "";
+       this.weaponmodel = "";
+       for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+       {
+               this.weaponentities[slot] = NULL;
+       }
+       this.exteriorweaponentity = NULL;
+       this.killcount = FRAGS_SPECTATOR;
+       this.velocity = '0 0 0';
+       this.avelocity = '0 0 0';
+       this.punchangle = '0 0 0';
+       this.punchvector = '0 0 0';
+       this.oldvelocity = this.velocity;
+       this.fire_endtime = -1;
+       this.event_damage = func_null;
  }
  
- .float model_randomizer;
- void FixPlayermodel()
+ int player_getspecies(entity this)
  {
-       string defaultmodel;
-       float defaultskin, chmdl, oldskin, n, i;
-       vector m1, m2;
-       defaultmodel = "";
-       defaultskin = 0;
-       chmdl = false;
+       get_model_parameters(this.model, this.skin);
+       int s = get_model_parameters_species;
+       get_model_parameters(string_null, 0);
+       if (s < 0) return SPECIES_HUMAN;
+       return s;
+ }
  
-       if(autocvar_sv_defaultcharacter == 1)
+ .float model_randomizer;
+ void FixPlayermodel(entity player)
+ {
+       string defaultmodel = "";
+       int defaultskin = 0;
+       if(autocvar_sv_defaultcharacter)
        {
                if(teamplay)
                {
@@@ -1310,49 -1173,29 +1207,31 @@@ Called when a client disconnects from t
  */
  .entity chatbubbleentity;
  void ReadyCount();
- void ClientDisconnect (void)
- {
-       if(self.vehicle)
-           vehicles_exit(VHEF_RELESE);
-       if (!IS_CLIENT(self))
-       {
-               print("Warning: ClientDisconnect without ClientConnect\n");
-               return;
-       }
-       PlayerStats_GameReport_FinalizePlayer(self);
-       SetSpectator(self, world);
-       if(IS_PLAYER(self)) { pointparticles(particleeffectnum("spawn_event_neutral"), self.origin, '0 0 0', 1); }
+ void ClientDisconnect()
+ {ENGINE_EVENT();
+       assert(IS_CLIENT(this), return);
  
-       CheatShutdownClient();
+       PlayerStats_GameReport_FinalizePlayer(this);
+       if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
+       if (this.active_minigame) part_minigame(this);
+       if (IS_PLAYER(this)) Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
  
-       W_HitPlotClose(self);
+       if (autocvar_sv_eventlog)
+               GameLogEcho(strcat(":part:", ftos(this.playerid)));
  
-       anticheat_report();
-       anticheat_shutdown();
+       Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_QUIT_DISCONNECT, this.netname);
  
-       playerdemo_shutdown();
++      SetSpectatee(this, NULL);
 +
-       bot_clientdisconnect();
+     MUTATOR_CALLHOOK(ClientDisconnect, this);
  
-       if(self.entcs)
-               detach_entcs();
+       ClientState_detach(this);
  
-       if(autocvar_sv_eventlog)
-               GameLogEcho(strcat(":part:", ftos(self.playerid)));
-       Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_QUIT_DISCONNECT, self.netname);
-       MUTATOR_CALLHOOK(ClientDisconnect);
+       Portal_ClearAll(this);
  
-       Portal_ClearAll(self);
+       Unfreeze(this);
  
-       Unfreeze(self);
-       RemoveGrapplingHook(self);
+       RemoveGrapplingHook(this);
  
        // Here, everything has been done that requires this player to be a client.
  
@@@ -1839,38 -1649,35 +1685,37 @@@ void SpectateCopy(entity this, entity s
      }
  }
  
float SpectateUpdate()
bool SpectateUpdate(entity this)
  {
-       if(!self.enemy)
-           return 0;
+       if(!this.enemy)
+           return false;
  
-       if(!IS_PLAYER(self.enemy) || self == self.enemy)
+       if(!IS_PLAYER(this.enemy) || this == this.enemy)
        {
-               SetSpectator(self, world);
-               return 0;
+               SetSpectatee(this, NULL);
+               return false;
        }
  
-       SpectateCopy(self.enemy);
+       SpectateCopy(this, this.enemy);
  
-       return 1;
+       return true;
  }
  
float SpectateSet()
bool SpectateSet(entity this)
  {
-       if(!IS_PLAYER(self.enemy))
+       if(!IS_PLAYER(this.enemy))
                return false;
  
-       ClientData_Touch(self.enemy);
++      ClientData_Touch(this.enemy);
 +
-       msg_entity = self;
+       msg_entity = this;
        WriteByte(MSG_ONE, SVC_SETVIEW);
-       WriteEntity(MSG_ONE, self.enemy);
-       //stuffcmd(self, "set viewsize $tmpviewsize \n");
-       self.movetype = MOVETYPE_NONE;
-       accuracy_resend(self);
+       WriteEntity(MSG_ONE, this.enemy);
+       this.movetype = MOVETYPE_NONE;
+       accuracy_resend(this);
  
-       if(!SpectateUpdate())
-               PutObserverInServer();
+       if(!SpectateUpdate(this))
+               PutObserverInServer(this);
  
        return true;
  }
@@@ -1884,68 -1691,37 +1729,40 @@@ void SetSpectatee(entity this, entity s
        // WEAPONTODO
        // 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(player.enemy && player.enemy.arc_beam) { player.enemy.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); }
  }
  
- float Spectate(entity pl)
- {
-       if(g_ca && !autocvar_g_ca_spectate_enemies && self.caplayer)
-       if(DIFF_TEAM(pl, self))
-               return 0;
-       SetSpectator(self, pl);
-       return SpectateSet();
- }
- // Returns next available player to spectate if g_ca_spectate_enemies == 0
- entity CA_SpectateNext(entity start)
+ bool Spectate(entity this, entity pl)
  {
-       if(SAME_TEAM(start, self)) { return start; }
-       other = start;
-       // continue from current player
-       while(other && DIFF_TEAM(other, self))
-               other = find(other, classname, "player");
-       if (!other)
-       {
-               // restart from begining
-               other = find(other, classname, "player");
-               while(other && DIFF_TEAM(other, self))
-                       other = find(other, classname, "player");
-       }
+       if(MUTATOR_CALLHOOK(SpectateSet, this, pl))
+               return false;
+       pl = M_ARGV(1, entity);
  
-       return other;
+       SetSpectatee(this, pl);
+       return SpectateSet(this);
  }
  
float SpectateNext()
bool SpectateNext(entity this)
  {
-       other = find(self.enemy, classname, "player");
+       other = find(this.enemy, classname, STR_PLAYER);
  
-       if (g_ca && !autocvar_g_ca_spectate_enemies && self.caplayer)
-       {
-               // CA and ca players when spectating enemies is forbidden
-               other = CA_SpectateNext(other);
-       }
-       else
-       {
-               // other modes and ca spectators or spectating enemies is allowed
-               if (!other)
-                       other = find(other, classname, "player");
-       }
+       if (MUTATOR_CALLHOOK(SpectateNext, this, other))
+               other = M_ARGV(1, entity);
+       else if (!other)
+               other = find(other, classname, STR_PLAYER);
  
-       if(other) { SetSpectator(self, other); }
+       if(other) { SetSpectatee(this, other); }
  
-       return SpectateSet();
+       return SpectateSet(this);
  }
  
float SpectatePrev()
bool SpectatePrev(entity this)
  {
        // NOTE: chain order is from the highest to the lower entnum (unlike find)
-       other = findchain(classname, "player");
+       other = findchain(classname, STR_PLAYER);
        if (!other) // no player
                return false;
  
@@@ -2006,33 -1780,30 +1821,32 @@@ void ShowRespawnCountdown(entity this
        }
  }
  
- void LeaveSpectatorMode()
+ void LeaveSpectatorMode(entity this)
  {
-       if(self.caplayer)
+       if(this.caplayer)
                return;
-       if(nJoinAllowed(self))
+       if(nJoinAllowed(this, this))
        {
-               if(!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || (self.wasplayer && autocvar_g_changeteam_banned) || self.team_forced > 0)
+               if(!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || (this.wasplayer && autocvar_g_changeteam_banned) || this.team_forced > 0)
                {
-                       self.classname = "player";
-                       nades_RemoveBonus(self);
+                       TRANSMUTE(Player, this);
  
-                       SetSpectator(self, world);
++                      SetSpectatee(self, world);
 +
                        if(autocvar_g_campaign || autocvar_g_balance_teams)
-                               { JoinBestTeam(self, false, true); }
+                               { JoinBestTeam(this, false, true); }
  
                        if(autocvar_g_campaign)
-                               { campaign_bots_may_start = 1; }
+                               { campaign_bots_may_start = true; }
  
-                       Kill_Notification(NOTIF_ONE_ONLY, self, MSG_CENTER_CPID, CPID_PREVENT_JOIN);
+                       Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
  
-                       PutClientInServer();
+                       WITHSELF(this, PutClientInServer());
  
-                       if(IS_PLAYER(self)) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_JOIN_PLAY, self.netname); }
+                       if(IS_PLAYER(this)) { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((teamplay && this.team != -1) ? APP_TEAM_ENT(this, INFO_JOIN_PLAY_TEAM) : INFO_JOIN_PLAY), this.netname); }
                }
                else
-                       stuffcmd(self, "menu_showteamselect\n");
+                       stuffcmd(this, "menu_showteamselect\n");
        }
        else
        {