core/ConsoleDetours.cpp
author Nicholas Hastings <skamonkey@gmail.com>
Wed Jul 31 18:42:15 2013 -0400 (2013-07-31)
changeset 3962 ba85947783be
parent 3942 7aaa00382627
child 4055 ed3aad0ec4cf
permissions -rw-r--r--
Build against MM:S 1.10.
     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 versions. 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 <IGameConfigs.h>
    48 #include "sm_stringutil.h"
    49 #include "ConCmdManager.h"
    50 #include "HalfLife2.h"
    51 #include "ConCommandBaseIterator.h"
    52 #include "logic_bridge.h"
    53 
    54 #if defined PLATFORM_POSIX
    55 # include <dlfcn.h>
    56 # include <sys/mman.h>
    57 # include <stdint.h>
    58 # include <unistd.h>
    59 #endif
    60 
    61 #if SH_IMPL_VERSION >= 5
    62 # if SOURCE_ENGINE == SE_DOTA
    63 	SH_DECL_EXTERN2_void(ConCommand, Dispatch, SH_NOATTRIB, false, const CCommandContext &, const CCommand &);
    64 # elif SOURCE_ENGINE >= SE_ORANGEBOX
    65 	SH_DECL_EXTERN1_void(ConCommand, Dispatch, SH_NOATTRIB, false, const CCommand &);
    66 # else
    67 	SH_DECL_EXTERN0_void(ConCommand, Dispatch, SH_NOATTRIB, false);
    68 # endif
    69 #else
    70 # if SH_IMPL_VERSION >= 4
    71  extern int __SourceHook_FHVPAddConCommandDispatch(void *,bool,class fastdelegate::FastDelegate0<void>,bool);
    72  extern int __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
    73 # else
    74  extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
    75 # endif
    76 extern bool __SourceHook_FHRemoveConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
    77 #endif
    78 
    79 #if SH_IMPL_VERSION >= 4
    80 
    81 class GenericCommandHooker : public IConCommandLinkListener
    82 {
    83 	struct HackInfo
    84 	{
    85 		void **vtable;
    86 		int hook;
    87 		unsigned int refcount;
    88 	};
    89 	CVector<HackInfo> vtables;
    90 	bool enabled;
    91 	SourceHook::MemFuncInfo dispatch;
    92 
    93 	inline void **GetVirtualTable(ConCommandBase *pBase)
    94 	{
    95 		return *reinterpret_cast<void***>(reinterpret_cast<char*>(pBase) +
    96 		                                  dispatch.thisptroffs +
    97 		                                  dispatch.vtbloffs);
    98 	}
    99 
   100 	inline bool FindVtable(void **ptr, size_t& index)
   101 	{
   102 		for (size_t i = 0; i < vtables.size(); i++)
   103 		{
   104 			if (vtables[i].vtable == ptr)
   105 			{
   106 				index = i;
   107 				return true;
   108 			}
   109 		}
   110 		return false;
   111 	}
   112 
   113 	void MakeHookable(ConCommandBase *pBase)
   114 	{
   115 		if (!pBase->IsCommand())
   116 			return;
   117 
   118 		ConCommand *cmd = (ConCommand*)pBase;
   119 		void **vtable = GetVirtualTable(cmd);
   120 
   121 		size_t index;
   122 		if (!FindVtable(vtable, index))
   123 		{
   124 			HackInfo hack;
   125 			hack.vtable = vtable;
   126 			hack.hook = SH_ADD_VPHOOK(ConCommand, Dispatch, cmd, SH_MEMBER(this, &GenericCommandHooker::Dispatch), false);
   127 			hack.refcount = 1;
   128 			vtables.push_back(hack);
   129 		}
   130 		else
   131 		{
   132 			vtables[index].refcount++;
   133 		}
   134 	}
   135 
   136 # if SOURCE_ENGINE == SE_DOTA
   137 	void Dispatch(const CCommandContext &context, const CCommand& args)
   138 # elif SOURCE_ENGINE >= SE_ORANGEBOX
   139 	void Dispatch(const CCommand& args)
   140 # else
   141 	void Dispatch()
   142 # endif
   143 	{
   144 		cell_t res = ConsoleDetours::Dispatch(META_IFACEPTR(ConCommand)
   145 # if SOURCE_ENGINE >= SE_ORANGEBOX
   146 			, args
   147 # endif
   148 			);
   149 		if (res >= Pl_Handled)
   150 			RETURN_META(MRES_SUPERCEDE);
   151 	}
   152 
   153 	void ReparseCommandList()
   154 	{
   155 		for (size_t i = 0; i < vtables.size(); i++)
   156 			vtables[i].refcount = 0;
   157 		for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
   158 			MakeHookable(iter.Get());
   159 		CVector<HackInfo>::iterator iter = vtables.begin();
   160 		while (iter != vtables.end())
   161 		{
   162 			if ((*iter).refcount)
   163 			{
   164 				iter++;
   165 				continue;
   166 			}
   167 			/* Damn it. This event happens AFTER the plugin has unloaded!
   168 			 * There's two options. Remove the hook now and hope SH's memory
   169 			 * protection will prevent a crash. Otherwise, we can wait until
   170 			 * the server shuts down and more likely crash then.
   171 			 *
   172 			 * This situation only arises if:
   173 			 * 1) Someone has used AddCommandFilter()
   174 			 * 2) ... on a Dark Messiah server (mm:s new api)
   175 			 * 3) ... and another MM:S plugin that uses ConCommands has unloaded.
   176 			 *
   177 			 * Even though the impact is really small, we'll wait until the
   178 			 * server shuts down, so normal operation isn't interrupted.
   179 			 *
   180 			 * See bug 4018.
   181 			 */
   182 			iter = vtables.erase(iter);
   183 		}
   184 	}
   185 
   186 	void UnhookCommand(ConCommandBase *pBase)
   187 	{
   188 		if (!pBase->IsCommand())
   189 			return;
   190 
   191 		ConCommand *cmd = (ConCommand*)pBase;
   192 		void **vtable = GetVirtualTable(cmd);
   193 
   194 		size_t index;
   195 		if (!FindVtable(vtable, index))
   196 		{
   197 			g_Logger.LogError("Console detour tried to unhook command \"%s\" but it wasn't found", pBase->GetName());
   198 			return;
   199 		}
   200 
   201 		assert(vtables[index].refcount > 0);
   202 		vtables[index].refcount--;
   203 		if (vtables[index].refcount == 0)
   204 		{
   205 			SH_REMOVE_HOOK_ID(vtables[index].hook);
   206 			vtables.erase(vtables.iterAt(index));
   207 		}
   208 	}
   209 
   210 public:
   211 	GenericCommandHooker() : enabled(false)
   212 	{
   213 	}
   214 
   215 	bool Enable()
   216 	{
   217 		SourceHook::GetFuncInfo(&ConCommand::Dispatch, dispatch);
   218 
   219 		if (dispatch.thisptroffs < 0)
   220 		{
   221 			g_Logger.LogError("Command filter could not determine ConCommand layout");
   222 			return false;
   223 		}
   224 
   225 		for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
   226 			MakeHookable(iter.Get());
   227 
   228 		if (!vtables.size())
   229 		{
   230 			g_Logger.LogError("Command filter could not find any cvars!");
   231 			return false;
   232 		}
   233 
   234 		enabled = true;
   235 
   236 		return true;
   237 	}
   238 
   239 	void Disable()
   240 	{
   241 		for (size_t i = 0; i < vtables.size(); i++)
   242 			SH_REMOVE_HOOK_ID(vtables[i].hook);
   243 		vtables.clear();
   244 	}
   245 
   246 	virtual void OnLinkConCommand(ConCommandBase *pBase)
   247 	{
   248 		if (!enabled)
   249 			return;
   250 		MakeHookable(pBase);
   251 	}
   252 
   253 	virtual void OnUnlinkConCommand(ConCommandBase *pBase)
   254 	{
   255 		if (!enabled)
   256 			return;
   257 		if (pBase == NULL)
   258 			ReparseCommandList();
   259 		else
   260 			UnhookCommand(pBase);
   261 	}
   262 };
   263 
   264 /**
   265  * END DVP HOOK VERSION
   266  */
   267 
   268 #else /* SH_IMPL_VERSION >= 4 */
   269 
   270 /**
   271  * BEGIN ENGINE DETOUR VERSION
   272  */
   273 
   274 # include <jit/jit_helpers.h>
   275 # include <jit/x86/x86_macros.h>
   276 
   277 class GenericCommandHooker
   278 {
   279 	struct Patch
   280 	{
   281 		Patch() : applied(false)
   282 		{
   283 		}
   284 
   285 		bool applied;
   286 		void *base;
   287 		unsigned char search[16];
   288 		size_t search_len;
   289 		size_t offset;
   290 		size_t saveat;
   291 		int regparam;
   292 		void *detour;
   293 	};
   294 	
   295 	Patch ces;
   296 	Patch cgc;
   297 
   298 public:
   299  	static void DelayedActivation(void *inparam)
   300  	{
   301  		GenericCommandHooker *cdtrs = reinterpret_cast<GenericCommandHooker*>(inparam);
   302  		/* Safe to re-enter because the frame queue is lock+swapped. */
   303  		if ((!cdtrs->ces.applied || !cdtrs->cgc.applied) &&
   304  		    g_HL2.PeekCommandStack() != NULL)
   305  		{
   306  			g_SourceMod.AddFrameAction(GenericCommandHooker::DelayedActivation, cdtrs);
   307  			return;
   308  		}
   309  
   310  		if (!cdtrs->ces.applied)
   311  			cdtrs->ApplyPatch(&cdtrs->ces);
   312  		if (!cdtrs->cgc.applied)
   313  			cdtrs->ApplyPatch(&cdtrs->cgc);
   314  	}
   315 
   316 	bool Enable()
   317 	{
   318 		const char *platform = NULL;
   319 # if defined(PLATFORM_WINDOWS)
   320 		platform = "Windows";
   321 # else	
   322 		void *addrInBase = (void *)g_SMAPI->GetEngineFactory(false);
   323 		Dl_info info;
   324 		if (!dladdr(addrInBase, &info))
   325 		{
   326 			g_Logger.LogError("Command filter could not find engine name");
   327 			return false;
   328 		}
   329 		if (strstr(info.dli_fname, "engine_i486"))
   330 		{
   331 			platform = "Linux_486";
   332 		}
   333 		else if (strstr(info.dli_fname, "engine_i686"))
   334 		{
   335 			platform = "Linux_686";
   336 		}
   337 		else if (strstr(info.dli_fname, "engine_amd"))
   338 		{
   339 			platform = "Linux_AMD";
   340 		}
   341 		else
   342 		{
   343 			g_Logger.LogError("Command filter could not determine engine (%s)", info.dli_fname);
   344 			return false;
   345 		}
   346 # endif
   347 
   348 		if (!PrepPatch("Cmd_ExecuteString", "CES", platform, &ces))
   349 			return false;
   350 		if (!PrepPatch("CGameClient::ExecuteString", "CGC", platform, &cgc))
   351 			return false;
   352 
   353  		if (g_HL2.PeekCommandStack() != NULL)
   354  		{
   355  			g_SourceMod.AddFrameAction(GenericCommandHooker::DelayedActivation, this);
   356  			return true;
   357  		}
   358 
   359 		ApplyPatch(&ces);
   360 		ApplyPatch(&cgc);
   361 
   362 		return true;
   363 	}
   364 
   365 	void Disable()
   366 	{
   367 		UndoPatch(&ces);
   368 		UndoPatch(&cgc);
   369 	}
   370 
   371 private:
   372 
   373 # if !defined PLATFORM_WINDOWS
   374 #  define PAGE_READWRITE     PROT_READ|PROT_WRITE
   375 #  define PAGE_EXECUTE_READ  PROT_READ|PROT_EXEC
   376 
   377 	static inline uintptr_t AddrToPage(uintptr_t address)
   378 	{
   379 		return (address & ~(uintptr_t(sysconf(_SC_PAGE_SIZE) - 1)));
   380 	}
   381 
   382 # endif
   383 
   384 	void Protect(void *addr, size_t length, int prot)
   385 	{
   386 # if defined PLATFORM_WINDOWS
   387 		DWORD ignore;
   388 		VirtualProtect(addr, length, prot, &ignore);
   389 # else
   390 		uintptr_t startPage = AddrToPage(uintptr_t(addr));
   391 		length += (uintptr_t(addr) - startPage);
   392 		mprotect((void*)startPage, length, prot);
   393 # endif
   394 	}
   395 
   396 	void UndoPatch(Patch *patch)
   397 	{
   398 		if (!patch->applied || patch->detour == NULL)
   399 			return;
   400 
   401 		g_pSourcePawn->FreePageMemory(patch->detour);
   402 
   403 		unsigned char *source = (unsigned char *)patch->base + patch->offset;
   404 		Protect(source, patch->search_len, PAGE_READWRITE);
   405 		for (size_t i = 0; i < patch->search_len; i++)
   406 			source[i] = patch->search[i];
   407 		Protect(source, patch->search_len, PAGE_EXECUTE_READ);
   408 	}
   409 
   410 	void ApplyPatch(Patch *patch)
   411 	{
   412 		assert(!patch->applied);
   413 
   414 		size_t length = 0;
   415 		void *callback = (void*)&ConsoleDetours::Dispatch;
   416 
   417 		/* Bogus assignment to make compiler is doing the right thing. */
   418 		patch->detour = callback;
   419 
   420 		/* Assemgle the detour. */
   421 		JitWriter writer;
   422 		writer.outbase = NULL;
   423 		writer.outptr = NULL;
   424 		do
   425 		{
   426 			/* Need a specific register, or value on stack? */
   427 			if (patch->regparam != -1)
   428 				IA32_Push_Reg(&writer, patch->regparam);
   429 			/* Call real function. */
   430 			IA32_Write_Jump32_Abs(&writer, IA32_Call_Imm32(&writer, 0), callback);
   431 			/* Restore stack. */
   432 			if (patch->regparam != -1)
   433 				IA32_Pop_Reg(&writer, patch->regparam);
   434 			/* Copy any saved bytes */
   435 			if (patch->saveat)
   436 			{
   437 				for (size_t i = patch->saveat; i < patch->search_len; i++)
   438 				{
   439 					writer.write_ubyte(patch->search[i]);
   440 				}
   441 			}
   442 			/* Jump back to the caller. */
   443 			unsigned char *target = (unsigned char *)patch->base + patch->offset + patch->search_len;
   444 			IA32_Jump_Imm32_Abs(&writer, target);
   445 			/* Assemble, if we can. */
   446 			if (writer.outbase == NULL)
   447 			{
   448 				length = writer.outptr - writer.outbase;
   449 				patch->detour = g_pSourcePawn->AllocatePageMemory(length);
   450 				if (patch->detour == NULL)
   451 				{
   452 					g_Logger.LogError("Ran out of memory!");
   453 					return;
   454 				}
   455 				g_pSourcePawn->SetReadWrite(patch->detour);
   456 				writer.outbase = (jitcode_t)patch->detour;
   457 				writer.outptr = writer.outbase;
   458 			}
   459 			else
   460 			{
   461 				break;
   462 			}
   463 		} while (true);
   464 
   465 		g_pSourcePawn->SetReadExecute(patch->detour);
   466 
   467 		unsigned char *source = (unsigned char *)patch->base + patch->offset;
   468 		Protect(source, 6, PAGE_READWRITE);
   469 		source[0] = 0xFF;
   470 		source[1] = 0x25;
   471 		*(void **)&source[2] = &patch->detour;
   472 		Protect(source, 6, PAGE_EXECUTE_READ);
   473 
   474 		patch->applied = true;
   475 	}
   476 
   477 	bool PrepPatch(const char *signature, const char *name, const char *platform, Patch *patch)
   478 	{
   479 		/* Get the base address of the function. */
   480 		if (!g_pGameConf->GetMemSig(signature, &patch->base) || patch->base == NULL)
   481 		{
   482 			g_Logger.LogError("Command filter could not find signature: %s", signature);
   483 			return false;
   484 		}
   485 
   486 		const char *value;
   487 		char keyname[255];
   488 
   489 		/* Get the verification bytes that will be written over. */
   490 		UTIL_Format(keyname, sizeof(keyname), "%s_Patch_%s", name, platform);
   491 		if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
   492 		{
   493 			g_Logger.LogError("Command filter could not find key: %s", keyname);
   494 			return false;
   495 		}
   496 		patch->search_len = UTIL_DecodeHexString(patch->search, sizeof(patch->search), value);
   497 		if (patch->search_len < 6)
   498 		{
   499 			g_Logger.LogError("Error decoding %s value, or not long enough", keyname);
   500 			return false;
   501 		}
   502 
   503 		/* Get the offset into the function. */
   504 		UTIL_Format(keyname, sizeof(keyname), "%s_Offset_%s", name, platform);
   505 		if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
   506 		{
   507 			g_Logger.LogError("Command filter could not find key: %s", keyname);
   508 			return false;
   509 		}
   510 		patch->offset = atoi(value);
   511 		if (patch->offset > 20000)
   512 		{
   513 			g_Logger.LogError("Command filter %s value is bogus", keyname);
   514 			return false;
   515 		}
   516 
   517 		/* Get the number of bytes to save from what was written over. */
   518 		patch->saveat = 0;
   519 		UTIL_Format(keyname, sizeof(keyname), "%s_Save_%s", name, platform);
   520 		if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
   521 		{
   522 			patch->saveat = atoi(value);
   523 			if (patch->saveat >= patch->search_len)
   524 			{
   525 				g_Logger.LogError("Command filter %s value is too large", keyname);
   526 				return false;
   527 			}
   528 		}
   529 
   530 		/* Get register for parameter, if any. */
   531 		patch->regparam = -1;
   532 		UTIL_Format(keyname, sizeof(keyname), "%s_Reg_%s", name, platform);
   533 		if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
   534 		{
   535 			patch->regparam = atoi(value);
   536 		}
   537 
   538 		/* Everything loaded from gamedata, make sure the patch will succeed. */
   539 		unsigned char *address = (unsigned char *)patch->base + patch->offset;
   540 		for (size_t i = 0; i < patch->search_len; i++)
   541 		{
   542 			if (address[i] != patch->search[i])
   543 			{
   544 				g_Logger.LogError("Command filter %s has changed (byte %x is not %x sub-offset %d)",
   545 				                  name, address[i], patch->search[i], i);
   546 				return false;
   547 			}
   548 		}
   549 
   550 		return true;
   551 	}
   552 };
   553 
   554 static bool dummy_hook_set = false;
   555 void DummyHook()
   556 {
   557 	if (dummy_hook_set)
   558 	{
   559 		dummy_hook_set = false;
   560 		RETURN_META(MRES_SUPERCEDE);
   561 	}
   562 }
   563 
   564 #endif
   565 
   566 /**
   567  * BEGIN THE ACTUALLY GENERIC CODE.
   568  */
   569 
   570 #define FEATURECAP_COMMANDLISTENER  "command listener"
   571 
   572 static GenericCommandHooker s_GenericHooker;
   573 ConsoleDetours g_ConsoleDetours;
   574 
   575 ConsoleDetours::ConsoleDetours() : status(FeatureStatus_Unknown)
   576 {
   577 }
   578 
   579 void ConsoleDetours::OnSourceModAllInitialized()
   580 {
   581 	m_pForward = g_Forwards.CreateForwardEx("OnAnyCommand", ET_Hook, 3, NULL, Param_Cell,
   582 	                                        Param_String, Param_Cell);
   583 	sharesys->AddCapabilityProvider(NULL, this, FEATURECAP_COMMANDLISTENER);
   584 }
   585 
   586 void ConsoleDetours::OnSourceModShutdown()
   587 {
   588 	List<Listener*>::iterator iter = m_Listeners.begin();
   589 	while (iter != m_Listeners.end())
   590 	{
   591 		Listener *listener = (*iter);
   592 		g_Forwards.ReleaseForward(listener->forward);
   593 		delete listener;
   594 		iter = m_Listeners.erase(iter);
   595 	}
   596 
   597 	g_Forwards.ReleaseForward(m_pForward);
   598 	s_GenericHooker.Disable();
   599 }
   600 
   601 FeatureStatus ConsoleDetours::GetFeatureStatus(FeatureType type, const char *name)
   602 {
   603 	return GetStatus();
   604 }
   605 
   606 FeatureStatus ConsoleDetours::GetStatus()
   607 {
   608 	if (status == FeatureStatus_Unknown)
   609 	{
   610 		status = s_GenericHooker.Enable() ? FeatureStatus_Available : FeatureStatus_Unavailable;
   611 	}
   612 	return status;
   613 }
   614 
   615 bool ConsoleDetours::AddListener(IPluginFunction *fun, const char *command)
   616 {
   617 	if (GetStatus() != FeatureStatus_Available)
   618 		return false;
   619 
   620 	if (command == NULL)
   621 	{
   622 		m_pForward->AddFunction(fun);
   623 	}
   624 	else
   625 	{
   626 		const char *str = UTIL_ToLowerCase(command);
   627 		Listener *listener;
   628 		Listener **plistener = m_CmdLookup.retrieve(str);
   629 		if (plistener == NULL)
   630 		{
   631 			listener = new Listener;
   632 			listener->forward = g_Forwards.CreateForwardEx(NULL, ET_Hook, 3, NULL, Param_Cell,
   633 			                                               Param_String, Param_Cell);
   634 			m_CmdLookup.insert(str, listener);
   635 		}
   636 		else
   637 		{
   638 			listener = *plistener;
   639 		}
   640 		listener->forward->AddFunction(fun);
   641 		delete [] str;
   642 	}
   643 
   644 	return true;
   645 }
   646 
   647 bool ConsoleDetours::RemoveListener(IPluginFunction *fun, const char *command)
   648 {
   649 	if (command == NULL)
   650 	{
   651 		return m_pForward->RemoveFunction(fun);
   652 	}
   653 	else
   654 	{
   655 		const char *str = UTIL_ToLowerCase(command);
   656 		Listener *listener;
   657 		Listener **plistener = m_CmdLookup.retrieve(str);
   658 		delete [] str;
   659 		if (plistener == NULL)
   660 			return false;
   661 		listener = *plistener;
   662 		return listener->forward->RemoveFunction(fun);
   663 	}
   664 }
   665 
   666 cell_t ConsoleDetours::InternalDispatch(int client, const CCommand& args)
   667 {
   668 	char name[255];
   669 	const char *realname = args.Arg(0);
   670 	size_t len = strlen(realname);
   671 
   672 	// Disallow command strings that are too long, for now.
   673 	if (len >= sizeof(name) - 1)
   674 		return Pl_Continue;
   675 
   676 	for (size_t i = 0; i < len; i++)
   677 	{
   678 		if (realname[i] >= 'A' && realname[i] <= 'Z')
   679 			name[i] = tolower(realname[i]);
   680 		else
   681 			name[i] = realname[i];
   682 	}
   683 	name[len] = '\0';
   684 
   685 	cell_t result = Pl_Continue;
   686 	m_pForward->PushCell(client);
   687 	m_pForward->PushString(name);
   688 	m_pForward->PushCell(args.ArgC() - 1);
   689 	m_pForward->Execute(&result, NULL);
   690 
   691 	/* Don't let plugins block this. */
   692 	if (strcmp(name, "sm") == 0)
   693 		result = Pl_Continue;
   694 
   695 	if (result >= Pl_Stop)
   696 		return result;
   697 	
   698 	Listener **plistener = m_CmdLookup.retrieve(name);
   699 	if (plistener == NULL)
   700 		return result;
   701 	Listener *listener = *plistener;
   702 	if (listener->forward->GetFunctionCount() == 0)
   703 		return result;
   704 
   705 	cell_t result2 = Pl_Continue;
   706 	listener->forward->PushCell(client);
   707 	listener->forward->PushString(name);
   708 	listener->forward->PushCell(args.ArgC() - 1);
   709 	listener->forward->Execute(&result2, NULL);
   710 
   711 	if (result2 > result)
   712 		result = result2;
   713 
   714 	/* "sm" should not have flown through the above. */
   715 	assert(strcmp(name, "sm") != 0 || result == Pl_Continue);
   716 
   717 	return result;
   718 }
   719 
   720 #if SOURCE_ENGINE >= SE_ORANGEBOX
   721 cell_t ConsoleDetours::Dispatch(ConCommand *pBase, const CCommand& args)
   722 {
   723 #else
   724 cell_t ConsoleDetours::Dispatch(ConCommand *pBase)
   725 {
   726 	CCommand args;
   727 #endif
   728 	g_HL2.PushCommandStack(&args);
   729 	cell_t res = g_ConsoleDetours.InternalDispatch(g_ConCmds.GetCommandClient(), args);
   730 	g_HL2.PopCommandStack();
   731 
   732 #if SH_IMPL_VERSION < 4
   733 	if (res >= Pl_Handled)
   734 	{
   735 		/* See bug 4020 - we can't optimize this because looking at the vtable
   736 		 * is probably more expensive, since there's no SH_GET_ORIG_VFNPTR_ENTRY.
   737 		 */
   738 		SH_ADD_HOOK(ConCommand, Dispatch, pBase, SH_STATIC(DummyHook), false);
   739 		dummy_hook_set = true;
   740 		pBase->Dispatch();
   741 		SH_REMOVE_HOOK(ConCommand, Dispatch, pBase, SH_STATIC(DummyHook), false);
   742 	}
   743 	else
   744 	{
   745 		/* Make sure the command gets invoked. See bug 4019 on making this better. */
   746 		pBase->Dispatch();
   747 	}
   748 #endif
   749 
   750 	return res;
   751 }