Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Networking

The ns-network crate provides optional TCP, HTTP, and WebSocket support for Epitaph scripts. It is opt-in – the game’s Rust runner must install the network runtime on the engine before scripts can use these functions.

All networking functions live under the net module:

access <net>

Networking is asynchronous – connections and requests run on background threads. Scripts poll for results each tick rather than blocking.


Single script tree, two binaries

ORPG-style games should use one script tree (one directory or one scripts.pak) per title, not parallel scripts/client/ and scripts/server/ mounts that must be kept in sync. Each process still has exactly one Engine::mount_scripts / entry load: ship two Cargo binaries (for example game-client and game-server) that both point at the same on-disk script pack or folder, and set EngineConfig::runtime_role (Client vs Server) plus EngineConfig::headless on the server. Branch in Epitaph with sys.runtime_role() and sys.is_headless() (from ns-core, not this crate; see engine-api.md and Runtime role & single script tree).

Use the same asset pack or paths.assets directory for both binaries whenever rules and art must match: Engine::mount_assets registers the same keys in each process so assets.load_image and raw paths resolve identically.

   scripts/ (or scripts.pak)                    assets/ (or game.pak)
          │                                           │
    ┌─────┴─────┐                               ┌─────┴─────┐
    │           │                               │           │
 client      server                         client      server
 binary      binary                         binary      binary

Shared Functions

These work across all protocol types.

FunctionArgsReturnsDescription
net.status(handle)inttext or voidConnection status string. Returns void for unknown handles.
net.poll_event()record or voidPop the next network event. Returns void if no events.
net.close(handle)intvoidClose any connection or cancel a request.

Status Values

ProtocolPossible statuses
TCP / WebSocket"connecting", "connected", "error", "closed"
HTTP"building", "pending", "done", "error"

Event Records

Events returned by net.poll_event() are records with a type field (text) and a handle field (int). Some events include additional data:

Event typeExtra fieldsDescription
"connected"TCP/WS connection established
"data"data (text)TCP data received
"message"data (text)WebSocket message received
"accepted"data (int)New client handle from TCP listener
"http_done"HTTP request completed
"error"data (text)Error message
"closed"Connection closed

Example: Event Loop Pattern

access <net>
access <log>

phase tick(dt) {
    sustain evt = net.poll_event() {
        inspect evt.type {
            "connected" => { log.info("Connected:", evt.handle) }
            "data"      => { log.info("Received:", evt.data) }
            "error"     => { log.error("Error:", evt.data) }
            "closed"    => { log.info("Closed:", evt.handle) }
        }
    }
}

TCP

Feature-gated behind tcp in the ns-network crate.

Client Functions

FunctionArgsReturnsDescription
net.tcp_connect(host, port)text, intintStart an async TCP connection. Returns a handle immediately.
net.tcp_send(handle, data)int, textbool or textSend UTF-8 data. Returns active on success, or an error message string.
net.tcp_recv(handle)inttext or voidPop next received data chunk. Returns void if nothing queued.
net.tcp_close(handle)intvoidClose the connection.

Server Functions

FunctionArgsReturnsDescription
net.tcp_listen(port)intintStart listening on a port. Returns a server handle.
net.tcp_accept(handle)intint or voidAccept next pending client. Returns client handle or void.
net.tcp_stop(handle)intvoidStop the listener.

Example: TCP Client

access <net>
access <log>

let conn = 0

phase init() {
    conn = net.tcp_connect("127.0.0.1", 8080)
}

phase tick(dt) {
    sustain evt = net.poll_event() {
        inspect evt.type {
            "connected" => {
                log.info("Connected to server")
                net.tcp_send(conn, "hello\n")
            }
            "data" => {
                log.info("Server says:", evt.data)
            }
            "error" => {
                log.error("Connection error:", evt.data)
            }
        }
    }
}

Example: TCP Server

access <net>
access <log>

let server = 0
let clients = []

phase init() {
    server = net.tcp_listen(9000)
    log.info("Listening on port 9000")
}

phase tick(dt) {
    sustain evt = net.poll_event() {
        inspect evt.type {
            "accepted" => {
                append(clients, evt.data)
                log.info("Client connected:", evt.data)
                net.tcp_send(evt.data, "welcome\n")
            }
            "data" => {
                log.info("Client", evt.handle, "says:", evt.data)
            }
            "closed" => {
                log.info("Client disconnected:", evt.handle)
            }
        }
    }
}

HTTP

Feature-gated behind http in the ns-network crate.

Request Lifecycle

  1. Build a request with net.http_request or a convenience function
  2. Optionally set headers with net.http_set_header
  3. Send the request (automatic for convenience functions)
  4. Poll for the response via net.http_response

Request Functions

FunctionArgsReturnsDescription
net.http_request(method, url)symbol/text, textintCreate a request. Method is a symbol (:get, :post, etc.) or text. Returns handle. Does not send yet.
net.http_request_body(method, url, body)symbol/text, text, textintCreate a request with a body. Does not send yet.
net.http_set_header(handle, name, value)int, text, textvoidSet a header on an unsent request.
net.http_set_headers(handle, headers)int, mapvoidSet multiple headers from a map.
net.http_send(handle)intvoidSend the request (runs in background).

Convenience Functions

These create and immediately send a request in one call:

FunctionArgsReturnsDescription
net.http_get(url)textintGET request
net.http_head(url)textintHEAD request
net.http_delete(url)textintDELETE request
net.http_options(url)textintOPTIONS request
net.http_post(url, body)text, textintPOST request with body
net.http_put(url, body)text, textintPUT request with body
net.http_patch(url, body)text, textintPATCH request with body

Response Functions

FunctionArgsReturnsDescription
net.http_response(handle)intrecord or voidGet the response. Returns void while pending. On success: record with status (int), body (text), headers (map). On failure: record with error (text).
net.http_response_header(handle, name)int, texttext or voidGet a specific response header (case-insensitive).

Example: Simple GET

access <net>
access <log>
access <json>

let req = 0

phase init() {
    req = net.http_get("https://api.example.com/data")
}

phase tick(dt) {
    when req != 0 {
        let resp = net.http_response(req)
        when resp != void {
            when resp.status == 200 {
                let data = json.decode(resp.body)
                log.info("Got data:", data)
            } otherwise {
                log.error("HTTP error:", resp.status)
            }
            req = 0
        }
    }
}

Example: POST with Headers

access <net>
access <json>

let req = net.http_request(:post, "https://api.example.com/submit")
net.http_set_header(req, "Content-Type", "application/json")
net.http_set_header(req, "Authorization", "Bearer my-token")
net.http_send(req)

WebSocket

Feature-gated behind ws in the ns-network crate.

FunctionArgsReturnsDescription
net.ws_connect(url)textintOpen a WebSocket connection. Returns handle.
net.ws_send(handle, text)int, textbool or textSend a text message. Returns active on success.
net.ws_send_binary(handle, data)int, textbool or textSend binary data (text bytes). Returns active on success.
net.ws_recv(handle)inttext or voidPop next received message. Returns void if empty.
net.ws_close(handle)intvoidClose the WebSocket.

Example: WebSocket Chat

access <net>
access <log>

let ws = 0

phase init() {
    ws = net.ws_connect("wss://chat.example.com/room")
}

phase tick(dt) {
    -- handle connection events
    sustain evt = net.poll_event() {
        inspect evt.type {
            "connected" => {
                log.info("WebSocket connected")
                net.ws_send(ws, "Hello, room!")
            }
            "message" => {
                log.info("Chat:", evt.data)
            }
            "error" => {
                log.error("WS error:", evt.data)
            }
            "closed" => {
                log.info("WebSocket closed")
                ws = 0
            }
        }
    }
}

Notes

  • All handle-based operations are safe to call with stale handles – they return void or no-op gracefully.
  • Network operations never block the game loop. Always poll for results in game.tick.
  • The net.poll_event() queue is shared across all protocols. Use evt.handle to identify which connection an event belongs to.
  • Feature flags (tcp, http, ws) control which protocol implementations are compiled into ns-network. Functions for disabled protocols will not be registered.