Skip to content

Custom Networking

Anomaly handles encryption, routing, replay protection, and version checks for you. Mod authors define an IAnomalyMessage, register it on both sides, and send it through AnomalyMessaging.

Do not parse or depend on Anomaly’s internal transport format.

Every custom message implements IAnomalyMessage from Anomaly.Shared.Networking:

using System.IO;
using Anomaly.Shared.Networking;
namespace MyMod.Shared.Networking.Messages;
public class GreetingMessage : IAnomalyMessage
{
public string MessageName => "mymod:greeting";
public MessageChannel TransportChannel => MessageChannel.ReliableOrdered;
public string Text { get; set; } = "";
public int Value { get; set; }
public void Serialize(BinaryWriter writer)
{
writer.Write(Text ?? "");
writer.Write(Value);
}
public void Deserialize(BinaryReader reader)
{
Text = reader.ReadString();
Value = reader.ReadInt32();
}
}

Four things matter:

  1. MessageName is a string in yourmod:name format.
  2. TransportChannel chooses delivery behavior.
  3. Serialize writes fields in a stable order.
  4. Deserialize reads fields in the same order.

Put custom message classes in a project that both client and server reference, usually MyMod.Shared. Keep it free of Unity, Mirror, LabAPI, and MelonLoader references.

If you need game-specific types such as vectors, serialize primitive values in the shared message and convert on each side.

Register the same name and factory on both sides after Anomaly has initialized.

// Client side, during OnInitializeMelon
using Anomaly.Shared.Networking;
AnomalyMessageRegistry.Register(
"mymod:greeting",
() => new GreetingMessage());
// Server side, during plugin Enable()
using Anomaly.Shared.Networking;
AnomalyMessageRegistry.Register(
"mymod:greeting",
() => new GreetingMessage());
using Anomaly.Client.Api.Networking;
AnomalyMessaging.Send(new GreetingMessage
{
Text = "hello",
Value = 42
});
using Anomaly.Server.Networking;
AnomalyMessaging.SendToClient(hub, new GreetingMessage
{
Text = "world",
Value = 7
});
AnomalyMessaging.SendToAll(new GreetingMessage { Text = "broadcast" });
AnomalyMessaging.SendToAllExcept(senderHub.connectionToClient, msg);
AnomalyMessaging.MessageReceived += msg =>
{
if (msg is GreetingMessage g)
MelonLogger.Msg($"Got greeting: {g.Text} ({g.Value})");
};

Or use the event system:

NetworkMessageEvents.AnomalyMessageReceived += ev =>
{
if (ev.Message is GreetingMessage g) { /* ... */ }
};
AnomalyMessaging.MessageReceived += (senderHub, msg) =>
{
if (msg is GreetingMessage g)
Logger.Info($"Received greeting: {g.Text}");
};

The server receives the sender’s ReferenceHub, so you can associate the message with the player using normal server APIs.

Pick the channel based on the kind of data you send.

ChannelUse for
ReliableOrderedCommands, purchases, settings, and one-shot state changes.
UnreliableHigh-frequency data where occasional loss is acceptable.
SequencedFrequent state where only the newest value matters.
ReliableSequencedReliable state where stale intermediate updates should be skipped.
ReliableUnorderedReliable independent messages that do not need ordering.

For most low-frequency mod features, start with ReliableOrdered.

Keep frequent messages small. Send only the fields the receiver needs, avoid large strings in per-frame updates, and prefer Sequenced for state that is immediately superseded by newer state.

  • Registered but not receiving. Check the opposite side registers the same message name.
  • Deserialize throws. Confirm fields are read in exactly the same order they were written.
  • Version mismatch. Client and server Anomaly releases are incompatible; update the mismatched side.
  • Message name conflict. Rename your message with a unique mod id prefix.

Assets and Localization.