]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/client.qc
Merge branch 'bones_was_here/monsterstuff' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / client.qc
1 #include "client.qh"
2
3 #include <common/csqcmodel_settings.qh>
4 #include <common/deathtypes/all.qh>
5 #include <common/debug.qh>
6 #include <common/effects/all.qh>
7 #include <common/effects/qc/globalsound.qh>
8 #include <common/ent_cs.qh>
9 #include <common/gamemodes/_mod.qh>
10 #include <common/gamemodes/gamemode/nexball/sv_nexball.qh>
11 #include <common/items/_mod.qh>
12 #include <common/items/inventory.qh>
13 #include <common/mapobjects/func/conveyor.qh>
14 #include <common/mapobjects/func/ladder.qh>
15 #include <common/mapobjects/subs.qh>
16 #include <common/mapobjects/target/spawnpoint.qh>
17 #include <common/mapobjects/teleporters.qh>
18 #include <common/mapobjects/trigger/counter.qh>
19 #include <common/mapobjects/trigger/secret.qh>
20 #include <common/mapobjects/trigger/swamp.qh>
21 #include <common/mapobjects/triggers.qh>
22 #include <common/minigames/sv_minigames.qh>
23 #include <common/monsters/sv_monsters.qh>
24 #include <common/mutators/mutator/instagib/sv_instagib.qh>
25 #include <common/mutators/mutator/nades/nades.qh>
26 #include <common/mutators/mutator/overkill/oknex.qh>
27 #include <common/mutators/mutator/status_effects/_mod.qh>
28 #include <common/mutators/mutator/waypoints/all.qh>
29 #include <common/net_linked.qh>
30 #include <common/net_notice.qh>
31 #include <common/notifications/all.qh>
32 #include <common/physics/player.qh>
33 #include <common/playerstats.qh>
34 #include <common/resources/sv_resources.qh>
35 #include <common/state.qh>
36 #include <common/stats.qh>
37 #include <common/vehicles/all.qh>
38 #include <common/vehicles/sv_vehicles.qh>
39 #include <common/viewloc.qh>
40 #include <common/weapons/_all.qh>
41 #include <common/weapons/weapon/vortex.qh>
42 #include <common/wepent.qh>
43 #include <lib/csqcmodel/sv_model.qh>
44 #include <lib/warpzone/common.qh>
45 #include <lib/warpzone/server.qh>
46 #include <server/anticheat.qh>
47 #include <server/antilag.qh>
48 #include <server/bot/api.qh>
49 #include <server/bot/default/cvars.qh>
50 #include <server/bot/default/waypoints.qh>
51 #include <server/campaign.qh>
52 #include <server/chat.qh>
53 #include <server/cheats.qh>
54 #include <server/clientkill.qh>
55 #include <server/command/banning.qh>
56 #include <server/command/cmd.qh>
57 #include <server/command/common.qh>
58 #include <server/command/vote.qh>
59 #include <server/compat/quake3.qh>
60 #include <server/damage.qh>
61 #include <server/gamelog.qh>
62 #include <server/handicap.qh>
63 #include <server/hook.qh>
64 #include <server/impulse.qh>
65 #include <server/intermission.qh>
66 #include <server/ipban.qh>
67 #include <server/main.qh>
68 #include <server/mutators/_mod.qh>
69 #include <server/player.qh>
70 #include <server/portals.qh>
71 #include <server/race.qh>
72 #include <server/scores.qh>
73 #include <server/scores_rules.qh>
74 #include <server/spawnpoints.qh>
75 #include <server/teamplay.qh>
76 #include <server/weapons/accuracy.qh>
77 #include <server/weapons/common.qh>
78 #include <server/weapons/hitplot.qh>
79 #include <server/weapons/selection.qh>
80 #include <server/weapons/tracing.qh>
81 #include <server/weapons/weaponsystem.qh>
82 #include <server/world.qh>
83
84 STATIC_METHOD(Client, Add, void(Client this, int _team))
85 {
86     ClientConnect(this);
87     TRANSMUTE(Player, this);
88     this.frame = 12; // 7
89     this.team = _team;
90     PutClientInServer(this);
91 }
92
93 STATIC_METHOD(Client, Remove, void(Client this))
94 {
95     TRANSMUTE(Observer, this);
96     PutClientInServer(this);
97     ClientDisconnect(this);
98 }
99
100 void send_CSQC_teamnagger() {
101         WriteHeader(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
102 }
103
104 int CountSpectators(entity player, entity to)
105 {
106         if(!player) { return 0; } // not sure how, but best to be safe
107
108         int spec_count = 0;
109
110         FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
111         {
112                 spec_count++;
113         });
114
115         return spec_count;
116 }
117
118 void WriteSpectators(entity player, entity to)
119 {
120         if(!player) { return; } // not sure how, but best to be safe
121
122         int spec_count = 0;
123         FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
124         {
125                 if(spec_count >= MAX_SPECTATORS)
126                         break;
127                 WriteByte(MSG_ENTITY, num_for_edict(it));
128                 ++spec_count;
129         });
130 }
131
132 bool ClientData_Send(entity this, entity to, int sf)
133 {
134         assert(to == this.owner, return false);
135
136         entity e = to;
137         if (IS_SPEC(e)) e = e.enemy;
138
139         sf = 0;
140         if (CS(e).race_completed)       sf |= BIT(0); // forced scoreboard
141         if (CS(to).spectatee_status)    sf |= BIT(1); // spectator ent number follows
142         if (CS(e).zoomstate)            sf |= BIT(2); // zoomed
143         if (observe_blocked_if_eliminated && INGAME(to))
144                                         sf |= BIT(3); // observing blocked
145         if (autocvar_sv_showspectators == 1 || (autocvar_sv_showspectators && IS_SPEC(to)))
146                                         sf |= BIT(4); // show spectators
147
148         WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
149         WriteByte(MSG_ENTITY, sf);
150
151         if (sf & BIT(1))
152                 WriteByte(MSG_ENTITY, CS(to).spectatee_status);
153
154         if(sf & BIT(4))
155         {
156                 float specs = CountSpectators(e, to);
157                 WriteByte(MSG_ENTITY, specs);
158                 WriteSpectators(e, to);
159         }
160
161         return true;
162 }
163
164 void ClientData_Attach(entity this)
165 {
166         Net_LinkEntity(CS(this).clientdata = new_pure(clientdata), false, 0, ClientData_Send);
167         CS(this).clientdata.drawonlytoclient = this;
168         CS(this).clientdata.owner = this;
169 }
170
171 void ClientData_Detach(entity this)
172 {
173         delete(CS(this).clientdata);
174         CS(this).clientdata = NULL;
175 }
176
177 void ClientData_Touch(entity e)
178 {
179         entity cd = CS(e).clientdata;
180         if (cd) { cd.SendFlags = 1; }
181
182         // make it spectatable
183         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != e && IS_SPEC(it) && it.enemy == e,
184         {
185                 entity cd = CS(it).clientdata;
186                 if (cd) { cd.SendFlags = 1; }
187         });
188 }
189
190
191 /*
192 =============
193 CheckPlayerModel
194
195 Checks if the argument string can be a valid playermodel.
196 Returns a valid one in doubt.
197 =============
198 */
199 string FallbackPlayerModel;
200 string CheckPlayerModel(string plyermodel) {
201         if(FallbackPlayerModel != cvar_defstring("_cl_playermodel"))
202         {
203                 // note: we cannot summon Don Strunzone here, some player may
204                 // still have the model string set. In case anyone manages how
205                 // to change a cvar default, we'll have a small leak here.
206                 FallbackPlayerModel = strzone(cvar_defstring("_cl_playermodel"));
207         }
208         // only in right path
209         if(substring(plyermodel, 0, 14) != "models/player/")
210                 return FallbackPlayerModel;
211         // only good file extensions
212         if(substring(plyermodel, -4, 4) != ".iqm"
213                 && substring(plyermodel, -4, 4) != ".zym"
214                 && substring(plyermodel, -4, 4) != ".dpm"
215                 && substring(plyermodel, -4, 4) != ".md3"
216                 && substring(plyermodel, -4, 4) != ".psk")
217         {
218                 return FallbackPlayerModel;
219         }
220         // forbid the LOD models
221         if(substring(plyermodel, -9, 5) == "_lod1" || substring(plyermodel, -9, 5) == "_lod2")
222                 return FallbackPlayerModel;
223         if(plyermodel != strtolower(plyermodel))
224                 return FallbackPlayerModel;
225         // also, restrict to server models
226         if(autocvar_sv_servermodelsonly)
227         {
228                 if(!fexists(plyermodel))
229                         return FallbackPlayerModel;
230         }
231         return plyermodel;
232 }
233
234 void setplayermodel(entity e, string modelname)
235 {
236         precache_model(modelname);
237         _setmodel(e, modelname);
238         player_setupanimsformodel(e);
239         if(!autocvar_g_debug_globalsounds)
240                 UpdatePlayerSounds(e);
241 }
242
243 /** putting a client as observer in the server */
244 void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
245 {
246         bool mutator_returnvalue = MUTATOR_CALLHOOK(MakePlayerObserver, this, is_forced);
247         bool recount_ready = false;
248         PlayerState_detach(this);
249
250         if (IS_PLAYER(this))
251         {
252                 if(GetResource(this, RES_HEALTH) >= 1)
253                 {
254                         // despawn effect
255                         Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
256                 }
257
258                 // was a player, recount votes and ready status
259                 if(IS_REAL_CLIENT(this))
260                 {
261                         if (vote_called) { VoteCount(false); }
262                         this.ready = false;
263                         if (warmup_stage || game_starttime > time) recount_ready = true;
264                 }
265                 entcs_update_players(this);
266         }
267
268         if (use_spawnpoint)
269         {
270                 entity spot = SelectSpawnPoint(this, true);
271                 if (!spot) LOG_FATAL("No spawnpoints for observers?!?");
272                 this.angles = vec2(spot.angles);
273                 // offset it so that the spectator spawns higher off the ground, looks better this way
274                 setorigin(this, spot.origin + STAT(PL_VIEW_OFS, this));
275         }
276         else // change origin to restore previous view origin
277                 setorigin(this, this.origin + STAT(PL_VIEW_OFS, this) - STAT(PL_CROUCH_VIEW_OFS, this));
278         this.fixangle = true;
279
280         if (IS_REAL_CLIENT(this))
281         {
282                 msg_entity = this;
283                 WriteByte(MSG_ONE, SVC_SETVIEW);
284                 WriteEntity(MSG_ONE, this);
285         }
286         // give the spectator some space between walls for MOVETYPE_FLY_WORLDONLY
287         // so that your view doesn't go into the ceiling with MOVETYPE_FLY_WORLDONLY, previously "PL_VIEW_OFS"
288         if(!autocvar_g_debug_globalsounds)
289         {
290                 // needed for player sounds
291                 this.model = "";
292                 FixPlayermodel(this);
293         }
294         setmodel(this, MDL_Null);
295         setsize(this, STAT(PL_CROUCH_MIN, this), STAT(PL_CROUCH_MAX, this));
296         this.view_ofs = '0 0 0';
297
298         RemoveGrapplingHooks(this);
299         Portal_ClearAll(this);
300         Unfreeze(this, false);
301         SetSpectatee(this, NULL);
302
303         if (this.alivetime)
304         {
305                 if (!warmup_stage)
306                         PlayerStats_GameReport_Event_Player(this, PLAYERSTATS_ALIVETIME, time - this.alivetime);
307                 this.alivetime = 0;
308         }
309
310         if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
311
312         TRANSMUTE(Observer, this);
313
314         if(recount_ready) ReadyCount(); // FIXME: please add comment about why this is delayed
315
316         WaypointSprite_PlayerDead(this);
317         accuracy_resend(this);
318
319         if (CS(this).killcount != FRAGS_SPECTATOR && !game_stopped && CHAT_NOSPECTATORS())
320                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS);
321
322         CS(this).spectatortime = time;
323         if(this.bot_attack)
324                 IL_REMOVE(g_bot_targets, this);
325         this.bot_attack = false;
326         if(this.monster_attack)
327                 IL_REMOVE(g_monster_targets, this);
328         this.monster_attack = false;
329         STAT(HUD, this) = HUD_NORMAL;
330         this.iscreature = false;
331         this.teleportable = TELEPORT_SIMPLE;
332         if(this.damagedbycontents)
333                 IL_REMOVE(g_damagedbycontents, this);
334         this.damagedbycontents = false;
335         SetResourceExplicit(this, RES_HEALTH, FRAGS_SPECTATOR);
336         SetSpectatee_status(this, etof(this));
337         this.takedamage = DAMAGE_NO;
338         this.solid = SOLID_NOT;
339         set_movetype(this, MOVETYPE_FLY_WORLDONLY); // user preference is controlled by playerprethink
340         this.flags = FL_CLIENT | FL_NOTARGET;
341         this.effects = 0;
342         SetResourceExplicit(this, RES_ARMOR, autocvar_g_balance_armor_start); // was 666?!
343         this.pauserotarmor_finished = 0;
344         this.pauserothealth_finished = 0;
345         this.pauseregen_finished = 0;
346         this.damageforcescale = 0;
347         this.death_time = 0;
348         this.respawn_flags = 0;
349         this.respawn_time = 0;
350         STAT(RESPAWN_TIME, this) = 0;
351         this.alpha = 0;
352         this.scale = 0;
353         this.fade_time = 0;
354         this.pain_finished = 0;
355         STAT(AIR_FINISHED, this) = 0;
356         //this.dphitcontentsmask = 0;
357         this.dphitcontentsmask = DPCONTENTS_SOLID;
358         if (autocvar_g_playerclip_collisions)
359                 this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP;
360         this.pushltime = 0;
361         this.istypefrag = 0;
362         setthink(this, func_null);
363         this.nextthink = 0;
364         this.deadflag = DEAD_NO;
365         UNSET_DUCKED(this);
366         STAT(REVIVE_PROGRESS, this) = 0;
367         this.revival_time = 0;
368         this.draggable = drag_undraggable;
369
370         player_powerups_remove_all(this);
371         this.items = 0;
372         STAT(WEAPONS, this) = '0 0 0';
373         this.drawonlytoclient = this;
374
375         this.viewloc = NULL;
376
377         //this.spawnpoint_targ = NULL; // keep it so they can return to where they were?
378
379         this.weaponmodel = "";
380         for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
381         {
382                 this.weaponentities[slot] = NULL;
383         }
384         this.exteriorweaponentity = NULL;
385         CS(this).killcount = FRAGS_SPECTATOR;
386         this.velocity = '0 0 0';
387         this.avelocity = '0 0 0';
388         this.punchangle = '0 0 0';
389         this.punchvector = '0 0 0';
390         this.oldvelocity = this.velocity;
391         this.event_damage = func_null;
392         this.event_heal = func_null;
393
394         for(int slot = 0; slot < MAX_AXH; ++slot)
395         {
396                 entity axh = this.(AuxiliaryXhair[slot]);
397                 this.(AuxiliaryXhair[slot]) = NULL;
398
399                 if(axh.owner == this && axh != NULL && !wasfreed(axh))
400                         delete(axh);
401         }
402
403         if (mutator_returnvalue)
404         {
405                 // mutator prevents resetting teams+score
406         }
407         else
408         {
409                 SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR); // clears scores too in game modes without teams
410                 this.frags = FRAGS_SPECTATOR;
411         }
412
413         bot_relinkplayerlist();
414
415         if (CS(this).just_joined)
416                 CS(this).just_joined = false;
417 }
418
419 int player_getspecies(entity this)
420 {
421         get_model_parameters(this.model, this.skin);
422         int s = get_model_parameters_species;
423         get_model_parameters(string_null, 0);
424         if (s < 0) return SPECIES_HUMAN;
425         return s;
426 }
427
428 .float model_randomizer;
429 void FixPlayermodel(entity player)
430 {
431         string defaultmodel = "";
432         int defaultskin = 0;
433         if(autocvar_sv_defaultcharacter)
434         {
435                 if(teamplay)
436                 {
437                         switch(player.team)
438                         {
439                                 case NUM_TEAM_1: defaultmodel = autocvar_sv_defaultplayermodel_red; defaultskin = autocvar_sv_defaultplayerskin_red; break;
440                                 case NUM_TEAM_2: defaultmodel = autocvar_sv_defaultplayermodel_blue; defaultskin = autocvar_sv_defaultplayerskin_blue; break;
441                                 case NUM_TEAM_3: defaultmodel = autocvar_sv_defaultplayermodel_yellow; defaultskin = autocvar_sv_defaultplayerskin_yellow; break;
442                                 case NUM_TEAM_4: defaultmodel = autocvar_sv_defaultplayermodel_pink; defaultskin = autocvar_sv_defaultplayerskin_pink; break;
443                         }
444                 }
445
446                 if(defaultmodel == "")
447                 {
448                         defaultmodel = autocvar_sv_defaultplayermodel;
449                         defaultskin = autocvar_sv_defaultplayerskin;
450                 }
451
452                 int n = tokenize_console(defaultmodel);
453                 if(n > 0)
454                 {
455                         defaultmodel = argv(floor(n * CS(player).model_randomizer));
456                         // However, do NOT randomize if the player-selected model is in the list.
457                         for (int i = 0; i < n; ++i)
458                                 if ((argv(i) == player.playermodel && defaultskin == stof(player.playerskin)) || argv(i) == strcat(player.playermodel, ":", player.playerskin))
459                                         defaultmodel = argv(i);
460                 }
461
462                 int i = strstrofs(defaultmodel, ":", 0);
463                 if(i >= 0)
464                 {
465                         defaultskin = stof(substring(defaultmodel, i+1, -1));
466                         defaultmodel = substring(defaultmodel, 0, i);
467                 }
468         }
469         if(autocvar_sv_defaultcharacterskin && !defaultskin)
470         {
471                 if(teamplay)
472                 {
473                         switch(player.team)
474                         {
475                                 case NUM_TEAM_1: defaultskin = autocvar_sv_defaultplayerskin_red; break;
476                                 case NUM_TEAM_2: defaultskin = autocvar_sv_defaultplayerskin_blue; break;
477                                 case NUM_TEAM_3: defaultskin = autocvar_sv_defaultplayerskin_yellow; break;
478                                 case NUM_TEAM_4: defaultskin = autocvar_sv_defaultplayerskin_pink; break;
479                         }
480                 }
481
482                 if(!defaultskin)
483                         defaultskin = autocvar_sv_defaultplayerskin;
484         }
485
486         MUTATOR_CALLHOOK(FixPlayermodel, defaultmodel, defaultskin, player);
487         defaultmodel = M_ARGV(0, string);
488         defaultskin = M_ARGV(1, int);
489
490         bool chmdl = false;
491         int oldskin;
492         if(defaultmodel != "")
493         {
494                 if (defaultmodel != player.model)
495                 {
496                         vector m1 = player.mins;
497                         vector m2 = player.maxs;
498                         setplayermodel (player, defaultmodel);
499                         setsize (player, m1, m2);
500                         chmdl = true;
501                 }
502
503                 oldskin = player.skin;
504                 player.skin = defaultskin;
505         } else {
506                 if (player.playermodel != player.model || player.playermodel == "")
507                 {
508                         player.playermodel = CheckPlayerModel(player.playermodel); // this is never "", so no endless loop
509                         vector m1 = player.mins;
510                         vector m2 = player.maxs;
511                         setplayermodel (player, player.playermodel);
512                         setsize (player, m1, m2);
513                         chmdl = true;
514                 }
515
516                 if(!autocvar_sv_defaultcharacterskin)
517                 {
518                         oldskin = player.skin;
519                         player.skin = stof(player.playerskin);
520                 }
521                 else
522                 {
523                         oldskin = player.skin;
524                         player.skin = defaultskin;
525                 }
526         }
527
528         if(chmdl || oldskin != player.skin) // model or skin has changed
529         {
530                 player.species = player_getspecies(player); // update species
531                 if(!autocvar_g_debug_globalsounds)
532                         UpdatePlayerSounds(player); // update skin sounds
533         }
534
535         if(!teamplay)
536                 if(strlen(autocvar_sv_defaultplayercolors))
537                         if(player.clientcolors != stof(autocvar_sv_defaultplayercolors))
538                                 setcolor(player, stof(autocvar_sv_defaultplayercolors));
539 }
540
541 void GiveWarmupResources(entity this)
542 {
543         SetResource(this, RES_SHELLS, warmup_start_ammo_shells);
544         SetResource(this, RES_BULLETS, warmup_start_ammo_nails);
545         SetResource(this, RES_ROCKETS, warmup_start_ammo_rockets);
546         SetResource(this, RES_CELLS, warmup_start_ammo_cells);
547         SetResource(this, RES_PLASMA, warmup_start_ammo_plasma);
548         SetResource(this, RES_FUEL, warmup_start_ammo_fuel);
549         SetResource(this, RES_HEALTH, warmup_start_health);
550         SetResource(this, RES_ARMOR, warmup_start_armorvalue);
551         STAT(WEAPONS, this) = WARMUP_START_WEAPONS;
552 }
553
554 void PutPlayerInServer(entity this)
555 {
556         if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
557
558         PlayerState_attach(this);
559         accuracy_resend(this);
560
561         if (teamplay && this.bot_forced_team)
562                 SetPlayerTeam(this, this.bot_forced_team, TEAM_CHANGE_MANUAL);
563
564         if (this.team < 0)
565                 TeamBalance_JoinBestTeam(this);
566
567         entity spot = SelectSpawnPoint(this, false);
568         if (!spot) {
569                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_NOSPAWNS);
570                 return; // spawn failed
571         }
572
573         TRANSMUTE(Player, this);
574
575         CS(this).wasplayer = true;
576         this.iscreature = true;
577         this.teleportable = TELEPORT_NORMAL;
578         if(!this.damagedbycontents)
579                 IL_PUSH(g_damagedbycontents, this);
580         this.damagedbycontents = true;
581         set_movetype(this, MOVETYPE_WALK);
582         this.solid = SOLID_SLIDEBOX;
583         this.dphitcontentsmask = DPCONTENTS_BODY | DPCONTENTS_SOLID;
584         if (autocvar_g_playerclip_collisions)
585                 this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP;
586         if (IS_BOT_CLIENT(this) && autocvar_g_botclip_collisions)
587                 this.dphitcontentsmask |= DPCONTENTS_BOTCLIP;
588         this.frags = FRAGS_PLAYER;
589         if (INDEPENDENT_PLAYERS) MAKE_INDEPENDENT_PLAYER(this);
590         this.flags = FL_CLIENT | FL_PICKUPITEMS;
591         if (autocvar__notarget)
592                 this.flags |= FL_NOTARGET;
593         this.takedamage = DAMAGE_AIM;
594         this.effects = EF_TELEPORT_BIT | EF_RESTARTANIM_BIT;
595
596         if (warmup_stage)
597                 GiveWarmupResources(this);
598         else
599         {
600                 SetResource(this, RES_SHELLS, start_ammo_shells);
601                 SetResource(this, RES_BULLETS, start_ammo_nails);
602                 SetResource(this, RES_ROCKETS, start_ammo_rockets);
603                 SetResource(this, RES_CELLS, start_ammo_cells);
604                 SetResource(this, RES_PLASMA, start_ammo_plasma);
605                 SetResource(this, RES_FUEL, start_ammo_fuel);
606                 SetResource(this, RES_HEALTH, start_health);
607                 SetResource(this, RES_ARMOR, start_armorvalue);
608                 STAT(WEAPONS, this) = start_weapons;
609                 if (MUTATOR_CALLHOOK(ForbidRandomStartWeapons, this) == false)
610                 {
611                         GiveRandomWeapons(this, random_start_weapons_count,
612                                 autocvar_g_random_start_weapons, random_start_ammo);
613                 }
614         }
615         SetSpectatee_status(this, 0);
616
617         PS(this).dual_weapons = '0 0 0';
618
619         if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
620                 StatusEffects_apply(STATUSEFFECT_Superweapons, this, time + autocvar_g_balance_superweapons_time, 0);
621
622         this.items = start_items;
623
624         float shieldtime = time + autocvar_g_spawnshieldtime;
625
626         this.pauserotarmor_finished = time + autocvar_g_balance_pause_armor_rot_spawn;
627         this.pauserothealth_finished = time + autocvar_g_balance_pause_health_rot_spawn;
628         this.pauserotfuel_finished = time + autocvar_g_balance_pause_fuel_rot_spawn;
629         this.pauseregen_finished = time + autocvar_g_balance_pause_health_regen_spawn;
630         if (!sv_ready_restart_after_countdown && time < game_starttime)
631         {
632                 float f = game_starttime - time;
633                 shieldtime += f;
634                 this.pauserotarmor_finished += f;
635                 this.pauserothealth_finished += f;
636                 this.pauseregen_finished += f;
637         }
638
639         StatusEffects_apply(STATUSEFFECT_SpawnShield, this, shieldtime, 0);
640
641         this.damageforcescale = autocvar_g_player_damageforcescale;
642         this.death_time = 0;
643         this.respawn_flags = 0;
644         this.respawn_time = 0;
645         STAT(RESPAWN_TIME, this) = 0;
646         this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) || !autocvar_sv_mapformat_is_quake3)
647                 ? 0.8125 // DP model scaling uses 1/16 accuracy and 13/16 is closest to 56/69
648                 : autocvar_sv_player_scale;
649         this.fade_time = 0;
650         this.pain_finished = 0;
651         this.pushltime = 0;
652         setthink(this, func_null); // players have no think function
653         this.nextthink = 0;
654         this.dmg_team = 0;
655         PS(this).ballistics_density = autocvar_g_ballistics_density_player;
656
657         this.deadflag = DEAD_NO;
658
659         this.angles = spot.angles;
660         this.angles_z = 0; // never spawn tilted even if the spot says to
661         if (IS_BOT_CLIENT(this))
662         {
663                 this.v_angle = this.angles;
664                 bot_aim_reset(this);
665         }
666         this.fixangle = true; // turn this way immediately
667         this.oldvelocity = this.velocity = '0 0 0';
668         this.avelocity = '0 0 0';
669         this.punchangle = '0 0 0';
670         this.punchvector = '0 0 0';
671
672         STAT(REVIVE_PROGRESS, this) = 0;
673         this.revival_time = 0;
674
675         STAT(AIR_FINISHED, this) = 0;
676         this.waterlevel = WATERLEVEL_NONE;
677         this.watertype = CONTENT_EMPTY;
678
679         entity spawnevent = new_pure(spawnevent);
680         spawnevent.owner = this;
681         Net_LinkEntity(spawnevent, false, 0.5, SpawnEvent_Send);
682
683         // Cut off any still running player sounds.
684         stopsound(this, CH_PLAYER_SINGLE);
685
686         this.model = "";
687         FixPlayermodel(this);
688         this.drawonlytoclient = NULL;
689
690         this.viewloc = NULL;
691
692         for(int slot = 0; slot < MAX_AXH; ++slot)
693         {
694                 entity axh = this.(AuxiliaryXhair[slot]);
695                 this.(AuxiliaryXhair[slot]) = NULL;
696
697                 if(axh.owner == this && axh != NULL && !wasfreed(axh))
698                         delete(axh);
699         }
700
701         this.spawnpoint_targ = NULL;
702
703         UNSET_DUCKED(this);
704         this.view_ofs = STAT(PL_VIEW_OFS, this);
705         setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
706         this.spawnorigin = spot.origin;
707         setorigin(this, spot.origin + '0 0 1' * (1 - this.mins.z - 24));
708         // don't reset back to last position, even if new position is stuck in solid
709         this.oldorigin = this.origin;
710         if(this.conveyor)
711                 IL_REMOVE(g_conveyed, this);
712         this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player
713         if(this.swampslug)
714                 IL_REMOVE(g_swamped, this);
715         this.swampslug = NULL;
716         this.swamp_interval = 0;
717         if(this.ladder_entity)
718                 IL_REMOVE(g_ladderents, this);
719         this.ladder_entity = NULL;
720         IL_EACH(g_counters, it.realowner == this,
721         {
722                 delete(it);
723         });
724         STAT(HUD, this) = HUD_NORMAL;
725
726         this.event_damage = PlayerDamage;
727         this.event_heal = PlayerHeal;
728
729         this.draggable = func_null;
730
731         if(!this.bot_attack)
732                 IL_PUSH(g_bot_targets, this);
733         this.bot_attack = true;
734         if(!this.monster_attack)
735                 IL_PUSH(g_monster_targets, this);
736         this.monster_attack = true;
737         navigation_dynamicgoal_init(this, false);
738
739         PHYS_INPUT_BUTTON_ATCK(this) = PHYS_INPUT_BUTTON_JUMP(this) = PHYS_INPUT_BUTTON_ATCK2(this) = false;
740
741         // player was spectator
742         if (CS(this).killcount == FRAGS_SPECTATOR) {
743                 PlayerScore_Clear(this);
744                 CS(this).killcount = 0;
745                 CS(this).startplaytime = time;
746         }
747
748         for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
749         {
750                 .entity weaponentity = weaponentities[slot];
751                 CL_SpawnWeaponentity(this, weaponentity);
752         }
753         this.alpha = default_player_alpha;
754         this.colormod = '1 1 1' * autocvar_g_player_brightness;
755         this.exteriorweaponentity.alpha = default_weapon_alpha;
756
757         this.speedrunning = false;
758
759         this.counter_cnt = 0;
760         this.fragsfilter_cnt = 0;
761
762         target_voicescript_clear(this);
763
764         // reset fields the weapons may use
765         FOREACH(Weapons, true, {
766                 it.wr_resetplayer(it, this);
767                         // reload all reloadable weapons
768                 if (it.spawnflags & WEP_FLAG_RELOADABLE) {
769                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
770                         {
771                                 .entity weaponentity = weaponentities[slot];
772                                 this.(weaponentity).weapon_load[it.m_id] = it.reloading_ammo;
773                         }
774                 }
775         });
776
777         Unfreeze(this, false);
778
779         MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
780         {
781                 string s = spot.target;
782                 if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack
783                         spot.target = string_null;
784                 SUB_UseTargets(spot, this, NULL);
785                 if(g_assault || g_race)
786                         spot.target = s;
787         }
788
789         if (autocvar_spawn_debug)
790         {
791                 sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
792                 delete(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
793         }
794
795         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
796         {
797                 .entity weaponentity = weaponentities[slot];
798                 entity w_ent = this.(weaponentity);
799                 if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
800                         w_ent.m_switchweapon = w_getbestweapon(this, weaponentity);
801                 else
802                         w_ent.m_switchweapon = WEP_Null;
803                 w_ent.m_weapon = WEP_Null;
804                 w_ent.weaponname = "";
805                 w_ent.m_switchingweapon = WEP_Null;
806                 w_ent.cnt = -1;
807         }
808
809         MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
810
811         if (CS(this).impulse) ImpulseCommands(this);
812
813         W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
814         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
815         {
816                 .entity weaponentity = weaponentities[slot];
817                 W_WeaponFrame(this, weaponentity);
818         }
819
820         if (!warmup_stage && !this.alivetime)
821                 this.alivetime = time;
822
823         antilag_clear(this, CS(this));
824
825         if (warmup_stage < 0 || warmup_stage > 1)
826                 ReadyCount();
827 }
828
829 /** Called when a client spawns in the server */
830 void PutClientInServer(entity this)
831 {
832         if (IS_REAL_CLIENT(this)) {
833                 msg_entity = this;
834                 WriteByte(MSG_ONE, SVC_SETVIEW);
835                 WriteEntity(MSG_ONE, this);
836         }
837         if (game_stopped)
838                 TRANSMUTE(Observer, this);
839
840         bool use_spawnpoint = (!this.enemy); // check this.enemy here since SetSpectatee will clear it
841         SetSpectatee(this, NULL);
842
843         // reset player keys
844         if(PS(this))
845                 PS(this).itemkeys = 0;
846
847         MUTATOR_CALLHOOK(PutClientInServer, this);
848
849         if (IS_OBSERVER(this)) {
850                 PutObserverInServer(this, false, use_spawnpoint);
851         } else if (IS_PLAYER(this)) {
852                 PutPlayerInServer(this);
853         }
854
855         bot_relinkplayerlist();
856 }
857
858 // TODO do we need all these fields, or should we stop autodetecting runtime
859 // changes and just have a console command to update this?
860 bool ClientInit_SendEntity(entity this, entity to, int sf)
861 {
862         WriteHeader(MSG_ENTITY, _ENT_CLIENT_INIT);
863         return = true;
864         msg_entity = to;
865         // MSG_INIT replacement
866         // TODO: make easier to use
867         Registry_send_all();
868         W_PROP_reload(MSG_ONE, to);
869         ClientInit_misc(this);
870         MUTATOR_CALLHOOK(Ent_Init);
871 }
872 void ClientInit_misc(entity this)
873 {
874         int channel = MSG_ONE;
875         WriteHeader(channel, ENT_CLIENT_INIT);
876         WriteByte(channel, g_nexball_meter_period * 32);
877         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[0]));
878         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[1]));
879         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[2]));
880         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[3]));
881         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[0]));
882         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[1]));
883         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[2]));
884         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[3]));
885
886         if(autocvar_sv_foginterval && world.fog != "")
887                 WriteString(channel, world.fog);
888         else
889                 WriteString(channel, "");
890         WriteByte(channel, this.count * 255.0); // g_balance_armor_blockpercent
891         WriteByte(channel, this.cnt * 255.0); // g_balance_damagepush_speedfactor
892         WriteByte(channel, serverflags);
893         WriteCoord(channel, autocvar_g_trueaim_minrange);
894 }
895
896 void ClientInit_CheckUpdate(entity this)
897 {
898         this.nextthink = time;
899         if(this.count != autocvar_g_balance_armor_blockpercent)
900         {
901                 this.count = autocvar_g_balance_armor_blockpercent;
902                 this.SendFlags |= 1;
903         }
904         if(this.cnt != autocvar_g_balance_damagepush_speedfactor)
905         {
906                 this.cnt = autocvar_g_balance_damagepush_speedfactor;
907                 this.SendFlags |= 1;
908         }
909 }
910
911 void ClientInit_Spawn()
912 {
913         entity e = new_pure(clientinit);
914         setthink(e, ClientInit_CheckUpdate);
915         Net_LinkEntity(e, false, 0, ClientInit_SendEntity);
916
917         ClientInit_CheckUpdate(e);
918 }
919
920 /*
921 =============
922 SetNewParms
923 =============
924 */
925 void SetNewParms ()
926 {
927         // initialize parms for a new player
928         parm1 = -(86400 * 366);
929
930         MUTATOR_CALLHOOK(SetNewParms);
931 }
932
933 /*
934 =============
935 SetChangeParms
936 =============
937 */
938 void SetChangeParms (entity this)
939 {
940         // save parms for level change
941         parm1 = CS(this).parm_idlesince - time;
942
943         MUTATOR_CALLHOOK(SetChangeParms);
944 }
945
946 /*
947 =============
948 DecodeLevelParms
949 =============
950 */
951 void DecodeLevelParms(entity this)
952 {
953         // load parms
954         CS(this).parm_idlesince = parm1;
955         if (CS(this).parm_idlesince == -(86400 * 366))
956                 CS(this).parm_idlesince = time;
957
958         // whatever happens, allow 60 seconds of idling directly after connect for map loading
959         CS(this).parm_idlesince = max(CS(this).parm_idlesince, time - autocvar_sv_maxidle + 60);
960
961         MUTATOR_CALLHOOK(DecodeLevelParms);
962 }
963
964 void FixClientCvars(entity e)
965 {
966         // send prediction settings to the client
967         if(autocvar_g_antilag == 3) // client side hitscan
968                 stuffcmd(e, "cl_cmd settemp cl_prydoncursor_notrace 0\n");
969         if(autocvar_sv_gentle)
970                 stuffcmd(e, "cl_cmd settemp cl_gentle 1\n");
971
972         stuffcmd(e, sprintf("\ncl_jumpspeedcap_min \"%s\"\n", autocvar_sv_jumpspeedcap_min));
973         stuffcmd(e, sprintf("\ncl_jumpspeedcap_max \"%s\"\n", autocvar_sv_jumpspeedcap_max));
974
975         stuffcmd(e, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
976
977         MUTATOR_CALLHOOK(FixClientCvars, e);
978 }
979
980 bool findinlist_abbrev(string tofind, string list)
981 {
982         if(list == "" || tofind == "")
983                 return false; // empty list or search, just return
984
985         // this function allows abbreviated strings!
986         FOREACH_WORD(list, it != "" && it == substring(tofind, 0, strlen(it)),
987         {
988                 return true;
989         });
990
991         return false;
992 }
993
994 bool PlayerInIPList(entity p, string iplist)
995 {
996         // some safety checks (never allow local?)
997         if(p.netaddress == "local" || p.netaddress == "" || !IS_REAL_CLIENT(p))
998                 return false;
999
1000         return findinlist_abbrev(p.netaddress, iplist);
1001 }
1002
1003 bool PlayerInIDList(entity p, string idlist)
1004 {
1005         // NOTE: we do NOT check crypto_idfp_signed here, an unsigned ID is fine too for this
1006         if(!p.crypto_idfp)
1007                 return false;
1008
1009         return findinlist_abbrev(p.crypto_idfp, idlist);
1010 }
1011
1012 bool PlayerInList(entity player, string list)
1013 {
1014         if (list == "")
1015                 return false;
1016         return boolean(PlayerInIDList(player, list) || PlayerInIPList(player, list));
1017 }
1018
1019 #ifdef DP_EXT_PRECONNECT
1020 /*
1021 =============
1022 ClientPreConnect
1023
1024 Called once (not at each match start) when a client begins a connection to the server
1025 =============
1026 */
1027 void ClientPreConnect(entity this)
1028 {
1029         if(autocvar_sv_eventlog)
1030         {
1031                 GameLogEcho(sprintf(":connect:%d:%d:%s",
1032                         this.playerid,
1033                         etof(this),
1034                         ((IS_REAL_CLIENT(this)) ? this.netaddress : "bot")
1035                 ));
1036         }
1037 }
1038 #endif
1039
1040 // NOTE csqc uses the active mutators list sent by this function
1041 // to understand which mutators are enabled
1042 // also note that they aren't all registered mutators, e.g. jetpack, low gravity
1043 void SendWelcomeMessage(entity this, int msg_type)
1044 {
1045         if (boolean(autocvar_g_campaign))
1046         {
1047                 WriteByte(msg_type, 1);
1048                 WriteByte(msg_type, Campaign_GetLevelNum());
1049                 return;
1050         }
1051
1052         int flags = 0;
1053         if (CS(this).version_mismatch)
1054                 flags |= 2;
1055         if (CS(this).version < autocvar_gameversion)
1056                 flags |= 4;
1057         MapInfo_Get_ByName(mi_shortname, 0, NULL);
1058         if (MapInfo_Map_author != "")
1059                 flags |= 8;
1060         WriteByte(msg_type, flags);
1061
1062         WriteString(msg_type, autocvar_hostname);
1063         WriteString(msg_type, autocvar_g_xonoticversion);
1064
1065         WriteString(msg_type, MapInfo_Map_titlestring);
1066         if (flags & 8)
1067                 WriteString(msg_type, MapInfo_Map_author);
1068         MapInfo_ClearTemps();
1069
1070         WriteByte(msg_type, autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers);
1071         WriteByte(msg_type, GetPlayerLimit());
1072
1073         MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
1074         string modifications = M_ARGV(0, string);
1075
1076         if (!g_weaponarena && cvar("g_balance_blaster_weaponstartoverride") == 0)
1077                 modifications = strcat(modifications, ", No start weapons");
1078         if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
1079                 modifications = strcat(modifications, ", Low gravity");
1080         if(g_weapon_stay && !g_cts)
1081                 modifications = strcat(modifications, ", Weapons stay");
1082         if(autocvar_g_jetpack)
1083                 modifications = strcat(modifications, ", Jetpack");
1084         modifications = substring(modifications, 2, strlen(modifications) - 2);
1085
1086         WriteString(msg_type, modifications);
1087
1088         WriteString(msg_type, g_weaponarena_list);
1089
1090         if(cache_lastmutatormsg != autocvar_g_mutatormsg)
1091         {
1092                 strcpy(cache_lastmutatormsg, autocvar_g_mutatormsg);
1093                 strcpy(cache_mutatormsg, cache_lastmutatormsg);
1094         }
1095
1096         WriteString(msg_type, cache_mutatormsg);
1097
1098         WriteString(msg_type, strreplace("\\n", "\n", autocvar_sv_motd));
1099 }
1100
1101 /**
1102 =============
1103 ClientConnect
1104
1105 Called when a client connects to the server
1106 =============
1107 */
1108 void ClientConnect(entity this)
1109 {
1110         if (Ban_MaybeEnforceBanOnce(this)) return;
1111         assert(!IS_CLIENT(this), return);
1112         this.flags |= FL_CLIENT;
1113         assert(player_count >= 0, player_count = 0);
1114
1115         TRANSMUTE(Client, this);
1116         CS(this).version_nagtime = time + 10 + random() * 10;
1117
1118         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_CONNECT, this.netname);
1119
1120         bot_clientconnect(this);
1121
1122         Player_DetermineForcedTeam(this);
1123
1124         TRANSMUTE(Observer, this);
1125
1126         PlayerStats_GameReport_AddEvent(sprintf("kills-%d", this.playerid));
1127
1128         // always track bots, don't ask for cl_allow_uidtracking
1129         if (IS_BOT_CLIENT(this))
1130                 PlayerStats_GameReport_AddPlayer(this);
1131         else
1132                 CS(this).allowed_timeouts = autocvar_sv_timeout_number;
1133
1134         if (autocvar_sv_eventlog)
1135                 GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this.netname, this.team, false)));
1136
1137         CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
1138         this.wants_join = 0;
1139
1140         stuffcmd(this, clientstuff, "\n");
1141         stuffcmd(this, "cl_particles_reloadeffects\n"); // TODO do we still need this?
1142
1143         FixClientCvars(this);
1144
1145         // get version info from player
1146         stuffcmd(this, "cmd clientversion $gameversion\n");
1147
1148         // notify about available teams
1149         if (teamplay)
1150         {
1151                 entity balance = TeamBalance_CheckAllowedTeams(this);
1152                 int t = TeamBalance_GetAllowedTeams(balance);
1153                 TeamBalance_Destroy(balance);
1154                 stuffcmd(this, sprintf("set _teams_available %d\n", t));
1155         }
1156         else
1157         {
1158                 stuffcmd(this, "set _teams_available 0\n");
1159         }
1160
1161         bot_relinkplayerlist();
1162
1163         CS(this).spectatortime = time;
1164         if (blockSpectators)
1165         {
1166                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
1167         }
1168
1169         CS(this).jointime = time;
1170
1171         if (IS_REAL_CLIENT(this))
1172         {
1173                 if (g_weaponarena_weapons == WEPSET(TUBA))
1174                         stuffcmd(this, "cl_cmd settemp chase_active 1\n");
1175                 // quickmenu file must be put in a subfolder with an unique name
1176                 // to reduce chances of overriding custom client quickmenus
1177                 if (waypointeditor_enabled)
1178                         stuffcmd(this, sprintf("cl_cmd settemp _hud_panel_quickmenu_file_from_server %s\n", "wpeditor.txt"));
1179                 else if (autocvar_sv_quickmenu_file != "" && strstrofs(autocvar_sv_quickmenu_file, "/", 0) && fexists(autocvar_sv_quickmenu_file))
1180                         stuffcmd(this, sprintf("cl_cmd settemp _hud_panel_quickmenu_file_from_server %s\n", autocvar_sv_quickmenu_file));
1181         }
1182
1183         if (!autocvar_sv_foginterval && world.fog != "")
1184                 stuffcmd(this, strcat("\nfog ", world.fog, "\nr_fog_exp2 0\nr_drawfog 1\n"));
1185
1186         if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
1187                 if(!MUTATOR_CALLHOOK(HideTeamNagger, this))
1188                         send_CSQC_teamnagger();
1189
1190         CSQCMODEL_AUTOINIT(this);
1191
1192         CS(this).model_randomizer = random();
1193
1194         if (IS_REAL_CLIENT(this))
1195                 sv_notice_join(this);
1196
1197         this.move_qcphysics = true;
1198
1199         // update physics stats (players can spawn before physics runs)
1200         Physics_UpdateStats(this);
1201
1202         IL_EACH(g_initforplayer, it.init_for_player, {
1203                 it.init_for_player(it, this);
1204         });
1205
1206         Handicap_Initialize(this);
1207
1208         // playban
1209         if (PlayerInList(this, autocvar_g_playban_list))
1210                 TRANSMUTE(Observer, this);
1211
1212         if (PlayerInList(this, autocvar_g_chatban_list)) // chatban
1213                 CS(this).muted = true;
1214
1215         MUTATOR_CALLHOOK(ClientConnect, this);
1216
1217         if (player_count == 1)
1218         {
1219                 if (autocvar_sv_autopause && server_is_dedicated)
1220                         setpause(0);
1221                 localcmd("\nsv_hook_firstjoin\n");
1222         }
1223 }
1224
1225 .string shootfromfixedorigin;
1226 .entity chatbubbleentity;
1227 void player_powerups_remove_all(entity this);
1228
1229 /*
1230 =============
1231 ClientDisconnect
1232
1233 Called when a client disconnects from the server
1234 =============
1235 */
1236 void ClientDisconnect(entity this)
1237 {
1238         assert(IS_CLIENT(this), return);
1239
1240         /* from "ignore" command */
1241         strfree(this.ignore_list);
1242         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it.ignore_list,
1243         {
1244                 if(it.crypto_idfp && it.crypto_idfp != "")
1245                         continue;
1246                 string mylist = ignore_removefromlist(it, this);
1247                 if(it.ignore_list)
1248                         strunzone(it.ignore_list);
1249
1250                 it.ignore_list = strzone(mylist);
1251         });
1252         /* from "ignore" command */
1253
1254         PlayerStats_GameReport_FinalizePlayer(this);
1255         if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
1256         if (CS(this).active_minigame) part_minigame(this);
1257         if (IS_PLAYER(this)) Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
1258
1259         if (autocvar_sv_eventlog)
1260                 GameLogEcho(strcat(":part:", ftos(this.playerid)));
1261
1262         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_DISCONNECT, this.netname);
1263
1264         if(IS_SPEC(this))
1265                 SetSpectatee(this, NULL);
1266
1267         MUTATOR_CALLHOOK(ClientDisconnect, this);
1268
1269         strfree(CS(this).netname_previous); // needs to be before the CS entity is removed!
1270         strfree(CS_CVAR(this).weaponorder_byimpulse);
1271         ClientState_detach(this);
1272
1273         Portal_ClearAll(this);
1274
1275         Unfreeze(this, false);
1276
1277         RemoveGrapplingHooks(this);
1278
1279         strfree(this.shootfromfixedorigin);
1280
1281         // Here, everything has been done that requires this player to be a client.
1282
1283         this.flags &= ~FL_CLIENT;
1284
1285         if (this.chatbubbleentity) delete(this.chatbubbleentity);
1286         if (this.killindicator) delete(this.killindicator);
1287
1288         IL_EACH(g_counters, it.realowner == this,
1289         {
1290                 delete(it);
1291         });
1292
1293         WaypointSprite_PlayerGone(this);
1294
1295         bot_relinkplayerlist();
1296
1297         strfree(this.clientstatus);
1298         if (this.personal) delete(this.personal);
1299
1300         this.playerid = 0;
1301         if (warmup_stage || game_starttime > time) ReadyCount();
1302         if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
1303
1304         player_powerups_remove_all(this); // stop powerup sound
1305
1306         ONREMOVE(this);
1307
1308         if (player_count == 0)
1309                 localcmd("\nsv_hook_lastleave\n");
1310
1311         if (!TeamBalance_QueuedPlayersTagIn(this))
1312         if (autocvar_g_balance_teams_remove)
1313                 TeamBalance_RemoveExcessPlayers(NULL);
1314 }
1315
1316 void ChatBubbleThink(entity this)
1317 {
1318         this.nextthink = time;
1319         if ((this.owner.alpha < 0) || this.owner.chatbubbleentity != this)
1320         {
1321                 if(this.owner) // but why can that ever be NULL?
1322                         this.owner.chatbubbleentity = NULL;
1323                 delete(this);
1324                 return;
1325         }
1326
1327         this.mdl = "";
1328
1329         if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
1330         {
1331                 if ( CS(this.owner).active_minigame && PHYS_INPUT_BUTTON_MINIGAME(this.owner) )
1332                         this.mdl = "models/sprites/minigame_busy.iqm";
1333                 else if (PHYS_INPUT_BUTTON_CHAT(this.owner))
1334                         this.mdl = "models/misc/chatbubble.spr";
1335         }
1336
1337         if ( this.model != this.mdl )
1338                 _setmodel(this, this.mdl);
1339
1340 }
1341
1342 void UpdateChatBubble(entity this)
1343 {
1344         if (this.alpha < 0)
1345                 return;
1346         // spawn a chatbubble entity if needed
1347         if (!this.chatbubbleentity)
1348         {
1349                 this.chatbubbleentity = new(chatbubbleentity);
1350                 this.chatbubbleentity.owner = this;
1351                 this.chatbubbleentity.exteriormodeltoclient = this;
1352                 setthink(this.chatbubbleentity, ChatBubbleThink);
1353                 this.chatbubbleentity.nextthink = time;
1354                 setmodel(this.chatbubbleentity, MDL_CHAT); // precision set below
1355                 //setorigin(this.chatbubbleentity, this.origin + '0 0 15' + this.maxs_z * '0 0 1');
1356                 setorigin(this.chatbubbleentity, '0 0 15' + this.maxs_z * '0 0 1');
1357                 setattachment(this.chatbubbleentity, this, "");  // sticks to moving player better, also conserves bandwidth
1358                 this.chatbubbleentity.mdl = this.chatbubbleentity.model;
1359                 //this.chatbubbleentity.model = "";
1360                 this.chatbubbleentity.effects = EF_LOWPRECISION;
1361         }
1362 }
1363
1364 void calculate_player_respawn_time(entity this)
1365 {
1366         if(MUTATOR_CALLHOOK(CalculateRespawnTime, this))
1367                 return;
1368
1369         float gametype_setting_tmp;
1370         float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
1371         float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
1372         float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
1373         float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
1374         float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
1375         float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
1376
1377         float pcount = 1;  // Include myself whether or not team is already set right and I'm a "player".
1378         if (teamplay)
1379         {
1380                 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1381                         if(it.team == this.team)
1382                                 ++pcount;
1383                 });
1384                 if (sdelay_small_count == 0)
1385                         sdelay_small_count = 1;
1386                 if (sdelay_large_count == 0)
1387                         sdelay_large_count = 1;
1388         }
1389         else
1390         {
1391                 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1392                         ++pcount;
1393                 });
1394                 if (sdelay_small_count == 0)
1395                 {
1396                         if (IS_INDEPENDENT_PLAYER(this))
1397                         {
1398                                 // Players play independently. No point in requiring enemies.
1399                                 sdelay_small_count = 1;
1400                         }
1401                         else
1402                         {
1403                                 // Players play AGAINST each other. Enemies required.
1404                                 sdelay_small_count = 2;
1405                         }
1406                 }
1407                 if (sdelay_large_count == 0)
1408                 {
1409                         if (IS_INDEPENDENT_PLAYER(this))
1410                         {
1411                                 // Players play independently. No point in requiring enemies.
1412                                 sdelay_large_count = 1;
1413                         }
1414                         else
1415                         {
1416                                 // Players play AGAINST each other. Enemies required.
1417                                 sdelay_large_count = 2;
1418                         }
1419                 }
1420         }
1421
1422         float sdelay;
1423
1424         if (pcount <= sdelay_small_count)
1425                 sdelay = sdelay_small;
1426         else if (pcount >= sdelay_large_count)
1427                 sdelay = sdelay_large;
1428         else  // NOTE: this case implies sdelay_large_count > sdelay_small_count.
1429                 sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
1430
1431         if(waves)
1432                 this.respawn_time = ceil((time + sdelay) / waves) * waves;
1433         else
1434                 this.respawn_time = time + sdelay;
1435
1436         if(sdelay < sdelay_max)
1437                 this.respawn_time_max = time + sdelay_max;
1438         else
1439                 this.respawn_time_max = this.respawn_time;
1440
1441         if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
1442                 this.respawn_countdown = 10; // first number to count down from is 10
1443         else
1444                 this.respawn_countdown = -1; // do not count down
1445
1446         if(autocvar_g_forced_respawn)
1447                 this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
1448 }
1449
1450 // LordHavoc: this hack will be removed when proper _pants/_shirt layers are
1451 // added to the model skins
1452 /*void UpdateColorModHack()
1453 {
1454         float c;
1455         c = this.clientcolors & 15;
1456         // LordHavoc: only bothering to support white, green, red, yellow, blue
1457              if (!teamplay) this.colormod = '0 0 0';
1458         else if (c ==  0) this.colormod = '1.00 1.00 1.00';
1459         else if (c ==  3) this.colormod = '0.10 1.73 0.10';
1460         else if (c ==  4) this.colormod = '1.73 0.10 0.10';
1461         else if (c == 12) this.colormod = '1.22 1.22 0.10';
1462         else if (c == 13) this.colormod = '0.10 0.10 1.73';
1463         else this.colormod = '1 1 1';
1464 }*/
1465
1466 void respawn(entity this)
1467 {
1468         bool damagedbycontents_prev = this.damagedbycontents;
1469         if(this.alpha >= 0)
1470         {
1471                 if(autocvar_g_respawn_ghosts)
1472                 {
1473                         this.solid = SOLID_NOT;
1474                         this.takedamage = DAMAGE_NO;
1475                         this.damagedbycontents = false;
1476                         set_movetype(this, MOVETYPE_FLY);
1477                         this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
1478                         this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
1479                         this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
1480                         this.alpha = min(this.alpha, autocvar_g_respawn_ghosts_alpha);
1481                         Send_Effect(EFFECT_RESPAWN_GHOST, this.origin, '0 0 0', 1);
1482                         if(autocvar_g_respawn_ghosts_time > 0)
1483                                 SUB_SetFade(this, time + autocvar_g_respawn_ghosts_time, autocvar_g_respawn_ghosts_fadetime);
1484                 }
1485                 else
1486                         SUB_SetFade (this, time, 1); // fade out the corpse immediately
1487         }
1488
1489         CopyBody(this, 1);
1490         this.damagedbycontents = damagedbycontents_prev;
1491
1492         this.effects |= EF_NODRAW; // prevent another CopyBody
1493         PutClientInServer(this);
1494 }
1495
1496 void play_countdown(entity this, float finished, Sound samp)
1497 {
1498         TC(Sound, samp);
1499         float time_left = finished - time;
1500         if(IS_REAL_CLIENT(this) && time_left < 6 && floor(time_left - frametime) != floor(time_left))
1501                 sound(this, CH_INFO, samp, VOL_BASE, ATTEN_NORM);
1502 }
1503
1504 // it removes special powerups not handled by StatusEffects
1505 void player_powerups_remove_all(entity this)
1506 {
1507         if (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1508         {
1509                 // don't play the poweroff sound when the game restarts or the player disconnects
1510                 if (time > game_starttime + 1 && IS_CLIENT(this)
1511                         && !(start_items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS)))
1512                 {
1513                         sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM);
1514                 }
1515                 if (this.items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1516                         stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound
1517                 this.items -= (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS));
1518         }
1519 }
1520
1521 void player_powerups(entity this)
1522 {
1523         if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped)
1524                 this.modelflags |= MF_ROCKET;
1525         else
1526                 this.modelflags &= ~MF_ROCKET;
1527
1528         this.effects &= ~EF_NODEPTHTEST;
1529
1530         if (IS_DEAD(this))
1531                 player_powerups_remove_all(this);
1532
1533         if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed
1534                 return;
1535
1536         // add a way to see what the items were BEFORE all of these checks for the mutator hook
1537         int items_prev = this.items;
1538
1539         if (!MUTATOR_IS_ENABLED(mutator_instagib))
1540         {
1541                 // NOTE: superweapons are a special case and as such are handled here instead of the status effects system
1542                 if (this.items & IT_SUPERWEAPON)
1543                 {
1544                         if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
1545                         {
1546                                 StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL);
1547                                 this.items = this.items - (this.items & IT_SUPERWEAPON);
1548                                 //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname);
1549                                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST);
1550                         }
1551                         else if (this.items & IT_UNLIMITED_SUPERWEAPONS)
1552                         {
1553                                 // don't let them run out
1554                         }
1555                         else
1556                         {
1557                                 play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
1558                                 if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
1559                                 {
1560                                         this.items = this.items - (this.items & IT_SUPERWEAPON);
1561                                         STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1562                                         //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_BROKEN, this.netname);
1563                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_BROKEN);
1564                                 }
1565                         }
1566                 }
1567                 else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
1568                 {
1569                         if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
1570                         {
1571                                 this.items = this.items | IT_SUPERWEAPON;
1572                                 if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
1573                                 {
1574                                         if(!g_cts)
1575                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_PICKUP, this.netname);
1576                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_PICKUP);
1577                                 }
1578                         }
1579                         else
1580                         {
1581                                 if(StatusEffects_active(STATUSEFFECT_Superweapons, this))
1582                                         StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT);
1583                                 STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1584                         }
1585                 }
1586                 else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame!
1587                 {
1588                         StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR);
1589                 }
1590         }
1591
1592         if(autocvar_g_nodepthtestplayers)
1593                 this.effects = this.effects | EF_NODEPTHTEST;
1594
1595         if(autocvar_g_fullbrightplayers)
1596                 this.effects = this.effects | EF_FULLBRIGHT;
1597
1598         MUTATOR_CALLHOOK(PlayerPowerups, this, items_prev);
1599 }
1600
1601 float CalcRegen(float current, float stable, float regenfactor, float regenframetime)
1602 {
1603         if(current > stable)
1604                 return current;
1605         else if(current > stable - 0.25) // when close enough, "snap"
1606                 return stable;
1607         else
1608                 return min(stable, current + (stable - current) * regenfactor * regenframetime);
1609 }
1610
1611 float CalcRot(float current, float stable, float rotfactor, float rotframetime)
1612 {
1613         if(current < stable)
1614                 return current;
1615         else if(current < stable + 0.25) // when close enough, "snap"
1616                 return stable;
1617         else
1618                 return max(stable, current + (stable - current) * rotfactor * rotframetime);
1619 }
1620
1621 void RotRegen(entity this, Resource res, float limit_mod,
1622         float regenstable, float regenfactor, float regenlinear, float regenframetime,
1623         float rotstable, float rotfactor, float rotlinear, float rotframetime)
1624 {
1625         float old = GetResource(this, res);
1626         float current = old;
1627         if(current > rotstable)
1628         {
1629                 if(rotframetime > 0)
1630                 {
1631                         current = CalcRot(current, rotstable, rotfactor, rotframetime);
1632                         current = max(rotstable, current - rotlinear * rotframetime);
1633                 }
1634         }
1635         else if(current < regenstable)
1636         {
1637                 if(regenframetime > 0)
1638                 {
1639                         current = CalcRegen(current, regenstable, regenfactor, regenframetime);
1640                         current = min(regenstable, current + regenlinear * regenframetime);
1641                 }
1642         }
1643
1644         float limit = GetResourceLimit(this, res) * limit_mod;
1645         if(current > limit)
1646                 current = limit;
1647
1648         if (current != old)
1649                 SetResource(this, res, current);
1650 }
1651
1652 void player_regen(entity this)
1653 {
1654         float max_mod, regen_mod, rot_mod, limit_mod;
1655         max_mod = regen_mod = rot_mod = limit_mod = 1;
1656
1657         float regen_health = autocvar_g_balance_health_regen;
1658         float regen_health_linear = autocvar_g_balance_health_regenlinear;
1659         float regen_health_rot = autocvar_g_balance_health_rot;
1660         float regen_health_rotlinear = autocvar_g_balance_health_rotlinear;
1661         float regen_health_stable = autocvar_g_balance_health_regenstable;
1662         float regen_health_rotstable = autocvar_g_balance_health_rotstable;
1663         bool mutator_returnvalue = MUTATOR_CALLHOOK(PlayerRegen, this, max_mod, regen_mod, rot_mod, limit_mod, regen_health, regen_health_linear, regen_health_rot,
1664                 regen_health_rotlinear, regen_health_stable, regen_health_rotstable);
1665         max_mod = M_ARGV(1, float);
1666         regen_mod = M_ARGV(2, float);
1667         rot_mod = M_ARGV(3, float);
1668         limit_mod = M_ARGV(4, float);
1669         regen_health = M_ARGV(5, float);
1670         regen_health_linear = M_ARGV(6, float);
1671         regen_health_rot = M_ARGV(7, float);
1672         regen_health_rotlinear = M_ARGV(8, float);
1673         regen_health_stable = M_ARGV(9, float);
1674         regen_health_rotstable = M_ARGV(10, float);
1675
1676         float rotstable, regenstable, rotframetime, regenframetime;
1677
1678         if(!mutator_returnvalue)
1679         if(!STAT(FROZEN, this))
1680         {
1681                 regenstable = autocvar_g_balance_armor_regenstable;
1682                 rotstable = autocvar_g_balance_armor_rotstable;
1683                 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1684                 rotframetime = (time > this.pauserotarmor_finished) ? (rot_mod * frametime) : 0;
1685                 RotRegen(this, RES_ARMOR, limit_mod,
1686                         regenstable, autocvar_g_balance_armor_regen, autocvar_g_balance_armor_regenlinear, regenframetime,
1687                         rotstable, autocvar_g_balance_armor_rot, autocvar_g_balance_armor_rotlinear, rotframetime);
1688
1689                 // NOTE: max_mod is only applied to health
1690                 regenstable = regen_health_stable * max_mod;
1691                 rotstable = regen_health_rotstable * max_mod;
1692                 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1693                 rotframetime = (time > this.pauserothealth_finished) ? (rot_mod * frametime) : 0;
1694                 RotRegen(this, RES_HEALTH, limit_mod,
1695                         regenstable, regen_health, regen_health_linear, regenframetime,
1696                         rotstable, regen_health_rot, regen_health_rotlinear, rotframetime);
1697         }
1698
1699         // if player rotted to death...  die!
1700         // check this outside above checks, as player may still be able to rot to death
1701         if(GetResource(this, RES_HEALTH) < 1)
1702         {
1703                 if(this.vehicle)
1704                         vehicles_exit(this.vehicle, VHEF_RELEASE);
1705                 if(this.event_damage)
1706                         this.event_damage(this, this, this, 1, DEATH_ROT.m_id, DMG_NOWEP, this.origin, '0 0 0');
1707         }
1708
1709         if (!(this.items & IT_UNLIMITED_AMMO))
1710         {
1711                 regenstable = autocvar_g_balance_fuel_regenstable;
1712                 rotstable = autocvar_g_balance_fuel_rotstable;
1713                 regenframetime = ((time > this.pauseregen_finished) && (this.items & ITEM_JetpackRegen.m_itemid)) ? frametime : 0;
1714                 rotframetime = (time > this.pauserotfuel_finished) ? frametime : 0;
1715                 RotRegen(this, RES_FUEL, 1,
1716                         regenstable, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear, regenframetime,
1717                         rotstable, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, rotframetime);
1718         }
1719 }
1720
1721 bool zoomstate_set;
1722 void SetZoomState(entity this, float newzoom)
1723 {
1724         if(newzoom != CS(this).zoomstate)
1725         {
1726                 CS(this).zoomstate = newzoom;
1727                 ClientData_Touch(this);
1728         }
1729         zoomstate_set = true;
1730 }
1731
1732 void GetPressedKeys(entity this)
1733 {
1734         MUTATOR_CALLHOOK(GetPressedKeys, this);
1735         if (game_stopped)
1736         {
1737                 CS(this).pressedkeys = 0;
1738                 STAT(PRESSED_KEYS, this) = 0;
1739                 return;
1740         }
1741
1742         // NOTE: GetPressedKeys and PM_dodging_GetPressedKeys use similar code
1743         int keys = STAT(PRESSED_KEYS, this);
1744         keys = BITSET(keys, KEY_FORWARD,        CS(this).movement.x > 0);
1745         keys = BITSET(keys, KEY_BACKWARD,       CS(this).movement.x < 0);
1746         keys = BITSET(keys, KEY_RIGHT,          CS(this).movement.y > 0);
1747         keys = BITSET(keys, KEY_LEFT,           CS(this).movement.y < 0);
1748
1749         keys = BITSET(keys, KEY_JUMP,           PHYS_INPUT_BUTTON_JUMP(this));
1750         keys = BITSET(keys, KEY_CROUCH,         IS_DUCKED(this)); // workaround: player can't un-crouch until their path is clear, so we keep the button held here
1751         keys = BITSET(keys, KEY_ATCK,           PHYS_INPUT_BUTTON_ATCK(this));
1752         keys = BITSET(keys, KEY_ATCK2,          PHYS_INPUT_BUTTON_ATCK2(this));
1753         CS(this).pressedkeys = keys; // store for other users
1754
1755         STAT(PRESSED_KEYS, this) = keys;
1756 }
1757
1758 /*
1759 ======================
1760 spectate mode routines
1761 ======================
1762 */
1763
1764 void SpectateCopy(entity this, entity spectatee)
1765 {
1766         TC(Client, this); TC(Client, spectatee);
1767
1768         MUTATOR_CALLHOOK(SpectateCopy, spectatee, this);
1769         PS(this) = PS(spectatee);
1770         this.armortype = spectatee.armortype;
1771         SetResourceExplicit(this, RES_ARMOR, GetResource(spectatee, RES_ARMOR));
1772         SetResourceExplicit(this, RES_CELLS, GetResource(spectatee, RES_CELLS));
1773         SetResourceExplicit(this, RES_PLASMA, GetResource(spectatee, RES_PLASMA));
1774         SetResourceExplicit(this, RES_SHELLS, GetResource(spectatee, RES_SHELLS));
1775         SetResourceExplicit(this, RES_BULLETS, GetResource(spectatee, RES_BULLETS));
1776         SetResourceExplicit(this, RES_ROCKETS, GetResource(spectatee, RES_ROCKETS));
1777         SetResourceExplicit(this, RES_FUEL, GetResource(spectatee, RES_FUEL));
1778         this.effects = spectatee.effects & EFMASK_CHEAP; // eat performance
1779         SetResourceExplicit(this, RES_HEALTH, GetResource(spectatee, RES_HEALTH));
1780         CS(this).impulse = 0;
1781         this.disableclientprediction = 1; // no need to run prediction on a spectator
1782         this.items = spectatee.items;
1783         STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
1784         STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
1785         STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee);
1786         STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
1787         STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
1788         this.punchangle = spectatee.punchangle;
1789         this.view_ofs = spectatee.view_ofs;
1790         this.velocity = spectatee.velocity;
1791         this.dmg_take = spectatee.dmg_take;
1792         this.dmg_save = spectatee.dmg_save;
1793         this.dmg_inflictor = spectatee.dmg_inflictor;
1794         this.v_angle = spectatee.v_angle;
1795         this.angles = spectatee.v_angle;
1796         STAT(FROZEN, this) = STAT(FROZEN, spectatee);
1797         STAT(REVIVE_PROGRESS, this) = STAT(REVIVE_PROGRESS, spectatee);
1798         this.viewloc = spectatee.viewloc;
1799         if(!PHYS_INPUT_BUTTON_USE(this) && STAT(CAMERA_SPECTATOR, this) != 2)
1800                 this.fixangle = true;
1801         setorigin(this, spectatee.origin);
1802         setsize(this, spectatee.mins, spectatee.maxs);
1803         SetZoomState(this, CS(spectatee).zoomstate);
1804
1805     anticheat_spectatecopy(this, spectatee);
1806         STAT(HUD, this) = STAT(HUD, spectatee);
1807         if(spectatee.vehicle)
1808     {
1809         this.angles = spectatee.v_angle;
1810
1811         //this.fixangle = false;
1812         //this.velocity = spectatee.vehicle.velocity;
1813         this.vehicle_health = spectatee.vehicle_health;
1814         this.vehicle_shield = spectatee.vehicle_shield;
1815         this.vehicle_energy = spectatee.vehicle_energy;
1816         this.vehicle_ammo1 = spectatee.vehicle_ammo1;
1817         this.vehicle_ammo2 = spectatee.vehicle_ammo2;
1818         this.vehicle_reload1 = spectatee.vehicle_reload1;
1819         this.vehicle_reload2 = spectatee.vehicle_reload2;
1820
1821         //msg_entity = this;
1822
1823        // WriteByte (MSG_ONE, SVC_SETVIEWANGLES);
1824             //WriteAngle(MSG_ONE,  spectatee.v_angle.x);
1825            // WriteAngle(MSG_ONE,  spectatee.v_angle.y);
1826            // WriteAngle(MSG_ONE,  spectatee.v_angle.z);
1827
1828         //WriteByte (MSG_ONE, SVC_SETVIEW);
1829         //    WriteEntity(MSG_ONE, this);
1830         //makevectors(spectatee.v_angle);
1831         //setorigin(this, spectatee.origin - v_forward * 400 + v_up * 300);*/
1832     }
1833 }
1834
1835 bool SpectateUpdate(entity this)
1836 {
1837         if(!this.enemy)
1838                 return false;
1839
1840         if(!IS_PLAYER(this.enemy) || this == this.enemy)
1841         {
1842                 SetSpectatee(this, NULL);
1843                 return false;
1844         }
1845
1846         SpectateCopy(this, this.enemy);
1847
1848         return true;
1849 }
1850
1851 bool SpectateSet(entity this)
1852 {
1853         if(!IS_PLAYER(this.enemy))
1854                 return false;
1855
1856         ClientData_Touch(this.enemy);
1857
1858         msg_entity = this;
1859         WriteByte(MSG_ONE, SVC_SETVIEW);
1860         WriteEntity(MSG_ONE, this.enemy);
1861         set_movetype(this, MOVETYPE_NONE);
1862         accuracy_resend(this);
1863
1864         if(!SpectateUpdate(this))
1865                 PutObserverInServer(this, false, true);
1866
1867         return true;
1868 }
1869
1870 void SetSpectatee_status(entity this, int spectatee_num)
1871 {
1872         int oldspectatee_status = CS(this).spectatee_status;
1873         CS(this).spectatee_status = spectatee_num;
1874
1875         if (CS(this).spectatee_status != oldspectatee_status)
1876         {
1877                 if (STAT(PRESSED_KEYS, this))
1878                 {
1879                         CS(this).pressedkeys = 0;
1880                         STAT(PRESSED_KEYS, this) = 0;
1881                 }
1882                 ClientData_Touch(this);
1883                 if (g_race || g_cts) race_InitSpectator();
1884         }
1885 }
1886
1887 void SetSpectatee(entity this, entity spectatee)
1888 {
1889         if(IS_BOT_CLIENT(this))
1890                 return; // bots abuse .enemy, this code is useless to them
1891
1892         entity old_spectatee = this.enemy;
1893
1894         this.enemy = spectatee;
1895
1896         // WEAPONTODO
1897         // these are required to fix the spectator bug with arc
1898         if(old_spectatee)
1899         {
1900                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1901                 {
1902                         .entity weaponentity = weaponentities[slot];
1903                         if(old_spectatee.(weaponentity).arc_beam)
1904                                 old_spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1905                 }
1906         }
1907         if(spectatee)
1908         {
1909                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1910                 {
1911                         .entity weaponentity = weaponentities[slot];
1912                         if(spectatee.(weaponentity).arc_beam)
1913                                 spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1914                 }
1915         }
1916
1917         if (spectatee)
1918                 SetSpectatee_status(this, etof(spectatee));
1919
1920         // needed to update spectator list
1921         if(old_spectatee) { ClientData_Touch(old_spectatee); }
1922 }
1923
1924 bool Spectate(entity this, entity pl)
1925 {
1926         if(MUTATOR_CALLHOOK(SpectateSet, this, pl))
1927                 return false;
1928         pl = M_ARGV(1, entity);
1929
1930         SetSpectatee(this, pl);
1931         return SpectateSet(this);
1932 }
1933
1934 bool SpectateNext(entity this)
1935 {
1936         entity ent = find(this.enemy, classname, STR_PLAYER);
1937
1938         if (MUTATOR_CALLHOOK(SpectateNext, this, ent))
1939                 ent = M_ARGV(1, entity);
1940         else if (!ent)
1941                 ent = find(ent, classname, STR_PLAYER);
1942
1943         if(ent) { SetSpectatee(this, ent); }
1944
1945         return SpectateSet(this);
1946 }
1947
1948 bool SpectatePrev(entity this)
1949 {
1950         // NOTE: chain order is from the highest to the lower entnum (unlike find)
1951         entity ent = findchain(classname, STR_PLAYER);
1952         if (!ent) // no player
1953                 return false;
1954
1955         entity first = ent;
1956         // skip players until current spectated player
1957         if(this.enemy)
1958         while(ent && ent != this.enemy)
1959                 ent = ent.chain;
1960
1961         switch (MUTATOR_CALLHOOK(SpectatePrev, this, ent, first))
1962         {
1963                 case MUT_SPECPREV_FOUND:
1964                         ent = M_ARGV(1, entity);
1965                         break;
1966                 case MUT_SPECPREV_RETURN:
1967                         return true;
1968                 case MUT_SPECPREV_CONTINUE:
1969                 default:
1970                 {
1971                         if(ent.chain)
1972                                 ent = ent.chain;
1973                         else
1974                                 ent = first;
1975                         break;
1976                 }
1977         }
1978
1979         SetSpectatee(this, ent);
1980         return SpectateSet(this);
1981 }
1982
1983 /*
1984 =============
1985 ShowRespawnCountdown()
1986
1987 Update a respawn countdown display.
1988 =============
1989 */
1990 void ShowRespawnCountdown(entity this)
1991 {
1992         float number;
1993         if(!IS_DEAD(this)) // just respawned?
1994                 return;
1995         else
1996         {
1997                 number = ceil(this.respawn_time - time);
1998                 if(number <= 0)
1999                         return;
2000                 if(number <= this.respawn_countdown)
2001                 {
2002                         this.respawn_countdown = number - 1;
2003                         if(ceil(this.respawn_time - (time + 0.5)) == number) // only say it if it is the same number even in 0.5s; to prevent overlapping sounds
2004                                 { Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_RESPAWN, number)); }
2005                 }
2006         }
2007 }
2008
2009 bool ShowTeamSelection(entity this)
2010 {
2011         if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
2012                 return false;
2013         if (QueuedPlayersReady(this, true))
2014                 return false;
2015         if (frametime) // once per frame is more than enough
2016                 stuffcmd(this, "_scoreboard_team_selection 1\n");
2017         return true;
2018 }
2019
2020 void Join(entity this, bool queued_join)
2021 {
2022         bool teamautoselect = autocvar_g_campaign || autocvar_g_balance_teams || this.wants_join < 0;
2023
2024         if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
2025                 ReadyRestart(true);
2026
2027         TRANSMUTE(Player, this);
2028
2029         if(queued_join)
2030         {
2031                 // First we must put queued player(s) in their team(s) (they chose first).
2032                 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join,
2033                 {
2034                         Join(it, false);
2035                         // ensure TeamBalance_JoinBestTeam will run if necessary for `this`
2036                         teamautoselect = true;
2037                 });
2038         }
2039
2040         if(!this.team_selected && teamautoselect)
2041                 TeamBalance_JoinBestTeam(this);
2042
2043         if(autocvar_g_campaign)
2044                 campaign_bots_may_start = true;
2045
2046         Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
2047
2048         PutClientInServer(this);
2049
2050         if(IS_PLAYER(this))
2051         if(teamplay && this.team != -1)
2052         {
2053                 if(this.wants_join)
2054                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
2055         }
2056         else
2057                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
2058         this.team_selected = false;
2059         this.wants_join = 0;
2060 }
2061
2062 int GetPlayerLimit()
2063 {
2064         if(g_duel)
2065                 return 2; // TODO: this workaround is needed since the mutator hook from duel can't be activated before the gametype is loaded (e.g. switching modes via gametype vote screen)
2066         // don't return map_maxplayers during intermission as it would interfere with MapHasRightSize()
2067         int player_limit = (autocvar_g_maxplayers >= 0 || intermission_running) ? autocvar_g_maxplayers : map_maxplayers;
2068         MUTATOR_CALLHOOK(GetPlayerLimit, player_limit);
2069         player_limit = M_ARGV(0, int);
2070         return player_limit < maxclients ? player_limit : 0;
2071 }
2072
2073 /**
2074  * Determines whether the player is allowed to join. This depends on cvar
2075  * g_maxplayers, if it isn't used this function always return true, otherwise
2076  * it checks whether the number of currently playing players exceeds g_maxplayers.
2077  * @return int number of free slots for players, 0 if none
2078  */
2079 int nJoinAllowed(entity this, entity ignore)
2080 {
2081         if(!ignore)
2082         // this is called that way when checking if anyone may be able to join (to build qcstatus)
2083         // so report 0 free slots if restricted
2084         {
2085                 if(autocvar_g_forced_team_otherwise == "spectate")
2086                         return 0;
2087                 if(autocvar_g_forced_team_otherwise == "spectator")
2088                         return 0;
2089         }
2090
2091         if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2092                 return 0; // forced spectators can never join
2093
2094         static float msg_time = 0;
2095         if(this && !INGAME(this) && ignore && PlayerInList(this, autocvar_g_playban_list))
2096         {
2097                 if(time > msg_time)
2098                 {
2099                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PLAYBAN);
2100                         msg_time = time + 0.5;
2101                 }
2102                 return 0;
2103         }
2104
2105         // TODO simplify this
2106         int totalClients = 0;
2107         int currentlyPlaying = 0;
2108         FOREACH_CLIENT(true, {
2109                 if(it != ignore)
2110                         ++totalClients;
2111                 if(IS_REAL_CLIENT(it) && (IS_PLAYER(it) || INGAME(it)))
2112                         ++currentlyPlaying;
2113         });
2114
2115         int player_limit = GetPlayerLimit();
2116
2117         int free_slots = 0;
2118         if (!player_limit)
2119                 free_slots = maxclients - totalClients;
2120         else if(player_limit > 0 && currentlyPlaying < player_limit)
2121                 free_slots = min(maxclients - totalClients, player_limit - currentlyPlaying);
2122
2123         if(this && !INGAME(this) && ignore && !free_slots && time > msg_time)
2124         {
2125                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT, player_limit);
2126                 msg_time = time + 0.5;
2127         }
2128
2129         return free_slots;
2130 }
2131
2132 bool queuePlayer(entity this, int team_index)
2133 {
2134         if(IS_BOT_CLIENT(this) || !IS_QUEUE_NEEDED(this) || QueuedPlayersReady(this, false))
2135                 return false;
2136
2137         if(team_index <= 0)
2138         {
2139                 // defer team selection until Join()
2140                 this.wants_join = -1;
2141                 this.team_selected = false;
2142                 this.team = -1;
2143                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_WANTS, this.netname);
2144                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_JOIN_PREVENT_QUEUE);
2145         }
2146         else
2147         {
2148                 this.wants_join = team_index; // Player queued to join
2149                 this.team_selected = true; // no autoselect in Join()
2150                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_WANTS_TEAM), this.netname);
2151                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_JOIN_PREVENT_QUEUE_TEAM));
2152         }
2153
2154         return true;
2155 }
2156
2157 bool joinAllowed(entity this)
2158 {
2159         if (CS(this).version_mismatch) return false;
2160         if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
2161         if (!nJoinAllowed(this, this)) return false;
2162         if (teamplay && lockteams) return false;
2163         if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
2164         if (ShowTeamSelection(this)) return false;
2165         if (this.wants_join) return false;
2166         if (queuePlayer(this, 0)) return false;
2167         return true;
2168 }
2169
2170 void show_entnum(entity this)
2171 {
2172         // waypoint editor implements a similar feature for waypoints
2173         if (waypointeditor_enabled)
2174                 return;
2175
2176         if (wasfreed(this.wp_aimed))
2177                 this.wp_aimed = NULL;
2178
2179         WarpZone_crosshair_trace_plusvisibletriggers(this);
2180         entity ent = NULL;
2181         if (trace_ent)
2182         {
2183                 ent = trace_ent;
2184                 if (ent != this.wp_aimed)
2185                 {
2186                         string str = sprintf(
2187                                 "^7ent #%d\n^8 netname: ^3%s\n^8 classname: ^5%s\n^8 origin: ^2'%s'",
2188                                 etof(ent), ent.netname, ent.classname, vtos(ent.origin));
2189                         debug_text_3d((ent.absmin + ent.absmax) * 0.5, str, 0, 7, '0 0 0');
2190                 }
2191         }
2192         if (this.wp_aimed != ent)
2193                 this.wp_aimed = ent;
2194 }
2195
2196 .bool dualwielding_prev;
2197 bool PlayerThink(entity this)
2198 {
2199         if (game_stopped || intermission_running) {
2200                 this.modelflags &= ~MF_ROCKET;
2201                 if(intermission_running)
2202                         IntermissionThink(this);
2203                 return false;
2204         }
2205
2206         if (timeout_status == TIMEOUT_ACTIVE) {
2207                 // don't allow the player to turn around while game is paused
2208                 // FIXME turn this into CSQC stuff
2209                 this.v_angle = this.lastV_angle;
2210                 this.angles = this.lastV_angle;
2211                 this.fixangle = true;
2212         }
2213
2214         if (frametime) player_powerups(this);
2215
2216         if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2217
2218         if (IS_DEAD(this)) {
2219                 if (this.personal && g_race_qualifying) {
2220                         if (time > this.respawn_time) {
2221                                 STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second
2222                                 respawn(this);
2223                                 CS(this).impulse = CHIMPULSE_SPEEDRUN.impulse;
2224                         }
2225                 } else {
2226                         if (frametime) player_anim(this);
2227
2228                         if (this.respawn_flags & RESPAWN_DENY)
2229                         {
2230                                 STAT(RESPAWN_TIME, this) = 0;
2231                                 return false;
2232                         }
2233
2234                         bool button_pressed = (PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this) || PHYS_INPUT_BUTTON_ATCK2(this) || PHYS_INPUT_BUTTON_HOOK(this) || PHYS_INPUT_BUTTON_USE(this));
2235
2236                         switch(this.deadflag)
2237                         {
2238                                 case DEAD_DYING:
2239                                 {
2240                                         if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max))
2241                                                 this.deadflag = DEAD_RESPAWNING;
2242                                         else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)))
2243                                                 this.deadflag = DEAD_DEAD;
2244                                         break;
2245                                 }
2246                                 case DEAD_DEAD:
2247                                 {
2248                                         if (button_pressed)
2249                                                 this.deadflag = DEAD_RESPAWNABLE;
2250                                         else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))
2251                                                 this.deadflag = DEAD_RESPAWNING;
2252                                         break;
2253                                 }
2254                                 case DEAD_RESPAWNABLE:
2255                                 {
2256                                         if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE))
2257                                                 this.deadflag = DEAD_RESPAWNING;
2258                                         break;
2259                                 }
2260                                 case DEAD_RESPAWNING:
2261                                 {
2262                                         if (time > this.respawn_time)
2263                                         {
2264                                                 this.respawn_time = time + 1; // only retry once a second
2265                                                 this.respawn_time_max = this.respawn_time;
2266                                                 respawn(this);
2267                                         }
2268                                         break;
2269                                 }
2270                         }
2271
2272                         ShowRespawnCountdown(this);
2273
2274                         if (this.respawn_flags & RESPAWN_SILENT)
2275                                 STAT(RESPAWN_TIME, this) = 0;
2276                         else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max)
2277                         {
2278                                 if (time < this.respawn_time)
2279                                         STAT(RESPAWN_TIME, this) = this.respawn_time;
2280                                 else if (this.deadflag != DEAD_RESPAWNING)
2281                                         STAT(RESPAWN_TIME, this) = -this.respawn_time_max;
2282                         }
2283                         else
2284                                 STAT(RESPAWN_TIME, this) = this.respawn_time;
2285                 }
2286
2287                 // if respawning, invert stat_respawn_time to indicate this, the client translates it
2288                 if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0)
2289                         STAT(RESPAWN_TIME, this) *= -1;
2290
2291                 return false;
2292         }
2293
2294         FixPlayermodel(this);
2295
2296         if (this.shootfromfixedorigin != autocvar_g_shootfromfixedorigin) {
2297                 strcpy(this.shootfromfixedorigin, autocvar_g_shootfromfixedorigin);
2298                 stuffcmd(this, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
2299         }
2300
2301         // reset gun alignment when dual wielding status changes
2302         // to ensure guns are always aligned right and left
2303         bool dualwielding = W_DualWielding(this);
2304         if(this.dualwielding_prev != dualwielding)
2305         {
2306                 W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
2307                 this.dualwielding_prev = dualwielding;
2308         }
2309
2310         // LordHavoc: allow firing on move frames (sub-ticrate), this gives better timing on slow servers
2311         //if(frametime)
2312         {
2313                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2314                 {
2315                         .entity weaponentity = weaponentities[slot];
2316                         if(WEP_CVAR(vortex, charge_always))
2317                                 W_Vortex_Charge(this, weaponentity, frametime);
2318                         W_WeaponFrame(this, weaponentity);
2319                 }
2320         }
2321
2322         if (frametime)
2323         {
2324                 // WEAPONTODO: Add a weapon request for this
2325                 // rot vortex charge to the charge limit
2326                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2327                 {
2328                         .entity weaponentity = weaponentities[slot];
2329                         if (WEP_CVAR(vortex, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(vortex, charge_limit) && this.(weaponentity).vortex_charge_rottime < time)
2330                                 this.(weaponentity).vortex_charge = bound(WEP_CVAR(vortex, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(vortex, charge_rot_rate) * frametime / W_TICSPERFRAME, 1);
2331                 }
2332
2333                 player_regen(this);
2334                 player_anim(this);
2335                 this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
2336         }
2337
2338         monsters_setstatus(this);
2339
2340         return true;
2341 }
2342
2343 .bool would_spectate;
2344 // merged SpectatorThink and ObserverThink (old names are here so you can grep for them)
2345 void ObserverOrSpectatorThink(entity this)
2346 {
2347         bool is_spec = IS_SPEC(this);
2348         if ( CS(this).impulse )
2349         {
2350                 int r = MinigameImpulse(this, CS(this).impulse);
2351                 if (!is_spec || r)
2352                         CS(this).impulse = 0;
2353
2354                 if (is_spec && CS(this).impulse == IMP_weapon_drop.impulse)
2355                 {
2356                         STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
2357                         CS(this).impulse = 0;
2358                         return;
2359                 }
2360         }
2361
2362         if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2363
2364         if (IS_BOT_CLIENT(this) && !CS(this).autojoin_checked)
2365         {
2366                 CS(this).autojoin_checked = true;
2367                 TRANSMUTE(Player, this);
2368                 PutClientInServer(this);
2369
2370                 .entity weaponentity = weaponentities[0];
2371                 if(this.(weaponentity).m_weapon == WEP_Null)
2372                         W_NextWeapon(this, 0, weaponentity);
2373
2374                 return;
2375         }
2376
2377         if (this.flags & FL_JUMPRELEASED) {
2378                 if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
2379                         this.flags &= ~FL_JUMPRELEASED;
2380                         this.flags |= FL_SPAWNING;
2381                 } else if((is_spec && (PHYS_INPUT_BUTTON_ATCK(this) || CS(this).impulse == 10 || CS(this).impulse == 15 || CS(this).impulse == 18 || (CS(this).impulse >= 200 && CS(this).impulse <= 209)))
2382                         || (!is_spec && ((PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) || this.would_spectate))) {
2383                         this.flags &= ~FL_JUMPRELEASED;
2384                         if(SpectateNext(this)) {
2385                                 TRANSMUTE(Spectator, this);
2386                         } else if (is_spec) {
2387                                 TRANSMUTE(Observer, this);
2388                                 PutClientInServer(this);
2389                         }
2390                         else
2391                                 this.would_spectate = false; // unable to spectate anyone
2392                         if (is_spec)
2393                                 CS(this).impulse = 0;
2394                 } else if (is_spec) {
2395                         if(CS(this).impulse == 12 || CS(this).impulse == 16  || CS(this).impulse == 19 || (CS(this).impulse >= 220 && CS(this).impulse <= 229)) {
2396                                 this.flags &= ~FL_JUMPRELEASED;
2397                                 if(SpectatePrev(this)) {
2398                                         TRANSMUTE(Spectator, this);
2399                                 } else {
2400                                         TRANSMUTE(Observer, this);
2401                                         PutClientInServer(this);
2402                                 }
2403                                 CS(this).impulse = 0;
2404                         } else if(PHYS_INPUT_BUTTON_ATCK2(this)) {
2405                                 if(!observe_blocked_if_eliminated || !INGAME(this)) {
2406                                         this.would_spectate = false;
2407                                         this.flags &= ~FL_JUMPRELEASED;
2408                                         TRANSMUTE(Observer, this);
2409                                         PutClientInServer(this);
2410                                 }
2411                         } else if(!SpectateUpdate(this) && !SpectateNext(this)) {
2412                                 PutObserverInServer(this, false, true);
2413                                 this.would_spectate = true;
2414                         }
2415                 }
2416                 else {
2417                         bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating;
2418                         if (PHYS_INPUT_BUTTON_USE(this))
2419                                 wouldclip = !wouldclip;
2420                         int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
2421                         set_movetype(this, preferred_movetype);
2422                 }
2423         } else { // jump pressed
2424                 if ((is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)))
2425                         || (!is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this)))) {
2426                         this.flags |= FL_JUMPRELEASED;
2427                         // primary attack pressed
2428                         if(this.flags & FL_SPAWNING)
2429                         {
2430                                 this.flags &= ~FL_SPAWNING;
2431                                 if(joinAllowed(this))
2432                                         Join(this, true);
2433                                 else if(time < CS(this).jointime + MIN_SPEC_TIME)
2434                                         CS(this).autojoin_checked = -1;
2435                                 return;
2436                         }
2437                 }
2438                 if(is_spec && !SpectateUpdate(this))
2439                         PutObserverInServer(this, false, true);
2440         }
2441         if (is_spec)
2442                 this.flags |= FL_CLIENT | FL_NOTARGET;
2443 }
2444
2445 void PlayerUseKey(entity this)
2446 {
2447         if (!IS_PLAYER(this))
2448                 return;
2449
2450         if(this.vehicle)
2451         {
2452                 if(!game_stopped)
2453                 {
2454                         vehicles_exit(this.vehicle, VHEF_NORMAL);
2455                         return;
2456                 }
2457         }
2458         else if(autocvar_g_vehicles_enter)
2459         {
2460                 if(!game_stopped && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2461                 {
2462                         entity head, closest_target = NULL;
2463                         head = WarpZone_FindRadius(this.origin, autocvar_g_vehicles_enter_radius, true);
2464
2465                         while(head) // find the closest acceptable target to enter
2466                         {
2467                                 if(IS_VEHICLE(head) && !IS_DEAD(head) && head.takedamage != DAMAGE_NO)
2468                                 if(!head.owner || ((head.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(head.owner, this)))
2469                                 {
2470                                         if(closest_target)
2471                                         {
2472                                                 if(vlen2(this.origin - head.origin) < vlen2(this.origin - closest_target.origin))
2473                                                 { closest_target = head; }
2474                                         }
2475                                         else { closest_target = head; }
2476                                 }
2477
2478                                 head = head.chain;
2479                         }
2480
2481                         if(closest_target) { vehicles_enter(this, closest_target); return; }
2482                 }
2483         }
2484
2485         // a use key was pressed; call handlers
2486         MUTATOR_CALLHOOK(PlayerUseKey, this);
2487 }
2488
2489
2490 /*
2491 =============
2492 PlayerPreThink
2493
2494 Called every frame for each real client by DP (and for each bot by StartFrame()),
2495 and when executing every asynchronous move, so only include things that MUST be done then.
2496 Use PlayerFrame() instead for code that only needs to run once per server frame.
2497 frametime == 0 in the asynchronous code path.
2498
2499 TODO: move more stuff from here and PlayerThink() and ObserverOrSpectatorThink() to PlayerFrame() (frametime is always set there)
2500 =============
2501 */
2502 .float last_vehiclecheck;
2503 void PlayerPreThink (entity this)
2504 {
2505         WarpZone_PlayerPhysics_FixVAngle(this);
2506
2507         zoomstate_set = false;
2508
2509         MUTATOR_CALLHOOK(PlayerPreThink, this);
2510
2511         if(PHYS_INPUT_BUTTON_USE(this) && !CS(this).usekeypressed)
2512                 PlayerUseKey(this);
2513         CS(this).usekeypressed = PHYS_INPUT_BUTTON_USE(this);
2514
2515         if (IS_PLAYER(this)) {
2516                 if (IS_REAL_CLIENT(this) && time < CS(this).jointime + MIN_SPEC_TIME)
2517                         error("Client can't be spawned as player on connection!");
2518                 if(!PlayerThink(this))
2519                         return;
2520         }
2521         else if (game_stopped || intermission_running) {
2522                 if(intermission_running)
2523                         IntermissionThink(this);
2524                 return;
2525         }
2526         else if (IS_REAL_CLIENT(this) && CS(this).autojoin_checked <= 0 && time >= CS(this).jointime + MIN_SPEC_TIME)
2527         {
2528                 bool early_join_requested = (CS(this).autojoin_checked < 0);
2529                 CS(this).autojoin_checked = 1;
2530                 // don't do this in ClientConnect
2531                 // many things can go wrong if a client is spawned as player on connection
2532                 if (early_join_requested || MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
2533                         || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2534                                 && (!teamplay || autocvar_g_balance_teams)))
2535                 {
2536                         if(joinAllowed(this))
2537                                 Join(this, true);
2538                         return;
2539                 }
2540         }
2541         else if (IS_OBSERVER(this) || IS_SPEC(this)) {
2542                 ObserverOrSpectatorThink(this);
2543         }
2544
2545         // WEAPONTODO: Add weapon request for this
2546         if (!zoomstate_set) {
2547                 bool wep_zoomed = false;
2548                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2549                 {
2550                         .entity weaponentity = weaponentities[slot];
2551                         Weapon thiswep = this.(weaponentity).m_weapon;
2552                         if(thiswep != WEP_Null && thiswep.wr_zoom)
2553                                 wep_zoomed += thiswep.wr_zoom(thiswep, this);
2554                 }
2555                 SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
2556         }
2557
2558         // Voice sound effects
2559         if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
2560         {
2561                 CS(this).teamkill_soundtime = 0;
2562
2563                 entity e = CS(this).teamkill_soundsource;
2564                 entity oldpusher = e.pusher;
2565                 e.pusher = this;
2566                 PlayerSound(e, playersound_teamshoot, CH_VOICE, VOL_BASEVOICE, VOICETYPE_LASTATTACKER_ONLY);
2567                 e.pusher = oldpusher;
2568         }
2569
2570         if (CS(this).taunt_soundtime && time > CS(this).taunt_soundtime) {
2571                 CS(this).taunt_soundtime = 0;
2572                 PlayerSound(this, playersound_taunt, CH_VOICE, VOL_BASEVOICE, VOICETYPE_AUTOTAUNT);
2573         }
2574
2575         target_voicescript_next(this);
2576 }
2577
2578 void DrownPlayer(entity this)
2579 {
2580         if(IS_DEAD(this) || game_stopped || time < game_starttime || this.vehicle
2581                 || STAT(FROZEN, this) || this.watertype != CONTENT_WATER)
2582         {
2583                 STAT(AIR_FINISHED, this) = 0;
2584                 return;
2585         }
2586
2587         if (this.waterlevel != WATERLEVEL_SUBMERGED)
2588         {
2589                 if(STAT(AIR_FINISHED, this) && STAT(AIR_FINISHED, this) < time)
2590                         PlayerSound(this, playersound_gasp, CH_PLAYER, VOL_BASE, VOICETYPE_PLAYERSOUND);
2591                 STAT(AIR_FINISHED, this) = 0;
2592         }
2593         else
2594         {
2595                 if (!STAT(AIR_FINISHED, this))
2596                         STAT(AIR_FINISHED, this) = time + autocvar_g_balance_contents_drowndelay;
2597                 if (STAT(AIR_FINISHED, this) < time)
2598                 {       // drown!
2599                         if (this.pain_finished < time)
2600                         {
2601                                 Damage (this, NULL, NULL, autocvar_g_balance_contents_playerdamage_drowning * autocvar_g_balance_contents_damagerate, DEATH_DROWN.m_id, DMG_NOWEP, this.origin, '0 0 0');
2602                                 this.pain_finished = time + 0.5;
2603                         }
2604                 }
2605         }
2606 }
2607
2608 .bool move_qcphysics;
2609
2610 void Player_Physics(entity this)
2611 {
2612         this.movetype = (this.move_qcphysics) ? MOVETYPE_QCPLAYER : this.move_movetype;
2613
2614         if(!this.move_qcphysics)
2615                 return;
2616
2617         if(!frametime && !CS(this).pm_frametime)
2618                 return;
2619
2620         Movetype_Physics_NoMatchTicrate(this, CS(this).pm_frametime, true);
2621
2622         CS(this).pm_frametime = 0;
2623 }
2624
2625 /*
2626 =============
2627 PlayerPostThink
2628
2629 Called every frame for each real client by DP (and for each bot by StartFrame()),
2630 and when executing every asynchronous move, so only include things that MUST be done then.
2631 Use PlayerFrame() instead for code that only needs to run once per server frame.
2632 frametime == 0 in the asynchronous code path.
2633 =============
2634 */
2635 void PlayerPostThink (entity this)
2636 {
2637         Player_Physics(this);
2638
2639         if (IS_PLAYER(this)) {
2640                 if(this.death_time == time && IS_DEAD(this))
2641                 {
2642                         // player's bbox gets resized now, instead of in the damage event that killed the player,
2643                         // once all the damage events of this frame have been processed with normal size
2644                         this.maxs.z = 5;
2645                         setsize(this, this.mins, this.maxs);
2646                 }
2647                 DrownPlayer(this);
2648                 UpdateChatBubble(this);
2649                 if (CS(this).impulse) ImpulseCommands(this);
2650                 GetPressedKeys(this);
2651                 if (game_stopped)
2652                 {
2653                         CSQCMODEL_AUTOUPDATE(this);
2654                         return;
2655                 }
2656         }
2657         else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
2658         {
2659                 CS(this).pressedkeys = 0;
2660                 STAT(PRESSED_KEYS, this) = 0;
2661         }
2662
2663         CSQCMODEL_AUTOUPDATE(this);
2664 }
2665
2666 /*
2667 =============
2668 PlayerFrame
2669
2670 Called every frame for each client by StartFrame().
2671 Use this for code that only needs to run once per server frame.
2672 frametime is always set here.
2673 =============
2674 */
2675 void PlayerFrame (entity this)
2676 {
2677 // formerly PreThink code
2678         STAT(GUNALIGN, this) = CS_CVAR(this).cvar_cl_gunalign; // TODO
2679         STAT(MOVEVARS_CL_TRACK_CANJUMP, this) = CS_CVAR(this).cvar_cl_movement_track_canjump;
2680
2681         // physics frames: update anticheat stuff
2682         anticheat_prethink(this);
2683
2684         // Check if spectating is allowed
2685         if (blockSpectators && IS_REAL_CLIENT(this)
2686         && (IS_SPEC(this) || IS_OBSERVER(this)) && !INGAME(this)
2687         && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime))
2688         {
2689                 if (dropclient_schedule(this))
2690                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
2691         }
2692
2693         // Check for nameless players
2694         if (this.netname == "" || this.netname != CS(this).netname_previous)
2695         {
2696                 bool assume_unchanged = (CS(this).netname_previous == "");
2697                 if (autocvar_sv_name_maxlength > 0 && strlennocol(this.netname) > autocvar_sv_name_maxlength)
2698                 {
2699                         int new_length = textLengthUpToLength(this.netname, autocvar_sv_name_maxlength, strlennocol);
2700                         this.netname = strzone(strcat(substring(this.netname, 0, new_length), "^7"));
2701                         sprint(this, sprintf("Warning: your name is longer than %d characters, it has been truncated.\n", autocvar_sv_name_maxlength));
2702                         assume_unchanged = false;
2703                         // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2704                 }
2705                 if (isInvisibleString(this.netname))
2706                 {
2707                         this.netname = strzone(sprintf("Player#%d", this.playerid));
2708                         sprint(this, "Warning: invisible names are not allowed.\n");
2709                         assume_unchanged = false;
2710                         // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2711                 }
2712                 if (!assume_unchanged && autocvar_sv_eventlog)
2713                         GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this.netname, this.team, false)));
2714                 strcpy(CS(this).netname_previous, this.netname);
2715         }
2716
2717         // version nagging
2718         if (CS(this).version_nagtime && CS_CVAR(this).cvar_g_xonoticversion && time > CS(this).version_nagtime)
2719         {
2720                 CS(this).version_nagtime = 0;
2721                 if (strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "autobuild", 0) >= 0)
2722                 {
2723                         // git client
2724                 }
2725                 else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0)
2726                 {
2727                         // git server
2728                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2729                 }
2730                 else
2731                 {
2732                         int r = vercmp(CS_CVAR(this).cvar_g_xonoticversion, autocvar_g_xonoticversion);
2733                         if (r < 0) // old client
2734                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2735                         else if (r > 0) // old server
2736                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2737                 }
2738         }
2739
2740         // GOD MODE info
2741         if (!(this.flags & FL_GODMODE) && this.max_armorvalue)
2742         {
2743                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue);
2744                 this.max_armorvalue = 0;
2745         }
2746
2747         // FreezeTag
2748         if (IS_PLAYER(this) && time >= game_starttime)
2749         {
2750                 if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
2751                 {
2752                         STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
2753                         SetResourceExplicit(this, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
2754                         if (this.iceblock)
2755                                 this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
2756
2757                         if (STAT(REVIVE_PROGRESS, this) >= 1)
2758                                 Unfreeze(this, false);
2759                 }
2760                 else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
2761                 {
2762                         STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
2763                         SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
2764
2765                         if (GetResource(this, RES_HEALTH) < 1)
2766                         {
2767                                 if (this.vehicle)
2768                                         vehicles_exit(this.vehicle, VHEF_RELEASE);
2769                                 if(this.event_damage)
2770                                         this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
2771                         }
2772                         else if (STAT(REVIVE_PROGRESS, this) <= 0)
2773                                 Unfreeze(this, false);
2774                 }
2775         }
2776
2777         // Vehicles
2778         if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle)
2779         if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2780         {
2781                 FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
2782                 {
2783                         if(!it.owner)
2784                         {
2785                                 if(!it.team || SAME_TEAM(this, it))
2786                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER);
2787                                 else if(autocvar_g_vehicles_steal)
2788                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL);
2789                         }
2790                         else if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
2791                         {
2792                                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
2793                         }
2794                 });
2795
2796                 this.last_vehiclecheck = time + 1;
2797         }
2798
2799
2800
2801 // formerly PostThink code
2802         if (autocvar_sv_maxidle > 0 || ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0))
2803         if (IS_REAL_CLIENT(this))
2804         if (IS_PLAYER(this) || this.wants_join || autocvar_sv_maxidle_alsokickspectators)
2805         if (!intermission_running) // NextLevel() kills all centerprints after setting this true
2806         {
2807                 int totalClients = 0;
2808                 if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0)
2809                 {
2810                         // maxidle disabled in local matches by not counting clients (totalClients 0)
2811                         if (server_is_dedicated)
2812                         {
2813                                 FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
2814                                 {
2815                                         ++totalClients;
2816                                 });
2817                                 if (maxclients - totalClients > autocvar_sv_maxidle_slots)
2818                                         totalClients = 0;
2819                         }
2820                 }
2821                 else if ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0)
2822                 {
2823                         FOREACH_CLIENT(IS_REAL_CLIENT(it),
2824                         {
2825                                 ++totalClients;
2826                         });
2827                 }
2828
2829                 if (totalClients < autocvar_sv_maxidle_minplayers)
2830                 {
2831                         // idle kick disabled
2832                         CS(this).parm_idlesince = time;
2833                 }
2834                 else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
2835                 {
2836                         if (CS(this).idlekick_lasttimeleft)
2837                         {
2838                                 CS(this).idlekick_lasttimeleft = 0;
2839                                 Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_IDLING);
2840                         }
2841                 }
2842                 else
2843                 {
2844                         float maxidle_time = autocvar_sv_maxidle;
2845                         if ((IS_PLAYER(this) || this.wants_join)
2846                         && autocvar_sv_maxidle_playertospectator > 0)
2847                                 maxidle_time = autocvar_sv_maxidle_playertospectator;
2848                         float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
2849                         float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
2850                         if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
2851                         {
2852                                 if ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0)
2853                                 {
2854                                         if (!this.wants_join) // no countdown centreprint when getting kicked off the join queue
2855                                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
2856                                 }
2857                                 else
2858                                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
2859                         }
2860                         if (timeleft <= 0)
2861                         {
2862                                 if ((IS_PLAYER(this) || this.wants_join)
2863                                 && autocvar_sv_maxidle_playertospectator > 0)
2864                                 {
2865                                         if (this.wants_join)
2866                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING_QUEUE, this.netname, maxidle_time);
2867                                         else
2868                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
2869                                         PutObserverInServer(this, true, true);
2870                                         // when the player is kicked off the server, these are called in ClientDisconnect()
2871                                         if (!TeamBalance_QueuedPlayersTagIn(this))
2872                                         if (autocvar_g_balance_teams_remove)
2873                                                 TeamBalance_RemoveExcessPlayers(this);
2874                                 }
2875                                 else
2876                                 {
2877                                         if (dropclient_schedule(this))
2878                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time);
2879                                 }
2880                                 return;
2881                         }
2882                         else if (timeleft <= countdown_time
2883                         && !this.wants_join) // no countdown bangs when getting kicked off the join queue
2884                         {
2885                                 if (timeleft != CS(this).idlekick_lasttimeleft)
2886                                         play2(this, SND(TALK2));
2887                                 CS(this).idlekick_lasttimeleft = timeleft;
2888                         }
2889                 }
2890         }
2891
2892         CheatFrame(this);
2893
2894         if (game_stopped)
2895         {
2896                 this.solid = SOLID_NOT;
2897                 this.takedamage = DAMAGE_NO;
2898                 set_movetype(this, MOVETYPE_NONE);
2899                 CS(this).teamkill_complain = 0;
2900                 CS(this).teamkill_soundtime = 0;
2901                 CS(this).teamkill_soundsource = NULL;
2902         }
2903
2904         if (this.waypointsprite_attachedforcarrier) {
2905                 float hp = healtharmor_maxdamage(GetResource(this, RES_HEALTH), GetResource(this, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x;
2906                 WaypointSprite_UpdateHealth(this.waypointsprite_attachedforcarrier, hp);
2907         }
2908 }
2909
2910 // hack to copy the button fields from the client entity to the Client State
2911 void PM_UpdateButtons(entity this, entity store)
2912 {
2913         if(this.impulse)
2914                 store.impulse = this.impulse;
2915         this.impulse = 0;
2916
2917         bool typing = this.buttonchat || this.button12;
2918
2919         store.button0 = (typing) ? 0 : this.button0;
2920         //button1?!
2921         store.button2 = (typing) ? 0 : this.button2;
2922         store.button3 = (typing) ? 0 : this.button3;
2923         store.button4 = this.button4;
2924         store.button5 = (typing) ? 0 : this.button5;
2925         store.button6 = this.button6;
2926         store.button7 = this.button7;
2927         store.button8 = this.button8;
2928         store.button9 = this.button9;
2929         store.button10 = this.button10;
2930         store.button11 = this.button11;
2931         store.button12 = this.button12;
2932         store.button13 = this.button13;
2933         store.button14 = this.button14;
2934         store.button15 = this.button15;
2935         store.button16 = this.button16;
2936         store.buttonuse = this.buttonuse;
2937         store.buttonchat = this.buttonchat;
2938
2939         store.cursor_active = this.cursor_active;
2940         store.cursor_screen = this.cursor_screen;
2941         store.cursor_trace_start = this.cursor_trace_start;
2942         store.cursor_trace_endpos = this.cursor_trace_endpos;
2943         store.cursor_trace_ent = this.cursor_trace_ent;
2944
2945         store.ping = this.ping;
2946         store.ping_packetloss = this.ping_packetloss;
2947         store.ping_movementloss = this.ping_movementloss;
2948
2949         store.v_angle = this.v_angle;
2950         store.movement = this.movement;
2951 }
2952
2953 NET_HANDLE(fpsreport, bool)
2954 {
2955         int fps = ReadShort();
2956         PlayerScore_Set(sender, SP_FPS, fps);
2957         return true;
2958 }