diff --git a/core/HalfLife2.cpp b/core/HalfLife2.cpp index f7fd7c17..93f84133 100644 --- a/core/HalfLife2.cpp +++ b/core/HalfLife2.cpp @@ -511,3 +511,31 @@ const char *CHalfLife2::CurrentCommandName() return m_CommandStack.front().cmd; #endif } + +void CHalfLife2::AddDelayedKick(int client, int userid, const char *msg) +{ + DelayedKickInfo kick; + + kick.client = client; + kick.userid = userid; + UTIL_Format(kick.buffer, sizeof(kick.buffer), "%s", msg); + + m_DelayedKicks.push(kick); +} + +void CHalfLife2::ProcessDelayedKicks() +{ + while (!m_DelayedKicks.empty()) + { + DelayedKickInfo info = m_DelayedKicks.first(); + m_DelayedKicks.pop(); + + CPlayer *player = g_Players.GetPlayerByIndex(info.client); + if (player == NULL || player->GetUserId() != info.userid) + { + continue; + } + + player->Kick(info.buffer); + } +} diff --git a/core/HalfLife2.h b/core/HalfLife2.h index 799fe131..48d0c2fb 100644 --- a/core/HalfLife2.h +++ b/core/HalfLife2.h @@ -79,6 +79,13 @@ struct CachedCommandInfo #endif }; +struct DelayedKickInfo +{ + int userid; + int client; + char buffer[384]; +}; + class CHalfLife2 : public SMGlobalClass, public IGameHelpers @@ -110,6 +117,8 @@ public: void PopCommandStack(); const CCommand *PeekCommandStack(); const char *CurrentCommandName(); + void AddDelayedKick(int client, int userid, const char *msg); + void ProcessDelayedKicks(); #if !defined METAMOD_PLAPI_VERSION bool IsOriginalEngine(); #endif @@ -125,6 +134,7 @@ private: Queue m_CmdQueue; CStack m_FreeCmds; CStack m_CommandStack; + Queue m_DelayedKicks; }; extern CHalfLife2 g_HL2; diff --git a/core/frame_hooks.cpp b/core/frame_hooks.cpp index b84a3e53..4573af5a 100644 --- a/core/frame_hooks.cpp +++ b/core/frame_hooks.cpp @@ -44,6 +44,7 @@ void RunFrameHooks(bool simulating) /* Frame based hooks */ g_DBMan.RunFrame(); g_HL2.ProcessFakeCliCmdQueue(); + g_HL2.ProcessDelayedKicks(); g_SourceMod.ProcessGameFrameHooks(simulating); float curtime = *g_pUniversalTime; diff --git a/core/smn_player.cpp b/core/smn_player.cpp index 3c7dfb09..ed185f09 100644 --- a/core/smn_player.cpp +++ b/core/smn_player.cpp @@ -1258,10 +1258,64 @@ static cell_t KickClient(IPluginContext *pContext, const cell_t *params) if (!pPlayer) { return pContext->ThrowNativeError("Client index %d is invalid", client); - } else if (!pPlayer->IsConnected()) { + } + else if (!pPlayer->IsConnected()) + { return pContext->ThrowNativeError("Client %d is not connected", client); } + /* Ignore duplicate kicks */ + if (pPlayer->IsInKickQueue()) + { + return 1; + } + + pPlayer->MarkAsBeingKicked(); + + if (pPlayer->IsFakeClient()) + { + char kickcmd[40]; + UTIL_Format(kickcmd, sizeof(kickcmd), "kick %s\n", pPlayer->GetName()); + + engine->ServerCommand(kickcmd); + return 1; + } + + g_SourceMod.SetGlobalTarget(client); + + char buffer[256]; + g_SourceMod.FormatString(buffer, sizeof(buffer), pContext, params, 2); + + if (pContext->GetContext()->n_err != SP_ERROR_NONE) + { + return 0; + } + + g_HL2.AddDelayedKick(client, pPlayer->GetUserId(), buffer); + + return 1; +} + +static cell_t KickClientEx(IPluginContext *pContext, const cell_t *params) +{ + int client = params[1]; + + CPlayer *pPlayer = g_Players.GetPlayerByIndex(client); + if (!pPlayer) + { + return pContext->ThrowNativeError("Client index %d is invalid", client); + } + else if (!pPlayer->IsConnected()) + { + return pContext->ThrowNativeError("Client %d is not connected", client); + } + + /* Ignore duplicate kicks */ + if (pPlayer->IsInKickQueue()) + { + return 1; + } + pPlayer->MarkAsBeingKicked(); if (pPlayer->IsFakeClient()) @@ -1460,6 +1514,7 @@ REGISTER_NATIVES(playernatives) {"ShowActivityEx", ShowActivityEx}, {"ShowActivity2", ShowActivity2}, {"KickClient", KickClient}, + {"KickClientEx", KickClientEx}, {"RunAdminCacheChecks", RunAdminCacheChecks}, {"NotifyPostAdminCheck", NotifyPostAdminCheck}, {"IsClientInKickQueue", IsClientInKickQueue}, diff --git a/plugins/include/clients.inc b/plugins/include/clients.inc index e17807dc..01ba4b7b 100644 --- a/plugins/include/clients.inc +++ b/plugins/include/clients.inc @@ -634,7 +634,28 @@ native Float:GetClientAvgPackets(client, NetFlow:flow); native GetClientOfUserId(userid); /** - * Disconnects a client from the server. + * Disconnects a client from the server as soon as the next frame starts. + * + * Note: Originally, KickClient() was immediate. The delay was introduced + * because despite warnings, plugins were using it in ways that would crash. + * The new safe version can break cases that rely on immediate disconnects, + * but ensures that plugins do not accidentally cause crashes. + * + * If you need immediate disconnects, use KickClientEx(). + * + * Note: IsClientInKickQueue() will return true before the kick occurs. + * + * @param client Client index. + * @param format Optional formatting rules for disconnect reason. + * Note that a period is automatically appended to the string by the engine. + * @param ... Variable number of format parameters. + * @noreturn + * @error Invalid client index, or client not connected. + */ +native KickClient(client, const String:format[]="", any:...); + +/** + * Immediately disconnects a client from the server. * * Kicking clients from certain events or callbacks may cause crashes. If in * doubt, create a short (0.1 second) timer to kick the client in the next @@ -647,7 +668,7 @@ native GetClientOfUserId(userid); * @noreturn * @error Invalid client index, or client not connected. */ -native KickClient(client, const String:format[]="", any:...); +native KickClientEx(client, const String:format[]="", any:...); /** * Changes a client's team through the mod's generic team changing function.