Server lifecycle & hooks
Your server-side game logic is JavaScript. system.js runs when your game starts up; it must call server.start() to configure the game, and typically registers the lifecycle hooks below.
| Signature | Description |
|---|---|
server.start({...}) | Required game configuration and startup. Accepts startup options such as tickInterval (how often onTick fires, in milliseconds; default 100). Players sign in with their Verge Online account; their record is created on first join, so there is nothing for your game to register. |
server.onTick(fn) | Registers a function called every tick. The tick rate is whatever you set for tickInterval in server.start (default 100ms). |
server.onReady(fn) | Called once after startup, when the world is fully loaded and ready to serve players. |
server.onAfterLogin(fn) | Called for every player at login, new and returning; use it to deliver assets with server.sendAsset(idx, name). |
server.onBeforeDisconnect(fn) | Called as a player leaves, before their record is saved; fn(idx). Use it to finalise anything you keep per-player. |
server.onAfterDisconnect(fn) | Called after a player has left and been saved; fn(idx). Use it to clear any state you keep keyed by their slot index, since slots are reused. |
server.onEnterMap(mapName, fnName) | Registers a function called when any player enters that map (fires on login, map change, and warp-to-map). |
server.onChatCommand(fn) | Registers a single handler invoked as fn(idx, text) for any /-command that isn't built in. Inspect the full command text and dispatch it yourself. |
server.onShutdown(fn) | Called during a graceful shutdown, before players are disconnected, so you can persist any final world state. |
server.env(key, default?) | Reads a per-deployment config value; returns its string value when set, otherwise the default argument unchanged (or undefined if omitted). Use to thread environment-specific settings into system.js. |
server.setEpoch(isoString) | Sets the game-time origin (RFC 3339); the game clock is computed from elapsed real time since this point. If not called, the time the game came online is used. |
server.gameTime() | Returns the current in-game time as {day, hour, minute} (day zero-indexed; hour 0–23). Scale: 1 real minute = 15 game minutes. |
assetsServer.start({menu, files}), where files is either a string or a {path, transport} object.The menu screen
Every server has a small menu: a background image, a short piece of music, and a name. It is the first thing a player sees for your world, its front door, shown as they connect to it. You set it in assetsServer.start, alongside your game files:
assetsServer.start({
menu: {
image: "../menu/menu.png", // background
midi: "../menu/menu.mid", // music (loops)
name: "My World", // shown to the player
},
files: { base: { path: "../world.vpack", transport: "tcp" } },
});
The menu is delivered over a lightweight channel before the full game loads, so it appears almost instantly. Keeping each piece tiny is what keeps that near-instant open true for every world.
| Piece | Format | Notes |
|---|---|---|
image | PNG, 640 × 360 | Drawn at 2× to fill the screen (1280 × 720). Use an indexed (8-bit) palette with a small number of colors, 16 to 32, so the file stays a few kilobytes. No transparency is needed; it is a full background. |
midi | Standard MIDI, .mid | Menu music is a MIDI file, synthesized on the client, so it is only a few kilobytes. This is different from in-game music, which uses tracker modules. Keep it short and simple; it loops. |
name | Text | Your world's name, shown to the player. |
Entities & movement
All entity types (players, NPCs, monsters) share one index space; a slot index (idx) identifies an entity. Coordinates are tile-based everywhere on the server.
| Signature | Description |
|---|---|
server.warpEntity(idx, tileX, tileY) | Warps an entity within its current map. |
server.warpEntityToMap(idx, tileX, tileY, mapName) | Warps an entity to a different map. |
server.setPosition(tileX, tileY) | Sets position, in tile coords. |
server.getEntity(idx) | Returns a read-only entity proxy: type ("player"/"npc"/"unknown"), name, x, y, map, costume. |
server.getPlayer(idx) | Returns a player proxy (null if not a player). See Players & data. |
server.entityCount() | Returns the current entity slot count. |
server.forEachPlayer(fn) | Calls fn(idx) for each player in the game. |
server.forEachNPC(fn) | Calls fn(idx) for each NPC. |
server.forEachMonster(fn) | Calls fn(idx) for each monster. |
Players & data
server.getPlayer(idx) returns a player proxy: read-only props plus the methods below. Player attributes are arbitrary key/value pairs saved with the player. Use them for anything your game wants to remember about someone (level, gold, quest progress).
| Signature | Description |
|---|---|
player.getAttribute(key) | Reads a saved player attribute. |
player.setAttribute(key, value) | Sets a saved player attribute. |
player.setMap(...) | Sets the player's map. |
player.setPosition(...) | Sets the player's position. |
player.save() | Persists the player record. |
player.setAttribute in a client script), which pushes the change to the server.Assets, scripts & client calls
Your game streams to the client the moment a player connects, and the server can push additional assets, scripts, and function calls to a specific client mid-game.
| Signature | Description |
|---|---|
server.sendAsset(idx, name) | Delivers a named asset to a client, using the asset's transport config. Mid-game, bytes are routed by name prefix (script.X, image.X, music.X, map.X, etc.). |
server.sendScript(idx, name, source) | Sends JavaScript source to a client to run there. |
server.callClientFunc(idx, fnName) | Tells a client to call a named global function. |
server.playMusic(idx, trackName) | Tells a player's client to start playing one of your game's music tracks. |
server.getMapInfo(mapName) | Returns map metadata: { width, height, spawnX, spawnY, walls: [int], triggers: [{x,y,name}] }, or null. walls[idx] is a 4-bit mask per tile: bit 0 = N entry blocked, bit 1 = E, bit 2 = S, bit 3 = W. |
A typical login handler wires assets to each connecting player:
server.onAfterLogin(function(idx) {
server.sendAsset(idx, "hud.js");
const info = server.getMapInfo("town");
server.playMusic(idx, "town");
});
Diagnostics
Every game records diagnostics automatically: player sessions, disconnects, rejected logins, restarts, and crashes. You watch them live in the manager's Diagnostics tab, per game and per environment, with sessions, a concurrency graph, health, and a crash history that survives the server going down. It is on by default and needs no code.
Your game can also record its own events, so the dashboard shows what matters in your world (level ups, quests, boss kills), and can tell the manager which player stat to surface. All three calls below are optional, and safe to call even when diagnostics are turned off.
| Signature | Description |
|---|---|
server.logEvent(kind, idx, data) | Records one event. kind is any label you choose ("levelup", "quest_done"); idx is the player it happened to, or -1 for a world-wide event; data is an optional object of details. Each kind appears under Activity in the dashboard. |
server.defineEvent(kind, {label, group, unit}) | Optional. Gives an event kind a friendly label and grouping in the dashboard. An event you never define still charts, under its raw name. |
server.defineStat(key, {label, from}) | Optional. Tells the manager's Users list which saved player attribute to show as a stat column. from is a dotted attribute path, e.g. "stats.Level". Declare none and no stat column is shown. |
server.defineEvent(kind, {label, group, unit}) and the diagnostics block | The diagnostics option on server.start tunes retention (retainDays, maxEvents), how often load is sampled (sampleSec), address handling (recordIP), and whether engine crashes are reported to Verge Online (reportCrashes, off unless you turn it on). Every field has a working default, so you can leave the block out entirely. |
server.forgetPlayer(id) | Erases every recorded event for one player. Takes the player's account id, since whoever is asking is usually not online at the time. Use it to honour a deletion request. Deleting an account from the manager erases their events too. |
recordIP to "omit" to store nothing at all, or "plain" to keep it. History is capped by age and by row count, so it cannot grow without bound.A game records its own progression like this:
// Label the event and the stat once, at startup.
server.defineEvent("levelup", { label: "Level ups", group: "Progression" });
server.defineStat("level", { label: "Level", from: "stats.Level" });
// Record one whenever a player levels up.
function onLevelUp(idx, newLevel) {
server.logEvent("levelup", idx, { level: newLevel });
}
You can tune retention and privacy in the diagnostics block of server.start(). Player addresses are hashed by default, so the dashboard can tell two sessions apart without storing where anyone connected from.
server.start({
// ...
diagnostics: {
enabled: true, // on by default
retainDays: 30, // prune events older than this
recordIP: "hash" // "hash" | "omit" | "plain"
}
});
test.server.calls("logEvent") to check the right events fired.Client-side (presentation)
Client scripts run on the player's own machine and are presentation-only: HUD, panels, and overlays. They load with your game at start (from its script.* sections), plus runtime delivery via server.sendScript.
| Signature | Description |
|---|---|
client.onDraw(fn) | Per-frame draw callback with ctx (drawText, fillRect, read-only game state); renders above all game elements (the UI layer). |
client.onDrawWorld(fn) | Per-frame draw callback at the entity/world layer (below UI panels); use for quest markers, entity overlays, etc. |
client.onTick(fn) | Per-frame update callback with delta time. |
client.createPanel(x, y, w, h, opts) | Creates a retained-mode UI panel with addLabel(), addProgressBar(), and addButton(). Buttons support onClick callbacks with mouse hit-testing. |
client.log(msg) | Debug output. |
client.setLocalAccess(level) | Tells the client the player's access level (so it knows when to honour admin-only edit mode). |
The player global
Client scripts have a live player global, updated per-frame.
| Member | Description |
|---|---|
player.name | Player name (read-only, live per-frame). |
player.x | Player tile X (read-only, live per-frame). |
player.y | Player tile Y (read-only, live per-frame). |
player.map | Current map name (read-only, live per-frame). |
player.setAttribute(key, value) | Updates a saved player attribute from the client (pushed to the server). |