Skip to content

Server Mod

Pixnop edited this page Aug 7, 2026 · 6 revisions

Nimbus.ServerMod is a standard Vintage Story server-side code mod. It must be installed on every backend server that connects to Nimbus. It works with vanilla VS.

Installation

  1. Copy Nimbus.ServerMod into your VS server's Data/Mods/ folder.
  2. Start the server once to generate ModConfig/nimbus-server.json.
  3. Edit the config (see below).
  4. Restart.

Configuration - nimbus-server.json

{
  "Enabled": true,
  "ServerId": "hub",
  "DisplayName": "Hub",
  "PublicHost": "play.example.com",
  "PublicPort": 42420,
  "RegistryUrl": "http://127.0.0.1:8765",
  "SharedSecret": "PASTE-THE-SECRET-FROM-nimbus.proxy.toml",
  "HeartbeatIntervalSeconds": 5,
  "RegistryHttpTimeoutSeconds": 5,
  "Maintenance": false,
  "ReservationRequired": true,
  "FailClosedWhenRegistryUnreachable": false,
  "AllowPlayerServerCommand": true,
  "TransferMode": "redirect",
  "SeamlessPrepareAckTimeoutSeconds": 8,
  "ShortcutCommands": [],
  "Tags": []
}

Field reference

Field Default Description
Enabled true Set false to disable the mod without removing it. The mod loads but does nothing.
ServerId "" Required. Unique ID for this backend. Must exactly match the key in the proxy's [servers] table.
DisplayName "" Human-readable name shown in /nimbus servers output. Defaults to ServerId if empty.
PublicHost "" The hostname players use to reach the network, the proxy address. Used in registry listings.
PublicPort 42420 The port players use, the proxy port, not this backend's port.
RegistryUrl "" URL of the Nimbus registry. For embedded mode: http://127.0.0.1:8765 (or wherever embedded_bind is set).
SharedSecret PASTE-THE-SECRET-FROM-nimbus.proxy.toml HMAC secret. Must match embedded_shared_secret (or shared_secret for a standalone registry). Never generated here: the default names the file to copy the real one out of. See below.
HeartbeatIntervalSeconds 5 How often to send heartbeats. The registry marks backends stale after 20 seconds, so keep this well below that.
RegistryHttpTimeoutSeconds 5 HTTP timeout for all registry API calls.
Maintenance false When true, marks this backend as in maintenance. The proxy router skips it for new connections.
ReservationRequired true When true, players who join without a valid proxy reservation are kicked. Use this to enforce that all players arrive through the proxy.
FailClosedWhenRegistryUnreachable false What to do when ReservationRequired is on but the registry cannot be reached to confirm a reservation. Default fails open (the player is let in, so an outage does not lock everyone out). Set true to fail closed on a gated network, where an outage must not become a way around the proxy.
AllowPlayerServerCommand true When true, players can use /server <id> and /join <id> to switch backends.
TransferMode redirect Transfer mode used for player-initiated switches. redirect or seamless.
SeamlessPrepareAckTimeoutSeconds 8 How long a seamless transfer waits for the client to acknowledge the prepare packet before aborting. Only used in seamless mode; clamped to 1-30.
ShortcutCommands [] Named shortcuts for /server, so players type /hub instead of /server hub. See below.
Tags [] Arbitrary string tags sent with heartbeats. Readable by plugins via the registry snapshot.

Minimum working config

The mod stays idle (logging a warning) until ServerId, RegistryUrl, PublicHost, and SharedSecret are all set. Enabled = true is the default - you just need to fill in these four fields:

{
  "ServerId": "hub",
  "PublicHost": "play.example.com",
  "RegistryUrl": "http://127.0.0.1:8765",
  "SharedSecret": "your-shared-secret-here"
}

The secret is copied, never invented

SharedSecret is the one of those four that is not simply about emptiness. Every placeholder this project has ever documented counts as unset, and the mod treats a config holding one exactly as it treats a config holding nothing. The list is short and nothing is ever removed from it:

Literal Where it came from
PASTE-THE-SECRET-FROM-nimbus.proxy.toml what the mod writes into a nimbus-server.json it had to create
change-me-and-keep-secret the default shipped everywhere through v0.4.0
REPLACE_ME_WITH_A_LONG_RANDOM_STRING this wiki's older wording for the same thing
REPLACE-ME-THE-INSTALL-REFUSES-THIS-VALUE the panel eggs' NIMBUS_SHARED_SECRET variable
"" nobody filled it in

All of them are printed in the Nimbus repository, on this wiki, or on a panel's variable screen, so a backend holding one is authenticating with a value anyone can read. Rather than heartbeat with it, the mod stays misconfigured and says so, naming the keys it still needs:

Nimbus server mod is enabled but nimbus-server.json still needs: SharedSecret. Heartbeats are off
until it does. SharedSecret is not generated here: copy the value the network already runs on,
which is registry.embedded_shared_secret in the proxy's nimbus.proxy.toml, or shared_secret in a
standalone nimbus.registry.toml. A secret invented on this side matches nothing.

That warning is written at boot and again on every /nimbus reload that does not wire the mod up, which is where you are watching after editing the file.

There is no version of this the backend can solve on its own. The secret is the key the whole network signs with, so a value minted here would be a value that fails to authenticate against every other component. Copying is the whole job.

Upgrading from v0.4.0 or earlier: read this

change-me-and-keep-secret used to be the working default. It is not one any more. A backend still holding it stops heartbeating the moment it runs v0.5.0, loudly, with the message above. Nothing is lost and nothing is silently degraded, but that backend drops out of the network until somebody pastes a real secret in.

Do it before or right after the upgrade: open the proxy's nimbus.proxy.toml, take registry.embedded_shared_secret, and put it in SharedSecret on every backend. If your proxy is also on the old literal, replace it there first with a fresh random value (openssl rand -hex 32) and distribute that, because the proxy refuses to start on a non-loopback registry bind while the placeholder is in place.

In-game commands (backend server)

These commands run on the backend game server (not the proxy). They require the controlserver privilege by default.

Command Description
/nimbus status Shows mod status, registry connection state, last heartbeat result, snapshot age, and the last seamless handshake this backend completed as a transfer target.
/nimbus servers Lists all backends from the last registry snapshot with player counts, TPS, and maintenance flag.
/nimbus send <player> <serverId> Posts a transfer intent to move a player to another backend.
/nimbus reload Hot-reloads nimbus-server.json. Note: RegistryUrl and SharedSecret changes require a full VS server restart to take effect.

Player-facing commands (require AllowPlayerServerCommand = true):

Command Description
/server <serverId> Move yourself to another backend.
/join <serverId> Alias for /server.

The last line of /nimbus status only appears once this backend has received a player through a seamless transfer: it names the player and the transfer id of the handshake it closed. A seamless transfer that fails is otherwise invisible from the receiving side, which is where you end up looking when someone reports a stuck transfer screen.

Shortcut commands

Players would rather type /hub than /server hub. Each backend declares its own shortcuts in nimbus-server.json:

"ShortcutCommands": [
  { "Name": "hub", "Targets": [ "hub" ] },
  { "Name": "lobby", "Targets": [ "survival-lobby", "hub" ], "Description": "Back to your lobby" },
  { "Name": "staff", "Targets": [ "staff" ], "Privilege": "controlserver" }
]
Field Default Description
Name - The command word, without the slash. A leading / is trimmed if you write one.
Targets [] Server IDs tried in order. A shortcut with no targets is ignored, with a warning in the server log.
Privilege chat Vintage Story privilege required to run it. chat means everyone; controlserver keeps a shortcut to admins.
Description generated Text shown in the in-game command list. Left empty it becomes "Move yourself to <first target>".

Targets is a fallback chain, not a list of aliases. The first target that is registered in the last registry snapshot, not stale, not in maintenance, and not this server wins, which is what makes a /lobby meaning "this gamemode's lobby, or the hub if it has none" expressible as ["survival-lobby", "hub"]. When the whole chain is unusable the player is told why: "You are already there" if their own server was the only match, otherwise that no server is available right now.

Shortcuts are subject to AllowPlayerServerCommand like /server is, and they never shadow an existing command. /server, /join and /nimbus are reserved, a duplicate name inside the list is skipped, and a name already taken by another mod or by the game is left alone. That last rule is why there is no /tp shortcut: /tp is vanilla teleport, and hijacking it would break coordinate teleports. Every skip is logged at startup.

What a reload can change

Vintage Story registers chat commands at startup and offers no way to unregister one, so /nimbus reload can only go so far, and it is deliberately limited in the safe direction:

Change Takes effect
Retargeting an existing shortcut on reload
Tightening a privilege on reload
Loosening a privilege, adding or removing a shortcut needs a restart

Retargeting works because the handler re-reads the config on every call. Tightening works because the handler re-checks the caller's privilege too, so an operator who moves /staff from chat to controlserver and reloads is not left with everyone still walking through the boot-time gate. Loosening cannot work the same way: the gate registered at boot rejects the caller before the handler ever runs. The asymmetry is intentional. A half-applied permission change fails closed, never open.

Forwarding

When a player arrives via the proxy, the mod automatically consumes their reservation on join and stores the forwarding data for that session. This gives you access to the player's real IP as seen by the proxy - not the proxy's internal IP.

From another mod:

var nimbus = api.ModLoader.GetModSystem<NimbusServerModSystem>();
var forwarded = nimbus?.GetForwardedPlayer(player.PlayerUID);
if (forwarded != null)
{
    string realIp  = forwarded.RealRemoteIp;   // player's actual IP
    string source  = forwarded.SourceServerId; // backend they transferred from (null for first join)
}

GetForwardedPlayer returns null if:

  • The player connected directly (no proxy)
  • The registry is not configured on this backend
  • The reservation had already expired when they joined

See Forwarding for more detail on the trust model.

ReservationRequired

ReservationRequired = true (the default) blocks any player who does not have a valid proxy reservation. This enforces that all players connect through Nimbus, preventing direct backend access.

When a blocked player is kicked they receive the message: Direct connections are not permitted. Please connect via the Nimbus proxy.

Behaviour when the registry is unreachable is a choice, not a fixed guarantee:

  • Fail open (default): the check is skipped and the player is let in, so a registry outage never locks everyone out. This is the right default for most networks, and it is what happens unless you say otherwise.
  • Fail closed: set FailClosedWhenRegistryUnreachable to true and the player is kicked instead. Use it on a gated network, where an outage must not become a way around the proxy.

Either way the decision is per backend, so a hub can stay welcoming while a staff server stays strict.

Triggering transfers from your own mod

RequestTransferAsync is public, so any server-side mod can move a player through Nimbus. Portals, NPC dialogue options, pressure plates, quest rewards: they are content mods that call one method, and none of them need a change in Nimbus itself.

using Vintagestory.API.Server;
using Nimbus.ServerMod;

// Somewhere in your mod system, once:
var nimbus = api.ModLoader.GetModSystem<NimbusServerModSystem>();

// Then, whenever your content decides a player should move:
var response = await nimbus.RequestTransferAsync(
    player,                    // IServerPlayer
    "creative",                // target serverId, as registered in the network
    "stepped into the portal", // reason, recorded for audit
    "portal-mod");             // requestedBy, shows up in logs
if (!response.Ok)
    player.SendMessage(0, $"The portal is closed: {response.Error}", EnumChatType.Notification);

The call posts a transfer intent to the registry; the proxy drains it and runs its normal transfer path, so your mod inherits reservation minting, the seamless handshake when the player has the client mod, and the redirect fallback when they do not.

A block entity for a portal is the same three lines from OnEntityInside or a collision handler, plus a cooldown so a player standing in the frame does not queue a transfer every tick. Add your mod to Nimbus.ServerMod's dependants in your modinfo.json so load order is right:

"dependencies": { "Nimbus.ServerMod": "0.5.0" }

Useful companions on the same mod system:

  • HasSeamlessCapability(player) tells you whether this player has the Nimbus client mod, if you want a different visual treatment for the two paths.
  • GetForwardedPlayer(uid) gives you the reservation the player arrived with, including their real IP and the backend they came from, which is handy for "welcome back" content.

Clone this wiki locally