Write metamod plugin based on .NET 10 and later
- Full MetaMod API Support: Complete binding for MetaMod 1.21 (Interface 5:16)
- Memory Safety: Advanced GC lifecycle management to prevent dangling pointers
- C#-Style API: Modern, idiomatic C# API design with optional C++ style compatibility
- AOT Compatible: Native AOT compilation support for optimal performance
- Type Safety: Strongly-typed wrappers for all engine structures
dotnet add package DrAbc.NuggetModusing NuggetMod.Interface;
using NuggetMod.Enum.Metamod;
using NuggetMod.Wrapper.Metamod;
public class MyPlugin : IPlugin
{
public MetaPluginInfo GetPluginInfo() => new()
{
InterfaceVersion = InterfaceVersion.V5_16,
Name = "MyPlugin",
Version = "1.0.0",
Date = "2024-01-01",
Author = "Your Name",
Url = "https://github.com/yourname/myplugin",
LogTag = "[MYPLUGIN]",
Loadable = PluginLoadTime.Anytime,
Unloadable = PluginLoadTime.Anytime
};
public void MetaInit()
{
// Plugin initialization code
}
public bool MetaQuery(InterfaceVersion interfaceVersion, MetaUtilFunctions pMetaUtilFuncs)
{
return true; // Return true if compatible
}
public bool MetaAttach(PluginLoadTime now, MetaGlobals pMGlobals, MetaGameDLLFunctions pGamedllFuncs)
{
// Register event handlers
var events = new DLLEvents();
events.ClientConnect += OnClientConnect;
SafeEventRegistration.Register(new EventRegistrationBuilder()
.WithEntityApi(events));
return true;
}
public bool MetaDetach(PluginLoadTime now, PluginUnloadReason reason)
{
SafeEventRegistration.UnregisterAll();
return true;
}
private (MetaResult, bool) OnClientConnect(Edict pEntity, string pszName, string pszAddress, ref string szRejectReason)
{
Console.WriteLine($"Player {pszName} connecting from {pszAddress}");
return (MetaResult.Handled, true);
}
}To quickly set up your first MetaMod plugin, refer to the template repository:
Or refer ChatEngine
# Build with AOT for Windows x86
dotnet publish -c Release -r win-x86 -o ./build -p:PublishAot=true
# Build with AOT for Linux x86
dotnet publish -c Release -r linux-x86 -o ./build -p:PublishAot=trueCritical: When passing managed delegates to native code, you must ensure they are not garbage collected while native code may still invoke them.
Always use SafeEventRegistration to register event handlers:
// CORRECT: Uses SafeEventRegistration
var builder = new EventRegistrationBuilder()
.WithEntityApi(new MyDLLEvents())
.WithEngineFunctions(new MyEngineEvents());
SafeEventRegistration.Register(builder);Check for conflicts before registering:
var builder = new EventRegistrationBuilder()
.WithEntityApi(events);
var validation = SafeEventRegistration.ValidateRegistration(builder);
if (!validation.CanRegister)
{
Console.WriteLine($"Conflicts: {string.Join(", ", validation.ConflictingTypes)}");
return;
}
SafeEventRegistration.Register(builder);For custom native interop, use DelegateLifetimeManager:
// Keep delegate alive
var myDelegate = new MyDelegateType(MyCallback);
DelegateLifetimeManager.Register("MyComponent_MyCallback", myDelegate);
// Later, when no longer needed
DelegateLifetimeManager.Unregister("MyComponent_MyCallback");public void AnalyzeModules()
{
var metaUtil = MetaMod.MetaUtilFuncs;
// Get engine module information
nint engineBase = metaUtil.GetEngineBase();
nint engineHandle = metaUtil.GetEngineHandle();
// Get code section for pattern scanning
var (codeBase, codeSize) = metaUtil.GetCodeSection(engineBase);
Console.WriteLine($"Engine code: 0x{codeBase:X} - 0x{codeBase + codeSize:X}");
// Search for pattern in code section
byte[] pattern = [0x55, 0x8B, 0xEC]; // x86 function prologue
nint address = metaUtil.SearchPatternInCodeSection(engineBase, pattern);
}public void InstallHook()
{
var metaUtil = MetaMod.MetaUtilFuncs;
// Get function addresses
nint gameDllBase = metaUtil.GetGameDllBase();
nint targetFunc = metaUtil.GetProcAddress(gameDllBase, "TargetFunction");
nint hookFunc = metaUtil.GetProcAddress(metaUtil.GetModuleHandle("myplugin.dll"), "HookFunction");
// Install inline hook
var hook = metaUtil.InlineHook(targetFunc, hookFunc, out nint originalCall, false);
// Later: remove hook
metaUtil.UnHook(hook);
}public void ManageClients()
{
var metaUtil = MetaMod.MetaUtilFuncs;
// Query client cvar
int requestId = metaUtil.MakeRequestID();
MetaMod.EngineFuncs.QueryClientCvarValue2(playerEdict, "cl_crosshair_color", requestId);
// Get user message ID
int msgId = metaUtil.GetUserMessageId("DeathMsg", out int size);
// Send center message
metaUtil.CenterSay("Welcome to the server!");
}The following APIs are maintained for C++ compatibility but marked obsolete. Use the C# alternatives instead:
| Deprecated API | Recommended Alternative |
|---|---|
MetaMod.RegisterEvents() |
SafeEventRegistration.Register() |
| Direct delegate assignment | EventRegistrationBuilder |
Manual GCHandle management |
DelegateLifetimeManager |
- Interface 5:16 (Latest - MetaMod 1.21-p)
- Interface 5:15 (MetaMod 1.21)
- Interface 5:13 (MetaMod 1.20)
- All versions back to 1.0
Run the test suite:
dotnet testThe test suite includes:
- Memory safety validation
- GC lifecycle tests
- API compatibility tests
- Event registration tests
Contributions are welcome! Please ensure:
- All tests pass
- New features include corresponding tests
- Memory safety is maintained for all native interop
- API follows C# conventions
GPL-3.0-or-later