Triggering build.
2 * vim: set ts=4 sw=4 tw=99 noet :
3 * =============================================================================
5 * Copyright (C) 2004-2009 AlliedModders LLC. All rights reserved.
6 * =============================================================================
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.
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
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/>.
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>.
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
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.
42 #include "sourcemod.h"
43 #include "sourcemm_api.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"
54 #if defined PLATFORM_POSIX
56 # include <sys/mman.h>
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 &);
67 SH_DECL_EXTERN0_void(ConCommand, Dispatch, SH_NOATTRIB, false);
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>);
74 extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
76 extern bool __SourceHook_FHRemoveConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
79 #if SH_IMPL_VERSION >= 4
81 class GenericCommandHooker : public IConCommandLinkListener
87 unsigned int refcount;
89 CVector<HackInfo> vtables;
91 SourceHook::MemFuncInfo dispatch;
93 inline void **GetVirtualTable(ConCommandBase *pBase)
95 return *reinterpret_cast<void***>(reinterpret_cast<char*>(pBase) +
96 dispatch.thisptroffs +
100 inline bool FindVtable(void **ptr, size_t& index)
102 for (size_t i = 0; i < vtables.size(); i++)
104 if (vtables[i].vtable == ptr)
113 void MakeHookable(ConCommandBase *pBase)
115 if (!pBase->IsCommand())
118 ConCommand *cmd = (ConCommand*)pBase;
119 void **vtable = GetVirtualTable(cmd);
122 if (!FindVtable(vtable, index))
125 hack.vtable = vtable;
126 hack.hook = SH_ADD_VPHOOK(ConCommand, Dispatch, cmd, SH_MEMBER(this, &GenericCommandHooker::Dispatch), false);
128 vtables.push_back(hack);
132 vtables[index].refcount++;
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)
144 cell_t res = ConsoleDetours::Dispatch(META_IFACEPTR(ConCommand)
145 # if SOURCE_ENGINE >= SE_ORANGEBOX
149 if (res >= Pl_Handled)
150 RETURN_META(MRES_SUPERCEDE);
153 void ReparseCommandList()
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())
162 if ((*iter).refcount)
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.
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.
177 * Even though the impact is really small, we'll wait until the
178 * server shuts down, so normal operation isn't interrupted.
182 iter = vtables.erase(iter);
186 void UnhookCommand(ConCommandBase *pBase)
188 if (!pBase->IsCommand())
191 ConCommand *cmd = (ConCommand*)pBase;
192 void **vtable = GetVirtualTable(cmd);
195 if (!FindVtable(vtable, index))
197 g_Logger.LogError("Console detour tried to unhook command \"%s\" but it wasn't found", pBase->GetName());
201 assert(vtables[index].refcount > 0);
202 vtables[index].refcount--;
203 if (vtables[index].refcount == 0)
205 SH_REMOVE_HOOK_ID(vtables[index].hook);
206 vtables.erase(vtables.iterAt(index));
211 GenericCommandHooker() : enabled(false)
217 SourceHook::GetFuncInfo(&ConCommand::Dispatch, dispatch);
219 if (dispatch.thisptroffs < 0)
221 g_Logger.LogError("Command filter could not determine ConCommand layout");
225 for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
226 MakeHookable(iter.Get());
230 g_Logger.LogError("Command filter could not find any cvars!");
241 for (size_t i = 0; i < vtables.size(); i++)
242 SH_REMOVE_HOOK_ID(vtables[i].hook);
246 virtual void OnLinkConCommand(ConCommandBase *pBase)
253 virtual void OnUnlinkConCommand(ConCommandBase *pBase)
258 ReparseCommandList();
260 UnhookCommand(pBase);
265 * END DVP HOOK VERSION
268 #else /* SH_IMPL_VERSION >= 4 */
271 * BEGIN ENGINE DETOUR VERSION
274 # include <jit/jit_helpers.h>
275 # include <jit/x86/x86_macros.h>
277 class GenericCommandHooker
281 Patch() : applied(false)
287 unsigned char search[16];
299 static void DelayedActivation(void *inparam)
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)
306 g_SourceMod.AddFrameAction(GenericCommandHooker::DelayedActivation, cdtrs);
310 if (!cdtrs->ces.applied)
311 cdtrs->ApplyPatch(&cdtrs->ces);
312 if (!cdtrs->cgc.applied)
313 cdtrs->ApplyPatch(&cdtrs->cgc);
318 const char *platform = NULL;
319 # if defined(PLATFORM_WINDOWS)
320 platform = "Windows";
322 void *addrInBase = (void *)g_SMAPI->GetEngineFactory(false);
324 if (!dladdr(addrInBase, &info))
326 g_Logger.LogError("Command filter could not find engine name");
329 if (strstr(info.dli_fname, "engine_i486"))
331 platform = "Linux_486";
333 else if (strstr(info.dli_fname, "engine_i686"))
335 platform = "Linux_686";
337 else if (strstr(info.dli_fname, "engine_amd"))
339 platform = "Linux_AMD";
343 g_Logger.LogError("Command filter could not determine engine (%s)", info.dli_fname);
348 if (!PrepPatch("Cmd_ExecuteString", "CES", platform, &ces))
350 if (!PrepPatch("CGameClient::ExecuteString", "CGC", platform, &cgc))
353 if (g_HL2.PeekCommandStack() != NULL)
355 g_SourceMod.AddFrameAction(GenericCommandHooker::DelayedActivation, this);
373 # if !defined PLATFORM_WINDOWS
374 # define PAGE_READWRITE PROT_READ|PROT_WRITE
375 # define PAGE_EXECUTE_READ PROT_READ|PROT_EXEC
377 static inline uintptr_t AddrToPage(uintptr_t address)
379 return (address & ~(uintptr_t(sysconf(_SC_PAGE_SIZE) - 1)));
384 void Protect(void *addr, size_t length, int prot)
386 # if defined PLATFORM_WINDOWS
388 VirtualProtect(addr, length, prot, &ignore);
390 uintptr_t startPage = AddrToPage(uintptr_t(addr));
391 length += (uintptr_t(addr) - startPage);
392 mprotect((void*)startPage, length, prot);
396 void UndoPatch(Patch *patch)
398 if (!patch->applied || patch->detour == NULL)
401 g_pSourcePawn->FreePageMemory(patch->detour);
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);
410 void ApplyPatch(Patch *patch)
412 assert(!patch->applied);
415 void *callback = (void*)&ConsoleDetours::Dispatch;
417 /* Bogus assignment to make compiler is doing the right thing. */
418 patch->detour = callback;
420 /* Assemgle the detour. */
422 writer.outbase = NULL;
423 writer.outptr = NULL;
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);
432 if (patch->regparam != -1)
433 IA32_Pop_Reg(&writer, patch->regparam);
434 /* Copy any saved bytes */
437 for (size_t i = patch->saveat; i < patch->search_len; i++)
439 writer.write_ubyte(patch->search[i]);
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)
448 length = writer.outptr - writer.outbase;
449 patch->detour = g_pSourcePawn->AllocatePageMemory(length);
450 if (patch->detour == NULL)
452 g_Logger.LogError("Ran out of memory!");
455 g_pSourcePawn->SetReadWrite(patch->detour);
456 writer.outbase = (jitcode_t)patch->detour;
457 writer.outptr = writer.outbase;
465 g_pSourcePawn->SetReadExecute(patch->detour);
467 unsigned char *source = (unsigned char *)patch->base + patch->offset;
468 Protect(source, 6, PAGE_READWRITE);
471 *(void **)&source[2] = &patch->detour;
472 Protect(source, 6, PAGE_EXECUTE_READ);
474 patch->applied = true;
477 bool PrepPatch(const char *signature, const char *name, const char *platform, Patch *patch)
479 /* Get the base address of the function. */
480 if (!g_pGameConf->GetMemSig(signature, &patch->base) || patch->base == NULL)
482 g_Logger.LogError("Command filter could not find signature: %s", signature);
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)
493 g_Logger.LogError("Command filter could not find key: %s", keyname);
496 patch->search_len = UTIL_DecodeHexString(patch->search, sizeof(patch->search), value);
497 if (patch->search_len < 6)
499 g_Logger.LogError("Error decoding %s value, or not long enough", keyname);
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)
507 g_Logger.LogError("Command filter could not find key: %s", keyname);
510 patch->offset = atoi(value);
511 if (patch->offset > 20000)
513 g_Logger.LogError("Command filter %s value is bogus", keyname);
517 /* Get the number of bytes to save from what was written over. */
519 UTIL_Format(keyname, sizeof(keyname), "%s_Save_%s", name, platform);
520 if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
522 patch->saveat = atoi(value);
523 if (patch->saveat >= patch->search_len)
525 g_Logger.LogError("Command filter %s value is too large", keyname);
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)
535 patch->regparam = atoi(value);
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++)
542 if (address[i] != patch->search[i])
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);
554 static bool dummy_hook_set = false;
559 dummy_hook_set = false;
560 RETURN_META(MRES_SUPERCEDE);
567 * BEGIN THE ACTUALLY GENERIC CODE.
570 #define FEATURECAP_COMMANDLISTENER "command listener"
572 static GenericCommandHooker s_GenericHooker;
573 ConsoleDetours g_ConsoleDetours;
575 ConsoleDetours::ConsoleDetours() : status(FeatureStatus_Unknown)
579 void ConsoleDetours::OnSourceModAllInitialized()
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);
586 void ConsoleDetours::OnSourceModShutdown()
588 List<Listener*>::iterator iter = m_Listeners.begin();
589 while (iter != m_Listeners.end())
591 Listener *listener = (*iter);
592 g_Forwards.ReleaseForward(listener->forward);
594 iter = m_Listeners.erase(iter);
597 g_Forwards.ReleaseForward(m_pForward);
598 s_GenericHooker.Disable();
601 FeatureStatus ConsoleDetours::GetFeatureStatus(FeatureType type, const char *name)
606 FeatureStatus ConsoleDetours::GetStatus()
608 if (status == FeatureStatus_Unknown)
610 status = s_GenericHooker.Enable() ? FeatureStatus_Available : FeatureStatus_Unavailable;
615 bool ConsoleDetours::AddListener(IPluginFunction *fun, const char *command)
617 if (GetStatus() != FeatureStatus_Available)
622 m_pForward->AddFunction(fun);
626 const char *str = UTIL_ToLowerCase(command);
628 Listener **plistener = m_CmdLookup.retrieve(str);
629 if (plistener == NULL)
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);
638 listener = *plistener;
640 listener->forward->AddFunction(fun);
647 bool ConsoleDetours::RemoveListener(IPluginFunction *fun, const char *command)
651 return m_pForward->RemoveFunction(fun);
655 const char *str = UTIL_ToLowerCase(command);
657 Listener **plistener = m_CmdLookup.retrieve(str);
659 if (plistener == NULL)
661 listener = *plistener;
662 return listener->forward->RemoveFunction(fun);
666 cell_t ConsoleDetours::InternalDispatch(int client, const CCommand& args)
669 const char *realname = args.Arg(0);
670 size_t len = strlen(realname);
672 // Disallow command strings that are too long, for now.
673 if (len >= sizeof(name) - 1)
676 for (size_t i = 0; i < len; i++)
678 if (realname[i] >= 'A' && realname[i] <= 'Z')
679 name[i] = tolower(realname[i]);
681 name[i] = realname[i];
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);
691 /* Don't let plugins block this. */
692 if (strcmp(name, "sm") == 0)
693 result = Pl_Continue;
695 if (result >= Pl_Stop)
698 Listener **plistener = m_CmdLookup.retrieve(name);
699 if (plistener == NULL)
701 Listener *listener = *plistener;
702 if (listener->forward->GetFunctionCount() == 0)
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);
711 if (result2 > result)
714 /* "sm" should not have flown through the above. */
715 assert(strcmp(name, "sm") != 0 || result == Pl_Continue);
720 #if SOURCE_ENGINE >= SE_ORANGEBOX
721 cell_t ConsoleDetours::Dispatch(ConCommand *pBase, const CCommand& args)
724 cell_t ConsoleDetours::Dispatch(ConCommand *pBase)
728 g_HL2.PushCommandStack(&args);
729 cell_t res = g_ConsoleDetours.InternalDispatch(g_ConCmds.GetCommandClient(), args);
730 g_HL2.PopCommandStack();
732 #if SH_IMPL_VERSION < 4
733 if (res >= Pl_Handled)
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.
738 SH_ADD_HOOK(ConCommand, Dispatch, pBase, SH_STATIC(DummyHook), false);
739 dummy_hook_set = true;
741 SH_REMOVE_HOOK(ConCommand, Dispatch, pBase, SH_STATIC(DummyHook), false);
745 /* Make sure the command gets invoked. See bug 4019 on making this better. */