c0f634005d
If the pointer to the IDemoRecorder instance fails to be found after an update, don't try to use it blindly, but check if it's not null before..
465 lines
14 KiB
C++
465 lines
14 KiB
C++
#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);
|
|
|
|
// Linux has the ClientPrintf method in both CGameClient and IClient's vtables
|
|
// and uses both.. Need to hook both....... i guess?
|
|
#ifndef WIN32
|
|
SH_DECL_MANUALHOOK0_void_vafmt(CGameClient_ClientPrintf, 0, 0, 0);
|
|
#endif
|
|
|
|
// 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);
|
|
}
|
|
|
|
HLTVClientWrapper *HLTVServerWrapper::GetClient(int index)
|
|
{
|
|
// Grow the vector with null pointers
|
|
// There might have been clients with lower indexes before we were loaded.
|
|
if (m_Clients.length() < (size_t)index)
|
|
{
|
|
int start = m_Clients.length();
|
|
m_Clients.resize(index);
|
|
for (int i = start; i < index; i++)
|
|
{
|
|
m_Clients[i] = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!m_Clients[index - 1])
|
|
{
|
|
m_Clients[index - 1] = new HLTVClientWrapper();
|
|
}
|
|
|
|
return m_Clients[index - 1];
|
|
}
|
|
|
|
void HLTVServerWrapper::Hook()
|
|
{
|
|
if (!m_Connected)
|
|
return;
|
|
|
|
g_pSTVForwards.HookServer(this);
|
|
if (m_DemoRecorder)
|
|
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::OnIClient_ClientPrintf_Post), false);
|
|
#ifndef WIN32
|
|
// The IClient vtable is +4 from the CBaseClient vtable due to multiple inheritance.
|
|
void *pGameClient = (void *)((intptr_t)pClient - 4);
|
|
if (g_HLTVServers.HasClientPrintfOffset())
|
|
SH_ADD_MANUALHOOK(CGameClient_ClientPrintf, pGameClient, SH_MEMBER(this, &HLTVServerWrapper::OnCGameClient_ClientPrintf_Post), false);
|
|
#endif // !WIN32
|
|
#endif // SOURCE_ENGINE != SE_CSGO
|
|
}
|
|
}
|
|
}
|
|
|
|
void HLTVServerWrapper::Unhook()
|
|
{
|
|
if (!m_Connected)
|
|
return;
|
|
|
|
g_pSTVForwards.UnhookServer(this);
|
|
if (m_DemoRecorder)
|
|
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::OnIClient_ClientPrintf_Post), false);
|
|
#ifndef WIN32
|
|
// The IClient vtable is +4 from the CBaseClient vtable due to multiple inheritance.
|
|
void *pGameClient = (void *)((intptr_t)pClient - 4);
|
|
if (g_HLTVServers.HasClientPrintfOffset())
|
|
SH_REMOVE_MANUALHOOK(CGameClient_ClientPrintf, pGameClient, SH_MEMBER(this, &HLTVServerWrapper::OnCGameClient_ClientPrintf_Post), false);
|
|
#endif // !WIN32
|
|
#endif // SOURCE_ENGINE != SE_CSGO
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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::OnCGameClient_ClientPrintf_Post(const char* buf)
|
|
{
|
|
void *pGameClient = META_IFACEPTR(void);
|
|
IClient *pClient = (IClient *)((intptr_t)pGameClient + 4);
|
|
HandleClientPrintf(pClient, buf);
|
|
|
|
RETURN_META(MRES_IGNORED);
|
|
}
|
|
|
|
void HLTVServerWrapper::OnIClient_ClientPrintf_Post(const char* buf)
|
|
{
|
|
IClient *pClient = META_IFACEPTR(IClient);
|
|
HandleClientPrintf(pClient, buf);
|
|
|
|
RETURN_META(MRES_IGNORED);
|
|
}
|
|
|
|
void HLTVServerWrapper::HandleClientPrintf(IClient *pClient, 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;
|
|
}
|
|
|
|
#ifdef WIN32
|
|
void *pNetChannel = (void *)((char *)pClient + offset);
|
|
#else
|
|
void *pNetChannel = (void *)((char *)pClient + offset - 4);
|
|
#endif
|
|
// 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;
|
|
}
|
|
#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
|
|
#ifndef WIN32
|
|
if (g_pGameConf->GetOffset("CGameClient::ClientPrintf", &offset))
|
|
{
|
|
SH_MANUALHOOK_RECONFIGURE(CGameClient_ClientPrintf, offset, 0, 0);
|
|
m_bHasClientPrintfOffset = true;
|
|
}
|
|
else
|
|
{
|
|
smutils->LogError(myself, "Failed to find CGameClient::ClientPrintf offset. Won't catch \"status\" console output.");
|
|
}
|
|
#endif // !WIN32
|
|
|
|
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()
|
|
{
|
|
#ifndef WIN32
|
|
g_pSTVForwards.RemoveStartRecordingDetour();
|
|
g_pSTVForwards.RemoveStopRecordingDetour();
|
|
#endif
|
|
|
|
#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)
|
|
{
|
|
#ifndef WIN32
|
|
// Create the detours once the first sourcetv server is created.
|
|
g_pSTVForwards.CreateStartRecordingDetour();
|
|
g_pSTVForwards.CreateStopRecordingDetour();
|
|
#endif
|
|
|
|
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;
|
|
}
|
|
|
|
HLTVServerWrapper *HLTVServerWrapperManager::GetWrapper(IServer *server)
|
|
{
|
|
for (unsigned int i = 0; i < m_HLTVServers.length(); i++)
|
|
{
|
|
HLTVServerWrapper *wrapper = m_HLTVServers[i];
|
|
if (wrapper->GetBaseServer() == server)
|
|
return wrapper;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
HLTVServerWrapper *HLTVServerWrapperManager::GetWrapper(IDemoRecorder *demorecorder)
|
|
{
|
|
for (unsigned int i = 0; i < m_HLTVServers.length(); i++)
|
|
{
|
|
HLTVServerWrapper *wrapper = m_HLTVServers[i];
|
|
if (wrapper->GetDemoRecorder() != nullptr && wrapper->GetDemoRecorder() == demorecorder)
|
|
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();
|
|
#ifndef WIN32
|
|
return (IDemoRecorder *)((intptr_t)baseServer + offset - 4);
|
|
#else
|
|
return (IDemoRecorder *)((intptr_t)baseServer + offset);
|
|
#endif // WIN32
|
|
#endif // SOURCE_ENGINE == SE_CSGO
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
bool HLTVServerWrapperManager::HasShutdownOffset()
|
|
{
|
|
return m_bHasShutdownOffset;
|
|
}
|
|
|
|
bool HLTVServerWrapperManager::HasClientPrintfOffset()
|
|
{
|
|
return m_bHasClientPrintfOffset;
|
|
}
|
|
|
|
#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;
|