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