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