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
- func DefaultHostKeyPath() (string, error)
- func WrapHost(app *tui.App, srv *Server, port int) tea.Model
- type Config
- type Server
- func (s *Server) Addr() string
- func (s *Server) AnnounceRestart()
- func (s *Server) HostModel(app *tui.App) tea.Model
- func (s *Server) Relay() *relay.Store
- func (s *Server) ResetFleet(hostClock time.Time) ([]sessiondir.FleetResetEntry, time.Time, error)
- func (s *Server) Serve() error
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) Wait(timeout time.Duration)
Constants ¶
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.
const DefaultPort = 23234
DefaultPort is the SSH listener port when --serve-port isn't given.
Variables ¶
This section is empty.
Functions ¶
func DefaultHostKeyPath ¶
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
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 ¶
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 ¶
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) 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 ¶
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) 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 ¶
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.