Horizons has no client application. Every screen in the game — a bridge console,
the Main Viewer, the Game Master station, the editor — is a web page served by the
game server and drawn in a browser using WebGL. The server runs the simulation and
holds all of the state; the browser draws and sends input.
The two connections
Every console makes two, and both must work:
- HTTP on port 1864 delivers the page and its assets. Port 1866 is the
TLS equivalent.
- A WebSocket on port 1865 carries the live game. Port 1867 is the TLS
equivalent.
A page that loads but never comes alive has the first and not the second. That is
worth knowing before you debug anything else — see
Hosting Over The Internet.
Screens
A screen is a registered page. Screens are declared in XML, one
registration per station:
<screen name="Flight" icon="roles/badge-flight.webp" base="console.htm"
url="flight" file="flight.htm" role="Flight"
mission-summary="true" packets="" tags="role-primary,flight" />
| Attribute | Means |
name | The display title. This is what the GM console lists. |
url | The address slug. Not the same as the name, which catches people out. |
base | Which base page wraps it. |
file | The screen content itself. |
role | Which vessel role this station binds as. |
packets | Server packets this screen wants from the outset. |
visible | Whether it appears on the station selection page. Invisible screens are still reachable at their address. |
assign-vessel | Whether opening it binds to a vessel. False for admin stations. |
Screen registrations are contributed by modules and merged after the base game
loads, so a module can add stations or replace existing ones. See Modules.
Base pages
A console page is a base page with the screen's content injected into it. There are
four bases, and the choice matters:
console.htm — a player console. Chrome, alert bar, role indicators,
and the waiting / offline / spawning states.
console-admin.htm — an admin station.
console-child.htm — a page hosted inside another, used by the editor
for its document tabs.
dialog.htm — a small standalone panel.
The base page loads hydra.js (the whole client runtime, as a classic
script exposing globals) and hydra-module.js (the ES module entry point,
which brings in the widget controller and the three.js import map). The screen's own
content is placed where the base says [Screen.Content].
The .msp extension
Pages are requested with a .msp suffix — flight.msp,
gamemaster.msp. The server strips it, looks the screen up by its
url slug, renders the base page around the screen file and substitutes
tokens. A request without .msp gets a static file, not a rendered screen.
Server-side tokens
Bracketed tokens in a page are replaced by the server before it is sent:
| Token | Value |
[Screen.Title], [Screen.Name], [Screen.Url] | This screen's identity |
[Screen.Content] | The screen file, injected into the base |
[Screen.Roles], [Screen.MissionSummary] | Screen configuration, as metadata |
[Server.LocalVersion], [Server.Version] | Build version |
[Server.IP], [Server.Port] | Where this server is |
[Game.AvailableScreens], [Game.ActiveScreens] | Station lists |
[Vessel.Alert], [Vessel.Name], [Vessel.ID] | The bound vessel |
Two of these do more than they look:
[Server.LocalVersion] is the cache‑buster. Every
script and stylesheet is requested as panel.css?[Server.LocalVersion]. It
changes when the build version changes — which means editing a stylesheet does
not invalidate it. Already‑running clients keep the old sheet until they are
reloaded. Freshly requested .msp pages are always current.
[Vessel.Alert] selects a stylesheet. The page links
alert[Vessel.Alert].css, so the alert condition physically swaps a
stylesheet rather than toggling classes. That is how a whole console restyles itself at
Red.
Console states
A player console is always in one of several states, each a panel in the base page
that is shown or hidden: offline (no connection),
waiting (connected, no vessel assigned), spawning,
and online. The waiting state is the one players report as "my console
does nothing" — it means no vessel, which is a Game Master matter, not a client one.
What the client holds
hydra.js maintains a set of globals that page code and widgets read
directly:
game — the session: players, missions, variables, events, and the
broadcast bus.
thisvessel — the vessel this console is bound to. Almost everything a
widget renders comes from here.
workbench — the page itself, including
workbench.Widgets, the widget controller.
socket — the live connection.
These are globals on purpose: widgets are loaded dynamically and independently, and
this is the shared surface they agree on. See Widgets and
Packets And Topics.
Layout and 3D
Consoles are laid out with flexbox and a set of panel classes
(panel-area, panel-container, panel-header,
panel-full) rather than absolute positioning, so a console adapts to
whatever screen it is on.
The 3D views — viewport, main viewer, radar — are three.js scenes drawn into
canvases, loaded through an import map so widgets can import
"three" directly. Several of them can be on one page at once: a role console
typically has a viewport, a 3D radar, and two smaller relative radars, each with its
own render loop.
Because a console page can hold nearly a dozen independent animation loops, the
useful measurement is milliseconds of JavaScript per frame, attributed to the
loop that spent it — not frame rate. Frame rate is capped by the display's
refresh and hides all available headroom, so a console can look perfectly smooth while
one widget is using most of the budget. Radar widgets have historically been the
surprise.