Skip to content

Commit

Permalink
Add Entity Output Hooks (#174)
Browse files Browse the repository at this point in the history
Co-authored-by: Poggu <[email protected]>
Co-authored-by: KillStr3aK <[email protected]>
Co-authored-by: Poggu <[email protected]>
  • Loading branch information
4 people authored Dec 7, 2023
1 parent 1d6bee0 commit 2a15a8d
Show file tree
Hide file tree
Showing 24 changed files with 630 additions and 12 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down
7 changes: 7 additions & 0 deletions configs/addons/counterstrikesharp/gamedata/gamedata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
5 changes: 5 additions & 0 deletions docfx/examples/WithEntityOutputHooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[!INCLUDE [WithEntityOutputHooks](../../examples/WithEntityOutputHooks/README.md)]

<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WithEntityOutputHooks" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>

[!code-csharp[](../../examples/WithEntityOutputHooks/WithEntityOutputHooksPlugin.cs)]
2 changes: 2 additions & 0 deletions docfx/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions examples/WithEntityOutputHooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# With Entity Output Hooks
This example shows how to implement hooks for entity output, such as StartTouch, OnPickup etc.
12 changes: 12 additions & 0 deletions examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
</Project>
43 changes: 43 additions & 0 deletions examples/WithEntityOutputHooks/WithEntityOutputHooksPlugin.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
26 changes: 26 additions & 0 deletions managed/CounterStrikeSharp.API/Core/API.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
91 changes: 91 additions & 0 deletions managed/CounterStrikeSharp.API/Core/BasePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,6 +37,13 @@ public abstract class BasePlugin : IPlugin

public BasePlugin()
{
RegisterListener<Listeners.OnMapEnd>(() =>
{
foreach (KeyValuePair<Delegate, EntityIO.EntityOutputCallback> callback in EntitySingleOutputHooks)
{
UnhookSingleEntityOutputInternal(callback.Value.Classname, callback.Value.Output, callback.Value.Handler);
}
});
}

public abstract string ModuleName { get; }
Expand Down Expand Up @@ -108,6 +116,12 @@ public void Dispose()
public readonly Dictionary<Delegate, CallbackSubscriber> Listeners =
new Dictionary<Delegate, CallbackSubscriber>();

public readonly Dictionary<Delegate, CallbackSubscriber> EntityOutputHooks =
new Dictionary<Delegate, CallbackSubscriber>();

internal readonly Dictionary<Delegate, EntityIO.EntityOutputCallback> EntitySingleOutputHooks =
new Dictionary<Delegate, EntityIO.EntityOutputCallback>();

public readonly List<Timer> Timers = new List<Timer>();

public delegate HookResult GameEventHandler<T>(T @event, GameEventInfo info) where T : GameEvent;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -430,6 +445,77 @@ public void RegisterConsoleCommandAttributeHandlers(object instance)
}
}

public void RegisterEntityOutputAttributeHandlers(object instance)
{
var handlers = instance.GetType()
.GetMethods()
.Where(method => method.GetCustomAttributes<EntityOutputHookAttribute>().Any())
.ToArray();

foreach (var handler in handlers)
{
var attributes = handler.GetCustomAttributes<EntityOutputHookAttribute>();
foreach (var outputInfo in attributes)
{
HookEntityOutput(outputInfo.Classname, outputInfo.OutputName, handler.CreateDelegate<EntityIO.EntityOutputHandler>(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);
Expand Down Expand Up @@ -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();
Expand Down
49 changes: 49 additions & 0 deletions managed/CounterStrikeSharp.API/Core/Model/CEntityIOOutput.cs
Original file line number Diff line number Diff line change
@@ -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<EntityIOConnection_t>(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<uint>((void*)(Handle + 8));
public unsafe ref uint OutputOffset => ref Unsafe.AsRef<uint>((void*)(Handle + 16));
}

public class EntityIOConnection_t : EntityIOConnectionDesc_t
{
public EntityIOConnection_t(IntPtr pointer) : base(pointer)
{
}

public unsafe ref bool MarkedForRemoval => ref Unsafe.AsRef<bool>((void*)(Handle + 40));

public EntityIOConnection_t? Next => Utilities.GetPointer<EntityIOConnection_t>(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<EntityIOTargetType_t>((void*)(Handle + 28));
public unsafe ref int TimesToFire => ref Unsafe.AsRef<int>((void*)(Handle + 32));
public unsafe ref float Delay => ref Unsafe.AsRef<float>((void*)(Handle + 36));
}
13 changes: 13 additions & 0 deletions managed/CounterStrikeSharp.API/Core/Model/CVariant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CounterStrikeSharp.API.Core;

/// <summary>
/// Placeholder for CVariant
/// <see href="https://github.com/alliedmodders/hl2sdk/blob/cs2/public/variant.h"/>
/// <remarks>A lot of entity outputs do not use this value</remarks>
/// </summary>
public class CVariant : NativeObject
{
public CVariant(IntPtr pointer) : base(pointer)
{
}
}
39 changes: 39 additions & 0 deletions managed/CounterStrikeSharp.API/Modules/Entities/EntityIO.cs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>. *
*/

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;
}
}
}
}
Loading

0 comments on commit 2a15a8d

Please sign in to comment.