| Both sides previous revisionPrevious revisionNext revision | Previous revision |
| moss:initial_notes_for_v0.1 [2026/06/26 10:06] – appledog | moss:initial_notes_for_v0.1 [2026/06/26 10:21] (current) – appledog |
|---|
| So Mossworld is split into **two cooperating programs**: | 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 client** is a 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. | * **The server (the "daemon")** is 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. |
| |
| {{{ | {{{ |
| }}} | }}} |
| |
| 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. | 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? | == What is a WebSocket? |
| 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. | 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: | This allows push messages: |
| * You press Enter and the client sends one line (e.g. ''east'') down the pipe. | * You press Enter and 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 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 <code>wss://</code> (WebSocket-over-TLS), the encrypted sibling of ''https://''. Browsers refuse an insecure ''ws://'' connection from an ''https://'' page, so we always use ''wss://''. | 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"? | == What is a "daemon"? |
| |
| == The world engine | == 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 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 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. | 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 | == Authentication: bridging the web login to the live game |
| We have two separate worlds of identity: | 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 **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 **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**: | The bridge is a one-time **ticket**: |
| # You log in on the web → a normal PHP session marks you as authenticated. | # You log in on the web and the 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**). | # 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"//). |
| # 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. | # 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 browser opens the WebSocket and **its very first message is** ''auth <ticket>''. |
| |
| == Security choices | == 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. | * **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. | * **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. | * **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. | * **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. | * **The WebSocket ticket** is short-lived and tied to your account, so an open socket can't impersonate someone. |
| == Deployment Protocol | == Deployment Protocol |
| * ''deploy.sh'' pushes the whole tree to the live server in **one rsync** over SSH (fast, delta-only), skipping editor/tooling files. | * ''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. | * 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. | * ''server/moss-ctl.sh'' starts/stops/restarts the daemon. A **restart is required after changing daemon code.** |
| |
| == File-by-file tour | == File-by-file tour |
| === Top level | === 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. | * ''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. | * ''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). | * ''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. | * ''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) | === db/ (web-locked; durable data) |
| * ''mdb.php'' — **MeekroDB**, a small library wrapping MySQL with safe parameterised queries (''DB::query(...)''). Also holds the DB credentials. | * ''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''. | * ''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. | * ''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''). | * ''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. | * ''.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). | * ''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) | === php/ (web-locked; shared code) |
| * ''Engine.php'' — the transport-agnostic **world engine**: loads rooms, renders a room, processes movement/look commands. | * ''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. | * ''Context.php'' -- the per-player **state bundle** described above. |
| * ''globalvars.php'' — site constants (host, app base path) and URL helpers. | * ''globalvars.php'' -- site constants (host, app base path) and URL helpers. |
| * ''cregistry.php'' — cookie helpers (prefixed, Secure, HttpOnly, path-scoped). | * ''cregistry.php'' -- cookie helpers (prefixed, Secure, HttpOnly, path-scoped). |
| * ''ncrypt.php'' — a small random-token generator (utility). | * ''ncrypt.php'' -- a small random-token generator (utility). |
| * ''.htaccess'' — denies all web access. | * ''.htaccess'' -- denies all web access. |
| |
| === server/ (web-locked; the daemon) | === 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-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). | * ''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. | * ''.htaccess'' -- denies all web access. |
| |
| === sec/ (web-served; the login surface) | === sec/ (web-served; the login surface) |
| * ''security.php'' — starts the session and sets ''$user''. Include this and you know who's logged in. | * ''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. | * ''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**. | * ''sign-in.php'' / ''login.php'' -- the login **form** and its **handler**. |
| * ''new-user-form.php'' / ''create-new-user.php'' — the signup **form** and its | * ''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). |
| **handler** (CSRF-checked; on success you're logged in and sent to the game). | * ''logout.php'' -- clears the session and remember-me key. |
| * ''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). |
| * ''bootstrap.php'', ''doing.php'', ''new.php'', ''settings.php'' — legacy nelsonacademy auth scaffolding, unused (kept for future forums/ticketing). | |
| |
| === admin/ | === 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. | * ''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. | * ''design-notes.txt'' -- this document. |
| |
| === bootstrap/ and js/ | === bootstrap/ and js/ |
| * Vendored **Bootstrap 5** (CSS/JS) and a couple of jQuery helpers, used only by the web pages. Static assets. | * Vendored **Bootstrap 5** (CSS/JS) and a couple of jQuery helpers, used only by the web pages. Static assets. |
| |
| === Known gaps and next steps | === Immediate improvements |
| * No brute-force rate-limiting on login/signup yet. | 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. |
| | * No rate-limiting on login/signup or on game commands. |
| | * No filters on chat. |
| * The WebSocket ticket is reusable within a short TTL (convenient for reconnects; could be hardened to strictly one-time). | * 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). | * 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. | Lots of other little things are sure to come out under analysis. But as milestones go, v0.1 is a big one. |
| | |
| | <blockquote>"When in doubt, just start coding."</blockquote> |