From cc6059a4b72f588f275e20d5a6ecf7913fd7dd00 Mon Sep 17 00:00:00 2001 From: BotoX Date: Fri, 28 Feb 2020 01:21:31 +0100 Subject: [PATCH] engine: Implement message buffering. (#1071) * Avoid losing console messages. Buffers up to 16k bytes of SVC_Print if buffer would overflow, then sends chunks every frame. Sends up to 2048 bytes per frame and does not split messages. * UNTESTED! Switch to ke::LinkedList for PrintfBuffer. Switch from OnGameFrame to FramAction. Fix compiling on Episode1 by essentially disabling the feature. * UNTESTED! Cleanup on disconnect, passthrough for >= 2048 msgs * try reference for CPlayer. * fix * remove m_PrintfStop * remove m_PrintfStop * ensure empty queue when netchan drops * flip to serials. * serials * style * Update PlayerManager.cpp * lift consts to header. * remove local const references * ep1 static const * flip to queue - fix serial on resched. * Update PlayerManager.h * Update PlayerManager.cpp * Update PlayerManager.h * am-deque.h Co-authored-by: Kyle Sanderson --- core/PlayerManager.cpp | 98 ++++++++++++++++++++++++++++++++++++++++++ core/PlayerManager.h | 8 ++++ 2 files changed, 106 insertions(+) diff --git a/core/PlayerManager.cpp b/core/PlayerManager.cpp index b936098b..381e42b7 100644 --- a/core/PlayerManager.cpp +++ b/core/PlayerManager.cpp @@ -30,6 +30,7 @@ */ #include "PlayerManager.h" +#include "sourcemod.h" #include "IAdminSystem.h" #include "ConCmdManager.h" #include "MenuStyle_Valve.h" @@ -92,6 +93,12 @@ SH_DECL_EXTERN1_void(ConCommand, Dispatch, SH_NOATTRIB, false, const CCommand &) #else SH_DECL_EXTERN0_void(ConCommand, Dispatch, SH_NOATTRIB, false); #endif +SH_DECL_HOOK2_void(IVEngineServer, ClientPrintf, SH_NOATTRIB, 0, edict_t *, const char *); + +static void PrintfBuffer_FrameAction(void *data) +{ + g_Players.OnPrintfFrameAction(reinterpret_cast(data)); +} ConCommand *maxplayersCmd = NULL; @@ -172,6 +179,7 @@ void PlayerManager::OnSourceModAllInitialized() #elif SOURCE_ENGINE > SE_EYE // 2013/orangebox, but not original orangebox. SH_ADD_HOOK(IServerGameDLL, SetServerHibernation, gamedll, SH_MEMBER(this, &PlayerManager::OnServerHibernationUpdate), true); #endif + SH_ADD_HOOK(IVEngineServer, ClientPrintf, engine, SH_MEMBER(this, &PlayerManager::OnClientPrintf), false); sharesys->AddInterface(NULL, this); @@ -225,6 +233,7 @@ void PlayerManager::OnSourceModShutdown() #elif SOURCE_ENGINE > SE_EYE // 2013/orangebox, but not original orangebox. SH_REMOVE_HOOK(IServerGameDLL, SetServerHibernation, gamedll, SH_MEMBER(this, &PlayerManager::OnServerHibernationUpdate), true); #endif + SH_REMOVE_HOOK(IVEngineServer, ClientPrintf, engine, SH_MEMBER(this, &PlayerManager::OnClientPrintf), false); /* Release forwards */ forwardsys->ReleaseForward(m_clconnect); @@ -846,6 +855,88 @@ void PlayerManager::OnClientDisconnect_Post(edict_t *pEntity) } } +void PlayerManager::OnClientPrintf(edict_t *pEdict, const char *szMsg) +{ + int client = IndexOfEdict(pEdict); + + CPlayer &player = m_Players[client]; + if (!player.IsConnected()) + RETURN_META(MRES_IGNORED); + + INetChannel *pNetChan = static_cast(engine->GetPlayerNetInfo(client)); + if (pNetChan == NULL) + RETURN_META(MRES_IGNORED); + + size_t nMsgLen = strlen(szMsg); +#if SOURCE_ENGINE == SE_EPISODEONE + static const int nNumBitsWritten = 0; +#else + int nNumBitsWritten = pNetChan->GetNumBitsWritten(false); // SVC_Print uses unreliable netchan +#endif + + // if the msg is bigger than allowed then just let it fail + if (nMsgLen + 1 >= SVC_Print_BufferSize) // +1 for NETMSG_TYPE_BITS + RETURN_META(MRES_IGNORED); + + // enqueue msgs if we'd overflow the SVC_Print buffer (+7 as ceil) + if (!player.m_PrintfBuffer.empty() || (nNumBitsWritten + NETMSG_TYPE_BITS + 7) / 8 + nMsgLen >= SVC_Print_BufferSize) + { + // Don't send any more messages for this player until the buffer is empty. + // Queue up a gameframe hook to empty the buffer (if we haven't already) + if (player.m_PrintfBuffer.empty()) + g_SourceMod.AddFrameAction(PrintfBuffer_FrameAction, (void *)(uintptr_t)player.GetSerial()); + + player.m_PrintfBuffer.append(szMsg); + + RETURN_META(MRES_SUPERCEDE); + } + + RETURN_META(MRES_IGNORED); +} + +void PlayerManager::OnPrintfFrameAction(unsigned int serial) +{ + int client = GetClientFromSerial(serial); + CPlayer &player = m_Players[client]; + if (!player.IsConnected()) + { + player.ClearNetchannelQueue(); + return; + } + + INetChannel *pNetChan = static_cast(engine->GetPlayerNetInfo(client)); + if (pNetChan == NULL) + { + player.ClearNetchannelQueue(); + return; + } + + while (!player.m_PrintfBuffer.empty()) + { +#if SOURCE_ENGINE == SE_EPISODEONE + static const int nNumBitsWritten = 0; +#else + int nNumBitsWritten = pNetChan->GetNumBitsWritten(false); // SVC_Print uses unreliable netchan +#endif + + ke::AString &string = player.m_PrintfBuffer.front(); + + // stop if we'd overflow the SVC_Print buffer (+7 as ceil) + if ((nNumBitsWritten + NETMSG_TYPE_BITS + 7) / 8 + string.length() >= SVC_Print_BufferSize) + break; + + SH_CALL(engine, &IVEngineServer::ClientPrintf)(player.m_pEdict, string.chars()); + + player.m_PrintfBuffer.popFront(); + } + + if (!player.m_PrintfBuffer.empty()) + { + // continue processing it on the next gameframe as buffer is not empty + g_SourceMod.AddFrameAction(PrintfBuffer_FrameAction, (void *)(uintptr_t)player.GetSerial()); + } +} + void ClientConsolePrint(edict_t *e, const char *fmt, ...) { char buffer[512]; @@ -2148,6 +2239,13 @@ void CPlayer::Disconnect() #if SOURCE_ENGINE == SE_CSGO m_LanguageCookie = InvalidQueryCvarCookie; #endif + ClearNetchannelQueue(); +} + +void CPlayer::ClearNetchannelQueue(void) +{ + while (!m_PrintfBuffer.empty()) + m_PrintfBuffer.popFront(); } void CPlayer::SetName(const char *name) diff --git a/core/PlayerManager.h b/core/PlayerManager.h index e66995aa..34462a4f 100644 --- a/core/PlayerManager.h +++ b/core/PlayerManager.h @@ -43,6 +43,7 @@ #include #include #include +#include #include "ConVarManager.h" #include @@ -123,6 +124,7 @@ private: bool IsAuthStringValidated(); bool SetEngineString(); bool SetCSteamID(); + void ClearNetchannelQueue(void); private: bool m_IsConnected = false; bool m_IsInGame = false; @@ -152,6 +154,7 @@ private: #if SOURCE_ENGINE == SE_CSGO QueryCvarCookie_t m_LanguageCookie = InvalidQueryCvarCookie; #endif + ke::Deque m_PrintfBuffer; }; class PlayerManager : @@ -190,6 +193,8 @@ public: void OnClientSettingsChanged(edict_t *pEntity); //void OnClientSettingsChanged_Pre(edict_t *pEntity); void OnServerHibernationUpdate(bool bHibernating); + void OnClientPrintf(edict_t *pEdict, const char *szMsg); + void OnPrintfFrameAction(unsigned int serial); public: //IPlayerManager void AddClientListener(IClientListener *listener); void RemoveClientListener(IClientListener *listener); @@ -267,6 +272,9 @@ private: int m_SourceTVUserId; int m_ReplayUserId; bool m_bInCCKVHook; +private: + static const int NETMSG_TYPE_BITS = 5; // SVC_Print overhead for netmsg type + static const int SVC_Print_BufferSize = 2048 - 1; // -1 for terminating \0 }; #if SOURCE_ENGINE >= SE_ORANGEBOX