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.
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.
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.
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:
east) down the pipe.
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://.
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.
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 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.
This is the trickiest seam in the system.
We have two separate worlds of identity:
$_SESSION, a cookie). This is well-understood and where account signup/login lives.The bridge is a one-time ticket:
index.php) loads, it checks that session. No session and you are redirected to the sign-in page (“there are no guests”).tickets table (your uid + a timestamp), and embeds it in the page.auth <ticket>.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.
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).
The host is a modest metered server, so two policies are baked in:
tx()) that tallies the byte count by category (look, say, presence, system, …). A 10-second timer flushes those tallies into a traffic table in one batched insert. We get a full picture of where bandwidth goes, without putting a database write in the hot path. The in-game stats command shows live totals.db/, php/, and server/ each carry a .htaccess that denies all web access. PHP still includes those files from disk (that's a filesystem read, unaffected), but a browser asking for them directly gets 403 so the DB password, the engine, etc. are never downloadable. sec/ and admin/ are web-served, because they are real pages.roles table (named roles like admin) rather than a single “security level” number. More granular and practical.deploy.sh pushes the whole tree to the live server in one rsync over SSH (fast, delta-only), skipping editor/tooling files.127.0.0.1:2346 speaking plain ws://. Apache sits in front, terminates TLS using the real certificate, and proxies wss://helloneo.ca/mossworld/ws through to the local daemon. This way the daemon needs no privileges and never touches the private key.server/moss-ctl.sh starts/stops/restarts the daemon. A restart is required after changing daemon code.index.php – the game client. Top half is PHP: require login, mint the WebSocket ticket. Bottom half is the browser terminal (scrolling output + input box), the WebSocket connect/reconnect logic, the colour profiles, and the control-frame handling. This is the only game file a browser loads directly.deploy.sh – one-pass rsync deploy to the live host.composer.json – declares the Workerman dependency (installed into vendor/ on the server, which is not committed).movetest.php – a tiny command-line harness that drives the Engine with no network, to prove movement logic in isolation.mdb.php – MeekroDB, a small library wrapping MySQL with safe parameterised queries (DB::query(…)). Also holds the DB credentials.schema.php – every table definition as a create_table_*() function: rooms, traffic, members (accounts), roles, tickets.install.php – command-line installer: (re)creates/seeds the tables.User.php – the account class: load by id, check username/email, verify Argon2id password, remember-me key, and the role methods (has_role, add_role, is_admin)..htaccess – denies all web access to this folder.registry.php, iplog.php, roles.php are legacy helpers carried over from an earlier project, currently unused (kept as reference).Engine.php – the transport-agnostic world engine: loads rooms, renders a room, processes movement/look commands.Context.php – the per-player state bundle described above.globalvars.php – site constants (host, app base path) and URL helpers.cregistry.php – cookie helpers (prefixed, Secure, HttpOnly, path-scoped).ncrypt.php – a small random-token generator (utility)..htaccess – denies all web access.moss-server.php – the WebSocket daemon. Holds the player list, the tx() metering helper, the control/assist/broadcast helpers, the auth gate and ticket validation, the one-session-per-user kick, and the connect / message / disconnect handlers. This is the live heart of the game.moss-ctl.sh – start / stop / restart / status wrapper (with a force-kill fallback so a stuck server can always be replaced)..htaccess – denies all web access.security.php – starts the session and sets $user. Include this and you know who's logged in.chrome.php – a slim Bootstrap-5 (dark) page header/nav/footer used only by the web pages (login, admin). The game does not use Bootstrap.sign-in.php / login.php – the login form and its handler.new-user-form.php / create-new-user.php – the signup form and its handler (CSRF-checked; on success you're logged in and sent to the game).logout.php – clears the session and remember-me key.bootstrap.php, doing.php, new.php, settings.php – legacy nelsonacademy auth scaffolding, unused (kept for future forums/ticketing).admin.php – admin panel stub: shows who you're logged in as and whether you hold the admin role. Real tools will grow here.design-notes.txt – this document.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.”