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.
Client
Section titled “Client”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"); }}Server
Section titled “Server”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.
Build and deploy
Section titled “Build and deploy”- Build one
MyMod.Shared.dllcontaining the bounded directional messages, targeting a framework compatible with both runtimes. - Reference it from the
net6.0/x64 client andnet48/x64 server companion, using the matching Anomaly SDK. - Put the client mod in
Mods/and its shared dependency inUserLibs/for the selected loadout. - Put the server plugin in
plugins/global/and its shared dependency independencies/global/. - Complete the loadout’s trust review, restart the server, connect, and enter
mymod.ping.
For content instead of messages, continue to Custom Items.