serve

package
v0.36.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package serve hosts the embedded SSH front door for multiplayer sessions (ADR 0034, v0.27). `--serve` starts a wish listener next to the host's in-process game. S1 gave every connection a fresh ephemeral World; S3 adds identity and persistence: unknown keys go through the invite-code enroll flow, enrolled keys resume their per-player world from the session directory, and every guest's autosaves write back there — never into the host's local saves.

Index

Constants

View Source
const DefaultIdleTimeout = 10 * time.Minute

DefaultIdleTimeout bounds how long a session may go with no traffic in either direction before the connection is dropped (#243).

Without it a connection never dies on its own. A guest's game runs server-side, driven by its own tick loop, so a client whose machine sleeps leaves behind a half-open TCP that nothing reaps: the session keeps running and warping unattended, presence keeps counting it online, and — because a key may hold only one live session — its owner is locked out of their own program behind a socket nobody is using.

The deadline covers reads and writes both, which is what makes it bite on an absent peer rather than merely a quiet one: frames the server renders back up in the send buffer, the blocked write passes the deadline, and the connection is torn down through persistMiddleware like any other disconnect — payload written, slot freed.

Generous on purpose. The cost of firing early is small (progress is persisted and a reconnect resumes) but not nothing: a player who pauses and walks away renders no new frames, so a short timeout would disconnect someone sitting right there. Ten minutes is far longer than anyone stares at a paused screen and far shorter than a laptop lid stays shut.

View Source
const DefaultPort = 23234

DefaultPort is the SSH listener port when --serve-port isn't given.

Variables

This section is empty.

Functions

func DefaultHostKeyPath

func DefaultHostKeyPath() (string, error)

DefaultHostKeyPath returns the per-host SSH identity path, sibling to the save state: $XDG_STATE_HOME/terminal-space-program/ ssh_host_ed25519_key, falling back to ~/.local/state (same resolution as save.DefaultPath).

func WrapHost added in v0.28.0

func WrapHost(app *tui.App, srv *Server, port int) tea.Model

WrapHost always wraps app in the reporting model (v0.28 S3): the wrapper is now present in solo play too. A non-nil srv (the --serve headless path) reports immediately as the host; a nil srv stays inert until [h] on the Session screen lazily binds a listener on port. Value-receiver models: main reads back the final model's HostServer() to shut a lazily started listener down at exit.

Types

type Config

type Config struct {
	Addr        string
	HostKeyPath string
	SessionDir  string
	IdleTimeout time.Duration
}

Config shapes a Server. Addr is a listen address ("[host]:port"; use port 0 to let the OS pick — Addr() reports the bound address). HostKeyPath locates the server's ed25519 identity; a missing key is generated there on first start. SessionDir is the session store (roster, invites, per-player payloads); empty means the XDG default. IdleTimeout overrides DefaultIdleTimeout; zero takes the default.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a running (or startable) SSH listener whose sessions each run their own game. The listener is bound in New so port conflicts surface before the host's own TUI takes the screen.

func New

func New(cfg Config) (*Server, error)

New binds cfg.Addr, opens the session store (auto-enrolling the host as roster entry #1 on first serve), and prepares the wish server. The host key is created at cfg.HostKeyPath if absent. Serve must be called to start accepting sessions.

func (*Server) Addr

func (s *Server) Addr() string

Addr reports the bound listen address (useful with ":0").

func (*Server) AnnounceRestart added in v0.33.0

func (s *Server) AnnounceRestart()

AnnounceRestart broadcasts the server-restart moment to every session. It is addressed at nobody in particular — everyone connected is about to be dropped, and everyone's progress is about to be persisted.

func (*Server) HostModel

func (s *Server) HostModel(app *tui.App) tea.Model

HostModel wraps the host's own in-process game so the host's craft enter the store like any guest's (the host is roster entry #1, not a special case on the wire). main runs the returned model.

func (*Server) Relay

func (s *Server) Relay() *relay.Store

Relay exposes the session store (tests and later slices read it).

func (*Server) ResetFleet added in v0.32.2

func (s *Server) ResetFleet(hostClock time.Time) ([]sessiondir.FleetResetEntry, time.Time, error)

ResetFleet performs the startup fleet reset behind --reset-fleet: every enrolled player's slate is wiped to one default vessel on the shared 500x500 km ring (sessiondir.ResetFleet) and every subspace clock is aligned to a single epoch, so nobody needs a Sync after the reset.

The epoch is the server's current sim time: the frontier across all persisted payloads, floored by the host's in-process clock — clocks only ever move forward, matching the join-at-the-frontier rule (ADR 0034). The caller must set the host's in-process world clock to the returned epoch (the host's world never lives in the session store) and should run this before accepting connections.

One-shot by construction: nothing about the reset is persisted as pending state, so a restart without the flag changes nothing.

func (*Server) Serve

func (s *Server) Serve() error

Serve accepts sessions until Shutdown; it blocks. A graceful shutdown returns nil. The version-surface poll runs for the listener's life (v0.30 S5) — started here, not in New, so it is off the wire in unit tests that never serve.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops the listener and ends every live session.

func (*Server) Wait

func (s *Server) Wait(timeout time.Duration)

Wait blocks until every session handler (and its final payload persist) has finished, or the timeout passes. Call after Shutdown — force-closed connections still unwind through persistMiddleware, and the process must not exit under them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL