NetStack

Writing Functions

Functions are where your game rules live. Each one is a single JavaScript file with a default export. You create and edit them in the Functions tab of your project.

Anatomy

export default async function (ctx, args) {
  return { ok: true };
}
  • ctx gives you access to players, rooms and global state. See the Function API.
  • args is the object your game passed as the second argument to client.call().
  • Whatever you return is sent back to the caller. It must be JSON-serializable.

You can define helper functions and constants in the same file, but you cannot import other modules. Keep everything in one file with a single export default.

Validating input

Never trust args. The client can send anything, so check types and ranges before using them:

export default async function (ctx, args) {
  if (typeof args.amount !== "number" || args.amount <= 0 || args.amount > 100) {
    throw new NetStackError("BAD_AMOUNT", "Amount must be between 1 and 100.");
  }
  // ...
}

Rejecting a call

Throw a NetStackError with a code and message. The caller receives both, and nothing your Function changed is kept:

throw new NetStackError("NOT_ENOUGH_GOLD", "You cannot afford that.");

Any other thrown error is reported to the caller as FUNCTION_ERROR without exposing internals.

All-or-nothing changes

Property writes and broadcasts are collected while your Function runs and applied together only if it finishes without throwing. If anything throws, none of it happens. You never need to undo partial changes.

Reading and writing other players

ctx.players lists everyone in the room. Each entry has an id and the same get, set and delete methods as ctx.player:

const target = ctx.players.find((p) => p.id === args.toPlayerId);
if (!target) throw new NetStackError("PLAYER_NOT_IN_ROOM", "That player is not here.");

target.set("gold", (target.get("gold") ?? 0) + args.amount);

Testing

The Test run panel executes your code in the same sandbox as production against sample state you provide. It shows the returned value, every property change, broadcast events, ctx.log output and any error, and it never saves anything or touches live players.

Limits

Each call gets 150 ms of CPU time and 32 MB of memory, with no network, filesystem or timers. See Limits and plans for the full list.

Built-in functions

Every project comes with these Functions already installed. Call them by name from your game; there is nothing to set up.

Name What it does
broadcast Best for transporting player movement, chat messages, and more. Sends { from, type, data } to everyone in the room as a broadcast event.
sendChat Validates a message (args.text, up to 200 characters) and sends it to the room as a chat event.
updatePosition Saves the caller's position property and sends a playerMoved event. Takes x, y and optional z.
getPlayers Returns the IDs of everyone in the caller's room.
setReady Marks the caller ready or not ready and sends readyChanged, including whether the whole room is ready.
getServerTime Returns the server clock in milliseconds.
await client.call("broadcast", { type: "move", data: { x: 12, y: 4 } });

Open a built-in in the Functions tab to read its code or try it in the Test run panel. Press Customize to save your own copy under the same name. Your copy replaces the built-in for that project, and deleting it brings the built-in back. Built-ins do not count toward the 50 Functions per project.

Starter templates

The editor offers templates for using an item, giving an item to another player, and a shared counter. They are a good starting point for common patterns.