Skip to content

Server Companion Quickstart

A client-only mod can do a lot — but once you need authoritative state (a player’s real health, a server-wide cooldown, an admin-issued command) or effects that have to appear identically on every client in the round, you need a server-side component. This chapter walks through standing up a LabAPI plugin that pairs with your client mod.

Common cases:

  • Authoritative state — a “coin” or score system that players can’t edit locally.
  • Cross-player effects — a buff that everyone in the room sees, not just the caster.
  • Admin integration — a custom remote-admin command or RA-gated action.
  • Anti-cheat-sensitive logic — anything where trusting the client is a bug.

If your feature is purely cosmetic, local-only, or observational, you don’t need a server component. See Architecture Choices for the decision tree.

A minimum-viable server companion is a LabAPI Plugin<Config> that references Anomaly.Server.dll and Anomaly.Shared.dll:

using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using LabApi.Loader.Features.Plugins;
using Anomaly.Server.Networking;
namespace MyMod.Server;
public class Plugin : Plugin<Config>
{
public override string Name => "MyMod";
public override string Description => "Server companion for MyMod.";
public override string Author => "YourName";
public override System.Version Version => new(0, 1, 0);
public override System.Version RequiredApiVersion => new(1, 1, 5);
public override void Enable()
{
PlayerEvents.Joined += OnJoined;
}
public override void Disable()
{
PlayerEvents.Joined -= OnJoined;
}
private void OnJoined(PlayerJoinedEventArgs ev)
{
// ev.Player.UserId is the @anomaly ID on an Anomaly-enabled server
Logger.Info($"Player joined: {ev.Player.UserId}");
}
}

Config.cs:

namespace MyMod.Server;
public class Config
{
public bool FeatureEnabled { get; set; } = true;
public int CooldownSeconds { get; set; } = 30;
}
  1. Build your plugin targeting .NET Framework 4.8. LabAPI and its ecosystem target netfx; SDK-style projects work fine as long as you set the target framework correctly.

  2. Copy three DLLs into your LabAPI server’s plugins directory:

    • MyMod.Server.dll — your plugin.
    • Anomaly.Server.dll — required at runtime for Anomaly types.
    • Anomaly.Shared.dll — required at runtime for shared message contracts.

    All three need to be present alongside your normal LabAPI install. See Install Anomaly.Server for the host setup.

  3. Restart the server. LabAPI loads plugins on boot; your Enable() runs during the standard plugin-init sequence.

Sharing the mod id across client and server

Section titled “Sharing the mod id across client and server”

Use the same mod id on both sides. The id is a string you’ll thread through several places — make sure these agree:

  • Client: CommandRegistry.Register("mymod", ...), Scheduler.StartCooldown("mymod.x", ...), Tr.RegisterMod("mymod", ...).
  • Server: Logger.Info lines, any custom message names (e.g. "mymod:greeting"), ClientConfig keys if you read the same config key on the server for any reason.

There is no framework-enforced link between client and server mod ids — they’re just strings — but getting them identical makes logs correlate cleanly and prevents “wait, which mod is this?” confusion.

Your server companion can:

  1. React to LabAPI eventsPlayerEvents.Joined, PlayerEvents.Dying, RoundEvents.Starting, etc. Same API as any LabAPI plugin.
  2. Send asset updatesAssetSpawner.Spawn(...), AssetSpawner.SetTexture(...), AssetOverrides.SetOverride(...) ship to all connected clients.
  3. Send custom messages — define an IAnomalyMessage in a shared project, register it on both sides, and call AnomalyMessaging.SendToClient(hub, msg) / SendToAll(msg) / SendToAllExcept(connection, msg). See Custom Networking.

On an Anomaly-enabled server, ev.Player.UserId is the player’s stable @anomaly ID. Use it the same way you would use a Steam ID:

if (ev.Player.UserId == "abc123DefGHJ...@anomaly")
GrantSpecial(ev.Player);

Or store per-player state keyed off UserId:

private readonly Dictionary<string, int> _coins = new();
private void OnJoined(PlayerJoinedEventArgs ev)
{
_coins.TryAdd(ev.Player.UserId, 0);
}

IDs survive between sessions as long as the player keeps the same user.json, so storing per-player state by UserId is legitimate.

If you’re building in the EXILED ecosystem rather than pure LabAPI, use EXILED’s documented APIs to read the underlying user ID and test the plugin on an Anomaly server.

Custom Networking — define and register your own shared message types.