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.
| Function | Args | Returns | Description |
|---|---|---|---|
net.status(handle) | int | text or void | Connection status string. Returns void for unknown handles. |
net.poll_event() | – | record or void | Pop the next network event. Returns void if no events. |
net.close(handle) | int | void | Close any connection or cancel a request. |
Status Values
| Protocol | Possible 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 type | Extra fields | Description |
|---|---|---|
"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
| Function | Args | Returns | Description |
|---|---|---|---|
net.tcp_connect(host, port) | text, int | int | Start an async TCP connection. Returns a handle immediately. |
net.tcp_send(handle, data) | int, text | bool or text | Send UTF-8 data. Returns active on success, or an error message string. |
net.tcp_recv(handle) | int | text or void | Pop next received data chunk. Returns void if nothing queued. |
net.tcp_close(handle) | int | void | Close the connection. |
Server Functions
| Function | Args | Returns | Description |
|---|---|---|---|
net.tcp_listen(port) | int | int | Start listening on a port. Returns a server handle. |
net.tcp_accept(handle) | int | int or void | Accept next pending client. Returns client handle or void. |
net.tcp_stop(handle) | int | void | Stop 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
- Build a request with
net.http_requestor a convenience function - Optionally set headers with
net.http_set_header - Send the request (automatic for convenience functions)
- Poll for the response via
net.http_response
Request Functions
| Function | Args | Returns | Description |
|---|---|---|---|
net.http_request(method, url) | symbol/text, text | int | Create 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, text | int | Create a request with a body. Does not send yet. |
net.http_set_header(handle, name, value) | int, text, text | void | Set a header on an unsent request. |
net.http_set_headers(handle, headers) | int, map | void | Set multiple headers from a map. |
net.http_send(handle) | int | void | Send the request (runs in background). |
Convenience Functions
These create and immediately send a request in one call:
| Function | Args | Returns | Description |
|---|---|---|---|
net.http_get(url) | text | int | GET request |
net.http_head(url) | text | int | HEAD request |
net.http_delete(url) | text | int | DELETE request |
net.http_options(url) | text | int | OPTIONS request |
net.http_post(url, body) | text, text | int | POST request with body |
net.http_put(url, body) | text, text | int | PUT request with body |
net.http_patch(url, body) | text, text | int | PATCH request with body |
Response Functions
| Function | Args | Returns | Description |
|---|---|---|---|
net.http_response(handle) | int | record or void | Get 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, text | text or void | Get 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.
| Function | Args | Returns | Description |
|---|---|---|---|
net.ws_connect(url) | text | int | Open a WebSocket connection. Returns handle. |
net.ws_send(handle, text) | int, text | bool or text | Send a text message. Returns active on success. |
net.ws_send_binary(handle, data) | int, text | bool or text | Send binary data (text bytes). Returns active on success. |
net.ws_recv(handle) | int | text or void | Pop next received message. Returns void if empty. |
net.ws_close(handle) | int | void | Close 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
voidor 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. Useevt.handleto identify which connection an event belongs to. - Feature flags (
tcp,http,ws) control which protocol implementations are compiled intons-network. Functions for disabled protocols will not be registered.