]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/client.qc
Transifex autosync
[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 .string shootfromfixedorigin;
1209 .entity chatbubbleentity;
1210 void player_powerups_remove_all(entity this);
1211
1212 /*
1213 =============
1214 ClientDisconnect
1215
1216 Called when a client disconnects from the server
1217 =============
1218 */
1219 void ClientDisconnect(entity this)
1220 {
1221         assert(IS_CLIENT(this), return);
1222
1223         /* from "ignore" command */
1224         strfree(this.ignore_list);
1225         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it.ignore_list,
1226         {
1227                 if(it.crypto_idfp && it.crypto_idfp != "")
1228                         continue;
1229                 string mylist = ignore_removefromlist(it, this);
1230                 if(it.ignore_list)
1231                         strunzone(it.ignore_list);
1232
1233                 it.ignore_list = strzone(mylist);
1234         });
1235         /* from "ignore" command */
1236
1237         PlayerStats_GameReport_FinalizePlayer(this);
1238         if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
1239         if (CS(this).active_minigame) part_minigame(this);
1240         if (IS_PLAYER(this)) Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
1241
1242         if (autocvar_sv_eventlog)
1243                 GameLogEcho(strcat(":part:", ftos(this.playerid)));
1244
1245         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_DISCONNECT, this.netname);
1246
1247         if(IS_SPEC(this))
1248                 SetSpectatee(this, NULL);
1249
1250         MUTATOR_CALLHOOK(ClientDisconnect, this);
1251
1252         strfree(CS(this).netname_previous); // needs to be before the CS entity is removed!
1253         strfree(CS_CVAR(this).weaponorder_byimpulse);
1254         ClientState_detach(this);
1255
1256         Portal_ClearAll(this);
1257
1258         Unfreeze(this, false);
1259
1260         RemoveGrapplingHooks(this);
1261
1262         strfree(this.shootfromfixedorigin);
1263
1264         // Here, everything has been done that requires this player to be a client.
1265
1266         this.flags &= ~FL_CLIENT;
1267
1268         if (this.chatbubbleentity) delete(this.chatbubbleentity);
1269         if (this.killindicator) delete(this.killindicator);
1270
1271         IL_EACH(g_counters, it.realowner == this,
1272         {
1273                 delete(it);
1274         });
1275
1276         WaypointSprite_PlayerGone(this);
1277
1278         bot_relinkplayerlist();
1279
1280         strfree(this.clientstatus);
1281         if (this.personal) delete(this.personal);
1282
1283         this.playerid = 0;
1284         if (warmup_stage || game_starttime > time) ReadyCount();
1285         if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
1286
1287         player_powerups_remove_all(this); // stop powerup sound
1288
1289         ONREMOVE(this);
1290
1291         if (player_count == 0)
1292                 localcmd("\nsv_hook_lastleave\n");
1293 }
1294
1295 void ChatBubbleThink(entity this)
1296 {
1297         this.nextthink = time;
1298         if ((this.owner.alpha < 0) || this.owner.chatbubbleentity != this)
1299         {
1300                 if(this.owner) // but why can that ever be NULL?
1301                         this.owner.chatbubbleentity = NULL;
1302                 delete(this);
1303                 return;
1304         }
1305
1306         this.mdl = "";
1307
1308         if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
1309         {
1310                 if ( CS(this.owner).active_minigame && PHYS_INPUT_BUTTON_MINIGAME(this.owner) )
1311                         this.mdl = "models/sprites/minigame_busy.iqm";
1312                 else if (PHYS_INPUT_BUTTON_CHAT(this.owner))
1313                         this.mdl = "models/misc/chatbubble.spr";
1314         }
1315
1316         if ( this.model != this.mdl )
1317                 _setmodel(this, this.mdl);
1318
1319 }
1320
1321 void UpdateChatBubble(entity this)
1322 {
1323         if (this.alpha < 0)
1324                 return;
1325         // spawn a chatbubble entity if needed
1326         if (!this.chatbubbleentity)
1327         {
1328                 this.chatbubbleentity = new(chatbubbleentity);
1329                 this.chatbubbleentity.owner = this;
1330                 this.chatbubbleentity.exteriormodeltoclient = this;
1331                 setthink(this.chatbubbleentity, ChatBubbleThink);
1332                 this.chatbubbleentity.nextthink = time;
1333                 setmodel(this.chatbubbleentity, MDL_CHAT); // precision set below
1334                 //setorigin(this.chatbubbleentity, this.origin + '0 0 15' + this.maxs_z * '0 0 1');
1335                 setorigin(this.chatbubbleentity, '0 0 15' + this.maxs_z * '0 0 1');
1336                 setattachment(this.chatbubbleentity, this, "");  // sticks to moving player better, also conserves bandwidth
1337                 this.chatbubbleentity.mdl = this.chatbubbleentity.model;
1338                 //this.chatbubbleentity.model = "";
1339                 this.chatbubbleentity.effects = EF_LOWPRECISION;
1340         }
1341 }
1342
1343 void calculate_player_respawn_time(entity this)
1344 {
1345         if(MUTATOR_CALLHOOK(CalculateRespawnTime, this))
1346                 return;
1347
1348         float gametype_setting_tmp;
1349         float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
1350         float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
1351         float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
1352         float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
1353         float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
1354         float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
1355
1356         float pcount = 1;  // Include myself whether or not team is already set right and I'm a "player".
1357         if (teamplay)
1358         {
1359                 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1360                         if(it.team == this.team)
1361                                 ++pcount;
1362                 });
1363                 if (sdelay_small_count == 0)
1364                         sdelay_small_count = 1;
1365                 if (sdelay_large_count == 0)
1366                         sdelay_large_count = 1;
1367         }
1368         else
1369         {
1370                 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1371                         ++pcount;
1372                 });
1373                 if (sdelay_small_count == 0)
1374                 {
1375                         if (IS_INDEPENDENT_PLAYER(this))
1376                         {
1377                                 // Players play independently. No point in requiring enemies.
1378                                 sdelay_small_count = 1;
1379                         }
1380                         else
1381                         {
1382                                 // Players play AGAINST each other. Enemies required.
1383                                 sdelay_small_count = 2;
1384                         }
1385                 }
1386                 if (sdelay_large_count == 0)
1387                 {
1388                         if (IS_INDEPENDENT_PLAYER(this))
1389                         {
1390                                 // Players play independently. No point in requiring enemies.
1391                                 sdelay_large_count = 1;
1392                         }
1393                         else
1394                         {
1395                                 // Players play AGAINST each other. Enemies required.
1396                                 sdelay_large_count = 2;
1397                         }
1398                 }
1399         }
1400
1401         float sdelay;
1402
1403         if (pcount <= sdelay_small_count)
1404                 sdelay = sdelay_small;
1405         else if (pcount >= sdelay_large_count)
1406                 sdelay = sdelay_large;
1407         else  // NOTE: this case implies sdelay_large_count > sdelay_small_count.
1408                 sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
1409
1410         if(waves)
1411                 this.respawn_time = ceil((time + sdelay) / waves) * waves;
1412         else
1413                 this.respawn_time = time + sdelay;
1414
1415         if(sdelay < sdelay_max)
1416                 this.respawn_time_max = time + sdelay_max;
1417         else
1418                 this.respawn_time_max = this.respawn_time;
1419
1420         if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
1421                 this.respawn_countdown = 10; // first number to count down from is 10
1422         else
1423                 this.respawn_countdown = -1; // do not count down
1424
1425         if(autocvar_g_forced_respawn)
1426                 this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
1427 }
1428
1429 // LordHavoc: this hack will be removed when proper _pants/_shirt layers are
1430 // added to the model skins
1431 /*void UpdateColorModHack()
1432 {
1433         float c;
1434         c = this.clientcolors & 15;
1435         // LordHavoc: only bothering to support white, green, red, yellow, blue
1436              if (!teamplay) this.colormod = '0 0 0';
1437         else if (c ==  0) this.colormod = '1.00 1.00 1.00';
1438         else if (c ==  3) this.colormod = '0.10 1.73 0.10';
1439         else if (c ==  4) this.colormod = '1.73 0.10 0.10';
1440         else if (c == 12) this.colormod = '1.22 1.22 0.10';
1441         else if (c == 13) this.colormod = '0.10 0.10 1.73';
1442         else this.colormod = '1 1 1';
1443 }*/
1444
1445 void respawn(entity this)
1446 {
1447         bool damagedbycontents_prev = this.damagedbycontents;
1448         if(this.alpha >= 0)
1449         {
1450                 if(autocvar_g_respawn_ghosts)
1451                 {
1452                         this.solid = SOLID_NOT;
1453                         this.takedamage = DAMAGE_NO;
1454                         this.damagedbycontents = false;
1455                         set_movetype(this, MOVETYPE_FLY);
1456                         this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
1457                         this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
1458                         this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
1459                         this.alpha = min(this.alpha, autocvar_g_respawn_ghosts_alpha);
1460                         Send_Effect(EFFECT_RESPAWN_GHOST, this.origin, '0 0 0', 1);
1461                         if(autocvar_g_respawn_ghosts_time > 0)
1462                                 SUB_SetFade(this, time + autocvar_g_respawn_ghosts_time, autocvar_g_respawn_ghosts_fadetime);
1463                 }
1464                 else
1465                         SUB_SetFade (this, time, 1); // fade out the corpse immediately
1466         }
1467
1468         CopyBody(this, 1);
1469         this.damagedbycontents = damagedbycontents_prev;
1470
1471         this.effects |= EF_NODRAW; // prevent another CopyBody
1472         PutClientInServer(this);
1473 }
1474
1475 void play_countdown(entity this, float finished, Sound samp)
1476 {
1477         TC(Sound, samp);
1478         float time_left = finished - time;
1479         if(IS_REAL_CLIENT(this) && time_left < 6 && floor(time_left - frametime) != floor(time_left))
1480                 sound(this, CH_INFO, samp, VOL_BASE, ATTEN_NORM);
1481 }
1482
1483 // it removes special powerups not handled by StatusEffects
1484 void player_powerups_remove_all(entity this)
1485 {
1486         if (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1487         {
1488                 // don't play the poweroff sound when the game restarts or the player disconnects
1489                 if (time > game_starttime + 1 && IS_CLIENT(this)
1490                         && !(start_items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS)))
1491                 {
1492                         sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM);
1493                 }
1494                 if (this.items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1495                         stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound
1496                 this.items -= (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS));
1497         }
1498 }
1499
1500 void player_powerups(entity this)
1501 {
1502         if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped)
1503                 this.modelflags |= MF_ROCKET;
1504         else
1505                 this.modelflags &= ~MF_ROCKET;
1506
1507         this.effects &= ~EF_NODEPTHTEST;
1508
1509         if (IS_DEAD(this))
1510                 player_powerups_remove_all(this);
1511
1512         if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed
1513                 return;
1514
1515         // add a way to see what the items were BEFORE all of these checks for the mutator hook
1516         int items_prev = this.items;
1517
1518         if (!MUTATOR_IS_ENABLED(mutator_instagib))
1519         {
1520                 // NOTE: superweapons are a special case and as such are handled here instead of the status effects system
1521                 if (this.items & IT_SUPERWEAPON)
1522                 {
1523                         if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
1524                         {
1525                                 StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL);
1526                                 this.items = this.items - (this.items & IT_SUPERWEAPON);
1527                                 //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname);
1528                                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST);
1529                         }
1530                         else if (this.items & IT_UNLIMITED_SUPERWEAPONS)
1531                         {
1532                                 // don't let them run out
1533                         }
1534                         else
1535                         {
1536                                 play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
1537                                 if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
1538                                 {
1539                                         this.items = this.items - (this.items & IT_SUPERWEAPON);
1540                                         STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1541                                         //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_BROKEN, this.netname);
1542                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_BROKEN);
1543                                 }
1544                         }
1545                 }
1546                 else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
1547                 {
1548                         if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
1549                         {
1550                                 this.items = this.items | IT_SUPERWEAPON;
1551                                 if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
1552                                 {
1553                                         if(!g_cts)
1554                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_PICKUP, this.netname);
1555                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_PICKUP);
1556                                 }
1557                         }
1558                         else
1559                         {
1560                                 if(StatusEffects_active(STATUSEFFECT_Superweapons, this))
1561                                         StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT);
1562                                 STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1563                         }
1564                 }
1565                 else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame!
1566                 {
1567                         StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR);
1568                 }
1569         }
1570
1571         if(autocvar_g_nodepthtestplayers)
1572                 this.effects = this.effects | EF_NODEPTHTEST;
1573
1574         if(autocvar_g_fullbrightplayers)
1575                 this.effects = this.effects | EF_FULLBRIGHT;
1576
1577         MUTATOR_CALLHOOK(PlayerPowerups, this, items_prev);
1578 }
1579
1580 float CalcRegen(float current, float stable, float regenfactor, float regenframetime)
1581 {
1582         if(current > stable)
1583                 return current;
1584         else if(current > stable - 0.25) // when close enough, "snap"
1585                 return stable;
1586         else
1587                 return min(stable, current + (stable - current) * regenfactor * regenframetime);
1588 }
1589
1590 float CalcRot(float current, float stable, float rotfactor, float rotframetime)
1591 {
1592         if(current < stable)
1593                 return current;
1594         else if(current < stable + 0.25) // when close enough, "snap"
1595                 return stable;
1596         else
1597                 return max(stable, current + (stable - current) * rotfactor * rotframetime);
1598 }
1599
1600 void RotRegen(entity this, Resource res, float limit_mod,
1601         float regenstable, float regenfactor, float regenlinear, float regenframetime,
1602         float rotstable, float rotfactor, float rotlinear, float rotframetime)
1603 {
1604         float old = GetResource(this, res);
1605         float current = old;
1606         if(current > rotstable)
1607         {
1608                 if(rotframetime > 0)
1609                 {
1610                         current = CalcRot(current, rotstable, rotfactor, rotframetime);
1611                         current = max(rotstable, current - rotlinear * rotframetime);
1612                 }
1613         }
1614         else if(current < regenstable)
1615         {
1616                 if(regenframetime > 0)
1617                 {
1618                         current = CalcRegen(current, regenstable, regenfactor, regenframetime);
1619                         current = min(regenstable, current + regenlinear * regenframetime);
1620                 }
1621         }
1622
1623         float limit = GetResourceLimit(this, res) * limit_mod;
1624         if(current > limit)
1625                 current = limit;
1626
1627         if (current != old)
1628                 SetResource(this, res, current);
1629 }
1630
1631 void player_regen(entity this)
1632 {
1633         float max_mod, regen_mod, rot_mod, limit_mod;
1634         max_mod = regen_mod = rot_mod = limit_mod = 1;
1635
1636         float regen_health = autocvar_g_balance_health_regen;
1637         float regen_health_linear = autocvar_g_balance_health_regenlinear;
1638         float regen_health_rot = autocvar_g_balance_health_rot;
1639         float regen_health_rotlinear = autocvar_g_balance_health_rotlinear;
1640         float regen_health_stable = autocvar_g_balance_health_regenstable;
1641         float regen_health_rotstable = autocvar_g_balance_health_rotstable;
1642         bool mutator_returnvalue = MUTATOR_CALLHOOK(PlayerRegen, this, max_mod, regen_mod, rot_mod, limit_mod, regen_health, regen_health_linear, regen_health_rot,
1643                 regen_health_rotlinear, regen_health_stable, regen_health_rotstable);
1644         max_mod = M_ARGV(1, float);
1645         regen_mod = M_ARGV(2, float);
1646         rot_mod = M_ARGV(3, float);
1647         limit_mod = M_ARGV(4, float);
1648         regen_health = M_ARGV(5, float);
1649         regen_health_linear = M_ARGV(6, float);
1650         regen_health_rot = M_ARGV(7, float);
1651         regen_health_rotlinear = M_ARGV(8, float);
1652         regen_health_stable = M_ARGV(9, float);
1653         regen_health_rotstable = M_ARGV(10, float);
1654
1655         float rotstable, regenstable, rotframetime, regenframetime;
1656
1657         if(!mutator_returnvalue)
1658         if(!STAT(FROZEN, this))
1659         {
1660                 regenstable = autocvar_g_balance_armor_regenstable;
1661                 rotstable = autocvar_g_balance_armor_rotstable;
1662                 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1663                 rotframetime = (time > this.pauserotarmor_finished) ? (rot_mod * frametime) : 0;
1664                 RotRegen(this, RES_ARMOR, limit_mod,
1665                         regenstable, autocvar_g_balance_armor_regen, autocvar_g_balance_armor_regenlinear, regenframetime,
1666                         rotstable, autocvar_g_balance_armor_rot, autocvar_g_balance_armor_rotlinear, rotframetime);
1667
1668                 // NOTE: max_mod is only applied to health
1669                 regenstable = regen_health_stable * max_mod;
1670                 rotstable = regen_health_rotstable * max_mod;
1671                 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1672                 rotframetime = (time > this.pauserothealth_finished) ? (rot_mod * frametime) : 0;
1673                 RotRegen(this, RES_HEALTH, limit_mod,
1674                         regenstable, regen_health, regen_health_linear, regenframetime,
1675                         rotstable, regen_health_rot, regen_health_rotlinear, rotframetime);
1676         }
1677
1678         // if player rotted to death...  die!
1679         // check this outside above checks, as player may still be able to rot to death
1680         if(GetResource(this, RES_HEALTH) < 1)
1681         {
1682                 if(this.vehicle)
1683                         vehicles_exit(this.vehicle, VHEF_RELEASE);
1684                 if(this.event_damage)
1685                         this.event_damage(this, this, this, 1, DEATH_ROT.m_id, DMG_NOWEP, this.origin, '0 0 0');
1686         }
1687
1688         if (!(this.items & IT_UNLIMITED_AMMO))
1689         {
1690                 regenstable = autocvar_g_balance_fuel_regenstable;
1691                 rotstable = autocvar_g_balance_fuel_rotstable;
1692                 regenframetime = ((time > this.pauseregen_finished) && (this.items & ITEM_JetpackRegen.m_itemid)) ? frametime : 0;
1693                 rotframetime = (time > this.pauserotfuel_finished) ? frametime : 0;
1694                 RotRegen(this, RES_FUEL, 1,
1695                         regenstable, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear, regenframetime,
1696                         rotstable, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, rotframetime);
1697         }
1698 }
1699
1700 bool zoomstate_set;
1701 void SetZoomState(entity this, float newzoom)
1702 {
1703         if(newzoom != CS(this).zoomstate)
1704         {
1705                 CS(this).zoomstate = newzoom;
1706                 ClientData_Touch(this);
1707         }
1708         zoomstate_set = true;
1709 }
1710
1711 void GetPressedKeys(entity this)
1712 {
1713         MUTATOR_CALLHOOK(GetPressedKeys, this);
1714         if (game_stopped)
1715         {
1716                 CS(this).pressedkeys = 0;
1717                 STAT(PRESSED_KEYS, this) = 0;
1718                 return;
1719         }
1720
1721         // NOTE: GetPressedKeys and PM_dodging_GetPressedKeys use similar code
1722         int keys = STAT(PRESSED_KEYS, this);
1723         keys = BITSET(keys, KEY_FORWARD,        CS(this).movement.x > 0);
1724         keys = BITSET(keys, KEY_BACKWARD,       CS(this).movement.x < 0);
1725         keys = BITSET(keys, KEY_RIGHT,          CS(this).movement.y > 0);
1726         keys = BITSET(keys, KEY_LEFT,           CS(this).movement.y < 0);
1727
1728         keys = BITSET(keys, KEY_JUMP,           PHYS_INPUT_BUTTON_JUMP(this));
1729         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
1730         keys = BITSET(keys, KEY_ATCK,           PHYS_INPUT_BUTTON_ATCK(this));
1731         keys = BITSET(keys, KEY_ATCK2,          PHYS_INPUT_BUTTON_ATCK2(this));
1732         CS(this).pressedkeys = keys; // store for other users
1733
1734         STAT(PRESSED_KEYS, this) = keys;
1735 }
1736
1737 /*
1738 ======================
1739 spectate mode routines
1740 ======================
1741 */
1742
1743 void SpectateCopy(entity this, entity spectatee)
1744 {
1745         TC(Client, this); TC(Client, spectatee);
1746
1747         MUTATOR_CALLHOOK(SpectateCopy, spectatee, this);
1748         PS(this) = PS(spectatee);
1749         this.armortype = spectatee.armortype;
1750         SetResourceExplicit(this, RES_ARMOR, GetResource(spectatee, RES_ARMOR));
1751         SetResourceExplicit(this, RES_CELLS, GetResource(spectatee, RES_CELLS));
1752         SetResourceExplicit(this, RES_PLASMA, GetResource(spectatee, RES_PLASMA));
1753         SetResourceExplicit(this, RES_SHELLS, GetResource(spectatee, RES_SHELLS));
1754         SetResourceExplicit(this, RES_BULLETS, GetResource(spectatee, RES_BULLETS));
1755         SetResourceExplicit(this, RES_ROCKETS, GetResource(spectatee, RES_ROCKETS));
1756         SetResourceExplicit(this, RES_FUEL, GetResource(spectatee, RES_FUEL));
1757         this.effects = spectatee.effects & EFMASK_CHEAP; // eat performance
1758         SetResourceExplicit(this, RES_HEALTH, GetResource(spectatee, RES_HEALTH));
1759         CS(this).impulse = 0;
1760         this.disableclientprediction = 1; // no need to run prediction on a spectator
1761         this.items = spectatee.items;
1762         STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
1763         STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
1764         STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee);
1765         STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
1766         STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
1767         this.punchangle = spectatee.punchangle;
1768         this.view_ofs = spectatee.view_ofs;
1769         this.velocity = spectatee.velocity;
1770         this.dmg_take = spectatee.dmg_take;
1771         this.dmg_save = spectatee.dmg_save;
1772         this.dmg_inflictor = spectatee.dmg_inflictor;
1773         this.v_angle = spectatee.v_angle;
1774         this.angles = spectatee.v_angle;
1775         STAT(FROZEN, this) = STAT(FROZEN, spectatee);
1776         STAT(REVIVE_PROGRESS, this) = STAT(REVIVE_PROGRESS, spectatee);
1777         this.viewloc = spectatee.viewloc;
1778         if(!PHYS_INPUT_BUTTON_USE(this) && STAT(CAMERA_SPECTATOR, this) != 2)
1779                 this.fixangle = true;
1780         setorigin(this, spectatee.origin);
1781         setsize(this, spectatee.mins, spectatee.maxs);
1782         SetZoomState(this, CS(spectatee).zoomstate);
1783
1784     anticheat_spectatecopy(this, spectatee);
1785         STAT(HUD, this) = STAT(HUD, spectatee);
1786         if(spectatee.vehicle)
1787     {
1788         this.angles = spectatee.v_angle;
1789
1790         //this.fixangle = false;
1791         //this.velocity = spectatee.vehicle.velocity;
1792         this.vehicle_health = spectatee.vehicle_health;
1793         this.vehicle_shield = spectatee.vehicle_shield;
1794         this.vehicle_energy = spectatee.vehicle_energy;
1795         this.vehicle_ammo1 = spectatee.vehicle_ammo1;
1796         this.vehicle_ammo2 = spectatee.vehicle_ammo2;
1797         this.vehicle_reload1 = spectatee.vehicle_reload1;
1798         this.vehicle_reload2 = spectatee.vehicle_reload2;
1799
1800         //msg_entity = this;
1801
1802        // WriteByte (MSG_ONE, SVC_SETVIEWANGLES);
1803             //WriteAngle(MSG_ONE,  spectatee.v_angle.x);
1804            // WriteAngle(MSG_ONE,  spectatee.v_angle.y);
1805            // WriteAngle(MSG_ONE,  spectatee.v_angle.z);
1806
1807         //WriteByte (MSG_ONE, SVC_SETVIEW);
1808         //    WriteEntity(MSG_ONE, this);
1809         //makevectors(spectatee.v_angle);
1810         //setorigin(this, spectatee.origin - v_forward * 400 + v_up * 300);*/
1811     }
1812 }
1813
1814 bool SpectateUpdate(entity this)
1815 {
1816         if(!this.enemy)
1817                 return false;
1818
1819         if(!IS_PLAYER(this.enemy) || this == this.enemy)
1820         {
1821                 SetSpectatee(this, NULL);
1822                 return false;
1823         }
1824
1825         SpectateCopy(this, this.enemy);
1826
1827         return true;
1828 }
1829
1830 bool SpectateSet(entity this)
1831 {
1832         if(!IS_PLAYER(this.enemy))
1833                 return false;
1834
1835         ClientData_Touch(this.enemy);
1836
1837         msg_entity = this;
1838         WriteByte(MSG_ONE, SVC_SETVIEW);
1839         WriteEntity(MSG_ONE, this.enemy);
1840         set_movetype(this, MOVETYPE_NONE);
1841         accuracy_resend(this);
1842
1843         if(!SpectateUpdate(this))
1844                 PutObserverInServer(this, false, true);
1845
1846         return true;
1847 }
1848
1849 void SetSpectatee_status(entity this, int spectatee_num)
1850 {
1851         int oldspectatee_status = CS(this).spectatee_status;
1852         CS(this).spectatee_status = spectatee_num;
1853
1854         if (CS(this).spectatee_status != oldspectatee_status)
1855         {
1856                 if (STAT(PRESSED_KEYS, this))
1857                 {
1858                         CS(this).pressedkeys = 0;
1859                         STAT(PRESSED_KEYS, this) = 0;
1860                 }
1861                 ClientData_Touch(this);
1862                 if (g_race || g_cts) race_InitSpectator();
1863         }
1864 }
1865
1866 void SetSpectatee(entity this, entity spectatee)
1867 {
1868         if(IS_BOT_CLIENT(this))
1869                 return; // bots abuse .enemy, this code is useless to them
1870
1871         entity old_spectatee = this.enemy;
1872
1873         this.enemy = spectatee;
1874
1875         // WEAPONTODO
1876         // these are required to fix the spectator bug with arc
1877         if(old_spectatee)
1878         {
1879                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1880                 {
1881                         .entity weaponentity = weaponentities[slot];
1882                         if(old_spectatee.(weaponentity).arc_beam)
1883                                 old_spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1884                 }
1885         }
1886         if(spectatee)
1887         {
1888                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1889                 {
1890                         .entity weaponentity = weaponentities[slot];
1891                         if(spectatee.(weaponentity).arc_beam)
1892                                 spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1893                 }
1894         }
1895
1896         if (spectatee)
1897                 SetSpectatee_status(this, etof(spectatee));
1898
1899         // needed to update spectator list
1900         if(old_spectatee) { ClientData_Touch(old_spectatee); }
1901 }
1902
1903 bool Spectate(entity this, entity pl)
1904 {
1905         if(MUTATOR_CALLHOOK(SpectateSet, this, pl))
1906                 return false;
1907         pl = M_ARGV(1, entity);
1908
1909         SetSpectatee(this, pl);
1910         return SpectateSet(this);
1911 }
1912
1913 bool SpectateNext(entity this)
1914 {
1915         entity ent = find(this.enemy, classname, STR_PLAYER);
1916
1917         if (MUTATOR_CALLHOOK(SpectateNext, this, ent))
1918                 ent = M_ARGV(1, entity);
1919         else if (!ent)
1920                 ent = find(ent, classname, STR_PLAYER);
1921
1922         if(ent) { SetSpectatee(this, ent); }
1923
1924         return SpectateSet(this);
1925 }
1926
1927 bool SpectatePrev(entity this)
1928 {
1929         // NOTE: chain order is from the highest to the lower entnum (unlike find)
1930         entity ent = findchain(classname, STR_PLAYER);
1931         if (!ent) // no player
1932                 return false;
1933
1934         entity first = ent;
1935         // skip players until current spectated player
1936         if(this.enemy)
1937         while(ent && ent != this.enemy)
1938                 ent = ent.chain;
1939
1940         switch (MUTATOR_CALLHOOK(SpectatePrev, this, ent, first))
1941         {
1942                 case MUT_SPECPREV_FOUND:
1943                         ent = M_ARGV(1, entity);
1944                         break;
1945                 case MUT_SPECPREV_RETURN:
1946                         return true;
1947                 case MUT_SPECPREV_CONTINUE:
1948                 default:
1949                 {
1950                         if(ent.chain)
1951                                 ent = ent.chain;
1952                         else
1953                                 ent = first;
1954                         break;
1955                 }
1956         }
1957
1958         SetSpectatee(this, ent);
1959         return SpectateSet(this);
1960 }
1961
1962 /*
1963 =============
1964 ShowRespawnCountdown()
1965
1966 Update a respawn countdown display.
1967 =============
1968 */
1969 void ShowRespawnCountdown(entity this)
1970 {
1971         float number;
1972         if(!IS_DEAD(this)) // just respawned?
1973                 return;
1974         else
1975         {
1976                 number = ceil(this.respawn_time - time);
1977                 if(number <= 0)
1978                         return;
1979                 if(number <= this.respawn_countdown)
1980                 {
1981                         this.respawn_countdown = number - 1;
1982                         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
1983                                 { Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_RESPAWN, number)); }
1984                 }
1985         }
1986 }
1987
1988 .bool team_selected;
1989 bool ShowTeamSelection(entity this)
1990 {
1991         if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
1992                 return false;
1993         if (frametime) // once per frame is more than enough
1994                 stuffcmd(this, "_scoreboard_team_selection 1\n");
1995         return true;
1996 }
1997 void Join(entity this)
1998 {
1999         if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
2000                 ReadyRestart(true);
2001
2002         TRANSMUTE(Player, this);
2003
2004         if(!this.team_selected)
2005         if(autocvar_g_campaign || autocvar_g_balance_teams)
2006                 TeamBalance_JoinBestTeam(this);
2007
2008         if(autocvar_g_campaign)
2009                 campaign_bots_may_start = true;
2010
2011         Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
2012
2013         PutClientInServer(this);
2014
2015         if(IS_PLAYER(this))
2016         if(teamplay && this.team != -1)
2017         {
2018         }
2019         else
2020                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
2021         this.team_selected = false;
2022 }
2023
2024 int GetPlayerLimit()
2025 {
2026         if(g_duel)
2027                 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)
2028         // don't return map_maxplayers during intermission as it would interfere with MapHasRightSize()
2029         int player_limit = (autocvar_g_maxplayers >= 0 || intermission_running) ? autocvar_g_maxplayers : map_maxplayers;
2030         MUTATOR_CALLHOOK(GetPlayerLimit, player_limit);
2031         player_limit = M_ARGV(0, int);
2032         return player_limit < maxclients ? player_limit : 0;
2033 }
2034
2035 /**
2036  * Determines whether the player is allowed to join. This depends on cvar
2037  * g_maxplayers, if it isn't used this function always return true, otherwise
2038  * it checks whether the number of currently playing players exceeds g_maxplayers.
2039  * @return int number of free slots for players, 0 if none
2040  */
2041 int nJoinAllowed(entity this, entity ignore)
2042 {
2043         if(!ignore)
2044         // this is called that way when checking if anyone may be able to join (to build qcstatus)
2045         // so report 0 free slots if restricted
2046         {
2047                 if(autocvar_g_forced_team_otherwise == "spectate")
2048                         return 0;
2049                 if(autocvar_g_forced_team_otherwise == "spectator")
2050                         return 0;
2051         }
2052
2053         if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2054                 return 0; // forced spectators can never join
2055
2056         static float msg_time = 0;
2057         if(this && !INGAME(this) && ignore && PlayerInList(this, autocvar_g_playban_list))
2058         {
2059                 if(time > msg_time)
2060                 {
2061                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PLAYBAN);
2062                         msg_time = time + 0.5;
2063                 }
2064                 return 0;
2065         }
2066
2067         // TODO simplify this
2068         int totalClients = 0;
2069         int currentlyPlaying = 0;
2070         FOREACH_CLIENT(true, {
2071                 if(it != ignore)
2072                         ++totalClients;
2073                 if(IS_REAL_CLIENT(it) && (IS_PLAYER(it) || INGAME(it)))
2074                         ++currentlyPlaying;
2075         });
2076
2077         int player_limit = GetPlayerLimit();
2078
2079         int free_slots = 0;
2080         if (!player_limit)
2081                 free_slots = maxclients - totalClients;
2082         else if(player_limit > 0 && currentlyPlaying < player_limit)
2083                 free_slots = min(maxclients - totalClients, player_limit - currentlyPlaying);
2084
2085         if(this && !INGAME(this) && ignore && !free_slots && time > msg_time)
2086         {
2087                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT, player_limit);
2088                 msg_time = time + 0.5;
2089         }
2090
2091         return free_slots;
2092 }
2093
2094 bool joinAllowed(entity this)
2095 {
2096         if (CS(this).version_mismatch) return false;
2097         if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
2098         if (!nJoinAllowed(this, this)) return false;
2099         if (teamplay && lockteams) return false;
2100         if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
2101         if (ShowTeamSelection(this)) return false;
2102         return true;
2103 }
2104
2105 void show_entnum(entity this)
2106 {
2107         // waypoint editor implements a similar feature for waypoints
2108         if (waypointeditor_enabled)
2109                 return;
2110
2111         if (wasfreed(this.wp_aimed))
2112                 this.wp_aimed = NULL;
2113
2114         WarpZone_crosshair_trace_plusvisibletriggers(this);
2115         entity ent = NULL;
2116         if (trace_ent)
2117         {
2118                 ent = trace_ent;
2119                 if (ent != this.wp_aimed)
2120                 {
2121                         string str = sprintf(
2122                                 "^7ent #%d\n^8 netname: ^3%s\n^8 classname: ^5%s\n^8 origin: ^2'%s'",
2123                                 etof(ent), ent.netname, ent.classname, vtos(ent.origin));
2124                         debug_text_3d((ent.absmin + ent.absmax) * 0.5, str, 0, 7, '0 0 0');
2125                 }
2126         }
2127         if (this.wp_aimed != ent)
2128                 this.wp_aimed = ent;
2129 }
2130
2131 .bool dualwielding_prev;
2132 bool PlayerThink(entity this)
2133 {
2134         if (game_stopped || intermission_running) {
2135                 this.modelflags &= ~MF_ROCKET;
2136                 if(intermission_running)
2137                         IntermissionThink(this);
2138                 return false;
2139         }
2140
2141         if (timeout_status == TIMEOUT_ACTIVE) {
2142                 // don't allow the player to turn around while game is paused
2143                 // FIXME turn this into CSQC stuff
2144                 this.v_angle = this.lastV_angle;
2145                 this.angles = this.lastV_angle;
2146                 this.fixangle = true;
2147         }
2148
2149         if (frametime) player_powerups(this);
2150
2151         if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2152
2153         if (IS_DEAD(this)) {
2154                 if (this.personal && g_race_qualifying) {
2155                         if (time > this.respawn_time) {
2156                                 STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second
2157                                 respawn(this);
2158                                 CS(this).impulse = CHIMPULSE_SPEEDRUN.impulse;
2159                         }
2160                 } else {
2161                         if (frametime) player_anim(this);
2162
2163                         if (this.respawn_flags & RESPAWN_DENY)
2164                         {
2165                                 STAT(RESPAWN_TIME, this) = 0;
2166                                 return false;
2167                         }
2168
2169                         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));
2170
2171                         switch(this.deadflag)
2172                         {
2173                                 case DEAD_DYING:
2174                                 {
2175                                         if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max))
2176                                                 this.deadflag = DEAD_RESPAWNING;
2177                                         else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)))
2178                                                 this.deadflag = DEAD_DEAD;
2179                                         break;
2180                                 }
2181                                 case DEAD_DEAD:
2182                                 {
2183                                         if (button_pressed)
2184                                                 this.deadflag = DEAD_RESPAWNABLE;
2185                                         else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))
2186                                                 this.deadflag = DEAD_RESPAWNING;
2187                                         break;
2188                                 }
2189                                 case DEAD_RESPAWNABLE:
2190                                 {
2191                                         if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE))
2192                                                 this.deadflag = DEAD_RESPAWNING;
2193                                         break;
2194                                 }
2195                                 case DEAD_RESPAWNING:
2196                                 {
2197                                         if (time > this.respawn_time)
2198                                         {
2199                                                 this.respawn_time = time + 1; // only retry once a second
2200                                                 this.respawn_time_max = this.respawn_time;
2201                                                 respawn(this);
2202                                         }
2203                                         break;
2204                                 }
2205                         }
2206
2207                         ShowRespawnCountdown(this);
2208
2209                         if (this.respawn_flags & RESPAWN_SILENT)
2210                                 STAT(RESPAWN_TIME, this) = 0;
2211                         else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max)
2212                         {
2213                                 if (time < this.respawn_time)
2214                                         STAT(RESPAWN_TIME, this) = this.respawn_time;
2215                                 else if (this.deadflag != DEAD_RESPAWNING)
2216                                         STAT(RESPAWN_TIME, this) = -this.respawn_time_max;
2217                         }
2218                         else
2219                                 STAT(RESPAWN_TIME, this) = this.respawn_time;
2220                 }
2221
2222                 // if respawning, invert stat_respawn_time to indicate this, the client translates it
2223                 if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0)
2224                         STAT(RESPAWN_TIME, this) *= -1;
2225
2226                 return false;
2227         }
2228
2229         FixPlayermodel(this);
2230
2231         if (this.shootfromfixedorigin != autocvar_g_shootfromfixedorigin) {
2232                 strcpy(this.shootfromfixedorigin, autocvar_g_shootfromfixedorigin);
2233                 stuffcmd(this, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
2234         }
2235
2236         // reset gun alignment when dual wielding status changes
2237         // to ensure guns are always aligned right and left
2238         bool dualwielding = W_DualWielding(this);
2239         if(this.dualwielding_prev != dualwielding)
2240         {
2241                 W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
2242                 this.dualwielding_prev = dualwielding;
2243         }
2244
2245         // LordHavoc: allow firing on move frames (sub-ticrate), this gives better timing on slow servers
2246         //if(frametime)
2247         {
2248                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2249                 {
2250                         .entity weaponentity = weaponentities[slot];
2251                         if(WEP_CVAR(vortex, charge_always))
2252                                 W_Vortex_Charge(this, weaponentity, frametime);
2253                         W_WeaponFrame(this, weaponentity);
2254                 }
2255         }
2256
2257         if (frametime)
2258         {
2259                 // WEAPONTODO: Add a weapon request for this
2260                 // rot vortex charge to the charge limit
2261                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2262                 {
2263                         .entity weaponentity = weaponentities[slot];
2264                         if (WEP_CVAR(vortex, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(vortex, charge_limit) && this.(weaponentity).vortex_charge_rottime < time)
2265                                 this.(weaponentity).vortex_charge = bound(WEP_CVAR(vortex, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(vortex, charge_rot_rate) * frametime / W_TICSPERFRAME, 1);
2266                 }
2267
2268                 player_regen(this);
2269                 player_anim(this);
2270                 this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
2271         }
2272
2273         monsters_setstatus(this);
2274
2275         return true;
2276 }
2277
2278 .bool would_spectate;
2279 // merged SpectatorThink and ObserverThink (old names are here so you can grep for them)
2280 void ObserverOrSpectatorThink(entity this)
2281 {
2282         bool is_spec = IS_SPEC(this);
2283         if ( CS(this).impulse )
2284         {
2285                 int r = MinigameImpulse(this, CS(this).impulse);
2286                 if (!is_spec || r)
2287                         CS(this).impulse = 0;
2288
2289                 if (is_spec && CS(this).impulse == IMP_weapon_drop.impulse)
2290                 {
2291                         STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
2292                         CS(this).impulse = 0;
2293                         return;
2294                 }
2295         }
2296
2297         if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2298
2299         if (IS_BOT_CLIENT(this) && !CS(this).autojoin_checked)
2300         {
2301                 CS(this).autojoin_checked = true;
2302                 TRANSMUTE(Player, this);
2303                 PutClientInServer(this);
2304
2305                 .entity weaponentity = weaponentities[0];
2306                 if(this.(weaponentity).m_weapon == WEP_Null)
2307                         W_NextWeapon(this, 0, weaponentity);
2308
2309                 return;
2310         }
2311
2312         if (this.flags & FL_JUMPRELEASED) {
2313                 if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
2314                         this.flags &= ~FL_JUMPRELEASED;
2315                         this.flags |= FL_SPAWNING;
2316                 } 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)))
2317                         || (!is_spec && ((PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) || this.would_spectate))) {
2318                         this.flags &= ~FL_JUMPRELEASED;
2319                         if(SpectateNext(this)) {
2320                                 TRANSMUTE(Spectator, this);
2321                         } else if (is_spec) {
2322                                 TRANSMUTE(Observer, this);
2323                                 PutClientInServer(this);
2324                         }
2325                         else
2326                                 this.would_spectate = false; // unable to spectate anyone
2327                         if (is_spec)
2328                                 CS(this).impulse = 0;
2329                 } else if (is_spec) {
2330                         if(CS(this).impulse == 12 || CS(this).impulse == 16  || CS(this).impulse == 19 || (CS(this).impulse >= 220 && CS(this).impulse <= 229)) {
2331                                 this.flags &= ~FL_JUMPRELEASED;
2332                                 if(SpectatePrev(this)) {
2333                                         TRANSMUTE(Spectator, this);
2334                                 } else {
2335                                         TRANSMUTE(Observer, this);
2336                                         PutClientInServer(this);
2337                                 }
2338                                 CS(this).impulse = 0;
2339                         } else if(PHYS_INPUT_BUTTON_ATCK2(this)) {
2340                                 if(!observe_blocked_if_eliminated || !INGAME(this)) {
2341                                         this.would_spectate = false;
2342                                         this.flags &= ~FL_JUMPRELEASED;
2343                                         TRANSMUTE(Observer, this);
2344                                         PutClientInServer(this);
2345                                 }
2346                         } else if(!SpectateUpdate(this) && !SpectateNext(this)) {
2347                                 PutObserverInServer(this, false, true);
2348                                 this.would_spectate = true;
2349                         }
2350                 }
2351                 else {
2352                         bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating;
2353                         if (PHYS_INPUT_BUTTON_USE(this))
2354                                 wouldclip = !wouldclip;
2355                         int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
2356                         set_movetype(this, preferred_movetype);
2357                 }
2358         } else { // jump pressed
2359                 if ((is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)))
2360                         || (!is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this)))) {
2361                         this.flags |= FL_JUMPRELEASED;
2362                         // primary attack pressed
2363                         if(this.flags & FL_SPAWNING)
2364                         {
2365                                 this.flags &= ~FL_SPAWNING;
2366                                 if(joinAllowed(this))
2367                                         Join(this);
2368                                 else if(time < CS(this).jointime + MIN_SPEC_TIME)
2369                                         CS(this).autojoin_checked = -1;
2370                                 return;
2371                         }
2372                 }
2373                 if(is_spec && !SpectateUpdate(this))
2374                         PutObserverInServer(this, false, true);
2375         }
2376         if (is_spec)
2377                 this.flags |= FL_CLIENT | FL_NOTARGET;
2378 }
2379
2380 void PlayerUseKey(entity this)
2381 {
2382         if (!IS_PLAYER(this))
2383                 return;
2384
2385         if(this.vehicle)
2386         {
2387                 if(!game_stopped)
2388                 {
2389                         vehicles_exit(this.vehicle, VHEF_NORMAL);
2390                         return;
2391                 }
2392         }
2393         else if(autocvar_g_vehicles_enter)
2394         {
2395                 if(!game_stopped && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2396                 {
2397                         entity head, closest_target = NULL;
2398                         head = WarpZone_FindRadius(this.origin, autocvar_g_vehicles_enter_radius, true);
2399
2400                         while(head) // find the closest acceptable target to enter
2401                         {
2402                                 if(IS_VEHICLE(head) && !IS_DEAD(head) && head.takedamage != DAMAGE_NO)
2403                                 if(!head.owner || ((head.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(head.owner, this)))
2404                                 {
2405                                         if(closest_target)
2406                                         {
2407                                                 if(vlen2(this.origin - head.origin) < vlen2(this.origin - closest_target.origin))
2408                                                 { closest_target = head; }
2409                                         }
2410                                         else { closest_target = head; }
2411                                 }
2412
2413                                 head = head.chain;
2414                         }
2415
2416                         if(closest_target) { vehicles_enter(this, closest_target); return; }
2417                 }
2418         }
2419
2420         // a use key was pressed; call handlers
2421         MUTATOR_CALLHOOK(PlayerUseKey, this);
2422 }
2423
2424
2425 /*
2426 =============
2427 PlayerPreThink
2428
2429 Called every frame for each real client by DP (and for each bot by StartFrame()),
2430 and when executing every asynchronous move, so only include things that MUST be done then.
2431 Use PlayerFrame() instead for code that only needs to run once per server frame.
2432 frametime == 0 in the asynchronous code path.
2433
2434 TODO: move more stuff from here and PlayerThink() and ObserverOrSpectatorThink() to PlayerFrame() (frametime is always set there)
2435 =============
2436 */
2437 .float last_vehiclecheck;
2438 void PlayerPreThink (entity this)
2439 {
2440         WarpZone_PlayerPhysics_FixVAngle(this);
2441
2442         zoomstate_set = false;
2443
2444         MUTATOR_CALLHOOK(PlayerPreThink, this);
2445
2446         if(PHYS_INPUT_BUTTON_USE(this) && !CS(this).usekeypressed)
2447                 PlayerUseKey(this);
2448         CS(this).usekeypressed = PHYS_INPUT_BUTTON_USE(this);
2449
2450         if (IS_PLAYER(this)) {
2451                 if (IS_REAL_CLIENT(this) && time < CS(this).jointime + MIN_SPEC_TIME)
2452                         error("Client can't be spawned as player on connection!");
2453                 if(!PlayerThink(this))
2454                         return;
2455         }
2456         else if (game_stopped || intermission_running) {
2457                 if(intermission_running)
2458                         IntermissionThink(this);
2459                 return;
2460         }
2461         else if (IS_REAL_CLIENT(this) && CS(this).autojoin_checked <= 0 && time >= CS(this).jointime + MIN_SPEC_TIME)
2462         {
2463                 bool early_join_requested = (CS(this).autojoin_checked < 0);
2464                 CS(this).autojoin_checked = 1;
2465                 // don't do this in ClientConnect
2466                 // many things can go wrong if a client is spawned as player on connection
2467                 if (early_join_requested || MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
2468                         || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2469                                 && (!teamplay || autocvar_g_balance_teams)))
2470                 {
2471                         if(joinAllowed(this))
2472                                 Join(this);
2473                         return;
2474                 }
2475         }
2476         else if (IS_OBSERVER(this) || IS_SPEC(this)) {
2477                 ObserverOrSpectatorThink(this);
2478         }
2479
2480         // WEAPONTODO: Add weapon request for this
2481         if (!zoomstate_set) {
2482                 bool wep_zoomed = false;
2483                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2484                 {
2485                         .entity weaponentity = weaponentities[slot];
2486                         Weapon thiswep = this.(weaponentity).m_weapon;
2487                         if(thiswep != WEP_Null && thiswep.wr_zoom)
2488                                 wep_zoomed += thiswep.wr_zoom(thiswep, this);
2489                 }
2490                 SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
2491         }
2492
2493         // Voice sound effects
2494         if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
2495         {
2496                 CS(this).teamkill_soundtime = 0;
2497
2498                 entity e = CS(this).teamkill_soundsource;
2499                 entity oldpusher = e.pusher;
2500                 e.pusher = this;
2501                 PlayerSound(e, playersound_teamshoot, CH_VOICE, VOL_BASEVOICE, VOICETYPE_LASTATTACKER_ONLY);
2502                 e.pusher = oldpusher;
2503         }
2504
2505         if (CS(this).taunt_soundtime && time > CS(this).taunt_soundtime) {
2506                 CS(this).taunt_soundtime = 0;
2507                 PlayerSound(this, playersound_taunt, CH_VOICE, VOL_BASEVOICE, VOICETYPE_AUTOTAUNT);
2508         }
2509
2510         target_voicescript_next(this);
2511 }
2512
2513 void DrownPlayer(entity this)
2514 {
2515         if(IS_DEAD(this) || game_stopped || time < game_starttime || this.vehicle
2516                 || STAT(FROZEN, this) || this.watertype != CONTENT_WATER)
2517         {
2518                 STAT(AIR_FINISHED, this) = 0;
2519                 return;
2520         }
2521
2522         if (this.waterlevel != WATERLEVEL_SUBMERGED)
2523         {
2524                 if(STAT(AIR_FINISHED, this) && STAT(AIR_FINISHED, this) < time)
2525                         PlayerSound(this, playersound_gasp, CH_PLAYER, VOL_BASE, VOICETYPE_PLAYERSOUND);
2526                 STAT(AIR_FINISHED, this) = 0;
2527         }
2528         else
2529         {
2530                 if (!STAT(AIR_FINISHED, this))
2531                         STAT(AIR_FINISHED, this) = time + autocvar_g_balance_contents_drowndelay;
2532                 if (STAT(AIR_FINISHED, this) < time)
2533                 {       // drown!
2534                         if (this.pain_finished < time)
2535                         {
2536                                 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');
2537                                 this.pain_finished = time + 0.5;
2538                         }
2539                 }
2540         }
2541 }
2542
2543 .bool move_qcphysics;
2544
2545 void Player_Physics(entity this)
2546 {
2547         this.movetype = (this.move_qcphysics) ? MOVETYPE_QCPLAYER : this.move_movetype;
2548
2549         if(!this.move_qcphysics)
2550                 return;
2551
2552         if(!frametime && !CS(this).pm_frametime)
2553                 return;
2554
2555         Movetype_Physics_NoMatchTicrate(this, CS(this).pm_frametime, true);
2556
2557         CS(this).pm_frametime = 0;
2558 }
2559
2560 /*
2561 =============
2562 PlayerPostThink
2563
2564 Called every frame for each real client by DP (and for each bot by StartFrame()),
2565 and when executing every asynchronous move, so only include things that MUST be done then.
2566 Use PlayerFrame() instead for code that only needs to run once per server frame.
2567 frametime == 0 in the asynchronous code path.
2568 =============
2569 */
2570 void PlayerPostThink (entity this)
2571 {
2572         Player_Physics(this);
2573
2574         if (IS_PLAYER(this)) {
2575                 if(this.death_time == time && IS_DEAD(this))
2576                 {
2577                         // player's bbox gets resized now, instead of in the damage event that killed the player,
2578                         // once all the damage events of this frame have been processed with normal size
2579                         this.maxs.z = 5;
2580                         setsize(this, this.mins, this.maxs);
2581                 }
2582                 DrownPlayer(this);
2583                 UpdateChatBubble(this);
2584                 if (CS(this).impulse) ImpulseCommands(this);
2585                 GetPressedKeys(this);
2586                 if (game_stopped)
2587                 {
2588                         CSQCMODEL_AUTOUPDATE(this);
2589                         return;
2590                 }
2591         }
2592         else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
2593         {
2594                 CS(this).pressedkeys = 0;
2595                 STAT(PRESSED_KEYS, this) = 0;
2596         }
2597
2598         CSQCMODEL_AUTOUPDATE(this);
2599 }
2600
2601 /*
2602 =============
2603 PlayerFrame
2604
2605 Called every frame for each client by StartFrame().
2606 Use this for code that only needs to run once per server frame.
2607 frametime is always set here.
2608 =============
2609 */
2610 void PlayerFrame (entity this)
2611 {
2612 // formerly PreThink code
2613         STAT(GUNALIGN, this) = CS_CVAR(this).cvar_cl_gunalign; // TODO
2614         STAT(MOVEVARS_CL_TRACK_CANJUMP, this) = CS_CVAR(this).cvar_cl_movement_track_canjump;
2615
2616         // physics frames: update anticheat stuff
2617         anticheat_prethink(this);
2618
2619         // Check if spectating is allowed
2620         if (blockSpectators && IS_REAL_CLIENT(this)
2621         && (IS_SPEC(this) || IS_OBSERVER(this)) && !INGAME(this)
2622         && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime))
2623         {
2624                 if (dropclient_schedule(this))
2625                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
2626         }
2627
2628         // Check for nameless players
2629         if (this.netname == "" || this.netname != CS(this).netname_previous)
2630         {
2631                 bool assume_unchanged = (CS(this).netname_previous == "");
2632                 if (autocvar_sv_name_maxlength > 0 && strlennocol(this.netname) > autocvar_sv_name_maxlength)
2633                 {
2634                         int new_length = textLengthUpToLength(this.netname, autocvar_sv_name_maxlength, strlennocol);
2635                         this.netname = strzone(strcat(substring(this.netname, 0, new_length), "^7"));
2636                         sprint(this, sprintf("Warning: your name is longer than %d characters, it has been truncated.\n", autocvar_sv_name_maxlength));
2637                         assume_unchanged = false;
2638                         // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2639                 }
2640                 if (isInvisibleString(this.netname))
2641                 {
2642                         this.netname = strzone(sprintf("Player#%d", this.playerid));
2643                         sprint(this, "Warning: invisible names are not allowed.\n");
2644                         assume_unchanged = false;
2645                         // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2646                 }
2647                 if (!assume_unchanged && autocvar_sv_eventlog)
2648                         GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this.netname, this.team, false)));
2649                 strcpy(CS(this).netname_previous, this.netname);
2650         }
2651
2652         // version nagging
2653         if (CS(this).version_nagtime && CS_CVAR(this).cvar_g_xonoticversion && time > CS(this).version_nagtime)
2654         {
2655                 CS(this).version_nagtime = 0;
2656                 if (strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "autobuild", 0) >= 0)
2657                 {
2658                         // git client
2659                 }
2660                 else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0)
2661                 {
2662                         // git server
2663                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2664                 }
2665                 else
2666                 {
2667                         int r = vercmp(CS_CVAR(this).cvar_g_xonoticversion, autocvar_g_xonoticversion);
2668                         if (r < 0) // old client
2669                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2670                         else if (r > 0) // old server
2671                                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2672                 }
2673         }
2674
2675         // GOD MODE info
2676         if (!(this.flags & FL_GODMODE) && this.max_armorvalue)
2677         {
2678                 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue);
2679                 this.max_armorvalue = 0;
2680         }
2681
2682         // FreezeTag
2683         if (IS_PLAYER(this) && time >= game_starttime)
2684         {
2685                 if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
2686                 {
2687                         STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
2688                         SetResourceExplicit(this, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
2689                         if (this.iceblock)
2690                                 this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
2691
2692                         if (STAT(REVIVE_PROGRESS, this) >= 1)
2693                                 Unfreeze(this, false);
2694                 }
2695                 else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
2696                 {
2697                         STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
2698                         SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
2699
2700                         if (GetResource(this, RES_HEALTH) < 1)
2701                         {
2702                                 if (this.vehicle)
2703                                         vehicles_exit(this.vehicle, VHEF_RELEASE);
2704                                 if(this.event_damage)
2705                                         this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
2706                         }
2707                         else if (STAT(REVIVE_PROGRESS, this) <= 0)
2708                                 Unfreeze(this, false);
2709                 }
2710         }
2711
2712         // Vehicles
2713         if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle)
2714         if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2715         {
2716                 FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
2717                 {
2718                         if(!it.owner)
2719                         {
2720                                 if(!it.team || SAME_TEAM(this, it))
2721                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER);
2722                                 else if(autocvar_g_vehicles_steal)
2723                                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL);
2724                         }
2725                         else if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
2726                         {
2727                                 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
2728                         }
2729                 });
2730
2731                 this.last_vehiclecheck = time + 1;
2732         }
2733
2734
2735
2736 // formerly PostThink code
2737         if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0))
2738         if (IS_REAL_CLIENT(this))
2739         if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
2740         if (!intermission_running) // NextLevel() kills all centerprints after setting this true
2741         {
2742                 int totalClients = 0;
2743                 if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0)
2744                 {
2745                         // maxidle disabled in local matches by not counting clients (totalClients 0)
2746                         if (server_is_dedicated)
2747                         {
2748                                 FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
2749                                 {
2750                                         ++totalClients;
2751                                 });
2752                                 if (maxclients - totalClients > autocvar_sv_maxidle_slots)
2753                                         totalClients = 0;
2754                         }
2755                 }
2756                 else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2757                 {
2758                         FOREACH_CLIENT(IS_REAL_CLIENT(it),
2759                         {
2760                                 ++totalClients;
2761                         });
2762                 }
2763
2764                 if (totalClients < autocvar_sv_maxidle_minplayers)
2765                 {
2766                         // idle kick disabled
2767                         CS(this).parm_idlesince = time;
2768                 }
2769                 else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
2770                 {
2771                         if (CS(this).idlekick_lasttimeleft)
2772                         {
2773                                 CS(this).idlekick_lasttimeleft = 0;
2774                                 Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_IDLING);
2775                         }
2776                 }
2777                 else
2778                 {
2779                         float maxidle_time = autocvar_sv_maxidle;
2780                         if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2781                                 maxidle_time = autocvar_sv_maxidle_playertospectator;
2782                         float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
2783                         float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
2784                         if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
2785                         {
2786                                 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2787                                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
2788                                 else
2789                                         Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
2790                         }
2791                         if (timeleft <= 0) {
2792                                 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2793                                 {
2794                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
2795                                         PutObserverInServer(this, true, true);
2796                                 }
2797                                 else
2798                                 {
2799                                         if (dropclient_schedule(this))
2800                                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time);
2801                                 }
2802                                 return;
2803                         }
2804                         else if (timeleft <= countdown_time) {
2805                                 if (timeleft != CS(this).idlekick_lasttimeleft)
2806                                         play2(this, SND(TALK2));
2807                                 CS(this).idlekick_lasttimeleft = timeleft;
2808                         }
2809                 }
2810         }
2811
2812         CheatFrame(this);
2813
2814         if (game_stopped)
2815         {
2816                 this.solid = SOLID_NOT;
2817                 this.takedamage = DAMAGE_NO;
2818                 set_movetype(this, MOVETYPE_NONE);
2819                 CS(this).teamkill_complain = 0;
2820                 CS(this).teamkill_soundtime = 0;
2821                 CS(this).teamkill_soundsource = NULL;
2822         }
2823
2824         if (this.waypointsprite_attachedforcarrier) {
2825                 float hp = healtharmor_maxdamage(GetResource(this, RES_HEALTH), GetResource(this, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x;
2826                 WaypointSprite_UpdateHealth(this.waypointsprite_attachedforcarrier, hp);
2827         }
2828 }
2829
2830 // hack to copy the button fields from the client entity to the Client State
2831 void PM_UpdateButtons(entity this, entity store)
2832 {
2833         if(this.impulse)
2834                 store.impulse = this.impulse;
2835         this.impulse = 0;
2836
2837         bool typing = this.buttonchat || this.button12;
2838
2839         store.button0 = (typing) ? 0 : this.button0;
2840         //button1?!
2841         store.button2 = (typing) ? 0 : this.button2;
2842         store.button3 = (typing) ? 0 : this.button3;
2843         store.button4 = this.button4;
2844         store.button5 = (typing) ? 0 : this.button5;
2845         store.button6 = this.button6;
2846         store.button7 = this.button7;
2847         store.button8 = this.button8;
2848         store.button9 = this.button9;
2849         store.button10 = this.button10;
2850         store.button11 = this.button11;
2851         store.button12 = this.button12;
2852         store.button13 = this.button13;
2853         store.button14 = this.button14;
2854         store.button15 = this.button15;
2855         store.button16 = this.button16;
2856         store.buttonuse = this.buttonuse;
2857         store.buttonchat = this.buttonchat;
2858
2859         store.cursor_active = this.cursor_active;
2860         store.cursor_screen = this.cursor_screen;
2861         store.cursor_trace_start = this.cursor_trace_start;
2862         store.cursor_trace_endpos = this.cursor_trace_endpos;
2863         store.cursor_trace_ent = this.cursor_trace_ent;
2864
2865         store.ping = this.ping;
2866         store.ping_packetloss = this.ping_packetloss;
2867         store.ping_movementloss = this.ping_movementloss;
2868
2869         store.v_angle = this.v_angle;
2870         store.movement = this.movement;
2871 }
2872
2873 NET_HANDLE(fpsreport, bool)
2874 {
2875         int fps = ReadShort();
2876         PlayerScore_Set(sender, SP_FPS, fps);
2877         return true;
2878 }