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.
When you need a server component
Section titled “When you need a server component”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.
The skeleton
Section titled “The skeleton”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;}Deployment
Section titled “Deployment”-
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. -
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.
-
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.Infolines, any custom message names (e.g."mymod:greeting"),ClientConfigkeys 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.
Talking to the client
Section titled “Talking to the client”Your server companion can:
- React to LabAPI events —
PlayerEvents.Joined,PlayerEvents.Dying,RoundEvents.Starting, etc. Same API as any LabAPI plugin. - Send asset updates —
AssetSpawner.Spawn(...),AssetSpawner.SetTexture(...),AssetOverrides.SetOverride(...)ship to all connected clients. - Send custom messages — define an
IAnomalyMessagein a shared project, register it on both sides, and callAnomalyMessaging.SendToClient(hub, msg)/SendToAll(msg)/SendToAllExcept(connection, msg). See Custom Networking.
Using @anomaly IDs
Section titled “Using @anomaly IDs”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.
EXILED plugins
Section titled “EXILED plugins”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.