From 4ccb09148b694d0c60d5f30528dc407a7bfd4a32 Mon Sep 17 00:00:00 2001 From: Roflmuffin Date: Sun, 15 Oct 2023 12:17:00 +1000 Subject: [PATCH] feat: add `OnClientAuthorized` listener & steamid class --- .../Core/Attributes/CastFromAttribute.cs | 18 ++++++++ .../CounterStrikeSharp.API/Core/BasePlugin.cs | 25 +++++++---- .../Core/Listeners.g.cs | 4 ++ .../Modules/Entities/SteamID.cs | 44 +++++++++++++++++++ managed/TestPlugin/TestPlugin.cs | 7 ++- src/core/managers/player_manager.cpp | 36 ++++++++++++++- src/core/managers/player_manager.h | 8 +++- src/core/timer_system.cpp | 3 ++ src/scripting/listeners/players.yaml | 3 +- tooling/CodeGen.Natives/Mapping.cs | 2 + .../Scripts/GenerateListeners.cs | 1 + 11 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 managed/CounterStrikeSharp.API/Core/Attributes/CastFromAttribute.cs create mode 100644 managed/CounterStrikeSharp.API/Modules/Entities/SteamID.cs diff --git a/managed/CounterStrikeSharp.API/Core/Attributes/CastFromAttribute.cs b/managed/CounterStrikeSharp.API/Core/Attributes/CastFromAttribute.cs new file mode 100644 index 000000000..ca909cbb6 --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Attributes/CastFromAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace CounterStrikeSharp.API.Core.Attributes; + +/** + * Indicates that the parameter should be pulled from the ScriptContext as the passed in type, + * then cast/converted to the parameter type. + */ +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +public class CastFromAttribute : Attribute +{ + public Type Type { get; } + + public CastFromAttribute(Type type) + { + Type = type; + } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs index 0d4d4a0c9..b613fab2b 100644 --- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs +++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs @@ -41,7 +41,7 @@ public BasePlugin() public abstract string ModuleName { get; } public abstract string ModuleVersion { get; } - + public string ModulePath { get; internal set; } public string ModuleDirectory => Path.GetDirectoryName(ModulePath); @@ -189,7 +189,7 @@ public void UnhookConVarChange(ConVar convar, ConVar.ConVarChangedCallback handl CommandHandlers.Remove(handler); } }*/ - + // Adds global listener, e.g. OnTick, OnClientConnect protected void RegisterListener(T handler) where T : Delegate { @@ -198,23 +198,29 @@ protected void RegisterListener(T handler) where T : Delegate { throw new Exception("Listener of type T is invalid and does not have a name attribute"); } - + var parameterTypes = typeof(T).GetMethod("Invoke").GetParameters().Select(p => p.ParameterType).ToArray(); + var castedParameterTypes = typeof(T).GetMethod("Invoke").GetParameters() + .Select(p => p.GetCustomAttribute()?.Type) + .ToArray(); Console.WriteLine($"Registering listener for {listenerName} with {parameterTypes.Length}"); - + var wrappedHandler = new Action(context => { var args = new object[parameterTypes.Length]; for (int i = 0; i < parameterTypes.Length; i++) { - args[i] = context.GetArgument(parameterTypes[i], i); + args[i] = context.GetArgument(castedParameterTypes[i] ?? parameterTypes[i], i); + if (castedParameterTypes[i] != null) + args[i] = Activator.CreateInstance(parameterTypes[i], new[] { args[i] }); } handler.DynamicInvoke(args); }); - - var subscriber = new CallbackSubscriber(handler, wrappedHandler, () => { RemoveListener(listenerName, handler); }); + + var subscriber = + new CallbackSubscriber(handler, wrappedHandler, () => { RemoveListener(listenerName, handler); }); NativeAPI.AddListener(listenerName, subscriber.GetInputArgument()); Listeners[handler] = subscriber; @@ -235,7 +241,7 @@ public Timer AddTimer(float interval, Action callback, TimerFlags? flags = null) Timers.Add(timer); return timer; } - + public void RegisterAllAttributes(object instance) { @@ -281,7 +287,8 @@ public void RegisterConsoleCommandAttributeHandlers(object instance) foreach (var eventHandler in eventHandlers) { var commandInfo = eventHandler.GetCustomAttribute(); - AddCommand(commandInfo.Command, commandInfo.Description, eventHandler.CreateDelegate(instance)); + AddCommand(commandInfo.Command, commandInfo.Description, + eventHandler.CreateDelegate(instance)); } } diff --git a/managed/CounterStrikeSharp.API/Core/Listeners.g.cs b/managed/CounterStrikeSharp.API/Core/Listeners.g.cs index 1897d8634..ac15ed078 100644 --- a/managed/CounterStrikeSharp.API/Core/Listeners.g.cs +++ b/managed/CounterStrikeSharp.API/Core/Listeners.g.cs @@ -1,6 +1,7 @@ using System; using CounterStrikeSharp.API.Core.Attributes; +using CounterStrikeSharp.API.Modules.Entities; namespace CounterStrikeSharp.API.Core { @@ -29,5 +30,8 @@ public partial class Listeners { [ListenerName("OnClientDisconnectPost")] public delegate void OnClientDisconnectPost(int index); + + [ListenerName("OnClientAuthorized")] + public delegate void OnClientAuthorized(int index, [CastFrom(typeof(ulong))]SteamID steamId); } } diff --git a/managed/CounterStrikeSharp.API/Modules/Entities/SteamID.cs b/managed/CounterStrikeSharp.API/Modules/Entities/SteamID.cs new file mode 100644 index 000000000..0cf6e875a --- /dev/null +++ b/managed/CounterStrikeSharp.API/Modules/Entities/SteamID.cs @@ -0,0 +1,44 @@ +using System; + +namespace CounterStrikeSharp.API.Modules.Entities +{ + public class SteamID + { + const long Base = 76561197960265728; + public ulong SteamId64 { get; set; } + + public SteamID(ulong id) => SteamId64 = id; + public SteamID(string id) => SteamId64 = id.StartsWith("[") ? ParseId3(id) : ParseId(id); + + public static explicit operator SteamID(ulong u) => new(u); + public static explicit operator SteamID(string s) => new(s); + + ulong ParseId(string id) + { + var parts = id.Split(':'); + if (parts.Length != 3 || !ulong.TryParse(parts[2], out var num)) throw new FormatException(); + return Base + num * 2 + (parts[1] == "1" ? 1UL : 0); + } + + ulong ParseId3(string id) + { + var parts = id.Replace("[", "").Replace("]", "").Split(':'); + if (parts.Length != 3 || !ulong.TryParse(parts[2], out var num)) throw new FormatException(); + return Base + num; + } + + public string SteamId2 + { + get => $"STEAM_0:{(SteamId64 - Base) % 2}:{(SteamId64 - Base) / 2}"; + set => SteamId64 = ParseId(value); + } + + public string SteamId3 + { + get => $"[U:1:{SteamId64 - Base}]"; + set => SteamId64 = ParseId3(value); + } + + public override string ToString() => $"[SteamID {SteamId64}, {SteamId2}, {SteamId3}]"; + } +} \ No newline at end of file diff --git a/managed/TestPlugin/TestPlugin.cs b/managed/TestPlugin/TestPlugin.cs index 921f368b8..02beea25b 100644 --- a/managed/TestPlugin/TestPlugin.cs +++ b/managed/TestPlugin/TestPlugin.cs @@ -21,6 +21,7 @@ using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; +using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Events; using CounterStrikeSharp.API.Modules.Memory; @@ -54,7 +55,11 @@ public override void Load(bool hotReload) { Log($"Client {name} from {ip} has connected!"); }); - + RegisterListener((index, id) => + { + Log($"Client {index} with address {id}"); + }); + // You can use `ModuleDirectory` to get the directory of the plugin (for storing config files, saving database files etc.) File.WriteAllText(Path.Join(ModuleDirectory, "example.txt"), $"Test file created by TestPlugin at {DateTime.Now}"); diff --git a/src/core/managers/player_manager.cpp b/src/core/managers/player_manager.cpp index 4bf77d1ab..850be69ae 100644 --- a/src/core/managers/player_manager.cpp +++ b/src/core/managers/player_manager.cpp @@ -104,6 +104,7 @@ void PlayerManager::OnAllInitialized() { m_on_client_disconnect_callback = globals::callbackManager.CreateCallback("OnClientDisconnect"); m_on_client_disconnect_post_callback = globals::callbackManager.CreateCallback("OnClientDisconnectPost"); + m_on_client_authorized_callback = globals::callbackManager.CreateCallback("OnClientAuthorized"); } void PlayerManager::OnShutdown() { @@ -125,6 +126,7 @@ void PlayerManager::OnShutdown() { globals::callbackManager.ReleaseCallback(m_on_client_put_in_server_callback); globals::callbackManager.ReleaseCallback(m_on_client_disconnect_callback); globals::callbackManager.ReleaseCallback(m_on_client_disconnect_post_callback); + globals::callbackManager.ReleaseCallback(m_on_client_authorized_callback); } bool PlayerManager::OnClientConnect(CPlayerSlot slot, @@ -397,8 +399,6 @@ IPlayerInfo *CPlayer::GetPlayerInfo() const { return m_info; } const char *CPlayer::GetName() const { return strdup(m_name.c_str()); } -const char *CPlayer::GetAuthString() { return ""; } - bool CPlayer::IsConnected() const { return m_is_connected; } bool CPlayer::IsFakeClient() const { return m_is_fake_client; } @@ -453,6 +453,36 @@ PlayerManager::PlayerManager() { memset(m_user_id_lookup, 0, sizeof(int) * (USHRT_MAX + 1)); } +void PlayerManager::RunAuthChecks() { + if (globals::getGlobalVars()->curtime - m_last_auth_check_time < 0.5F) { + return; + } + + m_last_auth_check_time = globals::getGlobalVars()->curtime; + + for (int i = 1; i <= m_max_clients; i++) { + if (m_players[i].IsConnected()) { + if (m_players[i].IsAuthorized() || m_players[i].IsFakeClient()) continue; + + if (globals::engine->IsClientFullyAuthenticated(i)) { + m_players[i].Authorize(); + m_players[i].SetSteamId(globals::engine->GetClientSteamID(i)); + OnAuthorized(&m_players[i]); + } + } + } +} + +void PlayerManager::OnAuthorized(CPlayer *player) const { + CSSHARP_CORE_TRACE("[PlayerManager][OnAuthorized] - {} {}", player->GetName(), + player->GetSteamId()->ConvertToUint64()); + + m_on_client_authorized_callback->ScriptContext().Reset(); + m_on_client_authorized_callback->ScriptContext().Push(player->m_slot.Get()); + m_on_client_authorized_callback->ScriptContext().Push(player->GetSteamId()->ConvertToUint64()); + m_on_client_authorized_callback->Execute(); +} + bool CPlayer::WasCountedAsInGame() const { return m_is_in_game; } int CPlayer::GetUserId() { @@ -543,5 +573,7 @@ bool CPlayer::IsAlive() const { return !m_info->IsDead(); } +const CSteamID *CPlayer::GetSteamId() { return m_steamId; } +void CPlayer::SetSteamId(const CSteamID *steam_id) { m_steamId = steam_id; } } // namespace counterstrikesharp \ No newline at end of file diff --git a/src/core/managers/player_manager.h b/src/core/managers/player_manager.h index e07296432..b9d26b96c 100644 --- a/src/core/managers/player_manager.h +++ b/src/core/managers/player_manager.h @@ -63,7 +63,8 @@ class CPlayer { public: const char *GetName() const; - const char *GetAuthString(); + const CSteamID *GetSteamId(); + void SetSteamId(const CSteamID *steam_id); bool IsConnected() const; bool IsFakeClient() const; bool IsAuthorized() const; @@ -102,6 +103,7 @@ class CPlayer { bool m_is_authorized = false; int m_user_id = 1; CPlayerSlot m_slot = CPlayerSlot(-1); + const CSteamID* m_steamId; std::string m_ip_address; void SetName(const char *name); INetChannelInfo *GetNetInfo() const; @@ -137,12 +139,14 @@ class PlayerManager : public GlobalClass { const char *pszName, uint64 xuid, const char *pszNetworkID) const; + void OnAuthorized(CPlayer* player) const; void OnServerActivate(edict_t *pEdictList, int edictCount, int clientMax) const; void OnThink(bool last_tick) const; void OnShutdown() override; void OnLevelEnd() override; void OnClientCommand(CPlayerSlot slot, const CCommand &args) const; int ListenClient() const; + void RunAuthChecks(); public: int NumPlayers() const; @@ -159,12 +163,14 @@ class PlayerManager : public GlobalClass { int *m_user_id_lookup; int m_listen_client; bool m_is_listen_server; + float m_last_auth_check_time = 0; ScriptCallback *m_on_client_connect_callback; ScriptCallback *m_on_client_put_in_server_callback; ScriptCallback *m_on_client_connected_callback; ScriptCallback *m_on_client_disconnect_callback; ScriptCallback *m_on_client_disconnect_post_callback; + ScriptCallback *m_on_client_authorized_callback; }; } // namespace counterstrikesharp \ No newline at end of file diff --git a/src/core/timer_system.cpp b/src/core/timer_system.cpp index ab94e979b..d51696166 100644 --- a/src/core/timer_system.cpp +++ b/src/core/timer_system.cpp @@ -37,6 +37,7 @@ #include "core/globals.h" #include "core/log.h" #include "scripting/callback_manager.h" +#include "core/managers/player_manager.h" namespace counterstrikesharp { namespace timers { @@ -95,6 +96,8 @@ void TimerSystem::OnGameFrame(bool simulating) { m_on_tick_callback_->ScriptContext().Reset(); m_on_tick_callback_->Execute(); } + + globals::playerManager.RunAuthChecks(); } double TimerSystem::CalculateNextThink(double last_think_time, float interval) { diff --git a/src/scripting/listeners/players.yaml b/src/scripting/listeners/players.yaml index 5c9d29162..a872485f4 100644 --- a/src/scripting/listeners/players.yaml +++ b/src/scripting/listeners/players.yaml @@ -2,4 +2,5 @@ OnClientConnect: index:int, name:string, ipAddress:string OnClientConnected: index:int OnClientPutInServer: index:int OnClientDisconnect: index:int -OnClientDisconnectPost: index:int \ No newline at end of file +OnClientDisconnectPost: index:int +OnClientAuthorized: index:int, steamId:SteamID \ No newline at end of file diff --git a/tooling/CodeGen.Natives/Mapping.cs b/tooling/CodeGen.Natives/Mapping.cs index 31b7ae5a5..b74b56d80 100644 --- a/tooling/CodeGen.Natives/Mapping.cs +++ b/tooling/CodeGen.Natives/Mapping.cs @@ -56,6 +56,8 @@ public static string GetCSharpType(string type) return "InputArgument"; case "object[]": return "object[]"; + case "SteamID": + return "[CastFrom(typeof(ulong))]SteamID"; } return "object"; diff --git a/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs b/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs index 996e950a6..b05615e0b 100644 --- a/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs +++ b/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs @@ -57,6 +57,7 @@ public static void GenerateListeners() var result = $@" using System; using CounterStrikeSharp.API.Core.Attributes; +using CounterStrikeSharp.API.Modules.Entities; namespace CounterStrikeSharp.API.Core {{