diff --git a/API/Events/EventId.cs b/API/Events/EventId.cs new file mode 100644 index 0000000..1e47e24 --- /dev/null +++ b/API/Events/EventId.cs @@ -0,0 +1,10 @@ +namespace WKLib.API.Events; + +public enum HookId +{ + GamemodeStart, + PlayerPerkAdded, + PlayerTookDamage, + PlayerDealtDamage, + PlayerTookGripStrengthDamage +} \ No newline at end of file diff --git a/API/Events/EventPriority.cs b/API/Events/EventPriority.cs new file mode 100644 index 0000000..32a17c1 --- /dev/null +++ b/API/Events/EventPriority.cs @@ -0,0 +1,10 @@ +namespace WKLib.API.Events; + +public enum EventPriority +{ + Earliest = -200, + Early = -100, + Normal = 0, + Late = 100, + Latest = 200 +} \ No newline at end of file diff --git a/API/Events/EventService.cs b/API/Events/EventService.cs new file mode 100644 index 0000000..d692179 --- /dev/null +++ b/API/Events/EventService.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using WKLib.Utilities; + +namespace WKLib.API.Events; + +public class EventService +{ + private readonly WKLibAPI _owner; + private readonly List<(HookId hook, Action wrapped)> _subscriptions = new(); + + internal EventService(WKLibAPI owner) => _owner = owner; + + public void Subscribe(HookId hook, Action callback, EventPriority priority = EventPriority.Normal) + { + WKLog.Debug($"[{_owner.DisplayName} ({_owner.GUID})] subscribing to {hook} with priority {priority}"); + Action wrapped = () => Invoke(hook, callback); + _subscriptions.Add((hook, wrapped)); + GameEvents.Subscribe(hook, wrapped, priority); + } + + private void Invoke(HookId hook, Action callback) + { + WKLog.Debug($"[{_owner.DisplayName} ({_owner.GUID})] invoking {hook} callback"); + try + { + callback(); + } + catch (Exception ex) + { + WKLog.Error($"[{_owner.DisplayName} ({_owner.GUID})] threw in {hook} callback: {ex}"); + } + } + + internal void UnsubscribeAll() + { + foreach (var (hook, wrapped) in _subscriptions) + GameEvents.Unsubscribe(hook, wrapped); + _subscriptions.Clear(); + } +} \ No newline at end of file diff --git a/API/Events/GameEvents.cs b/API/Events/GameEvents.cs new file mode 100644 index 0000000..e2d9271 --- /dev/null +++ b/API/Events/GameEvents.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using WKLib.Utilities; + +namespace WKLib.API.Events; + +public static class GameEvents +{ + private readonly struct Subscription + { + public readonly EventPriority Priority; + public readonly long Order; + public readonly Action Handler; + + public Subscription(EventPriority priority, long order, Action handler) + { + Priority = priority; + Order = order; + Handler = handler; + } + } + + private static readonly Dictionary> _handlers = new(); + private static long _nextOrder = 0; + + internal static void Subscribe(HookId hook, Action handler, EventPriority priority) + { + if (!_handlers.TryGetValue(hook, out var list)) + { + list = new List(); + _handlers[hook] = list; + } + + list.Add(new Subscription(priority, _nextOrder++, handler)); + + // handlers are sorted by priority, then by order of subscription + list.Sort((a, b) => + { + int cmp = a.Priority.CompareTo(b.Priority); + return cmp != 0 ? cmp : a.Order.CompareTo(b.Order); + }); + } + + internal static void Unsubscribe(HookId hook, Action handler) + { + if (_handlers.TryGetValue(hook, out var list)) + list.RemoveAll(s => s.Handler == handler); + } + + internal static void Raise(HookId hook) + { + WKLog.Debug($"Raising event {hook}"); + if (!_handlers.TryGetValue(hook, out var list) || list.Count == 0) + return; + + foreach (var sub in list.ToArray()) + sub.Handler.Invoke(); + } +} \ No newline at end of file diff --git a/API/WKLibAPI.cs b/API/WKLibAPI.cs index be341f2..e1df6ba 100644 --- a/API/WKLibAPI.cs +++ b/API/WKLibAPI.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using WKLib.API.Assets; +using WKLib.API.Events; using WKLib.API.UI; namespace WKLib.API; @@ -16,13 +17,15 @@ public class WKLibAPI public ModTab ModTab { get; internal set; } = null; public AssetService AssetService { get; internal set; } = null; - + public EventService EventService { get; internal set; } = null; + private WKLibAPI(string displayName, string guid, string defaultConfigFileName) { DisplayName = displayName; GUID = guid; AssetService = new AssetService(this); + EventService = new EventService(this); } /// diff --git a/Core/Patches/CL_GameManagerPatch.cs b/Core/Patches/CL_GameManagerPatch.cs index ab6ae7e..869d0e2 100644 --- a/Core/Patches/CL_GameManagerPatch.cs +++ b/Core/Patches/CL_GameManagerPatch.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using WKLib.API.Events; using WKLib.Core.Attributes; using WKLib.Core.UI; using static WKLib.Core.Config.ConfigManager; @@ -22,4 +23,10 @@ private static void CL_GameManager_UnPause(CL_GameManager __instance) RootPanel.Instance.IsOpen = false; } + + [HarmonyPatch(typeof(CL_GameManager), nameof(CL_GameManager.LoadIn)), HarmonyPostfix] + private static void CL_GameManager_LoadIn(CL_GameManager __instance) + { + GameEvents.Raise(HookId.GamemodeStart); + } } diff --git a/Core/Patches/ENT_PlayerPatch.cs b/Core/Patches/ENT_PlayerPatch.cs new file mode 100644 index 0000000..bddba87 --- /dev/null +++ b/Core/Patches/ENT_PlayerPatch.cs @@ -0,0 +1,28 @@ +using HarmonyLib; +using WKLib.API.Events; +using WKLib.Core.Attributes; + +namespace WKLib.Core.Patches; + +[PatchOnEntry] +[HarmonyPatch] +internal static class ENT_PlayerPatch +{ + [HarmonyPatch(typeof(ENT_Player), nameof(ENT_Player.AddPerk)), HarmonyPostfix] + private static void ENT_Player_AddPerk(ENT_Player __instance) + { + GameEvents.Raise(HookId.PlayerPerkAdded); + } + + [HarmonyPatch(typeof(ENT_Player), nameof(ENT_Player.Damage)), HarmonyPostfix] + private static void ENT_Player_Damage(ENT_Player __instance) + { + GameEvents.Raise(HookId.PlayerTookDamage); + } + + [HarmonyPatch(typeof(ENT_Player), nameof(ENT_Player.DamageGripStrength)), HarmonyPostfix] + private static void ENT_Player_DamageGripStrength(ENT_Player __instance) + { + GameEvents.Raise(HookId.PlayerTookGripStrengthDamage); + } +} \ No newline at end of file diff --git a/Core/Patches/GameEntityPatch.cs b/Core/Patches/GameEntityPatch.cs new file mode 100644 index 0000000..2ea9cba --- /dev/null +++ b/Core/Patches/GameEntityPatch.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Text; +using WKLib.API.Events; +using WKLib.Core.Attributes; + +namespace WKLib.Core.Patches +{ + [PatchOnEntry] + [HarmonyPatch] + internal static class GameEntityPatch + { + [HarmonyPatch(typeof(GameEntity), nameof(GameEntity.Damage)), HarmonyPostfix] + private static void GameEntity_Damage(GameEntity __instance, ref Damageable.DamageInfo info) + { + if (info.sourceEntity == ENT_Player.GetPlayer()) + { + GameEvents.Raise(HookId.PlayerDealtDamage); + } + } + } +} \ No newline at end of file