Appearance
Login server scripting
Every sign-in lands on the Login Server first. It's an ordinary GServer started with "role": "login", but it hosts no world, only weapons, and the client trusts those weapons. That trust lets one set of scripts provide the server list, the friends list and the global PM system for every game server, instead of each server building its own.
For platform maintainers
The Login Server is run by the platform, not by individual game servers, and you can't script it from your server's GRC. Game-server developers only need this page to understand how the server list, friends and private messages that every player sees fit around their own scripts. For those, see Chat & private messages.
How it differs from a game server
| Game server | Login Server | |
|---|---|---|
serveroptions.json role | "game" | "login" |
| World (levels, NPCs, movement) | yes | none |
| Where its weapons run on the client | the game runtime (one per connection) | the system runtime, a separate privileged V8 engine |
| Weapon API | client globals + GUI | system API + GUI |
| Weapon lifetime | until the player leaves the server | until the player leaves, or the whole session for persistentWeapons |
| Listed on the server lister | yes | no, clients reach it through a pinned address |
The Login Server's serverside scripts are normal server scripts with the normal server globals. The template login.ts grants startWeapons on join, exactly like a game server. Everything special happens on the client side.
Trust: the system runtime
The client only treats a server as the Login Server if its identity key matches the one pinned in the client's settings.json (LoginServerHost, LoginServerPort, LoginServerKeys). Weapons from that server run in the system runtime, a V8 engine of its own with its own GUI:
- Game servers' weapons can't call the privileged API, read system GUI controls, or spoof their events. A game server that claims
role: "login"just gets ordinary game weapons. - PM text never enters a game server's engine. Game weapons only get
onPMReceived(sender).
Persistent weapons
Weapons listed in the Login Server's persistentWeapons keep running (state, timers, open windows) for the whole client session, across every server switch, until sign-out. The others unload when the player leaves the Login Server. When the player returns, an unchanged persistent weapon keeps running (no second onCreated). A changed one reloads, and a revoked one unloads.
json
{
"name": "Login Server",
"role": "login",
"listenPort": 14901,
"staff": ["Astram"],
"startWeapons": ["-serverlist", "-pm"],
"persistentWeapons": ["-serverlist", "-pm"]
}persistentWeapons can be edited live from GRC, and connected clients get the new flags without reconnecting.
The system API
Login Server weapons are .client.ts files in the Login Server's scripts/weapons/, compiled against system.client.d.ts (plus gui.client.d.ts) instead of the game weapons' globals. GUI controls work as usual, and so do lib/ imports.
No world here
There's no findimg, no flags (client/clientr/serverr), no collections, no classes, no tiles or camera, and no setAni. On the Login Server itself there's no level at all.
Basics
| Symbol | Notes |
|---|---|
| echo | Prints to the client's stdout. Batched |
| sleep, setTimeout, clearTimeout, setInterval, clearInterval | Frame-resolution timers, as for game weapons. Persistent weapons' timers keep running across server switches |
| keydown | Held-key check. False while typing in a text box |
| mousex, mousey, mousedown, mouseonui | Mouse in window pixels |
| ScreenWidth, ScreenHeight | Live viewport size |
| measuretext → TextMeasurement | Measure and word-wrap text like a label |
| triggerServer | Reaches the Login Server's serverside scripts, only while the player is on it. Elsewhere it's dropped and logged |
| WeaponThis | this in handlers: this.name plus your own state (no join, since there are no classes) |
| findweapon / findWeapon → WeaponHandle | Calls into another loaded system weapon. Game servers' weapons aren't visible |
| player | The local player on whichever server the client is on. On the Login Server only id/name/account/nick mean anything. On a game server, x/y/dir/chat/ani/level are live, and writes to x/y/dir/chat apply there. hasWeapon checks system weapons |
| ChatPlayer | Frozen local-player snapshot passed to onPlayerChats (also has account and level) |
Session and servers
session describes the signed-in session: account, onLoginServer (true while connected to the Login Server itself) and server, a ServerInfo (name, host, port) plus login: true for the Login Server.
servers is the server list and switching:
ts
async function refresh() {
try {
const list = await servers.list() // ServerInfo[], sorted by players then name
for (const s of list) echo(`${s.name} ${s.host}:${s.port} (${s.players ?? 0} online)`)
} catch {
echo('Server list unavailable') // rejects when the lister can't be reached
}
}
function join(s: ServerInfo) {
servers.connect({ host: s.host, port: s.port, name: s.name }) // batched, applies next frame
}servers.connect leaves the current server (non-persistent system weapons unload), connects, and fires onServerChanged on success. On failure it fires onServerSwitchFailed and the client returns to the Login Server. servers.returnToLogin() leaves a game server for the Login Server. logout() signs out: it forgets the remembered session and returns to the login form.
Friends and private messages
friends is the cross-server friends list:
| Member | Notes |
|---|---|
connected, account | Whether the social service is reachable, and as whom |
list() | Friend[] (account, online, server), online first, then by name |
get(account), has(account) | Look up one friend |
add(account), remove(account) | Asynchronous. Results arrive as onFriendsChanged / onFriendRemoved / onFriendError |
Friend values are plain copies. Re-read after a change event instead of holding on to them.
PMs work across every server:
| Symbol | Notes |
|---|---|
sendPM(account, text) | Send to a player on any server. Online-only, max 200 characters, 10 per 10 seconds. Failures come back as onPMFailed(account, reason) with reason 'offline', 'rate_limited', 'invalid' or 'disconnected' |
openPM(sender) | Returns and marks read the messages from sender (a PMSender or account name) since the last openPM for them, oldest first, as PMMessages (text, at in ms since epoch, system). The only way to read PM text. Held for this session only, up to 200 per sender |
pendingPMs() | Senders with unread messages, oldest first, as PMPending (a PMSender plus count) |
ts
function onPMReceived(sender: PMSender) {
for (const m of openPM(sender))
echo(`${sender.system ? '[system] ' : ''}${sender.account}: ${m.text}`)
}System PMs from game-server scripts (sendPM on a GServer) arrive the same way with sender.system === true. See Chat & private messages.
Events
| Handler | Fires |
|---|---|
onCreated(), onUpdate(delta) | As for game weapons. Persistent weapons keep receiving onUpdate on every server |
onActionClientSide(...params) | The Login Server called triggerClient (only while on it) |
onServerChanged(server, onLoginServer) | Joined a server, the Login Server included |
onServerSwitchFailed(server, reason) | servers.connect failed. The client heads back to the Login Server |
onServerDisconnected(reason) | Dropped or kicked from a game server. The client heads back to the Login Server |
onPlayerChats(player, chat) | The local player typed a / command on a game server (e.g. /pm name hi) |
onPMReceived(sender) | A PM arrived. Read it with openPM(sender) |
onPMFailed(account, reason) | A sendPM couldn't be delivered |
onFriendsChanged(friend), onFriendRemoved(account), onFriendError(account, reason) | Friends-list results |
onServerChanged only reaches running weapons
A weapon granted when the player joins the Login Server loads after that onServerChanged. Check session.onLoginServer in onCreated too. The shipped -serverlist does:
ts
function onCreated() {
buildWindow()
if (session.onLoginServer) show()
}Because system weapons hear the local player's / commands on every game server, a persistent weapon can add global chat commands. That's how -pm implements /pm <account> <text> and /r <text> everywhere.
Default assets
Everything the Login Server serves, except levels, becomes a default asset for every game server. Clients sync it at login and fall back to it when a game server lacks a file. Heads and bodies live there, for example, which is why a brand-new game server still has player art.