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":