From 54364d213d27c60040b05c3b4fe3383a1a4117c8 Mon Sep 17 00:00:00 2001 From: Vladimir <47463683+Wend4r@users.noreply.github.com> Date: Sat, 17 Jul 2021 17:30:09 +0300 Subject: [PATCH] Fix output hooks when caller/activator are flipped (#1411) Co-authored-by: Asher Baker --- extensions/sdktools/output.cpp | 61 +++++++++++++++++++++++++++------ extensions/sdktools/output.h | 2 +- plugins/testsuite/outputtest.sp | 59 ++++++++++++++++++++++++++----- 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/extensions/sdktools/output.cpp b/extensions/sdktools/output.cpp index fbf852e8..25e44dbc 100644 --- a/extensions/sdktools/output.cpp +++ b/extensions/sdktools/output.cpp @@ -121,14 +121,10 @@ bool EntityOutputManager::FireEventDetour(void *pOutput, CBaseEntity *pActivator // attempt to directly lookup a hook using the pOutput pointer OutputNameStruct *pOutputName = NULL; - const char *classname = gamehelpers->GetEntityClassname(pCaller); - if (!classname) - { - return true; - } + const char *classname; + const char *outputname = FindOutputName(pOutput, pActivator, pCaller, &classname); - const char *outputname = FindOutputName(pOutput, pCaller); - if (!outputname) + if (!outputname || !classname) { return true; } @@ -345,8 +341,15 @@ OutputNameStruct *EntityOutputManager::FindOutputPointer(const char *classname, return pOutputName; } -// Iterate the datamap of pCaller and look for output pointers with the same address as pOutput -const char *EntityOutputManager::FindOutputName(void *pOutput, CBaseEntity *pCaller) +// Iterate the datamap of pCaller/pActivator and look for output pointers with the same address as pOutput. +// Store the classname of the entity we found the output on in |entity_classname| if provided. +// +// TODO: It turns out this logic isn't very sane, and it relies heavily on convention how most entities call +// FireOutput rather than explicitly conforming to the design of the engine's output system. We need a +// big refactor here to lookup the underlying per-entity CBaseEntityOutput instances and introduce an +// explicit concept of the entity owning the output being triggered, rather than assuming it is also at +// least one of the caller or activator entity. +const char *EntityOutputManager::FindOutputName(void *pOutput, CBaseEntity *pActivator, CBaseEntity *pCaller, const char **entity_classname) { datamap_t *pMap = gamehelpers->GetDataMap(pCaller); @@ -358,6 +361,11 @@ const char *EntityOutputManager::FindOutputName(void *pOutput, CBaseEntity *pCal { if ((char *)pCaller + GetTypeDescOffs(&pMap->dataDesc[i]) == pOutput) { + if (entity_classname) + { + *entity_classname = gamehelpers->GetEntityClassname(pCaller); + } + return pMap->dataDesc[i].externalName; } } @@ -365,5 +373,38 @@ const char *EntityOutputManager::FindOutputName(void *pOutput, CBaseEntity *pCal pMap = pMap->baseMap; } + // HACK: Generally, the game passes the entity that triggered the output as pCaller, but occasionally (because the + // param order is confusing), the entity gets passed in as pActivator instead. We do a 2nd pass over + // pActivator looking for the output if we couldn't find it on pCaller. + if (pActivator) + { + pMap = gamehelpers->GetDataMap(pActivator); + + while (pMap) + { + for (int i=0; idataNumFields; i++) + { + if (pMap->dataDesc[i].flags & FTYPEDESC_OUTPUT) + { + if ((char *)pActivator + GetTypeDescOffs(&pMap->dataDesc[i]) == pOutput) + { + if (entity_classname) + { + *entity_classname = gamehelpers->GetEntityClassname(pActivator); + } + + return pMap->dataDesc[i].externalName; + } + } + } + pMap = pMap->baseMap; + } + } + + if(entity_classname) + { + *entity_classname = nullptr; + } + return NULL; -} +} \ No newline at end of file diff --git a/extensions/sdktools/output.h b/extensions/sdktools/output.h index a0394598..d3e11e4b 100644 --- a/extensions/sdktools/output.h +++ b/extensions/sdktools/output.h @@ -119,7 +119,7 @@ private: bool CreateFireEventDetour(); void DeleteFireEventDetour(); - const char *FindOutputName(void *pOutput, CBaseEntity *pCaller); + const char *FindOutputName(void *pOutput, CBaseEntity *pActivator, CBaseEntity *pCaller, const char **entity_classname); // Maps classname to a ClassNameStruct IBasicTrie *ClassNames; diff --git a/plugins/testsuite/outputtest.sp b/plugins/testsuite/outputtest.sp index f9312ee5..d3fde2d5 100644 --- a/plugins/testsuite/outputtest.sp +++ b/plugins/testsuite/outputtest.sp @@ -1,7 +1,7 @@ #include #include -public Plugin:myinfo = +public Plugin myinfo = { name = "Entity Output Hook Testing", author = "AlliedModders LLC", @@ -10,32 +10,70 @@ public Plugin:myinfo = url = "http://www.sourcemod.net/" }; -public OnPluginStart() +public void OnPluginStart() { HookEntityOutput("point_spotlight", "OnLightOn", OutputHook); + HookEntityOutput("point_spotlight", "OnLightOff", OutputHook); HookEntityOutput("func_door", "OnOpen", OutputHook); HookEntityOutput("func_door_rotating", "OnOpen", OutputHook); + HookEntityOutput("prop_door_rotating", "OnOpen", OutputHook); HookEntityOutput("func_door", "OnClose", OutputHook); HookEntityOutput("func_door_rotating", "OnClose", OutputHook); + HookEntityOutput("prop_door_rotating", "OnClose", OutputHook); + + if (GetEngineVersion() == Engine_CSGO) { + // The server library calls with output names from Activator (from "plated_c4" activator entity in current example). + HookEntityOutput("planted_c4", "OnBombBeginDefuse", OutputHook); + HookEntityOutput("planted_c4", "OnBombDefuseAborted", OutputHook); + + // Never fired for planted_c4, only planted_c4_training. + HookEntityOutput("planted_c4", "OnBombDefused", OutputHook); + } } -public OutputHook(const String:name[], caller, activator, Float:delay) +public void OutputHook(const char[] name, int caller, int activator, float delay) { - LogMessage("[ENTOUTPUT] %s", name); + char callerClassname[64]; + if (caller >= 0 && IsValidEntity(caller)) { + GetEntityClassname(caller, callerClassname, sizeof(callerClassname)); + } + + char activatorClassname[64]; + if (activator >= 0 && IsValidEntity(activator)) { + GetEntityClassname(activator, activatorClassname, sizeof(activatorClassname)); + } + + LogMessage("[ENTOUTPUT] %s (caller: %d/%s, activator: %d/%s)", name, caller, callerClassname, activator, activatorClassname); } -public OnMapStart() +public void OnMapStart() { - new ent = FindEntityByClassname(-1, "point_spotlight"); + int ent = FindEntityByClassname(-1, "point_spotlight"); if (ent == -1) { - LogError("Could not find a point_spotlight"); + LogMessage("[ENTOUTPUT] Could not find a point_spotlight"); ent = CreateEntityByName("point_spotlight"); DispatchSpawn(ent); } + LogMessage("[ENTOUTPUT] Begin basic"); + + AcceptEntityInput(ent, "LightOff"); + AcceptEntityInput(ent, "LightOn"); + + AcceptEntityInput(ent, "LightOff", .caller = ent); + AcceptEntityInput(ent, "LightOn", .caller = ent); + + AcceptEntityInput(ent, "LightOff", .activator = ent); + AcceptEntityInput(ent, "LightOn", .activator = ent); + + AcceptEntityInput(ent, "LightOff", ent, ent); + AcceptEntityInput(ent, "LightOn", ent, ent); + + LogMessage("[ENTOUTPUT] End basic, begin once"); + HookSingleEntityOutput(ent, "OnLightOn", OutputHook, true); HookSingleEntityOutput(ent, "OnLightOff", OutputHook, true); @@ -44,16 +82,21 @@ public OnMapStart() AcceptEntityInput(ent, "LightOff", ent, ent); AcceptEntityInput(ent, "LightOn", ent, ent); + + LogMessage("[ENTOUTPUT] End once, begin single"); HookSingleEntityOutput(ent, "OnLightOn", OutputHook, false); HookSingleEntityOutput(ent, "OnLightOff", OutputHook, false); AcceptEntityInput(ent, "LightOff", ent, ent); AcceptEntityInput(ent, "LightOn", ent, ent); + AcceptEntityInput(ent, "LightOff", ent, ent); AcceptEntityInput(ent, "LightOn", ent, ent); - //Comment these out (and reload the plugin heaps) to test for leaks on plugin unload + // Comment these out (and reload the plugin heaps) to test for leaks on plugin unload UnhookSingleEntityOutput(ent, "OnLightOn", OutputHook); UnhookSingleEntityOutput(ent, "OnLightOff", OutputHook); + + LogMessage("[ENTOUTPUT] End single"); } \ No newline at end of file