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