Skip to content

End-to-End Tutorial

This walkthrough uses PingRequest and PingResponse from Custom Networking. Keep those message types in one shared assembly used unchanged by both runtimes. Start with the client and server companion projects, then add the feature classes below.

Call ClientFeature.Enable() from your MelonMod’s OnInitializeMelon() and ClientFeature.Disable() from OnDeinitializeMelon(). Run the command only after connecting.

using System;
using System.Collections.Generic;
using Anomaly.Client.Api.Commands;
using Anomaly.Client.Api.Networking;
using Anomaly.Shared.Networking;
using MelonLoader;
using MyMod.Shared;
public sealed class PingCommand : ICommand
{
public string Name => "ping";
public IReadOnlyList<string> Aliases => Array.Empty<string>();
public string Description => "Ping the authenticated Anomaly server.";
public void Execute(CommandContext context)
{
try
{
AnomalyMessaging.Send(new PingRequest
{
ClientTicks = DateTime.UtcNow.Ticks,
});
context.Reply("Ping sent.");
}
catch (Exception exception)
{
context.Reply($"Ping could not be sent: {exception.Message}");
}
}
}
public static class ClientFeature
{
public static void Enable()
{
AnomalyMessageRegistry.Register(
"mymod:ping_request",
() => new PingRequest());
AnomalyMessageRegistry.Register(
"mymod:ping_response",
() => new PingResponse());
CommandRegistry.Register("mymod", new PingCommand());
AnomalyMessaging.MessageReceived += OnMessage;
}
public static void Disable()
{
AnomalyMessaging.MessageReceived -= OnMessage;
CommandRegistry.UnregisterAll("mymod");
}
private static void OnMessage(IAnomalyMessage message)
{
if (message is not PingResponse response)
return;
long elapsed = DateTime.UtcNow.Ticks - response.ClientTicks;
MelonLogger.Msg(
$"Round trip: {TimeSpan.FromTicks(elapsed).TotalMilliseconds:F0} ms");
}
}

Call ServerFeature.Enable() from your LabAPI plugin’s Enable() and ServerFeature.Disable() from Disable().

using System;
using Anomaly.Server.Api.Networking;
using Anomaly.Server.Api.Events.Arguments;
using Anomaly.Server.Api.Events.Handlers;
using Anomaly.Shared.Networking;
using LabApi.Features.Console;
using MyMod.Shared;
public static class ServerFeature
{
public static void Enable()
{
AnomalyMessageRegistry.Register(
"mymod:ping_request",
() => new PingRequest());
AnomalyMessageRegistry.Register(
"mymod:ping_response",
() => new PingResponse());
ClientEvents.MessageReceived += OnMessage;
}
public static void Disable()
{
ClientEvents.MessageReceived -= OnMessage;
}
private static void OnMessage(
ClientMessageReceivedEventArgs ev)
{
if (ev.Message is not PingRequest request)
return;
Logger.Info($"Ping from {ev.Player.UserId}");
if (!ClientMessaging.TrySend(ev.Player, new PingResponse
{
ClientTicks = request.ClientTicks,
ServerTicks = DateTime.UtcNow.Ticks,
}))
Logger.Warn("Ping reply could not be delivered.");
}
}

The server receives the authenticated sender. The ID is server-local, so it is suitable for this server’s rate limits and moderation records but not cross-server correlation.

  1. Build one MyMod.Shared.dll containing the bounded directional messages, targeting a framework compatible with both runtimes.
  2. Reference it from the net6.0/x64 client and net48/x64 server companion, using the matching Anomaly SDK.
  3. Put the client mod in Mods/ and its shared dependency in UserLibs/ for the selected loadout.
  4. Put the server plugin in plugins/global/ and its shared dependency in dependencies/global/.
  5. Complete the loadout’s trust review, restart the server, connect, and enter mymod.ping.

For content instead of messages, continue to Custom Items.