A game's own network

tide/network.h is how a game's C carries its matches over a network of its own, in the place of Tide's rooms: a store's, a server fleet's, plain sockets. Your own network and service walks through one; this page is the reference.

A game's C includes tide/network.h (and with it tide/net.h) on every target, and nothing else of the engine. Nothing of the engine is built for a particular network.

c
#include "tide/network.h"

bool UseMyNetwork(void) // extern bool UseMyNetwork(); in the game's Tide
{
    tide_network_set(&(tide_network){.host = my_host, .join = my_join, .address = my_address});
    return true;
}

Registering

Function Does
tide_network_set(const tide_network *network) Registers a copy of network as the game's network from now on. NULL registers none: Tide's rooms again
tide_network_get() What's registered, or NULL
  • When. Before the Session.Open() or Session.Join() it's for. The same frame will do, as a frame's session calls are served at its end.
  • A match stays on its network until this machine leaves it, whatever is registered meanwhile. The next match opened or joined is on what's registered then.
  • self is the network's own pointer, handed back to every function. What it points at stays good while the network is registered and while a match is on it.
  • Threads. Every function of a network and of its transports is called on the thread that runs the game's frames, never during a tick's systems. A network with threads of its own hands their datagrams over itself.
  • Under tide run a game's C starts over at each reload, so the game registers again. A reload while a match is on the game's own network starts the game over.
  • On the web the functions are there as everywhere. A network that doesn't exist in a page registers nothing there.

With a network registered, Session.Open() opens the match on it alone, with no room and no UDP port, and Session.Join(text) hands it the text. Session.Connect(address) is still Tide's own UDP.

The network's functions

c
typedef struct tide_network {
    void *self;
    bool (*host)(void *self, tide_transport *out);
    bool (*join)(void *self, const char *address, tide_transport *out, tide_address *server);
    void (*address)(void *self, char *out, size_t size);
    bool (*failed)(void *self);  // Optional
    void (*update)(void *self);  // Optional
    // Host migration: all three, or none
    void (*place)(void *self, char *out, size_t size);
    void (*migrate)(void *self, const char *place);
    int (*migrated)(void *self, tide_transport *out, tide_address *server);
} tide_network;
Function Called Contract
host Session.Open() on a match this machine runs Writes a transport the players who join arrive on into out and returns true. False: the match stays closed, and out is untouched. It keeps hosting until the transport's close
join Session.Join(text) address is the game's text, zero-terminated, at most 255 bytes, good for the call only, and anything a player could type. Writes a transport to that match into out and the host's address on it into server, and returns true. False for text that can't be an address: the join fails at once with Failed. True promises nothing about the host being there
address Every frame a match is on the network Writes the text others join this machine's match by into out (size is TIDE_NETWORK_ADDRESS_BYTES, 64): of the match it hosts, or of the one it joined. "" while there's none to give. It's what Session.room says. The engine cuts it at 63 bytes and at the first byte that isn't printable ASCII
failed Every frame a match is on the network True when the match can't go on: the address joined is nobody's, or the network is gone. The match ends on this machine with Failed, and it isn't asked again for that match
update Once a frame, before the session reads its transports, in a match or not Where a library's callbacks are run. Called for as long as the network is registered, and for a match's network until the match is left

A join whose host never answers, with failed saying nothing, ends after 15 seconds with TimedOut, as a room's does.

Transports

host, join and migrated each hand over a tide_transport (tide/net.h), which a session then calls:

c
typedef struct tide_transport {
    void *self;
    void (*send)(void *self, tide_address to, const void *data, uint32_t size);
    uint32_t (*receive)(void *self, tide_address *from, void *data, uint32_t capacity);
    void (*close)(void *self);
    void (*end)(void *self);       // Optional
    double (*clock)(void *self);   // Optional, with arrived
    double (*arrived)(void *self);
} tide_transport;
Function Contract
send One datagram of up to TIDE_NET_MTU (1200) bytes to to, an address this transport gave. Returns at once. data is the caller's, gone after the call. A datagram that can't go is dropped
receive Copies the next datagram that arrived into data, writes who sent it into from, and returns its size; 0 with none. Never waits. One longer than capacity is dropped. Called until it returns 0, every frame
close The session is done with the transport and calls nothing of it again: free what self holds. Called exactly once for every transport handed over
end On a host, just before close, when the match ended for everyone (Session.End(), or its last scene unloading) and not when its host only left. A network with host migration records it where migrated reads it
clock, arrived The network's clock in seconds, and when the datagram receive last returned arrived by it. With them Session.ping is the network's round trip alone; without, it counts from when datagrams are read

A transport carries datagrams and nothing more. They can be lost, doubled or arrive out of order, and sessions make up for all of it: nothing is resent, ordered or acknowledged by the network.

What arrives is another machine's bytes. A session checks every datagram as hostile, whatever carried it, and a network adds no trust: it hands over what came, with who it came from as it knows them.

Addresses

A transport names machines with tide_address values of kind TIDE_ADDRESS_NETWORK, whose host (32 bits) and port (16 bits) are the network's to give a meaning: an IPv4 address and a port, an account number, an index into a table of the network's own peers. A session only compares them and hands them back to send. What it relies on:

  • One machine has one address for as long as a transport lives, and two machines never share one: a player is the address its datagrams come from.
  • server, from join and migrated, is the address the host's datagrams arrive from on that transport.
  • A machine that joins the same host again should get the same server, and another host another: that's how it comes back as the same player.

The engine sets server's kind itself. tide_address_format writes one as network 3:0.

Host migration

Optional, for a game whose settings say hostMigration = true. With all three functions, a match whose host goes changes hands; with none, it ends, as a match joined with Session.Connect does.

One authority decides who hosts next, and it's the network's to have: a relay, a lobby's owner, a matchmaker. Nothing is decided by who was first or who waited longest.

Function Called Contract
place On the host, every frame Writes where the match's players meet when it goes into out (size is TIDE_NETWORK_PLACE_BYTES, 33): up to 32 letters and digits, which the session sends every player. "", or anything else, is nowhere: no migration
migrate On a player whose match lost its host, once Go to place, the text the host's place gave, and find out who hosts now. The engine has checked it's letters and digits
migrated Every frame after that, until it's known Returns one of the values below
migrated returns Means
TIDE_NETWORK_MOVING (0) Not known yet: asked again next frame
TIDE_NETWORK_HOSTS (1) This machine hosts the match now. out is a transport the players who come back arrive on
TIDE_NETWORK_JOINS (2) Another machine does. out reaches it, at server
TIDE_NETWORK_LOST (-1) The place can't be reached: the match is over for this machine (TimedOut)
TIDE_NETWORK_ENDED (-2) The match's host ended it for everyone (its transport's end): nobody takes it over (Ended)

Exactly one machine may be told it hosts. A new host has 30 seconds to be found, and then the match is over for this machine. While a match changes hands, local code sees Session.state as Connecting and no Connected or Disconnected, as with rooms.

For hosts

The standard host does everything above. A C program with a loop of its own (C hosts) keeps a zeroed tide_network_match for each session and calls:

Function When
tide_network_match_host(&m, network, &transport) In Session.Open()'s place, with tide_network_get()'s network when there is one: the transport for tide_session_open
tide_network_match_join(&m, network, text, &transport, &server) In Session.Join()'s place: the transport and server for tide_session_join
tide_network_match_update(&m, tide_network_get()) Once a frame, before tide_session_update
tide_network_match_after(&m, session, now) Once a frame, after it: fails a match whose network says so, and does a host's and a player's part of host migration
tide_network_match_address(&m, room) The text for tide_local_set_session's room, into char room[TIDE_NETWORK_ADDRESS_BYTES]
tide_network_match_leave(&m) When the match is left, ended or replaced by one on no network of the game's

A game's library has its own copy of the engine under tide run, so the standard host asks the game for its network through tide_host_game.network, which tide/run.h fills with tide_network_get.

Dedicated hosting

tide/hosting.h hosts independent matches of one compiled tide_game over a shared transport. tide/serve.h supplies the native service loop, without a window, views, audio or local player. tide/server_run.h adapts a generated game.

An optional C process entry point configures the standard service:

c
#include "game.h"
#include "tide/hosting.h"

void tide_server_entry(tide_hosting_desc *host)
{
    host->max_matches = 32;
    host->idle_seconds = 120;
    // Set authorize for your identity provider and admission policy.
    // Set start to choose each match's scene and startup parameters.
}

start(user, request, match) receives an authorized create request and a tide_server_desc. Set match->start to a normal typed tide_start, using the scene selector and union member listed in the generated header. Validate the request's application-defined parameters and return false to refuse them. Use host-owned/static startup storage that survives callback return; startup consumes it before the next callback. The host supplies the game, transports, shared worker pool and entropy. A local Main requires this callback. No new Tide orchestration syntax is defined yet.

ready(user, hosting) runs once after service creation; poll(user, hosting, now) runs before each service update, outside ticks. Use them to create/stop matches or complete external authentication work.

Function Purpose
tide_hosting_create, tide_hosting_destroy Own the host/transport; failed creation leaves transport ownership with the caller
tide_hosting_update Read bounded traffic and advance matches
tide_hosting_start, tide_hosting_stop Trusted in-process lifecycle; start returns the match ID
tide_hosting_world, tide_hosting_match_count Inspect authoritative state and capacity
tide_hosting_player_identity Read verified player metadata outside simulation
tide_hosting_resolve Complete pending authorization by its host-assigned request handle

authorize(user, request, grant) sees request->match == 0 for create-and-enter, or a match ID for joining. Return TIDE_HOSTING_ACCEPTED, TIDE_HOSTING_DENIED or TIDE_HOSTING_PENDING. Acceptance supplies a verified opaque 32-byte identity; all zero means anonymous. A verifier takes precedence over anonymous flags. Copy any borrowed request data needed by asynchronous work, then complete it with tide_hosting_resolve. Pending requests expire after ten seconds; stale completion handles are rejected. Credentials are cleared after resolution.

The verifier checks validity, expiry, intended server/match and permission using its provider's protocol. Tide supplies no JWT implementation, account database or platform sign-in. Credentials and create parameters are each bounded to 512 bytes. secure_peer must confirm transport protection before credentials are accepted; the standard service permits them only through WebRTC. Identity metadata stays outside world snapshots. Reconnect cookies cannot transfer a different verified account's player slot; the original account can resume without a cookie.

Custom clients call tide_hosting_client_create with the transport, endpoint, match ID (zero to create), fresh nonce and optional credentials/parameters. Copy the generated game's simulation array into the client description's simulation field; incompatible requests are rejected before allocation. Set secure only for a protected transport. Update until accepted, then hand tide_hosting_client_transport and tide_hosting_client_address to an ordinary client/session. Keep updating the hosting client before its session, and destroy the session first. The virtual address distinguishes matches for reconnects.

Hosting APIs run on one host thread, with matches updating sequentially through the shared jobs pool. ready/poll belong to the service loop, not the transport-neutral update function. Match-specific C globals remain the game's responsibility. Native crash containment, restart persistence and dedicated-server host migration are not supplied by this API.

Managed WASM hosting uses the same start callback for scene selection, with project admission and trusted creation handled by its service. Guard server C with #ifdef TIDE_SERVER_BUILD before including game.h. Native process/auth hooks do not run inside managed guests. See Managed hosting for deployment, credentials, lifetime and recovery. The service's optional C save/load callbacks are documented in hosting/README.md in the repository.