Host, Join, and Solo in One Build

← Networking Overview | Game Flow | Connection Flow | Home

Status: Design agreed (2026-10); see DecisionsDecisions. Nothing here is implemented yet.


Goal

Any released game executable can be played alone, can host a game other players join, and can join a game someone else hosts — chosen at run time, from the game's own flow: a menu comes up first, in a world of its own, and choosing Host or Join turns that into a session. One build carries everything; nothing is a separate executable.

The first test is deliberately small: direct IP on a local network, two machines or two processes on one.

Not in this design: host migration and per-entity authority transfer (the rest of the ListenSim design in Overview, which this is the first stage of), NAT traversal, session discovery, and typing an address into the game (GG-UI text fields exist; the screen itself does not).


Where It Starts From

PieceToday
RoleFixed when the engine is built (TNX_NET_MODEL): Server (Authority only, forced headless), Client (Owner only), or PIE (editor). A windowed game build is Client
ConnectingOutside the editor's play-in-editor and Testbed tests, nothing calls NetConnectionManager::Listen or Connect. --client <ip> and --port fill EngineConfig fields nothing reads
Playing aloneA Client build has no Authority and no Souls (Known Issue 6): it cannot spawn a player without a remote Authority
What the role is, in a worldA type: World<TNet, …> holds a LogicThread<TNet, …> whose net policy (SoloSim, AuthoritySim, OwnerSim) is a member. The policy is called at two points of a fixed step — input in, frame published — and at world setup (an Owner's input ring; an Authority's binding to replication). Nothing else in a world depends on it
A local player on the AuthorityAlready modelled: owner 0's Soul is given both Authority and Owner roles (FlowManagerBase::OnClientLoaded), and WorldBase::GetSimInput(0) is the window's input. It has only ever run inside the editor
Both sides in one processPIENetThread drives one AuthorityNet and any number of OwnerNet handlers over one connection manager, routing each message by which side its connection is on. Editor builds only
Emptying a worldWorldBase::ResetRegistry wipes entities, handles, and caches in place. Tests use it; no game path does
Replacing a worldA game build creates one world at startup and keeps it. The renderer is given that world's registry and logic thread once. Only the editor shows other worlds, through its own viewports

The Model

One world, whose role changes

The world a game build starts with is the world it keeps. What changes at run time is what that world is doing:

RoleThe world's net policy acts asNet sessionThe local player
SoloAuthorityNoneOwner 0
HostAuthorityListeningOwner 0; each Owner that joins gets 1–255
Owner (joined)OwnerConnected to a Host or a dedicated AuthorityThe OwnerID the Authority assigns

Solo is a Host nobody can reach. The same Authority path, the same GameMode path, the same Soul for owner 0, and no listening socket. So going from the menu to hosting is not a change of world at all: the session starts listening and the level changes. A solo game can be opened to others the same way. It also makes Known Issue 6 go away rather than be worked around: there is one way a local player comes to exist.

Joining is the one change of kind, Authority to Owner, and it is a change of policy on the same world, after the world has been emptied.

What has to become adjustable, and what does not

At run timeWhy
The world's roleYesA net policy for game builds (ListenSim, the name already reserved for it) that holds both the Authority's and the Owner's and uses the one the role says. The cost is one branch at each of the two points a step calls its policy. The typed policies stay as they are for the stripped builds and the editor
The net sessionYesListen, connect, close — owned by the flow manager, so a state that needs one gets one and a state that does not loses it
The world's contentsYesUnloading the level and emptying the world in place: entities, Constructs of World and Level lifetime, physics bodies, replication channels, input logs, rollback history, and the frame counter an Owner aligns to its Authority's. Built on ResetRegistry, which has to grow from a test helper into something a game can rely on
Rates (net update, input send)YesAlready read from the config where they are used
Capacities (entity and body counts, history length)NoSlabs are sized and committed when the world is created (Known Issue 33). A menu and a session share the project's sizes. A game that wants a small menu world and a large session world needs a new world; that is the Reset World travel lever, and it is not needed here
Rollback, determinism, fixed stepNoCompile-time, as today

Replacing the world — destroying it and creating another, with the renderer and the engine re-pointed — is what Game Flow calls Reset World. It remains worth building, for games whose worlds differ in size and as the strongest guarantee that nothing carries over. It is not on the path to hosting and joining, and this design does not depend on it.

Starting and ending a session

A game's GameplayState is the same class whether it is hosting, joined, or solo. So the role is given when a session is started, not baked into the state:

// From a menu state, in response to a button.
flow.StartSession({ .State = "Gameplay", .Role = SessionRole::Solo });
flow.StartSession({ .State = "Gameplay", .Role = SessionRole::Host, .Port = 27015 });
flow.StartSession({ .State = "Gameplay", .Role = SessionRole::Owner, .Address = "192.168.1.20", .Port = 27015 });

// From anywhere: leave, and go back to a state that says what it needs.
flow.EndSession("MainMenu");

StartSession is a transition: the current state exits and its level is unloaded; the world is emptied; its role is set; the net session is brought up (listen or connect); the state is entered. EndSession closes the net session, empties the world, sets it back to Solo, and enters the named state. A session that fails — the port is taken, nobody answers — ends back in the state it was started from with a reason the state can show.

The menu is an ordinary state with a Solo world: a level behind it if it wants one, and a GG-UI document in front.

What changes underneath

PieceChange
Build modelA new TNX_NET_MODEL=Game: both net handlers compiled, the Authority side not tied to headless, ListenSim as the world's policy. It becomes the model of windowed, non-editor builds. Server (headless, Authority only) and Client (Owner only) remain for stripped binaries; PIE is unchanged
Net policyListenSim: both policies and a role. Owner setup (the input ring) is done once for a world that may become one
Net threadOne net thread for game builds that owns the connection manager and uses AuthorityNet or OwnerNet as the role says. It is PIENetThread made general
Net sessionA real thing the flow manager owns. NeedsNetSession is enforced like NeedsWorld
Emptying a worldA supported operation, run between steps with the logic thread held, that leaves a world as it was when created
Role-specific code behind #ifThe few places that compile one side out (GameplayState's local-player spawn, FlowManager::LoadLevel's level-ready acknowledgement) ask the world's role instead
Command line--host, --join <address>, --port start a session directly, without the menu: for tests and for scripts

Build Order

StepWorkTest
1The Game build model, ListenSim, and the game net thread. The role comes from the command line and is fixed for the runA windowed Host with a local player; a second process joins it over loopback and both see both players; Solo spawns its player with no net session
2Emptying a world in place; changing its role between sessionsSolo → empty → Host → empty → Owner → empty, many times over, each session behaving as a freshly started process does; no growth in memory, handles, or bodies
3StartSession / EndSession; the net session owned by the flow manager; failure returns to the calling stateHosting on a taken port and joining an address nobody answers both end in the menu with the reason
4The resilience demo: a menu state and a GG-UI menu (Solo, Host, Join the configured address), and leaving a session back to the menuTwo machines on a local network

Known Limits Carried In

These exist today and a Host inherits them; none is made worse:

  • One Owner far behind stalls the Authority's sim for everyone (Known Issue 17).
  • With rollback compiled out there is no reconciliation of the controlled entity (Known Issue 16). The demo build has rollback on.
  • The Host's own player is not predicted: it is the Authority. It has no latency to hide.
  • If the Host leaves, the session ends for everyone. Migration is the later ListenSim work.
  • No interest management: every Owner is sent every entity.

Decisions

Decided 2026-10: each as proposed.

#QuestionDecision
1Solo is a Host that is not listening: the Authority path for bothYes
2One world whose role is set at run time, through a ListenSim policy for game builds, rather than a world replaced by one of another typeYes: it removes the largest unknown (no game build has ever replaced its world) and makes menu → Host no change of world at all
3Joining and leaving empty the world in place instead of destroying itYes, with the test in step 2 as the proof that nothing carries over. Replacing the world stays available later as Reset World
4The role is given when a session starts (StartSession({ State, Role, Address, Port }), EndSession(state)), not declared by the state class or fixed by the buildYes
5A third build model, Game, for windowed non-editor builds, with Server and Client kept as stripped buildsYes
6One general net thread for game builds, which the editor's play-in-editor thread later becomes a use ofYes, with the editor moved onto it after game builds work, not at the same time

Open Questions

  • Everything a world holds that emptying must reach. The list above is from reading; step 2 starts by finding the rest (anything keyed by frame number, the streaming manager's requests in flight, audio voices).
  • Whether Souls survive from the menu into a session. Game Flow says a Soul is session-scoped; the menu's owner 0 and the session's are then different Souls, and anything to carry across belongs in a Persistent Construct.
  • How an Owner learns why it could not join (full, wrong version) beyond "nobody answered".