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