sourcemod/extensions/sdktools/extension.cpp
Asher Baker fcafd0784c
Workaround CS:GO Stringtable bug (#1046)
The cause of the infamous "Index error writing string table baseline"
error appears to be a timing issue between the engine's network message
processing, the stringtable encoding, and command processing when adding
stringtable entries in OnConfigsExecuted.

When the first client connects the map is re-loaded which causes a full
refresh, the game's stringtable entries are added at tick 65, the client
connection is registered at tick 66, and stringtable entries added in
OnConfigsExecuted are registered as being added in tick 67. The engine
later calls WriteBaselines with the client's signon tick, and neglects
to encode the SM added entries as it considers them from the future.

To avoid this, always pass INT_MAX as the current tick when encoding
the baseline, so all stringtable entries are included regardless of when
they were added. Tested on both Windows and Linux.
2019-07-19 09:13:34 +01:00

569 lines
17 KiB
C++

/**
* vim: set ts=4 :
* =============================================================================
* SourceMod SDKTools Extension
* Copyright (C) 2004-2017 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 <http://www.gnu.org/licenses/>.
*
* 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 <http://www.sourcemod.net/license.php>.
*
* Version: $Id$
*/
#include <sourcemod_version.h>
#include "extension.h"
#include <compat_wrappers.h>
#include "vcallbuilder.h"
#include "vnatives.h"
#include "vhelpers.h"
#include "vglobals.h"
#include "tempents.h"
#include "vsound.h"
#include "variant-t.h"
#include "output.h"
#include "hooks.h"
#include "gamerulesnatives.h"
#include <ISDKTools.h>
#include "clientnatives.h"
#include "teamnatives.h"
#include "filesystem.h"
#include "am-string.h"
/**
* @file extension.cpp
* @brief Implements SDK Tools extension code.
*/
SH_DECL_HOOK6(IServerGameDLL, LevelInit, SH_NOATTRIB, false, bool, const char *, const char *, const char *, const char *, bool, bool);
#if SOURCE_ENGINE == SE_CSS || SOURCE_ENGINE == SE_CSGO
SH_DECL_HOOK1_void_vafmt(IVEngineServer, ClientCommand, SH_NOATTRIB, 0, edict_t *);
#endif
SDKTools g_SdkTools; /**< Global singleton for extension's main interface */
IServerGameEnts *gameents = NULL;
IEngineTrace *enginetrace = NULL;
ISpatialPartition *partition = NULL;
IEngineSound *engsound = NULL;
INetworkStringTableContainer *netstringtables = NULL;
IServerPluginHelpers *pluginhelpers = NULL;
IBinTools *g_pBinTools = NULL;
IGameConfig *g_pGameConf = NULL;
IGameHelpers *g_pGameHelpers = NULL;
IServerGameClients *serverClients = NULL;
IVoiceServer *voiceserver = NULL;
IPlayerInfoManager *playerinfomngr = NULL;
ICvar *icvar = NULL;
IServer *iserver = NULL;
IBaseFileSystem *basefilesystem = NULL;
CGlobalVars *gpGlobals;
ISoundEmitterSystemBase *soundemitterbase = NULL;
#if SOURCE_ENGINE >= SE_ORANGEBOX
IServerTools *servertools = NULL;
#endif
SourceHook::CallClass<IVEngineServer> *enginePatch = NULL;
SourceHook::CallClass<IEngineSound> *enginesoundPatch = NULL;
HandleType_t g_CallHandle = 0;
HandleType_t g_TraceHandle = 0;
ISDKTools *g_pSDKTools;
SMEXT_LINK(&g_SdkTools);
extern sp_nativeinfo_t g_CallNatives[];
extern sp_nativeinfo_t g_TENatives[];
extern sp_nativeinfo_t g_TRNatives[];
extern sp_nativeinfo_t g_StringTableNatives[];
extern sp_nativeinfo_t g_VoiceNatives[];
extern sp_nativeinfo_t g_EntInputNatives[];
extern sp_nativeinfo_t g_TeamNatives[];
extern sp_nativeinfo_t g_GameRulesNatives[];
extern sp_nativeinfo_t g_ClientNatives[];
static void InitSDKToolsAPI();
#if SOURCE_ENGINE == SE_CSGO
CDetour *g_WriteBaselinesDetour = NULL;
DETOUR_DECL_MEMBER3(CNetworkStringTableContainer__WriteBaselines, void, char const *, mapName, void *, buffer, int, currentTick)
{
// Replace nAtTick with INT_MAX to work around CS:GO engine bug.
// Due to a timing issue in the engine, stringtable entries added in OnConfigsExecuted can be considered
// to have been added in the future for the first client that connects, which causes them to be ignored
// when iterating for networking, which triggers a Host_Error encoding the CreateStringTable netmsg.
return DETOUR_MEMBER_CALL(CNetworkStringTableContainer__WriteBaselines)(mapName, buffer, INT_MAX);
}
#endif
bool SDKTools::SDK_OnLoad(char *error, size_t maxlength, bool late)
{
HandleError err;
if (!gameconfs->LoadGameConfigFile("sdktools.games", &g_pGameConf, error, maxlength))
{
return false;
}
sharesys->AddDependency(myself, "bintools.ext", true, true);
sharesys->AddNatives(myself, g_CallNatives);
sharesys->AddNatives(myself, g_Natives);
sharesys->AddNatives(myself, g_TENatives);
sharesys->AddNatives(myself, g_SoundNatives);
sharesys->AddNatives(myself, g_TRNatives);
sharesys->AddNatives(myself, g_StringTableNatives);
sharesys->AddNatives(myself, g_VoiceNatives);
sharesys->AddNatives(myself, g_VariantTNatives);
sharesys->AddNatives(myself, g_EntInputNatives);
sharesys->AddNatives(myself, g_TeamNatives);
sharesys->AddNatives(myself, g_EntOutputNatives);
sharesys->AddNatives(myself, g_GameRulesNatives);
sharesys->AddNatives(myself, g_ClientNatives);
SM_GET_IFACE(GAMEHELPERS, g_pGameHelpers);
playerhelpers->AddClientListener(&g_SdkTools);
g_CallHandle = handlesys->CreateType("ValveCall", this, 0, NULL, NULL, myself->GetIdentity(), &err);
if (g_CallHandle == 0)
{
ke::SafeSprintf(error, maxlength, "Could not create call handle type (err: %d)", err);
return false;
}
TypeAccess TraceAccess;
handlesys->InitAccessDefaults(&TraceAccess, NULL);
TraceAccess.ident = myself->GetIdentity();
TraceAccess.access[HTypeAccess_Create] = true;
TraceAccess.access[HTypeAccess_Inherit] = true;
g_TraceHandle = handlesys->CreateType("TraceRay", this, 0, &TraceAccess, NULL, myself->GetIdentity(), &err);
if (g_TraceHandle == 0)
{
handlesys->RemoveType(g_CallHandle, myself->GetIdentity());
g_CallHandle = 0;
ke::SafeSprintf(error, maxlength, "Could not create traceray handle type (err: %d)", err);
return false;
}
#if SOURCE_ENGINE >= SE_ORANGEBOX
g_pCVar = icvar;
#endif
CONVAR_REGISTER(this);
SH_ADD_HOOK(IServerGameDLL, LevelInit, gamedll, SH_MEMBER(this, &SDKTools::LevelInit), true);
playerhelpers->RegisterCommandTargetProcessor(this);
MathLib_Init(2.2f, 2.2f, 0.0f, 2);
spengine = g_pSM->GetScriptingEngine();
plsys->AddPluginsListener(&g_OutputManager);
CDetourManager::Init(g_pSM->GetScriptingEngine(), g_pGameConf);
g_OutputManager.Init();
VoiceInit();
GetIServer();
GameRulesNativesInit();
InitSDKToolsAPI();
#if SOURCE_ENGINE == SE_CSGO
m_bFollowCSGOServerGuidelines = true;
const char *pszValue = g_pSM->GetCoreConfigValue("FollowCSGOServerGuidelines");
if (pszValue && strcasecmp(pszValue, "no") == 0)
{
m_bFollowCSGOServerGuidelines = false;
}
m_CSGOBadList.init();
m_CSGOBadList.add("m_bIsValveDS");
m_CSGOBadList.add("m_bIsQuestEligible");
g_WriteBaselinesDetour = DETOUR_CREATE_MEMBER(CNetworkStringTableContainer__WriteBaselines, "WriteBaselines");
if (g_WriteBaselinesDetour) {
g_WriteBaselinesDetour->EnableDetour();
} else {
g_pSM->LogError(myself, "Failed to find WriteBaselines signature -- stringtable error workaround disabled.");
}
#endif
return true;
}
void SDKTools::OnHandleDestroy(HandleType_t type, void *object)
{
if (type == g_CallHandle)
{
ValveCall *v = (ValveCall *)object;
delete v;
}
else if (type == g_TraceHandle)
{
trace_t *tr = (trace_t *)object;
delete tr;
}
}
void SDKTools::SDK_OnUnload()
{
SourceHook::List<ValveCall *>::iterator iter;
for (iter = g_RegCalls.begin();
iter != g_RegCalls.end();
iter++)
{
delete (*iter);
}
g_RegCalls.clear();
ShutdownHelpers();
#if SOURCE_ENGINE == SE_CSGO
if (g_WriteBaselinesDetour) {
g_WriteBaselinesDetour->DisableDetour();
g_WriteBaselinesDetour = NULL;
}
#endif
if (g_pAcceptInput)
{
g_pAcceptInput->Destroy();
g_pAcceptInput = NULL;
}
g_TEManager.Shutdown();
s_TempEntHooks.Shutdown();
s_SoundHooks.Shutdown();
g_Hooks.Shutdown();
g_OutputManager.Shutdown();
gameconfs->CloseGameConfigFile(g_pGameConf);
playerhelpers->RemoveClientListener(&g_SdkTools);
playerhelpers->UnregisterCommandTargetProcessor(this);
plsys->RemovePluginsListener(&g_OutputManager);
SH_REMOVE_HOOK(IServerGameDLL, LevelInit, gamedll, SH_MEMBER(this, &SDKTools::LevelInit), true);
if (enginePatch)
{
SH_RELEASE_CALLCLASS(enginePatch);
enginePatch = NULL;
}
if (enginesoundPatch)
{
SH_RELEASE_CALLCLASS(enginesoundPatch);
enginesoundPatch = NULL;
}
bool err;
if (g_CallHandle != 0)
{
if ((err = handlesys->RemoveType(g_CallHandle, myself->GetIdentity())) != true)
{
g_pSM->LogError(myself, "Could not remove call handle (type=%x, err=%d)", g_CallHandle, err);
}
}
if (g_TraceHandle != 0)
{
if ((err = handlesys->RemoveType(g_TraceHandle, myself->GetIdentity())) != true)
{
g_pSM->LogError(myself, "Could not remove trace handle (type=%x, err=%d)", g_TraceHandle, err);
}
}
}
bool SDKTools::SDK_OnMetamodLoad(ISmmAPI *ismm, char *error, size_t maxlen, bool late)
{
GET_V_IFACE_ANY(GetServerFactory, gameents, IServerGameEnts, INTERFACEVERSION_SERVERGAMEENTS);
GET_V_IFACE_ANY(GetEngineFactory, engsound, IEngineSound, IENGINESOUND_SERVER_INTERFACE_VERSION);
GET_V_IFACE_ANY(GetEngineFactory, enginetrace, IEngineTrace, INTERFACEVERSION_ENGINETRACE_SERVER);
GET_V_IFACE_ANY(GetEngineFactory, partition, ISpatialPartition, INTERFACEVERSION_SPATIALPARTITION);
GET_V_IFACE_ANY(GetEngineFactory, netstringtables, INetworkStringTableContainer, INTERFACENAME_NETWORKSTRINGTABLESERVER);
GET_V_IFACE_ANY(GetEngineFactory, pluginhelpers, IServerPluginHelpers, INTERFACEVERSION_ISERVERPLUGINHELPERS);
GET_V_IFACE_ANY(GetServerFactory, serverClients, IServerGameClients, INTERFACEVERSION_SERVERGAMECLIENTS);
GET_V_IFACE_ANY(GetEngineFactory, voiceserver, IVoiceServer, INTERFACEVERSION_VOICESERVER);
GET_V_IFACE_ANY(GetServerFactory, playerinfomngr, IPlayerInfoManager, INTERFACEVERSION_PLAYERINFOMANAGER);
GET_V_IFACE_CURRENT(GetEngineFactory, icvar, ICvar, CVAR_INTERFACE_VERSION);
GET_V_IFACE_CURRENT(GetFileSystemFactory, basefilesystem, IBaseFileSystem, BASEFILESYSTEM_INTERFACE_VERSION);
#if SOURCE_ENGINE >= SE_ORANGEBOX
GET_V_IFACE_ANY(GetServerFactory, servertools, IServerTools, VSERVERTOOLS_INTERFACE_VERSION);
#endif
GET_V_IFACE_ANY(GetEngineFactory, soundemitterbase, ISoundEmitterSystemBase, SOUNDEMITTERSYSTEM_INTERFACE_VERSION);
#if SOURCE_ENGINE == SE_CSS || SOURCE_ENGINE == SE_CSGO
SH_ADD_HOOK(IVEngineServer, ClientCommand, engine, SH_MEMBER(this, &SDKTools::OnSendClientCommand), false);
#endif
gpGlobals = ismm->GetCGlobals();
enginePatch = SH_GET_CALLCLASS(engine);
enginesoundPatch = SH_GET_CALLCLASS(engsound);
return true;
}
bool SDKTools::SDK_OnMetamodUnload(char *error, size_t maxlen)
{
#if SOURCE_ENGINE == SE_CSS || SOURCE_ENGINE == SE_CSGO
SH_REMOVE_HOOK(IVEngineServer, ClientCommand, engine, SH_MEMBER(this, &SDKTools::OnSendClientCommand), false);
#endif
return true;
}
void SDKTools::SDK_OnAllLoaded()
{
SM_GET_LATE_IFACE(BINTOOLS, g_pBinTools);
if (!g_pBinTools)
{
return;
}
g_TEManager.Initialize();
s_TempEntHooks.Initialize();
s_SoundHooks.Initialize();
g_Hooks.Initialize();
InitializeValveGlobals();
}
void SDKTools::OnCoreMapStart(edict_t *pEdictList, int edictCount, int clientMax)
{
InitTeamNatives();
GetResourceEntity();
g_Hooks.OnMapStart();
}
bool SDKTools::QueryRunning(char *error, size_t maxlength)
{
SM_CHECK_IFACE(BINTOOLS, g_pBinTools);
return true;
}
bool SDKTools::QueryInterfaceDrop(SMInterface *pInterface)
{
if (pInterface == g_pBinTools)
{
return false;
}
return IExtensionInterface::QueryInterfaceDrop(pInterface);
}
void SDKTools::NotifyInterfaceDrop(SMInterface *pInterface)
{
SourceHook::List<ValveCall *>::iterator iter;
for (iter = g_RegCalls.begin();
iter != g_RegCalls.end();
iter++)
{
delete (*iter);
}
g_RegCalls.clear();
ShutdownHelpers();
g_TEManager.Shutdown();
s_TempEntHooks.Shutdown();
if (g_pAcceptInput)
{
g_pAcceptInput->Destroy();
g_pAcceptInput = NULL;
}
}
bool SDKTools::RegisterConCommandBase(ConCommandBase *pVar)
{
return g_SMAPI->RegisterConCommandBase(g_PLAPI, pVar);
}
bool SDKTools::LevelInit(char const *pMapName, char const *pMapEntities, char const *pOldLevel, char const *pLandmarkName, bool loadGame, bool background)
{
m_bAnyLevelInited = true;
UpdateValveGlobals();
const char *name;
char key[32];
int count, n = 1;
if (!(name=g_pGameConf->GetKeyValue("SlapSoundCount")))
{
RETURN_META_VALUE(MRES_IGNORED, true);
}
count = atoi(name);
while (n <= count)
{
ke::SafeSprintf(key, sizeof(key), "SlapSound%d", n);
if ((name=g_pGameConf->GetKeyValue(key)))
{
engsound->PrecacheSound(name, true);
}
n++;
}
RETURN_META_VALUE(MRES_IGNORED, true);
}
bool SDKTools::ProcessCommandTarget(cmd_target_info_t *info)
{
IGamePlayer *pAdmin = info->admin ? playerhelpers->GetGamePlayer(info->admin) : NULL;
if (strcmp(info->pattern, "@aim") == 0)
{
/* The server can't aim, of course. */
if (pAdmin == NULL)
{
return false;
}
int player_index;
if ((player_index = GetClientAimTarget(pAdmin->GetEdict(), true)) < 1)
{
info->reason = COMMAND_TARGET_NONE;
info->num_targets = 0;
return true;
}
IGamePlayer *pTarget = playerhelpers->GetGamePlayer(player_index);
if (pTarget == NULL)
{
info->reason = COMMAND_TARGET_NONE;
info->num_targets = 0;
return true;
}
info->reason = playerhelpers->FilterCommandTarget(pAdmin, pTarget, info->flags);
if (info->reason != COMMAND_TARGET_VALID)
{
info->num_targets = 0;
return true;
}
info->targets[0] = player_index;
info->num_targets = 1;
info->reason = COMMAND_TARGET_VALID;
info->target_name_style = COMMAND_TARGETNAME_RAW;
ke::SafeStrcpy(info->target_name, info->target_name_maxlength, pTarget->GetName());
return true;
}
else if (strcmp(info->pattern, "@spec") == 0)
{
const char *teamname = tools_GetTeamName(1);
if (strcasecmp(teamname, "spectator") != 0)
return false;
info->num_targets = 0;
for (int i = 1; i <= playerhelpers->GetMaxClients(); i++)
{
IGamePlayer *player = playerhelpers->GetGamePlayer(i);
if (player == NULL || !player->IsInGame() || player->IsSourceTV() || player->IsReplay())
continue;
IPlayerInfo *plinfo = player->GetPlayerInfo();
if (plinfo == NULL)
continue;
if (plinfo->GetTeamIndex() == 1 &&
playerhelpers->FilterCommandTarget(pAdmin, player, info->flags) ==
COMMAND_TARGET_VALID)
{
info->targets[info->num_targets++] = i;
}
}
info->reason = info->num_targets > 0 ? COMMAND_TARGET_VALID : COMMAND_TARGET_EMPTY_FILTER;
info->target_name_style = COMMAND_TARGETNAME_ML;
ke::SafeStrcpy(info->target_name, info->target_name_maxlength, "all spectators");
return true;
}
return false;
}
const char *SDKTools::GetExtensionVerString()
{
return SOURCEMOD_VERSION;
}
const char *SDKTools::GetExtensionDateString()
{
return SOURCEMOD_BUILD_TIME;
}
bool SDKTools::InterceptClientConnect(int client, char *error, size_t maxlength)
{
g_Hooks.OnClientConnect(client);
return true;
}
#if SOURCE_ENGINE == SE_CSS || SOURCE_ENGINE == SE_CSGO
void SDKTools::OnSendClientCommand(edict_t *pPlayer, const char *szFormat)
{
// Due to legacy code, CS:S and CS:GO still sends "name \"newname\"" to the
// client after aname change. The engine has a change hook on name causing
// it to reset to the player's Steam name. This quashes that to make
// SetClientName work properly.
if (!strncmp(szFormat, "name ", 5))
{
RETURN_META(MRES_SUPERCEDE);
}
RETURN_META(MRES_IGNORED);
}
#endif
void SDKTools::OnClientPutInServer(int client)
{
g_Hooks.OnClientPutInServer(client);
}
class SDKTools_API : public ISDKTools
{
public:
virtual const char *GetInterfaceName()
{
return SMINTERFACE_SDKTOOLS_NAME;
}
virtual unsigned int GetInterfaceVersion()
{
return SMINTERFACE_SDKTOOLS_VERSION;
}
virtual IServer *GetIServer()
{
return iserver;
}
virtual void *GetGameRules()
{
return GameRules();
}
} g_SDKTools_API;
static void InitSDKToolsAPI()
{
g_pSDKTools = &g_SDKTools_API;
sharesys->AddInterface(myself, g_pSDKTools);
}