User Tools

Site Tools


mossworld

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
mossworld [2026/06/26 10:02] appledogmossworld [2026/06/26 15:19] (current) appledog
Line 15: Line 15:
 Brought to you by Neo and Appledog! Brought to you by Neo and Appledog!
  
-* [[https://www.helloneo.ca/mossworld|Play Mossworld now!]] +* [[https://www.mossworld.ca|Play Mossworld now!]]
- +
-== System Design Notes +
-This document explains how Mossworld v0.1 wa built: the moving parts, the choices behind them, and a file-by-file tour. It assumes basic experience with PHP, JavaScript, and MySQL. +
- +
-=== 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**: +
- +
-* **The client** — plain HTML/CSS/JavaScript running in each player's browser. It draws the terminal (a scrolling output area plus a one-line input) and does nothing clever: it sends whatever you type, and prints whatever it receives. +
-* **The server (the "daemon")** — a single long-lived PHP program running on the host. It holds the whole world in memory, knows every connected player, and pushes messages out to them the instant something happens. +
- +
-{{{ +
-   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. +
- +
-For Mossworld this is exactly right: +
- +
-* You press Enter → the client sends one line (e.g. ''east'') down the pipe. +
-* The server moves you, and **pushes** "Appledog leaves east." to everyone else in the room — without them asking. That push is the thing plain HTTP can't do. +
- +
-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: +
- +
-* **Event-driven, not polling.** The daemon does not loop (poll). Tthat would peg a CPU. Instead it //sleeps// inside   the operating system until a real event arrives (a new connection, an incoming message, or a timer). An idle server with players just sitting there uses **near 0% CPU**. +
-* **One process.** A single process comfortably handles thousands of these tiny text connections, because typing is slow and messages are small. +
- +
-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 **web side** is a classic PHP site (login form, ''$_SESSION'', a cookie). This is well-understood and where account signup/login lives. +
-* The **daemon** is a different process entirely. It does **not** share PHP's sessions or cookies — it just has open sockets. So when a browser opens a WebSocket, the daemon has no idea //who// that is. +
- +
-The bridge is a one-time **ticket**: +
-# You log in on the web → a normal PHP session marks you as authenticated. +
-# When the game page (''index.php'') loads, it checks that session. No session → you are redirected to the sign-in page (**there are no guests**). +
-# 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. +
-# The browser opens the WebSocket and **its very first message is** ''auth <ticket>''+
-# 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. +
-# 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. +
- +
-=== The 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: +
-* **CPU.** Single process, event-driven (sleeps when idle), world cached in RAM, and no fast timers. The only periodic job runs every 10 seconds. Result: an idle world costs almost nothing. +
-* **Outbound bandwidth.** On a metered host, //outgoing// bytes cost money while //incoming// is free. So every send goes through one function (''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. +
- +
-= Security choices +
-* **Source code is not web-served.** The folders ''db/'', ''php/'', and ''server/'' each carry a ''.htaccess'' that denies all web access. PHP still ''include''s 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. +
-* **Passwords** are hashed with **Argon2id** (a modern, deliberately-slow hash). We never store or compare plaintext. +
-* **Privileges** use a ''roles'' table (named roles like ''admin'') rather than a single "security level" number — more granular and practical. +
-* **Signup** is protected with a CSRF token; usernames are restricted so the account name is also a clean in-game name. +
-* **The WebSocket ticket** is short-lived and tied to your account, so an open socket can't impersonate someone. +
- +
- +
-=== Deployment Protocol +
-* ''deploy.sh'' pushes the whole tree to the live server in **one rsync** over SSH (fast, delta-only), skipping editor/tooling files. +
-* The daemon runs unprivileged on ''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** — a long-running program holds its code in memory, unlike normal per-request PHP. +
- +
- +
-=== File-by-file tour +
-=== Top level +
-* ''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. +
- +
-=== db/  (web-locked; durable data) +
-* ''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). +
- +
-=== php/  (web-locked; shared code) +
-* ''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. +
- +
-=== server/  (web-locked; the daemon) +
-* ''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. +
- +
-=== sec/  (web-served; the login surface) +
-* ''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/ +
-* ''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. +
- +
-=== bootstrap/ and js/ +
-* Vendored **Bootstrap 5** (CSS/JS) and a couple of jQuery helpers, used only by the web pages. Static assets. +
- +
-=== Known gaps and next steps +
-* No brute-force rate-limiting on login/signup yet. +
-* The WebSocket ticket is reusable within a short TTL (convenient for reconnects; could be hardened to strictly one-time). +
-* Half-open sockets aren't actively reaped yet (a heartbeat/ping would do it). +
-* The world is a 4-room placeholder; the object model (objects, properties, verbs, containment) is the real next chapter. +
-* Legacy files noted above are candidates to remove or adapt. +
- +
-=== 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.+
  
 +== Design Notes
 +* [[moss:Initial Notes for v0.1]]
mossworld.1782468153.txt.gz · Last modified: by appledog

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki