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