diff --git a/extension.cpp b/extension.cpp index f8b1ba3..b3f4199 100644 --- a/extension.cpp +++ b/extension.cpp @@ -30,15 +30,13 @@ */ #include "extension.h" +#include "hltvserverwrapper.h" #include "forwards.h" #include "natives.h" IHLTVDirector *hltvdirector = nullptr; -IHLTVServer *hltvserver = nullptr; -IDemoRecorder *demorecorder = nullptr; void *host_client = nullptr; -void *old_host_client = nullptr; -bool g_HostClientOverridden = false; +HLTVServerWrapper *hltvserver = nullptr; IGameEventManager2 *gameevents = nullptr; CGlobalVars *gpGlobals = nullptr; @@ -58,19 +56,8 @@ SH_DECL_HOOK1_void(IHLTVDirector, AddHLTVServer, SH_NOATTRIB, 0, IHLTVServer *); SH_DECL_HOOK1_void(IHLTVDirector, RemoveHLTVServer, SH_NOATTRIB, 0, IHLTVServer *); #else SH_DECL_HOOK1_void(IHLTVDirector, SetHLTVServer, SH_NOATTRIB, 0, IHLTVServer *); - -// Stuff to print to demo console -SH_DECL_HOOK0_void_vafmt(IClient, ClientPrintf, SH_NOATTRIB, 0); -// This should be large enough. -#define FAKE_VTBL_LENGTH 70 -static void *FakeNetChanVtbl[FAKE_VTBL_LENGTH]; -static void *FakeNetChan = &FakeNetChanVtbl; - -SH_DECL_MANUALHOOK3(NetChan_SendNetMsg, 0, 0, 0, bool, INetMessage &, bool, bool); #endif -SH_DECL_HOOK1(IClient, ExecuteStringCommand, SH_NOATTRIB, 0, bool, const char *); - /** * @file extension.cpp * @brief Implement extension code here. @@ -106,33 +93,7 @@ bool SourceTVManager::SDK_OnLoad(char *error, size_t maxlength, bool late) smutils->LogError(myself, "Failed to find host_client pointer. Server might crash when executing commands on SourceTV bot."); } -#if SOURCE_ENGINE != SE_CSGO - int offset; - if (g_pGameConf->GetOffset("CNetChan::SendNetMsg", &offset)) - { - if (offset >= FAKE_VTBL_LENGTH) - { - smutils->LogError(myself, "CNetChan::SendNetMsg offset too big. Need to raise define and recompile. Contact the author."); - } - else - { - // This is a hack. Bots don't have a net channel, but ClientPrintf tries to call m_NetChannel->SendNetMsg directly. - // CGameClient::SendNetMsg would have redirected it to the hltvserver correctly, but isn't used there.. - // We craft a fake object with a large enough "vtable" and hook it using sourcehook. - // Before a call to ClientPrintf, this fake object is set as CBaseClient::m_NetChannel, so ClientPrintf creates - // the SVC_Print INetMessage and calls our "hooked" m_NetChannel->SendNetMsg function. - // In that function we just call CGameClient::SendNetMsg with the given INetMessage to flow it through the same - // path as other net messages. - SH_MANUALHOOK_RECONFIGURE(NetChan_SendNetMsg, offset, 0, 0); - SH_ADD_MANUALHOOK(NetChan_SendNetMsg, &FakeNetChan, SH_MEMBER(this, &SourceTVManager::OnHLTVBotNetChanSendNetMsg), false); - g_SendNetMsgHooked = true; - } - } - else - { - smutils->LogError(myself, "Failed to find CNetChan::SendNetMsg offset. Can't print to demo console."); - } -#endif + g_HLTVServers.InitHooks(); sharesys->AddNatives(myself, sourcetv_natives); sharesys->RegisterLibrary(myself, "sourcetvmanager"); @@ -160,19 +121,19 @@ void SourceTVManager::SDK_OnAllLoaded() smutils->LogError(myself, "Failed to get IServer interface from SDKTools. Some functions won't work."); #if SOURCE_ENGINE == SE_CSGO - if (hltvdirector->GetHLTVServerCount() > 0) - SelectSourceTVServer(hltvdirector->GetHLTVServer(0)); - // Hook all the exisiting servers. for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++) { - HookSourceTVServer(hltvdirector->GetHLTVServer(i)); + g_HLTVServers.AddServer(hltvdirector->GetHLTVServer(i)); } + + if (hltvdirector->GetHLTVServerCount() > 0) + SelectSourceTVServer(hltvdirector->GetHLTVServer(0)); #else if (hltvdirector->GetHLTVServer()) { + g_HLTVServers.AddServer(hltvdirector->GetHLTVServer()); SelectSourceTVServer(hltvdirector->GetHLTVServer()); - HookSourceTVServer(hltvdirector->GetHLTVServer()); } #endif } @@ -204,27 +165,23 @@ void SourceTVManager::SDK_OnUnload() SH_REMOVE_HOOK(IHLTVDirector, RemoveHLTVServer, hltvdirector, SH_MEMBER(this, &SourceTVManager::OnRemoveHLTVServer_Post), true); #else SH_REMOVE_HOOK(IHLTVDirector, SetHLTVServer, hltvdirector, SH_MEMBER(this, &SourceTVManager::OnSetHLTVServer_Post), true); - - if (g_SendNetMsgHooked) - { - SH_REMOVE_MANUALHOOK(NetChan_SendNetMsg, &FakeNetChan, SH_MEMBER(this, &SourceTVManager::OnHLTVBotNetChanSendNetMsg), false); - g_SendNetMsgHooked = false; - } - #endif + g_HLTVServers.ShutdownHooks(); + gameconfs->CloseGameConfigFile(g_pGameConf); #if SOURCE_ENGINE == SE_CSGO // Unhook all the existing servers. for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++) { - UnhookSourceTVServer(hltvdirector->GetHLTVServer(i)); + // We don't know if the extension is just being unloaded or the server is shutting down. + // So don't inform the plugins of this removal. + g_HLTVServers.RemoveServer(hltvdirector->GetHLTVServer(i), false); } #else // Unhook the server - if (hltvdirector->GetHLTVServer()) - UnhookSourceTVServer(hltvdirector->GetHLTVServer()); + g_HLTVServers.RemoveServer(hltvdirector->GetHLTVServer(), false); #endif g_pSTVForwards.Shutdown(); } @@ -237,113 +194,43 @@ bool SourceTVManager::QueryRunning(char *error, size_t maxlength) return true; } -void SourceTVManager::HookSourceTVServer(IHLTVServer *hltv) -{ - if (hltv != nullptr) - { - g_pSTVForwards.HookServer(hltv->GetBaseServer()); - g_pSTVForwards.HookRecorder(GetDemoRecorderPtr(hltv)); - - if (iserver) - { - IClient *pClient = iserver->GetClient(hltv->GetHLTVSlot()); - if (pClient) - { - SH_ADD_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand), false); - SH_ADD_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand_Post), true); -#if SOURCE_ENGINE != SE_CSGO - SH_ADD_HOOK(IClient, ClientPrintf, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotClientPrintf_Post), false); -#endif - } - } - } -} - -void SourceTVManager::UnhookSourceTVServer(IHLTVServer *hltv) -{ - if (hltv != nullptr) - { - g_pSTVForwards.UnhookServer(hltv->GetBaseServer()); - g_pSTVForwards.UnhookRecorder(GetDemoRecorderPtr(hltv)); - - if (iserver) - { - IClient *pClient = iserver->GetClient(hltv->GetHLTVSlot()); - if (pClient) - { - SH_REMOVE_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand), false); - SH_REMOVE_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand_Post), true); -#if SOURCE_ENGINE != SE_CSGO - SH_REMOVE_HOOK(IClient, ClientPrintf, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotClientPrintf_Post), false); -#endif - } - } - } -} - void SourceTVManager::SelectSourceTVServer(IHLTVServer *hltv) { // Select the new server. - hltvserver = hltv; - demorecorder = GetDemoRecorderPtr(hltvserver); -} - -IDemoRecorder *SourceTVManager::GetDemoRecorderPtr(IHLTVServer *hltv) -{ - static int offset = -1; - if (offset == -1) - { - void *addr; - if (!g_pGameConf->GetAddress("CHLTVServer::m_DemoRecorder", &addr)) - { - smutils->LogError(myself, "Failed to get CHLTVServer::m_DemoRecorder offset."); - return nullptr; - } - - *(int **)&offset = (int *)addr; - } - - if (hltv) - { -#if SOURCE_ENGINE == SE_CSGO - return (IDemoRecorder *)((intptr_t)hltv + offset); -#else - IServer *baseServer = hltv->GetBaseServer(); - return (IDemoRecorder *)((intptr_t)baseServer + offset); -#endif - } - else - { - return nullptr; - } + hltvserver = g_HLTVServers.GetWrapper(hltv); } #if SOURCE_ENGINE == SE_CSGO void SourceTVManager::OnAddHLTVServer_Post(IHLTVServer *hltv) { - HookSourceTVServer(hltv); + g_HLTVServers.AddServer(hltv); // We already selected some SourceTV server. Keep it. if (hltvserver != nullptr) RETURN_META(MRES_IGNORED); - // This is the first SourceTV server to be added. Hook it. + // This is the first SourceTV server to be added. SelectSourceTVServer(hltv); RETURN_META(MRES_IGNORED); } void SourceTVManager::OnRemoveHLTVServer_Post(IHLTVServer *hltv) { - UnhookSourceTVServer(hltv); + HLTVServerWrapper *wrapper = g_HLTVServers.GetWrapper(hltv); + if (!wrapper) + RETURN_META(MRES_IGNORED); + + // With the CHLTVServer::Shutdown hook, this isn't needed? + // Doesn't hurt either.. + g_HLTVServers.RemoveServer(hltv, true); // We got this SourceTV server selected. Now it's gone :( - if (hltvserver == hltv) + if (hltvserver == wrapper) { // Is there another one available? Try to keep us operable. if (hltvdirector->GetHLTVServerCount() > 0) { SelectSourceTVServer(hltvdirector->GetHLTVServer(0)); - HookSourceTVServer(hltvserver); } // No sourcetv active. else @@ -354,45 +241,6 @@ void SourceTVManager::OnRemoveHLTVServer_Post(IHLTVServer *hltv) RETURN_META(MRES_IGNORED); } #else -void SourceTVManager::OnHLTVBotClientPrintf_Post(const char* buf) -{ - // Craft our own "NetChan" pointer - static int offset = -1; - if (!g_pGameConf->GetOffset("CBaseClient::m_NetChannel", &offset) || offset == -1) - { - smutils->LogError(myself, "Failed to find CBaseClient::m_NetChannel offset. Can't print to demo console."); - RETURN_META(MRES_IGNORED); - } - - IClient *pClient = META_IFACEPTR(IClient); - - void *pNetChannel = (void *)((char *)pClient + offset); - // Set our fake netchannel - *(void **)pNetChannel = &FakeNetChan; - // Call ClientPrintf again, this time with a "Netchannel" set on the bot. - // This will call our own OnHLTVBotNetChanSendNetMsg function - SH_CALL(pClient, &IClient::ClientPrintf)("%s", buf); - // Set the fake netchannel back to 0. - *(void **)pNetChannel = nullptr; - - RETURN_META(MRES_IGNORED); -} - -bool SourceTVManager::OnHLTVBotNetChanSendNetMsg(INetMessage &msg, bool bForceReliable, bool bVoice) -{ - IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot()); - if (!pClient) - RETURN_META_VALUE(MRES_SUPERCEDE, false); - - // Let the message flow through the intended path like CGameClient::SendNetMsg wants to. - bool bRetSent = pClient->SendNetMsg(msg, bForceReliable); - - // It's important to supercede, because there is no original function to call. - // (the "vtable" was empty before hooking it) - // See FakeNetChan variable at the top. - RETURN_META_VALUE(MRES_SUPERCEDE, bRetSent); -} - void SourceTVManager::OnSetHLTVServer_Post(IHLTVServer *hltv) { // Server shut down? @@ -402,53 +250,15 @@ void SourceTVManager::OnSetHLTVServer_Post(IHLTVServer *hltv) if (!hltvserver) RETURN_META(MRES_IGNORED); - UnhookSourceTVServer(hltvserver); + // With the CHLTVServer::Shutdown hook, this isn't needed? + // Doesn't hurt either.. + g_HLTVServers.RemoveServer(hltvserver->GetHLTVServer(), true); } else { - HookSourceTVServer(hltv); + g_HLTVServers.AddServer(hltv); } SelectSourceTVServer(hltv); RETURN_META(MRES_IGNORED); } #endif - - -// When bots issue a command that would print stuff to their console, -// the server might crash, because ExecuteStringCommand doesn't set the -// global host_client pointer to the client on whom the command is run. -// Host_Client_Printf blatantly tries to call host_client->ClientPrintf -// while the pointer might point to some other player or garbage. -// This leads to e.g. the output of the "status" command not being -// recorded in the SourceTV demo. -// The approach here is to set host_client correctly for the SourceTV -// bot and reset it to the old value after command execution. -bool SourceTVManager::OnHLTVBotExecuteStringCommand(const char *s) -{ - if (!hltvserver || !iserver || !host_client) - RETURN_META_VALUE(MRES_IGNORED, 0); - - IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot()); - if (!pClient) - RETURN_META_VALUE(MRES_IGNORED, 0); - - // The IClient vtable is +4 from the CBaseClient vtable due to multiple inheritance. - void *pGameClient = (void *)((intptr_t)pClient - 4); - - old_host_client = *(void **)host_client; - *(void **)host_client = pGameClient; - g_HostClientOverridden = true; - - RETURN_META_VALUE(MRES_IGNORED, 0); -} - -bool SourceTVManager::OnHLTVBotExecuteStringCommand_Post(const char *s) -{ - if (!host_client || !g_HostClientOverridden) - RETURN_META_VALUE(MRES_IGNORED, 0); - - *(void **)host_client = old_host_client; - g_HostClientOverridden = false; - RETURN_META_VALUE(MRES_IGNORED, 0); -} - diff --git a/extension.h b/extension.h index 15214da..a650c65 100644 --- a/extension.h +++ b/extension.h @@ -52,6 +52,7 @@ #include "hltvclientwrapper.h" class INetMessage; +class HLTVServerWrapper; extern ConVar tv_force_steamauth; @@ -135,7 +136,6 @@ public: // IConCommandBaseAccessor public: void SelectSourceTVServer(IHLTVServer *hltv); - IDemoRecorder *GetDemoRecorderPtr(IHLTVServer *hltvserver); private: #if SOURCE_ENGINE == SE_CSGO @@ -143,16 +143,7 @@ private: void OnRemoveHLTVServer_Post(IHLTVServer *hltv); #else void OnSetHLTVServer_Post(IHLTVServer *hltv); - - bool OnHLTVBotNetChanSendNetMsg(INetMessage &msg, bool bForceReliable, bool bVoice); - void OnHLTVBotClientPrintf_Post(const char *buf); #endif - bool OnHLTVBotExecuteStringCommand(const char *s); - bool OnHLTVBotExecuteStringCommand_Post(const char *s); - -private: - void HookSourceTVServer(IHLTVServer *hltv); - void UnhookSourceTVServer(IHLTVServer *hltv); }; /* Interfaces from SourceMod */ @@ -167,7 +158,7 @@ extern IGameEventManager2 *gameevents; extern ICvar *icvar; extern IHLTVDirector *hltvdirector; -extern IHLTVServer *hltvserver; -extern IDemoRecorder *demorecorder; +extern HLTVServerWrapper *hltvserver; +extern void *host_client; #endif // _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_ diff --git a/forwards.cpp b/forwards.cpp index 250e2d1..2228171 100644 --- a/forwards.cpp +++ b/forwards.cpp @@ -104,6 +104,9 @@ void CForwardManager::Init() 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); m_SpectatorPutInServerFwd = forwards->CreateForward("SourceTV_OnSpectatorPutInServer", ET_Ignore, 1, NULL, Param_Cell); + + m_ServerStartFwd = forwards->CreateForward("SourceTV_OnServerStart", ET_Ignore, 1, NULL, Param_Cell); + m_ServerShutdownFwd = forwards->CreateForward("SourceTV_OnServerShutdown", ET_Ignore, 1, NULL, Param_Cell); } void CForwardManager::Shutdown() @@ -115,6 +118,9 @@ void CForwardManager::Shutdown() forwards->ReleaseForward(m_SpectatorDisconnectFwd); forwards->ReleaseForward(m_SpectatorDisconnectedFwd); forwards->ReleaseForward(m_SpectatorPutInServerFwd); + + forwards->ReleaseForward(m_ServerStartFwd); + forwards->ReleaseForward(m_ServerShutdownFwd); } void CForwardManager::HookRecorder(IDemoRecorder *recorder) @@ -188,6 +194,18 @@ void CForwardManager::UnhookClient(IClient *client) SH_REMOVE_HOOK(IClient, Disconnect, client, SH_MEMBER(this, &CForwardManager::OnSpectatorDisconnect), false); } +void CForwardManager::CallOnServerStart(IHLTVServer *server) +{ + m_ServerStartFwd->PushCell(0); // TODO: Get right hltvinstance + m_ServerStartFwd->Execute(); +} + +void CForwardManager::CallOnServerShutdown(IHLTVServer *server) +{ + m_ServerShutdownFwd->PushCell(0); // TODO: Get right hltvinstance + m_ServerShutdownFwd->Execute(); +} + #if SOURCE_ENGINE == SE_CSGO static bool ExtractPlayerName(CUtlVector &pSplitPlayerConnectVector, char *name, int maxlen) { diff --git a/forwards.h b/forwards.h index 4d577fe..85ff809 100644 --- a/forwards.h +++ b/forwards.h @@ -69,6 +69,9 @@ public: void HookServer(IServer *server); void UnhookServer(IServer *server); + void CallOnServerStart(IHLTVServer *server); + void CallOnServerShutdown(IHLTVServer *server); + private: void HookClient(IClient *client); void UnhookClient(IClient *client); @@ -96,6 +99,9 @@ private: IForward *m_SpectatorDisconnectedFwd; IForward *m_SpectatorPutInServerFwd; + IForward *m_ServerStartFwd; + IForward *m_ServerShutdownFwd; + bool m_bHasClientConnectOffset = false; bool m_bHasRejectConnectionOffset = false; bool m_bHasGetChallengeTypeOffset = false; diff --git a/hltvserverwrapper.cpp b/hltvserverwrapper.cpp new file mode 100644 index 0000000..81e40e5 --- /dev/null +++ b/hltvserverwrapper.cpp @@ -0,0 +1,350 @@ +#include "hltvserverwrapper.h" +#include "forwards.h" + +void *old_host_client = nullptr; +bool g_HostClientOverridden = false; + +SH_DECL_HOOK1(IClient, ExecuteStringCommand, SH_NOATTRIB, 0, bool, const char *); +SH_DECL_MANUALHOOK0_void(CHLTVServer_Shutdown, 0, 0, 0); + +#if SOURCE_ENGINE != SE_CSGO + +// Stuff to print to demo console +SH_DECL_HOOK0_void_vafmt(IClient, ClientPrintf, SH_NOATTRIB, 0); +// This should be large enough. +#define FAKE_VTBL_LENGTH 70 +static void *FakeNetChanVtbl[FAKE_VTBL_LENGTH]; +static void *FakeNetChan = &FakeNetChanVtbl; +SH_DECL_MANUALHOOK3(NetChan_SendNetMsg, 0, 0, 0, bool, INetMessage &, bool, bool); +#endif // SOURCE_ENGINE != SE_CSGO + +HLTVServerWrapper::HLTVServerWrapper(IHLTVServer *hltvserver) +{ + m_HLTVServer = hltvserver; + m_DemoRecorder = g_HLTVServers.GetDemoRecorderPtr(hltvserver); + m_Connected = true; + + Hook(); + + // Inform the plugins + g_pSTVForwards.CallOnServerStart(hltvserver); +} + +void HLTVServerWrapper::Shutdown(bool bInformPlugins) +{ + if (!m_Connected) + return; + + if (bInformPlugins) + g_pSTVForwards.CallOnServerShutdown(m_HLTVServer); + + Unhook(); + + m_HLTVServer = nullptr; + m_DemoRecorder = nullptr; + m_Connected = false; +} + +IServer *HLTVServerWrapper::GetBaseServer() +{ + return m_HLTVServer->GetBaseServer(); +} + +IHLTVServer *HLTVServerWrapper::GetHLTVServer() +{ + return m_HLTVServer; +} + +IDemoRecorder *HLTVServerWrapper::GetDemoRecorder() +{ + return m_DemoRecorder; +} + +int HLTVServerWrapper::GetInstanceNumber() +{ + return g_HLTVServers.GetInstanceNumber(m_HLTVServer); +} + +void HLTVServerWrapper::Hook() +{ + if (!m_Connected) + return; + + g_pSTVForwards.HookServer(m_HLTVServer->GetBaseServer()); + g_pSTVForwards.HookRecorder(m_DemoRecorder); + + if (g_HLTVServers.HasShutdownOffset()) + SH_ADD_MANUALHOOK(CHLTVServer_Shutdown, m_HLTVServer->GetBaseServer(), SH_MEMBER(this, &HLTVServerWrapper::OnHLTVServerShutdown), false); + + if (iserver) + { + IClient *pClient = iserver->GetClient(m_HLTVServer->GetHLTVSlot()); + if (pClient) + { + SH_ADD_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &HLTVServerWrapper::OnHLTVBotExecuteStringCommand), false); + SH_ADD_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &HLTVServerWrapper::OnHLTVBotExecuteStringCommand_Post), true); +#if SOURCE_ENGINE != SE_CSGO + SH_ADD_HOOK(IClient, ClientPrintf, pClient, SH_MEMBER(this, &HLTVServerWrapper::OnHLTVBotClientPrintf_Post), false); +#endif + } + } +} + +void HLTVServerWrapper::Unhook() +{ + if (!m_Connected) + return; + + g_pSTVForwards.UnhookServer(m_HLTVServer->GetBaseServer()); + g_pSTVForwards.UnhookRecorder(m_DemoRecorder); + + if (g_HLTVServers.HasShutdownOffset()) + SH_REMOVE_MANUALHOOK(CHLTVServer_Shutdown, m_HLTVServer->GetBaseServer(), SH_MEMBER(this, &HLTVServerWrapper::OnHLTVServerShutdown), false); + + if (iserver) + { + IClient *pClient = iserver->GetClient(m_HLTVServer->GetHLTVSlot()); + if (pClient) + { + SH_REMOVE_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &HLTVServerWrapper::OnHLTVBotExecuteStringCommand), false); + SH_REMOVE_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &HLTVServerWrapper::OnHLTVBotExecuteStringCommand_Post), true); +#if SOURCE_ENGINE != SE_CSGO + SH_REMOVE_HOOK(IClient, ClientPrintf, pClient, SH_MEMBER(this, &HLTVServerWrapper::OnHLTVBotClientPrintf_Post), false); +#endif + } + } +} + +// CHLTVServer::Shutdown deregisters the hltvserver from the hltvdirector, +// so RemoveHLTVServer/SetHLTVServer(NULL) is called too on the master proxy. +void HLTVServerWrapper::OnHLTVServerShutdown() +{ + if (!m_Connected) + RETURN_META(MRES_IGNORED); + + Shutdown(true); + + RETURN_META(MRES_IGNORED); +} + +// When bots issue a command that would print stuff to their console, +// the server might crash, because ExecuteStringCommand doesn't set the +// global host_client pointer to the client on whom the command is run. +// Host_Client_Printf blatantly tries to call host_client->ClientPrintf +// while the pointer might point to some other player or garbage. +// This leads to e.g. the output of the "status" command not being +// recorded in the SourceTV demo. +// The approach here is to set host_client correctly for the SourceTV +// bot and reset it to the old value after command execution. +bool HLTVServerWrapper::OnHLTVBotExecuteStringCommand(const char *s) +{ + if (!host_client) + RETURN_META_VALUE(MRES_IGNORED, 0); + + IClient *pClient = META_IFACEPTR(IClient); + if (!pClient) + RETURN_META_VALUE(MRES_IGNORED, 0); + + // The IClient vtable is +4 from the CBaseClient vtable due to multiple inheritance. + void *pGameClient = (void *)((intptr_t)pClient - 4); + + old_host_client = *(void **)host_client; + *(void **)host_client = pGameClient; + g_HostClientOverridden = true; + + RETURN_META_VALUE(MRES_IGNORED, 0); +} + +bool HLTVServerWrapper::OnHLTVBotExecuteStringCommand_Post(const char *s) +{ + if (!host_client || !g_HostClientOverridden) + RETURN_META_VALUE(MRES_IGNORED, 0); + + *(void **)host_client = old_host_client; + g_HostClientOverridden = false; + RETURN_META_VALUE(MRES_IGNORED, 0); +} + +#if SOURCE_ENGINE != SE_CSGO +void HLTVServerWrapper::OnHLTVBotClientPrintf_Post(const char* buf) +{ + // Craft our own "NetChan" pointer + static int offset = -1; + if (!g_pGameConf->GetOffset("CBaseClient::m_NetChannel", &offset) || offset == -1) + { + smutils->LogError(myself, "Failed to find CBaseClient::m_NetChannel offset. Can't print to demo console."); + RETURN_META(MRES_IGNORED); + } + + IClient *pClient = META_IFACEPTR(IClient); + + void *pNetChannel = (void *)((char *)pClient + offset); + // Set our fake netchannel + *(void **)pNetChannel = &FakeNetChan; + // Call ClientPrintf again, this time with a "Netchannel" set on the bot. + // This will call our own OnHLTVBotNetChanSendNetMsg function + SH_CALL(pClient, &IClient::ClientPrintf)("%s", buf); + // Set the fake netchannel back to 0. + *(void **)pNetChannel = nullptr; + + RETURN_META(MRES_IGNORED); +} +#endif + +/** + * Manage the wrappers! + */ +void HLTVServerWrapperManager::InitHooks() +{ + int offset; + if (g_pGameConf->GetOffset("CHLTVServer::Shutdown", &offset)) + { + SH_MANUALHOOK_RECONFIGURE(CHLTVServer_Shutdown, offset, 0, 0); + m_bHasShutdownOffset = true; + } + else + { + smutils->LogError(myself, "Failed to find CHLTVServer::Shutdown offset."); + } + +#if SOURCE_ENGINE != SE_CSGO + if (g_pGameConf->GetOffset("CNetChan::SendNetMsg", &offset)) + { + if (offset >= FAKE_VTBL_LENGTH) + { + smutils->LogError(myself, "CNetChan::SendNetMsg offset too big. Need to raise define and recompile. Contact the author."); + } + else + { + // This is a hack. Bots don't have a net channel, but ClientPrintf tries to call m_NetChannel->SendNetMsg directly. + // CGameClient::SendNetMsg would have redirected it to the hltvserver correctly, but isn't used there.. + // We craft a fake object with a large enough "vtable" and hook it using sourcehook. + // Before a call to ClientPrintf, this fake object is set as CBaseClient::m_NetChannel, so ClientPrintf creates + // the SVC_Print INetMessage and calls our "hooked" m_NetChannel->SendNetMsg function. + // In that function we just call CGameClient::SendNetMsg with the given INetMessage to flow it through the same + // path as other net messages. + SH_MANUALHOOK_RECONFIGURE(NetChan_SendNetMsg, offset, 0, 0); + SH_ADD_MANUALHOOK(NetChan_SendNetMsg, &FakeNetChan, SH_MEMBER(this, &HLTVServerWrapperManager::OnHLTVBotNetChanSendNetMsg), false); + m_bSendNetMsgHooked = true; + } + } + else + { + smutils->LogError(myself, "Failed to find CNetChan::SendNetMsg offset. Can't print to demo console."); + } +#endif +} + +void HLTVServerWrapperManager::ShutdownHooks() +{ +#if SOURCE_ENGINE != SE_CSGO + if (m_bSendNetMsgHooked) + { + SH_REMOVE_MANUALHOOK(NetChan_SendNetMsg, &FakeNetChan, SH_MEMBER(this, &HLTVServerWrapperManager::OnHLTVBotNetChanSendNetMsg), false); + m_bSendNetMsgHooked = false; + } +#endif +} + +void HLTVServerWrapperManager::AddServer(IHLTVServer *hltvserver) +{ + HLTVServerWrapper *wrapper = new HLTVServerWrapper(hltvserver); + m_HLTVServers.append(wrapper); +} + +void HLTVServerWrapperManager::RemoveServer(IHLTVServer *hltvserver, bool bInformPlugins) +{ + for (unsigned int i = 0; i < m_HLTVServers.length(); i++) + { + HLTVServerWrapper *wrapper = m_HLTVServers[i]; + if (wrapper->GetHLTVServer() != hltvserver) + continue; + + wrapper->Shutdown(bInformPlugins); + m_HLTVServers.remove(i); + break; + } +} + +HLTVServerWrapper *HLTVServerWrapperManager::GetWrapper(IHLTVServer *hltvserver) +{ + for (unsigned int i = 0; i < m_HLTVServers.length(); i++) + { + HLTVServerWrapper *wrapper = m_HLTVServers[i]; + if (wrapper->GetHLTVServer() == hltvserver) + return wrapper; + } + return nullptr; +} + +int HLTVServerWrapperManager::GetInstanceNumber(IHLTVServer *hltvserver) +{ +#if SOURCE_ENGINE == SE_CSGO + for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++) + { + if (hltvserver == hltvdirector->GetHLTVServer(i)) + return i; + } + + // We should have found it in the above loop :S + smutils->LogError(myself, "Failed to find IHLTVServer instance in director."); + return -1; +#else + return 0; +#endif +} + +IDemoRecorder *HLTVServerWrapperManager::GetDemoRecorderPtr(IHLTVServer *hltv) +{ + static int offset = -1; + if (offset == -1) + { + void *addr; + if (!g_pGameConf->GetAddress("CHLTVServer::m_DemoRecorder", &addr)) + { + smutils->LogError(myself, "Failed to get CHLTVServer::m_DemoRecorder offset."); + return nullptr; + } + + *(int **)&offset = (int *)addr; + } + + if (hltv) + { +#if SOURCE_ENGINE == SE_CSGO + return (IDemoRecorder *)((intptr_t)hltv + offset); +#else + IServer *baseServer = hltv->GetBaseServer(); + return (IDemoRecorder *)((intptr_t)baseServer + offset); +#endif + } + else + { + return nullptr; + } +} + +bool HLTVServerWrapperManager::HasShutdownOffset() +{ + return m_bHasShutdownOffset; +} + +#if SOURCE_ENGINE != SE_CSGO +bool HLTVServerWrapperManager::OnHLTVBotNetChanSendNetMsg(INetMessage &msg, bool bForceReliable, bool bVoice) +{ + // No need to worry about the right selected hltvserver, because there can only be one. + IClient *pClient = iserver->GetClient(hltvserver->GetHLTVServer()->GetHLTVSlot()); + if (!pClient) + RETURN_META_VALUE(MRES_SUPERCEDE, false); + + // Let the message flow through the intended path like CGameClient::SendNetMsg wants to. + bool bRetSent = pClient->SendNetMsg(msg, bForceReliable); + + // It's important to supercede, because there is no original function to call. + // (the "vtable" was empty before hooking it) + // See FakeNetChan variable at the top. + RETURN_META_VALUE(MRES_SUPERCEDE, bRetSent); +} +#endif + +HLTVServerWrapperManager g_HLTVServers; \ No newline at end of file diff --git a/hltvserverwrapper.h b/hltvserverwrapper.h new file mode 100644 index 0000000..a1d1b0f --- /dev/null +++ b/hltvserverwrapper.h @@ -0,0 +1,95 @@ +/** +* vim: set ts=4 : +* ============================================================================= +* SourceMod SourceTV Manager Extension +* Copyright (C) 2004-2016 AlliedModders LLC. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, AlliedModders LLC gives you permission to link the +* code of this program (as well as its derivative works) to "Half-Life 2," the +* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, AlliedModders LLC grants +* this exception to all derivative works. AlliedModders LLC defines further +* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), +* or . +* +* Version: $Id$ +*/ + +#ifndef _INCLUDE_SOURCEMOD_EXTENSION_HLTVSERVER_H_ +#define _INCLUDE_SOURCEMOD_EXTENSION_HLTVSERVER_H_ + +#include "extension.h" +#include "amtl/am-vector.h" +#include "amtl/am-utility.h" + +class HLTVServerWrapper { +public: + HLTVServerWrapper(IHLTVServer *hltvserver); + void Shutdown(bool bInformPlugins); + + IHLTVServer *GetHLTVServer(); + IServer *GetBaseServer(); + IDemoRecorder *GetDemoRecorder(); + int GetInstanceNumber(); + +private: + void Hook(); + void Unhook(); + + // Hooks + bool OnHLTVBotExecuteStringCommand(const char *s); + bool OnHLTVBotExecuteStringCommand_Post(const char *s); + void OnHLTVServerShutdown(); + +#if SOURCE_ENGINE != SE_CSGO + void OnHLTVBotClientPrintf_Post(const char *buf); +#endif + +private: + bool m_Connected = false; + IHLTVServer *m_HLTVServer = nullptr; + IDemoRecorder *m_DemoRecorder = nullptr; +}; + +class HLTVServerWrapperManager +{ +public: + void InitHooks(); + void ShutdownHooks(); + void AddServer(IHLTVServer *hltvserver); + void RemoveServer(IHLTVServer *hltvserver, bool bInformPlugins); + HLTVServerWrapper *GetWrapper(IHLTVServer *hltvserver); + int GetInstanceNumber(IHLTVServer *hltvserver); + + IDemoRecorder *GetDemoRecorderPtr(IHLTVServer *hltv); + bool HasShutdownOffset(); + +#if SOURCE_ENGINE != SE_CSGO + bool OnHLTVBotNetChanSendNetMsg(INetMessage &msg, bool bForceReliable, bool bVoice); +#endif + +private: +#if SOURCE_ENGINE != SE_CSGO + bool m_bSendNetMsgHooked = false; +#endif + bool m_bHasShutdownOffset = false; + ke::Vector> m_HLTVServers; +}; + +extern HLTVServerWrapperManager g_HLTVServers; + +#endif // _INCLUDE_SOURCEMOD_EXTENSION_HLTVSERVER_H_ \ No newline at end of file diff --git a/natives.cpp b/natives.cpp index 7248f7a..4e54faf 100644 --- a/natives.cpp +++ b/natives.cpp @@ -31,6 +31,7 @@ #include "extension.h" #include "natives.h" +#include "hltvserverwrapper.h" #define TICK_INTERVAL (gpGlobals->interval_per_tick) #define TIME_TO_TICKS( dt ) ( (int)( 0.5f + (float)(dt) / TICK_INTERVAL ) ) @@ -94,20 +95,16 @@ static cell_t Native_GetSelectedServerInstance(IPluginContext *pContext, const c if (hltvserver == nullptr) return -1; -#if SOURCE_ENGINE == SE_CSGO - for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++) - { - if (hltvserver == hltvdirector->GetHLTVServer(i)) - return i; - } + return hltvserver->GetInstanceNumber(); +} - // We should have found it in the above loop :S - hltvserver = nullptr; - return -1; -#else - // There only is one hltv server. - return 0; -#endif +// native SourceTV_IsActive(); +static cell_t Native_IsActive(IPluginContext *pContext, const cell_t *params) +{ + if (hltvserver == nullptr) + return 0; + + return hltvserver->GetBaseServer()->IsActive(); } // native SourceTV_IsMasterProxy(); @@ -116,7 +113,7 @@ static cell_t Native_IsMasterProxy(IPluginContext *pContext, const cell_t *param if (hltvserver == nullptr) return 0; - return hltvserver->IsMasterProxy(); + return hltvserver->GetHLTVServer()->IsMasterProxy(); } // native bool:SourceTV_GetServerIP(String:ip[], maxlen); @@ -125,7 +122,7 @@ static cell_t Native_GetServerIP(IPluginContext *pContext, const cell_t *params) if (hltvserver == nullptr) return 0; - const netadr_t *adr = hltvserver->GetRelayAddress(); + const netadr_t *adr = hltvserver->GetHLTVServer()->GetRelayAddress(); char buf[16]; V_snprintf(buf, sizeof(buf), "%d.%d.%d.%d", adr->ip[0], adr->ip[1], adr->ip[2], adr->ip[3]); @@ -149,7 +146,7 @@ static cell_t Native_GetBotIndex(IPluginContext *pContext, const cell_t *params) if (hltvserver == nullptr) return 0; - return hltvserver->GetHLTVSlot() + 1; + return hltvserver->GetHLTVServer()->GetHLTVSlot() + 1; } // native bool:SourceTV_GetLocalStats(&proxies, &slots, &specs); @@ -159,7 +156,7 @@ static cell_t Native_GetLocalStats(IPluginContext *pContext, const cell_t *param return 0; int proxies, slots, specs; - hltvserver->GetLocalStats(proxies, slots, specs); + hltvserver->GetHLTVServer()->GetLocalStats(proxies, slots, specs); cell_t *plProxies, *plSlots, *plSpecs; pContext->LocalToPhysAddr(params[1], &plProxies); @@ -179,7 +176,7 @@ static cell_t Native_GetGlobalStats(IPluginContext *pContext, const cell_t *para return 0; int proxies, slots, specs; - hltvserver->GetGlobalStats(proxies, slots, specs); + hltvserver->GetHLTVServer()->GetGlobalStats(proxies, slots, specs); cell_t *plProxies, *plSlots, *plSpecs; pContext->LocalToPhysAddr(params[1], &plProxies); @@ -275,9 +272,9 @@ static cell_t Native_BroadcastScreenMessage(IPluginContext *pContext, const cell int ret = 1; bool bLocalOnly = params[1] != 0; if (bLocalOnly) - hltvserver->BroadcastEvent(msg); + hltvserver->GetHLTVServer()->BroadcastEvent(msg); else - ret = BroadcastEventLocal(hltvserver, msg, false); + ret = BroadcastEventLocal(hltvserver->GetHLTVServer(), msg, false); gameevents->FreeEvent(msg); @@ -331,9 +328,9 @@ static cell_t Native_BroadcastChatMessage(IPluginContext *pContext, const cell_t int ret = 1; bool bLocalOnly = params[1] != 0; if (bLocalOnly) - hltvserver->BroadcastEvent(msg); + hltvserver->GetHLTVServer()->BroadcastEvent(msg); else - ret = BroadcastEventLocal(hltvserver, msg, false); + ret = BroadcastEventLocal(hltvserver->GetHLTVServer(), msg, false); gameevents->FreeEvent(msg); @@ -395,7 +392,7 @@ static cell_t Native_ForceFixedCameraShot(IPluginContext *pContext, const cell_t shot->SetInt("target", params[3] ? gamehelpers->ReferenceToIndex(params[3]) : 0); shot->SetFloat("fov", sp_ctof(params[4])); - hltvserver->BroadcastEvent(shot); + hltvserver->GetHLTVServer()->BroadcastEvent(shot); gameevents->FreeEvent(shot); // Prevent auto director from changing shots until we allow it to again. @@ -437,7 +434,7 @@ static cell_t Native_ForceChaseCameraShot(IPluginContext *pContext, const cell_t // Update director state g_HLTVDirectorWrapper.SetPVSEntity(gamehelpers->ReferenceToIndex(params[1])); - hltvserver->BroadcastEvent(shot); + hltvserver->GetHLTVServer()->BroadcastEvent(shot); gameevents->FreeEvent(shot); // Prevent auto director from changing shots until we allow it to again. @@ -449,10 +446,7 @@ static cell_t Native_ForceChaseCameraShot(IPluginContext *pContext, const cell_t // native bool:SourceTV_IsRecording(); static cell_t Native_IsRecording(IPluginContext *pContext, const cell_t *params) { - if (demorecorder == nullptr) - return 0; - - return demorecorder->IsRecording(); + return hltvserver->GetDemoRecorder()->IsRecording(); } // Checks in COM_IsValidPath in the engine @@ -464,7 +458,7 @@ static bool IsValidPath(const char *path) // native bool:SourceTV_StartRecording(const String:sFilename[]); static cell_t Native_StartRecording(IPluginContext *pContext, const cell_t *params) { - if (hltvserver == nullptr || demorecorder == nullptr) + if (hltvserver == nullptr) return 0; // SourceTV is not active. @@ -472,11 +466,11 @@ static cell_t Native_StartRecording(IPluginContext *pContext, const cell_t *para return 0; // Only SourceTV Master can record demos instantly - if (!hltvserver->IsMasterProxy()) + if (!hltvserver->GetHLTVServer()->IsMasterProxy()) return 0; // already recording - if (demorecorder->IsRecording()) + if (hltvserver->GetDemoRecorder()->IsRecording()) return 0; char *pFile; @@ -512,7 +506,7 @@ static cell_t Native_StartRecording(IPluginContext *pContext, const cell_t *para } #endif - demorecorder->StartRecording(pPath, false); + hltvserver->GetDemoRecorder()->StartRecording(pPath, false); return 1; } @@ -520,17 +514,14 @@ static cell_t Native_StartRecording(IPluginContext *pContext, const cell_t *para // native bool:SourceTV_StopRecording(); static cell_t Native_StopRecording(IPluginContext *pContext, const cell_t *params) { - if (demorecorder == nullptr) - return 0; - - if (!demorecorder->IsRecording()) + if (!hltvserver->GetDemoRecorder()->IsRecording()) return 0; #if SOURCE_ENGINE == SE_CSGO - hltvserver->StopRecording(NULL); + hltvserver->GetDemoRecorder()->StopRecording(NULL); // TODO: Stop recording on all other active hltvservers (tv_stoprecord in csgo does this) #else - demorecorder->StopRecording(); + hltvserver->GetDemoRecorder()->StopRecording(); #endif return 1; @@ -539,13 +530,10 @@ static cell_t Native_StopRecording(IPluginContext *pContext, const cell_t *param // native bool:SourceTV_GetDemoFileName(String:sFilename[], maxlen); static cell_t Native_GetDemoFileName(IPluginContext *pContext, const cell_t *params) { - if (demorecorder == nullptr) + if (!hltvserver->GetDemoRecorder()->IsRecording()) return 0; - if (!demorecorder->IsRecording()) - return 0; - - char *pDemoFile = (char *)demorecorder->GetDemoFile(); + char *pDemoFile = (char *)hltvserver->GetDemoRecorder()->GetDemoFile(); if (!pDemoFile) return 0; @@ -557,13 +545,10 @@ static cell_t Native_GetDemoFileName(IPluginContext *pContext, const cell_t *par // native SourceTV_GetRecordingTick(); static cell_t Native_GetRecordingTick(IPluginContext *pContext, const cell_t *params) { - if (demorecorder == nullptr) + if (!hltvserver->GetDemoRecorder()->IsRecording()) return -1; - if (!demorecorder->IsRecording()) - return -1; - - return demorecorder->GetRecordingTick(); + return hltvserver->GetDemoRecorder()->GetRecordingTick(); } // native bool:SourceTV_PrintToDemoConsole(const String:format[], any:...); @@ -574,7 +559,7 @@ static cell_t Native_PrintToDemoConsole(IPluginContext *pContext, const cell_t * if (!iserver) return 0; - IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot()); + IClient *pClient = iserver->GetClient(hltvserver->GetHLTVServer()->GetHLTVSlot()); if (!pClient) return 0; @@ -742,6 +727,7 @@ const sp_nativeinfo_t sourcetv_natives[] = { "SourceTV_GetServerInstanceCount", Native_GetServerInstanceCount }, { "SourceTV_SelectServerInstance", Native_SelectServerInstance }, { "SourceTV_GetSelectedServerInstance", Native_GetSelectedServerInstance }, + { "SourceTV_IsActive", Native_IsActive }, { "SourceTV_IsMasterProxy", Native_IsMasterProxy }, { "SourceTV_GetServerIP", Native_GetServerIP }, { "SourceTV_GetServerPort", Native_GetServerPort }, diff --git a/sourcetv_test.sp b/sourcetv_test.sp index ce368eb..d56a3da 100644 --- a/sourcetv_test.sp +++ b/sourcetv_test.sp @@ -57,6 +57,16 @@ public bool:SourceTV_OnSpectatorPreConnect(const String:name[], String:password[ return true; } +public SourceTV_OnServerStart(instance) +{ + PrintToServer("SourceTV instance %d started.", instance); +} + +public SourceTV_OnServerShutdown(instance) +{ + PrintToServer("SourceTV instance %d shutdown.", instance); +} + public SourceTV_OnSpectatorConnected(client) { PrintToServer("SourceTV client %d connected. (isconnected %d)", client, SourceTV_IsClientConnected(client)); diff --git a/sourcetvmanager.games.txt b/sourcetvmanager.games.txt index 8c4d37d..b803011 100644 --- a/sourcetvmanager.games.txt +++ b/sourcetvmanager.games.txt @@ -63,6 +63,12 @@ "linux" "65" } + "CHLTVServer::Shutdown" + { + "windows" "45" + "linux" "46" + } + "CHLTVDirector::m_iPVSEntity" { "windows" "32" @@ -182,6 +188,12 @@ "linux" "56" } + "CHLTVServer::Shutdown" + { + "windows" "41" + "linux" "42" + } + "CHLTVDirector::m_iPVSEntity" { "windows" "16" diff --git a/sourcetvmanager.inc b/sourcetvmanager.inc index ae5e88d..c12c6a4 100644 --- a/sourcetvmanager.inc +++ b/sourcetvmanager.inc @@ -35,12 +35,35 @@ native SourceTV_SelectServerInstance(instance); */ native SourceTV_GetSelectedServerInstance(); +/** + * Called when a SourceTV is initialized. + * + * @param instance The SourceTV instance number. + * @noreturn + */ +forward SourceTV_OnServerStart(instance); + + /** + * Called when a SourceTV server instance is shutdown. + * + * @param instance The SourceTV instance number. + * @noreturn + */ +forward SourceTV_OnServerShutdown(instance); + +/** + * Returns whether this SourceTV instance is currently broadcasting. + * + * @return True if SourceTV instance is broadcasting, false otherwise. + */ +native bool:SourceTV_IsActive(); + /** * Returns whether this SourceTV instance is a master proxy or relay. * * @return True if SourceTV instance is master proxy, false otherwise. */ -native SourceTV_IsMasterProxy(); +native bool:SourceTV_IsMasterProxy(); /** * Get the local ip of the SourceTV server. @@ -427,7 +450,10 @@ public __ext_stvmngr_SetNTVOptional() MarkNativeAsOptional("SourceTV_GetServerInstanceCount"); MarkNativeAsOptional("SourceTV_SelectServerInstance"); MarkNativeAsOptional("SourceTV_GetSelectedServerInstance"); + MarkNativeAsOptional("SourceTV_IsActive"); MarkNativeAsOptional("SourceTV_IsMasterProxy"); + MarkNativeAsOptional("SourceTV_GetServerIP"); + MarkNativeAsOptional("SourceTV_GetServerPort"); MarkNativeAsOptional("SourceTV_GetBotIndex"); MarkNativeAsOptional("SourceTV_GetLocalStats"); MarkNativeAsOptional("SourceTV_GetGlobalStats");