core/ConsoleDetours.cpp
author David Anderson <dvander@alliedmods.net>
Fri Oct 02 03:47:42 2009 -0700 (2009-10-02)
changeset 2815 2c7137f077fe
parent 2807 973290878e2f
child 2841 dde3dd0b4b8b
permissions -rw-r--r--
Re-enabled PDB files.
     1 /**
     2  * vim: set ts=4 sw=4 tw=99 noet :
     3  * =============================================================================
     4  * SourceMod
     5  * Copyright (C) 2004-2009 AlliedModders LLC.  All rights reserved.
     6  * =============================================================================
     7  *
     8  * This program is free software; you can redistribute it and/or modify it under
     9  * the terms of the GNU General Public License, version 3.0, as published by the
    10  * Free Software Foundation.
    11  *
    12  * This program is distributed in the hope that it will be useful, but WITHOUT
    13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    14  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    15  * details.
    16  *
    17  * You should have received a copy of the GNU General Public License along with
    18  * this program.  If not, see <http://www.gnu.org/licenses/>.
    19  *
    20  * As a special exception, AlliedModders LLC gives you permission to link the
    21  * code of this program (as well as its derivative works) to "Half-Life 2," the
    22  * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
    23  * by the Valve Corporation.  You must obey the GNU General Public License in
    24  * all respects for all other code used.  Additionally, AlliedModders LLC grants
    25  * this exception to all derivative works.  AlliedModders LLC defines further
    26  * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
    27  * or <http://www.sourcemod.net/license.php>.
    28  *
    29  * Version: $Id$
    30  */
    31 
    32 /**
    33  * On SourceHook v4.3 or lower, there are no DVP hooks. Very sad, right?
    34  * Only do this on newer version. For the older code, we'll do an incredibly
    35  * hacky detour.
    36  *
    37  * The idea of the "non-hacky" (yeah... no) code is that every unique
    38  * ConCommand vtable gets its own DVP hook. We watch for unloading and
    39  * loading commands to remove stale hooks from SH.
    40  */
    41 
    42 #include "sourcemod.h"
    43 #include "sourcemm_api.h"
    44 #include "Logger.h"
    45 #include "compat_wrappers.h"
    46 #include "ConsoleDetours.h"
    47 #include "GameConfigs.h"
    48 #include "sm_stringutil.h"
    49 #include "ConCmdManager.h"
    50 #include "HalfLife2.h"
    51 #include "ConCommandBaseIterator.h"
    52 
    53 #if defined PLATFORM_LINUX
    54 # include <dlfcn.h>
    55 # include <sys/mman.h>
    56 # include <stdint.h>
    57 # include <unistd.h>
    58 #endif
    59 
    60 #if SH_IMPL_VERSION >= 5
    61 # if SOURCE_ENGINE >= SE_ORANGEBOX
    62 	SH_DECL_EXTERN1_void(ConCommand, Dispatch, SH_NOATTRIB, false, const CCommand &);
    63 # else
    64 	SH_DECL_EXTERN0_void(ConCommand, Dispatch, SH_NOATTRIB, false);
    65 # endif
    66 #elif SH_IMPL_VERSION == 3
    67 extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
    68 extern bool __SourceHook_FHRemoveConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
    69 #endif
    70 
    71 #if SH_IMPL_VERSION >= 4
    72 
    73 class GenericCommandHooker : public IConCommandLinkListener
    74 {
    75 	struct HackInfo
    76 	{
    77 		void **vtable;
    78 		int hook;
    79 		unsigned int refcount;
    80 	};
    81 	CVector<HackInfo> vtables;
    82 	bool enabled;
    83 	SourceHook::MemFuncInfo dispatch;
    84 
    85 	inline void **GetVirtualTable(ConCommandBase *pBase)
    86 	{
    87 		return *reinterpret_cast<void***>(reinterpret_cast<char*>(pBase) +
    88 		                                  dispatch.thisptroffs +
    89 		                                  dispatch.vtbloffs);
    90 	}
    91 
    92 	inline bool FindVtable(void **ptr, size_t& index)
    93 	{
    94 		for (size_t i = 0; i < vtables.size(); i++)
    95 		{
    96 			if (vtables[i].vtable == ptr)
    97 			{
    98 				index = i;
    99 				return true;
   100 			}
   101 		}
   102 		return false;
   103 	}
   104 
   105 	void MakeHookable(ConCommandBase *pBase)
   106 	{
   107 		if (!pBase->IsCommand())
   108 			return;
   109 
   110 		ConCommand *cmd = (ConCommand*)pBase;
   111 		void **vtable = GetVirtualTable(cmd);
   112 
   113 		size_t index;
   114 		if (!FindVtable(vtable, index))
   115 		{
   116 			HackInfo hack;
   117 			hack.vtable = vtable;
   118 			hack.hook = SH_ADD_VPHOOK(ConCommand, Dispatch, cmd, SH_MEMBER(this, &GenericCommandHooker::Dispatch), false);
   119 			hack.refcount = 1;
   120 			vtables.push_back(hack);
   121 		}
   122 		else
   123 		{
   124 			vtables[index].refcount++;
   125 		}
   126 	}
   127 
   128 # if SOURCE_ENGINE >= SE_ORANGEBOX
   129 	void Dispatch(const CCommand& args)
   130 # else
   131 	void Dispatch()
   132 # endif
   133 	{
   134 		cell_t res = ConsoleDetours::Dispatch(META_IFACEPTR(ConCommand)
   135 # if SOURCE_ENGINE >= SE_ORANGEBOX
   136 			, args
   137 # endif
   138 			);
   139 		if (res >= Pl_Handled)
   140 			RETURN_META(MRES_SUPERCEDE);
   141 	}
   142 
   143 	void ReparseCommandList()
   144 	{
   145 		for (size_t i = 0; i < vtables.size(); i++)
   146 			vtables[i].refcount = 0;
   147 		for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
   148 			MakeHookable(iter.Get());
   149 		CVector<HackInfo>::iterator iter = vtables.begin();
   150 		while (iter != vtables.end())
   151 		{
   152 			if ((*iter).refcount)
   153 				continue;
   154 			/* Damn it. This event happens AFTER the plugin has unloaded!
   155 			 * There's two options. Remove the hook now and hope SH's memory
   156 			 * protection will prevent a crash. Otherwise, we can wait until
   157 			 * the server shuts down and more likely crash then.
   158 			 *
   159 			 * This situation only arises if:
   160 			 * 1) Someone has used AddCommandFilter()
   161 			 * 2) ... on a Dark Messiah server (mm:s new api)
   162 			 * 3) ... and another MM:S plugin that uses ConCommands has unloaded.
   163 			 *
   164 			 * Even though the impact is really small, we'll wait until the
   165 			 * server shuts down, so normal operation isn't interrupted.
   166 			 *
   167 			 * See bug 4018.
   168 			 */
   169 			iter = vtables.erase(iter);
   170 		}
   171 	}
   172 
   173 	void UnhookCommand(ConCommandBase *pBase)
   174 	{
   175 		if (!pBase->IsCommand())
   176 			return;
   177 
   178 		ConCommand *cmd = (ConCommand*)pBase;
   179 		void **vtable = GetVirtualTable(cmd);
   180 
   181 		size_t index;
   182 		if (!FindVtable(vtable, index))
   183 		{
   184 			g_Logger.LogError("Console detour tried to unhook command \"%s\" but it wasn't found", pBase->GetName());
   185 			return;
   186 		}
   187 
   188 		assert(vtables[index].refcount > 0);
   189 		vtables[index].refcount--;
   190 		if (vtables[index].refcount == 0)
   191 		{
   192 			SH_REMOVE_HOOK_ID(vtables[index].hook);
   193 			vtables.erase(vtables.iterAt(index));
   194 		}
   195 	}
   196 
   197 public:
   198 	GenericCommandHooker() : enabled(false)
   199 	{
   200 	}
   201 
   202 	bool Enable()
   203 	{
   204 		SourceHook::GetFuncInfo(&ConCommand::Dispatch, dispatch);
   205 
   206 		if (dispatch.thisptroffs < 0)
   207 		{
   208 			g_Logger.LogError("Command filter could not determine ConCommand layout");
   209 			return false;
   210 		}
   211 
   212 		for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
   213 			MakeHookable(iter.Get());
   214 
   215 		if (!vtables.size())
   216 		{
   217 			g_Logger.LogError("Command filter could not find any cvars!");
   218 			return false;
   219 		}
   220 
   221 		enabled = true;
   222 
   223 		return true;
   224 	}
   225 
   226 	void Disable()
   227 	{
   228 		for (size_t i = 0; i < vtables.size(); i++)
   229 			SH_REMOVE_HOOK_ID(vtables[i].hook);
   230 		vtables.clear();
   231 	}
   232 
   233 	virtual void OnLinkConCommand(ConCommandBase *pBase)
   234 	{
   235 		if (!enabled)
   236 			return;
   237 		MakeHookable(pBase);
   238 	}
   239 
   240 	virtual void OnUnlinkConCommand(ConCommandBase *pBase)
   241 	{
   242 		if (!enabled)
   243 			return;
   244 		if (pBase == NULL)
   245 			ReparseCommandList();
   246 		else
   247 			UnhookCommand(pBase);
   248 	}
   249 };
   250 
   251 /**
   252  * END DVP HOOK VERSION
   253  */
   254 
   255 #else /* SH_IMPL_VERSION >= 4 */
   256 
   257 /**
   258  * BEGIN ENGINE DETOUR VERSION
   259  */
   260 
   261 # include <jit/jit_helpers.h>
   262 # include <jit/x86/x86_macros.h>
   263 
   264 class GenericCommandHooker
   265 {
   266 	struct Patch
   267 	{
   268 		Patch() : applied(false)
   269 		{
   270 		}
   271 
   272 		bool applied;
   273 		void *base;
   274 		unsigned char search[16];
   275 		size_t search_len;
   276 		size_t offset;
   277 		size_t saveat;
   278 		int regparam;
   279 		void *detour;
   280 	};
   281 	
   282 	Patch ces;
   283 	Patch cgc;
   284 
   285 public:
   286 	bool Enable()
   287 	{
   288 		const char *platform = NULL;
   289 # if defined(PLATFORM_WINDOWS)
   290 		platform = "Windows";
   291 # else	
   292 		void *addrInBase = (void *)g_SMAPI->GetEngineFactory(false);
   293 		Dl_info info;
   294 		if (!dladdr(addrInBase, &info))
   295 		{
   296 			g_Logger.LogError("Command filter could not find engine name");
   297 			return false;
   298 		}
   299 		if (strstr(info.dli_fname, "engine_i486"))
   300 		{
   301 			platform = "Linux_486";
   302 		}
   303 		else if (strstr(info.dli_fname, "engine_i686"))
   304 		{
   305 			platform = "Linux_686";
   306 		}
   307 		else if (strstr(info.dli_fname, "engine_amd"))
   308 		{
   309 			platform = "Linux_AMD";
   310 		}
   311 		else
   312 		{
   313 			g_Logger.LogError("Command filter could not determine engine (%s)", info.dli_fname);
   314 			return false;
   315 		}
   316 # endif
   317 
   318 		if (!PrepPatch("Cmd_ExecuteString", "CES", platform, &ces))
   319 			return false;
   320 		if (!PrepPatch("CGameClient::ExecuteString", "CGC", platform, &cgc))
   321 			return false;
   322 
   323 		ApplyPatch(&ces);
   324 		ApplyPatch(&cgc);
   325 
   326 		return true;
   327 	}
   328 
   329 	void Disable()
   330 	{
   331 		UndoPatch(&ces);
   332 		UndoPatch(&cgc);
   333 	}
   334 
   335 private:
   336 
   337 # if !defined PLATFORM_WINDOWS
   338 #  define PAGE_READWRITE     PROT_READ|PROT_WRITE
   339 #  define PAGE_EXECUTE_READ  PROT_READ|PROT_EXEC
   340 
   341 	static inline uintptr_t AddrToPage(uintptr_t address)
   342 	{
   343 		return (address & ~(uintptr_t(sysconf(_SC_PAGE_SIZE) - 1)));
   344 	}
   345 
   346 # endif
   347 
   348 	void Protect(void *addr, size_t length, int prot)
   349 	{
   350 # if defined PLATFORM_WINDOWS
   351 		DWORD ignore;
   352 		VirtualProtect(addr, length, prot, &ignore);
   353 # else
   354 		uintptr_t startPage = AddrToPage(uintptr_t(addr));
   355 		length += (uintptr_t(addr) - startPage);
   356 		mprotect((void*)startPage, length, prot);
   357 # endif
   358 	}
   359 
   360 	void UndoPatch(Patch *patch)
   361 	{
   362 		if (!patch->applied || patch->detour == NULL)
   363 			return;
   364 
   365 		g_pSourcePawn->FreePageMemory(patch->detour);
   366 
   367 		unsigned char *source = (unsigned char *)patch->base + patch->offset;
   368 		Protect(source, patch->search_len, PAGE_READWRITE);
   369 		for (size_t i = 0; i < patch->search_len; i++)
   370 			source[i] = patch->search[i];
   371 		Protect(source, patch->search_len, PAGE_EXECUTE_READ);
   372 	}
   373 
   374 	void ApplyPatch(Patch *patch)
   375 	{
   376 		assert(!patch->applied);
   377 
   378 		size_t length = 0;
   379 		void *callback = (void*)&ConsoleDetours::Dispatch;
   380 
   381 		/* Bogus assignment to make compiler is doing the right thing. */
   382 		patch->detour = callback;
   383 
   384 		/* Assemgle the detour. */
   385 		JitWriter writer;
   386 		writer.outbase = NULL;
   387 		writer.outptr = NULL;
   388 		do
   389 		{
   390 			/* Need a specific register, or value on stack? */
   391 			if (patch->regparam != -1)
   392 				IA32_Push_Reg(&writer, patch->regparam);
   393 			/* Call real function. */
   394 			IA32_Write_Jump32_Abs(&writer, IA32_Call_Imm32(&writer, 0), callback);
   395 			/* Restore stack. */
   396 			if (patch->regparam != -1)
   397 				IA32_Pop_Reg(&writer, patch->regparam);
   398 			/* Copy any saved bytes */
   399 			if (patch->saveat)
   400 			{
   401 				for (size_t i = patch->saveat; i < patch->search_len; i++)
   402 				{
   403 					writer.write_ubyte(patch->search[i]);
   404 				}
   405 			}
   406 			/* Jump back to the caller. */
   407 			unsigned char *target = (unsigned char *)patch->base + patch->offset + patch->search_len;
   408 			IA32_Jump_Imm32_Abs(&writer, target);
   409 			/* Assemble, if we can. */
   410 			if (writer.outbase == NULL)
   411 			{
   412 				length = writer.outptr - writer.outbase;
   413 				patch->detour = g_pSourcePawn->AllocatePageMemory(length);
   414 				if (patch->detour == NULL)
   415 				{
   416 					g_Logger.LogError("Ran out of memory!");
   417 					return;
   418 				}
   419 				g_pSourcePawn->SetReadWrite(patch->detour);
   420 				writer.outbase = (jitcode_t)patch->detour;
   421 				writer.outptr = writer.outbase;
   422 			}
   423 			else
   424 			{
   425 				break;
   426 			}
   427 		} while (true);
   428 
   429 		g_pSourcePawn->SetReadExecute(patch->detour);
   430 
   431 		unsigned char *source = (unsigned char *)patch->base + patch->offset;
   432 		Protect(source, 6, PAGE_READWRITE);
   433 		source[0] = 0xFF;
   434 		source[1] = 0x25;
   435 		*(void **)&source[2] = &patch->detour;
   436 		Protect(source, 6, PAGE_EXECUTE_READ);
   437 
   438 		patch->applied = true;
   439 	}
   440 
   441 	bool PrepPatch(const char *signature, const char *name, const char *platform, Patch *patch)
   442 	{
   443 		/* Get the base address of the function. */
   444 		if (!g_pGameConf->GetMemSig(signature, &patch->base) || patch->base == NULL)
   445 		{
   446 			g_Logger.LogError("Command filter could not find signature: %s", signature);
   447 			return false;
   448 		}
   449 
   450 		const char *value;
   451 		char keyname[255];
   452 
   453 		/* Get the verification bytes that will be written over. */
   454 		UTIL_Format(keyname, sizeof(keyname), "%s_Patch_%s", name, platform);
   455 		if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
   456 		{
   457 			g_Logger.LogError("Command filter could not find key: %s", keyname);
   458 			return false;
   459 		}
   460 		patch->search_len = UTIL_DecodeHexString(patch->search, sizeof(patch->search), value);
   461 		if (patch->search_len < 6)
   462 		{
   463 			g_Logger.LogError("Error decoding %s value, or not long enough", keyname);
   464 			return false;
   465 		}
   466 
   467 		/* Get the offset into the function. */
   468 		UTIL_Format(keyname, sizeof(keyname), "%s_Offset_%s", name, platform);
   469 		if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
   470 		{
   471 			g_Logger.LogError("Command filter could not find key: %s", keyname);
   472 			return false;
   473 		}
   474 		patch->offset = atoi(value);
   475 		if (patch->offset > 20000)
   476 		{
   477 			g_Logger.LogError("Command filter %s value is bogus", keyname);
   478 			return false;
   479 		}
   480 
   481 		/* Get the number of bytes to save from what was written over. */
   482 		patch->saveat = 0;
   483 		UTIL_Format(keyname, sizeof(keyname), "%s_Save_%s", name, platform);
   484 		if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
   485 		{
   486 			patch->saveat = atoi(value);
   487 			if (patch->saveat >= patch->search_len)
   488 			{
   489 				g_Logger.LogError("Command filter %s value is too large", keyname);
   490 				return false;
   491 			}
   492 		}
   493 
   494 		/* Get register for parameter, if any. */
   495 		patch->regparam = -1;
   496 		UTIL_Format(keyname, sizeof(keyname), "%s_Reg_%s", name, platform);
   497 		if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
   498 		{
   499 			patch->regparam = atoi(value);
   500 		}
   501 
   502 		/* Everything loaded from gamedata, make sure the patch will succeed. */
   503 		unsigned char *address = (unsigned char *)patch->base + patch->offset;
   504 		for (size_t i = 0; i < patch->search_len; i++)
   505 		{
   506 			if (address[i] != patch->search[i])
   507 			{
   508 				g_Logger.LogError("Command filter %s has changed (byte %x is not %x sub-offset %d)",
   509 				                  name, address[i], patch->search[i], i);
   510 				return false;
   511 			}
   512 		}
   513 
   514 		return true;
   515 	}
   516 };
   517 
   518 static bool dummy_hook_set = false;
   519 void DummyHook()
   520 {
   521 	if (dummy_hook_set)
   522 	{
   523 		dummy_hook_set = false;
   524 		RETURN_META(MRES_SUPERCEDE);
   525 	}
   526 }
   527 
   528 #endif
   529 
   530 /**
   531  * BEGIN THE ACTUALLY GENERIC CODE.
   532  */
   533 
   534 static GenericCommandHooker s_GenericHooker;
   535 ConsoleDetours g_ConsoleDetours;
   536 
   537 ConsoleDetours::ConsoleDetours() : triedToEnable(false), isEnabled(false)
   538 {
   539 }
   540 
   541 void ConsoleDetours::OnSourceModAllInitialized()
   542 {
   543 	m_pForward = g_Forwards.CreateForwardEx("OnAnyCommand", ET_Hook, 3, NULL, Param_Cell,
   544 	                                        Param_String, Param_Cell);
   545 }
   546 
   547 void ConsoleDetours::OnSourceModShutdown()
   548 {
   549 	List<Listener*>::iterator iter = m_Listeners.begin();
   550 	while (iter != m_Listeners.end())
   551 	{
   552 		Listener *listener = (*iter);
   553 		g_Forwards.ReleaseForward(listener->forward);
   554 		delete listener;
   555 		iter = m_Listeners.erase(iter);
   556 	}
   557 
   558 	g_Forwards.ReleaseForward(m_pForward);
   559 	s_GenericHooker.Disable();
   560 }
   561 
   562 bool ConsoleDetours::IsAvailable()
   563 {
   564 	if (triedToEnable)
   565 		return isEnabled;
   566 	isEnabled = s_GenericHooker.Enable();
   567 	triedToEnable = true;
   568 	return isEnabled;
   569 }
   570 
   571 bool ConsoleDetours::AddListener(IPluginFunction *fun, const char *command)
   572 {
   573 	if (!IsAvailable())
   574 		return false;
   575 
   576 	if (command == NULL)
   577 	{
   578 		m_pForward->AddFunction(fun);
   579 	}
   580 	else
   581 	{
   582 		const char *str = UTIL_ToLowerCase(command);
   583 		Listener *listener;
   584 		Listener **plistener = m_CmdLookup.retrieve(str);
   585 		if (plistener == NULL)
   586 		{
   587 			listener = new Listener;
   588 			listener->forward = g_Forwards.CreateForwardEx(NULL, ET_Hook, 3, NULL, Param_Cell,
   589 			                                               Param_String, Param_Cell);
   590 			m_CmdLookup.insert(str, listener);
   591 		}
   592 		else
   593 		{
   594 			listener = *plistener;
   595 		}
   596 		listener->forward->AddFunction(fun);
   597 		delete [] str;
   598 	}
   599 
   600 	return true;
   601 }
   602 
   603 bool ConsoleDetours::RemoveListener(IPluginFunction *fun, const char *command)
   604 {
   605 	if (command == NULL)
   606 	{
   607 		return m_pForward->RemoveFunction(fun);
   608 	}
   609 	else
   610 	{
   611 		const char *str = UTIL_ToLowerCase(command);
   612 		Listener *listener;
   613 		Listener **plistener = m_CmdLookup.retrieve(str);
   614 		delete [] str;
   615 		if (plistener == NULL)
   616 			return false;
   617 		listener = *plistener;
   618 		return listener->forward->RemoveFunction(fun);
   619 	}
   620 }
   621 
   622 cell_t ConsoleDetours::InternalDispatch(int client, const CCommand& args)
   623 {
   624 	char name[255];
   625 	const char *realname = args.Arg(0);
   626 	size_t len = strlen(realname);
   627 	for (size_t i = 0; i < len; i++)
   628 	{
   629 		if (realname[i] >= 'A' && realname[i] <= 'Z')
   630 			name[i] = tolower(realname[i]);
   631 		else
   632 			name[i] = realname[i];
   633 	}
   634 	name[len] = '\0';
   635 
   636 	cell_t result = Pl_Continue;
   637 	m_pForward->PushCell(client);
   638 	m_pForward->PushString(name);
   639 	m_pForward->PushCell(args.ArgC() - 1);
   640 	m_pForward->Execute(&result, NULL);
   641 
   642 	/* Don't let plugins block this. */
   643 	if (strcmp(name, "sm") == 0)
   644 		result = Pl_Continue;
   645 
   646 	if (result >= Pl_Stop)
   647 		return result;
   648 	
   649 	Listener **plistener = m_CmdLookup.retrieve(name);
   650 	if (plistener == NULL)
   651 		return result;
   652 	Listener *listener = *plistener;
   653 	if (listener->forward->GetFunctionCount() == 0)
   654 		return result;
   655 
   656 	cell_t result2 = Pl_Continue;
   657 	listener->forward->PushCell(client);
   658 	listener->forward->PushString(name);
   659 	listener->forward->PushCell(args.ArgC() - 1);
   660 	listener->forward->Execute(&result2, NULL);
   661 
   662 	if (result2 > result)
   663 		result = result2;
   664 
   665 	/* "sm" should not have flown through the above. */
   666 	assert(strcmp(name, "sm") != 0 || result == Pl_Continue);
   667 
   668 	return result;
   669 }
   670 
   671 #if SOURCE_ENGINE >= SE_ORANGEBOX
   672 cell_t ConsoleDetours::Dispatch(ConCommand *pBase, const CCommand& args)
   673 {
   674 #else
   675 cell_t ConsoleDetours::Dispatch(ConCommand *pBase)
   676 {
   677 	CCommand args;
   678 #endif
   679 	g_HL2.PushCommandStack(&args);
   680 	cell_t res = g_ConsoleDetours.InternalDispatch(g_ConCmds.GetCommandClient(), args);
   681 	g_HL2.PopCommandStack();
   682 
   683 #if SH_IMPL_VERSION < 4
   684 	if (res >= Pl_Handled)
   685 	{
   686 		/* See bug 4020 - we can't optimize this because looking at the vtable
   687 		 * is probably more expensive, since there's no SH_GET_ORIG_VFNPTR_ENTRY.
   688 		 */
   689 		SH_ADD_HOOK_STATICFUNC(ConCommand, Dispatch, pBase, DummyHook, false);
   690 		dummy_hook_set = true;
   691 		pBase->Dispatch();
   692 		SH_REMOVE_HOOK_STATICFUNC(ConCommand, Dispatch, pBase, DummyHook, false);
   693 	}
   694 	else
   695 	{
   696 		/* Make sure the command gets invoked. See bug 4019 on making this better. */
   697 		pBase->Dispatch();
   698 	}
   699 #endif
   700 
   701 	return res;
   702 }