Table of Contents

MOSS v0.1 Design Notes

This document explains how Mossworld v0.1 was built: the moving parts, the choices behind them, and a file-by-file tour. It assumes basic experience with PHP, JavaScript, and MySQL.

Executive summary

Mossworld is a browser client (dumb terminal) talking over a persistent WebSocket to a single long-lived PHP daemon that holds the whole world and every player in memory and pushes events out in real time. A classic PHP session login establishes who you are; a one-time ticket in the database hands that identity across to the daemon, which binds your socket to your account and enforces one live session per user. The server owns the world, the client owns the display, durable data lives in MariaDB, and everything is tuned to cost almost nothing when idle.

The big picture: two programs, not one

A normal PHP website is one kind of program: the browser asks for a page, PHP builds it, sends it back, and forgets everything. Each click is a fresh, independent request. That model cannot show you another player moving in real time, because nobody is “always there” holding the shared world in memory.

So Mossworld is split into two cooperating programs:

   Browser (client)                         Server (daemon)
  +------------------+                     +-----------------------+
  |  index.php       |   WebSocket (wss)   |  moss-server.php      |
  |  - terminal UI   | <=================> |  - the live world     |
  |  - send/recv     |   stays OPEN        |  - all players        |
  +------------------+                     +-----------------------+
                                                     |
                                                     v
                                              MariaDB (accounts,
                                              rooms, traffic log)

The clean rule we follow: the server owns the world; the client owns the display. Colours, for example, are 100% a client concern. The colormode command never even reaches the server.

What is a WebSocket?

Normal web traffic is request/response: the browser opens a connection, asks for one thing, gets one answer, and the connection closes. The server can never speak first. That is fine for documents, useless for a live shared world.

A WebSocket is a connection that stays open in both directions. After a brief handshake (which starts life as an ordinary HTTPS request and then “upgrades”), the browser and server hold a persistent two-way pipe. Either side can send a short text message at any moment, with almost no overhead.

This allows push messages:

The secure form is wss:// (WebSocket-over-TLS), the encrypted sibling of https://. Browsers refuse an insecure ws:// connection from an https:// page, so we always use wss://.

What is a "daemon"?

A daemon is just a program that keeps running in the background instead of starting up, doing one job, and exiting. (The name is old Unix tradition; the trailing “d” in httpd, sshd means “daemon”.)

Normal PHP is the opposite of a daemon: it boots, builds one page, and dies, many times per second. That's why a normal PHP site can't “remember” who is online. Nothing stays alive between requests.

Our game server is a PHP program run from the command line that never exits. It loads the world once, opens the WebSocket port, and then sleeps until someone sends a message. Because it stays alive, it can keep the list of players and the map in plain memory.

Two things make this practical and cheap:

We use a library called Workerman to do the low-level socket and event-loop work, so our code only has to answer three questions: a player connected, a player sent a line, a player disconnected.

Player Context

Each connected player needs a little bundle of state: who they are, what room they're in, whether they want tutorial hints, and so on. We put that in a small class called Context (php/Context.php).

The important design idea: the server keeps a list of Contexts, one per connected player. “Multiplayer” is then almost free: to support N players you just keep N Contexts in the list and process each one's commands against the shared world. A command handler is written as process(context, command), so it already works the same whether there is one player or a hundred.

A Context is deliberately tiny right now (id, name, room, account id, a few display flags). It is the natural place to grow later: inventory, a body object, permissions, an input buffer, etc.

The world engine

The actual game logic (what rooms exist, what east does, what you “see”) lives in Engine (php/Engine.php). It is deliberately transport-agnostic: it knows nothing about WebSockets, browsers, or networking. You hand it (context, “east”) and it returns text. That separation means we could drive the exact same engine from a command-line test, an old-style telnet server, or the WebSocket daemon; only the delivery changes.

The proof-of-concept world was a 2×2 square of four rooms. Exits are stored as four columns on each room (n_to, s_to, e_to, w_to), each holding the destination room id (0 = no exit). Dead simple to start; when we want stairs, portals, or locked doors we'll graduate to a proper exits table.

The whole map is loaded into memory once when the server starts, so moving around is a memory lookup; it never touches the database. The database is for durable things (accounts), not for the per-keystroke game loop.

Authentication: bridging the web login to the live game

This is the trickiest seam in the system.

We have two separate worlds of identity:

The bridge is a one-time ticket:

  1. You log in on the web and the PHP session marks you as authenticated.
  2. When the game page (index.php) loads, it checks that session. No session and you are redirected to the sign-in page (“there are no guests”).
  3. If you are logged in, the page mints a random ticket, stores it in a tickets table (your uid + a timestamp), and embeds it in the page.
  4. The browser opens the WebSocket and its very first message is auth <ticket>.
  5. The daemon looks the ticket up in the database, finds your uid and username, and binds this socket to your account. Now it knows who you are.
  6. Until that succeeds, the daemon accepts no other command.

So the database is the shared notebook that lets the web side and the daemon agree on identity without sharing memory.

One session per user. When you authenticate, the daemon kicks any other socket already signed in as you. To stop an infinite “kick each other” loop (the booted client would otherwise auto-reconnect and kick you back), the kick is announced over a control message and the kicked browser is told not to reconnect. Newest login wins; the loser stays put.

Out-of-band "control channel"

Most messages from the server are just text to print. But some are instructions for the UI, not content (ex “your name is Groo” to fill the status bar) or “you were kicked.” We send those as control frames: a message that begins with an invisible ESC byte followed by a tiny JSON object. The client spots the ESC, parses the JSON, acts on it, and never prints it. This keeps UI signals out of the visible text stream and is easy to extend (a future HUD could receive room name, health, etc. the same way).

Bandwidth and CPU choices

The host is a modest metered server, so two policies are baked in:

Security choices

Deployment Protocol

File-by-file tour

Top level

db/ (web-locked; durable data)

php/ (web-locked; shared code)

server/ (web-locked; the daemon)

sec/ (web-served; the login surface)

admin/

bootstrap/ and js/

Immediate improvements

v0.1 is a proof-of-concept / prototype. It's the basis for the whole system; the structural bones of it all. So there is naturally a lot of room for improvement.

Lots of other little things are sure to come out under analysis. But as milestones go, v0.1 is a big one.

“When in doubt, just start coding.”