diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5cdb32673..c15d1e45b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -83,7 +83,7 @@ SET(SOURCE_FILES
src/core/managers/server_manager.h
src/scripting/natives/natives_server.cpp
libraries/nlohmann/json.hpp
- src/scripting/natives/natives_dynamichooks.cpp
+ src/scripting/natives/natives_dynamichooks.cpp
)
diff --git a/configs/addons/counterstrikesharp/gamedata/gamedata.json b/configs/addons/counterstrikesharp/gamedata/gamedata.json
index 3ecbd7bd2..d7c38fb0d 100644
--- a/configs/addons/counterstrikesharp/gamedata/gamedata.json
+++ b/configs/addons/counterstrikesharp/gamedata/gamedata.json
@@ -164,5 +164,12 @@
"windows": 91,
"linux": 91
}
+ },
+ "CEntityIOOutput_FireOutputInternal": {
+ "signatures": {
+ "library": "server",
+ "windows": "\\x48\\x8B\\xC4\\x4C\\x89\\x48\\x20\\x55\\x57\\x41\\x54\\x41\\x56",
+ "linux": "\\x55\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x41\\x54\\x53\\x48\\x83\\xEC\\x58\\x4C\\x8B\\x6F\\x08"
+ }
}
}
\ No newline at end of file
diff --git a/docfx/examples/WithEntityOutputHooks.md b/docfx/examples/WithEntityOutputHooks.md
new file mode 100644
index 000000000..ee090a221
--- /dev/null
+++ b/docfx/examples/WithEntityOutputHooks.md
@@ -0,0 +1,5 @@
+[!INCLUDE [WithEntityOutputHooks](../../examples/WithEntityOutputHooks/README.md)]
+
+View project on Github
+
+[!code-csharp[](../../examples/WithEntityOutputHooks/WithEntityOutputHooksPlugin.cs)]
diff --git a/docfx/examples/toc.yml b/docfx/examples/toc.yml
index 87daf8407..e3a693796 100644
--- a/docfx/examples/toc.yml
+++ b/docfx/examples/toc.yml
@@ -7,6 +7,8 @@ items:
href: WithConfig.md
- name: Dependency Injection
href: WithDependencyInjection.md
+ - name: Entity Output Hooks
+ href: WithEntityOutputHooks.md
- name: Game Event Handlers
href: WithGameEventHandlers.md
- name: Database (Dapper)
diff --git a/examples/WithEntityOutputHooks/README.md b/examples/WithEntityOutputHooks/README.md
new file mode 100644
index 000000000..4c008145f
--- /dev/null
+++ b/examples/WithEntityOutputHooks/README.md
@@ -0,0 +1,2 @@
+# With Entity Output Hooks
+This example shows how to implement hooks for entity output, such as StartTouch, OnPickup etc.
\ No newline at end of file
diff --git a/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj b/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj
new file mode 100644
index 000000000..21eb946ff
--- /dev/null
+++ b/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj
@@ -0,0 +1,12 @@
+
+
+ net7.0
+ enable
+ enable
+ false
+ false
+
+
+
+
+
diff --git a/examples/WithEntityOutputHooks/WithEntityOutputHooksPlugin.cs b/examples/WithEntityOutputHooks/WithEntityOutputHooksPlugin.cs
new file mode 100644
index 000000000..39bcc0414
--- /dev/null
+++ b/examples/WithEntityOutputHooks/WithEntityOutputHooksPlugin.cs
@@ -0,0 +1,43 @@
+using CounterStrikeSharp.API.Core;
+using CounterStrikeSharp.API.Core.Attributes;
+using CounterStrikeSharp.API.Core.Attributes.Registration;
+using Microsoft.Extensions.Logging;
+
+namespace WithEntityOutputHooks;
+
+[MinimumApiVersion(80)]
+public class WithEntityOutputHooksPlugin : BasePlugin
+{
+ public override string ModuleName => "Example: With Entity Output Hooks";
+ public override string ModuleVersion => "1.0.0";
+ public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
+ public override string ModuleDescription => "A simple plugin that showcases entity output hooks";
+
+ public override void Load(bool hotReload)
+ {
+ HookEntityOutput("weapon_knife", "OnPlayerPickup", (CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay) =>
+ {
+ Logger.LogInformation("weapon_knife called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
+
+ return HookResult.Continue;
+ });
+ }
+
+ // Output hooks can use wildcards to match multiple entities
+ [EntityOutputHook("*", "OnPlayerPickup")]
+ public HookResult OnPickup(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay)
+ {
+ Logger.LogInformation("[EntityOutputHook Attribute] Called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
+
+ return HookResult.Continue;
+ }
+
+ // Output hooks can use wildcards to match multiple output names
+ [EntityOutputHook("func_buyzone", "*")]
+ public HookResult OnTouchStart(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay)
+ {
+ Logger.LogInformation("[EntityOutputHook Attribute] Buyzone called output ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
+
+ return HookResult.Continue;
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Core/API.cs b/managed/CounterStrikeSharp.API/Core/API.cs
index 1f02401b0..82d2b1012 100644
--- a/managed/CounterStrikeSharp.API/Core/API.cs
+++ b/managed/CounterStrikeSharp.API/Core/API.cs
@@ -654,6 +654,32 @@ public static string GetPlayerIpAddress(int slot){
}
}
+ public static void HookEntityOutput(string classname, string outputname, InputArgument callback, HookMode mode){
+ lock (ScriptContext.GlobalScriptContext.Lock) {
+ ScriptContext.GlobalScriptContext.Reset();
+ ScriptContext.GlobalScriptContext.Push(classname);
+ ScriptContext.GlobalScriptContext.Push(outputname);
+ ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
+ ScriptContext.GlobalScriptContext.Push(mode);
+ ScriptContext.GlobalScriptContext.SetIdentifier(0x15245242);
+ ScriptContext.GlobalScriptContext.Invoke();
+ ScriptContext.GlobalScriptContext.CheckErrors();
+ }
+ }
+
+ public static void UnhookEntityOutput(string classname, string outputname, InputArgument callback, HookMode mode){
+ lock (ScriptContext.GlobalScriptContext.Lock) {
+ ScriptContext.GlobalScriptContext.Reset();
+ ScriptContext.GlobalScriptContext.Push(classname);
+ ScriptContext.GlobalScriptContext.Push(outputname);
+ ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
+ ScriptContext.GlobalScriptContext.Push(mode);
+ ScriptContext.GlobalScriptContext.SetIdentifier(0x87DBD139);
+ ScriptContext.GlobalScriptContext.Invoke();
+ ScriptContext.GlobalScriptContext.CheckErrors();
+ }
+ }
+
public static void HookEvent(string name, InputArgument callback, bool ispost){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
diff --git a/managed/CounterStrikeSharp.API/Core/Attributes/Registration/EntityOutputHookAttribute.cs b/managed/CounterStrikeSharp.API/Core/Attributes/Registration/EntityOutputHookAttribute.cs
new file mode 100644
index 000000000..d2d443311
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Core/Attributes/Registration/EntityOutputHookAttribute.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace CounterStrikeSharp.API.Core.Attributes.Registration;
+
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+public class EntityOutputHookAttribute : Attribute
+{
+ public string Classname { get; }
+ public string OutputName { get; }
+
+ public EntityOutputHookAttribute(string classname, string outputName)
+ {
+ Classname = classname;
+ OutputName = outputName;
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs
index 7c57cdf3c..90a8cab2e 100644
--- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs
+++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs
@@ -26,6 +26,7 @@
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Config;
+using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -36,6 +37,13 @@ public abstract class BasePlugin : IPlugin
public BasePlugin()
{
+ RegisterListener(() =>
+ {
+ foreach (KeyValuePair callback in EntitySingleOutputHooks)
+ {
+ UnhookSingleEntityOutputInternal(callback.Value.Classname, callback.Value.Output, callback.Value.Handler);
+ }
+ });
}
public abstract string ModuleName { get; }
@@ -108,6 +116,12 @@ public void Dispose()
public readonly Dictionary Listeners =
new Dictionary();
+ public readonly Dictionary EntityOutputHooks =
+ new Dictionary();
+
+ internal readonly Dictionary EntitySingleOutputHooks =
+ new Dictionary();
+
public readonly List Timers = new List();
public delegate HookResult GameEventHandler(T @event, GameEventInfo info) where T : GameEvent;
@@ -354,6 +368,7 @@ public void RegisterAllAttributes(object instance)
{
this.RegisterAttributeHandlers(instance);
this.RegisterConsoleCommandAttributeHandlers(instance);
+ this.RegisterEntityOutputAttributeHandlers(instance);
}
public void InitializeConfig(object instance, Type pluginType)
@@ -430,6 +445,77 @@ public void RegisterConsoleCommandAttributeHandlers(object instance)
}
}
+ public void RegisterEntityOutputAttributeHandlers(object instance)
+ {
+ var handlers = instance.GetType()
+ .GetMethods()
+ .Where(method => method.GetCustomAttributes().Any())
+ .ToArray();
+
+ foreach (var handler in handlers)
+ {
+ var attributes = handler.GetCustomAttributes();
+ foreach (var outputInfo in attributes)
+ {
+ HookEntityOutput(outputInfo.Classname, outputInfo.OutputName, handler.CreateDelegate(instance));
+ }
+ }
+ }
+
+ public void HookEntityOutput(string classname, string outputName, EntityIO.EntityOutputHandler handler, HookMode mode = HookMode.Pre)
+ {
+ var subscriber = new CallbackSubscriber(handler, handler,
+ () => UnhookEntityOutput(classname, outputName, handler));
+
+ NativeAPI.HookEntityOutput(classname, outputName, subscriber.GetInputArgument(), mode);
+ EntityOutputHooks[handler] = subscriber;
+ }
+
+ public void UnhookEntityOutput(string classname, string outputName, EntityIO.EntityOutputHandler handler, HookMode mode = HookMode.Pre)
+ {
+ if (!EntityOutputHooks.TryGetValue(handler, out var subscriber)) return;
+
+ NativeAPI.UnhookEntityOutput(classname, outputName, subscriber.GetInputArgument(), mode);
+ FunctionReference.Remove(subscriber.GetReferenceIdentifier());
+ EntityOutputHooks.Remove(handler);
+ }
+
+ public void HookSingleEntityOutput(CEntityInstance entityInstance, string outputName, EntityIO.EntityOutputHandler handler)
+ {
+ // since we wrap around the plugin handler we need to do this to ensure that the plugin callback is only called
+ // if the entity instance is the same.
+ EntityIO.EntityOutputHandler internalHandler = (output, name, activator, caller, value, delay) =>
+ {
+ if (caller == entityInstance)
+ {
+ return handler(output, name, activator, caller, value, delay);
+ }
+
+ return HookResult.Continue;
+ };
+
+ HookEntityOutput(entityInstance.DesignerName, outputName, internalHandler);
+
+ // because of ^ we would not be able to unhook since we passed the 'internalHandler' and that's what is being stored, not the original handler
+ // but the plugin could only pass the original handler for unhooking.
+ // (this dictionary does not needed to be cleared on dispose as it has no unmanaged reference and those are already being disposed, but on map end)
+ // (the internal class is needed to be able to remove them on map start)
+ EntitySingleOutputHooks[handler] = new EntityIO.EntityOutputCallback(entityInstance.DesignerName, outputName, internalHandler);
+ }
+
+ public void UnhookSingleEntityOutput(CEntityInstance entityInstance, string outputName, EntityIO.EntityOutputHandler handler)
+ {
+ UnhookSingleEntityOutputInternal(entityInstance.DesignerName, outputName, handler);
+ }
+
+ private void UnhookSingleEntityOutputInternal(string classname, string outputName, EntityIO.EntityOutputHandler handler)
+ {
+ if (!EntitySingleOutputHooks.TryGetValue(handler, out var internalHandler)) return;
+
+ UnhookEntityOutput(classname, outputName, internalHandler.Handler);
+ EntitySingleOutputHooks.Remove(handler);
+ }
+
public void Dispose()
{
Dispose(true);
@@ -464,6 +550,11 @@ protected virtual void Dispose(bool disposing)
subscriber.Dispose();
}
+ foreach (var subscriber in EntityOutputHooks.Values)
+ {
+ subscriber.Dispose();
+ }
+
foreach (var timer in Timers)
{
timer.Kill();
diff --git a/managed/CounterStrikeSharp.API/Core/Model/CEntityIOOutput.cs b/managed/CounterStrikeSharp.API/Core/Model/CEntityIOOutput.cs
new file mode 100644
index 000000000..6550f85d4
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Core/Model/CEntityIOOutput.cs
@@ -0,0 +1,49 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using CounterStrikeSharp.API.Modules.Utils;
+
+namespace CounterStrikeSharp.API.Core;
+
+public partial class CEntityIOOutput
+{
+ public EntityIOConnection_t? Connections => Utilities.GetPointer(Handle + 8);
+
+ public EntityIOOutputDesc_t Description => new(Marshal.ReadIntPtr(Handle + 16));
+}
+
+public class EntityIOOutputDesc_t : NativeObject
+{
+ public EntityIOOutputDesc_t(IntPtr pointer) : base(pointer)
+ {
+ }
+
+ public string Name => Utilities.ReadStringUtf8(Handle + 0);
+ public unsafe ref uint Flags => ref Unsafe.AsRef((void*)(Handle + 8));
+ public unsafe ref uint OutputOffset => ref Unsafe.AsRef((void*)(Handle + 16));
+}
+
+public class EntityIOConnection_t : EntityIOConnectionDesc_t
+{
+ public EntityIOConnection_t(IntPtr pointer) : base(pointer)
+ {
+ }
+
+ public unsafe ref bool MarkedForRemoval => ref Unsafe.AsRef((void*)(Handle + 40));
+
+ public EntityIOConnection_t? Next => Utilities.GetPointer(Handle + 48);
+}
+
+public class EntityIOConnectionDesc_t : NativeObject
+{
+ public EntityIOConnectionDesc_t(IntPtr pointer) : base(pointer)
+ {
+ }
+
+ public string TargetDesc => Utilities.ReadStringUtf8(Handle + 0);
+ public string TargetInput => Utilities.ReadStringUtf8(Handle + 8);
+ public string ValueOverride => Utilities.ReadStringUtf8(Handle + 16);
+ public CEntityHandle Target => new(Handle + 24);
+ public unsafe ref EntityIOTargetType_t TargetType => ref Unsafe.AsRef((void*)(Handle + 28));
+ public unsafe ref int TimesToFire => ref Unsafe.AsRef((void*)(Handle + 32));
+ public unsafe ref float Delay => ref Unsafe.AsRef((void*)(Handle + 36));
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Core/Model/CVariant.cs b/managed/CounterStrikeSharp.API/Core/Model/CVariant.cs
new file mode 100644
index 000000000..5fc4532ca
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Core/Model/CVariant.cs
@@ -0,0 +1,13 @@
+namespace CounterStrikeSharp.API.Core;
+
+///
+/// Placeholder for CVariant
+///
+/// A lot of entity outputs do not use this value
+///
+public class CVariant : NativeObject
+{
+ public CVariant(IntPtr pointer) : base(pointer)
+ {
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Modules/Entities/EntityIO.cs b/managed/CounterStrikeSharp.API/Modules/Entities/EntityIO.cs
new file mode 100644
index 000000000..777817b4f
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Modules/Entities/EntityIO.cs
@@ -0,0 +1,39 @@
+/*
+ * This file is part of CounterStrikeSharp.
+ * CounterStrikeSharp is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * CounterStrikeSharp 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 CounterStrikeSharp. If not, see . *
+ */
+
+namespace CounterStrikeSharp.API.Modules.Entities
+{
+ public class EntityIO
+ {
+ public delegate HookResult EntityOutputHandler(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay);
+
+ internal class EntityOutputCallback
+ {
+ public string Classname;
+
+ public string Output;
+
+ public EntityOutputHandler Handler;
+
+ public EntityOutputCallback(string classname, string output, EntityOutputHandler handler)
+ {
+ Classname = classname;
+ Output = output;
+ Handler = handler;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Utilities.cs b/managed/CounterStrikeSharp.API/Utilities.cs
index 98928010a..c55fe1e74 100644
--- a/managed/CounterStrikeSharp.API/Utilities.cs
+++ b/managed/CounterStrikeSharp.API/Utilities.cs
@@ -150,5 +150,16 @@ public static unsafe string ReadStringUtf8(IntPtr ptr)
return Encoding.UTF8.GetString(buffer);
}
}
+
+ public static T? GetPointer(IntPtr pointer) where T : NativeObject
+ {
+ var pointerTo = Marshal.ReadIntPtr(pointer);
+ if (pointerTo == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ return (T)Activator.CreateInstance(typeof(T), pointerTo)!;
+ }
}
}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.sln b/managed/CounterStrikeSharp.sln
index 14c874fe4..6e3aae71c 100644
--- a/managed/CounterStrikeSharp.sln
+++ b/managed/CounterStrikeSharp.sln
@@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithGameEventHandlers", "..
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithDatabaseDapper", "..\examples\WithDatabaseDapper\WithDatabaseDapper.csproj", "{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithEntityOutputHooks", "..\examples\WithEntityOutputHooks\WithEntityOutputHooks.csproj", "{31EABE0B-871F-497B-BF36-37FFC6FAD15F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -74,6 +76,10 @@ Global
{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {31EABE0B-871F-497B-BF36-37FFC6FAD15F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {31EABE0B-871F-497B-BF36-37FFC6FAD15F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {31EABE0B-871F-497B-BF36-37FFC6FAD15F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {31EABE0B-871F-497B-BF36-37FFC6FAD15F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57E64289-5D69-4AA1-BEF0-D0D96A55EE8F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
@@ -84,5 +90,6 @@ Global
{EA2F596E-2236-4999-B476-B1FDA287674A} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{3032F3FA-E20A-4581-9A08-2FB5FF1524F4} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
+ {31EABE0B-871F-497B-BF36-37FFC6FAD15F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
EndGlobalSection
EndGlobal
diff --git a/managed/TestPlugin/TestPlugin.cs b/managed/TestPlugin/TestPlugin.cs
index 748e745ac..4d11f5767 100644
--- a/managed/TestPlugin/TestPlugin.cs
+++ b/managed/TestPlugin/TestPlugin.cs
@@ -93,6 +93,7 @@ public override void Load(bool hotReload)
SetupListeners();
SetupCommands();
SetupMenus();
+ SetupEntityOutputHooks();
// ValveInterface provides pointers to loaded modules via Interface Name exposed from the engine (e.g. Source2Server001)
var server = ValveInterface.Server;
@@ -406,6 +407,29 @@ private void SetupCommands()
});
}
+ private void SetupEntityOutputHooks()
+ {
+ HookEntityOutput("weapon_knife", "OnPlayerPickup", (output, name, activator, caller, value, delay) =>
+ {
+ Logger.LogInformation("weapon_knife called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", output.Description.Name, activator.DesignerName, caller.DesignerName, delay);
+
+ return HookResult.Continue;
+ });
+
+ HookEntityOutput("*", "*", (output, name, activator, caller, value, delay) =>
+ {
+ Logger.LogInformation("All EntityOutput ({name}, {activator}, {caller}, {delay})", output.Description.Name, activator.DesignerName, caller.DesignerName, delay);
+
+ return HookResult.Continue;
+ });
+
+ HookEntityOutput("*", "OnStartTouch", (output, name, activator, caller, value, delay) =>
+ {
+ Logger.LogInformation("OnStartTouch: ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
+ return HookResult.Continue;
+ });
+ }
+
[GameEventHandler]
public HookResult OnPlayerConnect(EventPlayerConnect @event, GameEventInfo info)
{
@@ -544,5 +568,13 @@ private HookResult GenericEventHandler(T @event, GameEventInfo info) where T
return HookResult.Continue;
}
+
+ [EntityOutputHook("*", "OnPlayerPickup")]
+ public HookResult OnPickup(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay)
+ {
+ Logger.LogInformation("[EntityOutputHook Attribute] Called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay);
+
+ return HookResult.Continue;
+ }
}
}
diff --git a/src/core/managers/entity_manager.cpp b/src/core/managers/entity_manager.cpp
index 7566c08ab..dd51415ef 100644
--- a/src/core/managers/entity_manager.cpp
+++ b/src/core/managers/entity_manager.cpp
@@ -15,6 +15,11 @@
*/
#include "core/managers/entity_manager.h"
+#include "core/gameconfig.h"
+#include "core/log.h"
+
+#include
+#include
#include
#include "scripting/callback_manager.h"
@@ -25,15 +30,31 @@ EntityManager::EntityManager() {}
EntityManager::~EntityManager() {}
-void EntityManager::OnAllInitialized() {
+void EntityManager::OnAllInitialized()
+{
on_entity_spawned_callback = globals::callbackManager.CreateCallback("OnEntitySpawned");
on_entity_created_callback = globals::callbackManager.CreateCallback("OnEntityCreated");
on_entity_deleted_callback = globals::callbackManager.CreateCallback("OnEntityDeleted");
- on_entity_parent_changed_callback = globals::callbackManager.CreateCallback("OnEntityParentChanged");
+ on_entity_parent_changed_callback =
+ globals::callbackManager.CreateCallback("OnEntityParentChanged");
+
+ m_pFireOutputInternal = reinterpret_cast(modules::server->FindSignature(
+ globals::gameConfig->GetSignature("CEntityIOOutput_FireOutputInternal")));
+
+ if (m_pFireOutputInternal == nullptr) {
+ CSSHARP_CORE_CRITICAL("Failed to find signature for \'CEntityIOOutput_FireOutputInternal\'");
+ return;
+ }
+
+ auto m_hook = funchook_create();
+ funchook_prepare(m_hook, (void**)&m_pFireOutputInternal, (void*)&DetourFireOutputInternal);
+ funchook_install(m_hook, 0);
+
// Listener is added in ServerStartup as entity system is not initialised at this stage.
}
-void EntityManager::OnShutdown() {
+void EntityManager::OnShutdown()
+{
globals::callbackManager.ReleaseCallback(on_entity_spawned_callback);
globals::callbackManager.ReleaseCallback(on_entity_created_callback);
globals::callbackManager.ReleaseCallback(on_entity_deleted_callback);
@@ -41,7 +62,8 @@ void EntityManager::OnShutdown() {
globals::entitySystem->RemoveListenerEntity(&entityListener);
}
-void CEntityListener::OnEntitySpawned(CEntityInstance *pEntity) {
+void CEntityListener::OnEntitySpawned(CEntityInstance* pEntity)
+{
auto callback = globals::entityManager.on_entity_spawned_callback;
if (callback && callback->GetFunctionCount()) {
@@ -50,7 +72,8 @@ void CEntityListener::OnEntitySpawned(CEntityInstance *pEntity) {
callback->Execute();
}
}
-void CEntityListener::OnEntityCreated(CEntityInstance *pEntity) {
+void CEntityListener::OnEntityCreated(CEntityInstance* pEntity)
+{
auto callback = globals::entityManager.on_entity_created_callback;
if (callback && callback->GetFunctionCount()) {
@@ -59,7 +82,8 @@ void CEntityListener::OnEntityCreated(CEntityInstance *pEntity) {
callback->Execute();
}
}
-void CEntityListener::OnEntityDeleted(CEntityInstance *pEntity) {
+void CEntityListener::OnEntityDeleted(CEntityInstance* pEntity)
+{
auto callback = globals::entityManager.on_entity_deleted_callback;
if (callback && callback->GetFunctionCount()) {
@@ -68,7 +92,8 @@ void CEntityListener::OnEntityDeleted(CEntityInstance *pEntity) {
callback->Execute();
}
}
-void CEntityListener::OnEntityParentChanged(CEntityInstance *pEntity, CEntityInstance *pNewParent) {
+void CEntityListener::OnEntityParentChanged(CEntityInstance* pEntity, CEntityInstance* pNewParent)
+{
auto callback = globals::entityManager.on_entity_parent_changed_callback;
if (callback && callback->GetFunctionCount()) {
@@ -79,4 +104,119 @@ void CEntityListener::OnEntityParentChanged(CEntityInstance *pEntity, CEntityIns
}
}
-} // namespace counterstrikesharp
\ No newline at end of file
+void EntityManager::HookEntityOutput(const char* szClassname, const char* szOutput,
+ CallbackT fnCallback, HookMode mode)
+{
+ auto outputKey = OutputKey_t(szClassname, szOutput);
+ CallbackPair* pCallbackPair;
+
+ auto search = m_pHookMap.find(outputKey);
+ if (search == m_pHookMap.end()) {
+ m_pHookMap[outputKey] = new CallbackPair();
+ pCallbackPair = m_pHookMap[outputKey];
+ } else
+ pCallbackPair = search->second;
+
+ auto* pCallback = mode == HookMode::Pre ? pCallbackPair->pre : pCallbackPair->post;
+ pCallback->AddListener(fnCallback);
+}
+
+void EntityManager::UnhookEntityOutput(const char* szClassname, const char* szOutput,
+ CallbackT fnCallback, HookMode mode)
+{
+ auto outputKey = OutputKey_t(szClassname, szOutput);
+
+ auto search = m_pHookMap.find(outputKey);
+ if (search != m_pHookMap.end()) {
+ auto* pCallbackPair = search->second;
+
+ auto* pCallback = mode == Pre ? pCallbackPair->pre : pCallbackPair->post;
+
+ pCallback->RemoveListener(fnCallback);
+
+ if (!pCallbackPair->HasCallbacks()) {
+ m_pHookMap.erase(outputKey);
+ }
+ }
+}
+
+void DetourFireOutputInternal(CEntityIOOutput* const pThis, CEntityInstance* pActivator,
+ CEntityInstance* pCaller, const CVariant* const value, float flDelay)
+{
+ std::vector vecSearchKeys{OutputKey_t("*", pThis->m_pDesc->m_pName),
+ OutputKey_t("*", "*")};
+
+ if (pCaller) {
+ vecSearchKeys.push_back(OutputKey_t(pCaller->GetClassname(), pThis->m_pDesc->m_pName));
+ OutputKey_t(pCaller->GetClassname(), "*");
+ }
+
+ std::vector vecCallbackPairs;
+
+ if (pCaller) {
+ CSSHARP_CORE_TRACE("[EntityManager][FireOutputHook] - {}, {}", pThis->m_pDesc->m_pName,
+ pCaller->GetClassname());
+
+ auto& hookMap = globals::entityManager.m_pHookMap;
+
+ for (auto& searchKey : vecSearchKeys) {
+ auto search = hookMap.find(searchKey);
+ if (search != hookMap.end()) {
+ vecCallbackPairs.push_back(search->second);
+ }
+ }
+ } else
+ CSSHARP_CORE_TRACE("[EntityManager][FireOutputHook] - {}, unknown caller",
+ pThis->m_pDesc->m_pName);
+
+ HookResult result = HookResult::Continue;
+
+ for (auto pCallbackPair : vecCallbackPairs) {
+ if (pCallbackPair->pre->GetFunctionCount()) {
+ pCallbackPair->pre->ScriptContext().Reset();
+ pCallbackPair->pre->ScriptContext().Push(pThis);
+ pCallbackPair->pre->ScriptContext().Push(pThis->m_pDesc->m_pName);
+ pCallbackPair->pre->ScriptContext().Push(pActivator);
+ pCallbackPair->pre->ScriptContext().Push(pCaller);
+ pCallbackPair->pre->ScriptContext().Push(value);
+ pCallbackPair->pre->ScriptContext().Push(flDelay);
+
+ for (auto fnMethodToCall : pCallbackPair->pre->GetFunctions()) {
+ if (!fnMethodToCall)
+ continue;
+ fnMethodToCall(&pCallbackPair->pre->ScriptContextStruct());
+
+ auto thisResult = pCallbackPair->pre->ScriptContext().GetResult();
+
+ if (thisResult >= HookResult::Stop) {
+ return;
+ }
+
+ if (thisResult > result) {
+ result = thisResult;
+ }
+ }
+ }
+ }
+
+ if (result >= HookResult::Handled) {
+ return;
+ }
+
+ m_pFireOutputInternal(pThis, pActivator, pCaller, value, flDelay);
+
+ for (auto pCallbackPair : vecCallbackPairs) {
+ if (pCallbackPair->post->GetFunctionCount()) {
+ pCallbackPair->post->ScriptContext().Reset();
+ pCallbackPair->post->ScriptContext().Push(pThis);
+ pCallbackPair->post->ScriptContext().Push(pThis->m_pDesc->m_pName);
+ pCallbackPair->post->ScriptContext().Push(pActivator);
+ pCallbackPair->post->ScriptContext().Push(pCaller);
+ pCallbackPair->post->ScriptContext().Push(value);
+ pCallbackPair->post->ScriptContext().Push(flDelay);
+ pCallbackPair->post->Execute();
+ }
+ }
+}
+
+} // namespace counterstrikesharp
\ No newline at end of file
diff --git a/src/core/managers/entity_manager.h b/src/core/managers/entity_manager.h
index 635758bad..bcca2d806 100644
--- a/src/core/managers/entity_manager.h
+++ b/src/core/managers/entity_manager.h
@@ -23,10 +23,18 @@
#include "core/global_listener.h"
#include "scripting/script_engine.h"
#include "entitysystem.h"
+#include "scripting/callback_manager.h"
+
+// variant.h depends on ivscript.h, lets not include the whole thing
+DECLARE_POINTER_HANDLE(HSCRIPT);
+
+#include
namespace counterstrikesharp {
class ScriptCallback;
+typedef std::pair OutputKey_t;
+
class CEntityListener : public IEntityListener {
void OnEntitySpawned(CEntityInstance *pEntity) override;
void OnEntityCreated(CEntityInstance *pEntity) override;
@@ -41,7 +49,10 @@ class EntityManager : public GlobalClass {
~EntityManager();
void OnAllInitialized() override;
void OnShutdown() override;
+ void HookEntityOutput(const char* szClassname, const char* szOutput, CallbackT fnCallback, HookMode mode);
+ void UnhookEntityOutput(const char* szClassname, const char* szOutput, CallbackT fnCallback, HookMode mode);
CEntityListener entityListener;
+ std::map m_pHookMap;
private:
ScriptCallback *on_entity_spawned_callback;
ScriptCallback *on_entity_created_callback;
@@ -49,4 +60,58 @@ class EntityManager : public GlobalClass {
ScriptCallback *on_entity_parent_changed_callback;
};
+
+enum EntityIOTargetType_t
+{
+ ENTITY_IO_TARGET_INVALID = 0xFFFFFFFF,
+ ENTITY_IO_TARGET_CLASSNAME = 0x0,
+ ENTITY_IO_TARGET_CLASSNAME_DERIVES_FROM = 0x1,
+ ENTITY_IO_TARGET_ENTITYNAME = 0x2,
+ ENTITY_IO_TARGET_CONTAINS_COMPONENT = 0x3,
+ ENTITY_IO_TARGET_SPECIAL_ACTIVATOR = 0x4,
+ ENTITY_IO_TARGET_SPECIAL_CALLER = 0x5,
+ ENTITY_IO_TARGET_EHANDLE = 0x6,
+ ENTITY_IO_TARGET_ENTITYNAME_OR_CLASSNAME = 0x7,
+};
+
+struct EntityIOConnectionDesc_t
+{
+ string_t m_targetDesc;
+ string_t m_targetInput;
+ string_t m_valueOverride;
+ CEntityHandle m_hTarget;
+ EntityIOTargetType_t m_nTargetType;
+ int32 m_nTimesToFire;
+ float m_flDelay;
+};
+
+struct EntityIOConnection_t : EntityIOConnectionDesc_t
+{
+ bool m_bMarkedForRemoval;
+ EntityIOConnection_t* m_pNext;
+};
+
+struct EntityIOOutputDesc_t
+{
+ const char* m_pName;
+ uint32 m_nFlags;
+ uint32 m_nOutputOffset;
+};
+
+class CEntityIOOutput
+{
+ public:
+ void* vtable;
+ EntityIOConnection_t* m_pConnections;
+ EntityIOOutputDesc_t* m_pDesc;
+};
+
+typedef void (*FireOutputInternal)(CEntityIOOutput* const, CEntityInstance*, CEntityInstance*,
+ const CVariant* const, float);
+
+static void DetourFireOutputInternal(CEntityIOOutput* const pThis, CEntityInstance* pActivator,
+ CEntityInstance* pCaller, const CVariant* const value, float flDelay);
+
+static FireOutputInternal m_pFireOutputInternal = nullptr;
+
} // namespace counterstrikesharp
\ No newline at end of file
diff --git a/src/core/memory_module.cpp b/src/core/memory_module.cpp
index 833c8422f..ad8f183b4 100644
--- a/src/core/memory_module.cpp
+++ b/src/core/memory_module.cpp
@@ -37,7 +37,7 @@ CModule::CModule(const char* path, const char* module) : m_pszModule(module), m_
void* CModule::FindSignature(const char* signature)
{
- if (strlen(signature) == 0) {
+ if (signature == nullptr || strlen(signature) == 0) {
return nullptr;
}
diff --git a/src/scripting/callback_manager.cpp b/src/scripting/callback_manager.cpp
index ae7c07c9f..6532fc0d2 100644
--- a/src/scripting/callback_manager.cpp
+++ b/src/scripting/callback_manager.cpp
@@ -118,4 +118,24 @@ void CallbackManager::PrintCallbackDebug()
CSSHARP_CORE_INFO("{0} ({0})\n", pCallback->GetName().c_str(), 1);
}
}
+CallbackPair::CallbackPair()
+{
+ pre = globals::callbackManager.CreateCallback("");
+ post = globals::callbackManager.CreateCallback("");
+}
+
+CallbackPair::CallbackPair(bool bNoCallbacks)
+{
+ if (!bNoCallbacks) {
+ pre = globals::callbackManager.CreateCallback("");
+ post = globals::callbackManager.CreateCallback("");
+ }
+}
+
+CallbackPair::~CallbackPair()
+{
+ globals::callbackManager.ReleaseCallback(pre);
+ globals::callbackManager.ReleaseCallback(post);
+}
+
} // namespace counterstrikesharp
\ No newline at end of file
diff --git a/src/scripting/callback_manager.h b/src/scripting/callback_manager.h
index f2c2f2052..5695e561f 100644
--- a/src/scripting/callback_manager.h
+++ b/src/scripting/callback_manager.h
@@ -35,7 +35,6 @@ class ScriptCallback
unsigned int GetFunctionCount() { return m_functions.size(); }
std::vector GetFunctions() { return m_functions; }
-
void Execute(bool bResetContext = true);
void Reset();
ScriptContextRaw& ScriptContext() { return m_script_context_raw; }
@@ -64,4 +63,18 @@ class CallbackManager : public GlobalClass
std::vector m_managed;
};
+class CallbackPair
+{
+ public:
+ CallbackPair();
+ CallbackPair(bool bNoCallbacks);
+ ~CallbackPair();
+ bool HasCallbacks() const
+ { return pre->GetFunctionCount() > 0 || post->GetFunctionCount() > 0; }
+
+ public:
+ ScriptCallback* pre;
+ ScriptCallback* post;
+};
+
} // namespace counterstrikesharp
\ No newline at end of file
diff --git a/src/scripting/natives/natives_entities.cpp b/src/scripting/natives/natives_entities.cpp
index 4529fba0a..6b47e7b5a 100644
--- a/src/scripting/natives/natives_entities.cpp
+++ b/src/scripting/natives/natives_entities.cpp
@@ -22,6 +22,7 @@
#include "core/memory.h"
#include "core/log.h"
#include "core/managers/player_manager.h"
+#include "core/managers/entity_manager.h"
#include
@@ -143,6 +144,24 @@ const char* GetPlayerIpAddress(ScriptContext& script_context) {
return pPlayer->GetIpAddress();
}
+void HookEntityOutput(ScriptContext& script_context)
+{
+ auto szClassname = script_context.GetArgument(0);
+ auto szOutput = script_context.GetArgument(1);
+ auto callback = script_context.GetArgument(2);
+ auto mode = script_context.GetArgument(3);
+ globals::entityManager.HookEntityOutput(szClassname, szOutput, callback, mode);
+}
+
+void UnhookEntityOutput(ScriptContext& script_context)
+{
+ auto szClassname = script_context.GetArgument(0);
+ auto szOutput = script_context.GetArgument(1);
+ auto callback = script_context.GetArgument(2);
+ auto mode = script_context.GetArgument(3);
+ globals::entityManager.UnhookEntityOutput(szClassname, szOutput, callback, mode);
+}
+
REGISTER_NATIVES(entities, {
ScriptEngine::RegisterNativeHandler("GET_ENTITY_FROM_INDEX", GetEntityFromIndex);
ScriptEngine::RegisterNativeHandler("GET_USERID_FROM_INDEX", GetUserIdFromIndex);
@@ -157,5 +176,7 @@ REGISTER_NATIVES(entities, {
ScriptEngine::RegisterNativeHandler("GET_FIRST_ACTIVE_ENTITY", GetFirstActiveEntity);
ScriptEngine::RegisterNativeHandler("GET_PLAYER_AUTHORIZED_STEAMID", GetPlayerAuthorizedSteamID);
ScriptEngine::RegisterNativeHandler("GET_PLAYER_IP_ADDRESS", GetPlayerIpAddress);
+ ScriptEngine::RegisterNativeHandler("HOOK_ENTITY_OUTPUT", HookEntityOutput);
+ ScriptEngine::RegisterNativeHandler("UNHOOK_ENTITY_OUTPUT", UnhookEntityOutput);
})
} // namespace counterstrikesharp
\ No newline at end of file
diff --git a/src/scripting/natives/natives_entities.yaml b/src/scripting/natives/natives_entities.yaml
index 915ea103b..81a014ddb 100644
--- a/src/scripting/natives/natives_entities.yaml
+++ b/src/scripting/natives/natives_entities.yaml
@@ -9,4 +9,6 @@ IS_REF_VALID_ENTITY: entityRef:uint -> bool
PRINT_TO_CONSOLE: index:int, message:string -> void
GET_FIRST_ACTIVE_ENTITY: -> pointer
GET_PLAYER_AUTHORIZED_STEAMID: slot:int -> uint64
-GET_PLAYER_IP_ADDRESS: slot:int -> string
\ No newline at end of file
+GET_PLAYER_IP_ADDRESS: slot:int -> string
+HOOK_ENTITY_OUTPUT: classname:string, outputName:string, callback:func, mode:HookMode -> void
+UNHOOK_ENTITY_OUTPUT: classname:string, outputName:string, callback:func, mode:HookMode -> void
\ No newline at end of file
diff --git a/tooling/CodeGen.Natives/Mapping.cs b/tooling/CodeGen.Natives/Mapping.cs
index 106988640..8d424d57c 100644
--- a/tooling/CodeGen.Natives/Mapping.cs
+++ b/tooling/CodeGen.Natives/Mapping.cs
@@ -61,6 +61,8 @@ public static string GetCSharpType(string type)
return "object[]";
case "SteamID":
return "[CastFrom(typeof(ulong))]SteamID";
+ case "HookMode":
+ return "HookMode";
case "DataType_t":
return "DataType";
case "any":