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>
84 STATIC_METHOD(Client, Add, void(Client this, int _team))
87 TRANSMUTE(Player, this);
90 PutClientInServer(this);
93 STATIC_METHOD(Client, Remove, void(Client this))
95 TRANSMUTE(Observer, this);
96 PutClientInServer(this);
97 ClientDisconnect(this);
100 void send_CSQC_teamnagger() {
101 WriteHeader(MSG_BROADCAST, TE_CSQC_TEAMNAGGER);
104 int CountSpectators(entity player, entity to)
106 if(!player) { return 0; } // not sure how, but best to be safe
110 FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
118 void WriteSpectators(entity player, entity to)
120 if(!player) { return; } // not sure how, but best to be safe
123 FOREACH_CLIENT(IS_REAL_CLIENT(it) && IS_SPEC(it) && it != to && it.enemy == player,
125 if(spec_count >= MAX_SPECTATORS)
127 WriteByte(MSG_ENTITY, num_for_edict(it));
132 bool ClientData_Send(entity this, entity to, int sf)
134 assert(to == this.owner, return false);
137 if (IS_SPEC(e)) e = e.enemy;
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
148 WriteHeader(MSG_ENTITY, ENT_CLIENT_CLIENTDATA);
149 WriteByte(MSG_ENTITY, sf);
152 WriteByte(MSG_ENTITY, CS(to).spectatee_status);
156 float specs = CountSpectators(e, to);
157 WriteByte(MSG_ENTITY, specs);
158 WriteSpectators(e, to);
164 void ClientData_Attach(entity this)
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;
171 void ClientData_Detach(entity this)
173 delete(CS(this).clientdata);
174 CS(this).clientdata = NULL;
177 void ClientData_Touch(entity e)
179 entity cd = CS(e).clientdata;
180 if (cd) { cd.SendFlags = 1; }
182 // make it spectatable
183 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != e && IS_SPEC(it) && it.enemy == e,
185 entity cd = CS(it).clientdata;
186 if (cd) { cd.SendFlags = 1; }
195 Checks if the argument string can be a valid playermodel.
196 Returns a valid one in doubt.
199 string FallbackPlayerModel;
200 string CheckPlayerModel(string plyermodel) {
201 if(FallbackPlayerModel != cvar_defstring("_cl_playermodel"))
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"));
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")
218 return FallbackPlayerModel;
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)
228 if(!fexists(plyermodel))
229 return FallbackPlayerModel;
234 void setplayermodel(entity e, string modelname)
236 precache_model(modelname);
237 _setmodel(e, modelname);
238 player_setupanimsformodel(e);
239 if(!autocvar_g_debug_globalsounds)
240 UpdatePlayerSounds(e);
243 /** putting a client as observer in the server */
244 void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
246 bool mutator_returnvalue = MUTATOR_CALLHOOK(MakePlayerObserver, this, is_forced);
247 bool recount_ready = false;
248 PlayerState_detach(this);
252 if(GetResource(this, RES_HEALTH) >= 1)
255 Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
258 // was a player, recount votes and ready status
259 if(IS_REAL_CLIENT(this))
261 if (vote_called) { VoteCount(false); }
263 if (warmup_stage || game_starttime > time) recount_ready = true;
265 entcs_update_players(this);
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));
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;
280 if (IS_REAL_CLIENT(this))
283 WriteByte(MSG_ONE, SVC_SETVIEW);
284 WriteEntity(MSG_ONE, this);
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)
290 // needed for player sounds
292 FixPlayermodel(this);
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';
298 RemoveGrapplingHooks(this);
299 Portal_ClearAll(this);
300 Unfreeze(this, false);
301 SetSpectatee(this, NULL);
306 PlayerStats_GameReport_Event_Player(this, PLAYERSTATS_ALIVETIME, time - this.alivetime);
310 if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
312 TRANSMUTE(Observer, this);
314 if(recount_ready) ReadyCount(); // FIXME: please add comment about why this is delayed
316 WaypointSprite_PlayerDead(this);
317 accuracy_resend(this);
319 if (CS(this).killcount != FRAGS_SPECTATOR && !game_stopped && CHAT_NOSPECTATORS())
320 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS);
322 CS(this).spectatortime = time;
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;
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;
348 this.respawn_flags = 0;
349 this.respawn_time = 0;
350 STAT(RESPAWN_TIME, this) = 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;
362 setthink(this, func_null);
364 this.deadflag = DEAD_NO;
366 STAT(REVIVE_PROGRESS, this) = 0;
367 this.revival_time = 0;
368 this.draggable = drag_undraggable;
370 player_powerups_remove_all(this);
372 STAT(WEAPONS, this) = '0 0 0';
373 this.drawonlytoclient = this;
377 //this.spawnpoint_targ = NULL; // keep it so they can return to where they were?
379 this.weaponmodel = "";
380 for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
382 this.weaponentities[slot] = NULL;
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;
394 for(int slot = 0; slot < MAX_AXH; ++slot)
396 entity axh = this.(AuxiliaryXhair[slot]);
397 this.(AuxiliaryXhair[slot]) = NULL;
399 if(axh.owner == this && axh != NULL && !wasfreed(axh))
403 if (mutator_returnvalue)
405 // mutator prevents resetting teams+score
409 SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR); // clears scores too in game modes without teams
410 this.frags = FRAGS_SPECTATOR;
413 bot_relinkplayerlist();
415 if (CS(this).just_joined)
416 CS(this).just_joined = false;
419 int player_getspecies(entity this)
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;
428 .float model_randomizer;
429 void FixPlayermodel(entity player)
431 string defaultmodel = "";
433 if(autocvar_sv_defaultcharacter)
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;
446 if(defaultmodel == "")
448 defaultmodel = autocvar_sv_defaultplayermodel;
449 defaultskin = autocvar_sv_defaultplayerskin;
452 int n = tokenize_console(defaultmodel);
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);
462 int i = strstrofs(defaultmodel, ":", 0);
465 defaultskin = stof(substring(defaultmodel, i+1, -1));
466 defaultmodel = substring(defaultmodel, 0, i);
469 if(autocvar_sv_defaultcharacterskin && !defaultskin)
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;
483 defaultskin = autocvar_sv_defaultplayerskin;
486 MUTATOR_CALLHOOK(FixPlayermodel, defaultmodel, defaultskin, player);
487 defaultmodel = M_ARGV(0, string);
488 defaultskin = M_ARGV(1, int);
492 if(defaultmodel != "")
494 if (defaultmodel != player.model)
496 vector m1 = player.mins;
497 vector m2 = player.maxs;
498 setplayermodel (player, defaultmodel);
499 setsize (player, m1, m2);
503 oldskin = player.skin;
504 player.skin = defaultskin;
506 if (player.playermodel != player.model || player.playermodel == "")
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);
516 if(!autocvar_sv_defaultcharacterskin)
518 oldskin = player.skin;
519 player.skin = stof(player.playerskin);
523 oldskin = player.skin;
524 player.skin = defaultskin;
528 if(chmdl || oldskin != player.skin) // model or skin has changed
530 player.species = player_getspecies(player); // update species
531 if(!autocvar_g_debug_globalsounds)
532 UpdatePlayerSounds(player); // update skin sounds
536 if(strlen(autocvar_sv_defaultplayercolors))
537 if(player.clientcolors != stof(autocvar_sv_defaultplayercolors))
538 setcolor(player, stof(autocvar_sv_defaultplayercolors));
541 void GiveWarmupResources(entity this)
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;
554 void PutPlayerInServer(entity this)
556 if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
558 PlayerState_attach(this);
559 accuracy_resend(this);
561 if (teamplay && this.bot_forced_team)
562 SetPlayerTeam(this, this.bot_forced_team, TEAM_CHANGE_MANUAL);
565 TeamBalance_JoinBestTeam(this);
567 entity spot = SelectSpawnPoint(this, false);
569 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_NOSPAWNS);
570 return; // spawn failed
573 TRANSMUTE(Player, this);
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;
597 GiveWarmupResources(this);
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)
611 GiveRandomWeapons(this, random_start_weapons_count,
612 autocvar_g_random_start_weapons, random_start_ammo);
615 SetSpectatee_status(this, 0);
617 PS(this).dual_weapons = '0 0 0';
619 if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
620 StatusEffects_apply(STATUSEFFECT_Superweapons, this, time + autocvar_g_balance_superweapons_time, 0);
622 this.items = start_items;
624 float shieldtime = time + autocvar_g_spawnshieldtime;
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)
632 float f = game_starttime - time;
634 this.pauserotarmor_finished += f;
635 this.pauserothealth_finished += f;
636 this.pauseregen_finished += f;
639 StatusEffects_apply(STATUSEFFECT_SpawnShield, this, shieldtime, 0);
641 this.damageforcescale = autocvar_g_player_damageforcescale;
643 this.respawn_flags = 0;
644 this.respawn_time = 0;
645 STAT(RESPAWN_TIME, this) = 0;
646 // DP model scaling uses 1/16 accuracy and 13/16 is closest to 56/69
647 this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) ? 0.8125 : autocvar_sv_player_scale);
649 this.pain_finished = 0;
651 setthink(this, func_null); // players have no think function
654 PS(this).ballistics_density = autocvar_g_ballistics_density_player;
656 this.deadflag = DEAD_NO;
658 this.angles = spot.angles;
659 this.angles_z = 0; // never spawn tilted even if the spot says to
660 if (IS_BOT_CLIENT(this))
662 this.v_angle = this.angles;
665 this.fixangle = true; // turn this way immediately
666 this.oldvelocity = this.velocity = '0 0 0';
667 this.avelocity = '0 0 0';
668 this.punchangle = '0 0 0';
669 this.punchvector = '0 0 0';
671 STAT(REVIVE_PROGRESS, this) = 0;
672 this.revival_time = 0;
674 STAT(AIR_FINISHED, this) = 0;
675 this.waterlevel = WATERLEVEL_NONE;
676 this.watertype = CONTENT_EMPTY;
678 entity spawnevent = new_pure(spawnevent);
679 spawnevent.owner = this;
680 Net_LinkEntity(spawnevent, false, 0.5, SpawnEvent_Send);
682 // Cut off any still running player sounds.
683 stopsound(this, CH_PLAYER_SINGLE);
686 FixPlayermodel(this);
687 this.drawonlytoclient = NULL;
691 for(int slot = 0; slot < MAX_AXH; ++slot)
693 entity axh = this.(AuxiliaryXhair[slot]);
694 this.(AuxiliaryXhair[slot]) = NULL;
696 if(axh.owner == this && axh != NULL && !wasfreed(axh))
700 this.spawnpoint_targ = NULL;
703 this.view_ofs = STAT(PL_VIEW_OFS, this);
704 setsize(this, STAT(PL_MIN, this), STAT(PL_MAX, this));
705 this.spawnorigin = spot.origin;
706 setorigin(this, spot.origin + '0 0 1' * (1 - this.mins.z - 24));
707 // don't reset back to last position, even if new position is stuck in solid
708 this.oldorigin = this.origin;
710 IL_REMOVE(g_conveyed, this);
711 this.conveyor = NULL; // prevent conveyors at the previous location from moving a freshly spawned player
713 IL_REMOVE(g_swamped, this);
714 this.swampslug = NULL;
715 this.swamp_interval = 0;
716 if(this.ladder_entity)
717 IL_REMOVE(g_ladderents, this);
718 this.ladder_entity = NULL;
719 IL_EACH(g_counters, it.realowner == this,
723 STAT(HUD, this) = HUD_NORMAL;
725 this.event_damage = PlayerDamage;
726 this.event_heal = PlayerHeal;
728 this.draggable = func_null;
731 IL_PUSH(g_bot_targets, this);
732 this.bot_attack = true;
733 if(!this.monster_attack)
734 IL_PUSH(g_monster_targets, this);
735 this.monster_attack = true;
736 navigation_dynamicgoal_init(this, false);
738 PHYS_INPUT_BUTTON_ATCK(this) = PHYS_INPUT_BUTTON_JUMP(this) = PHYS_INPUT_BUTTON_ATCK2(this) = false;
740 // player was spectator
741 if (CS(this).killcount == FRAGS_SPECTATOR) {
742 PlayerScore_Clear(this);
743 CS(this).killcount = 0;
744 CS(this).startplaytime = time;
747 for (int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
749 .entity weaponentity = weaponentities[slot];
750 CL_SpawnWeaponentity(this, weaponentity);
752 this.alpha = default_player_alpha;
753 this.colormod = '1 1 1' * autocvar_g_player_brightness;
754 this.exteriorweaponentity.alpha = default_weapon_alpha;
756 this.speedrunning = false;
758 this.counter_cnt = 0;
759 this.fragsfilter_cnt = 0;
761 target_voicescript_clear(this);
763 // reset fields the weapons may use
764 FOREACH(Weapons, true, {
765 it.wr_resetplayer(it, this);
766 // reload all reloadable weapons
767 if (it.spawnflags & WEP_FLAG_RELOADABLE) {
768 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
770 .entity weaponentity = weaponentities[slot];
771 this.(weaponentity).weapon_load[it.m_id] = it.reloading_ammo;
776 Unfreeze(this, false);
778 MUTATOR_CALLHOOK(PlayerSpawn, this, spot);
780 string s = spot.target;
781 if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack
782 spot.target = string_null;
783 SUB_UseTargets(spot, this, NULL);
784 if(g_assault || g_race)
788 if (autocvar_spawn_debug)
790 sprint(this, strcat("spawnpoint origin: ", vtos(spot.origin), "\n"));
791 delete(spot); // usefull for checking if there are spawnpoints, that let drop through the floor
794 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
796 .entity weaponentity = weaponentities[slot];
797 entity w_ent = this.(weaponentity);
798 if(slot == 0 || autocvar_g_weaponswitch_debug == 1)
799 w_ent.m_switchweapon = w_getbestweapon(this, weaponentity);
801 w_ent.m_switchweapon = WEP_Null;
802 w_ent.m_weapon = WEP_Null;
803 w_ent.weaponname = "";
804 w_ent.m_switchingweapon = WEP_Null;
808 MUTATOR_CALLHOOK(PlayerWeaponSelect, this);
810 if (CS(this).impulse) ImpulseCommands(this);
812 W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
813 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
815 .entity weaponentity = weaponentities[slot];
816 W_WeaponFrame(this, weaponentity);
819 if (!warmup_stage && !this.alivetime)
820 this.alivetime = time;
822 antilag_clear(this, CS(this));
824 if (warmup_stage < 0 || warmup_stage > 1)
828 /** Called when a client spawns in the server */
829 void PutClientInServer(entity this)
831 if (IS_REAL_CLIENT(this)) {
833 WriteByte(MSG_ONE, SVC_SETVIEW);
834 WriteEntity(MSG_ONE, this);
837 TRANSMUTE(Observer, this);
839 bool use_spawnpoint = (!this.enemy); // check this.enemy here since SetSpectatee will clear it
840 SetSpectatee(this, NULL);
844 PS(this).itemkeys = 0;
846 MUTATOR_CALLHOOK(PutClientInServer, this);
848 if (IS_OBSERVER(this)) {
849 PutObserverInServer(this, false, use_spawnpoint);
850 } else if (IS_PLAYER(this)) {
851 PutPlayerInServer(this);
854 bot_relinkplayerlist();
857 // TODO do we need all these fields, or should we stop autodetecting runtime
858 // changes and just have a console command to update this?
859 bool ClientInit_SendEntity(entity this, entity to, int sf)
861 WriteHeader(MSG_ENTITY, _ENT_CLIENT_INIT);
864 // MSG_INIT replacement
865 // TODO: make easier to use
867 W_PROP_reload(MSG_ONE, to);
868 ClientInit_misc(this);
869 MUTATOR_CALLHOOK(Ent_Init);
871 void ClientInit_misc(entity this)
873 int channel = MSG_ONE;
874 WriteHeader(channel, ENT_CLIENT_INIT);
875 WriteByte(channel, g_nexball_meter_period * 32);
876 WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[0]));
877 WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[1]));
878 WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[2]));
879 WriteInt24_t(channel, compressShotOrigin(hook_shotorigin[3]));
880 WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[0]));
881 WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[1]));
882 WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[2]));
883 WriteInt24_t(channel, compressShotOrigin(arc_shotorigin[3]));
885 if(autocvar_sv_foginterval && world.fog != "")
886 WriteString(channel, world.fog);
888 WriteString(channel, "");
889 WriteByte(channel, this.count * 255.0); // g_balance_armor_blockpercent
890 WriteByte(channel, this.cnt * 255.0); // g_balance_damagepush_speedfactor
891 WriteByte(channel, serverflags);
892 WriteCoord(channel, autocvar_g_trueaim_minrange);
895 void ClientInit_CheckUpdate(entity this)
897 this.nextthink = time;
898 if(this.count != autocvar_g_balance_armor_blockpercent)
900 this.count = autocvar_g_balance_armor_blockpercent;
903 if(this.cnt != autocvar_g_balance_damagepush_speedfactor)
905 this.cnt = autocvar_g_balance_damagepush_speedfactor;
910 void ClientInit_Spawn()
912 entity e = new_pure(clientinit);
913 setthink(e, ClientInit_CheckUpdate);
914 Net_LinkEntity(e, false, 0, ClientInit_SendEntity);
916 ClientInit_CheckUpdate(e);
926 // initialize parms for a new player
927 parm1 = -(86400 * 366);
929 MUTATOR_CALLHOOK(SetNewParms);
937 void SetChangeParms (entity this)
939 // save parms for level change
940 parm1 = CS(this).parm_idlesince - time;
942 MUTATOR_CALLHOOK(SetChangeParms);
950 void DecodeLevelParms(entity this)
953 CS(this).parm_idlesince = parm1;
954 if (CS(this).parm_idlesince == -(86400 * 366))
955 CS(this).parm_idlesince = time;
957 // whatever happens, allow 60 seconds of idling directly after connect for map loading
958 CS(this).parm_idlesince = max(CS(this).parm_idlesince, time - autocvar_sv_maxidle + 60);
960 MUTATOR_CALLHOOK(DecodeLevelParms);
963 void FixClientCvars(entity e)
965 // send prediction settings to the client
966 if(autocvar_g_antilag == 3) // client side hitscan
967 stuffcmd(e, "cl_cmd settemp cl_prydoncursor_notrace 0\n");
968 if(autocvar_sv_gentle)
969 stuffcmd(e, "cl_cmd settemp cl_gentle 1\n");
971 stuffcmd(e, sprintf("\ncl_jumpspeedcap_min \"%s\"\n", autocvar_sv_jumpspeedcap_min));
972 stuffcmd(e, sprintf("\ncl_jumpspeedcap_max \"%s\"\n", autocvar_sv_jumpspeedcap_max));
974 stuffcmd(e, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
976 MUTATOR_CALLHOOK(FixClientCvars, e);
979 bool findinlist_abbrev(string tofind, string list)
981 if(list == "" || tofind == "")
982 return false; // empty list or search, just return
984 // this function allows abbreviated strings!
985 FOREACH_WORD(list, it != "" && it == substring(tofind, 0, strlen(it)),
993 bool PlayerInIPList(entity p, string iplist)
995 // some safety checks (never allow local?)
996 if(p.netaddress == "local" || p.netaddress == "" || !IS_REAL_CLIENT(p))
999 return findinlist_abbrev(p.netaddress, iplist);
1002 bool PlayerInIDList(entity p, string idlist)
1004 // NOTE: we do NOT check crypto_idfp_signed here, an unsigned ID is fine too for this
1008 return findinlist_abbrev(p.crypto_idfp, idlist);
1011 bool PlayerInList(entity player, string list)
1015 return boolean(PlayerInIDList(player, list) || PlayerInIPList(player, list));
1018 #ifdef DP_EXT_PRECONNECT
1023 Called once (not at each match start) when a client begins a connection to the server
1026 void ClientPreConnect(entity this)
1028 if(autocvar_sv_eventlog)
1030 GameLogEcho(sprintf(":connect:%d:%d:%s",
1033 ((IS_REAL_CLIENT(this)) ? this.netaddress : "bot")
1039 // NOTE csqc uses the active mutators list sent by this function
1040 // to understand which mutators are enabled
1041 // also note that they aren't all registered mutators, e.g. jetpack, low gravity
1042 void SendWelcomeMessage(entity this, int msg_type)
1044 if (boolean(autocvar_g_campaign))
1046 WriteByte(msg_type, 1);
1047 WriteByte(msg_type, Campaign_GetLevelNum());
1052 if (CS(this).version_mismatch)
1054 if (CS(this).version < autocvar_gameversion)
1056 WriteByte(msg_type, flags);
1058 WriteString(msg_type, autocvar_hostname);
1059 WriteString(msg_type, autocvar_g_xonoticversion);
1061 WriteByte(msg_type, autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers);
1062 WriteByte(msg_type, GetPlayerLimit());
1064 MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
1065 string modifications = M_ARGV(0, string);
1067 if (!g_weaponarena && cvar("g_balance_blaster_weaponstartoverride") == 0)
1068 modifications = strcat(modifications, ", No start weapons");
1069 if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
1070 modifications = strcat(modifications, ", Low gravity");
1071 if(g_weapon_stay && !g_cts)
1072 modifications = strcat(modifications, ", Weapons stay");
1073 if(autocvar_g_jetpack)
1074 modifications = strcat(modifications, ", Jetpack");
1075 modifications = substring(modifications, 2, strlen(modifications) - 2);
1077 WriteString(msg_type, modifications);
1079 WriteString(msg_type, g_weaponarena_list);
1081 if(cache_lastmutatormsg != autocvar_g_mutatormsg)
1083 strcpy(cache_lastmutatormsg, autocvar_g_mutatormsg);
1084 strcpy(cache_mutatormsg, cache_lastmutatormsg);
1087 WriteString(msg_type, cache_mutatormsg);
1089 WriteString(msg_type, strreplace("\\n", "\n", autocvar_sv_motd));
1096 Called when a client connects to the server
1099 void ClientConnect(entity this)
1101 if (Ban_MaybeEnforceBanOnce(this)) return;
1102 assert(!IS_CLIENT(this), return);
1103 this.flags |= FL_CLIENT;
1104 assert(player_count >= 0, player_count = 0);
1106 TRANSMUTE(Client, this);
1107 CS(this).version_nagtime = time + 10 + random() * 10;
1109 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_CONNECT, this.netname);
1111 bot_clientconnect(this);
1113 Player_DetermineForcedTeam(this);
1115 TRANSMUTE(Observer, this);
1117 PlayerStats_GameReport_AddEvent(sprintf("kills-%d", this.playerid));
1119 // always track bots, don't ask for cl_allow_uidtracking
1120 if (IS_BOT_CLIENT(this))
1121 PlayerStats_GameReport_AddPlayer(this);
1123 CS(this).allowed_timeouts = autocvar_sv_timeout_number;
1125 if (autocvar_sv_eventlog)
1126 GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this.netname, this.team, false)));
1128 CS(this).just_joined = true; // stop spamming the eventlog with additional lines when the client connects
1130 stuffcmd(this, clientstuff, "\n");
1131 stuffcmd(this, "cl_particles_reloadeffects\n"); // TODO do we still need this?
1133 FixClientCvars(this);
1135 // get version info from player
1136 stuffcmd(this, "cmd clientversion $gameversion\n");
1138 // notify about available teams
1141 entity balance = TeamBalance_CheckAllowedTeams(this);
1142 int t = TeamBalance_GetAllowedTeams(balance);
1143 TeamBalance_Destroy(balance);
1144 stuffcmd(this, sprintf("set _teams_available %d\n", t));
1148 stuffcmd(this, "set _teams_available 0\n");
1151 bot_relinkplayerlist();
1153 CS(this).spectatortime = time;
1154 if (blockSpectators)
1156 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
1159 CS(this).jointime = time;
1161 if (IS_REAL_CLIENT(this))
1163 if (g_weaponarena_weapons == WEPSET(TUBA))
1164 stuffcmd(this, "cl_cmd settemp chase_active 1\n");
1165 // quickmenu file must be put in a subfolder with an unique name
1166 // to reduce chances of overriding custom client quickmenus
1167 if (waypointeditor_enabled)
1168 stuffcmd(this, sprintf("cl_cmd settemp _hud_panel_quickmenu_file_from_server %s\n", "wpeditor.txt"));
1169 else if (autocvar_sv_quickmenu_file != "" && strstrofs(autocvar_sv_quickmenu_file, "/", 0) && fexists(autocvar_sv_quickmenu_file))
1170 stuffcmd(this, sprintf("cl_cmd settemp _hud_panel_quickmenu_file_from_server %s\n", autocvar_sv_quickmenu_file));
1173 if (!autocvar_sv_foginterval && world.fog != "")
1174 stuffcmd(this, strcat("\nfog ", world.fog, "\nr_fog_exp2 0\nr_drawfog 1\n"));
1176 if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
1177 if(!MUTATOR_CALLHOOK(HideTeamNagger, this))
1178 send_CSQC_teamnagger();
1180 CSQCMODEL_AUTOINIT(this);
1182 CS(this).model_randomizer = random();
1184 if (IS_REAL_CLIENT(this))
1185 sv_notice_join(this);
1187 this.move_qcphysics = true;
1189 // update physics stats (players can spawn before physics runs)
1190 Physics_UpdateStats(this);
1192 IL_EACH(g_initforplayer, it.init_for_player, {
1193 it.init_for_player(it, this);
1196 Handicap_Initialize(this);
1199 if (PlayerInList(this, autocvar_g_playban_list))
1200 TRANSMUTE(Observer, this);
1202 if (PlayerInList(this, autocvar_g_chatban_list)) // chatban
1203 CS(this).muted = true;
1205 MUTATOR_CALLHOOK(ClientConnect, this);
1207 if (player_count == 1)
1209 if (autocvar_sv_autopause && server_is_dedicated)
1211 localcmd("\nsv_hook_firstjoin\n");
1215 .string shootfromfixedorigin;
1216 .entity chatbubbleentity;
1217 void player_powerups_remove_all(entity this);
1223 Called when a client disconnects from the server
1226 void ClientDisconnect(entity this)
1228 assert(IS_CLIENT(this), return);
1230 /* from "ignore" command */
1231 strfree(this.ignore_list);
1232 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it.ignore_list,
1234 if(it.crypto_idfp && it.crypto_idfp != "")
1236 string mylist = ignore_removefromlist(it, this);
1238 strunzone(it.ignore_list);
1240 it.ignore_list = strzone(mylist);
1242 /* from "ignore" command */
1244 PlayerStats_GameReport_FinalizePlayer(this);
1245 if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
1246 if (CS(this).active_minigame) part_minigame(this);
1247 if (IS_PLAYER(this)) Send_Effect(EFFECT_SPAWN_NEUTRAL, this.origin, '0 0 0', 1);
1249 if (autocvar_sv_eventlog)
1250 GameLogEcho(strcat(":part:", ftos(this.playerid)));
1252 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_DISCONNECT, this.netname);
1255 SetSpectatee(this, NULL);
1257 MUTATOR_CALLHOOK(ClientDisconnect, this);
1259 strfree(CS(this).netname_previous); // needs to be before the CS entity is removed!
1260 strfree(CS_CVAR(this).weaponorder_byimpulse);
1261 ClientState_detach(this);
1263 Portal_ClearAll(this);
1265 Unfreeze(this, false);
1267 RemoveGrapplingHooks(this);
1269 strfree(this.shootfromfixedorigin);
1271 // Here, everything has been done that requires this player to be a client.
1273 this.flags &= ~FL_CLIENT;
1275 if (this.chatbubbleentity) delete(this.chatbubbleentity);
1276 if (this.killindicator) delete(this.killindicator);
1278 IL_EACH(g_counters, it.realowner == this,
1283 WaypointSprite_PlayerGone(this);
1285 bot_relinkplayerlist();
1287 strfree(this.clientstatus);
1288 if (this.personal) delete(this.personal);
1291 if (warmup_stage || game_starttime > time) ReadyCount();
1292 if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
1294 player_powerups_remove_all(this); // stop powerup sound
1298 if (player_count == 0)
1299 localcmd("\nsv_hook_lastleave\n");
1302 void ChatBubbleThink(entity this)
1304 this.nextthink = time;
1305 if ((this.owner.alpha < 0) || this.owner.chatbubbleentity != this)
1307 if(this.owner) // but why can that ever be NULL?
1308 this.owner.chatbubbleentity = NULL;
1315 if ( !IS_DEAD(this.owner) && IS_PLAYER(this.owner) )
1317 if ( CS(this.owner).active_minigame && PHYS_INPUT_BUTTON_MINIGAME(this.owner) )
1318 this.mdl = "models/sprites/minigame_busy.iqm";
1319 else if (PHYS_INPUT_BUTTON_CHAT(this.owner))
1320 this.mdl = "models/misc/chatbubble.spr";
1323 if ( this.model != this.mdl )
1324 _setmodel(this, this.mdl);
1328 void UpdateChatBubble(entity this)
1332 // spawn a chatbubble entity if needed
1333 if (!this.chatbubbleentity)
1335 this.chatbubbleentity = new(chatbubbleentity);
1336 this.chatbubbleentity.owner = this;
1337 this.chatbubbleentity.exteriormodeltoclient = this;
1338 setthink(this.chatbubbleentity, ChatBubbleThink);
1339 this.chatbubbleentity.nextthink = time;
1340 setmodel(this.chatbubbleentity, MDL_CHAT); // precision set below
1341 //setorigin(this.chatbubbleentity, this.origin + '0 0 15' + this.maxs_z * '0 0 1');
1342 setorigin(this.chatbubbleentity, '0 0 15' + this.maxs_z * '0 0 1');
1343 setattachment(this.chatbubbleentity, this, ""); // sticks to moving player better, also conserves bandwidth
1344 this.chatbubbleentity.mdl = this.chatbubbleentity.model;
1345 //this.chatbubbleentity.model = "";
1346 this.chatbubbleentity.effects = EF_LOWPRECISION;
1350 void calculate_player_respawn_time(entity this)
1352 if(MUTATOR_CALLHOOK(CalculateRespawnTime, this))
1355 float gametype_setting_tmp;
1356 float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
1357 float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
1358 float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
1359 float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
1360 float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
1361 float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
1363 float pcount = 1; // Include myself whether or not team is already set right and I'm a "player".
1366 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1367 if(it.team == this.team)
1370 if (sdelay_small_count == 0)
1371 sdelay_small_count = 1;
1372 if (sdelay_large_count == 0)
1373 sdelay_large_count = 1;
1377 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
1380 if (sdelay_small_count == 0)
1382 if (IS_INDEPENDENT_PLAYER(this))
1384 // Players play independently. No point in requiring enemies.
1385 sdelay_small_count = 1;
1389 // Players play AGAINST each other. Enemies required.
1390 sdelay_small_count = 2;
1393 if (sdelay_large_count == 0)
1395 if (IS_INDEPENDENT_PLAYER(this))
1397 // Players play independently. No point in requiring enemies.
1398 sdelay_large_count = 1;
1402 // Players play AGAINST each other. Enemies required.
1403 sdelay_large_count = 2;
1410 if (pcount <= sdelay_small_count)
1411 sdelay = sdelay_small;
1412 else if (pcount >= sdelay_large_count)
1413 sdelay = sdelay_large;
1414 else // NOTE: this case implies sdelay_large_count > sdelay_small_count.
1415 sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
1418 this.respawn_time = ceil((time + sdelay) / waves) * waves;
1420 this.respawn_time = time + sdelay;
1422 if(sdelay < sdelay_max)
1423 this.respawn_time_max = time + sdelay_max;
1425 this.respawn_time_max = this.respawn_time;
1427 if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
1428 this.respawn_countdown = 10; // first number to count down from is 10
1430 this.respawn_countdown = -1; // do not count down
1432 if(autocvar_g_forced_respawn)
1433 this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
1436 // LordHavoc: this hack will be removed when proper _pants/_shirt layers are
1437 // added to the model skins
1438 /*void UpdateColorModHack()
1441 c = this.clientcolors & 15;
1442 // LordHavoc: only bothering to support white, green, red, yellow, blue
1443 if (!teamplay) this.colormod = '0 0 0';
1444 else if (c == 0) this.colormod = '1.00 1.00 1.00';
1445 else if (c == 3) this.colormod = '0.10 1.73 0.10';
1446 else if (c == 4) this.colormod = '1.73 0.10 0.10';
1447 else if (c == 12) this.colormod = '1.22 1.22 0.10';
1448 else if (c == 13) this.colormod = '0.10 0.10 1.73';
1449 else this.colormod = '1 1 1';
1452 void respawn(entity this)
1454 bool damagedbycontents_prev = this.damagedbycontents;
1457 if(autocvar_g_respawn_ghosts)
1459 this.solid = SOLID_NOT;
1460 this.takedamage = DAMAGE_NO;
1461 this.damagedbycontents = false;
1462 set_movetype(this, MOVETYPE_FLY);
1463 this.velocity = '0 0 1' * autocvar_g_respawn_ghosts_speed;
1464 this.avelocity = randomvec() * autocvar_g_respawn_ghosts_speed * 3 - randomvec() * autocvar_g_respawn_ghosts_speed * 3;
1465 this.effects |= CSQCMODEL_EF_RESPAWNGHOST;
1466 this.alpha = min(this.alpha, autocvar_g_respawn_ghosts_alpha);
1467 Send_Effect(EFFECT_RESPAWN_GHOST, this.origin, '0 0 0', 1);
1468 if(autocvar_g_respawn_ghosts_time > 0)
1469 SUB_SetFade(this, time + autocvar_g_respawn_ghosts_time, autocvar_g_respawn_ghosts_fadetime);
1472 SUB_SetFade (this, time, 1); // fade out the corpse immediately
1476 this.damagedbycontents = damagedbycontents_prev;
1478 this.effects |= EF_NODRAW; // prevent another CopyBody
1479 PutClientInServer(this);
1482 void play_countdown(entity this, float finished, Sound samp)
1485 float time_left = finished - time;
1486 if(IS_REAL_CLIENT(this) && time_left < 6 && floor(time_left - frametime) != floor(time_left))
1487 sound(this, CH_INFO, samp, VOL_BASE, ATTEN_NORM);
1490 // it removes special powerups not handled by StatusEffects
1491 void player_powerups_remove_all(entity this)
1493 if (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1495 // don't play the poweroff sound when the game restarts or the player disconnects
1496 if (time > game_starttime + 1 && IS_CLIENT(this)
1497 && !(start_items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS)))
1499 sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM);
1501 if (this.items & (IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS))
1502 stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound
1503 this.items -= (this.items & (IT_SUPERWEAPON | IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS));
1507 void player_powerups(entity this)
1509 if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped)
1510 this.modelflags |= MF_ROCKET;
1512 this.modelflags &= ~MF_ROCKET;
1514 this.effects &= ~EF_NODEPTHTEST;
1517 player_powerups_remove_all(this);
1519 if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed
1522 // add a way to see what the items were BEFORE all of these checks for the mutator hook
1523 int items_prev = this.items;
1525 if (!MUTATOR_IS_ENABLED(mutator_instagib))
1527 // NOTE: superweapons are a special case and as such are handled here instead of the status effects system
1528 if (this.items & IT_SUPERWEAPON)
1530 if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS))
1532 StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL);
1533 this.items = this.items - (this.items & IT_SUPERWEAPON);
1534 //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname);
1535 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST);
1537 else if (this.items & IT_UNLIMITED_SUPERWEAPONS)
1539 // don't let them run out
1543 play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF);
1544 if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this))
1546 this.items = this.items - (this.items & IT_SUPERWEAPON);
1547 STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1548 //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_BROKEN, this.netname);
1549 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_BROKEN);
1553 else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)
1555 if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS))
1557 this.items = this.items | IT_SUPERWEAPON;
1558 if(!(this.items & IT_UNLIMITED_SUPERWEAPONS))
1561 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_PICKUP, this.netname);
1562 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_PICKUP);
1567 if(StatusEffects_active(STATUSEFFECT_Superweapons, this))
1568 StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT);
1569 STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS;
1572 else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame!
1574 StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR);
1578 if(autocvar_g_nodepthtestplayers)
1579 this.effects = this.effects | EF_NODEPTHTEST;
1581 if(autocvar_g_fullbrightplayers)
1582 this.effects = this.effects | EF_FULLBRIGHT;
1584 MUTATOR_CALLHOOK(PlayerPowerups, this, items_prev);
1587 float CalcRegen(float current, float stable, float regenfactor, float regenframetime)
1589 if(current > stable)
1591 else if(current > stable - 0.25) // when close enough, "snap"
1594 return min(stable, current + (stable - current) * regenfactor * regenframetime);
1597 float CalcRot(float current, float stable, float rotfactor, float rotframetime)
1599 if(current < stable)
1601 else if(current < stable + 0.25) // when close enough, "snap"
1604 return max(stable, current + (stable - current) * rotfactor * rotframetime);
1607 void RotRegen(entity this, Resource res, float limit_mod,
1608 float regenstable, float regenfactor, float regenlinear, float regenframetime,
1609 float rotstable, float rotfactor, float rotlinear, float rotframetime)
1611 float old = GetResource(this, res);
1612 float current = old;
1613 if(current > rotstable)
1615 if(rotframetime > 0)
1617 current = CalcRot(current, rotstable, rotfactor, rotframetime);
1618 current = max(rotstable, current - rotlinear * rotframetime);
1621 else if(current < regenstable)
1623 if(regenframetime > 0)
1625 current = CalcRegen(current, regenstable, regenfactor, regenframetime);
1626 current = min(regenstable, current + regenlinear * regenframetime);
1630 float limit = GetResourceLimit(this, res) * limit_mod;
1635 SetResource(this, res, current);
1638 void player_regen(entity this)
1640 float max_mod, regen_mod, rot_mod, limit_mod;
1641 max_mod = regen_mod = rot_mod = limit_mod = 1;
1643 float regen_health = autocvar_g_balance_health_regen;
1644 float regen_health_linear = autocvar_g_balance_health_regenlinear;
1645 float regen_health_rot = autocvar_g_balance_health_rot;
1646 float regen_health_rotlinear = autocvar_g_balance_health_rotlinear;
1647 float regen_health_stable = autocvar_g_balance_health_regenstable;
1648 float regen_health_rotstable = autocvar_g_balance_health_rotstable;
1649 bool mutator_returnvalue = MUTATOR_CALLHOOK(PlayerRegen, this, max_mod, regen_mod, rot_mod, limit_mod, regen_health, regen_health_linear, regen_health_rot,
1650 regen_health_rotlinear, regen_health_stable, regen_health_rotstable);
1651 max_mod = M_ARGV(1, float);
1652 regen_mod = M_ARGV(2, float);
1653 rot_mod = M_ARGV(3, float);
1654 limit_mod = M_ARGV(4, float);
1655 regen_health = M_ARGV(5, float);
1656 regen_health_linear = M_ARGV(6, float);
1657 regen_health_rot = M_ARGV(7, float);
1658 regen_health_rotlinear = M_ARGV(8, float);
1659 regen_health_stable = M_ARGV(9, float);
1660 regen_health_rotstable = M_ARGV(10, float);
1662 float rotstable, regenstable, rotframetime, regenframetime;
1664 if(!mutator_returnvalue)
1665 if(!STAT(FROZEN, this))
1667 regenstable = autocvar_g_balance_armor_regenstable;
1668 rotstable = autocvar_g_balance_armor_rotstable;
1669 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1670 rotframetime = (time > this.pauserotarmor_finished) ? (rot_mod * frametime) : 0;
1671 RotRegen(this, RES_ARMOR, limit_mod,
1672 regenstable, autocvar_g_balance_armor_regen, autocvar_g_balance_armor_regenlinear, regenframetime,
1673 rotstable, autocvar_g_balance_armor_rot, autocvar_g_balance_armor_rotlinear, rotframetime);
1675 // NOTE: max_mod is only applied to health
1676 regenstable = regen_health_stable * max_mod;
1677 rotstable = regen_health_rotstable * max_mod;
1678 regenframetime = (time > this.pauseregen_finished) ? (regen_mod * frametime) : 0;
1679 rotframetime = (time > this.pauserothealth_finished) ? (rot_mod * frametime) : 0;
1680 RotRegen(this, RES_HEALTH, limit_mod,
1681 regenstable, regen_health, regen_health_linear, regenframetime,
1682 rotstable, regen_health_rot, regen_health_rotlinear, rotframetime);
1685 // if player rotted to death... die!
1686 // check this outside above checks, as player may still be able to rot to death
1687 if(GetResource(this, RES_HEALTH) < 1)
1690 vehicles_exit(this.vehicle, VHEF_RELEASE);
1691 if(this.event_damage)
1692 this.event_damage(this, this, this, 1, DEATH_ROT.m_id, DMG_NOWEP, this.origin, '0 0 0');
1695 if (!(this.items & IT_UNLIMITED_AMMO))
1697 regenstable = autocvar_g_balance_fuel_regenstable;
1698 rotstable = autocvar_g_balance_fuel_rotstable;
1699 regenframetime = ((time > this.pauseregen_finished) && (this.items & ITEM_JetpackRegen.m_itemid)) ? frametime : 0;
1700 rotframetime = (time > this.pauserotfuel_finished) ? frametime : 0;
1701 RotRegen(this, RES_FUEL, 1,
1702 regenstable, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear, regenframetime,
1703 rotstable, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, rotframetime);
1708 void SetZoomState(entity this, float newzoom)
1710 if(newzoom != CS(this).zoomstate)
1712 CS(this).zoomstate = newzoom;
1713 ClientData_Touch(this);
1715 zoomstate_set = true;
1718 void GetPressedKeys(entity this)
1720 MUTATOR_CALLHOOK(GetPressedKeys, this);
1723 CS(this).pressedkeys = 0;
1724 STAT(PRESSED_KEYS, this) = 0;
1728 // NOTE: GetPressedKeys and PM_dodging_GetPressedKeys use similar code
1729 int keys = STAT(PRESSED_KEYS, this);
1730 keys = BITSET(keys, KEY_FORWARD, CS(this).movement.x > 0);
1731 keys = BITSET(keys, KEY_BACKWARD, CS(this).movement.x < 0);
1732 keys = BITSET(keys, KEY_RIGHT, CS(this).movement.y > 0);
1733 keys = BITSET(keys, KEY_LEFT, CS(this).movement.y < 0);
1735 keys = BITSET(keys, KEY_JUMP, PHYS_INPUT_BUTTON_JUMP(this));
1736 keys = BITSET(keys, KEY_CROUCH, IS_DUCKED(this)); // workaround: player can't un-crouch until their path is clear, so we keep the button held here
1737 keys = BITSET(keys, KEY_ATCK, PHYS_INPUT_BUTTON_ATCK(this));
1738 keys = BITSET(keys, KEY_ATCK2, PHYS_INPUT_BUTTON_ATCK2(this));
1739 CS(this).pressedkeys = keys; // store for other users
1741 STAT(PRESSED_KEYS, this) = keys;
1745 ======================
1746 spectate mode routines
1747 ======================
1750 void SpectateCopy(entity this, entity spectatee)
1752 TC(Client, this); TC(Client, spectatee);
1754 MUTATOR_CALLHOOK(SpectateCopy, spectatee, this);
1755 PS(this) = PS(spectatee);
1756 this.armortype = spectatee.armortype;
1757 SetResourceExplicit(this, RES_ARMOR, GetResource(spectatee, RES_ARMOR));
1758 SetResourceExplicit(this, RES_CELLS, GetResource(spectatee, RES_CELLS));
1759 SetResourceExplicit(this, RES_PLASMA, GetResource(spectatee, RES_PLASMA));
1760 SetResourceExplicit(this, RES_SHELLS, GetResource(spectatee, RES_SHELLS));
1761 SetResourceExplicit(this, RES_BULLETS, GetResource(spectatee, RES_BULLETS));
1762 SetResourceExplicit(this, RES_ROCKETS, GetResource(spectatee, RES_ROCKETS));
1763 SetResourceExplicit(this, RES_FUEL, GetResource(spectatee, RES_FUEL));
1764 this.effects = spectatee.effects & EFMASK_CHEAP; // eat performance
1765 SetResourceExplicit(this, RES_HEALTH, GetResource(spectatee, RES_HEALTH));
1766 CS(this).impulse = 0;
1767 this.disableclientprediction = 1; // no need to run prediction on a spectator
1768 this.items = spectatee.items;
1769 STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee);
1770 STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee);
1771 STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee);
1772 STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee);
1773 STAT(WEAPONS, this) = STAT(WEAPONS, spectatee);
1774 this.punchangle = spectatee.punchangle;
1775 this.view_ofs = spectatee.view_ofs;
1776 this.velocity = spectatee.velocity;
1777 this.dmg_take = spectatee.dmg_take;
1778 this.dmg_save = spectatee.dmg_save;
1779 this.dmg_inflictor = spectatee.dmg_inflictor;
1780 this.v_angle = spectatee.v_angle;
1781 this.angles = spectatee.v_angle;
1782 STAT(FROZEN, this) = STAT(FROZEN, spectatee);
1783 STAT(REVIVE_PROGRESS, this) = STAT(REVIVE_PROGRESS, spectatee);
1784 this.viewloc = spectatee.viewloc;
1785 if(!PHYS_INPUT_BUTTON_USE(this) && STAT(CAMERA_SPECTATOR, this) != 2)
1786 this.fixangle = true;
1787 setorigin(this, spectatee.origin);
1788 setsize(this, spectatee.mins, spectatee.maxs);
1789 SetZoomState(this, CS(spectatee).zoomstate);
1791 anticheat_spectatecopy(this, spectatee);
1792 STAT(HUD, this) = STAT(HUD, spectatee);
1793 if(spectatee.vehicle)
1795 this.angles = spectatee.v_angle;
1797 //this.fixangle = false;
1798 //this.velocity = spectatee.vehicle.velocity;
1799 this.vehicle_health = spectatee.vehicle_health;
1800 this.vehicle_shield = spectatee.vehicle_shield;
1801 this.vehicle_energy = spectatee.vehicle_energy;
1802 this.vehicle_ammo1 = spectatee.vehicle_ammo1;
1803 this.vehicle_ammo2 = spectatee.vehicle_ammo2;
1804 this.vehicle_reload1 = spectatee.vehicle_reload1;
1805 this.vehicle_reload2 = spectatee.vehicle_reload2;
1807 //msg_entity = this;
1809 // WriteByte (MSG_ONE, SVC_SETVIEWANGLES);
1810 //WriteAngle(MSG_ONE, spectatee.v_angle.x);
1811 // WriteAngle(MSG_ONE, spectatee.v_angle.y);
1812 // WriteAngle(MSG_ONE, spectatee.v_angle.z);
1814 //WriteByte (MSG_ONE, SVC_SETVIEW);
1815 // WriteEntity(MSG_ONE, this);
1816 //makevectors(spectatee.v_angle);
1817 //setorigin(this, spectatee.origin - v_forward * 400 + v_up * 300);*/
1821 bool SpectateUpdate(entity this)
1826 if(!IS_PLAYER(this.enemy) || this == this.enemy)
1828 SetSpectatee(this, NULL);
1832 SpectateCopy(this, this.enemy);
1837 bool SpectateSet(entity this)
1839 if(!IS_PLAYER(this.enemy))
1842 ClientData_Touch(this.enemy);
1845 WriteByte(MSG_ONE, SVC_SETVIEW);
1846 WriteEntity(MSG_ONE, this.enemy);
1847 set_movetype(this, MOVETYPE_NONE);
1848 accuracy_resend(this);
1850 if(!SpectateUpdate(this))
1851 PutObserverInServer(this, false, true);
1856 void SetSpectatee_status(entity this, int spectatee_num)
1858 int oldspectatee_status = CS(this).spectatee_status;
1859 CS(this).spectatee_status = spectatee_num;
1861 if (CS(this).spectatee_status != oldspectatee_status)
1863 if (STAT(PRESSED_KEYS, this))
1865 CS(this).pressedkeys = 0;
1866 STAT(PRESSED_KEYS, this) = 0;
1868 ClientData_Touch(this);
1869 if (g_race || g_cts) race_InitSpectator();
1873 void SetSpectatee(entity this, entity spectatee)
1875 if(IS_BOT_CLIENT(this))
1876 return; // bots abuse .enemy, this code is useless to them
1878 entity old_spectatee = this.enemy;
1880 this.enemy = spectatee;
1883 // these are required to fix the spectator bug with arc
1886 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1888 .entity weaponentity = weaponentities[slot];
1889 if(old_spectatee.(weaponentity).arc_beam)
1890 old_spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1895 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1897 .entity weaponentity = weaponentities[slot];
1898 if(spectatee.(weaponentity).arc_beam)
1899 spectatee.(weaponentity).arc_beam.SendFlags |= ARC_SF_SETTINGS;
1904 SetSpectatee_status(this, etof(spectatee));
1906 // needed to update spectator list
1907 if(old_spectatee) { ClientData_Touch(old_spectatee); }
1910 bool Spectate(entity this, entity pl)
1912 if(MUTATOR_CALLHOOK(SpectateSet, this, pl))
1914 pl = M_ARGV(1, entity);
1916 SetSpectatee(this, pl);
1917 return SpectateSet(this);
1920 bool SpectateNext(entity this)
1922 entity ent = find(this.enemy, classname, STR_PLAYER);
1924 if (MUTATOR_CALLHOOK(SpectateNext, this, ent))
1925 ent = M_ARGV(1, entity);
1927 ent = find(ent, classname, STR_PLAYER);
1929 if(ent) { SetSpectatee(this, ent); }
1931 return SpectateSet(this);
1934 bool SpectatePrev(entity this)
1936 // NOTE: chain order is from the highest to the lower entnum (unlike find)
1937 entity ent = findchain(classname, STR_PLAYER);
1938 if (!ent) // no player
1942 // skip players until current spectated player
1944 while(ent && ent != this.enemy)
1947 switch (MUTATOR_CALLHOOK(SpectatePrev, this, ent, first))
1949 case MUT_SPECPREV_FOUND:
1950 ent = M_ARGV(1, entity);
1952 case MUT_SPECPREV_RETURN:
1954 case MUT_SPECPREV_CONTINUE:
1965 SetSpectatee(this, ent);
1966 return SpectateSet(this);
1971 ShowRespawnCountdown()
1973 Update a respawn countdown display.
1976 void ShowRespawnCountdown(entity this)
1979 if(!IS_DEAD(this)) // just respawned?
1983 number = ceil(this.respawn_time - time);
1986 if(number <= this.respawn_countdown)
1988 this.respawn_countdown = number - 1;
1989 if(ceil(this.respawn_time - (time + 0.5)) == number) // only say it if it is the same number even in 0.5s; to prevent overlapping sounds
1990 { Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_RESPAWN, number)); }
1995 .bool team_selected;
1996 bool ShowTeamSelection(entity this)
1998 if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
2000 if (frametime) // once per frame is more than enough
2001 stuffcmd(this, "_scoreboard_team_selection 1\n");
2004 void Join(entity this)
2006 if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
2009 TRANSMUTE(Player, this);
2011 if(!this.team_selected)
2012 if(autocvar_g_campaign || autocvar_g_balance_teams)
2013 TeamBalance_JoinBestTeam(this);
2015 if(autocvar_g_campaign)
2016 campaign_bots_may_start = true;
2018 Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
2020 PutClientInServer(this);
2023 if(teamplay && this.team != -1)
2027 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
2028 this.team_selected = false;
2031 int GetPlayerLimit()
2034 return 2; // TODO: this workaround is needed since the mutator hook from duel can't be activated before the gametype is loaded (e.g. switching modes via gametype vote screen)
2035 // don't return map_maxplayers during intermission as it would interfere with MapHasRightSize()
2036 int player_limit = (autocvar_g_maxplayers >= 0 || intermission_running) ? autocvar_g_maxplayers : map_maxplayers;
2037 MUTATOR_CALLHOOK(GetPlayerLimit, player_limit);
2038 player_limit = M_ARGV(0, int);
2039 return player_limit < maxclients ? player_limit : 0;
2043 * Determines whether the player is allowed to join. This depends on cvar
2044 * g_maxplayers, if it isn't used this function always return true, otherwise
2045 * it checks whether the number of currently playing players exceeds g_maxplayers.
2046 * @return int number of free slots for players, 0 if none
2048 int nJoinAllowed(entity this, entity ignore)
2051 // this is called that way when checking if anyone may be able to join (to build qcstatus)
2052 // so report 0 free slots if restricted
2054 if(autocvar_g_forced_team_otherwise == "spectate")
2056 if(autocvar_g_forced_team_otherwise == "spectator")
2060 if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2061 return 0; // forced spectators can never join
2063 static float msg_time = 0;
2064 if(this && !INGAME(this) && ignore && PlayerInList(this, autocvar_g_playban_list))
2068 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PLAYBAN);
2069 msg_time = time + 0.5;
2074 // TODO simplify this
2075 int totalClients = 0;
2076 int currentlyPlaying = 0;
2077 FOREACH_CLIENT(true, {
2080 if(IS_REAL_CLIENT(it) && (IS_PLAYER(it) || INGAME(it)))
2084 int player_limit = GetPlayerLimit();
2088 free_slots = maxclients - totalClients;
2089 else if(player_limit > 0 && currentlyPlaying < player_limit)
2090 free_slots = min(maxclients - totalClients, player_limit - currentlyPlaying);
2092 if(this && !INGAME(this) && ignore && !free_slots && time > msg_time)
2094 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT, player_limit);
2095 msg_time = time + 0.5;
2101 bool joinAllowed(entity this)
2103 if (CS(this).version_mismatch) return false;
2104 if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
2105 if (!nJoinAllowed(this, this)) return false;
2106 if (teamplay && lockteams) return false;
2107 if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
2108 if (ShowTeamSelection(this)) return false;
2112 void show_entnum(entity this)
2114 // waypoint editor implements a similar feature for waypoints
2115 if (waypointeditor_enabled)
2118 if (wasfreed(this.wp_aimed))
2119 this.wp_aimed = NULL;
2121 WarpZone_crosshair_trace_plusvisibletriggers(this);
2126 if (ent != this.wp_aimed)
2128 string str = sprintf(
2129 "^7ent #%d\n^8 netname: ^3%s\n^8 classname: ^5%s\n^8 origin: ^2'%s'",
2130 etof(ent), ent.netname, ent.classname, vtos(ent.origin));
2131 debug_text_3d((ent.absmin + ent.absmax) * 0.5, str, 0, 7, '0 0 0');
2134 if (this.wp_aimed != ent)
2135 this.wp_aimed = ent;
2138 .bool dualwielding_prev;
2139 bool PlayerThink(entity this)
2141 if (game_stopped || intermission_running) {
2142 this.modelflags &= ~MF_ROCKET;
2143 if(intermission_running)
2144 IntermissionThink(this);
2148 if (timeout_status == TIMEOUT_ACTIVE) {
2149 // don't allow the player to turn around while game is paused
2150 // FIXME turn this into CSQC stuff
2151 this.v_angle = this.lastV_angle;
2152 this.angles = this.lastV_angle;
2153 this.fixangle = true;
2156 if (frametime) player_powerups(this);
2158 if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2160 if (IS_DEAD(this)) {
2161 if (this.personal && g_race_qualifying) {
2162 if (time > this.respawn_time) {
2163 STAT(RESPAWN_TIME, this) = this.respawn_time = time + 1; // only retry once a second
2165 CS(this).impulse = CHIMPULSE_SPEEDRUN.impulse;
2168 if (frametime) player_anim(this);
2170 if (this.respawn_flags & RESPAWN_DENY)
2172 STAT(RESPAWN_TIME, this) = 0;
2176 bool button_pressed = (PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this) || PHYS_INPUT_BUTTON_ATCK2(this) || PHYS_INPUT_BUTTON_HOOK(this) || PHYS_INPUT_BUTTON_USE(this));
2178 switch(this.deadflag)
2182 if ((this.respawn_flags & RESPAWN_FORCE) && !(this.respawn_time < this.respawn_time_max))
2183 this.deadflag = DEAD_RESPAWNING;
2184 else if (!button_pressed || (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE)))
2185 this.deadflag = DEAD_DEAD;
2191 this.deadflag = DEAD_RESPAWNABLE;
2192 else if (time >= this.respawn_time_max && (this.respawn_flags & RESPAWN_FORCE))
2193 this.deadflag = DEAD_RESPAWNING;
2196 case DEAD_RESPAWNABLE:
2198 if (!button_pressed || (this.respawn_flags & RESPAWN_FORCE))
2199 this.deadflag = DEAD_RESPAWNING;
2202 case DEAD_RESPAWNING:
2204 if (time > this.respawn_time)
2206 this.respawn_time = time + 1; // only retry once a second
2207 this.respawn_time_max = this.respawn_time;
2214 ShowRespawnCountdown(this);
2216 if (this.respawn_flags & RESPAWN_SILENT)
2217 STAT(RESPAWN_TIME, this) = 0;
2218 else if ((this.respawn_flags & RESPAWN_FORCE) && this.respawn_time < this.respawn_time_max)
2220 if (time < this.respawn_time)
2221 STAT(RESPAWN_TIME, this) = this.respawn_time;
2222 else if (this.deadflag != DEAD_RESPAWNING)
2223 STAT(RESPAWN_TIME, this) = -this.respawn_time_max;
2226 STAT(RESPAWN_TIME, this) = this.respawn_time;
2229 // if respawning, invert stat_respawn_time to indicate this, the client translates it
2230 if (this.deadflag == DEAD_RESPAWNING && STAT(RESPAWN_TIME, this) > 0)
2231 STAT(RESPAWN_TIME, this) *= -1;
2236 FixPlayermodel(this);
2238 if (this.shootfromfixedorigin != autocvar_g_shootfromfixedorigin) {
2239 strcpy(this.shootfromfixedorigin, autocvar_g_shootfromfixedorigin);
2240 stuffcmd(this, sprintf("\ncl_shootfromfixedorigin \"%s\"\n", autocvar_g_shootfromfixedorigin));
2243 // reset gun alignment when dual wielding status changes
2244 // to ensure guns are always aligned right and left
2245 bool dualwielding = W_DualWielding(this);
2246 if(this.dualwielding_prev != dualwielding)
2248 W_ResetGunAlign(this, CS_CVAR(this).cvar_cl_gunalign);
2249 this.dualwielding_prev = dualwielding;
2252 // LordHavoc: allow firing on move frames (sub-ticrate), this gives better timing on slow servers
2255 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2257 .entity weaponentity = weaponentities[slot];
2258 if(WEP_CVAR(vortex, charge_always))
2259 W_Vortex_Charge(this, weaponentity, frametime);
2260 W_WeaponFrame(this, weaponentity);
2266 // WEAPONTODO: Add a weapon request for this
2267 // rot vortex charge to the charge limit
2268 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2270 .entity weaponentity = weaponentities[slot];
2271 if (WEP_CVAR(vortex, charge_rot_rate) && this.(weaponentity).vortex_charge > WEP_CVAR(vortex, charge_limit) && this.(weaponentity).vortex_charge_rottime < time)
2272 this.(weaponentity).vortex_charge = bound(WEP_CVAR(vortex, charge_limit), this.(weaponentity).vortex_charge - WEP_CVAR(vortex, charge_rot_rate) * frametime / W_TICSPERFRAME, 1);
2277 this.dmg_team = max(0, this.dmg_team - autocvar_g_teamdamage_resetspeed * frametime);
2280 monsters_setstatus(this);
2285 .bool would_spectate;
2286 // merged SpectatorThink and ObserverThink (old names are here so you can grep for them)
2287 void ObserverOrSpectatorThink(entity this)
2289 bool is_spec = IS_SPEC(this);
2290 if ( CS(this).impulse )
2292 int r = MinigameImpulse(this, CS(this).impulse);
2294 CS(this).impulse = 0;
2296 if (is_spec && CS(this).impulse == IMP_weapon_drop.impulse)
2298 STAT(CAMERA_SPECTATOR, this) = (STAT(CAMERA_SPECTATOR, this) + 1) % 3;
2299 CS(this).impulse = 0;
2304 if (frametime && autocvar_sv_show_entnum) show_entnum(this);
2306 if (IS_BOT_CLIENT(this) && !CS(this).autojoin_checked)
2308 CS(this).autojoin_checked = true;
2309 TRANSMUTE(Player, this);
2310 PutClientInServer(this);
2312 .entity weaponentity = weaponentities[0];
2313 if(this.(weaponentity).m_weapon == WEP_Null)
2314 W_NextWeapon(this, 0, weaponentity);
2319 if (this.flags & FL_JUMPRELEASED) {
2320 if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
2321 this.flags &= ~FL_JUMPRELEASED;
2322 this.flags |= FL_SPAWNING;
2323 } else if((is_spec && (PHYS_INPUT_BUTTON_ATCK(this) || CS(this).impulse == 10 || CS(this).impulse == 15 || CS(this).impulse == 18 || (CS(this).impulse >= 200 && CS(this).impulse <= 209)))
2324 || (!is_spec && ((PHYS_INPUT_BUTTON_ATCK(this) && !CS(this).version_mismatch) || this.would_spectate))) {
2325 this.flags &= ~FL_JUMPRELEASED;
2326 if(SpectateNext(this)) {
2327 TRANSMUTE(Spectator, this);
2328 } else if (is_spec) {
2329 TRANSMUTE(Observer, this);
2330 PutClientInServer(this);
2333 this.would_spectate = false; // unable to spectate anyone
2335 CS(this).impulse = 0;
2336 } else if (is_spec) {
2337 if(CS(this).impulse == 12 || CS(this).impulse == 16 || CS(this).impulse == 19 || (CS(this).impulse >= 220 && CS(this).impulse <= 229)) {
2338 this.flags &= ~FL_JUMPRELEASED;
2339 if(SpectatePrev(this)) {
2340 TRANSMUTE(Spectator, this);
2342 TRANSMUTE(Observer, this);
2343 PutClientInServer(this);
2345 CS(this).impulse = 0;
2346 } else if(PHYS_INPUT_BUTTON_ATCK2(this)) {
2347 if(!observe_blocked_if_eliminated || !INGAME(this)) {
2348 this.would_spectate = false;
2349 this.flags &= ~FL_JUMPRELEASED;
2350 TRANSMUTE(Observer, this);
2351 PutClientInServer(this);
2353 } else if(!SpectateUpdate(this) && !SpectateNext(this)) {
2354 PutObserverInServer(this, false, true);
2355 this.would_spectate = true;
2359 bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating;
2360 if (PHYS_INPUT_BUTTON_USE(this))
2361 wouldclip = !wouldclip;
2362 int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP);
2363 set_movetype(this, preferred_movetype);
2365 } else { // jump pressed
2366 if ((is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)))
2367 || (!is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this)))) {
2368 this.flags |= FL_JUMPRELEASED;
2369 // primary attack pressed
2370 if(this.flags & FL_SPAWNING)
2372 this.flags &= ~FL_SPAWNING;
2373 if(joinAllowed(this))
2375 else if(time < CS(this).jointime + MIN_SPEC_TIME)
2376 CS(this).autojoin_checked = -1;
2380 if(is_spec && !SpectateUpdate(this))
2381 PutObserverInServer(this, false, true);
2384 this.flags |= FL_CLIENT | FL_NOTARGET;
2387 void PlayerUseKey(entity this)
2389 if (!IS_PLAYER(this))
2396 vehicles_exit(this.vehicle, VHEF_NORMAL);
2400 else if(autocvar_g_vehicles_enter)
2402 if(!game_stopped && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2404 entity head, closest_target = NULL;
2405 head = WarpZone_FindRadius(this.origin, autocvar_g_vehicles_enter_radius, true);
2407 while(head) // find the closest acceptable target to enter
2409 if(IS_VEHICLE(head) && !IS_DEAD(head) && head.takedamage != DAMAGE_NO)
2410 if(!head.owner || ((head.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(head.owner, this)))
2414 if(vlen2(this.origin - head.origin) < vlen2(this.origin - closest_target.origin))
2415 { closest_target = head; }
2417 else { closest_target = head; }
2423 if(closest_target) { vehicles_enter(this, closest_target); return; }
2427 // a use key was pressed; call handlers
2428 MUTATOR_CALLHOOK(PlayerUseKey, this);
2436 Called every frame for each real client by DP (and for each bot by StartFrame()),
2437 and when executing every asynchronous move, so only include things that MUST be done then.
2438 Use PlayerFrame() instead for code that only needs to run once per server frame.
2439 frametime == 0 in the asynchronous code path.
2441 TODO: move more stuff from here and PlayerThink() and ObserverOrSpectatorThink() to PlayerFrame() (frametime is always set there)
2444 .float last_vehiclecheck;
2445 void PlayerPreThink (entity this)
2447 WarpZone_PlayerPhysics_FixVAngle(this);
2449 zoomstate_set = false;
2451 MUTATOR_CALLHOOK(PlayerPreThink, this);
2453 if(PHYS_INPUT_BUTTON_USE(this) && !CS(this).usekeypressed)
2455 CS(this).usekeypressed = PHYS_INPUT_BUTTON_USE(this);
2457 if (IS_PLAYER(this)) {
2458 if (IS_REAL_CLIENT(this) && time < CS(this).jointime + MIN_SPEC_TIME)
2459 error("Client can't be spawned as player on connection!");
2460 if(!PlayerThink(this))
2463 else if (game_stopped || intermission_running) {
2464 if(intermission_running)
2465 IntermissionThink(this);
2468 else if (IS_REAL_CLIENT(this) && CS(this).autojoin_checked <= 0 && time >= CS(this).jointime + MIN_SPEC_TIME)
2470 bool early_join_requested = (CS(this).autojoin_checked < 0);
2471 CS(this).autojoin_checked = 1;
2472 // don't do this in ClientConnect
2473 // many things can go wrong if a client is spawned as player on connection
2474 if (early_join_requested || MUTATOR_CALLHOOK(AutoJoinOnConnection, this)
2475 || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
2476 && (!teamplay || autocvar_g_balance_teams)))
2478 if(joinAllowed(this))
2483 else if (IS_OBSERVER(this) || IS_SPEC(this)) {
2484 ObserverOrSpectatorThink(this);
2487 // WEAPONTODO: Add weapon request for this
2488 if (!zoomstate_set) {
2489 bool wep_zoomed = false;
2490 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
2492 .entity weaponentity = weaponentities[slot];
2493 Weapon thiswep = this.(weaponentity).m_weapon;
2494 if(thiswep != WEP_Null && thiswep.wr_zoom)
2495 wep_zoomed += thiswep.wr_zoom(thiswep, this);
2497 SetZoomState(this, PHYS_INPUT_BUTTON_ZOOM(this) || PHYS_INPUT_BUTTON_ZOOMSCRIPT(this) || wep_zoomed);
2500 // Voice sound effects
2501 if (CS(this).teamkill_soundtime && time > CS(this).teamkill_soundtime)
2503 CS(this).teamkill_soundtime = 0;
2505 entity e = CS(this).teamkill_soundsource;
2506 entity oldpusher = e.pusher;
2508 PlayerSound(e, playersound_teamshoot, CH_VOICE, VOL_BASEVOICE, VOICETYPE_LASTATTACKER_ONLY);
2509 e.pusher = oldpusher;
2512 if (CS(this).taunt_soundtime && time > CS(this).taunt_soundtime) {
2513 CS(this).taunt_soundtime = 0;
2514 PlayerSound(this, playersound_taunt, CH_VOICE, VOL_BASEVOICE, VOICETYPE_AUTOTAUNT);
2517 target_voicescript_next(this);
2520 void DrownPlayer(entity this)
2522 if(IS_DEAD(this) || game_stopped || time < game_starttime || this.vehicle
2523 || STAT(FROZEN, this) || this.watertype != CONTENT_WATER)
2525 STAT(AIR_FINISHED, this) = 0;
2529 if (this.waterlevel != WATERLEVEL_SUBMERGED)
2531 if(STAT(AIR_FINISHED, this) && STAT(AIR_FINISHED, this) < time)
2532 PlayerSound(this, playersound_gasp, CH_PLAYER, VOL_BASE, VOICETYPE_PLAYERSOUND);
2533 STAT(AIR_FINISHED, this) = 0;
2537 if (!STAT(AIR_FINISHED, this))
2538 STAT(AIR_FINISHED, this) = time + autocvar_g_balance_contents_drowndelay;
2539 if (STAT(AIR_FINISHED, this) < time)
2541 if (this.pain_finished < time)
2543 Damage (this, NULL, NULL, autocvar_g_balance_contents_playerdamage_drowning * autocvar_g_balance_contents_damagerate, DEATH_DROWN.m_id, DMG_NOWEP, this.origin, '0 0 0');
2544 this.pain_finished = time + 0.5;
2550 .bool move_qcphysics;
2552 void Player_Physics(entity this)
2554 this.movetype = (this.move_qcphysics) ? MOVETYPE_QCPLAYER : this.move_movetype;
2556 if(!this.move_qcphysics)
2559 if(!frametime && !CS(this).pm_frametime)
2562 Movetype_Physics_NoMatchTicrate(this, CS(this).pm_frametime, true);
2564 CS(this).pm_frametime = 0;
2571 Called every frame for each real client by DP (and for each bot by StartFrame()),
2572 and when executing every asynchronous move, so only include things that MUST be done then.
2573 Use PlayerFrame() instead for code that only needs to run once per server frame.
2574 frametime == 0 in the asynchronous code path.
2577 void PlayerPostThink (entity this)
2579 Player_Physics(this);
2581 if (IS_PLAYER(this)) {
2582 if(this.death_time == time && IS_DEAD(this))
2584 // player's bbox gets resized now, instead of in the damage event that killed the player,
2585 // once all the damage events of this frame have been processed with normal size
2587 setsize(this, this.mins, this.maxs);
2590 UpdateChatBubble(this);
2591 if (CS(this).impulse) ImpulseCommands(this);
2592 GetPressedKeys(this);
2595 CSQCMODEL_AUTOUPDATE(this);
2599 else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this))
2601 CS(this).pressedkeys = 0;
2602 STAT(PRESSED_KEYS, this) = 0;
2605 CSQCMODEL_AUTOUPDATE(this);
2612 Called every frame for each client by StartFrame().
2613 Use this for code that only needs to run once per server frame.
2614 frametime is always set here.
2617 void PlayerFrame (entity this)
2619 // formerly PreThink code
2620 STAT(GUNALIGN, this) = CS_CVAR(this).cvar_cl_gunalign; // TODO
2621 STAT(MOVEVARS_CL_TRACK_CANJUMP, this) = CS_CVAR(this).cvar_cl_movement_track_canjump;
2623 // physics frames: update anticheat stuff
2624 anticheat_prethink(this);
2626 // Check if spectating is allowed
2627 if (blockSpectators && IS_REAL_CLIENT(this)
2628 && (IS_SPEC(this) || IS_OBSERVER(this)) && !INGAME(this)
2629 && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime))
2631 if (dropclient_schedule(this))
2632 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING);
2635 // Check for nameless players
2636 if (this.netname == "" || this.netname != CS(this).netname_previous)
2638 bool assume_unchanged = (CS(this).netname_previous == "");
2639 if (autocvar_sv_name_maxlength > 0 && strlennocol(this.netname) > autocvar_sv_name_maxlength)
2641 int new_length = textLengthUpToLength(this.netname, autocvar_sv_name_maxlength, strlennocol);
2642 this.netname = strzone(strcat(substring(this.netname, 0, new_length), "^7"));
2643 sprint(this, sprintf("Warning: your name is longer than %d characters, it has been truncated.\n", autocvar_sv_name_maxlength));
2644 assume_unchanged = false;
2645 // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2647 if (isInvisibleString(this.netname))
2649 this.netname = strzone(sprintf("Player#%d", this.playerid));
2650 sprint(this, "Warning: invisible names are not allowed.\n");
2651 assume_unchanged = false;
2652 // stuffcmd(this, strcat("name ", this.netname, "\n")); // maybe?
2654 if (!assume_unchanged && autocvar_sv_eventlog)
2655 GameLogEcho(strcat(":name:", ftos(this.playerid), ":", playername(this.netname, this.team, false)));
2656 strcpy(CS(this).netname_previous, this.netname);
2660 if (CS(this).version_nagtime && CS_CVAR(this).cvar_g_xonoticversion && time > CS(this).version_nagtime)
2662 CS(this).version_nagtime = 0;
2663 if (strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(CS_CVAR(this).cvar_g_xonoticversion, "autobuild", 0) >= 0)
2667 else if (strstrofs(autocvar_g_xonoticversion, "git", 0) >= 0 || strstrofs(autocvar_g_xonoticversion, "autobuild", 0) >= 0)
2670 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_BETA, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2674 int r = vercmp(CS_CVAR(this).cvar_g_xonoticversion, autocvar_g_xonoticversion);
2675 if (r < 0) // old client
2676 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OUTDATED, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2677 else if (r > 0) // old server
2678 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_VERSION_OLD, autocvar_g_xonoticversion, CS_CVAR(this).cvar_g_xonoticversion);
2683 if (!(this.flags & FL_GODMODE) && this.max_armorvalue)
2685 Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_GODMODE_OFF, this.max_armorvalue);
2686 this.max_armorvalue = 0;
2690 if (IS_PLAYER(this) && time >= game_starttime)
2692 if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING)
2694 STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + frametime * this.revive_speed, 1);
2695 SetResourceExplicit(this, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * start_health));
2697 this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
2699 if (STAT(REVIVE_PROGRESS, this) >= 1)
2700 Unfreeze(this, false);
2702 else if (STAT(FROZEN, this) == FROZEN_TEMP_DYING)
2704 STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - frametime * this.revive_speed, 1);
2705 SetResourceExplicit(this, RES_HEALTH, max(0, autocvar_g_nades_ice_health + (start_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
2707 if (GetResource(this, RES_HEALTH) < 1)
2710 vehicles_exit(this.vehicle, VHEF_RELEASE);
2711 if(this.event_damage)
2712 this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
2714 else if (STAT(REVIVE_PROGRESS, this) <= 0)
2715 Unfreeze(this, false);
2720 if(autocvar_g_vehicles_enter && (time > this.last_vehiclecheck) && !game_stopped && !this.vehicle)
2721 if(IS_PLAYER(this) && !STAT(FROZEN, this) && !IS_DEAD(this) && !IS_INDEPENDENT_PLAYER(this))
2723 FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_vehicles_enter_radius, IS_VEHICLE(it) && !IS_DEAD(it) && it.takedamage != DAMAGE_NO,
2727 if(!it.team || SAME_TEAM(this, it))
2728 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER);
2729 else if(autocvar_g_vehicles_steal)
2730 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_STEAL);
2732 else if((it.vehicle_flags & VHF_MULTISLOT) && SAME_TEAM(it.owner, this))
2734 Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_VEHICLE_ENTER_GUNNER);
2738 this.last_vehiclecheck = time + 1;
2743 // formerly PostThink code
2744 if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0))
2745 if (IS_REAL_CLIENT(this))
2746 if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
2747 if (!intermission_running) // NextLevel() kills all centerprints after setting this true
2749 int totalClients = 0;
2750 if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0)
2752 // maxidle disabled in local matches by not counting clients (totalClients 0)
2753 if (server_is_dedicated)
2755 FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots,
2759 if (maxclients - totalClients > autocvar_sv_maxidle_slots)
2763 else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2765 FOREACH_CLIENT(IS_REAL_CLIENT(it),
2771 if (totalClients < autocvar_sv_maxidle_minplayers)
2773 // idle kick disabled
2774 CS(this).parm_idlesince = time;
2776 else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10
2778 if (CS(this).idlekick_lasttimeleft)
2780 CS(this).idlekick_lasttimeleft = 0;
2781 Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_IDLING);
2786 float maxidle_time = autocvar_sv_maxidle;
2787 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2788 maxidle_time = autocvar_sv_maxidle_playertospectator;
2789 float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
2790 float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
2791 if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
2793 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2794 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
2796 Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
2798 if (timeleft <= 0) {
2799 if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
2801 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
2802 PutObserverInServer(this, true, true);
2806 if (dropclient_schedule(this))
2807 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time);
2811 else if (timeleft <= countdown_time) {
2812 if (timeleft != CS(this).idlekick_lasttimeleft)
2813 play2(this, SND(TALK2));
2814 CS(this).idlekick_lasttimeleft = timeleft;
2823 this.solid = SOLID_NOT;
2824 this.takedamage = DAMAGE_NO;
2825 set_movetype(this, MOVETYPE_NONE);
2826 CS(this).teamkill_complain = 0;
2827 CS(this).teamkill_soundtime = 0;
2828 CS(this).teamkill_soundsource = NULL;
2831 if (this.waypointsprite_attachedforcarrier) {
2832 float hp = healtharmor_maxdamage(GetResource(this, RES_HEALTH), GetResource(this, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x;
2833 WaypointSprite_UpdateHealth(this.waypointsprite_attachedforcarrier, hp);
2837 // hack to copy the button fields from the client entity to the Client State
2838 void PM_UpdateButtons(entity this, entity store)
2841 store.impulse = this.impulse;
2844 bool typing = this.buttonchat || this.button12;
2846 store.button0 = (typing) ? 0 : this.button0;
2848 store.button2 = (typing) ? 0 : this.button2;
2849 store.button3 = (typing) ? 0 : this.button3;
2850 store.button4 = this.button4;
2851 store.button5 = (typing) ? 0 : this.button5;
2852 store.button6 = this.button6;
2853 store.button7 = this.button7;
2854 store.button8 = this.button8;
2855 store.button9 = this.button9;
2856 store.button10 = this.button10;
2857 store.button11 = this.button11;
2858 store.button12 = this.button12;
2859 store.button13 = this.button13;
2860 store.button14 = this.button14;
2861 store.button15 = this.button15;
2862 store.button16 = this.button16;
2863 store.buttonuse = this.buttonuse;
2864 store.buttonchat = this.buttonchat;
2866 store.cursor_active = this.cursor_active;
2867 store.cursor_screen = this.cursor_screen;
2868 store.cursor_trace_start = this.cursor_trace_start;
2869 store.cursor_trace_endpos = this.cursor_trace_endpos;
2870 store.cursor_trace_ent = this.cursor_trace_ent;
2872 store.ping = this.ping;
2873 store.ping_packetloss = this.ping_packetloss;
2874 store.ping_movementloss = this.ping_movementloss;
2876 store.v_angle = this.v_angle;
2877 store.movement = this.movement;
2880 NET_HANDLE(fpsreport, bool)
2882 int fps = ReadShort();
2883 PlayerScore_Set(sender, SP_FPS, fps);