]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/client.qc
Merge branch 'terencehill/menu_registries' 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         // DP model scaling uses 1/16 accuracy and 13/16 is closest to 56/69
647         this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) ? 0.8125 : autocvar_sv_player_scale);
648         this.fade_time = 0;
649         this.pain_finished = 0;
650         this.pushltime = 0;
651         setthink(this, func_null); // players have no think function
652         this.nextthink = 0;
653         this.dmg_team = 0;
654         PS(this).ballistics_density = autocvar_g_ballistics_density_player;
655
656         this.deadflag = DEAD_NO;
657
658         this.angles = spot.angles;
659         this.angles_z = 0; // never spawn tilted even if the spot says to
660         if (IS_BOT_CLIENT(this))
661         {
662                 this.v_angle = this.angles;
663                 bot_aim_reset(this);
664         }
665         this.fixangle = true; // turn this way immediately
666         this.oldvelocity = this.velocity = '0 0 0';
667         this.avelocity = '0 0 0';
668         this.punchangle = '0 0 0';
669         this.punchvector = '0 0 0';
670
671         STAT(REVIVE_PROGRESS, this) = 0;
672         this.revival_time = 0;
673
674         STAT(AIR_FINISHED, this) = 0;
675         this.waterlevel = WATERLEVEL_NONE;
676         this.watertype = CONTENT_EMPTY;
677
678         entity spawnevent = new_pure(spawnevent);
679         spawnevent.owner = this;
680         Net_LinkEntity(spawnevent, false, 0.5, SpawnEvent_Send);
681
682         // Cut off any still running player sounds.
683         stopsound(this, CH_PLAYER_SINGLE);
684
685         this.model = "";
686         FixPlayermodel(this);
687         this.drawonlytoclient = NULL;
688
689         this.viewloc = NULL;
690
691         for(int slot = 0; slot < MAX_AXH; ++slot)
692         {
693                 entity axh = this.(AuxiliaryXhair[slot]);
694                 this.(AuxiliaryXhair[slot]) = NULL;
695
696                 if(axh.owner == this && axh != NULL && !wasfreed(axh))
697                         delete(axh);
698         }
699
700         this.spawnpoint_targ = NULL;
701
702         UNSET_DUCKED(this);
703         this.view_ofs = STAT(PL_VIEW_OFS, this);
704         setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
705         this.spawnorigin = spot.origin;
706         setorigin(this, spot.origin + '0 0 1' * (1 - this.mins.z - 24));
707         // don't reset back to last position, even if new position is stuck in solid
708         this.oldorigin = this.origin;
709         if(this.conveyor)
710                 IL_REMOVE(g_conveyed, this);
711         this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player
712         if(this.swampslug)
713                 IL_REMOVE(g_swamped, this);
714         this.swampslug = NULL;
715         this.swamp_interval = 0;
716         if(this.ladder_entity)
717                 IL_REMOVE(g_ladderents, this);
718         this.ladder_entity = NULL;
719         IL_EACH(g_counters, it.realowner == this,
720         {
721                 delete(it);
722         });
723         STAT(HUD, this) = HUD_NORMAL;
724
725         this.event_damage = PlayerDamage;
726         this.event_heal = PlayerHeal;
727
728         this.draggable = func_null;
729
730         if(!this.bot_attack)
731                 IL_PUSH(g_bot_targets, this);
732         this.bot_attack = true;
733         if(!this.monster_attack)
734                 IL_PUSH(g_monster_targets, this);
735         this.monster_attack = true;
736         navigation_dynamicgoal_init(this, false);
737
738         PHYS_INPUT_BUTTON_ATCK(this) = PHYS_INPUT_BUTTON_JUMP(this) = PHYS_INPUT_BUTTON_ATCK2(this) = false;
739
740         // player was spectator
741         if (CS(this).killcount == FRAGS_SPECTATOR) {
742                 PlayerScore_Clear(this);
743                 CS(this).killcount = 0;
744                 CS(this).startplaytime = time;
745         }
746
747         for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
748         {
749                 .entity weaponentity = weaponentities[slot];
750                 CL_SpawnWeaponentity(this, weaponentity);
751         }
752         this.alpha = default_player_alpha;
753         this.colormod = '1 1 1' * autocvar_g_player_brightness;
754         this.exteriorweaponentity.alpha = default_weapon_alpha;
755
756         this.speedrunning = false;
757
758         this.counter_cnt = 0;
759         this.fragsfilter_cnt = 0;
760
761         target_voicescript_clear(this);
762
763         // reset fields the weapons may use
764         FOREACH(Weapons, true, {
765                 it.wr_resetplayer(it, this);
766                         // reload all reloadable weapons
767                 if (it.spawnflags & WEP_FLAG_RELOADABLE) {
768                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
769                         {
770                                 .entity weaponentity = weaponentities[slot];
771                                 this.(weaponentity).weapon_load[it.m_id] = it.reloading_ammo;
772                         }
773                 }
774         });
775
776         Unfreeze(this, false);
777
778         MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
779         {
780                 string s = spot.target;
781                 if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack
782                         spot.target = string_null;
783                 SUB_UseTargets(spot, this, NULL);
784                 if(g_assault || g_race)
785                         spot.target = s;
786         }
787
788         if (autocvar_spawn_debug)
789         {
790                 sprint(this, strcat("spawnpoint origin:  ", vtos(spot.origin), "\n"));
791                 delete(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
792         }
793
794         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
795         {
796                 .entity weaponentity = weaponentities[slot];
797                 entity w_ent = this.(weaponentity);
798                 if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
799                         w_ent.m_switchweapon = w_getbestweapon(this, weaponentity);
800                 else
801                         w_ent.m_switchweapon = WEP_Null;
802                 w_ent.m_weapon = WEP_Null;
803                 w_ent.weaponname = "";
804                 w_ent.m_switchingweapon = WEP_Null;
805                 w_ent.cnt = -1;
806         }
807
808         MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
809
810         if (CS(this).impulse) ImpulseCommands(this);
811
812         W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
813         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
814         {
815                 .entity weaponentity = weaponentities[slot];
816                 W_WeaponFrame(this, weaponentity);
817         }
818
819         if (!warmup_stage && !this.alivetime)
820                 this.alivetime = time;
821
822         antilag_clear(this, CS(this));
823
824         if (warmup_stage < 0 || warmup_stage > 1)
825                 ReadyCount();
826 }
827
828 /** Called when a client spawns in the server */
829 void PutClientInServer(entity this)
830 {
831         if (IS_REAL_CLIENT(this)) {
832                 msg_entity = this;
833                 WriteByte(MSG_ONE, SVC_SETVIEW);
834                 WriteEntity(MSG_ONE, this);
835         }
836         if (game_stopped)
837                 TRANSMUTE(Observer, this);
838
839         bool use_spawnpoint = (!this.enemy); // check this.enemy here since SetSpectatee will clear it
840         SetSpectatee(this, NULL);
841
842         // reset player keys
843         if(PS(this))
844                 PS(this).itemkeys = 0;
845
846         MUTATOR_CALLHOOK(PutClientInServer, this);
847
848         if (IS_OBSERVER(this)) {
849                 PutObserverInServer(this, false, use_spawnpoint);
850         } else if (IS_PLAYER(this)) {
851                 PutPlayerInServer(this);
852         }
853
854         bot_relinkplayerlist();
855 }
856
857 // TODO do we need all these fields, or should we stop autodetecting runtime
858 // changes and just have a console command to update this?
859 bool ClientInit_SendEntity(entity this, entity to, int sf)
860 {
861         WriteHeader(MSG_ENTITY, _ENT_CLIENT_INIT);
862         return = true;
863         msg_entity = to;
864         // MSG_INIT replacement
865         // TODO: make easier to use
866         Registry_send_all();
867         W_PROP_reload(MSG_ONE, to);
868         ClientInit_misc(this);
869         MUTATOR_CALLHOOK(Ent_Init);
870 }
871 void ClientInit_misc(entity this)
872 {
873         int channel = MSG_ONE;
874         WriteHeader(channel, ENT_CLIENT_INIT);
875         WriteByte(channel, g_nexball_meter_period * 32);
876         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[0]));
877         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[1]));
878         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[2]));
879         WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[3]));
880         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[0]));
881         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[1]));
882         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[2]));
883         WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[3]));
884
885         if(autocvar_sv_foginterval && world.fog != "")
886                 WriteString(channel, world.fog);
887         else
888                 WriteString(channel, "");
889         WriteByte(channel, this.count * 255.0); // g_balance_armor_blockpercent
890         WriteByte(channel, this.cnt * 255.0); // g_balance_damagepush_speedfactor
891         WriteByte(channel, serverflags);
892         WriteCoord(channel, autocvar_g_trueaim_minrange);
893 }
894
895 void ClientInit_CheckUpdate(entity this)
896 {
897         this.nextthink = time;
898         if(this.count != autocvar_g_balance_armor_blockpercent)
899         {
900                 this.count = autocvar_g_balance_armor_blockpercent;
901                 this.SendFlags |= 1;
902         }
903         if(this.cnt != autocvar_g_balance_damagepush_speedfactor)
904         {
905                 this.cnt = autocvar_g_balance_damagepush_speedfactor;
906                 this.SendFlags |= 1;
907         }
908 }
909
910 void ClientInit_Spawn()
911 {
912         entity e = new_pure(clientinit);
913         setthink(e, ClientInit_CheckUpdate);
914         Net_LinkEntity(e, false, 0, ClientInit_SendEntity);
915
916         ClientInit_CheckUpdate(e);
917 }
918
919 /*
920 =============
921 SetNewParms
922 =============
923 */
924 void SetNewParms ()
925 {
926         // initialize parms for a new player
927         parm1 = -(86400 * 366);
928
929         MUTATOR_CALLHOOK(SetNewParms);
930 }
931
932 /*
933 =============
934 SetChangeParms
935 =============
936 */
937 void SetChangeParms (entity this)
938 {
939         // save parms for level change
940         parm1 = CS(this).parm_idlesince - time;
941
942         MUTATOR_CALLHOOK(SetChangeParms);
943 }
944
945 /*
946 =============
947 DecodeLevelParms
948 =============
949 */
950 void DecodeLevelParms(entity this)
951 {
952         // load parms
953         CS(this).parm_idlesince = parm1;
954         if (CS(this).parm_idlesince == -(86400 * 366))
955                 CS(this).parm_idlesince = time;
956
957         // whatever happens, allow 60 seconds of idling directly after connect for map loading
958         CS(this).parm_idlesince = max(CS(this).parm_idlesince, time - autocvar_sv_maxidle + 60);
959
960         MUTATOR_CALLHOOK(DecodeLevelParms);
961 }
962
963 void FixClientCvars(entity e)
964 {
965         // send prediction settings to the client
966         if(autocvar_g_antilag == 3) // client side hitscan
967                 stuffcmd(e, "cl_cmd settemp cl_prydoncursor_notrace 0\n");
968         if(autocvar_sv_gentle)
969                 stuffcmd(e, "cl_cmd settemp cl_gentle 1\n");
970
971         stuffcmd(e, sprintf("\ncl_jumpspeedcap_min \"%s\"\n", autocvar_sv_jumpspeedcap_min));
972         stuffcmd(e, sprintf("\ncl_jumpspeedcap_max \"%s\"\n", autocvar_sv_jumpspeedcap_max));
973
974         stuffcmd(e, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
975
976         MUTATOR_CALLHOOK(FixClientCvars, e);
977 }
978
979 bool findinlist_abbrev(string tofind, string list)
980 {
981         if(list == "" || tofind == "")
982                 return false; // empty list or search, just return
983
984         // this function allows abbreviated strings!
985         FOREACH_WORD(list, it != "" && it == substring(tofind, 0, strlen(it)),
986         {
987                 return true;
988         });
989
990         return false;
991 }
992
993 bool PlayerInIPList(entity p, string iplist)
994 {
995         // some safety checks (never allow local?)
996         if(p.netaddress == "local" || p.netaddress == "" || !IS_REAL_CLIENT(p))
997                 return false;
998
999         return findinlist_abbrev(p.netaddress, iplist);
1000 }
1001
1002 bool PlayerInIDList(entity p, string idlist)
1003 {
1004         // NOTE: we do NOT check crypto_idfp_signed here, an unsigned ID is fine too for this
1005         if(!p.crypto_idfp)
1006                 return false;
1007
1008         return findinlist_abbrev(p.crypto_idfp, idlist);
1009 }
1010
1011 bool PlayerInList(entity player, string list)
1012 {
1013         if (list == "")
1014                 return false;
1015         return boolean(PlayerInIDList(player, list) || PlayerInIPList(player, list));
1016 }
1017
1018 #ifdef DP_EXT_PRECONNECT
1019 /*
1020 =============
1021 ClientPreConnect
1022
1023 Called once (not at each match start) when a client begins a connection to the server
1024 =============
1025 */
1026 void ClientPreConnect(entity this)
1027 {
1028         if(autocvar_sv_eventlog)
1029         {
1030                 GameLogEcho(sprintf(":connect:%d:%d:%s",
1031                         this.playerid,
1032                         etof(this),
1033                         ((IS_REAL_CLIENT(this)) ? this.netaddress : "bot")
1034                 ));
1035         }
1036 }
1037 #endif
1038
1039 // NOTE csqc uses the active mutators list sent by this function
1040 // to understand which mutators are enabled
1041 // also note that they aren't all registered mutators, e.g. jetpack, low gravity
1042 void SendWelcomeMessage(entity this, int msg_type)
1043 {
1044         WriteByte(msg_type, boolean(autocvar_g_campaign));
1045         if (boolean(autocvar_g_campaign))
1046         {
1047                 WriteByte(msg_type, Campaign_GetLevelNum());
1048                 return;
1049         }
1050         WriteString(msg_type, autocvar_hostname);
1051         WriteString(msg_type, autocvar_g_xonoticversion);
1052         WriteByte(msg_type, CS(this).version_mismatch);
1053         WriteByte(msg_type, (CS(this).version < autocvar_gameversion));
1054         WriteByte(msg_type, autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers);
1055         WriteByte(msg_type, GetPlayerLimit());
1056
1057         MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
1058         string modifications = M_ARGV(0, string);
1059
1060         if (!g_weaponarena && cvar("g_balance_blaster_weaponstartoverride") == 0)
1061                 modifications = strcat(modifications, ", No start weapons");
1062         if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
1063                 modifications = strcat(modifications, ", Low gravity");
1064         if(g_weapon_stay && !g_cts)
1065                 modifications = strcat(modifications, ", Weapons stay");
1066         if(autocvar_g_jetpack)
1067                 modifications = strcat(modifications, ", Jetpack");
1068         modifications = substring(modifications, 2, strlen(modifications) - 2);
1069
1070         WriteString(msg_type, modifications);
1071
1072         WriteString(msg_type, g_weaponarena_list);
1073
1074         if(cache_lastmutatormsg != autocvar_g_mutatormsg)
1075         {
1076                 strcpy(cache_lastmutatormsg, autocvar_g_mutatormsg);
1077                 strcpy(cache_mutatormsg, cache_lastmutatormsg);
1078         }
1079
1080         WriteString(msg_type, cache_mutatormsg);
1081
1082         WriteString(msg_type, strreplace("\\n", "\n", autocvar_sv_motd));
1083 }
1084
1085 /**
1086 =============
1087 ClientConnect
1088
1089 Called when a client connects to the server
1090 =============
1091 */
1092 void ClientConnect(entity this)
1093 {
1094         if (Ban_MaybeEnforceBanOnce(this)) return;
1095         assert(!IS_CLIENT(this), return);
1096         this.flags |= FL_CLIENT;
1097         assert(player_count >= 0, player_count = 0);
1098
1099         TRANSMUTE(Client, this);
1100         CS(this).version_nagtime = time + 10 + random() * 10;
1101
1102         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_CONNECT, this.netname);
1103
1104         bot_clientconnect(this);
1105
1106         Player_DetermineForcedTeam(this);
1107
1108         TRANSMUTE(Observer, this);
1109
1110         PlayerStats_GameReport_AddEvent(sprintf("kills-%d", this.playerid));
1111
1112         // always track bots, don't ask for cl_allow_uidtracking
1113         if (IS_BOT_CLIENT(this))
1114                 PlayerStats_GameReport_AddPlayer(this);
1115         else
1116                 CS(this).allowed_timeouts = autocvar_sv_timeout_number;
1117
1118         if (autocvar_sv_eventlog)
1119                 GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this.netname, this.team, false)));
1120
1121         CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
1122
1123         stuffcmd(this, clientstuff, "\n");
1124         stuffcmd(this, "cl_particles_reloadeffects\n"); // TODO do we still need this?
1125
1126         FixClientCvars(this);
1127
1128         // get version info from player
1129         stuffcmd(this, "cmd clientversion $gameversion\n");
1130
1131         // notify about available teams
1132         if (teamplay)
1133         {
1134                 entity balance = TeamBalance_CheckAllowedTeams(this);
1135                 int t = TeamBalance_GetAllowedTeams(balance);
1136                 TeamBalance_Destroy(balance);
1137                 stuffcmd(this, sprintf("set _teams_available %d\n", t));
1138         }
1139         else
1140         {
1141                 stuffcmd(this, "set _teams_available 0\n");
1142         }
1143
1144         bot_relinkplayerlist();
1145
1146         CS(this).spectatortime = time;
1147         if (blockSpectators)
1148         {
1149                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
1150         }
1151
1152         CS(this).jointime = time;
1153
1154         if (IS_REAL_CLIENT(this))
1155         {
1156                 if (g_weaponarena_weapons == WEPSET(TUBA))
1157                         stuffcmd(this, "cl_cmd settemp chase_active 1\n");
1158                 // quickmenu file must be put in a subfolder with an unique name
1159                 // to reduce chances of overriding custom client quickmenus
1160                 if (waypointeditor_enabled)
1161                         stuffcmd(this, sprintf("cl_cmd settemp _hud_panel_quickmenu_file_from_server %s\n", "wpeditor.txt"));
1162                 else if (autocvar_sv_quickmenu_file != "" && strstrofs(autocvar_sv_quickmenu_file, "/", 0) && fexists(autocvar_sv_quickmenu_file))
1163                         stuffcmd(this, sprintf("cl_cmd settemp _hud_panel_quickmenu_file_from_server %s\n", autocvar_sv_quickmenu_file));
1164         }
1165
1166         if (!autocvar_sv_foginterval && world.fog != "")
1167                 stuffcmd(this, strcat("\nfog ", world.fog, "\nr_fog_exp2 0\nr_drawfog 1\n"));
1168
1169         if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
1170                 if(!MUTATOR_CALLHOOK(HideTeamNagger, this))
1171                         send_CSQC_teamnagger();
1172
1173         CSQCMODEL_AUTOINIT(this);
1174
1175         CS(this).model_randomizer = random();
1176
1177         if (IS_REAL_CLIENT(this))
1178                 sv_notice_join(this);
1179
1180         this.move_qcphysics = true;
1181
1182         // update physics stats (players can spawn before physics runs)
1183         Physics_UpdateStats(this);
1184
1185         IL_EACH(g_initforplayer, it.init_for_player, {
1186                 it.init_for_player(it, this);
1187         });
1188
1189         Handicap_Initialize(this);
1190
1191         // playban
1192         if (PlayerInList(this, autocvar_g_playban_list))
1193                 TRANSMUTE(Observer, this);
1194
1195         if (PlayerInList(this, autocvar_g_chatban_list)) // chatban
1196                 CS(this).muted = true;
1197
1198         MUTATOR_CALLHOOK(ClientConnect, this);
1199
1200         if (player_count == 1)
1201         {
1202                 if (autocvar_sv_autopause && server_is_dedicated)
1203                         setpause(0);
1204                 localcmd("\nsv_hook_firstjoin\n");
1205         }
1206 }
1207 /*
1208 =============
1209 ClientDisconnect
1210
1211 Called when a client disconnects from the server
1212 =============
1213 */
1214 .entity chatbubbleentity;
1215 void player_powerups_remove_all(entity this);
1216
1217 void ClientDisconnect(entity this)
1218 {
1219         assert(IS_CLIENT(this), return);
1220
1221         /* from "ignore" command */
1222         strfree(this.ignore_list);
1223         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it.ignore_list,
1224         {
1225                 if(it.crypto_idfp && it.crypto_idfp != "")
1226                         continue;
1227                 string mylist = ignore_removefromlist(it, this);
1228                 if(it.ignore_list)
1229                         strunzone(it.ignore_list);
1230
1231                 it.ignore_list = strzone(mylist);
1232         });
1233         /* from "ignore" command */
1234
1235         PlayerStats_GameReport_FinalizePlayer(this);
1236         if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
1237         if (CS(this).active_minigame) part_minigame(this);
1238         if (IS_PLAYER(this)) Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
1239
1240         if (autocvar_sv_eventlog)
1241                 GameLogEcho(strcat(":part:", ftos(this.playerid)));
1242
1243         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_DISCONNECT, this.netname);
1244
1245         if(IS_SPEC(this))
1246                 SetSpectatee(this, NULL);
1247
1248         MUTATOR_CALLHOOK(ClientDisconnect, this);
1249
1250         strfree(CS(this).netname_previous); // needs to be before the CS entity is removed!
1251         strfree(CS_CVAR(this).weaponorder_byimpulse);
1252         ClientState_detach(this);
1253
1254         Portal_ClearAll(this);
1255
1256         Unfreeze(this, false);
1257
1258         RemoveGrapplingHooks(this);
1259
1260         // Here, everything has been done that requires this player to be a client.
1261
1262         this.flags &= ~FL_CLIENT;
1263
1264         if (this.chatbubbleentity) delete(this.chatbubbleentity);
1265         if (this.killindicator) delete(this.killindicator);
1266
1267         IL_EACH(g_counters, it.realowner == this,
1268         {
1269                 delete(it);
1270         });
1271
1272         WaypointSprite_PlayerGone(this);
1273
1274         bot_relinkplayerlist();
1275
1276         strfree(this.clientstatus);
1277         if (this.personal) delete(this.personal);
1278
1279         this.playerid = 0;
1280         if (warmup_stage || game_starttime > time) ReadyCount();
1281         if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
1282
1283         player_powerups_remove_all(this); // stop powerup sound
1284
1285         ONREMOVE(this);
1286
1287         if (player_count == 0)
1288                 localcmd("\nsv_hook_lastleave\n");
1289 }
1290
1291 void ChatBubbleThink(entity this)
1292 {
1293         this.nextthink = time;
1294         if ((this.owner.alpha < 0) || this.owner.chatbubbleentity != this)
1295         {
1296                 if(this.owner) // but why can that ever be NULL?
1297                         this.owner.chatbubbleentity = NULL;
1298                 delete(this);
1299                 return;
1300         }
1301
1302         this.mdl = "";
1303
1304         if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
1305         {
1306                 if ( CS(this.owner).active_minigame && PHYS_INPUT_BUTTON_MINIGAME(this.owner) )
1307                         this.mdl = "models/sprites/minigame_busy.iqm";
1308                 else if (PHYS_INPUT_BUTTON_CHAT(this.owner))
1309                         this.mdl = "models/misc/chatbubble.spr";
1310         }
1311
1312         if ( this.model != this.mdl )
1313                 _setmodel(this, this.mdl);
1314
1315 }
1316
1317 void UpdateChatBubble(entity this)
1318 {
1319         if (this.alpha < 0)
1320                 return;
1321         // spawn a chatbubble entity if needed
1322         if (!this.chatbubbleentity)
1323         {
1324                 this.chatbubbleentity = new(chatbubbleentity);
1325                 this.chatbubbleentity.owner = this;
1326                 this.chatbubbleentity.exteriormodeltoclient = this;
1327                 setthink(this.chatbubbleentity, ChatBubbleThink);
1328                 this.chatbubbleentity.nextthink = time;
1329                 setmodel(this.chatbubbleentity, MDL_CHAT); // precision set below
1330                 //setorigin(this.chatbubbleentity, this.origin + '0 0 15' + this.maxs_z * '0 0 1');
1331                 setorigin(this.chatbubbleentity, '0 0 15' + this.maxs_z * '0 0 1');
1332                 setattachment(this.chatbubbleentity, this, "");  // sticks to moving player better, also conserves bandwidth
1333                 this.chatbubbleentity.mdl = this.chatbubbleentity.model;
1334                 //this.chatbubbleentity.model = "";
1335                 this.chatbubbleentity.effects = EF_LOWPRECISION;
1336         }
1337 }
1338
1339 void calculate_player_respawn_time(entity this)
1340 {
1341         if(MUTATOR_CALLHOOK(CalculateRespawnTime, this))
1342                 return;
1343
1344         float gametype_setting_tmp;
1345         float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
1346         float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
1347         float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
1348         float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
1349         float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
1350         float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
1351
1352         float pcount = 1;  // Include myself whether or not team is already set right and I'm a "player".
1353         if (teamplay)
1354         {
1355                 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1356                         if(it.team == this.team)
1357                                 ++pcount;
1358                 });
1359                 if (sdelay_small_count == 0)
1360                         sdelay_small_count = 1;
1361                 if (sdelay_large_count == 0)
1362                         sdelay_large_count = 1;
1363         }
1364         else
1365         {
1366                 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1367                         ++pcount;
1368                 });
1369                 if (sdelay_small_count == 0)
1370                 {
1371                         if (IS_INDEPENDENT_PLAYER(this))
1372                         {
1373                                 // Players play independently. No point in requiring enemies.
1374                                 sdelay_small_count = 1;
1375                         }
1376                         else
1377                         {
1378                                 // Players play AGAINST each other. Enemies required.
1379                                 sdelay_small_count = 2;
1380                         }
1381                 }
1382                 if (sdelay_large_count == 0)
1383                 {
1384                         if (IS_INDEPENDENT_PLAYER(this))
1385                         {
1386                                 // Players play independently. No point in requiring enemies.
1387                                 sdelay_large_count = 1;
1388                         }
1389                         else
1390                         {
1391                                 // Players play AGAINST each other. Enemies required.
1392                                 sdelay_large_count = 2;
1393                         }
1394                 }
1395         }
1396
1397         float sdelay;
1398
1399         if (pcount <= sdelay_small_count)
1400                 sdelay = sdelay_small;
1401         else if (pcount >= sdelay_large_count)
1402                 sdelay = sdelay_large;
1403         else  // NOTE: this case implies sdelay_large_count > sdelay_small_count.
1404                 sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
1405
1406         if(waves)
1407                 this.respawn_time = ceil((time + sdelay) / waves) * waves;
1408         else
1409                 this.respawn_time = time + sdelay;
1410
1411         if(sdelay < sdelay_max)
1412                 this.respawn_time_max = time + sdelay_max;
1413         else
1414                 this.respawn_time_max = this.respawn_time;
1415
1416         if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
1417                 this.respawn_countdown = 10; // first number to count down from is 10
1418         else
1419                 this.respawn_countdown = -1; // do not count down
1420
1421         if(autocvar_g_forced_respawn)
1422                 this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
1423 }
1424
1425 // LordHavoc: this hack will be removed when proper _pants/_shirt layers are
1426 // added to the model skins
1427 /*void UpdateColorModHack()
1428 {
1429         float c;
1430         c = this.clientcolors & 15;
1431         // LordHavoc: only bothering to support white, green, red, yellow, blue
1432              if (!teamplay) this.colormod = '0 0 0';
1433         else if (c ==  0) this.colormod = '1.00 1.00 1.00';
1434         else if (c ==  3) this.colormod = '0.10 1.73 0.10';
1435         else if (c ==  4) this.colormod = '1.73 0.10 0.10';
1436         else if (c == 12) this.colormod = '1.22 1.22 0.10';
1437         else if (c == 13) this.colormod = '0.10 0.10 1.73';
1438         else this.colormod = '1 1 1';
1439 }*/
1440
1441 void respawn(entity this)
1442 {
1443         bool damagedbycontents_prev = this.damagedbycontents;
1444         if(this.alpha >= 0)
1445         {
1446                 if(autocvar_g_respawn_ghosts)
1447                 {
1448                         this.solid = SOLID_NOT;
1449                         this.takedamage = DAMAGE_NO;
1450                         this.damagedbycontents = false;
1451                         set_movetype(this, MOVETYPE_FLY);
1452                         this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
1453                         this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
1454                         this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
1455                         this.alpha = min(this.alpha, autocvar_g_respawn_ghosts_alpha);
1456                         Send_Effect(EFFECT_RESPAWN_GHOST, this.origin, '0 0 0', 1);
1457                         if(autocvar_g_respawn_ghosts_time > 0)
1458                                 SUB_SetFade(this, time + autocvar_g_respawn_ghosts_time, autocvar_g_respawn_ghosts_fadetime);
1459                 }
1460                 else
1461                         SUB_SetFade (this, time, 1); // fade out the corpse immediately
1462         }
1463
1464         CopyBody(this, 1);
1465         this.damagedbycontents = damagedbycontents_prev;
1466
1467         this.effects |= EF_NODRAW; // prevent another CopyBody
1468         PutClientInServer(this);
1469 }
1470
1471 void play_countdown(entity this, float finished, Sound samp)
1472 {
1473         TC(Sound, samp);
1474         float time_left = finished - time;
1475         if(IS_REAL_CLIENT(this) && time_left < 6 && floor(time_left - frametime) != floor(time_left))
1476                 sound(this, CH_INFO, samp, VOL_BASE, ATTEN_NORM);
1477 }
1478
1479 // it removes special powerups not handled by StatusEffects
1480 void player_powerups_remove_all(entity this)
1481 {
1482         if (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1483         {
1484                 // don't play the poweroff sound when the game restarts or the player disconnects
1485                 if (time > game_starttime + 1 && IS_CLIENT(this)
1486                         && !(start_items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS)))
1487                 {
1488                         sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM);
1489                 }
1490                 if (this.items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1491                         stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound
1492                 this.items -= (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS));
1493         }
1494 }
1495
1496 void player_powerups(entity this)
1497 {
1498         if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped)
1499                 this.modelflags |= MF_ROCKET;
1500         else
1501                 this.modelflags &= ~MF_ROCKET;
1502
1503         this.effects &= ~EF_NODEPTHTEST;
1504
1505         if (IS_DEAD(this))
1506                 player_powerups_remove_all(this);
1507
1508         if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed
1509                 return;
1510
1511         // add a way to see what the items were BEFORE all of these checks for the mutator hook
1512         int items_prev = this.items;
1513
1514         if (!MUTATOR_IS_ENABLED(mutator_instagib))
1515         {
1516                 // NOTE: superweapons are a special case and as such are handled here instead of the status effects system
1517                 if (this.items & IT_SUPERWEAPON)
1518                 {
1519                         if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
1520                         {
1521                                 StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL);
1522                                 this.items = this.items - (this.items & IT_SUPERWEAPON);
1523                                 //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname);
1524                                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST);
1525                         }
1526                         else if (this.items & IT_UNLIMITED_SUPERWEAPONS)
1527                         {
1528                                 // don't let them run out
1529                         }
1530                         else
1531                         {
1532                                 play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
1533                                 if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
1534                                 {
1535                                         this.items = this.items - (this.items & IT_SUPERWEAPON);
1536                                         STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1537                                         //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_BROKEN, this.netname);
1538                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_BROKEN);
1539                                 }
1540                         }
1541                 }
1542                 else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
1543                 {
1544                         if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
1545                         {
1546                                 this.items = this.items | IT_SUPERWEAPON;
1547                                 if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
1548                                 {
1549                                         if(!g_cts)
1550                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_PICKUP, this.netname);
1551                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_PICKUP);
1552                                 }
1553                         }
1554                         else
1555                         {
1556                                 if(StatusEffects_active(STATUSEFFECT_Superweapons, this))
1557                                         StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT);
1558                                 STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1559                         }
1560                 }
1561                 else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame!
1562                 {
1563                         StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR);
1564                 }
1565         }
1566
1567         if(autocvar_g_nodepthtestplayers)
1568                 this.effects = this.effects | EF_NODEPTHTEST;
1569
1570         if(autocvar_g_fullbrightplayers)
1571                 this.effects = this.effects | EF_FULLBRIGHT;
1572
1573         MUTATOR_CALLHOOK(PlayerPowerups, this, items_prev);
1574 }
1575
1576 float CalcRegen(float current, float stable, float regenfactor, float regenframetime)
1577 {
1578         if(current > stable)
1579                 return current;
1580         else if(current > stable - 0.25) // when close enough, "snap"
1581                 return stable;
1582         else
1583                 return min(stable, current + (stable - current) * regenfactor * regenframetime);
1584 }
1585
1586 float CalcRot(float current, float stable, float rotfactor, float rotframetime)
1587 {
1588         if(current < stable)
1589                 return current;
1590         else if(current < stable + 0.25) // when close enough, "snap"
1591                 return stable;
1592         else
1593                 return max(stable, current + (stable - current) * rotfactor * rotframetime);
1594 }
1595
1596 void RotRegen(entity this, Resource res, float limit_mod,
1597         float regenstable, float regenfactor, float regenlinear, float regenframetime,
1598         float rotstable, float rotfactor, float rotlinear, float rotframetime)
1599 {
1600         float old = GetResource(this, res);
1601         float current = old;
1602         if(current > rotstable)
1603         {
1604                 if(rotframetime > 0)
1605                 {
1606                         current = CalcRot(current, rotstable, rotfactor, rotframetime);
1607                         current = max(rotstable, current - rotlinear * rotframetime);
1608                 }
1609         }
1610         else if(current < regenstable)
1611         {
1612                 if(regenframetime > 0)
1613                 {
1614                         current = CalcRegen(current, regenstable, regenfactor, regenframetime);
1615                         current = min(regenstable, current + regenlinear * regenframetime);
1616                 }
1617         }
1618
1619         float limit = GetResourceLimit(this, res) * limit_mod;
1620         if(current > limit)
1621                 current = limit;
1622
1623         if (current != old)
1624                 SetResource(this, res, current);
1625 }
1626
1627 void player_regen(entity this)
1628 {
1629         float max_mod, regen_mod, rot_mod, limit_mod;
1630         max_mod = regen_mod = rot_mod = limit_mod = 1;
1631
1632         float regen_health = autocvar_g_balance_health_regen;
1633         float regen_health_linear = autocvar_g_balance_health_regenlinear;
1634         float regen_health_rot = autocvar_g_balance_health_rot;
1635         float regen_health_rotlinear = autocvar_g_balance_health_rotlinear;
1636         float regen_health_stable = autocvar_g_balance_health_regenstable;
1637         float regen_health_rotstable = autocvar_g_balance_health_rotstable;
1638         bool mutator_returnvalue = MUTATOR_CALLHOOK(PlayerRegen, this, max_mod, regen_mod, rot_mod, limit_mod, regen_health, regen_health_linear, regen_health_rot,
1639                 regen_health_rotlinear, regen_health_stable, regen_health_rotstable);
1640         max_mod = M_ARGV(1, float);
1641         regen_mod = M_ARGV(2, float);
1642         rot_mod = M_ARGV(3, float);
1643         limit_mod = M_ARGV(4, float);
1644         regen_health = M_ARGV(5, float);
1645         regen_health_linear = M_ARGV(6, float);
1646         regen_health_rot = M_ARGV(7, float);
1647         regen_health_rotlinear = M_ARGV(8, float);
1648         regen_health_stable = M_ARGV(9, float);
1649         regen_health_rotstable = M_ARGV(10, float);
1650
1651         float rotstable, regenstable, rotframetime, regenframetime;
1652
1653         if(!mutator_returnvalue)
1654         if(!STAT(FROZEN, this))
1655         {
1656                 regenstable = autocvar_g_balance_armor_regenstable;
1657                 rotstable = autocvar_g_balance_armor_rotstable;
1658                 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1659                 rotframetime = (time > this.pauserotarmor_finished) ? (rot_mod * frametime) : 0;
1660                 RotRegen(this, RES_ARMOR, limit_mod,
1661                         regenstable, autocvar_g_balance_armor_regen, autocvar_g_balance_armor_regenlinear, regenframetime,
1662                         rotstable, autocvar_g_balance_armor_rot, autocvar_g_balance_armor_rotlinear, rotframetime);
1663
1664                 // NOTE: max_mod is only applied to health
1665                 regenstable = regen_health_stable * max_mod;
1666                 rotstable = regen_health_rotstable * max_mod;
1667                 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1668                 rotframetime = (time > this.pauserothealth_finished) ? (rot_mod * frametime) : 0;
1669                 RotRegen(this, RES_HEALTH, limit_mod,
1670                         regenstable, regen_health, regen_health_linear, regenframetime,
1671                         rotstable, regen_health_rot, regen_health_rotlinear, rotframetime);
1672         }
1673
1674         // if player rotted to death...  die!
1675         // check this outside above checks, as player may still be able to rot to death
1676         if(GetResource(this, RES_HEALTH) < 1)
1677         {
1678                 if(this.vehicle)
1679                         vehicles_exit(this.vehicle, VHEF_RELEASE);
1680                 if(this.event_damage)
1681                         this.event_damage(this, this, this, 1, DEATH_ROT.m_id, DMG_NOWEP, this.origin, '0 0 0');
1682         }
1683
1684         if (!(this.items & IT_UNLIMITED_AMMO))
1685         {
1686                 regenstable = autocvar_g_balance_fuel_regenstable;
1687                 rotstable = autocvar_g_balance_fuel_rotstable;
1688                 regenframetime = ((time > this.pauseregen_finished) && (this.items & ITEM_JetpackRegen.m_itemid)) ? frametime : 0;
1689                 rotframetime = (time > this.pauserotfuel_finished) ? frametime : 0;
1690                 RotRegen(this, RES_FUEL, 1,
1691                         regenstable, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear, regenframetime,
1692                         rotstable, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, rotframetime);
1693         }
1694 }
1695
1696 bool zoomstate_set;
1697 void SetZoomState(entity this, float newzoom)
1698 {
1699         if(newzoom != CS(this).zoomstate)
1700         {
1701                 CS(this).zoomstate = newzoom;
1702                 ClientData_Touch(this);
1703         }
1704         zoomstate_set = true;
1705 }
1706
1707 void GetPressedKeys(entity this)
1708 {
1709         MUTATOR_CALLHOOK(GetPressedKeys, this);
1710         if (game_stopped)
1711         {
1712                 CS(this).pressedkeys = 0;
1713                 STAT(PRESSED_KEYS, this) = 0;
1714                 return;
1715         }
1716
1717         // NOTE: GetPressedKeys and PM_dodging_GetPressedKeys use similar code
1718         int keys = STAT(PRESSED_KEYS, this);
1719         keys = BITSET(keys, KEY_FORWARD,        CS(this).movement.x > 0);
1720         keys = BITSET(keys, KEY_BACKWARD,       CS(this).movement.x < 0);
1721         keys = BITSET(keys, KEY_RIGHT,          CS(this).movement.y > 0);
1722         keys = BITSET(keys, KEY_LEFT,           CS(this).movement.y < 0);
1723
1724         keys = BITSET(keys, KEY_JUMP,           PHYS_INPUT_BUTTON_JUMP(this));
1725         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
1726         keys = BITSET(keys, KEY_ATCK,           PHYS_INPUT_BUTTON_ATCK(this));
1727         keys = BITSET(keys, KEY_ATCK2,          PHYS_INPUT_BUTTON_ATCK2(this));
1728         CS(this).pressedkeys = keys; // store for other users
1729
1730         STAT(PRESSED_KEYS, this) = keys;
1731 }
1732
1733 /*
1734 ======================
1735 spectate mode routines
1736 ======================
1737 */
1738
1739 void SpectateCopy(entity this, entity spectatee)
1740 {
1741         TC(Client, this); TC(Client, spectatee);
1742
1743         MUTATOR_CALLHOOK(SpectateCopy, spectatee, this);
1744         PS(this) = PS(spectatee);
1745         this.armortype = spectatee.armortype;
1746         SetResourceExplicit(this, RES_ARMOR, GetResource(spectatee, RES_ARMOR));
1747         SetResourceExplicit(this, RES_CELLS, GetResource(spectatee, RES_CELLS));
1748         SetResourceExplicit(this, RES_PLASMA, GetResource(spectatee, RES_PLASMA));
1749         SetResourceExplicit(this, RES_SHELLS, GetResource(spectatee, RES_SHELLS));
1750         SetResourceExplicit(this, RES_BULLETS, GetResource(spectatee, RES_BULLETS));
1751         SetResourceExplicit(this, RES_ROCKETS, GetResource(spectatee, RES_ROCKETS));
1752         SetResourceExplicit(this, RES_FUEL, GetResource(spectatee, RES_FUEL));
1753         this.effects = spectatee.effects & EFMASK_CHEAP; // eat performance
1754         SetResourceExplicit(this, RES_HEALTH, GetResource(spectatee, RES_HEALTH));
1755         CS(this).impulse = 0;
1756         this.disableclientprediction = 1; // no need to run prediction on a spectator
1757         this.items = spectatee.items;
1758         STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
1759         STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
1760         STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee);
1761         STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
1762         STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
1763         this.punchangle = spectatee.punchangle;
1764         this.view_ofs = spectatee.view_ofs;
1765         this.velocity = spectatee.velocity;
1766         this.dmg_take = spectatee.dmg_take;
1767         this.dmg_save = spectatee.dmg_save;
1768         this.dmg_inflictor = spectatee.dmg_inflictor;
1769         this.v_angle = spectatee.v_angle;
1770         this.angles = spectatee.v_angle;
1771         STAT(FROZEN, this) = STAT(FROZEN, spectatee);
1772         STAT(REVIVE_PROGRESS, this) = STAT(REVIVE_PROGRESS, spectatee);
1773         this.viewloc = spectatee.viewloc;
1774         if(!PHYS_INPUT_BUTTON_USE(this) && STAT(CAMERA_SPECTATOR, this) != 2)
1775                 this.fixangle = true;
1776         setorigin(this, spectatee.origin);
1777         setsize(this, spectatee.mins, spectatee.maxs);
1778         SetZoomState(this, CS(spectatee).zoomstate);
1779
1780     anticheat_spectatecopy(this, spectatee);
1781         STAT(HUD, this) = STAT(HUD, spectatee);
1782         if(spectatee.vehicle)
1783     {
1784         this.angles = spectatee.v_angle;
1785
1786         //this.fixangle = false;
1787         //this.velocity = spectatee.vehicle.velocity;
1788         this.vehicle_health = spectatee.vehicle_health;
1789         this.vehicle_shield = spectatee.vehicle_shield;
1790         this.vehicle_energy = spectatee.vehicle_energy;
1791         this.vehicle_ammo1 = spectatee.vehicle_ammo1;
1792         this.vehicle_ammo2 = spectatee.vehicle_ammo2;
1793         this.vehicle_reload1 = spectatee.vehicle_reload1;
1794         this.vehicle_reload2 = spectatee.vehicle_reload2;
1795
1796         //msg_entity = this;
1797
1798        // WriteByte (MSG_ONE, SVC_SETVIEWANGLES);
1799             //WriteAngle(MSG_ONE,  spectatee.v_angle.x);
1800            // WriteAngle(MSG_ONE,  spectatee.v_angle.y);
1801            // WriteAngle(MSG_ONE,  spectatee.v_angle.z);
1802
1803         //WriteByte (MSG_ONE, SVC_SETVIEW);
1804         //    WriteEntity(MSG_ONE, this);
1805         //makevectors(spectatee.v_angle);
1806         //setorigin(this, spectatee.origin - v_forward * 400 + v_up * 300);*/
1807     }
1808 }
1809
1810 bool SpectateUpdate(entity this)
1811 {
1812         if(!this.enemy)
1813                 return false;
1814
1815         if(!IS_PLAYER(this.enemy) || this == this.enemy)
1816         {
1817                 SetSpectatee(this, NULL);
1818                 return false;
1819         }
1820
1821         SpectateCopy(this, this.enemy);
1822
1823         return true;
1824 }
1825
1826 bool SpectateSet(entity this)
1827 {
1828         if(!IS_PLAYER(this.enemy))
1829                 return false;
1830
1831         ClientData_Touch(this.enemy);
1832
1833         msg_entity = this;
1834         WriteByte(MSG_ONE, SVC_SETVIEW);
1835         WriteEntity(MSG_ONE, this.enemy);
1836         set_movetype(this, MOVETYPE_NONE);
1837         accuracy_resend(this);
1838
1839         if(!SpectateUpdate(this))
1840                 PutObserverInServer(this, false, true);
1841
1842         return true;
1843 }
1844
1845 void SetSpectatee_status(entity this, int spectatee_num)
1846 {
1847         int oldspectatee_status = CS(this).spectatee_status;
1848         CS(this).spectatee_status = spectatee_num;
1849
1850         if (CS(this).spectatee_status != oldspectatee_status)
1851         {
1852                 if (STAT(PRESSED_KEYS, this))
1853                 {
1854                         CS(this).pressedkeys = 0;
1855                         STAT(PRESSED_KEYS, this) = 0;
1856                 }
1857                 ClientData_Touch(this);
1858                 if (g_race || g_cts) race_InitSpectator();
1859         }
1860 }
1861
1862 void SetSpectatee(entity this, entity spectatee)
1863 {
1864         if(IS_BOT_CLIENT(this))
1865                 return; // bots abuse .enemy, this code is useless to them
1866
1867         entity old_spectatee = this.enemy;
1868
1869         this.enemy = spectatee;
1870
1871         // WEAPONTODO
1872         // these are required to fix the spectator bug with arc
1873         if(old_spectatee)
1874         {
1875                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1876                 {
1877                         .entity weaponentity = weaponentities[slot];
1878                         if(old_spectatee.(weaponentity).arc_beam)
1879                                 old_spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1880                 }
1881         }
1882         if(spectatee)
1883         {
1884                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1885                 {
1886                         .entity weaponentity = weaponentities[slot];
1887                         if(spectatee.(weaponentity).arc_beam)
1888                                 spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1889                 }
1890         }
1891
1892         if (spectatee)
1893                 SetSpectatee_status(this, etof(spectatee));
1894
1895         // needed to update spectator list
1896         if(old_spectatee) { ClientData_Touch(old_spectatee); }
1897 }
1898
1899 bool Spectate(entity this, entity pl)
1900 {
1901         if(MUTATOR_CALLHOOK(SpectateSet, this, pl))
1902                 return false;
1903         pl = M_ARGV(1, entity);
1904
1905         SetSpectatee(this, pl);
1906         return SpectateSet(this);
1907 }
1908
1909 bool SpectateNext(entity this)
1910 {
1911         entity ent = find(this.enemy, classname, STR_PLAYER);
1912
1913         if (MUTATOR_CALLHOOK(SpectateNext, this, ent))
1914                 ent = M_ARGV(1, entity);
1915         else if (!ent)
1916                 ent = find(ent, classname, STR_PLAYER);
1917
1918         if(ent) { SetSpectatee(this, ent); }
1919
1920         return SpectateSet(this);
1921 }
1922
1923 bool SpectatePrev(entity this)
1924 {
1925         // NOTE: chain order is from the highest to the lower entnum (unlike find)
1926         entity ent = findchain(classname, STR_PLAYER);
1927         if (!ent) // no player
1928                 return false;
1929
1930         entity first = ent;
1931         // skip players until current spectated player
1932         if(this.enemy)
1933         while(ent && ent != this.enemy)
1934                 ent = ent.chain;
1935
1936         switch (MUTATOR_CALLHOOK(SpectatePrev, this, ent, first))
1937         {
1938                 case MUT_SPECPREV_FOUND:
1939                         ent = M_ARGV(1, entity);
1940                         break;
1941                 case MUT_SPECPREV_RETURN:
1942                         return true;
1943                 case MUT_SPECPREV_CONTINUE:
1944                 default:
1945                 {
1946                         if(ent.chain)
1947                                 ent = ent.chain;
1948                         else
1949                                 ent = first;
1950                         break;
1951                 }
1952         }
1953
1954         SetSpectatee(this, ent);
1955         return SpectateSet(this);
1956 }
1957
1958 /*
1959 =============
1960 ShowRespawnCountdown()
1961
1962 Update a respawn countdown display.
1963 =============
1964 */
1965 void ShowRespawnCountdown(entity this)
1966 {
1967         float number;
1968         if(!IS_DEAD(this)) // just respawned?
1969                 return;
1970         else
1971         {
1972                 number = ceil(this.respawn_time - time);
1973                 if(number <= 0)
1974                         return;
1975                 if(number <= this.respawn_countdown)
1976                 {
1977                         this.respawn_countdown = number - 1;
1978                         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
1979                                 { Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_RESPAWN, number)); }
1980                 }
1981         }
1982 }
1983
1984 .bool team_selected;
1985 bool ShowTeamSelection(entity this)
1986 {
1987         if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
1988                 return false;
1989         if (frametime) // once per frame is more than enough
1990                 stuffcmd(this, "_scoreboard_team_selection 1\n");
1991         return true;
1992 }
1993 void Join(entity this)
1994 {
1995         if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
1996                 ReadyRestart(true);
1997
1998         TRANSMUTE(Player, this);
1999
2000         if(!this.team_selected)
2001         if(autocvar_g_campaign || autocvar_g_balance_teams)
2002                 TeamBalance_JoinBestTeam(this);
2003
2004         if(autocvar_g_campaign)
2005                 campaign_bots_may_start = true;
2006
2007         Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
2008
2009         PutClientInServer(this);
2010
2011         if(IS_PLAYER(this))
2012         if(teamplay && this.team != -1)
2013         {
2014         }
2015         else
2016                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
2017         this.team_selected = false;
2018 }
2019
2020 int GetPlayerLimit()
2021 {
2022         if(g_duel)
2023                 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)
2024         // don't return map_maxplayers during intermission as it would interfere with MapHasRightSize()
2025         int player_limit = (autocvar_g_maxplayers >= 0 || intermission_running) ? autocvar_g_maxplayers : map_maxplayers;
2026         MUTATOR_CALLHOOK(GetPlayerLimit, player_limit);
2027         player_limit = M_ARGV(0, int);
2028         return player_limit < maxclients ? player_limit : 0;
2029 }
2030
2031 /**
2032  * Determines whether the player is allowed to join. This depends on cvar
2033  * g_maxplayers, if it isn't used this function always return true, otherwise
2034  * it checks whether the number of currently playing players exceeds g_maxplayers.
2035  * @return int number of free slots for players, 0 if none
2036  */
2037 int nJoinAllowed(entity this, entity ignore)
2038 {
2039         if(!ignore)
2040         // this is called that way when checking if anyone may be able to join (to build qcstatus)
2041         // so report 0 free slots if restricted
2042         {
2043                 if(autocvar_g_forced_team_otherwise == "spectate")
2044                         return 0;
2045                 if(autocvar_g_forced_team_otherwise == "spectator")
2046                         return 0;
2047         }
2048
2049         if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2050                 return 0; // forced spectators can never join
2051
2052         static float msg_time = 0;
2053         if(this && !INGAME(this) && ignore && PlayerInList(this, autocvar_g_playban_list))
2054         {
2055                 if(time > msg_time)
2056                 {
2057                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PLAYBAN);
2058                         msg_time = time + 0.5;
2059                 }
2060                 return 0;
2061         }
2062
2063         // TODO simplify this
2064         int totalClients = 0;
2065         int currentlyPlaying = 0;
2066         FOREACH_CLIENT(true, {
2067                 if(it != ignore)
2068                         ++totalClients;
2069                 if(IS_REAL_CLIENT(it) && (IS_PLAYER(it) || INGAME(it)))
2070                         ++currentlyPlaying;
2071         });
2072
2073         int player_limit = GetPlayerLimit();
2074
2075         int free_slots = 0;
2076         if (!player_limit)
2077                 free_slots = maxclients - totalClients;
2078         else if(player_limit > 0 && currentlyPlaying < player_limit)
2079                 free_slots = min(maxclients - totalClients, player_limit - currentlyPlaying);
2080
2081         if(this && !INGAME(this) && ignore && !free_slots && time > msg_time)
2082         {
2083                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT, player_limit);
2084                 msg_time = time + 0.5;
2085         }
2086
2087         return free_slots;
2088 }
2089
2090 bool joinAllowed(entity this)
2091 {
2092         if (CS(this).version_mismatch) return false;
2093         if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
2094         if (!nJoinAllowed(this, this)) return false;
2095         if (teamplay && lockteams) return false;
2096         if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
2097         if (ShowTeamSelection(this)) return false;
2098         return true;
2099 }
2100
2101 void show_entnum(entity this)
2102 {
2103         // waypoint editor implements a similar feature for waypoints
2104         if (waypointeditor_enabled)
2105                 return;
2106
2107         if (wasfreed(this.wp_aimed))
2108                 this.wp_aimed = NULL;
2109
2110         WarpZone_crosshair_trace_plusvisibletriggers(this);
2111         entity ent = NULL;
2112         if (trace_ent)
2113         {
2114                 ent = trace_ent;
2115                 if (ent != this.wp_aimed)
2116                 {
2117                         string str = sprintf(
2118                                 "^7ent #%d\n^8 netname: ^3%s\n^8 classname: ^5%s\n^8 origin: ^2'%s'",
2119                                 etof(ent), ent.netname, ent.classname, vtos(ent.origin));
2120                         debug_text_3d((ent.absmin + ent.absmax) * 0.5, str, 0, 7, '0 0 0');
2121                 }
2122         }
2123         if (this.wp_aimed != ent)
2124                 this.wp_aimed = ent;
2125 }
2126
2127 .string shootfromfixedorigin;
2128 .bool dualwielding_prev;
2129 bool PlayerThink(entity this)
2130 {
2131         if (game_stopped || intermission_running) {
2132                 this.modelflags &= ~MF_ROCKET;
2133                 if(intermission_running)
2134                         IntermissionThink(this);
2135                 return false;
2136         }
2137
2138         if (timeout_status == TIMEOUT_ACTIVE) {
2139                 // don't allow the player to turn around while game is paused
2140                 // FIXME turn this into CSQC stuff
2141                 this.v_angle = this.lastV_angle;
2142                 this.angles = this.lastV_angle;
2143                 this.fixangle = true;
2144         }
2145
2146         if (frametime) player_powerups(this);
2147
2148         if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2149
2150         if (IS_DEAD(this)) {
2151                 if (this.personal && g_race_qualifying) {
2152                         if (time > this.respawn_time) {
2153                                 STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second
2154                                 respawn(this);
2155                                 CS(this).impulse = CHIMPULSE_SPEEDRUN.impulse;
2156                         }
2157                 } else {
2158                         if (frametime) player_anim(this);
2159
2160                         if (this.respawn_flags & RESPAWN_DENY)
2161                         {
2162                                 STAT(RESPAWN_TIME, this) = 0;
2163                                 return false;
2164                         }
2165
2166                         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));
2167
2168                         switch(this.deadflag)
2169                         {
2170                                 case DEAD_DYING:
2171                                 {
2172                                         if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max))
2173                                                 this.deadflag = DEAD_RESPAWNING;
2174                                         else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)))
2175                                                 this.deadflag = DEAD_DEAD;
2176                                         break;
2177                                 }
2178                                 case DEAD_DEAD:
2179                                 {
2180                                         if (button_pressed)
2181                                                 this.deadflag = DEAD_RESPAWNABLE;
2182                                         else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))
2183                                                 this.deadflag = DEAD_RESPAWNING;
2184                                         break;
2185                                 }
2186                                 case DEAD_RESPAWNABLE:
2187                                 {
2188                                         if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE))
2189                                                 this.deadflag = DEAD_RESPAWNING;
2190                                         break;
2191                                 }
2192                                 case DEAD_RESPAWNING:
2193                                 {
2194                                         if (time > this.respawn_time)
2195                                         {
2196                                                 this.respawn_time = time + 1; // only retry once a second
2197                                                 this.respawn_time_max = this.respawn_time;
2198                                                 respawn(this);
2199                                         }
2200                                         break;
2201                                 }
2202                         }
2203
2204                         ShowRespawnCountdown(this);
2205
2206                         if (this.respawn_flags & RESPAWN_SILENT)
2207                                 STAT(RESPAWN_TIME, this) = 0;
2208                         else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max)
2209                         {
2210                                 if (time < this.respawn_time)
2211                                         STAT(RESPAWN_TIME, this) = this.respawn_time;
2212                                 else if (this.deadflag != DEAD_RESPAWNING)
2213                                         STAT(RESPAWN_TIME, this) = -this.respawn_time_max;
2214                         }
2215                         else
2216                                 STAT(RESPAWN_TIME, this) = this.respawn_time;
2217                 }
2218
2219                 // if respawning, invert stat_respawn_time to indicate this, the client translates it
2220                 if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0)
2221                         STAT(RESPAWN_TIME, this) *= -1;
2222
2223                 return false;
2224         }
2225
2226         FixPlayermodel(this);
2227
2228         if (this.shootfromfixedorigin != autocvar_g_shootfromfixedorigin) {
2229                 this.shootfromfixedorigin = autocvar_g_shootfromfixedorigin;
2230                 stuffcmd(this, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
2231         }
2232
2233         // reset gun alignment when dual wielding status changes
2234         // to ensure guns are always aligned right and left
2235         bool dualwielding = W_DualWielding(this);
2236         if(this.dualwielding_prev != dualwielding)
2237         {
2238                 W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
2239                 this.dualwielding_prev = dualwielding;
2240         }
2241
2242         // LordHavoc: allow firing on move frames (sub-ticrate), this gives better timing on slow servers
2243         //if(frametime)
2244         {
2245                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2246                 {
2247                         .entity weaponentity = weaponentities[slot];
2248                         if(WEP_CVAR(vortex, charge_always))
2249                                 W_Vortex_Charge(this, weaponentity, frametime);
2250                         W_WeaponFrame(this, weaponentity);
2251                 }
2252         }
2253
2254         if (frametime)
2255         {
2256                 // WEAPONTODO: Add a weapon request for this
2257                 // rot vortex charge to the charge limit
2258                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2259                 {
2260                         .entity weaponentity = weaponentities[slot];
2261                         if (WEP_CVAR(vortex, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(vortex, charge_limit) && this.(weaponentity).vortex_charge_rottime < time)
2262                                 this.(weaponentity).vortex_charge = bound(WEP_CVAR(vortex, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(vortex, charge_rot_rate) * frametime / W_TICSPERFRAME, 1);
2263                 }
2264
2265                 player_regen(this);
2266                 player_anim(this);
2267                 this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
2268         }
2269
2270         monsters_setstatus(this);
2271
2272         return true;
2273 }
2274
2275 .bool would_spectate;
2276 // merged SpectatorThink and ObserverThink (old names are here so you can grep for them)
2277 void ObserverOrSpectatorThink(entity this)
2278 {
2279         bool is_spec = IS_SPEC(this);
2280         if ( CS(this).impulse )
2281         {
2282                 int r = MinigameImpulse(this, CS(this).impulse);
2283                 if (!is_spec || r)
2284                         CS(this).impulse = 0;
2285
2286                 if (is_spec && CS(this).impulse == IMP_weapon_drop.impulse)
2287                 {
2288                         STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
2289                         CS(this).impulse = 0;
2290                         return;
2291                 }
2292         }
2293
2294         if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2295
2296         if (IS_BOT_CLIENT(this) && !CS(this).autojoin_checked)
2297         {
2298                 CS(this).autojoin_checked = true;
2299                 TRANSMUTE(Player, this);
2300                 PutClientInServer(this);
2301
2302                 .entity weaponentity = weaponentities[0];
2303                 if(this.(weaponentity).m_weapon == WEP_Null)
2304                         W_NextWeapon(this, 0, weaponentity);
2305
2306                 return;
2307         }
2308
2309         if (this.flags & FL_JUMPRELEASED) {
2310                 if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
2311                         this.flags &= ~FL_JUMPRELEASED;
2312                         this.flags |= FL_SPAWNING;
2313                 } 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)))
2314                         || (!is_spec && ((PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) || this.would_spectate))) {
2315                         this.flags &= ~FL_JUMPRELEASED;
2316                         if(SpectateNext(this)) {
2317                                 TRANSMUTE(Spectator, this);
2318                         } else if (is_spec) {
2319                                 TRANSMUTE(Observer, this);
2320                                 PutClientInServer(this);
2321                         }
2322                         else
2323                                 this.would_spectate = false; // unable to spectate anyone
2324                         if (is_spec)
2325                                 CS(this).impulse = 0;
2326                 } else if (is_spec) {
2327                         if(CS(this).impulse == 12 || CS(this).impulse == 16  || CS(this).impulse == 19 || (CS(this).impulse >= 220 && CS(this).impulse <= 229)) {
2328                                 this.flags &= ~FL_JUMPRELEASED;
2329                                 if(SpectatePrev(this)) {
2330                                         TRANSMUTE(Spectator, this);
2331                                 } else {
2332                                         TRANSMUTE(Observer, this);
2333                                         PutClientInServer(this);
2334                                 }
2335                                 CS(this).impulse = 0;
2336                         } else if(PHYS_INPUT_BUTTON_ATCK2(this)) {
2337                                 if(!observe_blocked_if_eliminated || !INGAME(this)) {
2338                                         this.would_spectate = false;
2339                                         this.flags &= ~FL_JUMPRELEASED;
2340                                         TRANSMUTE(Observer, this);
2341                                         PutClientInServer(this);
2342                                 }
2343                         } else if(!SpectateUpdate(this) && !SpectateNext(this)) {
2344                                 PutObserverInServer(this, false, true);
2345                                 this.would_spectate = true;
2346                         }
2347                 }
2348                 else {
2349                         bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating;
2350                         if (PHYS_INPUT_BUTTON_USE(this))
2351                                 wouldclip = !wouldclip;
2352                         int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
2353                         set_movetype(this, preferred_movetype);
2354                 }
2355         } else { // jump pressed
2356                 if ((is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)))
2357                         || (!is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this)))) {
2358                         this.flags |= FL_JUMPRELEASED;
2359                         // primary attack pressed
2360                         if(this.flags & FL_SPAWNING)
2361                         {
2362                                 this.flags &= ~FL_SPAWNING;
2363                                 if(joinAllowed(this))
2364                                         Join(this);
2365                                 else if(time < CS(this).jointime + MIN_SPEC_TIME)
2366                                         CS(this).autojoin_checked = -1;
2367                                 return;
2368                         }
2369                 }
2370                 if(is_spec && !SpectateUpdate(this))
2371                         PutObserverInServer(this, false, true);
2372         }
2373         if (is_spec)
2374                 this.flags |= FL_CLIENT | FL_NOTARGET;
2375 }
2376
2377 void PlayerUseKey(entity this)
2378 {
2379         if (!IS_PLAYER(this))
2380                 return;
2381
2382         if(this.vehicle)
2383         {
2384                 if(!game_stopped)
2385                 {
2386                         vehicles_exit(this.vehicle, VHEF_NORMAL);
2387                         return;
2388                 }
2389         }
2390         else if(autocvar_g_vehicles_enter)
2391         {
2392                 if(!game_stopped && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2393                 {
2394                         entity head, closest_target = NULL;
2395                         head = WarpZone_FindRadius(this.origin, autocvar_g_vehicles_enter_radius, true);
2396
2397                         while(head) // find the closest acceptable target to enter
2398                         {
2399                                 if(IS_VEHICLE(head) && !IS_DEAD(head) && head.takedamage != DAMAGE_NO)
2400                                 if(!head.owner || ((head.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(head.owner, this)))
2401                                 {
2402                                         if(closest_target)
2403                                         {
2404                                                 if(vlen2(this.origin - head.origin) < vlen2(this.origin - closest_target.origin))
2405                                                 { closest_target = head; }
2406                                         }
2407                                         else { closest_target = head; }
2408                                 }
2409
2410                                 head = head.chain;
2411                         }
2412
2413                         if(closest_target) { vehicles_enter(this, closest_target); return; }
2414                 }
2415         }
2416
2417         // a use key was pressed; call handlers
2418         MUTATOR_CALLHOOK(PlayerUseKey, this);
2419 }
2420
2421
2422 /*
2423 =============
2424 PlayerPreThink
2425
2426 Called every frame for each real client by DP (and for each bot by StartFrame()),
2427 and when executing every asynchronous move, so only include things that MUST be done then.
2428 Use PlayerFrame() instead for code that only needs to run once per server frame.
2429 frametime == 0 in the asynchronous code path.
2430
2431 TODO: move more stuff from here and PlayerThink() and ObserverOrSpectatorThink() to PlayerFrame() (frametime is always set there)
2432 =============
2433 */
2434 .float last_vehiclecheck;
2435 void PlayerPreThink (entity this)
2436 {
2437         WarpZone_PlayerPhysics_FixVAngle(this);
2438
2439         zoomstate_set = false;
2440
2441         MUTATOR_CALLHOOK(PlayerPreThink, this);
2442
2443         if(PHYS_INPUT_BUTTON_USE(this) && !CS(this).usekeypressed)
2444                 PlayerUseKey(this);
2445         CS(this).usekeypressed = PHYS_INPUT_BUTTON_USE(this);
2446
2447         if (IS_PLAYER(this)) {
2448                 if (IS_REAL_CLIENT(this) && time < CS(this).jointime + MIN_SPEC_TIME)
2449                         error("Client can't be spawned as player on connection!");
2450                 if(!PlayerThink(this))
2451                         return;
2452         }
2453         else if (game_stopped || intermission_running) {
2454                 if(intermission_running)
2455                         IntermissionThink(this);
2456                 return;
2457         }
2458         else if (IS_REAL_CLIENT(this) && CS(this).autojoin_checked <= 0 && time >= CS(this).jointime + MIN_SPEC_TIME)
2459         {
2460                 bool early_join_requested = (CS(this).autojoin_checked < 0);
2461                 CS(this).autojoin_checked = 1;
2462                 // don't do this in ClientConnect
2463                 // many things can go wrong if a client is spawned as player on connection
2464                 if (early_join_requested || MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
2465                         || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2466                                 && (!teamplay || autocvar_g_balance_teams)))
2467                 {
2468                         if(joinAllowed(this))
2469                                 Join(this);
2470                         return;
2471                 }
2472         }
2473         else if (IS_OBSERVER(this) || IS_SPEC(this)) {
2474                 ObserverOrSpectatorThink(this);
2475         }
2476
2477         // WEAPONTODO: Add weapon request for this
2478         if (!zoomstate_set) {
2479                 bool wep_zoomed = false;
2480                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2481                 {
2482                         .entity weaponentity = weaponentities[slot];
2483                         Weapon thiswep = this.(weaponentity).m_weapon;
2484                         if(thiswep != WEP_Null && thiswep.wr_zoom)
2485                                 wep_zoomed += thiswep.wr_zoom(thiswep, this);
2486                 }
2487                 SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
2488         }
2489
2490         // Voice sound effects
2491         if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
2492         {
2493                 CS(this).teamkill_soundtime = 0;
2494
2495                 entity e = CS(this).teamkill_soundsource;
2496                 entity oldpusher = e.pusher;
2497                 e.pusher = this;
2498                 PlayerSound(e, playersound_teamshoot, CH_VOICE, VOL_BASEVOICE, VOICETYPE_LASTATTACKER_ONLY);
2499                 e.pusher = oldpusher;
2500         }
2501
2502         if (CS(this).taunt_soundtime && time > CS(this).taunt_soundtime) {
2503                 CS(this).taunt_soundtime = 0;
2504                 PlayerSound(this, playersound_taunt, CH_VOICE, VOL_BASEVOICE, VOICETYPE_AUTOTAUNT);
2505         }
2506
2507         target_voicescript_next(this);
2508 }
2509
2510 void DrownPlayer(entity this)
2511 {
2512         if(IS_DEAD(this) || game_stopped || time < game_starttime || this.vehicle
2513                 || STAT(FROZEN, this) || this.watertype != CONTENT_WATER)
2514         {
2515                 STAT(AIR_FINISHED, this) = 0;
2516                 return;
2517         }
2518
2519         if (this.waterlevel != WATERLEVEL_SUBMERGED)
2520         {
2521                 if(STAT(AIR_FINISHED, this) && STAT(AIR_FINISHED, this) < time)
2522                         PlayerSound(this, playersound_gasp, CH_PLAYER, VOL_BASE, VOICETYPE_PLAYERSOUND);
2523                 STAT(AIR_FINISHED, this) = 0;
2524         }
2525         else
2526         {
2527                 if (!STAT(AIR_FINISHED, this))
2528                         STAT(AIR_FINISHED, this) = time + autocvar_g_balance_contents_drowndelay;
2529                 if (STAT(AIR_FINISHED, this) < time)
2530                 {       // drown!
2531                         if (this.pain_finished < time)
2532                         {
2533                                 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');
2534                                 this.pain_finished = time + 0.5;
2535                         }
2536                 }
2537         }
2538 }
2539
2540 .bool move_qcphysics;
2541
2542 void Player_Physics(entity this)
2543 {
2544         this.movetype = (this.move_qcphysics) ? MOVETYPE_QCPLAYER : this.move_movetype;
2545
2546         if(!this.move_qcphysics)
2547                 return;
2548
2549         if(!frametime && !CS(this).pm_frametime)
2550                 return;
2551
2552         Movetype_Physics_NoMatchTicrate(this, CS(this).pm_frametime, true);
2553
2554         CS(this).pm_frametime = 0;
2555 }
2556
2557 /*
2558 =============
2559 PlayerPostThink
2560
2561 Called every frame for each real client by DP (and for each bot by StartFrame()),
2562 and when executing every asynchronous move, so only include things that MUST be done then.
2563 Use PlayerFrame() instead for code that only needs to run once per server frame.
2564 frametime == 0 in the asynchronous code path.
2565 =============
2566 */
2567 void PlayerPostThink (entity this)
2568 {
2569         Player_Physics(this);
2570
2571         if (IS_PLAYER(this)) {
2572                 if(this.death_time == time && IS_DEAD(this))
2573                 {
2574                         // player's bbox gets resized now, instead of in the damage event that killed the player,
2575                         // once all the damage events of this frame have been processed with normal size
2576                         this.maxs.z = 5;
2577                         setsize(this, this.mins, this.maxs);
2578                 }
2579                 DrownPlayer(this);
2580                 UpdateChatBubble(this);
2581                 if (CS(this).impulse) ImpulseCommands(this);
2582                 GetPressedKeys(this);
2583                 if (game_stopped)
2584                 {
2585                         CSQCMODEL_AUTOUPDATE(this);
2586                         return;
2587                 }
2588         }
2589         else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
2590         {
2591                 CS(this).pressedkeys = 0;
2592                 STAT(PRESSED_KEYS, this) = 0;
2593         }
2594
2595         CSQCMODEL_AUTOUPDATE(this);
2596 }
2597
2598 /*
2599 =============
2600 PlayerFrame
2601
2602 Called every frame for each client by StartFrame().
2603 Use this for code that only needs to run once per server frame.
2604 frametime is always set here.
2605 =============
2606 */
2607 void PlayerFrame (entity this)
2608 {
2609 // formerly PreThink code
2610         STAT(GUNALIGN, this) = CS_CVAR(this).cvar_cl_gunalign; // TODO
2611         STAT(MOVEVARS_CL_TRACK_CANJUMP, this) = CS_CVAR(this).cvar_cl_movement_track_canjump;
2612
2613         // physics frames: update anticheat stuff
2614         anticheat_prethink(this);
2615
2616         // Check if spectating is allowed
2617         if (blockSpectators && IS_REAL_CLIENT(this)
2618         && (IS_SPEC(this) || IS_OBSERVER(this)) && !INGAME(this)
2619         && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime))
2620         {
2621                 if (dropclient_schedule(this))
2622                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
2623         }
2624
2625         // Check for nameless players
2626         if (this.netname == "" || this.netname != CS(this).netname_previous)
2627         {
2628                 bool assume_unchanged = (CS(this).netname_previous == "");
2629                 if (autocvar_sv_name_maxlength > 0 && strlennocol(this.netname) > autocvar_sv_name_maxlength)
2630                 {
2631                         int new_length = textLengthUpToLength(this.netname, autocvar_sv_name_maxlength, strlennocol);
2632                         this.netname = strzone(strcat(substring(this.netname, 0, new_length), "^7"));
2633                         sprint(this, sprintf("Warning: your name is longer than %d characters, it has been truncated.\n", autocvar_sv_name_maxlength));
2634                         assume_unchanged = false;
2635                         // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2636                 }
2637                 if (isInvisibleString(this.netname))
2638                 {
2639                         this.netname = strzone(sprintf("Player#%d", this.playerid));
2640                         sprint(this, "Warning: invisible names are not allowed.\n");
2641                         assume_unchanged = false;
2642                         // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2643                 }
2644                 if (!assume_unchanged && autocvar_sv_eventlog)
2645                         GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this.netname, this.team, false)));
2646                 strcpy(CS(this).netname_previous, this.netname);
2647         }
2648
2649         // version nagging
2650         if (CS(this).version_nagtime && CS_CVAR(this).cvar_g_xonoticversion && time > CS(this).version_nagtime)
2651         {
2652                 CS(this).version_nagtime = 0;
2653                 if (strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "autobuild", 0) >= 0)
2654                 {
2655                         // git client
2656                 }
2657                 else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0)
2658                 {
2659                         // git server
2660                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2661                 }
2662                 else
2663                 {
2664                         int r = vercmp(CS_CVAR(this).cvar_g_xonoticversion, autocvar_g_xonoticversion);
2665                         if (r < 0) // old client
2666                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2667                         else if (r > 0) // old server
2668                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2669                 }
2670         }
2671
2672         // GOD MODE info
2673         if (!(this.flags & FL_GODMODE) && this.max_armorvalue)
2674         {
2675                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue);
2676                 this.max_armorvalue = 0;
2677         }
2678
2679         // FreezeTag
2680         if (IS_PLAYER(this) && time >= game_starttime)
2681         {
2682                 if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
2683                 {
2684                         STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
2685                         SetResourceExplicit(this, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
2686                         if (this.iceblock)
2687                                 this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
2688
2689                         if (STAT(REVIVE_PROGRESS, this) >= 1)
2690                                 Unfreeze(this, false);
2691                 }
2692                 else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
2693                 {
2694                         STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
2695                         SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
2696
2697                         if (GetResource(this, RES_HEALTH) < 1)
2698                         {
2699                                 if (this.vehicle)
2700                                         vehicles_exit(this.vehicle, VHEF_RELEASE);
2701                                 if(this.event_damage)
2702                                         this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
2703                         }
2704                         else if (STAT(REVIVE_PROGRESS, this) <= 0)
2705                                 Unfreeze(this, false);
2706                 }
2707         }
2708
2709         // Vehicles
2710         if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle)
2711         if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2712         {
2713                 FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
2714                 {
2715                         if(!it.owner)
2716                         {
2717                                 if(!it.team || SAME_TEAM(this, it))
2718                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER);
2719                                 else if(autocvar_g_vehicles_steal)
2720                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL);
2721                         }
2722                         else if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
2723                         {
2724                                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
2725                         }
2726                 });
2727
2728                 this.last_vehiclecheck = time + 1;
2729         }
2730
2731
2732
2733 // formerly PostThink code
2734         if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0))
2735         if (IS_REAL_CLIENT(this))
2736         if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
2737         if (!intermission_running) // NextLevel() kills all centerprints after setting this true
2738         {
2739                 int totalClients = 0;
2740                 if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0)
2741                 {
2742                         // maxidle disabled in local matches by not counting clients (totalClients 0)
2743                         if (server_is_dedicated)
2744                         {
2745                                 FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
2746                                 {
2747                                         ++totalClients;
2748                                 });
2749                                 if (maxclients - totalClients > autocvar_sv_maxidle_slots)
2750                                         totalClients = 0;
2751                         }
2752                 }
2753                 else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2754                 {
2755                         FOREACH_CLIENT(IS_REAL_CLIENT(it),
2756                         {
2757                                 ++totalClients;
2758                         });
2759                 }
2760
2761                 if (totalClients < autocvar_sv_maxidle_minplayers)
2762                 {
2763                         // idle kick disabled
2764                         CS(this).parm_idlesince = time;
2765                 }
2766                 else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
2767                 {
2768                         if (CS(this).idlekick_lasttimeleft)
2769                         {
2770                                 CS(this).idlekick_lasttimeleft = 0;
2771                                 Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_IDLING);
2772                         }
2773                 }
2774                 else
2775                 {
2776                         float maxidle_time = autocvar_sv_maxidle;
2777                         if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2778                                 maxidle_time = autocvar_sv_maxidle_playertospectator;
2779                         float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
2780                         float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
2781                         if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
2782                         {
2783                                 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2784                                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
2785                                 else
2786                                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
2787                         }
2788                         if (timeleft <= 0) {
2789                                 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2790                                 {
2791                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
2792                                         PutObserverInServer(this, true, true);
2793                                 }
2794                                 else
2795                                 {
2796                                         if (dropclient_schedule(this))
2797                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time);
2798                                 }
2799                                 return;
2800                         }
2801                         else if (timeleft <= countdown_time) {
2802                                 if (timeleft != CS(this).idlekick_lasttimeleft)
2803                                         play2(this, SND(TALK2));
2804                                 CS(this).idlekick_lasttimeleft = timeleft;
2805                         }
2806                 }
2807         }
2808
2809         CheatFrame(this);
2810
2811         if (game_stopped)
2812         {
2813                 this.solid = SOLID_NOT;
2814                 this.takedamage = DAMAGE_NO;
2815                 set_movetype(this, MOVETYPE_NONE);
2816                 CS(this).teamkill_complain = 0;
2817                 CS(this).teamkill_soundtime = 0;
2818                 CS(this).teamkill_soundsource = NULL;
2819         }
2820
2821         if (this.waypointsprite_attachedforcarrier) {
2822                 float hp = healtharmor_maxdamage(GetResource(this, RES_HEALTH), GetResource(this, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x;
2823                 WaypointSprite_UpdateHealth(this.waypointsprite_attachedforcarrier, hp);
2824         }
2825 }
2826
2827 // hack to copy the button fields from the client entity to the Client State
2828 void PM_UpdateButtons(entity this, entity store)
2829 {
2830         if(this.impulse)
2831                 store.impulse = this.impulse;
2832         this.impulse = 0;
2833
2834         bool typing = this.buttonchat || this.button12;
2835
2836         store.button0 = (typing) ? 0 : this.button0;
2837         //button1?!
2838         store.button2 = (typing) ? 0 : this.button2;
2839         store.button3 = (typing) ? 0 : this.button3;
2840         store.button4 = this.button4;
2841         store.button5 = (typing) ? 0 : this.button5;
2842         store.button6 = this.button6;
2843         store.button7 = this.button7;
2844         store.button8 = this.button8;
2845         store.button9 = this.button9;
2846         store.button10 = this.button10;
2847         store.button11 = this.button11;
2848         store.button12 = this.button12;
2849         store.button13 = this.button13;
2850         store.button14 = this.button14;
2851         store.button15 = this.button15;
2852         store.button16 = this.button16;
2853         store.buttonuse = this.buttonuse;
2854         store.buttonchat = this.buttonchat;
2855
2856         store.cursor_active = this.cursor_active;
2857         store.cursor_screen = this.cursor_screen;
2858         store.cursor_trace_start = this.cursor_trace_start;
2859         store.cursor_trace_endpos = this.cursor_trace_endpos;
2860         store.cursor_trace_ent = this.cursor_trace_ent;
2861
2862         store.ping = this.ping;
2863         store.ping_packetloss = this.ping_packetloss;
2864         store.ping_movementloss = this.ping_movementloss;
2865
2866         store.v_angle = this.v_angle;
2867         store.movement = this.movement;
2868 }
2869
2870 NET_HANDLE(fpsreport, bool)
2871 {
2872         int fps = ReadShort();
2873         PlayerScore_Set(sender, SP_FPS, fps);
2874         return true;
2875 }