feat: Implement hook registry and related interfaces for dynamic hook management

This commit is contained in:
Marvin
2026-04-20 05:41:52 +02:00
parent a11caa1175
commit 932a72fa88
6 changed files with 190 additions and 2 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ The framework is strictly layered (as detailed in `modding_core_architecture_sum
1. `HarmonyPatches` intercept Unity methods (Prefix/Postfix).
2. Data is extracted into primitive/struct DTOs.
3. Dispatched via `GregEventBus` using canonical hook names: `greg.<Domain>.<Event>`.
4. Mappings are defined in `assets/greg_hooks.json` and `NativeEventHooks.cs`.
4. Mappings are defined in `assets/greg_hooks.json` and loaded dynamically via `GregHookRegistry` (`IGregHookRegistry`), providing stable hashed Event-IDs for the FFI.
## 4. Conventions and Rules
+1 -1
View File
@@ -53,7 +53,7 @@
5. Hook-Payload wird normalisiert (`BuildPayload(...)`) und an Bus/FFI weitergereicht.
### Canonical Hook-Namen
- Mapping zentral in `src/gregSdk/gregNativeEventHooks.cs`.
- Mapping erfolgt dynamisch via `GregHookRegistry` (`assets/greg_hooks.json`), was Hash-basierte Event-IDs für das FFI generiert.
- Primärquelle für Namen: `greg_hooks.json` + framework-only Ergänzungen via `gregHookName.Create(...)`.
- Fallback bei unbekannten IDs: `greg.SYSTEM.UnmappedNativeEvent`.
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using gregCore.Core.Models;
namespace gregCore.Core.Abstractions;
public interface IGregHookRegistry
{
/// <summary>
/// Ruft alle geladenen Hooks ab.
/// </summary>
IEnumerable<GregHookDef> GetAllHooks();
/// <summary>
/// Sucht einen Hook anhand seines Namens.
/// </summary>
bool TryGetHook(string name, out GregHookDef hookDef);
/// <summary>
/// Sucht eine Event-ID für einen Hooknamen (FFI Kompatibilität).
/// </summary>
bool TryGetEventId(string hookName, out int eventId);
/// <summary>
/// Sucht einen Hooknamen anhand der Event-ID (FFI Kompatibilität).
/// </summary>
bool TryGetHookName(int eventId, out string hookName);
}
+34
View File
@@ -0,0 +1,34 @@
namespace gregCore.Core.Models;
public class GregHookPayloadSchema
{
public string TargetType { get; set; } = string.Empty;
public bool IsStatic { get; set; }
public string HookSubject { get; set; } = string.Empty;
public string ReturnType { get; set; } = string.Empty;
public string Parameters { get; set; } = string.Empty;
}
public class GregHookDef
{
public string Name { get; set; } = string.Empty;
public string Legacy { get; set; } = string.Empty;
public string PatchTarget { get; set; } = string.Empty;
public string Strategy { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string HookSubject { get; set; } = string.Empty;
public bool HotLoop { get; set; }
public string ClassName { get; set; } = string.Empty;
public string MethodName { get; set; } = string.Empty;
public string FriendlyAlias { get; set; } = string.Empty;
public GregHookPayloadSchema? PayloadSchema { get; set; }
}
public class GregHooksManifest
{
public int Version { get; set; }
public string Description { get; set; } = string.Empty;
public string GeneratedFrom { get; set; } = string.Empty;
public List<GregHookDef> Hooks { get; set; } = new();
}
@@ -11,6 +11,7 @@ using gregCore.Infrastructure.Plugins;
using gregCore.Infrastructure.Scripting.Lua;
using gregCore.Infrastructure.Scripting.Js;
using gregCore.GameLayer.Hooks;
using gregCore.Core.Abstractions;
namespace gregCore.GameLayer.Bootstrap;
@@ -28,6 +29,7 @@ internal static class GregBootstrapper
container.Register<IGregEventBus>(bus);
container.Register<IGregConfigService>(new GregConfigService(logger));
container.Register<IGregPersistenceService>(new GregPersistenceService(logger));
container.Register<IGregHookRegistry>(new GregHookRegistry(logger));
var apiContext = new global::gregCore.PublicApi.GregApiContext {
Logger = logger,
@@ -0,0 +1,125 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using gregCore.Core.Abstractions;
using gregCore.Core.Models;
namespace gregCore.Infrastructure.Config;
/// <summary>
/// Registry, die greg_hooks.json zur Laufzeit lädt und verwaltet.
/// </summary>
public class GregHookRegistry : IGregHookRegistry
{
private readonly IGregLogger _logger;
private readonly List<GregHookDef> _hooks = new();
private readonly Dictionary<string, GregHookDef> _hookByName = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, int> _hookToId = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, string> _idToHook = new();
public GregHookRegistry(IGregLogger logger)
{
_logger = logger;
LoadHooks();
}
private void LoadHooks()
{
try
{
string hooksFile = ResolveHooksFilePath();
if (File.Exists(hooksFile))
{
_logger.Debug($"Lade Hooks von: {hooksFile}");
var content = File.ReadAllText(hooksFile);
var manifest = JsonConvert.DeserializeObject<GregHooksManifest>(content);
if (manifest?.Hooks != null)
{
_hooks.AddRange(manifest.Hooks);
foreach (var hook in manifest.Hooks)
{
if (string.IsNullOrEmpty(hook.Name)) continue;
_hookByName[hook.Name] = hook;
if (!string.IsNullOrEmpty(hook.FriendlyAlias))
{
_hookByName[hook.FriendlyAlias] = hook;
}
// Generiere konsistente Hash-basierte ID für FFI
int eventId = GetStableHashCode(hook.Name);
_hookToId[hook.Name] = eventId;
_idToHook[eventId] = hook.Name;
}
_logger.Info($"Erfolgreich {_hooks.Count} Hooks aus greg_hooks.json geladen.");
}
}
else
{
_logger.Warning($"greg_hooks.json nicht gefunden bei: {hooksFile}. Hook-Registry bleibt leer.");
}
}
catch (Exception ex)
{
_logger.Error("Fehler beim Laden der greg_hooks.json", ex);
}
}
private string ResolveHooksFilePath()
{
// 1. Priorität: MelonLoader Mods Ordner
var modsDir = global::MelonLoader.Utils.MelonEnvironment.ModsDirectory;
if (!string.IsNullOrEmpty(modsDir))
{
var path = Path.Combine(modsDir, "greg_hooks.json");
if (File.Exists(path)) return path;
}
// 2. Priorität: Assembly Location
var asmLoc = Assembly.GetExecutingAssembly().Location;
var asmDir = Path.GetDirectoryName(asmLoc);
if (!string.IsNullOrEmpty(asmDir))
{
var path = Path.Combine(asmDir, "greg_hooks.json");
if (File.Exists(path)) return path;
}
// 3. Fallback: Project assets
return Path.Combine(Environment.CurrentDirectory, "assets", "greg_hooks.json");
}
private static int GetStableHashCode(string str)
{
// Garantiert einen stabilen, positiven 32-bit Hash über verschiedene Laufzeiten hinweg
unchecked
{
int hash1 = 5381;
int hash2 = hash1;
for (int i = 0; i < str.Length && str[i] != '\0'; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1 || str[i + 1] == '\0') break;
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
return Math.Abs(hash1 + (hash2 * 1566083941));
}
}
public IEnumerable<GregHookDef> GetAllHooks() => _hooks;
public bool TryGetHook(string name, out GregHookDef hookDef) =>
_hookByName.TryGetValue(name, out hookDef!);
public bool TryGetEventId(string hookName, out int eventId) =>
_hookToId.TryGetValue(hookName, out eventId);
public bool TryGetHookName(int eventId, out string hookName) =>
_idToHook.TryGetValue(eventId, out hookName!);
}