X-Git-Url: https://git.xonotic.org/?a=blobdiff_plain;f=qcsrc%2Fserver%2Fclient.qc;h=804025ace1e5efab8a88d140ff2db5808a72b2eb;hb=350dd64390082f01f29e0d4e4ef70390ba700219;hp=6bebfeb3666d33591dfe7c5c865344dba112979b;hpb=02563156f7367d73057f6fd2f7327487aec7244d;p=xonotic%2Fxonotic-data.pk3dir.git diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 6bebfeb36..804025ace 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -199,19 +201,19 @@ string CheckPlayerModel(string plyermodel) { FallbackPlayerModel = strzone(cvar_defstring("_cl_playermodel")); } // only in right path - if( substring(plyermodel,0,14) != "models/player/") + if(substring(plyermodel, 0, 14) != "models/player/") return FallbackPlayerModel; // only good file extensions - if(substring(plyermodel,-4,4) != ".zym") - if(substring(plyermodel,-4,4) != ".dpm") - if(substring(plyermodel,-4,4) != ".iqm") - if(substring(plyermodel,-4,4) != ".md3") - if(substring(plyermodel,-4,4) != ".psk") + if(substring(plyermodel, -4, 4) != ".iqm" + && substring(plyermodel, -4, 4) != ".zym" + && substring(plyermodel, -4, 4) != ".dpm" + && substring(plyermodel, -4, 4) != ".md3" + && substring(plyermodel, -4, 4) != ".psk") + { return FallbackPlayerModel; + } // forbid the LOD models - if(substring(plyermodel, -9,5) == "_lod1") - return FallbackPlayerModel; - if(substring(plyermodel, -9,5) == "_lod2") + if(substring(plyermodel, -9, 5) == "_lod1" || substring(plyermodel, -9, 5) == "_lod2") return FallbackPlayerModel; if(plyermodel != strtolower(plyermodel)) return FallbackPlayerModel; @@ -253,6 +255,7 @@ void PutObserverInServer(entity this) if (vote_called) { VoteCount(false); } ReadyCount(); } + entcs_update_players(this); } entity spot = SelectSpawnPoint(this, true); @@ -334,9 +337,6 @@ void PutObserverInServer(entity this) this.scale = 0; this.fade_time = 0; this.pain_finished = 0; - STAT(STRENGTH_FINISHED, this) = 0; - STAT(INVINCIBLE_FINISHED, this) = 0; - STAT(SUPERWEAPONS_FINISHED, this) = 0; STAT(AIR_FINISHED, this) = 0; //this.dphitcontentsmask = 0; this.dphitcontentsmask = DPCONTENTS_SOLID; @@ -372,7 +372,6 @@ void PutObserverInServer(entity this) this.punchangle = '0 0 0'; this.punchvector = '0 0 0'; this.oldvelocity = this.velocity; - this.fire_endtime = -1; this.event_damage = func_null; this.event_heal = func_null; @@ -394,6 +393,9 @@ void PutObserverInServer(entity this) SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR); this.frags = FRAGS_SPECTATOR; } + + bot_relinkplayerlist(); + if (CS(this).just_joined) CS(this).just_joined = false; } @@ -589,11 +591,13 @@ void PutPlayerInServer(entity this) PS(this).dual_weapons = '0 0 0'; - STAT(SUPERWEAPONS_FINISHED, this) = (STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) ? time + autocvar_g_balance_superweapons_time : 0; + if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) + StatusEffects_apply(STATUSEFFECT_Superweapons, this, time + autocvar_g_balance_superweapons_time, 0); this.items = start_items; - this.spawnshieldtime = time + autocvar_g_spawnshieldtime; + float shieldtime = time + autocvar_g_spawnshieldtime; + this.pauserotarmor_finished = time + autocvar_g_balance_pause_armor_rot_spawn; this.pauserothealth_finished = time + autocvar_g_balance_pause_health_rot_spawn; this.pauserotfuel_finished = time + autocvar_g_balance_pause_fuel_rot_spawn; @@ -601,19 +605,20 @@ void PutPlayerInServer(entity this) if (!sv_ready_restart_after_countdown && time < game_starttime) { float f = game_starttime - time; - this.spawnshieldtime += f; + shieldtime += f; this.pauserotarmor_finished += f; this.pauserothealth_finished += f; this.pauseregen_finished += f; } + StatusEffects_apply(STATUSEFFECT_SpawnShield, this, shieldtime, 0); + this.damageforcescale = autocvar_g_player_damageforcescale; this.death_time = 0; this.respawn_flags = 0; this.respawn_time = 0; STAT(RESPAWN_TIME, this) = 0; - bool q3dfcompat = autocvar_sv_q3defragcompat && autocvar_sv_q3defragcompat_changehitbox; - this.scale = ((q3dfcompat) ? 0.9 : autocvar_sv_player_scale); + this.scale = ((q3compat && autocvar_sv_q3compat_changehitbox) ? 0.9 : autocvar_sv_player_scale); this.fade_time = 0; this.pain_finished = 0; this.pushltime = 0; @@ -637,16 +642,9 @@ void PutPlayerInServer(entity this) this.punchangle = '0 0 0'; this.punchvector = '0 0 0'; - STAT(STRENGTH_FINISHED, this) = 0; - STAT(INVINCIBLE_FINISHED, this) = 0; - this.fire_endtime = -1; STAT(REVIVE_PROGRESS, this) = 0; this.revival_time = 0; - // TODO: we can't set these in the PlayerSpawn hook since the target code is called before it! - STAT(BUFFS, this) = 0; - STAT(BUFF_TIME, this) = 0; - STAT(AIR_FINISHED, this) = 0; this.waterlevel = WATERLEVEL_NONE; this.watertype = CONTENT_EMPTY; @@ -749,6 +747,10 @@ void PutPlayerInServer(entity this) } }); + Unfreeze(this, false); + + MUTATOR_CALLHOOK(PlayerSpawn, this, spot); + { string s = spot.target; if(g_assault || g_race) // TODO: make targeting work in assault & race without this hack @@ -758,10 +760,6 @@ void PutPlayerInServer(entity this) spot.target = s; } - Unfreeze(this, false); - - MUTATOR_CALLHOOK(PlayerSpawn, this, spot); - if (autocvar_spawn_debug) { sprint(this, strcat("spawnpoint origin: ", vtos(spot.origin), "\n")); @@ -771,14 +769,15 @@ void PutPlayerInServer(entity this) for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) { .entity weaponentity = weaponentities[slot]; + entity w_ent = this.(weaponentity); if(slot == 0 || autocvar_g_weaponswitch_debug == 1) - this.(weaponentity).m_switchweapon = w_getbestweapon(this, weaponentity); + w_ent.m_switchweapon = w_getbestweapon(this, weaponentity); else - this.(weaponentity).m_switchweapon = WEP_Null; - this.(weaponentity).m_weapon = WEP_Null; - this.(weaponentity).weaponname = ""; - this.(weaponentity).m_switchingweapon = WEP_Null; - this.(weaponentity).cnt = -1; + w_ent.m_switchweapon = WEP_Null; + w_ent.m_weapon = WEP_Null; + w_ent.weaponname = ""; + w_ent.m_switchingweapon = WEP_Null; + w_ent.cnt = -1; } MUTATOR_CALLHOOK(PlayerWeaponSelect, this); @@ -824,6 +823,8 @@ void PutClientInServer(entity this) } else if (IS_PLAYER(this)) { PutPlayerInServer(this); } + + bot_relinkplayerlist(); } // TODO do we need all these fields, or should we stop autodetecting runtime @@ -1042,14 +1043,10 @@ string getwelcomemessage(entity this) modifications = strcat(modifications, ", Weapons stay"); if(autocvar_g_jetpack) modifications = strcat(modifications, ", Jet pack"); - if(autocvar_g_powerups == 0) - modifications = strcat(modifications, ", No powerups"); - if(autocvar_g_powerups > 0) - modifications = strcat(modifications, ", Powerups"); modifications = substring(modifications, 2, strlen(modifications) - 2); string versionmessage = GetClientVersionMessage(this); - string s = strcat(versionmessage, "^8\n^8\nhost is ^9", autocvar_hostname, "^8\n"); + string s = strcat(versionmessage, "^8\n^8\nserver is ^9", autocvar_hostname, "^8\n"); s = strcat(s, "^8\nmatch type is ^1", gamemode_name, "^8\n"); @@ -1079,8 +1076,6 @@ string getwelcomemessage(entity this) return s; } -bool autocvar_sv_qcphysics = true; // TODO this is for testing - remove when qcphysics work - /** ============= ClientConnect @@ -1200,6 +1195,8 @@ Called when a client disconnects from the server ============= */ .entity chatbubbleentity; +void player_powerups_remove_all(entity this); + void ClientDisconnect(entity this) { assert(IS_CLIENT(this), return); @@ -1252,6 +1249,8 @@ void ClientDisconnect(entity this) ReadyCount(); if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false); + player_powerups_remove_all(this); // stop powerup sound + ONREMOVE(this); } @@ -1444,6 +1443,18 @@ void play_countdown(entity this, float finished, Sound samp) sound (this, CH_INFO, samp, VOL_BASE, ATTEN_NORM); } +void player_powerups_remove_all(entity this) +{ + if (this.items & IT_SUPERWEAPON) + { + // don't play the poweroff sound when the game restarts or the player disconnects + if (time > game_starttime + 1 && IS_CLIENT(this)) + sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM); + stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound + this.items -= (this.items & IT_SUPERWEAPON); + } +} + void player_powerups(entity this) { if((this.items & IT_USING_JETPACK) && !IS_DEAD(this) && !game_stopped) @@ -1451,19 +1462,10 @@ void player_powerups(entity this) else this.modelflags &= ~MF_ROCKET; - this.effects &= ~(EF_RED | EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT | EF_FLAME | EF_NODEPTHTEST); + this.effects &= ~EF_NODEPTHTEST; if (IS_DEAD(this)) - { - if (this.items & (ITEM_Strength.m_itemid | ITEM_Shield.m_itemid | IT_SUPERWEAPON)) - { - sound(this, CH_INFO, SND_POWEROFF, VOL_BASE, ATTEN_NORM); - stopsound(this, CH_TRIGGER_SINGLE); // get rid of the pickup sound - this.items &= ~ITEM_Strength.m_itemid; - this.items &= ~ITEM_Shield.m_itemid; - this.items -= (this.items & IT_SUPERWEAPON); - } - } + player_powerups_remove_all(this); if((this.alpha < 0 || IS_DEAD(this)) && !this.vehicle) // don't apply the flags if the player is gibbed return; @@ -1471,58 +1473,14 @@ void player_powerups(entity this) // add a way to see what the items were BEFORE all of these checks for the mutator hook int items_prev = this.items; - Fire_ApplyDamage(this); - Fire_ApplyEffect(this); - if (!MUTATOR_IS_ENABLED(mutator_instagib)) { - if (this.items & ITEM_Strength.m_itemid) - { - play_countdown(this, STAT(STRENGTH_FINISHED, this), SND_POWEROFF); - this.effects = this.effects | (EF_BLUE | EF_ADDITIVE | EF_FULLBRIGHT); - if (time > STAT(STRENGTH_FINISHED, this)) - { - this.items = this.items - (this.items & ITEM_Strength.m_itemid); - //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERDOWN_STRENGTH, this.netname); - Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERDOWN_STRENGTH); - } - } - else - { - if (time < STAT(STRENGTH_FINISHED, this)) - { - this.items = this.items | ITEM_Strength.m_itemid; - if(!g_cts) - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERUP_STRENGTH, this.netname); - Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERUP_STRENGTH); - } - } - if (this.items & ITEM_Shield.m_itemid) - { - play_countdown(this, STAT(INVINCIBLE_FINISHED, this), SND_POWEROFF); - this.effects = this.effects | (EF_RED | EF_ADDITIVE | EF_FULLBRIGHT); - if (time > STAT(INVINCIBLE_FINISHED, this)) - { - this.items = this.items - (this.items & ITEM_Shield.m_itemid); - //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERDOWN_SHIELD, this.netname); - Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERDOWN_SHIELD); - } - } - else - { - if (time < STAT(INVINCIBLE_FINISHED, this)) - { - this.items = this.items | ITEM_Shield.m_itemid; - if(!g_cts) - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_POWERUP_SHIELD, this.netname); - Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_POWERUP_SHIELD); - } - } + // NOTE: superweapons are a special case and as such are handled here instead of the status effects system if (this.items & IT_SUPERWEAPON) { if (!(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS)) { - STAT(SUPERWEAPONS_FINISHED, this) = 0; + StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_NORMAL); this.items = this.items - (this.items & IT_SUPERWEAPON); //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SUPERWEAPON_LOST, this.netname); Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_SUPERWEAPON_LOST); @@ -1533,8 +1491,8 @@ void player_powerups(entity this) } else { - play_countdown(this, STAT(SUPERWEAPONS_FINISHED, this), SND_POWEROFF); - if (time > STAT(SUPERWEAPONS_FINISHED, this)) + play_countdown(this, StatusEffects_gettime(STATUSEFFECT_Superweapons, this), SND_POWEROFF); + if (time > StatusEffects_gettime(STATUSEFFECT_Superweapons, this)) { this.items = this.items - (this.items & IT_SUPERWEAPON); STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS; @@ -1545,7 +1503,7 @@ void player_powerups(entity this) } else if(STAT(WEAPONS, this) & WEPSET_SUPERWEAPONS) { - if (time < STAT(SUPERWEAPONS_FINISHED, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS)) + if (time < StatusEffects_gettime(STATUSEFFECT_Superweapons, this) || (this.items & IT_UNLIMITED_SUPERWEAPONS)) { this.items = this.items | IT_SUPERWEAPON; if(!(this.items & IT_UNLIMITED_SUPERWEAPONS)) @@ -1557,13 +1515,14 @@ void player_powerups(entity this) } else { - STAT(SUPERWEAPONS_FINISHED, this) = 0; + if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) + StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_TIMEOUT); STAT(WEAPONS, this) &= ~WEPSET_SUPERWEAPONS; } } - else + else if(StatusEffects_active(STATUSEFFECT_Superweapons, this)) // cheaper to check than to update each frame! { - STAT(SUPERWEAPONS_FINISHED, this) = 0; + StatusEffects_remove(STATUSEFFECT_Superweapons, this, STATUSEFFECT_REMOVE_CLEAR); } } @@ -1573,10 +1532,6 @@ void player_powerups(entity this) if(autocvar_g_fullbrightplayers) this.effects = this.effects | EF_FULLBRIGHT; - if (time >= game_starttime) - if (time < this.spawnshieldtime) - this.effects = this.effects | (EF_ADDITIVE | EF_FULLBRIGHT); - MUTATOR_CALLHOOK(PlayerPowerups, this, items_prev); } @@ -1703,6 +1658,14 @@ void SetZoomState(entity this, float newzoom) void GetPressedKeys(entity this) { MUTATOR_CALLHOOK(GetPressedKeys, this); + if (game_stopped) + { + CS(this).pressedkeys = 0; + STAT(PRESSED_KEYS, this) = 0; + return; + } + + // NOTE: GetPressedKeys and PM_dodging_GetPressedKeys use similar code int keys = STAT(PRESSED_KEYS, this); keys = BITSET(keys, KEY_FORWARD, CS(this).movement.x > 0); keys = BITSET(keys, KEY_BACKWARD, CS(this).movement.x < 0); @@ -1745,9 +1708,6 @@ void SpectateCopy(entity this, entity spectatee) this.items = spectatee.items; STAT(LAST_PICKUP, this) = STAT(LAST_PICKUP, spectatee); STAT(HIT_TIME, this) = STAT(HIT_TIME, spectatee); - STAT(STRENGTH_FINISHED, this) = STAT(STRENGTH_FINISHED, spectatee); - STAT(INVINCIBLE_FINISHED, this) = STAT(INVINCIBLE_FINISHED, spectatee); - STAT(SUPERWEAPONS_FINISHED, this) = STAT(SUPERWEAPONS_FINISHED, spectatee); STAT(AIR_FINISHED, this) = STAT(AIR_FINISHED, spectatee); STAT(PRESSED_KEYS, this) = STAT(PRESSED_KEYS, spectatee); STAT(WEAPONS, this) = STAT(WEAPONS, spectatee); @@ -2064,23 +2024,6 @@ int nJoinAllowed(entity this, entity ignore) return free_slots; } -/** - * Checks whether the client is an observer or spectator, if so, he will get kicked after - * g_maxplayers_spectator_blocktime seconds - */ -void checkSpectatorBlock(entity this) -{ - if(IS_SPEC(this) || IS_OBSERVER(this)) - if(!this.caplayer) - if(IS_REAL_CLIENT(this)) - { - if( time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime) ) { - Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING); - dropclient(this); - } - } -} - void PrintWelcomeMessage(entity this) { if(CS(this).motd_actived_time == 0) @@ -2298,6 +2241,7 @@ bool PlayerThink(entity this) } .bool would_spectate; +// merged SpectatorThink and ObserverThink (old names are here so you can grep for them) void ObserverOrSpectatorThink(entity this) { bool is_spec = IS_SPEC(this); @@ -2328,6 +2272,8 @@ void ObserverOrSpectatorThink(entity this) TRANSMUTE(Observer, this); PutClientInServer(this); } + else + this.would_spectate = false; // unable to spectate anyone if (is_spec) CS(this).impulse = 0; } else if (is_spec) { @@ -2351,10 +2297,13 @@ void ObserverOrSpectatorThink(entity this) } } else { - int preferred_movetype = ((!PHYS_INPUT_BUTTON_USE(this) ? CS_CVAR(this).cvar_cl_clippedspectating : !CS_CVAR(this).cvar_cl_clippedspectating) ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP); + bool wouldclip = CS_CVAR(this).cvar_cl_clippedspectating; + if (PHYS_INPUT_BUTTON_USE(this)) + wouldclip = !wouldclip; + int preferred_movetype = (wouldclip ? MOVETYPE_FLY_WORLDONLY : MOVETYPE_NOCLIP); set_movetype(this, preferred_movetype); } - } else { + } else { // jump pressed if ((is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this))) || (!is_spec && !(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_JUMP(this)))) { this.flags |= FL_JUMPRELEASED; @@ -2438,12 +2387,16 @@ void PlayerPreThink (entity this) if (frametime) { // physics frames: update anticheat stuff anticheat_prethink(this); - } - if (blockSpectators && frametime) { // WORKAROUND: only use dropclient in server frames (frametime set). // Never use it in cl_movement frames (frametime zero). - checkSpectatorBlock(this); + if (blockSpectators && IS_REAL_CLIENT(this) + && (IS_SPEC(this) || IS_OBSERVER(this)) && !this.caplayer + && time > (CS(this).spectatortime + autocvar_g_maxplayers_spectator_blocktime)) + { + if (dropclient_schedule(this)) + Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_QUIT_KICK_SPECTATING); + } } zoomstate_set = false; @@ -2497,7 +2450,7 @@ void PlayerPreThink (entity this) this.max_armorvalue = 0; } - if (frametime && IS_PLAYER(this)) + if (frametime && IS_PLAYER(this) && time >= game_starttime) { if (STAT(FROZEN, this) == FROZEN_TEMP_REVIVING) { @@ -2620,15 +2573,6 @@ void PlayerPreThink (entity this) } target_voicescript_next(this); - - // WEAPONTODO: Move into weaponsystem somehow - // if a player goes unarmed after holding a loaded weapon, empty his clip size and remove the crosshair ammo ring - for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) - { - .entity weaponentity = weaponentities[slot]; - if(this.(weaponentity).m_weapon == WEP_Null) - this.(weaponentity).clip_load = this.(weaponentity).clip_size = 0; - } } void DrownPlayer(entity this) @@ -2689,22 +2633,39 @@ void PlayerPostThink (entity this) { Player_Physics(this); - if (autocvar_sv_maxidle > 0) + if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)) if (frametime) // WORKAROUND: only use dropclient in server frames (frametime set). Never use it in cl_movement frames (frametime zero). if (IS_REAL_CLIENT(this)) - if (IS_PLAYER(this) || autocvar_sv_maxidle_spectatorsareidle) + if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators) + if (!intermission_running) // NextLevel() kills all centerprints after setting this true { int totalClients = 0; - if(autocvar_sv_maxidle_slots > 0) + if(autocvar_sv_maxidle > 0 && autocvar_sv_maxidle_slots > 0) { - FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots, + // maxidle disabled in local matches by not counting clients (totalClients 0) + if (server_is_dedicated) + { + FOREACH_CLIENT(IS_REAL_CLIENT(it) || autocvar_sv_maxidle_slots_countbots, + { + ++totalClients; + }); + if (maxclients - totalClients > autocvar_sv_maxidle_slots) + totalClients = 0; + } + } + else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0) + { + FOREACH_CLIENT(IS_REAL_CLIENT(it), { ++totalClients; }); } - if (autocvar_sv_maxidle_slots > 0 && (maxclients - totalClients) > autocvar_sv_maxidle_slots) - { /* do nothing */ } + if (totalClients < autocvar_sv_maxidle_minplayers) + { + // idle kick disabled + CS(this).parm_idlesince = time; + } else if (time - CS(this).parm_idlesince < 1) // instead of (time == this.parm_idlesince) to support sv_maxidle <= 10 { if (CS(this).idlekick_lasttimeleft) @@ -2715,19 +2676,37 @@ void PlayerPostThink (entity this) } else { - float timeleft = ceil(autocvar_sv_maxidle - (time - CS(this).parm_idlesince)); - if (timeleft == min(10, autocvar_sv_maxidle - 1)) { // - 1 to support sv_maxidle <= 10 - if (!CS(this).idlekick_lasttimeleft) + float maxidle_time = autocvar_sv_maxidle; + if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0) + maxidle_time = autocvar_sv_maxidle_playertospectator; + float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince)); + float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10 + if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft) + { + if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0) + Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft); + else Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft); } if (timeleft <= 0) { - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname); - dropclient(this); + if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0) + { + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time); + if (this.caplayer) + this.caplayer = 0; + this.lms_spectate_warning = 2; // TODO: mutator hook for players forcibly moved to spectator? + PutObserverInServer(this); + } + else + { + if (dropclient_schedule(this)) + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_KICK_IDLING, this.netname, maxidle_time); + } return; } - else if (timeleft <= 10) { + else if (timeleft <= countdown_time) { if (timeleft != CS(this).idlekick_lasttimeleft) - Send_Notification(NOTIF_ONE, this, MSG_ANNCE, Announcer_PickNumber(CNT_IDLE, timeleft)); + play2(this, SND(TALK2)); CS(this).idlekick_lasttimeleft = timeleft; } } @@ -2756,12 +2735,12 @@ void PlayerPostThink (entity this) DrownPlayer(this); UpdateChatBubble(this); if (CS(this).impulse) ImpulseCommands(this); + GetPressedKeys(this); if (game_stopped) { CSQCMODEL_AUTOUPDATE(this); return; } - GetPressedKeys(this); } else if (IS_OBSERVER(this) && STAT(PRESSED_KEYS, this)) {