From 4bb751afaf254cd9fb28015b5275e99b38a577d6 Mon Sep 17 00:00:00 2001 From: Peace-Maker Date: Wed, 2 Mar 2016 23:23:53 +0100 Subject: [PATCH] Add ClientConnect and Disconnect forwards Let plugins know when new spectators join or leave the SourceTV server. --- extension.cpp | 3 +- extension.h | 1 + forwards.cpp | 274 +++++++++++++++++++++++++++++++++++++- forwards.h | 19 +++ sourcetv_test.sp | 49 ++++++- sourcetvmanager.games.txt | 24 ++++ sourcetvmanager.inc | 39 ++++++ 7 files changed, 398 insertions(+), 11 deletions(-) diff --git a/extension.cpp b/extension.cpp index 65ad286..90bdbfa 100644 --- a/extension.cpp +++ b/extension.cpp @@ -31,7 +31,6 @@ #include "extension.h" #include "forwards.h" -#include "inetmessage.h" IHLTVDirector *hltvdirector = nullptr; IHLTVServer *hltvserver = nullptr; @@ -227,6 +226,7 @@ void SourceTVManager::HookSourceTVServer(IHLTVServer *hltv) { if (hltv != nullptr) { + g_pSTVForwards.HookServer(hltv->GetBaseServer()); g_pSTVForwards.HookRecorder(GetDemoRecorderPtr(hltv)); if (iserver) @@ -248,6 +248,7 @@ void SourceTVManager::UnhookSourceTVServer(IHLTVServer *hltv) { if (hltv != nullptr) { + g_pSTVForwards.UnhookServer(hltv->GetBaseServer()); g_pSTVForwards.UnhookRecorder(GetDemoRecorderPtr(hltv)); if (iserver) diff --git a/extension.h b/extension.h index 437cdd1..8acd8c9 100644 --- a/extension.h +++ b/extension.h @@ -47,6 +47,7 @@ #include "iclient.h" #include "ihltvdemorecorder.h" #include "igameevents.h" +#include "inetmessage.h" class INetMessage; diff --git a/forwards.cpp b/forwards.cpp index 2f79d7c..c9e8ec7 100644 --- a/forwards.cpp +++ b/forwards.cpp @@ -41,28 +41,292 @@ SH_DECL_HOOK1_void(IDemoRecorder, StopRecording, SH_NOATTRIB, 0, CGameInfo const SH_DECL_HOOK0_void(IDemoRecorder, StopRecording, SH_NOATTRIB, 0) #endif +#if SOURCE_ENGINE == SE_CSGO +SH_DECL_MANUALHOOK13(CHLTVServer_ConnectClient, 0, 0, 0, IClient *, netadr_s &, int, int, int, const char *, const char *, const char *, int, CUtlVector &, bool, CrossPlayPlatform_t, const unsigned char *, int); +SH_DECL_HOOK1_void(IClient, Disconnect, SH_NOATTRIB, 0, const char *); +#else +SH_DECL_MANUALHOOK9(CHLTVServer_ConnectClient, 0, 0, 0, IClient *, netadr_t &, int, int, int, int, const char *, const char *, const char *, int); +SH_DECL_HOOK0_void_vafmt(IClient, Disconnect, SH_NOATTRIB, 0); +#endif + void CForwardManager::Init() { - m_StartRecordingFwd = forwards->CreateForward("SourceTV_OnStartRecording", ET_Ignore, 3, NULL, Param_Cell, Param_String); + int offset = -1; + if (!g_pGameConf->GetOffset("CHLTVServer::ConnectClient", &offset) || offset == -1) + { + smutils->LogError(myself, "Failed to get CHLTVServer::ConnectClient offset."); + } + else + { + SH_MANUALHOOK_RECONFIGURE(CHLTVServer_ConnectClient, offset, 0, 0); + m_bHasClientConnectOffset = true; + } + m_StartRecordingFwd = forwards->CreateForward("SourceTV_OnStartRecording", ET_Ignore, 2, NULL, Param_Cell, Param_String); m_StopRecordingFwd = forwards->CreateForward("SourceTV_OnStopRecording", ET_Ignore, 3, NULL, Param_Cell, Param_String, Param_Cell); + m_SpectatorPreConnectFwd = forwards->CreateForward("SourceTV_OnSpectatorPreConnect", ET_LowEvent, 4, NULL, Param_String, Param_String, Param_String, Param_String); + m_SpectatorConnectedFwd = forwards->CreateForward("SourceTV_OnSpectatorConnected", ET_Ignore, 1, NULL, Param_Cell); + m_SpectatorDisconnectFwd = forwards->CreateForward("SourceTV_OnSpectatorDisconnect", ET_Ignore, 2, NULL, Param_Cell, Param_String); + m_SpectatorDisconnectedFwd = forwards->CreateForward("SourceTV_OnSpectatorDisconnected", ET_Ignore, 2, NULL, Param_Cell, Param_String); } void CForwardManager::Shutdown() { forwards->ReleaseForward(m_StartRecordingFwd); forwards->ReleaseForward(m_StopRecordingFwd); + forwards->ReleaseForward(m_SpectatorPreConnectFwd); + forwards->ReleaseForward(m_SpectatorConnectedFwd); + forwards->ReleaseForward(m_SpectatorDisconnectFwd); + forwards->ReleaseForward(m_SpectatorDisconnectedFwd); } void CForwardManager::HookRecorder(IDemoRecorder *recorder) { - SH_ADD_HOOK(IDemoRecorder, StartRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStartRecording_Post), false); - SH_ADD_HOOK(IDemoRecorder, StopRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStopRecording_Post), false); + SH_ADD_HOOK(IDemoRecorder, StartRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStartRecording_Post), true); + SH_ADD_HOOK(IDemoRecorder, StopRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStopRecording_Post), true); } void CForwardManager::UnhookRecorder(IDemoRecorder *recorder) { - SH_REMOVE_HOOK(IDemoRecorder, StartRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStartRecording_Post), false); - SH_REMOVE_HOOK(IDemoRecorder, StopRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStopRecording_Post), false); + SH_REMOVE_HOOK(IDemoRecorder, StartRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStartRecording_Post), true); + SH_REMOVE_HOOK(IDemoRecorder, StopRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStopRecording_Post), true); +} + +void CForwardManager::HookServer(IServer *server) +{ + if (!m_bHasClientConnectOffset) + return; + + SH_ADD_MANUALHOOK(CHLTVServer_ConnectClient, server, SH_MEMBER(this, &CForwardManager::OnSpectatorConnect), false); + SH_ADD_MANUALHOOK(CHLTVServer_ConnectClient, server, SH_MEMBER(this, &CForwardManager::OnSpectatorConnect_Post), true); + + // Hook all already connected clients as well for late loading + for (int i = 0; i < server->GetClientCount(); i++) + { + IClient *client = server->GetClient(i); + if (client->IsConnected()) + HookClient(client); + } +} + +void CForwardManager::UnhookServer(IServer *server) +{ + if (!m_bHasClientConnectOffset) + return; + + SH_REMOVE_MANUALHOOK(CHLTVServer_ConnectClient, server, SH_MEMBER(this, &CForwardManager::OnSpectatorConnect), false); + SH_REMOVE_MANUALHOOK(CHLTVServer_ConnectClient, server, SH_MEMBER(this, &CForwardManager::OnSpectatorConnect_Post), true); + + // Unhook all connected clients as well. + for (int i = 0; i < server->GetClientCount(); i++) + { + IClient *client = server->GetClient(i); + if (client->IsConnected()) + UnhookClient(client); + } +} + +void CForwardManager::HookClient(IClient *client) +{ + SH_ADD_HOOK(IClient, Disconnect, client, SH_MEMBER(this, &CForwardManager::OnSpectatorDisconnect), false); +} + +void CForwardManager::UnhookClient(IClient *client) +{ + SH_REMOVE_HOOK(IClient, Disconnect, client, SH_MEMBER(this, &CForwardManager::OnSpectatorDisconnect), false); +} + +#if SOURCE_ENGINE == SE_CSGO +// CBaseServer::RejectConnection(ns_address const&, char const*, ...) +static void RejectConnection(IServer *server, netadr_t &address, char *pchReason) +{ + static ICallWrapper *pRejectConnection = nullptr; + + if (!pRejectConnection) + { + int offset = -1; + if (!g_pGameConf->GetOffset("CHLTVServer::RejectConnection", &offset) || offset == -1) + { + smutils->LogError(myself, "Failed to get CHLTVServer::RejectConnection offset."); + return; + } + + PassInfo pass[4]; + pass[0].flags = PASSFLAG_BYVAL; + pass[0].type = PassType_Basic; + pass[0].size = sizeof(void *); + pass[1].flags = PASSFLAG_BYVAL; + pass[1].type = PassType_Basic; + pass[1].size = sizeof(void *); + pass[2].flags = PASSFLAG_BYVAL; + pass[2].type = PassType_Basic; + pass[2].size = sizeof(char *); + pass[3].flags = PASSFLAG_BYVAL; + pass[3].type = PassType_Basic; + pass[3].size = sizeof(char *); + + void **vtable = *(void ***)server; + void *func = vtable[offset]; + + pRejectConnection = bintools->CreateCall(func, CallConv_Cdecl, NULL, pass, 4); + } + + static char fmt[] = "%s"; + + if (pRejectConnection) + { + unsigned char vstk[sizeof(void *) * 2 + sizeof(char *) * 2]; + unsigned char *vptr = vstk; + + *(void **)vptr = (void *)server; + vptr += sizeof(void *); + *(void **)vptr = (void *)&address; + vptr += sizeof(void *); + *(char **)vptr = fmt; + vptr += sizeof(char *); + *(char **)vptr = pchReason; + + pRejectConnection->Execute(vstk, NULL); + } +} +#else +static void RejectConnection(IServer *server, netadr_t &address, int iClientChallenge, char *pchReason) +{ + static ICallWrapper *pRejectConnection = nullptr; + + if (!pRejectConnection) + { + int offset = -1; + if (!g_pGameConf->GetOffset("CHLTVServer::RejectConnection", &offset) || offset == -1) + { + smutils->LogError(myself, "Failed to get CHLTVServer::RejectConnection offset."); + return; + } + + PassInfo pass[3]; + pass[0].flags = PASSFLAG_BYVAL; + pass[0].type = PassType_Basic; + pass[0].size = sizeof(netadr_t *); + pass[1].flags = PASSFLAG_BYVAL; + pass[1].type = PassType_Basic; + pass[1].size = sizeof(int); + pass[2].flags = PASSFLAG_BYVAL; + pass[2].type = PassType_Basic; + pass[2].size = sizeof(char *); + + pRejectConnection = bintools->CreateVCall(offset, 0, 0, NULL, pass, 3); + } + + if (pRejectConnection) + { + unsigned char vstk[sizeof(void *) + sizeof(netadr_t *) + sizeof(int) + sizeof(char *)]; + unsigned char *vptr = vstk; + + *(void **)vptr = (void *)server; + vptr += sizeof(void *); + *(netadr_t **)vptr = &address; + vptr += sizeof(netadr_t *); + *(int *)vptr = iClientChallenge; + vptr += sizeof(int); + *(char **)vptr = pchReason; + + pRejectConnection->Execute(vstk, NULL); + } +} +#endif + +// Mimic Connect extension https://forums.alliedmods.net/showthread.php?t=162489 +// Thanks asherkin! +char passwordBuffer[255]; +#if SOURCE_ENGINE == SE_CSGO +// CHLTVServer::ConnectClient(ns_address const&, int, int, int, char const*, char const*, char const*, int, CUtlVector *, CUtlMemory *, int>> &, bool, CrossPlayPlatform_t, unsigned char const*, int) +IClient *CForwardManager::OnSpectatorConnect(netadr_s & address, int nProtocol, int iChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie, CUtlVector &pSplitPlayerConnectVector, bool bUnknown, CrossPlayPlatform_t platform, const unsigned char *pUnknown, int iUnknown) +#else +IClient *CForwardManager::OnSpectatorConnect(netadr_t & address, int nProtocol, int iChallenge, int iClientChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie) +#endif +{ + if (!pCookie || cbCookie < sizeof(uint64)) + RETURN_META_VALUE(MRES_IGNORED, nullptr); + + char ipString[16]; + V_snprintf(ipString, sizeof(ipString), "%u.%u.%u.%u", address.ip[0], address.ip[1], address.ip[2], address.ip[3]); + V_strncpy(passwordBuffer, pchPassword, 255); + + // SourceTV doesn't validate steamids?! + + char rejectReason[255]; + + m_SpectatorPreConnectFwd->PushString(pchName); + m_SpectatorPreConnectFwd->PushStringEx(passwordBuffer, 255, SM_PARAM_STRING_UTF8 | SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); + m_SpectatorPreConnectFwd->PushString(ipString); + m_SpectatorPreConnectFwd->PushStringEx(rejectReason, 255, SM_PARAM_STRING_UTF8 | SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); + + cell_t retVal = 1; + m_SpectatorPreConnectFwd->Execute(&retVal); + + if (retVal == 0) + { + IServer *server = META_IFACEPTR(IServer); +#if SOURCE_ENGINE == SE_CSGO + RejectConnection(server, address, rejectReason); +#else + RejectConnection(server, address, iClientChallenge, rejectReason); +#endif + RETURN_META_VALUE(MRES_SUPERCEDE, nullptr); + } + + pchPassword = passwordBuffer; +#if SOURCE_ENGINE == SE_CSGO + RETURN_META_VALUE_MNEWPARAMS(MRES_IGNORED, nullptr, CHLTVServer_ConnectClient, (address, nProtocol, iChallenge, nAuthProtocol, pchName, pchPassword, pCookie, cbCookie, pSplitPlayerConnectVector, bUnknown, platform, pUnknown, iUnknown)); +#else + RETURN_META_VALUE_MNEWPARAMS(MRES_IGNORED, nullptr, CHLTVServer_ConnectClient, (address, nProtocol, iChallenge, iClientChallenge, nAuthProtocol, pchName, pchPassword, pCookie, cbCookie)); +#endif +} + +#if SOURCE_ENGINE == SE_CSGO +IClient *CForwardManager::OnSpectatorConnect_Post(netadr_s & address, int nProtocol, int iChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie, CUtlVector &pSplitPlayerConnectVector, bool bUnknown, CrossPlayPlatform_t platform, const unsigned char * pUnknown, int iUnknown) +#else +IClient *CForwardManager::OnSpectatorConnect_Post(netadr_t & address, int nProtocol, int iChallenge, int iClientChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie) +#endif +{ + IClient *client = META_RESULT_ORIG_RET(IClient *); + if (!client) + RETURN_META_VALUE(MRES_IGNORED, nullptr); + + HookClient(client); + + m_SpectatorConnectedFwd->PushCell(client->GetPlayerSlot()+1); + m_SpectatorConnectedFwd->Execute(); + + RETURN_META_VALUE(MRES_IGNORED, nullptr); +} + +void CForwardManager::OnSpectatorDisconnect(const char *reason) +{ + IClient *client = META_IFACEPTR(IClient); + if (!client) + RETURN_META(MRES_IGNORED); + + UnhookClient(client); + + char disconnectReason[255]; + V_strncpy(disconnectReason, reason, 255); + int clientIndex = client->GetPlayerSlot() + 1; + + m_SpectatorDisconnectFwd->PushCell(clientIndex); + m_SpectatorDisconnectFwd->PushStringEx(disconnectReason, 255, SM_PARAM_STRING_UTF8 | SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); + m_SpectatorDisconnectFwd->Execute(); + +#if SOURCE_ENGINE == SE_CSGO + SH_CALL(client, &IClient::Disconnect)(disconnectReason); +#else + SH_CALL(client, &IClient::Disconnect)("%s", disconnectReason); +#endif + + m_SpectatorDisconnectedFwd->PushCell(clientIndex); + m_SpectatorDisconnectedFwd->PushString(disconnectReason); + m_SpectatorDisconnectedFwd->Execute(); + + RETURN_META(MRES_SUPERCEDE); } void CForwardManager::OnStartRecording_Post(const char *filename, bool bContinuously) diff --git a/forwards.h b/forwards.h index 2214344..6f7c0a6 100644 --- a/forwards.h +++ b/forwards.h @@ -33,6 +33,7 @@ #define _INCLUDE_SOURCEMOD_EXTENSION_FORWARDS_H_ #include "extension.h" +#include "netadr.h" class CGameInfo; @@ -45,17 +46,35 @@ public: void HookRecorder(IDemoRecorder *recorder); void UnhookRecorder(IDemoRecorder *recorder); + void HookServer(IServer *server); + void UnhookServer(IServer *server); + +private: + void HookClient(IClient *client); + void UnhookClient(IClient *client); + private: void OnStartRecording_Post(const char *filename, bool bContinuously); #if SOURCE_ENGINE == SE_CSGO void OnStopRecording_Post(CGameInfo const *info); + IClient *OnSpectatorConnect(netadr_s & address, int nProtocol, int iChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie, CUtlVector &pSplitPlayerConnectVector, bool bUnknown, CrossPlayPlatform_t platform, const unsigned char *pUnknown, int iUnknown); + IClient *OnSpectatorConnect_Post(netadr_s & address, int nProtocol, int iChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie, CUtlVector &pSplitPlayerConnectVector, bool bUnknown, CrossPlayPlatform_t platform, const unsigned char *pUnknown, int iUnknown); #else void OnStopRecording_Post(); + IClient *OnSpectatorConnect(netadr_t &address, int nProtocol, int iChallenge, int iClientChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie); + IClient *OnSpectatorConnect_Post(netadr_t &address, int nProtocol, int iChallenge, int iClientChallenge, int nAuthProtocol, const char *pchName, const char *pchPassword, const char *pCookie, int cbCookie); #endif + void OnSpectatorDisconnect(const char *reason); private: IForward *m_StartRecordingFwd; IForward *m_StopRecordingFwd; + IForward *m_SpectatorPreConnectFwd; + IForward *m_SpectatorConnectedFwd; + IForward *m_SpectatorDisconnectFwd; + IForward *m_SpectatorDisconnectedFwd; + + bool m_bHasClientConnectOffset = false; }; extern CForwardManager g_pSTVForwards; diff --git a/sourcetv_test.sp b/sourcetv_test.sp index 09883f9..ba98295 100644 --- a/sourcetv_test.sp +++ b/sourcetv_test.sp @@ -7,16 +7,17 @@ public OnPluginStart() RegConsoleCmd("sm_servercount", Cmd_GetServerCount); RegConsoleCmd("sm_selectserver", Cmd_SelectServer); - RegConsoleCmd("sm_getselectedserver", Cmd_GetSelectedServer); - RegConsoleCmd("sm_getbotindex", Cmd_GetBotIndex); - RegConsoleCmd("sm_getbroadcasttick", Cmd_GetBroadcastTick); + RegConsoleCmd("sm_selectedserver", Cmd_GetSelectedServer); + RegConsoleCmd("sm_botindex", Cmd_GetBotIndex); + RegConsoleCmd("sm_broadcasttick", Cmd_GetBroadcastTick); RegConsoleCmd("sm_localstats", Cmd_Localstats); + RegConsoleCmd("sm_globalstats", Cmd_Globalstats); RegConsoleCmd("sm_getdelay", Cmd_GetDelay); RegConsoleCmd("sm_spectators", Cmd_Spectators); RegConsoleCmd("sm_spechintmsg", Cmd_SendHintMessage); RegConsoleCmd("sm_specmsg", Cmd_SendMessage); - RegConsoleCmd("sm_getviewentity", Cmd_GetViewEntity); - RegConsoleCmd("sm_getvieworigin", Cmd_GetViewOrigin); + RegConsoleCmd("sm_viewentity", Cmd_GetViewEntity); + RegConsoleCmd("sm_vieworigin", Cmd_GetViewOrigin); RegConsoleCmd("sm_forcechasecam", Cmd_ForceChaseCameraShot); //RegConsoleCmd("sm_forcefixedcam", Cmd_ForceFixedCameraShot); RegConsoleCmd("sm_startrecording", Cmd_StartRecording); @@ -40,6 +41,32 @@ public SourceTV_OnStopRecording(instance, const String:filename[], recordingtick PrintToServer("Stopped recording sourcetv #%d demo to %s (%d ticks)", instance, filename, recordingtick); } +public bool:SourceTV_OnSpectatorPreConnect(const String:name[], String:password[255], const String:ip[], String:rejectReason[255]) +{ + PrintToServer("SourceTV spectator is connecting! Name: %s, pw: %s, ip: %s", name, password, ip); + if (StrEqual(password, "nope", false)) + { + strcopy(rejectReason, 255, "Heh, that password sucks."); + return false; + } + return true; +} + +public SourceTV_OnSpectatorConnected(client) +{ + PrintToServer("SourceTV client %d connected. (isconnected %d)", client, SourceTV_IsClientConnected(client)); +} + +public SourceTV_OnSpectatorDisconnect(client, String:reason[255]) +{ + PrintToServer("SourceTV client %d is disconnecting (isconnected %d) with reason -> %s.", client, SourceTV_IsClientConnected(client), reason); +} + +public SourceTV_OnSpectatorDisconnected(client, const String:reason[255]) +{ + PrintToServer("SourceTV client %d disconnected (isconnected %d) with reason -> %s.", client, SourceTV_IsClientConnected(client), reason); +} + public Action:Cmd_GetServerCount(client, args) { ReplyToCommand(client, "SourceTV server count: %d", SourceTV_GetServerInstanceCount()); @@ -93,6 +120,18 @@ public Action:Cmd_Localstats(client, args) return Plugin_Handled; } +public Action:Cmd_Globalstats(client, args) +{ + new proxies, slots, specs; + if (!SourceTV_GetGlobalStats(proxies, slots, specs)) + { + ReplyToCommand(client, "SourceTV global stats: no server selected :("); + return Plugin_Handled; + } + ReplyToCommand(client, "SourceTV global stats: proxies %d - slots %d - specs %d", proxies, slots, specs); + return Plugin_Handled; +} + public Action:Cmd_GetDelay(client, args) { ReplyToCommand(client, "SourceTV delay: %f", SourceTV_GetDelay()); diff --git a/sourcetvmanager.games.txt b/sourcetvmanager.games.txt index 785ce96..66d4cf4 100644 --- a/sourcetvmanager.games.txt +++ b/sourcetvmanager.games.txt @@ -25,6 +25,18 @@ "linux" "40" } + "CHLTVServer::ConnectClient" + { + "windows" "54" + "linux" "55" + } + + "CHLTVServer::RejectConnection" + { + "windows" "52" + "linux" "53" + } + "CHLTVServer::m_DemoRecorder" { "windows" "19600" @@ -97,6 +109,18 @@ "linux" "36" } + "CHLTVServer::ConnectClient" + { + "windows" "49" + "linux" "50" + } + + "CHLTVServer::RejectConnection" + { + "windows" "47" + "linux" "48" + } + "CHLTVServer::m_DemoRecorder" { "windows" "19192" diff --git a/sourcetvmanager.inc b/sourcetvmanager.inc index adf4298..1919a42 100644 --- a/sourcetvmanager.inc +++ b/sourcetvmanager.inc @@ -298,6 +298,45 @@ native SourceTV_GetSpectatorName(client, String:name[], maxlen); native SourceTV_KickClient(client, const String:sReason[]); +/** + * Called when a spectator wants to connect to the SourceTV server. + * This is called before any other validation has happened. + * Similar to the OnClientPreConnectEx forward in the Connect extension by asherkin. + * + * @param name The player name (always empty in CS:GO). + * @param password The password the client used to connect. Can be overwritten. + * @param ip The ip address of the client. + * @param rejectReason Buffer to write the reject reason to, if you want to reject the client from connecting. + * @return True to allow the client to connect, false to reject him with the given reason. + */ +forward bool:SourceTV_OnSpectatorPreConnect(const String:name[], String:password[255], const String:ip[], String:rejectReason[255]); + +/** + * Called when a spectator client connected to the SourceTV server. + * + * @param client The spectator client index. + * @noreturn + */ +forward SourceTV_OnSpectatorConnected(client); + +/** + * Called when a spectator client is about to disconnect. + * + * @param client The spectator client index. + * @param reason The reason for the disconnect. Can be overwritten. + * @noreturn + */ +forward SourceTV_OnSpectatorDisconnect(client, String:reason[255]); + +/** + * Called after a spectator client disconnected. + * + * @param client The spectator client index. + * @param reason The reason for the disconnect. + * @noreturn + */ +forward SourceTV_OnSpectatorDisconnected(client, const String:reason[255]); + /** * Do not edit below this line! */